Advertisement
Guest User

Untitled

a guest
Dec 8th, 2012
385
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.00 KB | None | 0 0
  1. ########################################
  2. # Sky.fm XBMC plugin
  3. # by Tim C. 'Bitcrusher' Steinmetz
  4. # http://qualisoft.dk
  5. # Github: https://github.com/Bitcrusher/Sky-FM-XBMC-plugin
  6. # Git Read-only: git://github.com/Bitcrusher/Sky-FM-XBMC-plugin.git
  7. #
  8. # This Program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2, or (at your option)
  11. # any later version.
  12. #
  13. # This Program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with XBMC; see the file COPYING. If not, write to
  20. # the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
  21. # http://www.gnu.org/copyleft/gpl.html
  22. #
  23.  
  24. import os
  25. import sys
  26. import re
  27. import urllib
  28. import urllib2
  29. import string
  30. import xbmc
  31. import xbmcgui
  32. import xbmcplugin
  33. import xbmcaddon
  34. import pickle
  35. import time
  36. from xml.dom import minidom
  37. from httpcomm import HTTPComm
  38.  
  39. # Import JSON - compatible with Python<v2.6
  40. try:
  41. import json
  42. except ImportError:
  43. import simplejson as json
  44.  
  45. # Various vars used throughout the script
  46. HANDLE = int(sys.argv[1])
  47. ADDON = xbmcaddon.Addon(id='plugin.audio.sky.fm')
  48.  
  49. # Plugin constants
  50. __plugin__ = ADDON.getAddonInfo('name')
  51. __author__ = "Tim C. Steinmetz"
  52. __url__ = "http://qualisoft.dk/"
  53. __platform__ = "xbmc media center, [LINUX, OS X, WIN32]"
  54. __date__ = "30. October 2012"
  55. __version__ = ADDON.getAddonInfo('version')
  56.  
  57. class musicAddonXbmc:
  58. _addonProfilePath = xbmc.translatePath( ADDON.getAddonInfo('profile') ).decode('utf-8') # Dir where plugin settings and cache will be stored
  59.  
  60. _cacheStreams = _addonProfilePath + "cache_streamlist.dat"
  61. _cacheListenkey = _addonProfilePath + "cache_listenkey.dat"
  62. _checkinFile = _addonProfilePath + "cache_lastcheckin.dat"
  63.  
  64. _baseUrl = "http://www.sky.fm"
  65. _loginUrl = "http://www.sky.fm/login"
  66. _listenkeyUrl = "http://www.sky.fm/member/listen_key"
  67.  
  68. _publicStreamsJson40k = "http://listen.sky.fm/appleapp_low" # Public AAC 40k/sec AAC+ JSON url
  69. _premiumStreamsJson40k = "http://listen.sky.fm/appleapp_premium_low" # AAC 40k/sec AAC+ JSON url
  70. _premiumStreamsJson64k = "http://listen.sky.fm/appleapp_premium_medium" # AAC 64k/sec AAC+ JSON url
  71. _premiumStreamsJson128k = "http://listen.sky.fm/appleapp_premium" # AAC 128k/sec AAC+ JSON url
  72. _favoritesStreamJson40k = "http://listen.sky.fm/premium_low/favorites.pls" # Favorites AAC 40k/sec AAC+ playlist url
  73. _favoritesStreamJson64k = "http://listen.sky.fm/premium_medium/favorites.pls" # Favorites AAC 64k/sec AAC+ playlist url
  74. _favoritesStreamJson128k= "http://listen.sky.fm/premium/favorites.pls" # Favorites AAC 128k/sec AAC+ playlist url
  75.  
  76. _httpComm = HTTPComm() # Init CURL thingy
  77. _frontpageHtml = ""
  78.  
  79. _newChannels = 0
  80. _bitrate = 40
  81.  
  82. def __init__( self ) :
  83. # If stats is allowed and its been at least 24 hours since last checkin
  84. if (ADDON.getSetting('allowstats') == "true") and (self.checkFileTime(self._checkinFile, self._addonProfilePath, 86400) == True) :
  85. open(self._checkinFile, "w")
  86.  
  87. account = 'public'
  88. if ADDON.getSetting('username') != "" :
  89. account = 'premium'
  90.  
  91. xbmc.log( 'Submitting stats', xbmc.LOGNOTICE )
  92. self._httpComm.get('http://stats.qualisoft.dk/?plugin=sky&version=' + __version__ + '&account=' + account + '&key=5a2a72084f40402656555b665a0fdcddbbd87553')
  93.  
  94. xbmc.log( "[PLUGIN] %s v%s (%s)" % ( __plugin__, __version__, __date__ ), xbmc.LOGNOTICE )
  95.  
  96.  
  97. # Let's get some tunes!
  98. def start( self ) :
  99. jsonList = [] # list that data from the JSON will be put in
  100. streamList = [] # the final list of channels, with small custom additions
  101.  
  102. # Check if cachefile has expired
  103. if ADDON.getSetting("forceupdate") == "true" or ((int( ADDON.getSetting("cacheexpire") ) * 60) != 0 and self.checkFileTime(self._cacheStreams, self._addonProfilePath, (int( ADDON.getSetting("cacheexpire") ) * 60)) == True) :
  104. listenkey = "" # will contain the premium listenkey
  105.  
  106. if ADDON.getSetting('username') != "" and ADDON.getSetting("usefavorites") == 'false' : # if username is set and not using favorites
  107. xbmc.log( "Going for Premium streams", xbmc.LOGNOTICE )
  108.  
  109. # Checks if forceupdate is set and if the listenkey cachefile exists
  110. if ADDON.getSetting("forceupdate") == "true" or not os.path.exists(self._cacheListenkey) :
  111. listenkey = self.getListenkey()
  112. pickle.dump( listenkey, open(self._cacheListenkey, "w"), protocol=0 ) # saves listenkey for further use
  113. else :
  114. listenkey = pickle.load( open(self._cacheListenkey, "r") )
  115.  
  116. if ADDON.getSetting('bitrate') == '0' :
  117. self._bitrate = 40
  118. jsonList = self.getJSONChannelList( self._premiumStreamsJson40k )
  119. streamList = self.customizeStreamListAddMenuitem( jsonList, listenkey )
  120. elif ADDON.getSetting('bitrate') == '1' :
  121. self._bitrate = 64
  122. jsonList = self.getJSONChannelList( self._premiumStreamsJson64k )
  123. streamList = self.customizeStreamListAddMenuitem( jsonList, listenkey )
  124. else :
  125. self._bitrate = 128
  126. jsonList = self.getJSONChannelList( self._premiumStreamsJson128k )
  127. streamList = self.customizeStreamListAddMenuitem( jsonList, listenkey )
  128.  
  129. xbmc.log( "Bitrate set to " + str( self._bitrate ), xbmc.LOGNOTICE )
  130.  
  131.  
  132. elif ADDON.getSetting('username') != "" and ADDON.getSetting("usefavorites") == 'true' : # if username is set and wants to use favorites
  133. xbmc.log( "Going for Premium favorite streams", xbmc.LOGNOTICE )
  134. listenkey = self.getListenkey()
  135. if ADDON.getSetting('bitrate') == '0' :
  136. self._bitrate = 40
  137. streamList = self.getFavoriteStreamsList( self._favoritesStreamJson40k + "?" + listenkey )
  138. elif ADDON.getSetting('bitrate') == '1' :
  139. self._bitrate = 64
  140. streamList = self.getFavoriteStreamsList( self._favoritesStreamJson64k + "?" + listenkey )
  141. else :
  142. self._bitrate = 128
  143. streamList = self.getFavoriteStreamsList( self._favoritesStreamJson128k + "?" + listenkey )
  144.  
  145. xbmc.log( "Bitrate set to " + str(self._bitrate), xbmc.LOGNOTICE )
  146.  
  147. for channel in streamList :
  148. self.addItem( channel['name'], channel['playlist'], channel["description"], channel['bitrate'], self._addonProfilePath + "art_" + channel['key'] + ".png", channel['isNew'] )
  149.  
  150. else :
  151. xbmc.log( "Going for Public streams", xbmc.LOGNOTICE )
  152. jsonList = self.getJSONChannelList( self._publicStreamsJson40k )
  153. streamList = self.customizeStreamListAddMenuitem(jsonList, "") # sending a blank string as listenkey
  154.  
  155. # save streams to cachefile
  156. pickle.dump( streamList, open(self._cacheStreams, "w"), protocol=0 )
  157.  
  158. if (self._newChannels > 0) : # Yay! New channels found
  159. xbmc.log( ADDON.getLocalizedString(30130) + " " + ADDON.getLocalizedString(30131) + str(self._newChannels) + ADDON.getLocalizedString(30132) + " " + ADDON.getLocalizedString(30133) + " " + ADDON.getLocalizedString(30134), xbmc.LOGNOTICE )
  160. xbmcgui.Dialog().ok( ADDON.getLocalizedString(30130), ADDON.getLocalizedString(30131) + str(self._newChannels) + ADDON.getLocalizedString(30132), ADDON.getLocalizedString(30133),ADDON.getLocalizedString(30134) )
  161.  
  162. else :
  163. xbmc.log( "Using cached streams", xbmc.LOGNOTICE )
  164. streamList = pickle.load( open(self._cacheStreams, "r") )
  165.  
  166. # Add streams to GUI
  167. for channel in streamList :
  168. self.addItem( channel['name'], channel['playlist'], channel["description"], channel['bitrate'], self._addonProfilePath + "art_" + channel['key'] + ".png", channel['isNew'] )
  169.  
  170. # If streams should be sorted A-Z
  171. if ADDON.getSetting('sortaz') == "true" :
  172. xbmcplugin.addSortMethod( HANDLE, sortMethod=xbmcplugin.SORT_METHOD_LABEL )
  173.  
  174. # End of channel list
  175. xbmcplugin.endOfDirectory( HANDLE, succeeded=True )
  176.  
  177. # Resets the 'Force refresh' setting
  178. ADDON.setSetting( id="forceupdate", value="false" )
  179.  
  180. return True
  181.  
  182.  
  183. """return list - False if it fails
  184. Gets the favorites playlist and returns the streams as a list
  185. Also every channel is added to the GUI from here, as the progress indication
  186. in the GUI would not reflect that something is actually happening till the very end
  187. """
  188. def customizeStreamListAddMenuitem( self, list, listenkey ) :
  189. # Precompiling regexes
  190. streamurl_re = re.compile('File\d+=([^\n]*)', re.I) # streams in .pls file
  191.  
  192. streamList = []
  193.  
  194. # Will add list elements to a new list, with a few additions
  195. for channel in list :
  196. channel['key'] = self.makeChannelIconname( channel['name'] ) # customize the key that is used to find channelart
  197. channel['isNew'] = False # is used to highlight when it's a new channel
  198. channelArt = "art_" + channel['key'] + ".png"
  199. channel['bitrate'] = self._bitrate
  200.  
  201. if ADDON.getSetting('username') != "" : # append listenkey to playlist url if username is set
  202. channel['playlist'] = self.getFirstStream( channel['playlist'] + "?" + listenkey, streamurl_re )
  203. else :
  204. channel['playlist'] = self.getFirstStream( channel['playlist'], streamurl_re )
  205. if (not "de Paris" in channel['name']) :
  206. if (not os.path.isfile(self._addonProfilePath + channelArt)) : # if channelart is not in cache
  207. print "Channelart for " + channel['name'] + " not found in cache at " + self._addonProfilePath + channelArt
  208. self.getChannelArt( channel['id'], "art_" + channel['key'] )
  209. channel['isNew'] = True
  210. self._newChannels = self._newChannels + 1
  211. streamList.append( channel )
  212.  
  213. # I'd have prefeered it if I didn't have to add menuitem from within this method
  214. # but I have to, too give the user some visual feedback that stuff is happening
  215. self.addItem( channel['name'], channel['playlist'], channel["description"], self._bitrate, self._addonProfilePath + "art_" + channel['key'] + ".png", channel['isNew'] )
  216.  
  217. return streamList # returns the channellist so it can be saved to cache
  218.  
  219.  
  220. """return bool
  221. Will check if channelart/icon is present in cache - if not, try to download
  222. """
  223. def getChannelArt( self, channelId, channelKey ) :
  224. channelArt_re = re.compile('data-id="' + str(channelId) +'">(?:[\n\s]*)<a(?:[^>]*)><img(?:[^>]*)src="([^"]*)"', re.I)
  225.  
  226. try :
  227. if (self._frontpageHtml == "") : # If frontpage html has not already been downloaded, do it
  228. self._frontpageHtml = self._httpComm.get( self._baseUrl )
  229.  
  230. channelartDict = channelArt_re.findall( self._frontpageHtml )
  231.  
  232. # Will download and save the channelart to the cache
  233. self._httpComm.getImage( channelartDict[0], self._addonProfilePath + channelKey + ".png" )
  234.  
  235. return True
  236.  
  237. except Exception :
  238. sys.exc_clear() # Clears all exceptions so the script will continue to run
  239. xbmcgui.Dialog().ok( ADDON.getLocalizedString(30160), ADDON.getLocalizedString(30161), ADDON.getLocalizedString(30162) + channelartDict[0] )
  240. xbmc.log( ADDON.getLocalizedString(30160) + " " + ADDON.getLocalizedString(30161) + channelKey + " " + ADDON.getLocalizedString(30162)+ channelartDict[0], xbmc.LOGERROR )
  241. return False
  242.  
  243. return True
  244.  
  245.  
  246. """return String
  247. Extracts the premium listenkey from the frontpage html
  248. """
  249. def getListenkey( self ) :
  250. listenkey_re = re.compile('Key is:<br />[^<]*<strong>([\w\d]*)<', re.DOTALL)
  251.  
  252. try :
  253. logindata = urllib.urlencode({ 'member_session[username]': ADDON.getSetting('username'),
  254. 'member_session[password]': ADDON.getSetting('password') })
  255.  
  256. self._httpComm.post( self._loginUrl, logindata ) # logs in so the listenkey page is accessible
  257.  
  258. listenkeyHtml = self._httpComm.get( self._listenkeyUrl)
  259. listenkeyDict = listenkey_re.findall( listenkeyHtml )
  260.  
  261. xbmc.log( "Found listenkey", xbmc.LOGNOTICE )
  262. return listenkeyDict[0]
  263.  
  264. except Exception :
  265. sys.exc_clear() # Clears all exceptions so the script will continue to run
  266. xbmcgui.Dialog().ok( ADDON.getLocalizedString(30100), ADDON.getLocalizedString(30101), ADDON.getLocalizedString(30102) )
  267. xbmc.log( ADDON.getLocalizedString(30100) + " " + ADDON.getLocalizedString(30101) + " " + ADDON.getLocalizedString(30102), xbmc.LOGERROR )
  268. return False
  269.  
  270. return False
  271.  
  272.  
  273. """return list - False if it fails
  274. Will get a HTML page containing JSON data, decode it and return
  275. """
  276. def getJSONChannelList( self, url ) :
  277. try :
  278. jsonData = self._httpComm.get( url )
  279. jsonData = json.loads(jsonData)
  280. except Exception : # Show error message in XBMC GUI if failing to parse JSON
  281. sys.exc_clear() # Clears all exceptions so the script will continue to run
  282. xbmcgui.Dialog().ok( ADDON.getLocalizedString(30100), ADDON.getLocalizedString(30101), ADDON.getLocalizedString(30102) )
  283. xbmc.log( ADDON.getLocalizedString(30100) + " " + ADDON.getLocalizedString(30101) + " " + ADDON.getLocalizedString(30102), xbmc.LOGERROR )
  284. return False
  285.  
  286. return jsonData
  287.  
  288.  
  289. """return list - False if it fails
  290. Gets the favorites playlist and returns the streams as a list
  291. """
  292. def getFavoriteStreamsList( self, url ) :
  293. try :
  294. favoritesPlaylist = self._httpComm.get( url ) # favorites .pls playlist in plaintext
  295. favoritesList = [] # list that will contain streamlist
  296.  
  297. streamurl_re = re.compile( 'File\d+=([^\n]*)', re.I ) # first stream in .pls file
  298. channeltitle_re = re.compile( 'Title\d+=([^\n]*)', re.I )
  299.  
  300. streamTitles = channeltitle_re.findall( favoritesPlaylist )
  301. streamUrls = streamurl_re.findall( favoritesPlaylist )
  302.  
  303. if len(streamUrls) == len( streamTitles ) : # only continue if the count of urls and titles are equal
  304. for streamUrl in streamUrls :
  305. listitem = {}
  306. listitem['playlist'] = streamUrl
  307. tmp_name = streamTitles.pop()
  308. listitem['name'] = tmp_name.replace( "SKY FM - ", "" ) # favorite stream titles has some "fluff" text it that is removed
  309. listitem['key'] = self.makeChannelIconname( listitem['name'] )
  310. listitem['isNew'] = False
  311. listitem['bitrate'] = self._bitrate
  312. listitem['description'] = ""
  313. favoritesList.append( listitem )
  314.  
  315. else :
  316. return False
  317.  
  318. return favoritesList
  319.  
  320. except Exception : # Show error message in XBMC GUI if failing to parse JSON
  321. #sys.exc_clear() # Clears all exceptions so the script will continue to run
  322. xbmcgui.Dialog().ok( ADDON.getLocalizedString(30120), ADDON.getLocalizedString(30111), url )
  323. xbmc.log( ADDON.getLocalizedString(30120) + " " + ADDON.getLocalizedString(30111) + " " + url, xbmc.LOGERROR )
  324. return False
  325.  
  326. return favoritesList
  327.  
  328.  
  329. """return string
  330. Will take a channelname, lowercase it and remove spaces, dashes and other special characters
  331. The string returned is normally used as part of the filename for the channelart
  332. """
  333. def makeChannelIconname( self, channelname ) :
  334. iconreplacement_re = re.compile('[^a-z0-9]', re.I) # regex that hits everything but a-z and 0-9
  335. iconname = string.lower(iconreplacement_re.sub( '', channelname) )
  336. return iconname
  337.  
  338.  
  339. """return bool
  340. Simply adds a music item to the XBMC GUI
  341. """
  342. # Adds item to XBMC itemlist
  343. def addItem( self, channelTitle, streamUrl, streamDescription, streamBitrate, icon, isNewChannel ) :
  344.  
  345. if isNewChannel == True : # tart it up a bit if it's a new channel
  346. li = xbmcgui.ListItem(label="[COLOR FF007EFF]" + channelTitle + "[/COLOR]",thumbnailImage=icon)
  347. xbmc.log( "New channel found: " + channelTitle, xbmc.LOGERROR )
  348. else :
  349. li = xbmcgui.ListItem(label=channelTitle, thumbnailImage=icon)
  350.  
  351. li.setProperty("mimetype", 'audio/aac')
  352. li.setInfo( type="Music", infoLabels={ "label": channelTitle, "Genre": channelTitle, "Comment": streamDescription, "Size": (streamBitrate * 1024) })
  353. li.setProperty("IsPlayable", "true")
  354. li.setProperty("IsLive", "true")
  355.  
  356. xbmcplugin.addDirectoryItem(handle=HANDLE, url=streamUrl, listitem=li, isFolder=False)
  357.  
  358. return True
  359.  
  360.  
  361. """return string
  362. Gets the first stream from a playlist
  363. """
  364. def getFirstStream( self, playlistUrl, regex ) :
  365. plsData = self._httpComm.get( playlistUrl )
  366.  
  367. streamurls = regex.findall(plsData)
  368.  
  369. return streamurls[0]
  370.  
  371.  
  372. """return bool
  373. Checks if a file is older than x seconds
  374. """
  375. def checkFileTime( self, tmpfile, cachedir, timesince ) :
  376. if not os.path.exists( cachedir ) :
  377. os.makedirs( cachedir )
  378. return False
  379.  
  380. # If file exists, check timestamp
  381. if os.path.exists( tmpfile ) :
  382. if os.path.getmtime( tmpfile ) > ( time.time() - timesince ) :
  383. xbmc.log( 'It has not been ' + str( timesince/60 ) + ' minutes since ' + tmpfile + ' was last updated', xbmc.LOGNOTICE )
  384. return False
  385. else :
  386. xbmc.log( 'The cachefile ' + tmpfile + ' + has expired', xbmc.LOGNOTICE )
  387. return True
  388. # If file does not exist, return true so the file will be created by scraping the page
  389. else :
  390. xbmc.log( 'The cachefile ' + tmpfile + ' does not exist', xbmc.LOGNOTICE )
  391. return True
  392.  
  393.  
  394. MusicAddonInstance = musicAddonXbmc()
  395. MusicAddonInstance.start()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement