home *** CD-ROM | disk | FTP | other *** search
/ PC Welt 2006 November (DVD) / PCWELT_11_2006.ISO / casper / filesystem.squashfs / usr / lib / rhythmbox / plugins / artdisplay / AmazonCoverArtSearch.py < prev    next >
Encoding:
Python Source  |  2006-08-30  |  7.8 KB  |  266 lines

  1. # -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- 
  2. #
  3. # Copyright (C) 2006 - Gareth Murphy, Martin Szulecki
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2, or (at your option)
  8. # any later version.
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
  17.  
  18. from xml.dom import minidom
  19. import re
  20. import locale
  21. import urllib
  22.  
  23. import rhythmdb
  24. from Loader import Loader
  25.  
  26. LICENSE_KEY = "18C3VZN9HCECM5G3HQG2"
  27. DEFAULT_LOCALE = "en_US"
  28. ASSOCIATE = "webservices-20"
  29.  
  30.  
  31. class Bag: pass
  32.  
  33. class AmazonCoverArtSearch (object):
  34.     def __init__ (self, loader):
  35.         self.searching = False
  36.         self.cancel = False
  37.         self.loader = loader
  38.         self._supportedLocales = {
  39.             "en_US" : ("us", "xml.amazon.com"),
  40.             "en_GB" : ("uk", "xml-eu.amazon.com"),
  41.             "de" : ("de", "xml-eu.amazon.com"),
  42.             "ja" : ("jp", "xml.amazon.co.jp")
  43.         }
  44.         self.db = None
  45.         self.entry = None
  46.  
  47.     def __get_locale (self):
  48.         default = locale.getdefaultlocale ()
  49.         lc_id = DEFAULT_LOCALE
  50.         if default[0] is not None:
  51.             if self._supportedLocales.has_key (default[0]):
  52.                 lc_id = default[0]
  53.  
  54.         lc_host = self._supportedLocales[lc_id][1]
  55.         lc_name = self._supportedLocales[lc_id][0]
  56.         return ((lc_host, lc_name))
  57.  
  58.     def search (self, db, entry, on_search_completed_callback, *args):
  59.         self.searching = True
  60.         self.cancel = False
  61.         self.db = db
  62.         self.entry = entry
  63.         self.on_search_completed_callback = on_search_completed_callback
  64.         self.args = args
  65.         self.keywords = []
  66.  
  67.         st_artist = db.entry_get (entry, rhythmdb.PROP_ARTIST)
  68.         st_album = db.entry_get (entry, rhythmdb.PROP_ALBUM)
  69.  
  70.         # Tidy up
  71.  
  72.         # Replace quote characters
  73.         # don't replace single quote: could be important punctuation
  74.         for char in ["\""]:
  75.             st_artist = st_artist.replace (char, '')
  76.             st_album = st_album.replace (char, '')
  77.  
  78.  
  79.         self.st_album = st_album
  80.         self.st_artist = st_artist
  81.  
  82.         # Remove variants of Disc/CD [1-9] from album title before search
  83.         for exp in ["\([Dd]isc *[1-9]+\)", "\([Cc][Dd] *[1-9]+\)"]:
  84.             p = re.compile (exp)
  85.             st_album = p.sub ('', st_album)
  86.  
  87.         st_album_no_vol = st_album
  88.         for exp in ["\(*[Vv]ol.*[1-9]+\)*"]:
  89.             p = re.compile (exp)
  90.             st_album_no_vol = p.sub ('', st_album_no_vol)
  91.  
  92.         self.st_album_no_vol = st_album_no_vol
  93.  
  94.         # Save current search's entry properties
  95.         self.search_album = st_album
  96.         self.search_artist = st_artist
  97.         self.search_album_no_vol = st_album_no_vol
  98.         
  99.         # TODO: Improve to decrease wrong cover downloads, maybe add severity?
  100.         # Assemble list of search keywords (and thus search queries)
  101.         if st_album == "Unknown":
  102.             self.keywords.append ("%s Best of" % (st_artist))
  103.             self.keywords.append ("%s Greatest Hits" % (st_artist))
  104.             self.keywords.append ("%s Essential" % (st_artist))
  105.             self.keywords.append ("%s Collection" % (st_artist))
  106.             self.keywords.append ("%s" % (st_artist))
  107.         elif st_artist == "Unknown":
  108.             self.keywords.append ("%s" % (st_album))
  109.             if st_album_no_vol != st_artist:
  110.                 self.keywords.append ("%s" % (st_album_no_vol))
  111.             self.keywords.append ("Various %s" % (st_album))
  112.         else:
  113.             if st_album != st_artist:
  114.                 self.keywords.append ("%s %s" % (st_artist, st_album))
  115.                 if st_album_no_vol != st_album:
  116.                     self.keywords.append ("%s %s" % (st_artist, st_album_no_vol))
  117.                 if (st_album != "Unknown"):
  118.                     self.keywords.append ("Various %s" % (st_album))
  119.             self.keywords.append ("%s" % (st_artist))
  120.  
  121.         # Initiate asynchronous search
  122.         self.search_next ();
  123.  
  124.     def __build_url (self, keyword):
  125.         (lc_host, lc_name) = self.__get_locale ()
  126.  
  127.         url = "http://" + lc_host + "/onca/xml3?f=xml"
  128.         url += "&t=%s" % ASSOCIATE
  129.         url += "&dev-t=%s" % LICENSE_KEY
  130.         url += "&type=%s" % 'lite'
  131.         url += "&locale=%s" % lc_name
  132.         url += "&mode=%s" % 'music'
  133.         url += "&%s=%s" % ('KeywordSearch', urllib.quote (keyword))
  134.  
  135.         return url
  136.  
  137.     def search_next (self):
  138.         self.searching = True
  139.         
  140.         if len (self.keywords)==0:
  141.             keyword = None
  142.         else:
  143.             keyword = self.keywords.pop (0)
  144.  
  145.         if keyword is None:
  146.             # No keywords left to search -> no results
  147.             self.on_search_completed (None)
  148.             ret = False
  149.         else:
  150.             # Retrieve search for keyword
  151.             url = self.__build_url (keyword.strip ())
  152.             self.loader.get_url (url, self.on_search_response)
  153.             ret = True
  154.  
  155.         return ret
  156.  
  157.     def __unmarshal (self, element):
  158.         rc = Bag ()
  159.         if isinstance (element, minidom.Element) and (element.tagName == 'Details'):
  160.             rc.URL = element.attributes["url"].value
  161.         childElements = [e for e in element.childNodes if isinstance (e, minidom.Element)]
  162.         if childElements:
  163.             for child in childElements:
  164.                 key = child.tagName
  165.                 if hasattr (rc, key):
  166.                     if type (getattr (rc, key)) <> type ([]):
  167.                         setattr (rc, key, [getattr (rc, key)])
  168.                     setattr (rc, key, getattr (rc, key) + [self.__unmarshal (child)])
  169.                 elif isinstance(child, minidom.Element) and (child.tagName == 'Details'):
  170.                     setattr (rc,key,[self.__unmarshal(child)])
  171.                 else:
  172.                     setattr (rc, key, self.__unmarshal(child))
  173.         else:
  174.             rc = "".join ([e.data for e in element.childNodes if isinstance (e, minidom.Text)])
  175.             if element.tagName == 'SalesRank':
  176.                 rc = rc.replace ('.', '')
  177.                 rc = rc.replace (',', '')
  178.                 rc = int (rc)
  179.         return rc
  180.  
  181.     def on_search_response (self, result_data):
  182.         if result_data is None:
  183.             self.search_next()
  184.             return
  185.  
  186.         try:
  187.             xmldoc = minidom.parseString (result_data)
  188.         except:
  189.             self.search_next()
  190.             return
  191.         
  192.         data = self.__unmarshal (xmldoc).ProductInfo
  193.  
  194.         if hasattr(data, 'ErrorMsg'):
  195.             # Search was unsuccessful, try next keyword
  196.             self.search_next ()
  197.         else:
  198.             # We got some search results
  199.             self.on_search_results (data.Details)
  200.  
  201.     def on_search_results (self, results):
  202.         self.on_search_completed (results)
  203.  
  204.     def on_search_completed (self, result):
  205.         self.on_search_completed_callback (self, self.entry, result, *self.args)
  206.         self.searching = False
  207.  
  208.     def __tidy_up_string (self, s):
  209.         # Lowercase
  210.         s = s.lower ()
  211.         # Strip
  212.         s = s.strip ()
  213.  
  214.         # TODO: Convert accented to unaccented (fixes matching Salom├⌐ vs Salome)
  215.         s = s.replace (" - ", " ")    
  216.         s = s.replace (": ", " ")
  217.         s = s.replace (" & ", " and ")
  218.  
  219.         return s
  220.  
  221.     def get_best_match (self, search_results):
  222.         # Default to "no match", our results must match our criteria
  223.         best_match = None
  224.  
  225.         try:
  226.             if self.search_album != "Unknown":
  227.                 album_check = self.__tidy_up_string (self.search_album)
  228.                 for item in search_results:
  229.                     # Check for album name in ProductName
  230.                     product_name = self.__tidy_up_string (item.ProductName)
  231.  
  232.                     if product_name == album_check:
  233.                         # Found exact album, can not get better than that
  234.                         best_match = item
  235.                         break
  236.                     # If we already found a best_match, just keep checking for exact one
  237.                     elif (best_match is None) and (product_name.find (album_check) != -1):
  238.                         best_match = item
  239.  
  240.             # If we still have no definite hit, use first result where artist matches
  241.             if (self.search_album == "Unknown" and self.search_artist != "Unknown"):
  242.                 artist_check = self.__tidy_up_string (self.search_artist)
  243.                 if best_match is None:
  244.                     # Check if artist appears in the Artists list
  245.                     hit = False
  246.                     for item in search_results:
  247.                         if type (item.Artists.Artist) <> type ([]):
  248.                             artists = [item.Artists.Artist]
  249.                         else:
  250.                             artists = item.Artists.Artist
  251.  
  252.                         for artist in artists:
  253.                             artist = self.__tidy_up_string (artist)
  254.                             if artist.find (artist_check) != -1:
  255.                                 best_match = item
  256.                                 hit = True                        
  257.                                 break
  258.                         if hit:
  259.                             break
  260.  
  261.             return best_match
  262.  
  263.         except TypeError:
  264.             return None
  265.