clJamendo.py

00001 #!/usr/bin/env python
00002 # -*- coding: utf-8 -*-
00003 
00004 # ----------------------------------------------------------------------------
00005 # pyjama - python jamendo audioplayer
00006 # Copyright (c) 2008 Daniel Nögel
00007 #
00008 # This program is free software: you can redistribute it and/or modify
00009 # it under the terms of the GNU General Public License as published by
00010 # the Free Software Foundation, either version 3 of the License, or
00011 # (at your option) any later version.
00012 #
00013 # This program is distributed in the hope that it will be useful,
00014 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00015 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00016 # GNU General Public License for more details.
00017 # You should have received a copy of the GNU General Public License
00018 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
00019 # ----------------------------------------------------------------------------
00020 
00021 ## @package Jamendo
00022 # The class Jamendo on this module manages communication
00023 # with jamendo
00024 
00025 #JSon parser
00026 import simplejson as json
00027 
00028 import os
00029 import hashlib
00030 import time
00031 
00032 # URL Handling
00033 from urllib2 import urlopen, URLError
00034 from urllib import urlencode as _urlencode
00035 
00036 #own classes
00037 import functions
00038 
00039 # Gettext - Übersetzung
00040 functions.translation_gettext()
00041 #def _(string):
00042 #    return string
00043 
00044 ###################################################################
00045 #
00046 # several urls for jamendo - should be moved to documentation
00047 #
00048 ### ari = Artistinfo
00049 ### tri = Trackinfo
00050 ### ali = albuminfo
00051 # ===> http://developer.jamendo.com/de/wiki/MusiclistApi_draft
00052 # info_min or info_common or full.
00053 
00054 ### ALLE MÖGLICHEN ALBEN INFOS:
00055     #http://www.jamendo.com/get/album/id/album/data/json/33?ali=info_common+archives+tracker+lengths&ari=info_min&aenc=ogg3
00056 
00057 ### Alle Tracks eines Albums:
00058     # http://www.jamendo.com/get/track/id/album/data/json/33?ali=full&ari=full+object&tri=full&item_o=track_no_asc&showhidden=1&shownotmod=1
00059     
00060 ### Alle Infos zum Track:
00061     # http://www.jamendo.com/get/track/id/track/data/json/4707?ali=full&ari=full+object&tri=full&item_o=track_no_asc&showhidden=1&shownotmod=1
00062 
00063 ### 50 populäre alben von rock
00064    # http://api.jamendo.com/get2/id+name+url+image+artist_name/album/jsonpretty/?tag_idstr=rock&n=50&order=rating_desc
00065 
00066 ### Torrent, redirect
00067     # http://api.jamendo.com/get2/bittorrent/file/redirect/?album_id=919&type=archive&class=ogg3
00068 
00069 ### 20 artists alphabetically #0-9
00070     # http://www.jamendo.com/get2/name/artist/plain?order=alpha_asc&hasalbums=1&firstletter=a
00071 
00072 #### Arists has albums
00073     # http://www.jamendo.com/get2/name/artist/plain?order=alpha_asc&firstletter=0-9&hasalbums=1&n=40
00074 
00075 #### Soundslike
00076     # http://api.jamendo.com/get2/id+name/album/jsonpretty/album_album2_soundslike/?album2_id=116
00077 
00078 #### Radio
00079     # Find radio by name
00080     # http://api.jamendo.com/get2/id+name+idstr/radio/jsonpretty/?radio_idstr=Alchimie
00081     # Get radio tracks
00082     # http://www.jamendo.com/get2/track_id/track/jsonpretty/radio_track_inradioplaylist/?order=numradio_asc&radio_id=17
00083 
00084 ## @cond skip
00085 ## This class isn't used at all.
00086 # I was just wondering how to make
00087 # this whole query thing more
00088 # relyable for the developer.
00089 class Query(object):
00090     def __init__(self, query=None, caching_time=None, raise_event=None, ignore_cache=None):
00091         self.query = query
00092         self.caching_time = caching_time
00093         self.raise_event = raise_event
00094         self.ignore_cache = ignore_cache
00095 
00096     def send(self):
00097         pass
00098 ## @endcond
00099         
00100 
00101 ## Module for interaction with jamendo
00102 class Jamendo:
00103     ## Constructor
00104     def __init__(self, parent):
00105         self.__parent = parent
00106         self.__home = functions.preparedirs()
00107         ## The directory in which pyjama stores cached queries
00108         self.cachedir = os.path.join(self.__home, "cache")
00109         ## Counter for queries from cache
00110         self.cache_counter = 0
00111         ## Counter for queries from jamendo
00112         self.jamendo_counter = 0
00113 
00114         self.__last_query = None
00115 
00116         self.__ignore_cache = None
00117 
00118         self.__parent.Events.add_event("jamendo-query")
00119         
00120         # Somehow simplejsons behaviour
00121         # changed some times so i had to
00122         # do it this way. 
00123         # If this new behaviour won't change
00124         # again, the strict-statement is
00125         # obsolete
00126         try:
00127             self.json = json.JSONDecoder(strict=False)
00128         except:
00129             self.json = json.JSONDecoder()
00130 
00131     ## Get cover image for a given album
00132     # This function will download a image
00133     # if it's not on disc, yet.
00134     # Else it will return the local uri
00135     # @param self OP
00136     # @param album_id Id of the album 
00137     # @param size Size of the cover to get (default: 100)
00138     def get_album_image(self, album_id, size=100):
00139         image = "http://api.jamendo.com/get2/image/album/redirect/?id=%i&imagesize=%i" % (int(album_id), int(size))
00140         md5hash = hashlib.md5(image).hexdigest()
00141         fh = os.path.join(self.main.home, "images", md5hash)
00142         if not os.path.exists(fh):
00143             try:
00144                 urllib.urlretrieve(image, fh)
00145                 return fh
00146             except IOError:
00147                 print ("Could not load image")
00148                 return None
00149         else:
00150             return fh
00151 
00152     ## Let the next query no be loaded from cache
00153     # @param self Object Pointer
00154     # @param value Boolean
00155     # If True is set, the next query will be ignored
00156     def set_ignore_cache(self, value):
00157         self.__ignore_cache = value
00158 
00159     ## Will the next query be cached?
00160     # @return bool
00161     def get_ignore_cache(self):
00162         return self.__ignore_cache
00163 
00164     ## Is a given jamendo-query-result usable?
00165     # @return bool
00166     # @todo Tidy up return values
00167     def check(self, query):
00168         if query is None:
00169             return False
00170         if isinstance(query, list) and query == []:
00171             return False
00172         if isinstance(query, int) and query < 0:
00173             return False
00174         if isinstance(query, str) and query == "":
00175             return False
00176         return True
00177 
00178     ## Decode a json encoded string
00179     # @param self Object Pointer
00180     # @param string The string to decode
00181     # @return 
00182     # - None if an error occured
00183     # - The decoded string if successful
00184     def json_decode(self, string):
00185         try:
00186 #            return simplejson.loads(string.decode('utf-8'))
00187 #            x= json.read(string)
00188 
00189             return self.json.decode(string) 
00190         except Exception, inst:
00191             desc = "An error occured while parsing Jamendos server-response\n"
00192             desc += "Most probably this is a json issue coming up with new\n"
00193             desc += "versions of debian.\n"
00194             desc += "<b>Get the newest version of pyjama to deal with it!</b>"
00195             self.__parent.Events.raise_event("error", inst, desc)
00196             return None
00197 
00198     ## Do a jamendo query
00199     # This function is obsolete, run query() instead
00200     # @param self Object Pointer
00201     # @param field The fields to get (think of SELECT)
00202     # @param unit The table(s) from which you want to get (think of FROM)
00203     # @param params Additional params (think of WHERE)
00204     # @param format Which output format would you like to get?
00205     # Use json or jsonpretty
00206     # @return json string
00207     def get(self, field, unit, params, format="json"):
00208         print ("Requesting database on jamendo.com")
00209         query = "http://api.jamendo.com/get2/" + field + "/" + unit + "/" + format  + "/" + params
00210         if self.__parent.debug:
00211             print query
00212         source = urlopen(query)
00213         print ("Requesting database - done")
00214         return self.json_decode(source.read())
00215 
00216     ## Tests if a specific query can be found in cache and if it is up to date
00217     # This methode is called by query() function for that you should not call this
00218     # methode directly.
00219     # @param query The jamendo query you want to make
00220     # @param CACHING_TIME Must be CACHING_TIME_LONG or CACHING_TIME_SHORT from pyjama.cfg
00221     # This param decides where is looked for the cached query
00222     # @return
00223     # - False if query not in cache or to old
00224     # - The cached itself when its cached
00225     def cache(self, query, CACHING_TIME = None):
00226         if CACHING_TIME == None or CACHING_TIME == self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_LONG"):
00227             CACHING_TIME = self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_LONG")
00228             subdir = "long"
00229         else:
00230             subdir = "short"
00231         if self.__ignore_cache is True: 
00232             print "Ignoring Cache"
00233             CACHING_TIME = 0
00234         md5hash = hashlib.md5(query).hexdigest()
00235         datei = os.path.join(self.cachedir, subdir, md5hash)
00236         if os.path.exists(datei):
00237             file_time = os.stat(datei).st_mtime
00238             if file_time + CACHING_TIME > time.time():
00239                 datei = open(datei,'r')
00240                 content = datei.read()
00241                 datei.close()
00242                 remaining = CACHING_TIME - (time.time() - file_time)
00243                 m = 60
00244                 h = 60*m
00245                 d = h*24
00246                 #w = d*7
00247                 days = remaining // d
00248                 hours = (remaining - d*days) // h
00249                 mins = ((remaining - d*days) - h*hours) // m
00250                 secs = (((remaining - d*days) - h*hours) - m*mins)
00251                 if self.__parent.verbose:
00252                     print ("This Query was loaded from Cache. New Data will be loaded from Jamendo  in %i days, %i hourse, %i minutes and %i seconds.") % (days, hours, mins, secs)
00253                     if CACHING_TIME == self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_LONG"):
00254                         print ("Don't mind this might be to long. This specific data- set won't change!")
00255 #                self.__parent.window.sbStatus.set_text("Info", _("Aus dem Cache. Gültig noch %iT, %i:%i:%i") % (days, hours, mins, secs))
00256                 self.__parent.window.sbStatus.set_text("Info", _("From Cache. Reload in %iD, %i:%i:%i") % (days, hours, mins, secs))
00257                 self.cache_counter += 1
00258                 return content
00259             print ("Cache too old, Loading Data from Jamendo")
00260             self.__parent.window.sbStatus.set_text("Info", _("Refreshing Cache"))
00261             return False
00262         print ("No Data cached, yet. Caching Query now")
00263         self.__parent.window.sbStatus.set_text("Info", _("Cached from Jamendo"))
00264         self.jamendo_counter += 1
00265         return False
00266 
00267     def __write2cache(self, query, result, caching_time = None):
00268         if caching_time == None or caching_time == self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_LONG"):
00269             subdir = "long"
00270         else:
00271             subdir = "short"
00272         if self.__parent.debug:
00273             print subdir
00274         md5hash = hashlib.md5(query).hexdigest()
00275         datei = os.path.join(self.cachedir, subdir, md5hash)
00276         fh = file(datei,'w')
00277         fh.write(result)
00278         fh.close()
00279 
00280     ## Query Jamendo via GET2 API
00281     # Allways use this function to query jamendo - it will automatically do the
00282     # caching and all the other stuff!
00283     # @param query The Jamendo query - strip of 'http://api.jamendo.com/get2/' (!)
00284     # @param caching_time Must be CACHING_TIME_LONG or CACHING_TIME_SHORT from pyjama.cfg
00285     # @return 
00286     # - None if this query came to fast after another
00287     # - -1 if jamendo could not be accesed
00288     # - Dictionary if succesfull
00289     def query(self, query, caching_time = None, raise_query_event=True):
00290         if raise_query_event:
00291             self.__parent.Events.raise_event("jamendo-query", "start")
00292         query = "http://api.jamendo.com/get2/" + query
00293         ret = self.cache(query, caching_time)
00294         if  ret != False: ##wenn cache daten vorliegen
00295             if raise_query_event:
00296                 self.__parent.Events.raise_event("jamendo-query", "end")
00297             return self.json_decode(ret)
00298         if self.__last_query == None:
00299             self.__last_query = time.time()
00300         elif self.__last_query + 1 > time.time():
00301             print "To fast"
00302             print (query)
00303             if raise_query_event:
00304                 self.__parent.Events.raise_event("jamendo-query", "end")
00305             return None
00306         else:
00307             self.__last_query = time.time()
00308         if self.__parent.verbose:
00309             print ("Requesting database on jamendo.com")
00310         if self.__parent.debug:
00311             print query
00312         try:
00313             source = urlopen(query)
00314         except URLError, inst:
00315             desc = "Could not query Data from jamendo. Perhaps Jamendo is maintening the servers or your connection is broken."
00316             self.__parent.Events.raise_event("error", inst, desc)
00317             print ("couldn't load from jamendo")
00318             if raise_query_event:
00319                 self.__parent.Events.raise_event("jamendo-query", "end")
00320             return -1
00321         ret = source.read()
00322         self.__write2cache(query, ret, caching_time)
00323         if self.__parent.verbose:
00324             print ("Requesting database - done, wrote cache")
00325         if raise_query_event:
00326             self.__parent.Events.raise_event("jamendo-query", "end")
00327         return self.json_decode(ret)
00328 
00329     ## This hack prevents the jamendo class from aborting the query
00330     # with a "to fast" message - there should be a better solution
00331     def last_query_hack(self):
00332         self.__last_query=0
00333 
00334     ## Query Jamendo via REST API
00335     # This function handles queries for jamendo's old rest api.
00336     # Please try not to use this api.
00337     # @param query The Jamendo query - strip of 'http://api.jamendo.com/get2/' (!)
00338     # @param caching_time Must be CACHING_TIME_LONG or CACHING_TIME_SHORT from pyjama.cfg
00339     # @return 
00340     # - None if this query came to fast after another
00341     # - -1 if jamendo could not be accesed
00342     # - Dictionary if succesfull
00343     def queryold(self, query, caching_time = None, raise_query_event=True): 
00344         if raise_query_event:
00345             self.__parent.Events.raise_event("jamendo-query", "start")
00346         query = "http://www.jamendo.com/get/" + query
00347         ret = self.cache(query, caching_time)
00348         if  ret != False: ##wenn cache daten vorliegen
00349             if raise_query_event:
00350                 self.__parent.Events.raise_event("jamendo-query", "end")
00351             return self.json_decode(ret)
00352         if self.__last_query == None:
00353             self.__last_query = time.time()
00354         elif self.__last_query + 1 > time.time():
00355             print ("To fast")
00356             print (query)
00357             if raise_query_event:
00358                 self.__parent.Events.raise_event("jamendo-query", "end")
00359             return None
00360         else:
00361             self.__last_query = time.time()   
00362         print ("Requesting database on jamendo.com")
00363         if self.__parent.debug:
00364             print query
00365         try:
00366             source = urlopen(query)
00367         except URLError, inst:
00368             desc = "Could not query Data from jamendo. Perhaps Jamendo is maintening the servers or your connection is broken."
00369             self.__parent.Events.raise_event("error", inst, desc)
00370             print ("couldn't load from jamendo")
00371             if raise_query_event:
00372                 self.__parent.Events.raise_event("jamendo-query", "end")
00373             return -1
00374         ret = source.read()
00375         self.__write2cache(query, ret, caching_time)
00376         print ("Requesting database - done, wrote cache")
00377         if raise_query_event:
00378             self.__parent.Events.raise_event("jamendo-query", "end")
00379         return self.json_decode(ret)
00380 
00381     ## Get albums similar to the given one
00382     # @param album_id The album to search similar albums for
00383     # @return results from query()
00384     def get_similar_albums(self, album_id, num=5):
00385         if self.__parent.debug: 
00386             print ("jamendo: getting similar albums")
00387         if num <= 0: return None
00388         ret = self.query("id/album/jsonpretty/album_album2_soundslike/?album2_id=%i&n=%i" % (album_id, num) )
00389         return ret
00390 
00391     ## Get albums from Jamendo
00392     # @param num Number of albums to fetch
00393     # @param order How the result are ordered (ratingweek, download ...)
00394     # @param page Which page should be fetched?
00395     # @param tag Only fetch albums with a particular tag
00396     # @return results from query()
00397     def top(self, num = 10, order = "tag", page=1, tag="all"):
00398         if self.__parent.debug: 
00399             print ("jamendo: getting top albums")
00400         if tag == "all": 
00401             if self.__parent.verbose:
00402                 print ("Getting top %i ordered by '%s'") % (num, order)
00403             ret = self.query("artist_name+artist_idstr+artist_id+album_name+album_id+album_image+album_genre/album/json/?order=%s_desc&n=%i&pn=%s" % (order, num, page), self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_SHORT"))
00404         else:
00405             if self.__parent.verbose:
00406                 print ("Getting %i albums tagged '%s' ordered by '%s'") % (num, tag, order)
00407             ret = self.query("artist_name+artist_idstr+artist_id+album_name+album_id+album_image+album_genre/album/json/?tag_idstr=%s&n=%i&pn=%s&order=%s_desc" % (tag, num, page, order), self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_SHORT"))
00408         return ret
00409 
00410     ## Get stream URL for a track
00411     # @param id The track-id
00412     # @return results from query()
00413     def stream(self, id):
00414         if self.__parent.debug: 
00415             print ("jamendo: getting stream")
00416         if self.__parent.verbose:
00417             print ("Getting stream informations")
00418         ret = self.query("stream/track/json/?id=%i&streamencoding=ogg2" % id, self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_LONG"))
00419         if ret == None:
00420             return None
00421         return ret[0]
00422                                 #track/id/album/data/json/15395?ali=common_info+archives+artist&ari=full+object&tri=full&item_o=track_no_asc&showhidden=1&shownotmod=1
00423 
00424     ## Get infos for a track
00425     # Loads a plenty of track infos from jamendo
00426     # @param id The track id you want to query
00427     # @return results from query()
00428     def trackinfos(self, id):
00429         if self.__parent.debug: 
00430             print ("jamendo: getting track infos")
00431         if self.__parent.verbose:
00432             print ("Getting track informations")
00433         ret = self.queryold("track/id/track/data/json/%s?ali=full&ari=full+object+stream&tri=full&item_o=track_no_asc&showhidden=1&streamencoding=ogg2&shownotmod=1" % str(id), self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_LONG"))
00434         return ret
00435 
00436         #!!!! ret = self.queryold("track/id/album/data/json/%s?ali=full&ari=full+object&tri=full&item_o=track_no_asc&showhidden=1&shownotmod=1" % id) 
00437 
00438     ###################################################################
00439     #
00440     # old
00441     # RETURNS: dictionary
00442     #
00443     def albuminfos_altundmaechtig(self, id):
00444         if self.__parent.debug: 
00445             print ("jamendo: getting a lot of album infos")
00446         strQuery = "track/id/album/data/json/ID?ali=full&ari=full+object&tri=full&item_o=track_no_asc&showhidden=1&shownotmod=1"
00447         strQuery = strQuery.replace("ID", str(id))
00448         ret = self.queryold(strQuery, self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_LONG"))
00449         return ret
00450 
00451     ###################################################################
00452     #
00453     # get infos for a album
00454     # RETURNS: dictionary
00455     #
00456     def albuminfos(self, id):
00457         if self.__parent.debug: 
00458             print ("jamendo: getting album infos")
00459         if self.__parent.verbose:
00460             print ("Getting album informations")
00461         strQuery = "album/id/album/data/prettyjson/ID?ali=full&ari=full+object&item_o=track_no_asc&showhidden=1&shownotmod=1"
00462         strQuery = strQuery.replace("ID", str(id))
00463         ret = self.queryold(strQuery, self.__parent.settings.get_value("JAMENDO", "CACHING_TIME_LONG"))
00464         if ret == None:
00465             return None
00466         return ret[0]
00467 
00468 
00469 
00470 
00471 #http://api.jamendo.com/get2/track_name+track_duration+track_url+license_url+album_id+album_name+artist_id+artist_idstr+artist_name/track/jsonpretty/album_artist/
00472 
00473     ###################################################################
00474     #
00475     # get an album's tracks
00476     # RETURNS: dictionary
00477     #
00478     def albumtracks(self, id): 
00479         if self.__parent.debug: 
00480             print ("jamendo: getting album's tracks")       
00481         ret = self.get("track_id+track_name+track_stream+track_duration+album_name", "track", "album_track/?album_id=" + str(id)+"&streamencoding=ogg2")        
00482         return ret
00483         
00484     ###################################################################
00485     #
00486     # get an artist's album 
00487     # RETURNS: dictionary
00488     # 
00489     def artistalbums(self, idstr):
00490         if self.__parent.debug: 
00491             print ("jamendo: getting artist's albums")
00492         ret = self.get("album_id+album_name+album_playlist", "artist", "?idstr=" + idstr + "&ali=full")
00493         print ret
00494         return ret
00495 
00496     ###################################################################
00497     #
00498     # get a list of artists
00499     # RETURNS: dictionary
00500     #
00501     def artistlist(self):
00502         if self.__parent.debug: 
00503             print ("jamendo: getting artist list")
00504         ret =  self.get("id+idstr+name", "artist", "?artist_hasalbums&n=50")
00505         return ret

Generated on Thu Jun 4 19:08:24 2009 for Pyjama by  doxygen 1.5.8