From ad645d960df7d6a41c2111dbca5c20e455bdc44c Mon Sep 17 00:00:00 2001 From: David Date: Tue, 5 Aug 2014 18:28:34 +0200 Subject: [PATCH 01/11] Replace Add with Search on top --- data/interfaces/default/base.html | 2 +- data/interfaces/default/css/style.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html index 2920f0a5..eba89c37 100644 --- a/data/interfaces/default/base.html +++ b/data/interfaces/default/base.html @@ -62,7 +62,7 @@ - + diff --git a/data/interfaces/default/css/style.css b/data/interfaces/default/css/style.css index 109a57b9..16f4171e 100644 --- a/data/interfaces/default/css/style.css +++ b/data/interfaces/default/css/style.css @@ -503,7 +503,7 @@ header .wrapper { margin: 0 auto; overflow: hidden; position: relative; - width: 960px; + width: 990px; } header #logo { float: left; From d48dbc6bbb7b9484303afc4ab26d323f5b90cc20 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 6 Aug 2014 00:59:03 +0200 Subject: [PATCH 02/11] Creating method of shutdown actions --- headphones/webserve.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/headphones/webserve.py b/headphones/webserve.py index eea2b836..7b52f4e8 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1393,25 +1393,23 @@ class WebInterface(object): configUpdate.exposed = True + def do_shutdown(self, signal, title, timer): + headphones.SIGNAL = signal + message = title + '...' + return serve_template(templatename="shutdown.html", title=title, + message=message, timer=timer) + def shutdown(self): - headphones.SIGNAL = 'shutdown' - message = 'Shutting Down...' - return serve_template(templatename="shutdown.html", title="Shutting Down", message=message, timer=15) - return page + return self.do_shutdown('shutdown', 'Shutting Down', 15) shutdown.exposed = True def restart(self): - headphones.SIGNAL = 'restart' - message = 'Restarting...' - return serve_template(templatename="shutdown.html", title="Restarting", message=message, timer=30) + return self.do_shutdown('restart', 'Restarting', 30) restart.exposed = True def update(self): - headphones.SIGNAL = 'update' - message = 'Updating...' - return serve_template(templatename="shutdown.html", title="Updating", message=message, timer=120) - return page + return self.do_shutdown('update', 'Updating', 120) update.exposed = True def extras(self): From 9ec4866cea04385314d595ac82f12ef545a99360 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 6 Aug 2014 12:10:03 +0200 Subject: [PATCH 03/11] renaming do_shutdown() -> do_state_change() --- headphones/webserve.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/headphones/webserve.py b/headphones/webserve.py index 7b52f4e8..8d43c87d 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1393,23 +1393,22 @@ class WebInterface(object): configUpdate.exposed = True - def do_shutdown(self, signal, title, timer): + def do_state_change(self, signal, title, timer): headphones.SIGNAL = signal message = title + '...' return serve_template(templatename="shutdown.html", title=title, message=message, timer=timer) def shutdown(self): - return self.do_shutdown('shutdown', 'Shutting Down', 15) - + return self.do_state_change('shutdown', 'Shutting Down', 15) shutdown.exposed = True def restart(self): - return self.do_shutdown('restart', 'Restarting', 30) + return self.do_state_change('restart', 'Restarting', 30) restart.exposed = True def update(self): - return self.do_shutdown('update', 'Updating', 120) + return self.do_state_change('update', 'Updating', 120) update.exposed = True def extras(self): From 0c61eb11f5627a22011c49838bbd1fa7d92c7809 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 6 Aug 2014 12:46:26 +0200 Subject: [PATCH 04/11] Dropping trailing whitespaces --- headphones/albumswitcher.py | 18 +- headphones/api.py | 176 +++--- headphones/db.py | 34 +- headphones/getXldProfile.py | 4 +- headphones/importer.py | 20 +- headphones/librarysync.py | 80 +-- headphones/lyrics.py | 16 +- headphones/mb.py | 148 +++--- headphones/notifiers.py | 18 +- headphones/postprocessor.py | 306 +++++------ headphones/searcher.py | 4 +- headphones/searcher_rutracker.py | 80 +-- headphones/updater.py | 6 +- headphones/utorrent.py | 2 +- headphones/webserve.py | 10 +- headphones/webstart.py | 20 +- lib/apscheduler/jobstores/ram_store.py | 2 +- lib/beets/autotag/__init__.py | 2 +- lib/beets/ui/migrate.py | 6 +- lib/beets/util/__init__.py | 2 +- lib/bs4/dammit.py | 2 +- lib/bs4/diagnose.py | 4 +- lib/cherrypy/__init__.py | 140 ++--- lib/cherrypy/_cpchecker.py | 84 +-- lib/cherrypy/_cpconfig.py | 14 +- lib/cherrypy/_cpdispatch.py | 134 ++--- lib/cherrypy/_cperror.py | 122 ++--- lib/cherrypy/_cplogging.py | 98 ++-- lib/cherrypy/_cpmodpy.py | 56 +- lib/cherrypy/_cpnative_server.py | 36 +- lib/cherrypy/_cpreqbody.py | 234 ++++---- lib/cherrypy/_cprequest.py | 292 +++++----- lib/cherrypy/_cpserver.py | 66 +-- lib/cherrypy/_cptools.py | 148 +++--- lib/cherrypy/_cptree.py | 110 ++-- lib/cherrypy/_cpwsgi.py | 98 ++-- lib/cherrypy/_cpwsgi_server.py | 10 +- lib/cherrypy/lib/__init__.py | 6 +- lib/cherrypy/lib/auth.py | 32 +- lib/cherrypy/lib/auth_basic.py | 8 +- lib/cherrypy/lib/auth_digest.py | 28 +- lib/cherrypy/lib/caching.py | 118 ++-- lib/cherrypy/lib/covercp.py | 50 +- lib/cherrypy/lib/cpstats.py | 70 +-- lib/cherrypy/lib/cptools.py | 128 ++--- lib/cherrypy/lib/encoding.py | 78 +-- lib/cherrypy/lib/gctools.py | 14 +- lib/cherrypy/lib/httpauth.py | 48 +- lib/cherrypy/lib/httputil.py | 138 ++--- lib/cherrypy/lib/jsontools.py | 14 +- lib/cherrypy/lib/profiler.py | 40 +- lib/cherrypy/lib/reprconf.py | 130 ++--- lib/cherrypy/lib/sessions.py | 238 ++++----- lib/cherrypy/lib/static.py | 76 +-- lib/cherrypy/process/plugins.py | 178 +++---- lib/cherrypy/process/servers.py | 72 +-- lib/cherrypy/process/win32.py | 46 +- lib/cherrypy/process/wspbus.py | 86 +-- lib/cherrypy/scaffold/__init__.py | 10 +- lib/cherrypy/wsgiserver/ssl_builtin.py | 14 +- lib/cherrypy/wsgiserver/ssl_pyopenssl.py | 62 +-- lib/cherrypy/wsgiserver/wsgiserver2.py | 502 +++++++++--------- lib/cherrypy/wsgiserver/wsgiserver3.py | 498 ++++++++--------- lib/configobj.py | 388 +++++++------- lib/feedparser.py | 210 ++++---- lib/gntp/core.py | 2 +- lib/httplib2/__init__.py | 154 +++--- lib/httplib2/iri2uri.py | 18 +- lib/mako/ext/autohandler.py | 4 +- lib/mako/ext/preprocessors.py | 6 +- lib/musicbrainzngs/musicbrainz.py | 8 +- lib/mutagen/easymp4.py | 2 +- lib/oauth2/__init__.py | 120 ++--- lib/pyItunes/Library.py | 2 +- lib/pyItunes/Song.py | 2 +- lib/pyItunes/XMLLibraryParser.py | 2 +- lib/pygazelle/api.py | 2 +- lib/pygazelle/inbox.py | 4 +- lib/pynma/__init__.py | 2 +- lib/pynma/pynma.py | 8 +- lib/requests/cookies.py | 2 +- .../packages/chardet/charsetgroupprober.py | 8 +- lib/requests/packages/chardet/constants.py | 4 +- lib/requests/packages/chardet/euckrfreq.py | 10 +- lib/requests/packages/chardet/euctwprober.py | 4 +- lib/requests/packages/chardet/gb2312prober.py | 4 +- lib/unidecode/__init__.py | 2 +- lib/yaml/constructor.py | 2 +- lib/yaml/emitter.py | 2 +- lib/yaml/parser.py | 2 +- lib/yaml/scanner.py | 18 +- 91 files changed, 3139 insertions(+), 3139 deletions(-) diff --git a/headphones/albumswitcher.py b/headphones/albumswitcher.py index e4a442d8..34ab7b99 100644 --- a/headphones/albumswitcher.py +++ b/headphones/albumswitcher.py @@ -39,11 +39,11 @@ def switch(AlbumID, ReleaseID): "ReleaseCountry": newalbumdata['ReleaseCountry'], "ReleaseFormat": newalbumdata['ReleaseFormat'] } - + myDB.upsert("albums", newValueDict, controlValueDict) - + for track in newtrackdata: - + controlValueDict = {"TrackID": track['TrackID'], "AlbumID": AlbumID} @@ -60,23 +60,23 @@ def switch(AlbumID, ReleaseID): "Format": track['Format'], "BitRate": track['BitRate'] } - + myDB.upsert("tracks", newValueDict, controlValueDict) - + # Mark albums as downloaded if they have at least 80% (by default, configurable) of the album total_track_count = len(newtrackdata) have_track_count = len(myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [AlbumID])) - + if oldalbumdata['Status'] == 'Skipped' and ((have_track_count/float(total_track_count)) >= (headphones.ALBUM_COMPLETION_PCT/100.0)): myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID]) - + # Update have track counts on index totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [newalbumdata['ArtistID']])) havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']])) controlValueDict = {"ArtistID": newalbumdata['ArtistID']} - + newValueDict = { "TotalTracks": totaltracks, "HaveTracks": havetracks} - + myDB.upsert("artists", newValueDict, controlValueDict) diff --git a/headphones/api.py b/headphones/api.py index 03888a26..87f20ae0 100644 --- a/headphones/api.py +++ b/headphones/api.py @@ -21,29 +21,29 @@ import lib.simplejson as simplejson from xml.dom.minidom import Document import copy -cmd_list = [ 'getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs', +cmd_list = [ 'getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs', 'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist', - 'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub', - 'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo', + 'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub', + 'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb', 'choose_specific_download', 'download_specific_release'] class Api(object): def __init__(self): - + self.apikey = None self.cmd = None self.id = None - + self.kwargs = None self.data = None self.callback = None - + def checkParams(self,*args,**kwargs): - + if not headphones.API_ENABLED: self.data = 'API not enabled' return @@ -53,32 +53,32 @@ class Api(object): if len(headphones.API_KEY) != 32: self.data = 'API key not generated correctly' return - + if 'apikey' not in kwargs: self.data = 'Missing api key' return - + if kwargs['apikey'] != headphones.API_KEY: self.data = 'Incorrect API key' return else: self.apikey = kwargs.pop('apikey') - + if 'cmd' not in kwargs: self.data = 'Missing parameter: cmd' return - + if kwargs['cmd'] not in cmd_list: self.data = 'Unknown command: %s' % kwargs['cmd'] return else: self.cmd = kwargs.pop('cmd') - + self.kwargs = kwargs self.data = 'OK' def fetchData(self): - + if self.data == 'OK': logger.info('Recieved API command: %s', self.cmd) methodToCall = getattr(self, "_" + self.cmd) @@ -95,74 +95,74 @@ class Api(object): return self.data else: return self.data - + def _dic_from_query(self,query): - + myDB = db.DBConnection() rows = myDB.select(query) - + rows_as_dic = [] - + for row in rows: row_as_dic = dict(zip(row.keys(), row)) rows_as_dic.append(row_as_dic) - + return rows_as_dic - + def _getIndex(self, **kwargs): - + self.data = self._dic_from_query('SELECT * from artists order by ArtistSortName COLLATE NOCASE') - return - + return + def _getArtist(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + artist = self._dic_from_query('SELECT * from artists WHERE ArtistID="' + self.id + '"') albums = self._dic_from_query('SELECT * from albums WHERE ArtistID="' + self.id + '" order by ReleaseDate DESC') description = self._dic_from_query('SELECT * from descriptions WHERE ArtistID="' + self.id + '"') - + self.data = { 'artist': artist, 'albums': albums, 'description' : description } return - + def _getAlbum(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + album = self._dic_from_query('SELECT * from albums WHERE AlbumID="' + self.id + '"') tracks = self._dic_from_query('SELECT * from tracks WHERE AlbumID="' + self.id + '"') description = self._dic_from_query('SELECT * from descriptions WHERE ReleaseGroupID="' + self.id + '"') - + self.data = { 'album' : album, 'tracks' : tracks, 'description' : description } return - + def _getHistory(self, **kwargs): self.data = self._dic_from_query('SELECT * from snatched order by DateAdded DESC') return - + def _getUpcoming(self, **kwargs): self.data = self._dic_from_query("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate DESC") return - + def _getWanted(self, **kwargs): self.data = self._dic_from_query("SELECT * from albums WHERE Status='Wanted'") return - + def _getSimilar(self, **kwargs): self.data = self._dic_from_query('SELECT * from lastfmcloud') return - + def _getLogs(self, **kwargs): pass - + def _findArtist(self, **kwargs): if 'name' not in kwargs: self.data = 'Missing parameter: name' @@ -171,7 +171,7 @@ class Api(object): limit = kwargs['limit'] else: limit=50 - + self.data = mb.findArtist(kwargs['name'], limit) def _findAlbum(self, **kwargs): @@ -182,216 +182,216 @@ class Api(object): limit = kwargs['limit'] else: limit=50 - + self.data = mb.findRelease(kwargs['name'], limit) - + def _addArtist(self, **kwargs): if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + try: importer.addArtisttoDB(self.id) except Exception, e: self.data = e - + return - + def _delArtist(self, **kwargs): if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + myDB = db.DBConnection() myDB.action('DELETE from artists WHERE ArtistID="' + self.id + '"') myDB.action('DELETE from albums WHERE ArtistID="' + self.id + '"') myDB.action('DELETE from tracks WHERE ArtistID="' + self.id + '"') - + def _pauseArtist(self, **kwargs): if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + myDB = db.DBConnection() controlValueDict = {'ArtistID': self.id} newValueDict = {'Status': 'Paused'} myDB.upsert("artists", newValueDict, controlValueDict) - + def _resumeArtist(self, **kwargs): if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + myDB = db.DBConnection() controlValueDict = {'ArtistID': self.id} newValueDict = {'Status': 'Active'} myDB.upsert("artists", newValueDict, controlValueDict) - + def _refreshArtist(self, **kwargs): if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + try: importer.addArtisttoDB(self.id) except Exception, e: self.data = e - + return - + def _addAlbum(self, **kwargs): if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + try: importer.addReleaseById(self.id) except Exception, e: self.data = e - + return - + def _queueAlbum(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + if 'new' in kwargs: new = kwargs['new'] else: new = False - + if 'lossless' in kwargs: lossless = kwargs['lossless'] else: lossless = False - + myDB = db.DBConnection() controlValueDict = {'AlbumID': self.id} if lossless: newValueDict = {'Status': 'Wanted Lossless'} - else: + else: newValueDict = {'Status': 'Wanted'} myDB.upsert("albums", newValueDict, controlValueDict) - searcher.searchforalbum(self.id, new) - + searcher.searchforalbum(self.id, new) + def _unqueueAlbum(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + myDB = db.DBConnection() controlValueDict = {'AlbumID': self.id} newValueDict = {'Status': 'Skipped'} myDB.upsert("albums", newValueDict, controlValueDict) - + def _forceSearch(self, **kwargs): searcher.searchforalbum() - + def _forceProcess(self, **kwargs): self.dir = None if 'dir' in kwargs: self.dir = kwargs['dir'] postprocessor.forcePostProcess(self.dir) - + def _getVersion(self, **kwargs): - self.data = { + self.data = { 'git_path' : headphones.GIT_PATH, 'install_type' : headphones.INSTALL_TYPE, 'current_version' : headphones.CURRENT_VERSION, 'latest_version' : headphones.LATEST_VERSION, 'commits_behind' : headphones.COMMITS_BEHIND, } - + def _checkGithub(self, **kwargs): versioncheck.checkGithub() self._getVersion() - + def _shutdown(self, **kwargs): headphones.SIGNAL = 'shutdown' - + def _restart(self, **kwargs): headphones.SIGNAL = 'restart' - + def _update(self, **kwargs): headphones.SIGNAL = 'update' - + def _getArtistArt(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + self.data = cache.getArtwork(ArtistID=self.id) def _getAlbumArt(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + self.data = cache.getArtwork(AlbumID=self.id) - + def _getArtistInfo(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + self.data = cache.getInfo(ArtistID=self.id) - + def _getAlbumInfo(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + self.data = cache.getInfo(AlbumID=self.id) - + def _getArtistThumb(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + self.data = cache.getThumb(ArtistID=self.id) def _getAlbumThumb(self, **kwargs): - + if 'id' not in kwargs: self.data = 'Missing parameter: id' return else: self.id = kwargs['id'] - + self.data = cache.getThumb(AlbumID=self.id) def _choose_specific_download(self, **kwargs): @@ -403,9 +403,9 @@ class Api(object): self.id = kwargs['id'] results = searcher.searchforalbum(self.id, choose_specific_download=True) - + results_as_dicts = [] - + for result in results: result_dict = { diff --git a/headphones/db.py b/headphones/db.py index 7068ba0e..576d2533 100644 --- a/headphones/db.py +++ b/headphones/db.py @@ -31,7 +31,7 @@ from headphones import logger def dbFilename(filename="headphones.db"): return os.path.join(headphones.DATA_DIR, filename) - + def getCacheSize(): #this will protect against typecasting problems produced by empty string and None settings if not headphones.CACHE_SIZEMB: @@ -42,25 +42,25 @@ def getCacheSize(): class DBConnection: def __init__(self, filename="headphones.db"): - + self.filename = filename self.connection = sqlite3.connect(dbFilename(filename), timeout=20) #don't wait for the disk to finish writing self.connection.execute("PRAGMA synchronous = OFF") #journal disabled since we never do rollbacks - self.connection.execute("PRAGMA journal_mode = %s" % headphones.JOURNAL_MODE) + self.connection.execute("PRAGMA journal_mode = %s" % headphones.JOURNAL_MODE) #64mb of cache memory,probably need to make it user configurable self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize()*1024)) self.connection.row_factory = sqlite3.Row - + def action(self, query, args=None): if query == None: return - + sqlResult = None attempt = 0 - + while attempt < 5: try: if args == None: @@ -82,28 +82,28 @@ class DBConnection: except sqlite3.DatabaseError, e: logger.error('Fatal Error executing %s :: %s', query, e) raise - + return sqlResult - + def select(self, query, args=None): - + sqlResults = self.action(query, args).fetchall() - + if sqlResults == None: return [] - + return sqlResults - + def upsert(self, tableName, valueDict, keyDict): - + changesBefore = self.connection.total_changes - + genParams = lambda myDict : [x + " = ?" for x in myDict.keys()] - + query = "UPDATE "+tableName+" SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict)) - + self.action(query, valueDict.values() + keyDict.values()) - + if self.connection.total_changes == changesBefore: query = "INSERT INTO "+tableName+" (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + \ " VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")" diff --git a/headphones/getXldProfile.py b/headphones/getXldProfile.py index 6c744a87..e083d1b6 100755 --- a/headphones/getXldProfile.py +++ b/headphones/getXldProfile.py @@ -8,7 +8,7 @@ from headphones import logger def getXldProfile(xldProfile): xldProfileNotFound = xldProfile expandedPath = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist') - try: + try: preferences = plistlib.Plist.fromFile(expandedPath) except (expat.ExpatError): os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath ) @@ -61,7 +61,7 @@ def getXldProfile(xldProfile): elif 'TVBR' in ShortDesc: XLDAacOutput2_VBRQuality = int(profile.get('XLDAacOutput2_VBRQuality')) if XLDAacOutput2_VBRQuality > 122: - xldBitrate = 320 + xldBitrate = 320 elif XLDAacOutput2_VBRQuality > 113 and XLDAacOutput2_VBRQuality <= 122: xldBitrate = 285 elif XLDAacOutput2_VBRQuality > 104 and XLDAacOutput2_VBRQuality <= 113: diff --git a/headphones/importer.py b/headphones/importer.py index 36eecaf5..a94bf35c 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -226,27 +226,27 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): skip_log = 0 #Make a user configurable variable to skip update of albums with release dates older than this date (in days) pause_delta = headphones.MB_IGNORE_AGE - + rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone() if not forcefull: - + new_release_group = False - + try: check_release_date = rg_exists['ReleaseDate'] except TypeError: check_release_date = None new_release_group = True - - + + if new_release_group: - + logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title'])) new_releases = mb.get_new_releases(rgid,includeExtras) - + else: - + if check_release_date is None or check_release_date == u"None": logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title'])) new_releases = mb.get_new_releases(rgid,includeExtras,True) @@ -384,7 +384,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): # If there's no release in the main albums tables, add the default (hybrid) # If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated) # check if the album already exists - + if not rg_exists: releaseid = rg['id'] else: @@ -410,7 +410,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): if rg_exists: newValueDict['DateAdded'] = rg_exists['DateAdded'] newValueDict['Status'] = rg_exists['Status'] - + else: today = helpers.today() diff --git a/headphones/librarysync.py b/headphones/librarysync.py index 6fe13e17..416023fc 100644 --- a/headphones/librarysync.py +++ b/headphones/librarysync.py @@ -27,18 +27,18 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal if cron and not headphones.LIBRARYSCAN: return - + if not dir: if not headphones.MUSIC_DIR: return else: dir = headphones.MUSIC_DIR - + # If we're appending a dir, it's coming from the post processor which is # already bytestring if not append: dir = dir.encode(headphones.SYS_ENCODING) - + if not os.path.isdir(dir): logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace')) return @@ -47,7 +47,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal new_artists = [] logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) - + if not append: # Clean up bad filepaths tracks = myDB.select('SELECT Location, TrackID from alltracks WHERE Location IS NOT NULL') @@ -57,7 +57,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal if not os.path.isfile(encoded_track_string): myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']]) myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']]) - + del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have') for track in del_have_tracks: @@ -71,13 +71,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal ###############myDB.action('DELETE from have') bitrates = [] - + song_list = [] new_song_count = 0 file_count = 0 latest_subdirectory = [] - + for r,d,f in os.walk(dir): #need to abuse slicing to get a copy of the list, doing it directly will skip the element after a deleted one #using a list comprehension will not work correctly for nested subdirectories (os.walk keeps its original list) @@ -108,11 +108,11 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal except: logger.error('Cannot read file: ' + unicode_song_path) continue - + # Grab the bitrates for the auto detect bit rate option if f.bitrate: bitrates.append(f.bitrate) - + # Use the album artist over the artist if available if f.albumartist: f_artist = f.albumartist @@ -120,8 +120,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal f_artist = f.artist else: f_artist = None - - # Add the song to our song list - + + # Add the song to our song list - # TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements) if f_artist and f.album and f.title: @@ -144,7 +144,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal 'Format' : f.format, 'CleanName' : CleanName } - + #song_list.append(song_dict) check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?", [unicode_song_path]).fetchone() #Only attempt to match songs that are new, haven't yet been matched, or metadata has changed. @@ -182,17 +182,17 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]) total_number_of_songs = myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]).fetchone()[0] logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....") - + # Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid) # When we insert into the database, the tracks with the most specific information will overwrite the more general matches - + ##############song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID']) song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle']) - + # We'll use this to give a % completion, just because the track matching might take a while song_count = 0 latest_artist = [] - + for song in song_list: latest_artist.append(song['ArtistName']) @@ -200,26 +200,26 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal logger.info("Now matching songs by %s" % song['ArtistName']) elif latest_artist[song_count] != latest_artist[song_count-1] and song_count !=0: logger.info("Now matching songs by %s" % song['ArtistName']) - + #print song['ArtistName']+' - '+song['AlbumTitle']+' - '+song['TrackTitle'] song_count += 1 completion_percentage = float(song_count)/total_number_of_songs * 100 - + if completion_percentage%10 == 0: logger.info("Track matching is " + str(completion_percentage) + "% complete") - + #THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT #EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION, #ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM #(IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT. if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']: - + track = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone() if track: controlValueDict = { 'ArtistName' : track['ArtistName'], 'AlbumTitle' : track['AlbumTitle'], - 'TrackTitle' : track['TrackTitle'] } + 'TrackTitle' : track['TrackTitle'] } newValueDict = { 'Location' : song['Location'], 'BitRate' : song['BitRate'], 'Format' : song['Format'] } @@ -231,10 +231,10 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal else: track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() if track: - controlValueDict = { 'CleanName' : track['CleanName']} + controlValueDict = { 'CleanName' : track['CleanName']} newValueDict = { 'Location' : song['Location'], 'BitRate' : song['BitRate'], - 'Format' : song['Format'] } + 'Format' : song['Format'] } myDB.upsert("tracks", newValueDict, controlValueDict) controlValueDict2 = { 'Location' : song['Location']} @@ -244,9 +244,9 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal controlValueDict2 = { 'Location' : song['Location']} newValueDict2 = { 'Matched' : "Failed"} myDB.upsert("have", newValueDict2, controlValueDict2) - - alltrack = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone() + + alltrack = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone() if alltrack: controlValueDict = { 'ArtistName' : alltrack['ArtistName'], 'AlbumTitle' : alltrack['AlbumTitle'], @@ -262,10 +262,10 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal else: alltrack = myDB.action('SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() if alltrack: - controlValueDict = { 'CleanName' : alltrack['CleanName']} + controlValueDict = { 'CleanName' : alltrack['CleanName']} newValueDict = { 'Location' : song['Location'], 'BitRate' : song['BitRate'], - 'Format' : song['Format'] } + 'Format' : song['Format'] } myDB.upsert("alltracks", newValueDict, controlValueDict) controlValueDict2 = { 'Location' : song['Location']} @@ -279,35 +279,35 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal controlValueDict2 = { 'Location' : song['Location']} newValueDict2 = { 'Matched' : "Failed"} myDB.upsert("have", newValueDict2, controlValueDict2) - + #######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']]) logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) - + if not append: logger.info('Updating scanned artist track counts') - + # Clean up the new artist list unique_artists = {}.fromkeys(new_artists).keys() current_artists = myDB.select('SELECT ArtistName, ArtistID from artists') - #There was a bug where artists with special characters (-,') would show up in new artists. + #There was a bug where artists with special characters (-,') would show up in new artists. artist_list = [f for f in unique_artists if helpers.cleanName(f).lower() not in [helpers.cleanName(x[0]).lower() for x in current_artists]] artists_checked = [f for f in unique_artists if helpers.cleanName(f).lower() in [helpers.cleanName(x[0]).lower() for x in current_artists]] # Update track counts - + for artist in artists_checked: # Have tracks are selected from tracks table and not all tracks because of duplicates # We update the track count upon an album switch to compliment this havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist])) - #Note, some people complain about having "artist have tracks" > # of tracks total in artist official releases + #Note, some people complain about having "artist have tracks" > # of tracks total in artist official releases # (can fix by getting rid of second len statement) myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist]) - + logger.info('Found %i new artists' % len(artist_list)) - + if len(artist_list): if headphones.ADD_ARTISTS: logger.info('Importing %i new artists' % len(artist_list)) @@ -317,14 +317,14 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal #myDB.action('DELETE from newartists') for artist in artist_list: myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist]) - + if headphones.DETECT_BITRATE: headphones.PREFERRED_BITRATE = sum(bitrates)/len(bitrates)/1000 - + else: # If we're appending a new album to the database, update the artists total track counts logger.info('Updating artist track counts') - + havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName])) myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID]) @@ -357,7 +357,7 @@ def update_album_status(AlbumID=None): if album_completion >= headphones.ALBUM_COMPLETION_PCT and album['Status'] == 'Skipped': new_album_status = "Downloaded" - + # I don't think we want to change Downloaded->Skipped..... # I think we can only automatically change Skipped->Downloaded when updating # There was a bug report where this was causing infinite downloads if the album was @@ -369,7 +369,7 @@ def update_album_status(AlbumID=None): # new_album_status = album['Status'] else: new_album_status = album['Status'] - + myDB.upsert("albums", {'Status' : new_album_status}, {'AlbumID' : album['AlbumID']}) if new_album_status != album['Status']: logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status)) diff --git a/headphones/lyrics.py b/headphones/lyrics.py index aa3c952a..ea8458ee 100644 --- a/headphones/lyrics.py +++ b/headphones/lyrics.py @@ -27,26 +27,26 @@ def getLyrics(artist, song): url = 'http://lyrics.wikia.com/api.php' data = request.request_minidom(url, params=params) - + if not data: return - + url = data.getElementsByTagName("url") - + if url: lyricsurl = url[0].firstChild.nodeValue else: logger.info('No lyrics found for %s - %s' % (artist, song)) return - + lyricspage = request.request_content(lyricsurl) - + if not lyricspage: logger.warn('Error fetching lyrics from: %s' % lyricsurl) return m = re.compile('''
.*?
(.*?) # Reconstruct the original comment. self.pieces.append('' % locals()) - + def handle_pi(self, text): # called for each processing instruction, e.g. # Reconstruct original processing instruction. @@ -1942,7 +1942,7 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser): # "http://www.w3.org/TR/html4/loose.dtd"> # Reconstruct original DOCTYPE self.pieces.append('' % locals()) - + _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match def _scan_name(self, i, declstartpos): rawdata = self.rawdata @@ -2006,7 +2006,7 @@ class _LooseFeedParser(_FeedParserMixin, _BaseHTMLProcessor): data = data.replace('"', '"') data = data.replace(''', "'") return data - + def strattrs(self, attrs): return ''.join([' %s="%s"' % (n,v.replace('"','"')) for n,v in attrs]) @@ -2030,12 +2030,12 @@ class _MicroformatsParser: self.enclosures = [] self.xfn = [] self.vcard = None - + def vcardEscape(self, s): if type(s) in (type(''), type(u'')): s = s.replace(',', '\\,').replace(';', '\\;').replace('\n', '\\n') return s - + def vcardFold(self, s): s = re.sub(';+$', '', s) sFolded = '' @@ -2051,14 +2051,14 @@ class _MicroformatsParser: def normalize(self, s): return re.sub(r'\s+', ' ', s).strip() - + def unique(self, aList): results = [] for element in aList: if element not in results: results.append(element) return results - + def toISO8601(self, dt): return time.strftime('%Y-%m-%dT%H:%M:%SZ', dt) @@ -2148,21 +2148,21 @@ class _MicroformatsParser: def findVCards(self, elmRoot, bAgentParsing=0): sVCards = '' - + if not bAgentParsing: arCards = self.getPropertyValue(elmRoot, 'vcard', bAllowMultiple=1) else: arCards = [elmRoot] - + for elmCard in arCards: arLines = [] - + def processSingleString(sProperty): sValue = self.getPropertyValue(elmCard, sProperty, self.STRING, bAutoEscape=1).decode(self.encoding) if sValue: arLines.append(self.vcardFold(sProperty.upper() + ':' + sValue)) return sValue or u'' - + def processSingleURI(sProperty): sValue = self.getPropertyValue(elmCard, sProperty, self.URI) if sValue: @@ -2185,7 +2185,7 @@ class _MicroformatsParser: if sContentType: sContentType = ';TYPE=' + sContentType.upper() arLines.append(self.vcardFold(sProperty.upper() + sEncoding + sContentType + sValueKey + ':' + sValue)) - + def processTypeValue(sProperty, arDefaultType, arForceType=None): arResults = self.getPropertyValue(elmCard, sProperty, bAllowMultiple=1) for elmResult in arResults: @@ -2197,7 +2197,7 @@ class _MicroformatsParser: sValue = self.getPropertyValue(elmResult, 'value', self.EMAIL, 0) if sValue: arLines.append(self.vcardFold(sProperty.upper() + ';TYPE=' + ','.join(arType) + ':' + sValue)) - + # AGENT # must do this before all other properties because it is destructive # (removes nested class="vcard" nodes so they don't interfere with @@ -2216,10 +2216,10 @@ class _MicroformatsParser: sAgentValue = self.getPropertyValue(elmAgent, 'value', self.URI, bAutoEscape=1); if sAgentValue: arLines.append(self.vcardFold('AGENT;VALUE=uri:' + sAgentValue)) - + # FN (full name) sFN = processSingleString('fn') - + # N (name) elmName = self.getPropertyValue(elmCard, 'n') if elmName: @@ -2228,7 +2228,7 @@ class _MicroformatsParser: arAdditionalNames = self.getPropertyValue(elmName, 'additional-name', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'additional-names', self.STRING, 1, 1) arHonorificPrefixes = self.getPropertyValue(elmName, 'honorific-prefix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-prefixes', self.STRING, 1, 1) arHonorificSuffixes = self.getPropertyValue(elmName, 'honorific-suffix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-suffixes', self.STRING, 1, 1) - arLines.append(self.vcardFold('N:' + sFamilyName + ';' + + arLines.append(self.vcardFold('N:' + sFamilyName + ';' + sGivenName + ';' + ','.join(arAdditionalNames) + ';' + ','.join(arHonorificPrefixes) + ';' + @@ -2245,25 +2245,25 @@ class _MicroformatsParser: arLines.append(self.vcardFold('N:' + arNames[0] + ';' + arNames[1])) else: arLines.append(self.vcardFold('N:' + arNames[1] + ';' + arNames[0])) - + # SORT-STRING sSortString = self.getPropertyValue(elmCard, 'sort-string', self.STRING, bAutoEscape=1) if sSortString: arLines.append(self.vcardFold('SORT-STRING:' + sSortString)) - + # NICKNAME arNickname = self.getPropertyValue(elmCard, 'nickname', self.STRING, 1, 1) if arNickname: arLines.append(self.vcardFold('NICKNAME:' + ','.join(arNickname))) - + # PHOTO processSingleURI('photo') - + # BDAY dtBday = self.getPropertyValue(elmCard, 'bday', self.DATE) if dtBday: arLines.append(self.vcardFold('BDAY:' + self.toISO8601(dtBday))) - + # ADR (address) arAdr = self.getPropertyValue(elmCard, 'adr', bAllowMultiple=1) for elmAdr in arAdr: @@ -2285,38 +2285,38 @@ class _MicroformatsParser: sRegion + ';' + sPostalCode + ';' + sCountryName)) - + # LABEL processTypeValue('label', ['intl','postal','parcel','work']) - + # TEL (phone number) processTypeValue('tel', ['voice']) - + # EMAIL processTypeValue('email', ['internet'], ['internet']) - + # MAILER processSingleString('mailer') - + # TZ (timezone) processSingleString('tz') - + # GEO (geographical information) elmGeo = self.getPropertyValue(elmCard, 'geo') if elmGeo: sLatitude = self.getPropertyValue(elmGeo, 'latitude', self.STRING, 0, 1) sLongitude = self.getPropertyValue(elmGeo, 'longitude', self.STRING, 0, 1) arLines.append(self.vcardFold('GEO:' + sLatitude + ';' + sLongitude)) - + # TITLE processSingleString('title') - + # ROLE processSingleString('role') # LOGO processSingleURI('logo') - + # ORG (organization) elmOrg = self.getPropertyValue(elmCard, 'org') if elmOrg: @@ -2330,39 +2330,39 @@ class _MicroformatsParser: else: arOrganizationUnit = self.getPropertyValue(elmOrg, 'organization-unit', self.STRING, 1, 1) arLines.append(self.vcardFold('ORG:' + sOrganizationName + ';' + ';'.join(arOrganizationUnit))) - + # CATEGORY arCategory = self.getPropertyValue(elmCard, 'category', self.STRING, 1, 1) + self.getPropertyValue(elmCard, 'categories', self.STRING, 1, 1) if arCategory: arLines.append(self.vcardFold('CATEGORIES:' + ','.join(arCategory))) - + # NOTE processSingleString('note') - + # REV processSingleString('rev') - + # SOUND processSingleURI('sound') - + # UID processSingleString('uid') - + # URL processSingleURI('url') - + # CLASS processSingleString('class') - + # KEY processSingleURI('key') - + if arLines: arLines = [u'BEGIN:vCard',u'VERSION:3.0'] + arLines + [u'END:vCard'] sVCards += u'\n'.join(arLines) + u'\n' - + return sVCards.strip() - + def isProbablyDownloadable(self, elm): attrsD = elm.attrMap if not attrsD.has_key('href'): return 0 @@ -2461,7 +2461,7 @@ class _RelativeURIResolver(_BaseHTMLProcessor): def resolveURI(self, uri): return _makeSafeAbsoluteURI(_urljoin(self.baseuri, uri.strip())) - + def unknown_starttag(self, tag, attrs): if _debug: sys.stderr.write('tag: [%s] with attributes: [%s]\n' % (tag, str(attrs))) @@ -2575,7 +2575,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor): # svgtiny - foreignObject + linearGradient + radialGradient + stop svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', 'animateTransform', 'circle', 'defs', 'desc', 'ellipse', 'foreignObject', - 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', + 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] @@ -2621,7 +2621,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor): self.unacceptablestack = 0 self.mathmlOK = 0 self.svgOK = 0 - + def unknown_starttag(self, tag, attrs): acceptable_attributes = self.acceptable_attributes keymap = {} @@ -2683,7 +2683,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor): clean_value = self.sanitize_style(value) if clean_value: clean_attrs.append((key,clean_value)) _BaseHTMLProcessor.unknown_starttag(self, tag, clean_attrs) - + def unknown_endtag(self, tag): if not tag in self.acceptable_elements: if tag in self.unacceptable_elements_with_end_tag: @@ -2815,7 +2815,7 @@ class _FeedURLHandler(urllib2.HTTPDigestAuthHandler, urllib2.HTTPRedirectHandler http_error_300 = http_error_302 http_error_303 = http_error_302 http_error_307 = http_error_302 - + def http_error_401(self, req, fp, code, msg, headers): # Check if # - server requires digest auth, AND @@ -2914,7 +2914,7 @@ def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, h return opener.open(request, timeout=15) finally: opener.close() # JohnD - + # try to open with native open function (if url_file_stream_or_string is a filename) try: return open(url_file_stream_or_string, 'rb') @@ -2966,7 +2966,7 @@ _date_handlers = [] def registerDateHandler(func): '''Register a date handler function (takes string, returns 9-tuple date in GMT)''' _date_handlers.insert(0, func) - + # ISO-8601 date parsing routines written by Fazal Majid. # The ISO 8601 standard is very convoluted and irregular - a full ISO 8601 # parser is beyond the scope of feedparser and would be a worthwhile addition @@ -2977,7 +2977,7 @@ def registerDateHandler(func): # Please note the order in templates is significant because we need a # greedy match. _iso8601_tmpl = ['YYYY-?MM-?DD', 'YYYY-0MM?-?DD', 'YYYY-MM', 'YYYY-?OOO', - 'YY-?MM-?DD', 'YY-?OOO', 'YYYY', + 'YY-?MM-?DD', 'YY-?OOO', 'YYYY', '-YY-?MM', '-OOO', '-YY', '--MM-?DD', '--MM', '---DD', @@ -3079,7 +3079,7 @@ def _parse_date_iso8601(dateString): # Many implementations have bugs, but we'll pretend they don't. return time.localtime(time.mktime(tuple(tm))) registerDateHandler(_parse_date_iso8601) - + # 8-bit date handling routines written by ytrewq1. _korean_year = u'\ub144' # b3e2 in euc-kr _korean_month = u'\uc6d4' # bff9 in euc-kr @@ -3170,7 +3170,7 @@ _greek_wdays = \ u'\u03a4\u03b5\u03c4': u'Wed', # d4e5f4 in iso-8859-7 u'\u03a0\u03b5\u03bc': u'Thu', # d0e5ec in iso-8859-7 u'\u03a0\u03b1\u03c1': u'Fri', # d0e1f1 in iso-8859-7 - u'\u03a3\u03b1\u03b2': u'Sat', # d3e1e2 in iso-8859-7 + u'\u03a3\u03b1\u03b2': u'Sat', # d3e1e2 in iso-8859-7 } _greek_date_format_re = \ @@ -3360,7 +3360,7 @@ def _parse_date_rfc822(dateString): # 'ET' is equivalent to 'EST', etc. _additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800} rfc822._timezones.update(_additional_timezones) -registerDateHandler(_parse_date_rfc822) +registerDateHandler(_parse_date_rfc822) def _parse_date_perforce(aDateString): """parse a date in yyyy/mm/dd hh:mm:ss TTT format""" @@ -3398,7 +3398,7 @@ def _getCharacterEncoding(http_headers, xml_data): http_headers is a dictionary xml_data is a raw string (not Unicode) - + This is so much trickier than it sounds, it's not even funny. According to RFC 3023 ('XML Media Types'), if the HTTP Content-Type is application/xml, application/*+xml, @@ -3417,12 +3417,12 @@ def _getCharacterEncoding(http_headers, xml_data): served with a Content-Type of text/* and no charset parameter must be treated as us-ascii. (We now do this.) And also that it must always be flagged as non-well-formed. (We now do this too.) - + If Content-Type is unspecified (input was local file or non-HTTP source) or unrecognized (server just got it totally wrong), then go by the encoding given in the XML prefix of the document and default to 'iso-8859-1' as per the HTTP specification (RFC 2616). - + Then, assuming we didn't find a character encoding in the HTTP headers (and the HTTP Content-type allowed us to look in the body), we need to sniff the first few bytes of the XML data and try to determine @@ -3532,7 +3532,7 @@ def _getCharacterEncoding(http_headers, xml_data): if true_encoding.lower() == 'gb2312': true_encoding = 'gb18030' return true_encoding, http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type - + def _toUTF8(data, encoding): '''Changes an XML data stream on the fly to specify a new encoding @@ -3595,7 +3595,7 @@ def _stripDoctype(data): start = re.search(_s2bytes('<\w'), data) start = start and start.start() or -1 head,data = data[:start+1], data[start+1:] - + entity_pattern = re.compile(_s2bytes(r'^\s*]*?)>'), re.MULTILINE) entity_results=entity_pattern.findall(head) head = entity_pattern.sub(_s2bytes(''), head) @@ -3617,10 +3617,10 @@ def _stripDoctype(data): data = doctype_pattern.sub(replacement, head) + data return version, data, dict(replacement and [(k.decode('utf-8'), v.decode('utf-8')) for k, v in safe_pattern.findall(replacement)]) - + def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, referrer=None, handlers=[], request_headers={}, response_headers={}): '''Parse a feed from a URL, file, stream, or string. - + request_headers, if given, is a dict from http header name to value to add to the request; this overrides internally generated values. ''' @@ -3861,7 +3861,7 @@ class TextSerializer(Serializer): stream.write('\n') except: pass - + class PprintSerializer(Serializer): def write(self, stream=sys.stdout): if self.results.has_key('href'): @@ -3869,7 +3869,7 @@ class PprintSerializer(Serializer): from pprint import pprint pprint(self.results, stream) stream.write('\n') - + if __name__ == '__main__': try: from optparse import OptionParser diff --git a/lib/gntp/core.py b/lib/gntp/core.py index ee544d3d..957b5270 100644 --- a/lib/gntp/core.py +++ b/lib/gntp/core.py @@ -70,7 +70,7 @@ class _GNTPBase(object): 'SHA1': hashlib.sha1, 'SHA256': hashlib.sha256, 'SHA512': hashlib.sha512, - } + } self.headers = {} self.resources = {} diff --git a/lib/httplib2/__init__.py b/lib/httplib2/__init__.py index 01151f7f..58b0de02 100755 --- a/lib/httplib2/__init__.py +++ b/lib/httplib2/__init__.py @@ -3,7 +3,7 @@ from __future__ import generators httplib2 A caching http interface that supports ETags and gzip -to conserve bandwidth. +to conserve bandwidth. Requires Python 2.3 or later @@ -24,8 +24,8 @@ __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", __license__ = "MIT" __version__ = "$Rev$" -import re -import sys +import re +import sys import email import email.Utils import email.Message @@ -85,7 +85,7 @@ def has_timeout(timeout): # python 2.6 return (timeout is not None) __all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error', - 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', + 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', 'debuglevel'] @@ -113,8 +113,8 @@ if not hasattr(httplib.HTTPResponse, 'getheaders'): # All exceptions raised here derive from HttpLib2Error class HttpLib2Error(Exception): pass -# Some exceptions can be caught and optionally -# be turned back into responses. +# Some exceptions can be caught and optionally +# be turned back into responses. class HttpLib2ErrorWithResponse(HttpLib2Error): def __init__(self, desc, response, content): self.response = response @@ -176,7 +176,7 @@ def urlnorm(uri): raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) authority = authority.lower() scheme = scheme.lower() - if not path: + if not path: path = "/" # Could do syntax based normalization of the URI before # computing the digest. See Section 6.2.2 of Std 66. @@ -228,7 +228,7 @@ def _parse_cache_control(headers): parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")] retval = dict(parts_with_args + parts_wo_args) - return retval + return retval # Whether to use a strict mode to parse WWW-Authenticate headers # Might lead to bad results in case of ill-formed header value, @@ -254,10 +254,10 @@ def _parse_www_authenticate(headers, headername='www-authenticate'): while authenticate: # Break off the scheme at the beginning of the line if headername == 'authentication-info': - (auth_scheme, the_rest) = ('digest', authenticate) + (auth_scheme, the_rest) = ('digest', authenticate) else: (auth_scheme, the_rest) = authenticate.split(" ", 1) - # Now loop over all the key value pairs that come after the scheme, + # Now loop over all the key value pairs that come after the scheme, # being careful not to roll into the next scheme match = www_auth.search(the_rest) auth_params = {} @@ -279,17 +279,17 @@ def _entry_disposition(response_headers, request_headers): 1. Cache-Control: max-stale 2. Age: headers are not used in the calculations. - Not that this algorithm is simpler than you might think + Not that this algorithm is simpler than you might think because we are operating as a private (non-shared) cache. This lets us ignore 's-maxage'. We can also ignore 'proxy-invalidate' since we aren't a proxy. - We will never return a stale document as - fresh as a design decision, and thus the non-implementation - of 'max-stale'. This also lets us safely ignore 'must-revalidate' + We will never return a stale document as + fresh as a design decision, and thus the non-implementation + of 'max-stale'. This also lets us safely ignore 'must-revalidate' since we operate as if every server has sent 'must-revalidate'. Since we are private we get to ignore both 'public' and 'private' parameters. We also ignore 'no-transform' since - we don't do any transformations. + we don't do any transformations. The 'no-store' parameter is handled at a higher level. So the only Cache-Control parameters we look at are: @@ -298,7 +298,7 @@ def _entry_disposition(response_headers, request_headers): max-age min-fresh """ - + retval = "STALE" cc = _parse_cache_control(request_headers) cc_response = _parse_cache_control(response_headers) @@ -340,10 +340,10 @@ def _entry_disposition(response_headers, request_headers): min_fresh = int(cc['min-fresh']) except ValueError: min_fresh = 0 - current_age += min_fresh + current_age += min_fresh if freshness_lifetime > current_age: retval = "FRESH" - return retval + return retval def _decompressContent(response, new_content): content = new_content @@ -408,10 +408,10 @@ def _wsse_username_token(cnonce, iso_now, password): return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip() -# For credentials we need two things, first +# For credentials we need two things, first # a pool of credential to try (not necesarily tied to BAsic, Digest, etc.) # Then we also need a list of URIs that have already demanded authentication -# That list is tricky since sub-URIs can take the same auth, or the +# That list is tricky since sub-URIs can take the same auth, or the # auth scheme may change as you descend the tree. # So we also need each Auth instance to be able to tell us # how close to the 'top' it is. @@ -443,7 +443,7 @@ class Authentication(object): or such returned from the last authorized response. Over-rise this in sub-classes if necessary. - Return TRUE is the request is to be retried, for + Return TRUE is the request is to be retried, for example Digest may return stale=true. """ return False @@ -461,7 +461,7 @@ class BasicAuthentication(Authentication): class DigestAuthentication(Authentication): - """Only do qop='auth' and MD5, since that + """Only do qop='auth' and MD5, since that is all Apache currently implements""" def __init__(self, credentials, host, request_uri, headers, response, content, http): Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) @@ -474,7 +474,7 @@ class DigestAuthentication(Authentication): self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upper() if self.challenge['algorithm'] != 'MD5': raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm'])) - self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) + self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) self.challenge['nc'] = 1 def request(self, method, request_uri, headers, content, cnonce = None): @@ -482,17 +482,17 @@ class DigestAuthentication(Authentication): H = lambda x: _md5(x).hexdigest() KD = lambda s, d: H("%s:%s" % (s, d)) A2 = "".join([method, ":", request_uri]) - self.challenge['cnonce'] = cnonce or _cnonce() - request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'], - '%08x' % self.challenge['nc'], - self.challenge['cnonce'], + self.challenge['cnonce'] = cnonce or _cnonce() + request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'], + '%08x' % self.challenge['nc'], + self.challenge['cnonce'], self.challenge['qop'], H(A2) - )) + )) headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( - self.credentials[0], + self.credentials[0], self.challenge['realm'], self.challenge['nonce'], - request_uri, + request_uri, self.challenge['algorithm'], request_digest, self.challenge['qop'], @@ -506,14 +506,14 @@ class DigestAuthentication(Authentication): challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {}) if 'true' == challenge.get('stale'): self.challenge['nonce'] = challenge['nonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return True else: updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {}) if updated_challenge.has_key('nextnonce'): self.challenge['nonce'] = updated_challenge['nextnonce'] - self.challenge['nc'] = 1 + self.challenge['nc'] = 1 return False @@ -562,11 +562,11 @@ class HmacDigestAuthentication(Authentication): request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val) request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower() headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( - self.credentials[0], + self.credentials[0], self.challenge['realm'], self.challenge['snonce'], cnonce, - request_uri, + request_uri, created, request_digest, keylist, @@ -583,7 +583,7 @@ class WsseAuthentication(Authentication): """This is thinly tested and should not be relied upon. At this time there isn't any third party server to test against. Blogger and TypePad implemented this algorithm at one point - but Blogger has since switched to Basic over HTTPS and + but Blogger has since switched to Basic over HTTPS and TypePad has implemented it wrong, by never issuing a 401 challenge but instead requiring your client to telepathically know that their endpoint is expecting WSSE profile="UsernameToken".""" @@ -629,7 +629,7 @@ class GoogleLoginAuthentication(Authentication): def request(self, method, request_uri, headers, content): """Modify the request headers to add the appropriate Authorization header.""" - headers['authorization'] = 'GoogleLogin Auth=' + self.Auth + headers['authorization'] = 'GoogleLogin Auth=' + self.Auth AUTH_SCHEME_CLASSES = { @@ -644,13 +644,13 @@ AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] class FileCache(object): """Uses a local directory as a store for cached files. - Not really safe to use if multiple threads or processes are going to + Not really safe to use if multiple threads or processes are going to be running on the same cache. """ def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior self.cache = cache self.safe = safe - if not os.path.exists(cache): + if not os.path.exists(cache): os.makedirs(self.cache) def get(self, key): @@ -688,7 +688,7 @@ class Credentials(object): def iter(self, domain): for (cdomain, name, password) in self.credentials: if cdomain == "" or domain == cdomain: - yield (name, password) + yield (name, password) class KeyCerts(Credentials): """Identical to Credentials except that @@ -772,7 +772,7 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): sock.setproxy(*self.proxy_info.astuple()) else: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - + if has_timeout(self.timeout): sock.settimeout(self.timeout) sock.connect((self.host, self.port)) @@ -820,7 +820,7 @@ the same interface as FileCache.""" # If set to False then no redirects are followed, even safe ones. self.follow_redirects = True - + # Which HTTP methods do we apply optimistic concurrency to, i.e. # which methods get an "if-match:" etag header added to them. self.optimistic_concurrency_methods = ["PUT"] @@ -831,7 +831,7 @@ the same interface as FileCache.""" self.ignore_etag = False - self.force_exception_to_status_code = False + self.force_exception_to_status_code = False self.timeout = timeout @@ -908,12 +908,12 @@ the same interface as FileCache.""" auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] auth = auths and sorted(auths)[0][1] or None - if auth: + if auth: auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers) - if auth: + if auth: if auth.response(response, body): auth.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers ) @@ -921,7 +921,7 @@ the same interface as FileCache.""" if response.status == 401: for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): - authorization.request(method, request_uri, headers, body) + authorization.request(method, request_uri, headers, body) (response, content) = self._conn_request(conn, request_uri, method, body, headers, ) if response.status != 401: self.authorizations.append(authorization) @@ -944,7 +944,7 @@ the same interface as FileCache.""" if response.status == 301 and method in ["GET", "HEAD"]: response['-x-permanent-redirect-url'] = response['location'] if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) if headers.has_key('if-none-match'): del headers['if-none-match'] @@ -954,7 +954,7 @@ the same interface as FileCache.""" location = response['location'] old_response = copy.deepcopy(response) if not old_response.has_key('content-location'): - old_response['content-location'] = absolute_uri + old_response['content-location'] = absolute_uri redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method (response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1) response.previous = old_response @@ -963,7 +963,7 @@ the same interface as FileCache.""" elif response.status in [200, 203] and method == "GET": # Don't cache 206's since we aren't going to handle byte range requests if not response.has_key('content-location'): - response['content-location'] = absolute_uri + response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) return (response, content) @@ -978,10 +978,10 @@ the same interface as FileCache.""" def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None): """ Performs a single HTTP request. -The 'uri' is the URI of the HTTP resource and can begin +The 'uri' is the URI of the HTTP resource and can begin with either 'http' or 'https'. The value of 'uri' must be an absolute URI. -The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc. +The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc. There is no restriction on the methods allowed. The 'body' is the entity body to be sent with the request. It is a string @@ -990,11 +990,11 @@ object. Any extra headers that are to be sent with the request should be provided in the 'headers' dictionary. -The maximum number of redirect to follow before raising an +The maximum number of redirect to follow before raising an exception is 'redirections. The default is 5. -The return value is a tuple of (response, content), the first -being and instance of the 'Response' class, the second being +The return value is a tuple of (response, content), the first +being and instance of the 'Response' class, the second being a string that contains the response entity body. """ try: @@ -1085,13 +1085,13 @@ a string that contains the response entity body. # Determine our course of action: # Is the cached entry fresh or stale? # Has the client requested a non-cached response? - # - # There seems to be three possible answers: + # + # There seems to be three possible answers: # 1. [FRESH] Return the cache entry w/o doing a GET # 2. [STALE] Do the GET (but add in cache validators if available) # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request - entry_disposition = _entry_disposition(info, headers) - + entry_disposition = _entry_disposition(info, headers) + if entry_disposition == "FRESH": if not cached_value: info['status'] = '504' @@ -1113,7 +1113,7 @@ a string that contains the response entity body. if response.status == 304 and method == "GET": # Rewrite the cache entry with the new end-to-end headers - # Take all headers that are in response + # Take all headers that are in response # and overwrite their values in info. # unless they are hop-by-hop, or are listed in the connection header. @@ -1125,14 +1125,14 @@ a string that contains the response entity body. _updateCache(headers, merged_response, content, self.cache, cachekey) response = merged_response response.status = 200 - response.fromcache = True + response.fromcache = True elif response.status == 200: content = new_content else: self.cache.delete(cachekey) - content = new_content - else: + content = new_content + else: cc = _parse_cache_control(headers) if cc.has_key('only-if-cached'): info['status'] = '504' @@ -1146,7 +1146,7 @@ a string that contains the response entity body. response = e.response content = e.content response.status = 500 - response.reason = str(e) + response.reason = str(e) elif isinstance(e, socket.timeout) or (isinstance(e, socket.error) and 'timed out' in str(e)): content = "Request Timeout" response = Response( { @@ -1156,24 +1156,24 @@ a string that contains the response entity body. }) response.reason = "Request Timeout" else: - content = str(e) + content = str(e) response = Response( { "content-type": "text/plain", "status": "400", "content-length": len(content) }) - response.reason = "Bad Request" + response.reason = "Bad Request" else: raise - + return (response, content) - + class Response(dict): """An object more like email.Message than httplib.HTTPResponse.""" - + """Is this response from our local cache""" fromcache = False @@ -1189,27 +1189,27 @@ class Response(dict): previous = None def __init__(self, info): - # info is either an email.Message or + # info is either an email.Message or # an httplib.HTTPResponse object. if isinstance(info, httplib.HTTPResponse): - for key, value in info.getheaders(): - self[key.lower()] = value + for key, value in info.getheaders(): + self[key.lower()] = value self.status = info.status self['status'] = str(self.status) self.reason = info.reason self.version = info.version elif isinstance(info, email.Message.Message): - for key, value in info.items(): - self[key] = value + for key, value in info.items(): + self[key] = value self.status = int(self['status']) else: - for key, value in info.iteritems(): - self[key] = value + for key, value in info.iteritems(): + self[key] = value self.status = int(self.get('status', self.status)) def __getattr__(self, name): if name == 'dict': - return self - else: - raise AttributeError, name + return self + else: + raise AttributeError, name diff --git a/lib/httplib2/iri2uri.py b/lib/httplib2/iri2uri.py index 70667edf..e4cda1d7 100755 --- a/lib/httplib2/iri2uri.py +++ b/lib/httplib2/iri2uri.py @@ -16,7 +16,7 @@ import urlparse # Convert an IRI to a URI following the rules in RFC 3987 -# +# # The characters we need to enocde and escape are defined in the spec: # # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD @@ -49,7 +49,7 @@ escape_range = [ (0xF0000, 0xFFFFD ), (0x100000, 0x10FFFD) ] - + def encode(c): retval = c i = ord(c) @@ -63,19 +63,19 @@ def encode(c): def iri2uri(uri): - """Convert an IRI to a URI. Note that IRIs must be + """Convert an IRI to a URI. Note that IRIs must be passed in a unicode strings. That is, do not utf-8 encode - the IRI before passing it into the function.""" + the IRI before passing it into the function.""" if isinstance(uri ,unicode): (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) authority = authority.encode('idna') # For each character in 'ucschar' or 'iprivate' # 1. encode as utf-8 - # 2. then %-encode each octet of that utf-8 + # 2. then %-encode each octet of that utf-8 uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) uri = "".join([encode(c) for c in uri]) return uri - + if __name__ == "__main__": import unittest @@ -83,7 +83,7 @@ if __name__ == "__main__": def test_uris(self): """Test that URIs are invariant under the transformation.""" - invariant = [ + invariant = [ u"ftp://ftp.is.co.za/rfc/rfc1808.txt", u"http://www.ietf.org/rfc/rfc2396.txt", u"ldap://[2001:db8::7]/c=GB?objectClass?one", @@ -94,7 +94,7 @@ if __name__ == "__main__": u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] for uri in invariant: self.assertEqual(uri, iri2uri(uri)) - + def test_iri(self): """ Test that the right type of escaping is done for each part of the URI.""" self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) @@ -107,4 +107,4 @@ if __name__ == "__main__": unittest.main() - + diff --git a/lib/mako/ext/autohandler.py b/lib/mako/ext/autohandler.py index d56cbc1e..7bd1c71f 100644 --- a/lib/mako/ext/autohandler.py +++ b/lib/mako/ext/autohandler.py @@ -48,7 +48,7 @@ def autohandler(template, context, name='autohandler'): if len(tokens) == 1: break tokens[-2:] = [name] - + if not lookup.filesystem_checks: return lookup._uri_cache.setdefault( (autohandler, _template_uri, name), None) @@ -62,4 +62,4 @@ def _file_exists(lookup, path): return True else: return False - + diff --git a/lib/mako/ext/preprocessors.py b/lib/mako/ext/preprocessors.py index 5a15ff35..acd32e55 100644 --- a/lib/mako/ext/preprocessors.py +++ b/lib/mako/ext/preprocessors.py @@ -4,16 +4,16 @@ # This module is part of Mako and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""preprocessing functions, used with the 'preprocessor' +"""preprocessing functions, used with the 'preprocessor' argument on Template, TemplateLookup""" import re def convert_comments(text): """preprocess old style comments. - + example: - + from mako.ext.preprocessors import convert_comments t = Template(..., preprocessor=preprocess_comments)""" return re.sub(r'(?<=\n)\s*#[^#]', "##", text) diff --git a/lib/musicbrainzngs/musicbrainz.py b/lib/musicbrainzngs/musicbrainz.py index 65454834..16492087 100644 --- a/lib/musicbrainzngs/musicbrainz.py +++ b/lib/musicbrainzngs/musicbrainz.py @@ -279,7 +279,7 @@ def auth(u, p): global user, password user = u password = p - + def hpauth(u, p): """Set the username and password to be used in subsequent queries to the MusicBrainz XML API that require authentication. @@ -574,7 +574,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False, whether exceptions should be raised if the client and username/password are left unspecified, respectively. """ - global parser_fun + global parser_fun if args is None: args = {} @@ -638,7 +638,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False, if hostname == '144.76.94.239:8181': base64string = base64.encodestring('%s:%s' % (hpuser, hppassword)).replace('\n', '') req.add_header("Authorization", "Basic %s" % base64string) - + _log.debug("requesting with UA %s" % _useragent) if body: req.add_header('Content-Type', 'application/xml; charset=UTF-8') @@ -908,7 +908,7 @@ def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True): The `toc` should have to same format as :attr:`discid.Disc.toc_string`. If no toc matches in musicbrainz but a :musicbrainz:`CD Stub` does, - the CD Stub will be returned. Prevent this from happening by + the CD Stub will be returned. Prevent this from happening by passing `cdstubs=False`. The result is a dict with either a 'disc' , a 'cdstub' key diff --git a/lib/mutagen/easymp4.py b/lib/mutagen/easymp4.py index 3abacccc..65e78b74 100644 --- a/lib/mutagen/easymp4.py +++ b/lib/mutagen/easymp4.py @@ -22,7 +22,7 @@ class EasyMP4Tags(DictMixin, Metadata): strings, and values are a list of Unicode strings (and these lists are always of length 0 or 1). - If you need access to the full MP4 metadata feature set, you should use + If you need access to the full MP4 metadata feature set, you should use MP4, not EasyMP4. """ diff --git a/lib/oauth2/__init__.py b/lib/oauth2/__init__.py index 2b938909..cf77705d 100644 --- a/lib/oauth2/__init__.py +++ b/lib/oauth2/__init__.py @@ -85,11 +85,11 @@ def generate_verifier(length=8): class Consumer(object): """A consumer of OAuth-protected services. - + The OAuth consumer is a "third-party" service that wants to access protected resources from an OAuth service provider on behalf of an end user. It's kind of the OAuth client. - + Usually a consumer must be registered with the service provider by the developer of the consumer software. As part of that process, the service provider gives the consumer a *key* and a *secret* with which the consumer @@ -97,7 +97,7 @@ class Consumer(object): key in each request to identify itself, but will use its secret only when signing requests, to prove that the request is from that particular registered consumer. - + Once registered, the consumer can then use its consumer credentials to ask the service provider for a request token, kicking off the OAuth authorization process. @@ -125,12 +125,12 @@ class Consumer(object): class Token(object): """An OAuth credential used to request authorization or a protected resource. - + Tokens in OAuth comprise a *key* and a *secret*. The key is included in requests to identify the token being used, but the secret is used only in the signature, to prove that the requester is who the server gave the token to. - + When first negotiating the authorization, the consumer asks for a *request token* that the live user authorizes with the service provider. The consumer then exchanges the request token for an *access token* that can @@ -175,7 +175,7 @@ class Token(object): def to_string(self): """Returns this token as a plain string, suitable for storage. - + The resulting string includes the token's secret, so you should never send or store this string where a third party can read it. """ @@ -188,7 +188,7 @@ class Token(object): if self.callback_confirmed is not None: data['oauth_callback_confirmed'] = self.callback_confirmed return urllib.urlencode(data) - + @staticmethod def from_string(s): """Deserializes a token from a string like one returned by @@ -209,7 +209,7 @@ class Token(object): try: secret = params['oauth_token_secret'][0] except Exception: - raise ValueError("'oauth_token_secret' not found in " + raise ValueError("'oauth_token_secret' not found in " "OAuth request.") token = Token(key, secret) @@ -225,45 +225,45 @@ class Token(object): def setter(attr): name = attr.__name__ - + def getter(self): try: return self.__dict__[name] except KeyError: raise AttributeError(name) - + def deleter(self): del self.__dict__[name] - + return property(getter, attr, deleter) class Request(dict): - + """The parameters and information for an HTTP request, suitable for authorizing with OAuth credentials. - + When a consumer wants to access a service's protected resources, it does so using a signed HTTP request identifying itself (the consumer) with its key, and providing an access token authorized by the end user to access those resources. - + """ - + http_method = HTTP_METHOD http_url = None version = VERSION - + def __init__(self, method=HTTP_METHOD, url=None, parameters=None): if method is not None: self.method = method - + if url is not None: self.url = url - + if parameters is not None: self.update(parameters) - + @setter def url(self, value): parts = urlparse.urlparse(value) @@ -280,33 +280,33 @@ class Request(dict): value = '%s://%s%s' % (scheme, netloc, path) self.__dict__['url'] = value - + @setter def method(self, value): self.__dict__['method'] = value.upper() - + def _get_timestamp_nonce(self): return self['oauth_timestamp'], self['oauth_nonce'] - + def get_nonoauth_parameters(self): """Get any non-OAuth parameters.""" - return dict([(k, v) for k, v in self.iteritems() + return dict([(k, v) for k, v in self.iteritems() if not k.startswith('oauth_')]) - + def to_header(self, realm=''): """Serialize as a header for an HTTPAuth request.""" - oauth_params = ((k, v) for k, v in self.items() + oauth_params = ((k, v) for k, v in self.items() if k.startswith('oauth_')) stringy_params = ((k, escape(str(v))) for k, v in oauth_params) header_params = ('%s="%s"' % (k, v) for k, v in stringy_params) params_header = ', '.join(header_params) - + auth_header = 'OAuth realm="%s"' % realm if params_header: auth_header = "%s, %s" % (auth_header, params_header) - + return {'Authorization': auth_header} - + def to_postdata(self): """Serialize as post data for a POST request.""" return self.encode_postdata(self) @@ -327,7 +327,7 @@ class Request(dict): raise Error('Parameter not found: %s' % parameter) return ret - + def get_normalized_parameters(self): """Return a string that contains the parameters that must be signed.""" items = [(k, v) for k, v in self.items() if k != 'oauth_signature'] @@ -337,7 +337,7 @@ class Request(dict): # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) # Spaces must be encoded with "%20" instead of "+" return encoded_str.replace('+', '%20') - + def sign_request(self, signature_method, consumer, token): """Set the signature parameter to the result of sign.""" @@ -349,24 +349,24 @@ class Request(dict): self['oauth_signature_method'] = signature_method.name self['oauth_signature'] = signature_method.sign(self, consumer, token) - + @classmethod def make_timestamp(cls): """Get seconds since epoch (UTC).""" return str(int(time.time())) - + @classmethod def make_nonce(cls): """Generate pseudorandom number.""" return str(random.randint(0, 100000000)) - + @classmethod def from_request(cls, http_method, http_url, headers=None, parameters=None, query_string=None): """Combines multiple parameter sources.""" if parameters is None: parameters = {} - + # Headers if headers and 'Authorization' in headers: auth_header = headers['Authorization'] @@ -380,57 +380,57 @@ class Request(dict): except: raise Error('Unable to parse OAuth parameters from ' 'Authorization header.') - + # GET or POST query string. if query_string: query_params = cls._split_url_string(query_string) parameters.update(query_params) - + # URL parameters. param_str = urlparse.urlparse(http_url)[4] # query url_params = cls._split_url_string(param_str) parameters.update(url_params) - + if parameters: return cls(http_method, http_url, parameters) - + return None - + @classmethod def from_consumer_and_token(cls, consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): if not parameters: parameters = {} - + defaults = { 'oauth_consumer_key': consumer.key, 'oauth_timestamp': cls.make_timestamp(), 'oauth_nonce': cls.make_nonce(), 'oauth_version': cls.version, } - + defaults.update(parameters) parameters = defaults - + if token: parameters['oauth_token'] = token.key - + return Request(http_method, http_url, parameters) - + @classmethod - def from_token_and_callback(cls, token, callback=None, + def from_token_and_callback(cls, token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): if not parameters: parameters = {} - + parameters['oauth_token'] = token.key - + if callback: parameters['oauth_callback'] = callback - + return cls(http_method, http_url, parameters) - + @staticmethod def _split_header(header): """Turn Authorization: header into parameters.""" @@ -447,7 +447,7 @@ class Request(dict): # Remove quotes and unescape the value. params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) return params - + @staticmethod def _split_url_string(param_str): """Turn URL string into parameters.""" @@ -460,7 +460,7 @@ class Request(dict): class Server(object): """A skeletal implementation of a service provider, providing protected resources to requests from authorized consumers. - + This class implements the logic to check requests for authorization. You can use it with your web server or web framework to protect certain resources with OAuth. @@ -536,7 +536,7 @@ class Server(object): if not valid: key, base = signature_method.signing_base(request, consumer, token) - raise Error('Invalid signature. Expected signature base ' + raise Error('Invalid signature. Expected signature base ' 'string: %s' % base) built = signature_method.sign(request, consumer, token) @@ -567,7 +567,7 @@ class Client(httplib2.Http): self.token = token self.method = SignatureMethod_HMAC_SHA1() - httplib2.Http.__init__(self, cache=cache, timeout=timeout, + httplib2.Http.__init__(self, cache=cache, timeout=timeout, proxy_info=proxy_info) def set_signature_method(self, method): @@ -576,10 +576,10 @@ class Client(httplib2.Http): self.method = method - def request(self, uri, method="GET", body=None, headers=None, + def request(self, uri, method="GET", body=None, headers=None, redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None, force_auth_header=False): - + if not isinstance(headers, dict): headers = {} @@ -587,7 +587,7 @@ class Client(httplib2.Http): parameters = dict(parse_qsl(body)) elif method == "GET": parsed = urlparse.urlparse(uri) - parameters = parse_qs(parsed.query) + parameters = parse_qs(parsed.query) else: parameters = None @@ -614,14 +614,14 @@ class Client(httplib2.Http): # don't call update twice. headers.update(req.to_header()) - return httplib2.Http.request(self, uri, method=method, body=body, - headers=headers, redirections=redirections, + return httplib2.Http.request(self, uri, method=method, body=body, + headers=headers, redirections=redirections, connection_type=connection_type) class SignatureMethod(object): """A way of signing requests. - + The OAuth protocol lets consumers and service providers pick a way to sign requests. This interface shows the methods expected by the other `oauth` modules for signing requests. Subclass it and implement its methods to @@ -657,7 +657,7 @@ class SignatureMethod(object): class SignatureMethod_HMAC_SHA1(SignatureMethod): name = 'HMAC-SHA1' - + def signing_base(self, request, consumer, token): sig = ( escape(request.method), diff --git a/lib/pyItunes/Library.py b/lib/pyItunes/Library.py index 2400c969..3118fb7b 100644 --- a/lib/pyItunes/Library.py +++ b/lib/pyItunes/Library.py @@ -36,6 +36,6 @@ class Library: if attributes.get('Play Count'): s.play_count = int(attributes.get('Play Count')) if attributes.get('Location'): - s.location = attributes.get('Location') + s.location = attributes.get('Location') songs.append(s) return songs \ No newline at end of file diff --git a/lib/pyItunes/Song.py b/lib/pyItunes/Song.py index 27d44d79..c59759e6 100644 --- a/lib/pyItunes/Song.py +++ b/lib/pyItunes/Song.py @@ -42,5 +42,5 @@ class Song: album_rating = None play_count = None location = None - + #title = property(getTitle,setTitle) \ No newline at end of file diff --git a/lib/pyItunes/XMLLibraryParser.py b/lib/pyItunes/XMLLibraryParser.py index 7e4b239a..6824aee7 100644 --- a/lib/pyItunes/XMLLibraryParser.py +++ b/lib/pyItunes/XMLLibraryParser.py @@ -5,7 +5,7 @@ class XMLLibraryParser: s = f.read() lines = s.split("\n") self.dictionary = self.parser(lines) - + def getValue(self,restOfLine): value = re.sub("<.*?>","",restOfLine) u = unicode(value,"utf-8") diff --git a/lib/pygazelle/api.py b/lib/pygazelle/api.py index 3bdd7c2e..91df1ad2 100644 --- a/lib/pygazelle/api.py +++ b/lib/pygazelle/api.py @@ -201,7 +201,7 @@ class GazelleAPI(object): Returns the inbox Mailbox for the logged in user """ return Mailbox(self, 'inbox', page, sort) - + def get_sentbox(self, page='1', sort='unread'): """ Returns the sentbox Mailbox for the logged in user diff --git a/lib/pygazelle/inbox.py b/lib/pygazelle/inbox.py index e5016286..a26c9a26 100644 --- a/lib/pygazelle/inbox.py +++ b/lib/pygazelle/inbox.py @@ -58,9 +58,9 @@ class Mailbox(object): """ This class represents the logged in user's inbox/sentbox """ - def __init__(self, parent_api, boxtype='inbox', page='1', sort='unread'): + def __init__(self, parent_api, boxtype='inbox', page='1', sort='unread'): self.parent_api = parent_api - self.boxtype = boxtype + self.boxtype = boxtype self.current_page = page self.total_pages = None self.sort = sort diff --git a/lib/pynma/__init__.py b/lib/pynma/__init__.py index f90424eb..cbd82cfd 100644 --- a/lib/pynma/__init__.py +++ b/lib/pynma/__init__.py @@ -1,4 +1,4 @@ #!/usr/bin/python -from pynma import PyNMA +from pynma import PyNMA diff --git a/lib/pynma/pynma.py b/lib/pynma/pynma.py index fc7d8de2..037145c8 100644 --- a/lib/pynma/pynma.py +++ b/lib/pynma/pynma.py @@ -99,7 +99,7 @@ class PyNMA(object): res = self.callapi('POST', ADD_PATH, datas) results[datas['apikey']] = res return results - + def callapi(self, method, path, args): headers = { 'User-Agent': USER_AGENT } if method == "POST": @@ -116,7 +116,7 @@ class PyNMA(object): 'message': str(e) } pass - + return res def _parse_reponse(self, response): @@ -133,5 +133,5 @@ class PyNMA(object): res['message'] = elem.firstChild.nodeValue res['type'] = elem.tagName return res - - + + diff --git a/lib/requests/cookies.py b/lib/requests/cookies.py index 831c49c6..3456a9fe 100644 --- a/lib/requests/cookies.py +++ b/lib/requests/cookies.py @@ -440,7 +440,7 @@ def merge_cookies(cookiejar, cookies): """ if not isinstance(cookiejar, cookielib.CookieJar): raise ValueError('You can only merge into CookieJar') - + if isinstance(cookies, dict): cookiejar = cookiejar_from_dict( cookies, cookiejar=cookiejar, overwrite=False) diff --git a/lib/requests/packages/chardet/charsetgroupprober.py b/lib/requests/packages/chardet/charsetgroupprober.py index 85e7a1c6..ddbeeea2 100644 --- a/lib/requests/packages/chardet/charsetgroupprober.py +++ b/lib/requests/packages/chardet/charsetgroupprober.py @@ -1,11 +1,11 @@ ######################## BEGIN LICENSE BLOCK ######################## # The Original Code is Mozilla Communicator client code. -# +# # The Initial Developer of the Original Code is # Netscape Communications Corporation. # Portions created by the Initial Developer are Copyright (C) 1998 # the Initial Developer. All Rights Reserved. -# +# # Contributor(s): # Mark Pilgrim - port to Python # @@ -13,12 +13,12 @@ # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. -# +# # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. -# +# # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA diff --git a/lib/requests/packages/chardet/constants.py b/lib/requests/packages/chardet/constants.py index e4d148b3..8895c94e 100644 --- a/lib/requests/packages/chardet/constants.py +++ b/lib/requests/packages/chardet/constants.py @@ -14,12 +14,12 @@ # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. -# +# # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. -# +# # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA diff --git a/lib/requests/packages/chardet/euckrfreq.py b/lib/requests/packages/chardet/euckrfreq.py index a179e4c2..1fa75883 100644 --- a/lib/requests/packages/chardet/euckrfreq.py +++ b/lib/requests/packages/chardet/euckrfreq.py @@ -13,12 +13,12 @@ # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. -# +# # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. -# +# # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA @@ -35,14 +35,14 @@ # # Idea Distribution Ratio = 0.98653 / (1-0.98653) = 73.24 # Random Distribution Ration = 512 / (2350-512) = 0.279. -# -# Typical Distribution Ratio +# +# Typical Distribution Ratio EUCKR_TYPICAL_DISTRIBUTION_RATIO = 6.0 EUCKR_TABLE_SIZE = 2352 -# Char to FreqOrder table , +# Char to FreqOrder table , EUCKRCharToFreqOrder = ( \ 13, 130, 120,1396, 481,1719,1720, 328, 609, 212,1721, 707, 400, 299,1722, 87, 1397,1723, 104, 536,1117,1203,1724,1267, 685,1268, 508,1725,1726,1727,1728,1398, diff --git a/lib/requests/packages/chardet/euctwprober.py b/lib/requests/packages/chardet/euctwprober.py index fe652fe3..178a3042 100644 --- a/lib/requests/packages/chardet/euctwprober.py +++ b/lib/requests/packages/chardet/euctwprober.py @@ -13,12 +13,12 @@ # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. -# +# # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. -# +# # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA diff --git a/lib/requests/packages/chardet/gb2312prober.py b/lib/requests/packages/chardet/gb2312prober.py index 0325a2d8..b7a181a2 100644 --- a/lib/requests/packages/chardet/gb2312prober.py +++ b/lib/requests/packages/chardet/gb2312prober.py @@ -13,12 +13,12 @@ # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. -# +# # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. -# +# # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA diff --git a/lib/unidecode/__init__.py b/lib/unidecode/__init__.py index 82eb5a3f..dd3d2310 100644 --- a/lib/unidecode/__init__.py +++ b/lib/unidecode/__init__.py @@ -39,7 +39,7 @@ def unidecode(string): if codepoint < 0x80: # Basic ASCII retval.append(str(char)) continue - + if codepoint > 0xeffff: continue # Characters in Private Use Area and above are ignored diff --git a/lib/yaml/constructor.py b/lib/yaml/constructor.py index 635faac3..abbff97b 100644 --- a/lib/yaml/constructor.py +++ b/lib/yaml/constructor.py @@ -287,7 +287,7 @@ class SafeConstructor(BaseConstructor): return str(value).decode('base64') except (binascii.Error, UnicodeEncodeError), exc: raise ConstructorError(None, None, - "failed to decode base64 data: %s" % exc, node.start_mark) + "failed to decode base64 data: %s" % exc, node.start_mark) timestamp_regexp = re.compile( ur'''^(?P[0-9][0-9][0-9][0-9]) diff --git a/lib/yaml/emitter.py b/lib/yaml/emitter.py index e5bcdccc..488337f2 100644 --- a/lib/yaml/emitter.py +++ b/lib/yaml/emitter.py @@ -674,7 +674,7 @@ class Emitter(object): # Check for indicators. if index == 0: # Leading indicators are special characters. - if ch in u'#,[]{}&*!|>\'\"%@`': + if ch in u'#,[]{}&*!|>\'\"%@`': flow_indicators = True block_indicators = True if ch in u'?:': diff --git a/lib/yaml/parser.py b/lib/yaml/parser.py index f9e3057f..ffec9552 100644 --- a/lib/yaml/parser.py +++ b/lib/yaml/parser.py @@ -482,7 +482,7 @@ class Parser(object): token = self.peek_token() raise ParserError("while parsing a flow sequence", self.marks[-1], "expected ',' or ']', but got %r" % token.id, token.start_mark) - + if self.check_token(KeyToken): token = self.peek_token() event = MappingStartEvent(None, None, True, diff --git a/lib/yaml/scanner.py b/lib/yaml/scanner.py index 5228fad6..4e578581 100644 --- a/lib/yaml/scanner.py +++ b/lib/yaml/scanner.py @@ -314,7 +314,7 @@ class Scanner(object): # Remove the saved possible key position at the current flow level. if self.flow_level in self.possible_simple_keys: key = self.possible_simple_keys[self.flow_level] - + if key.required: raise ScannerError("while scanning a simple key", key.mark, "could not found expected ':'", self.get_mark()) @@ -363,11 +363,11 @@ class Scanner(object): # Read the token. mark = self.get_mark() - + # Add STREAM-START. self.tokens.append(StreamStartToken(mark, mark, encoding=self.encoding)) - + def fetch_stream_end(self): @@ -381,7 +381,7 @@ class Scanner(object): # Read the token. mark = self.get_mark() - + # Add STREAM-END. self.tokens.append(StreamEndToken(mark, mark)) @@ -389,7 +389,7 @@ class Scanner(object): self.done = True def fetch_directive(self): - + # Set the current intendation to -1. self.unwind_indent(-1) @@ -516,7 +516,7 @@ class Scanner(object): self.tokens.append(BlockEntryToken(start_mark, end_mark)) def fetch_key(self): - + # Block context needs additional checks. if not self.flow_level: @@ -566,7 +566,7 @@ class Scanner(object): # It must be a part of a complex key. else: - + # Block context needs additional checks. # (Do we really need them? They will be catched by the parser # anyway.) @@ -1024,14 +1024,14 @@ class Scanner(object): # Unfortunately, folding rules are ambiguous. # # This is the folding according to the specification: - + if folded and line_break == u'\n' \ and leading_non_space and self.peek() not in u' \t': if not breaks: chunks.append(u' ') else: chunks.append(line_break) - + # This is Clark Evans's interpretation (also in the spec # examples): # From 296c9ca767fa52ace675516ac28a8737118ad78a Mon Sep 17 00:00:00 2001 From: David Date: Wed, 6 Aug 2014 15:55:08 +0200 Subject: [PATCH 05/11] Reducing search bar width --- data/interfaces/default/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/interfaces/default/css/style.css b/data/interfaces/default/css/style.css index 16f4171e..8bb4c597 100644 --- a/data/interfaces/default/css/style.css +++ b/data/interfaces/default/css/style.css @@ -758,7 +758,7 @@ div#searchbar input[type=text] { line-height: normal; margin-right: 5px; padding: 4px 5px 4px 25px; - width: 200px; + width: 185px; } div#searchbar .mini-icon { color: #999; From fd30b3501e4fdb4a0ef6e8a86360834af1e3dccc Mon Sep 17 00:00:00 2001 From: David Date: Wed, 6 Aug 2014 17:00:29 +0200 Subject: [PATCH 06/11] Converting crlf to just lf Making end-of-lines consistent throughout the codebase. --- README.md | 126 +- data/interfaces/default/css/config.less | 50 +- .../js/fancybox/jquery.fancybox-1.3.4.js | 2310 ++++++++--------- headphones/classes.py | 260 +- headphones/common.py | 354 +-- headphones/exceptions.py | 82 +- headphones/sab.py | 324 +-- lib/MultipartPostHandler.py | 174 +- lib/__init__.py | 2 +- lib/cherrypy/LICENSE.txt | 50 +- lib/ordereddict.py | 254 +- 11 files changed, 1993 insertions(+), 1993 deletions(-) diff --git a/README.md b/README.md index f30577f2..cde0f863 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,63 @@ -#![preview thumb](https://github.com/rembo10/headphones/raw/master/data/images/headphoneslogo.png)Headphones - -###Support & Discuss - -You are free to join the HP support community on IRC where you can ask questions, hang around and discuss anything related to HP. - -1. Use any IRC client and connect to the Freenode server. -2. Join #headphones - -###Installation and Notes - -[Read our Wiki](../../wiki) on how to install and use HeadPhones properly. - -**Issues** can be reported on the GitHub issue tracker considering these rules: - -1. Analyze your log, you just might find the solution yourself! -2. You read the wiki and searched existing issues, but this is not solving your problem. -3. Post the issue with a clear title, description and the HP log and use [proper markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your text (code/log in code blocks). -4. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it. - -**Feature requests** can be reported on the GitHub issue tracker too: - -1. Search for similar existing 'issues', feature requests can be recognized by the label 'Request'. -2. If a similar Request exists, post a comment (+1, or add a new idea to the existing request), otherwise you can create a new one. - -If you **comply with these rules** you can [post your request/issue](http://github.com/rembo10/headphones/issues). - -**Support** the project by implementing new features, solving support tickets and provide bug fixes. -If you change something in the code always make a PR to the developer branch instead of the master branch. - - -###Screenshots - -Homepage (Artist Overview) - -![preview thumb](http://i.imgur.com/LZO9a.png) - -One of the many settings pages.... - -![preview thumb](http://i.imgur.com/xcWNy.png) - -It might even know you better than you know yourself: - -![preview thumb](http://i.imgur.com/R7J0f.png) - -Import Your Favorite Artists: - -![preview thumb](http://i.imgur.com/6tZoC.png) - -Artist Search Results (also search by album!): - -![preview thumb](http://i.imgur.com/rIV0P.png) - -Artist Page with Bio & Album Overview: - -![preview thumb](http://i.imgur.com/SSil1.png) - -Album Page with track overview: - -![preview thumb](http://i.imgur.com/kcjES.png) - - -This is free software under the GPL v3 open source license - so feel free to do with it what you wish. +#![preview thumb](https://github.com/rembo10/headphones/raw/master/data/images/headphoneslogo.png)Headphones + +###Support & Discuss + +You are free to join the HP support community on IRC where you can ask questions, hang around and discuss anything related to HP. + +1. Use any IRC client and connect to the Freenode server. +2. Join #headphones + +###Installation and Notes + +[Read our Wiki](../../wiki) on how to install and use HeadPhones properly. + +**Issues** can be reported on the GitHub issue tracker considering these rules: + +1. Analyze your log, you just might find the solution yourself! +2. You read the wiki and searched existing issues, but this is not solving your problem. +3. Post the issue with a clear title, description and the HP log and use [proper markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your text (code/log in code blocks). +4. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it. + +**Feature requests** can be reported on the GitHub issue tracker too: + +1. Search for similar existing 'issues', feature requests can be recognized by the label 'Request'. +2. If a similar Request exists, post a comment (+1, or add a new idea to the existing request), otherwise you can create a new one. + +If you **comply with these rules** you can [post your request/issue](http://github.com/rembo10/headphones/issues). + +**Support** the project by implementing new features, solving support tickets and provide bug fixes. +If you change something in the code always make a PR to the developer branch instead of the master branch. + + +###Screenshots + +Homepage (Artist Overview) + +![preview thumb](http://i.imgur.com/LZO9a.png) + +One of the many settings pages.... + +![preview thumb](http://i.imgur.com/xcWNy.png) + +It might even know you better than you know yourself: + +![preview thumb](http://i.imgur.com/R7J0f.png) + +Import Your Favorite Artists: + +![preview thumb](http://i.imgur.com/6tZoC.png) + +Artist Search Results (also search by album!): + +![preview thumb](http://i.imgur.com/rIV0P.png) + +Artist Page with Bio & Album Overview: + +![preview thumb](http://i.imgur.com/SSil1.png) + +Album Page with track overview: + +![preview thumb](http://i.imgur.com/kcjES.png) + + +This is free software under the GPL v3 open source license - so feel free to do with it what you wish. diff --git a/data/interfaces/default/css/config.less b/data/interfaces/default/css/config.less index 4fdccaee..022a0700 100644 --- a/data/interfaces/default/css/config.less +++ b/data/interfaces/default/css/config.less @@ -1,4 +1,4 @@ -/* Variables */ +/* Variables */ @base-font-face: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; @alt-font-face: "Trebuchet MS", Helvetica, Arial, sans-serif; @base-font-size: 12px; @@ -10,20 +10,20 @@ @msg-bg: #FFF6A9; @msg-bg-success: #D3FFD7; @msg-bg-error: #FFD3D3; - -/* Mixins */ -.rounded(@radius: 5px) { - -moz-border-radius: @radius; - -webkit-border-radius: @radius; - border-radius: @radius; -} -.roundedTop(@radius: 5px) { - -moz-border-radius-topleft: @radius; - -moz-border-radius-topright: @radius; - -webkit-border-top-right-radius: @radius; - -webkit-border-top-left-radius: @radius; - border-top-left-radius: @radius; - border-top-right-radius: @radius; + +/* Mixins */ +.rounded(@radius: 5px) { + -moz-border-radius: @radius; + -webkit-border-radius: @radius; + border-radius: @radius; +} +.roundedTop(@radius: 5px) { + -moz-border-radius-topleft: @radius; + -moz-border-radius-topright: @radius; + -webkit-border-top-right-radius: @radius; + -webkit-border-top-left-radius: @radius; + border-top-left-radius: @radius; + border-top-right-radius: @radius; } .roundedLeftTop(@radius: 5px) { -moz-border-radius-topleft: @radius; @@ -34,14 +34,14 @@ -moz-border-radius-topright: @radius; -webkit-border-top-right-radius: @radius; border-top-right-radius: @radius; -} -.roundedBottom(@radius: 5px) { - -moz-border-radius-bottomleft: @radius; - -moz-border-radius-bottomright: @radius; - -webkit-border-bottom-right-radius: @radius; - -webkit-border-bottom-left-radius: @radius; - border-bottom-left-radius: @radius; - border-bottom-right-radius: @radius; +} +.roundedBottom(@radius: 5px) { + -moz-border-radius-bottomleft: @radius; + -moz-border-radius-bottomright: @radius; + -webkit-border-bottom-right-radius: @radius; + -webkit-border-bottom-left-radius: @radius; + border-bottom-left-radius: @radius; + border-bottom-right-radius: @radius; } .roundedLeftBottom(@radius: 5px) { -moz-border-radius-bottomleft: @radius; @@ -52,7 +52,7 @@ -moz-border-radius-bottomright: @radius; -webkit-border-bottom-right-radius: @radius; border-bottom-right-radius: @radius; -} +} .shadow(@shadow: 0 17px 11px -1px #ced8d9) { -moz-box-shadow: @shadow; -webkit-box-shadow: @shadow; @@ -74,4 +74,4 @@ -o-opacity:@opacity_percent / 100 !important; opacity:@opacity_percent / 100 !important; } - + diff --git a/data/interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js b/data/interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js index be772753..a8520051 100644 --- a/data/interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js +++ b/data/interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js @@ -1,1156 +1,1156 @@ -/* - * FancyBox - jQuery Plugin - * Simple and fancy lightbox alternative - * - * Examples and documentation at: http://fancybox.net - * - * Copyright (c) 2008 - 2010 Janis Skarnelis - * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated. - * - * Version: 1.3.4 (11/11/2010) - * Requires: jQuery v1.3+ - * - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - */ - -;(function($) { - var tmp, loading, overlay, wrap, outer, content, close, title, nav_left, nav_right, - - selectedIndex = 0, selectedOpts = {}, selectedArray = [], currentIndex = 0, currentOpts = {}, currentArray = [], - - ajaxLoader = null, imgPreloader = new Image(), imgRegExp = /\.(jpg|gif|png|bmp|jpeg)(.*)?$/i, swfRegExp = /[^\.]\.(swf)\s*$/i, - - loadingTimer, loadingFrame = 1, - - titleHeight = 0, titleStr = '', start_pos, final_pos, busy = false, fx = $.extend($('
')[0], { prop: 0 }), - - isIE6 = $.browser.msie && $.browser.version < 7 && !window.XMLHttpRequest, - - /* - * Private methods - */ - - _abort = function() { - loading.hide(); - - imgPreloader.onerror = imgPreloader.onload = null; - - if (ajaxLoader) { - ajaxLoader.abort(); - } - - tmp.empty(); - }, - - _error = function() { - if (false === selectedOpts.onError(selectedArray, selectedIndex, selectedOpts)) { - loading.hide(); - busy = false; - return; - } - - selectedOpts.titleShow = false; - - selectedOpts.width = 'auto'; - selectedOpts.height = 'auto'; - - tmp.html( '

The requested content cannot be loaded.
Please try again later.

' ); - - _process_inline(); - }, - - _start = function() { - var obj = selectedArray[ selectedIndex ], - href, - type, - title, - str, - emb, - ret; - - _abort(); - - selectedOpts = $.extend({}, $.fn.fancybox.defaults, (typeof $(obj).data('fancybox') == 'undefined' ? selectedOpts : $(obj).data('fancybox'))); - - ret = selectedOpts.onStart(selectedArray, selectedIndex, selectedOpts); - - if (ret === false) { - busy = false; - return; - } else if (typeof ret == 'object') { - selectedOpts = $.extend(selectedOpts, ret); - } - - title = selectedOpts.title || (obj.nodeName ? $(obj).attr('title') : obj.title) || ''; - - if (obj.nodeName && !selectedOpts.orig) { - selectedOpts.orig = $(obj).children("img:first").length ? $(obj).children("img:first") : $(obj); - } - - if (title === '' && selectedOpts.orig && selectedOpts.titleFromAlt) { - title = selectedOpts.orig.attr('alt'); - } - - href = selectedOpts.href || (obj.nodeName ? $(obj).attr('href') : obj.href) || null; - - if ((/^(?:javascript)/i).test(href) || href == '#') { - href = null; - } - - if (selectedOpts.type) { - type = selectedOpts.type; - - if (!href) { - href = selectedOpts.content; - } - - } else if (selectedOpts.content) { - type = 'html'; - - } else if (href) { - if (href.match(imgRegExp)) { - type = 'image'; - - } else if (href.match(swfRegExp)) { - type = 'swf'; - - } else if ($(obj).hasClass("iframe")) { - type = 'iframe'; - - } else if (href.indexOf("#") === 0) { - type = 'inline'; - - } else { - type = 'ajax'; - } - } - - if (!type) { - _error(); - return; - } - - if (type == 'inline') { - obj = href.substr(href.indexOf("#")); - type = $(obj).length > 0 ? 'inline' : 'ajax'; - } - - selectedOpts.type = type; - selectedOpts.href = href; - selectedOpts.title = title; - - if (selectedOpts.autoDimensions) { - if (selectedOpts.type == 'html' || selectedOpts.type == 'inline' || selectedOpts.type == 'ajax') { - selectedOpts.width = 'auto'; - selectedOpts.height = 'auto'; - } else { - selectedOpts.autoDimensions = false; - } - } - - if (selectedOpts.modal) { - selectedOpts.overlayShow = true; - selectedOpts.hideOnOverlayClick = false; - selectedOpts.hideOnContentClick = false; - selectedOpts.enableEscapeButton = false; - selectedOpts.showCloseButton = false; - } - - selectedOpts.padding = parseInt(selectedOpts.padding, 10); - selectedOpts.margin = parseInt(selectedOpts.margin, 10); - - tmp.css('padding', (selectedOpts.padding + selectedOpts.margin)); - - $('.fancybox-inline-tmp').unbind('fancybox-cancel').bind('fancybox-change', function() { - $(this).replaceWith(content.children()); - }); - - switch (type) { - case 'html' : - tmp.html( selectedOpts.content ); - _process_inline(); - break; - - case 'inline' : - if ( $(obj).parent().is('#fancybox-content') === true) { - busy = false; - return; - } - - $('
') - .hide() - .insertBefore( $(obj) ) - .bind('fancybox-cleanup', function() { - $(this).replaceWith(content.children()); - }).bind('fancybox-cancel', function() { - $(this).replaceWith(tmp.children()); - }); - - $(obj).appendTo(tmp); - - _process_inline(); - break; - - case 'image': - busy = false; - - $.fancybox.showActivity(); - - imgPreloader = new Image(); - - imgPreloader.onerror = function() { - _error(); - }; - - imgPreloader.onload = function() { - busy = true; - - imgPreloader.onerror = imgPreloader.onload = null; - - _process_image(); - }; - - imgPreloader.src = href; - break; - - case 'swf': - selectedOpts.scrolling = 'no'; - - str = ''; - emb = ''; - - $.each(selectedOpts.swf, function(name, val) { - str += ''; - emb += ' ' + name + '="' + val + '"'; - }); - - str += ''; - - tmp.html(str); - - _process_inline(); - break; - - case 'ajax': - busy = false; - - $.fancybox.showActivity(); - - selectedOpts.ajax.win = selectedOpts.ajax.success; - - ajaxLoader = $.ajax($.extend({}, selectedOpts.ajax, { - url : href, - data : selectedOpts.ajax.data || {}, - error : function(XMLHttpRequest, textStatus, errorThrown) { - if ( XMLHttpRequest.status > 0 ) { - _error(); - } - }, - success : function(data, textStatus, XMLHttpRequest) { - var o = typeof XMLHttpRequest == 'object' ? XMLHttpRequest : ajaxLoader; - if (o.status == 200) { - if ( typeof selectedOpts.ajax.win == 'function' ) { - ret = selectedOpts.ajax.win(href, data, textStatus, XMLHttpRequest); - - if (ret === false) { - loading.hide(); - return; - } else if (typeof ret == 'string' || typeof ret == 'object') { - data = ret; - } - } - - tmp.html( data ); - _process_inline(); - } - } - })); - - break; - - case 'iframe': - _show(); - break; - } - }, - - _process_inline = function() { - var - w = selectedOpts.width, - h = selectedOpts.height; - - if (w.toString().indexOf('%') > -1) { - w = parseInt( ($(window).width() - (selectedOpts.margin * 2)) * parseFloat(w) / 100, 10) + 'px'; - - } else { - w = w == 'auto' ? 'auto' : w + 'px'; - } - - if (h.toString().indexOf('%') > -1) { - h = parseInt( ($(window).height() - (selectedOpts.margin * 2)) * parseFloat(h) / 100, 10) + 'px'; - - } else { - h = h == 'auto' ? 'auto' : h + 'px'; - } - - tmp.wrapInner('
'); - - selectedOpts.width = tmp.width(); - selectedOpts.height = tmp.height(); - - _show(); - }, - - _process_image = function() { - selectedOpts.width = imgPreloader.width; - selectedOpts.height = imgPreloader.height; - - $("").attr({ - 'id' : 'fancybox-img', - 'src' : imgPreloader.src, - 'alt' : selectedOpts.title - }).appendTo( tmp ); - - _show(); - }, - - _show = function() { - var pos, equal; - - loading.hide(); - - if (wrap.is(":visible") && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) { - $.event.trigger('fancybox-cancel'); - - busy = false; - return; - } - - busy = true; - - $(content.add( overlay )).unbind(); - - $(window).unbind("resize.fb scroll.fb"); - $(document).unbind('keydown.fb'); - - if (wrap.is(":visible") && currentOpts.titlePosition !== 'outside') { - wrap.css('height', wrap.height()); - } - - currentArray = selectedArray; - currentIndex = selectedIndex; - currentOpts = selectedOpts; - - if (currentOpts.overlayShow) { - overlay.css({ - 'background-color' : currentOpts.overlayColor, - 'opacity' : currentOpts.overlayOpacity, - 'cursor' : currentOpts.hideOnOverlayClick ? 'pointer' : 'auto', - 'height' : $(document).height() - }); - - if (!overlay.is(':visible')) { - if (isIE6) { - $('select:not(#fancybox-tmp select)').filter(function() { - return this.style.visibility !== 'hidden'; - }).css({'visibility' : 'hidden'}).one('fancybox-cleanup', function() { - this.style.visibility = 'inherit'; - }); - } - - overlay.show(); - } - } else { - overlay.hide(); - } - - final_pos = _get_zoom_to(); - - _process_title(); - - if (wrap.is(":visible")) { - $( close.add( nav_left ).add( nav_right ) ).hide(); - - pos = wrap.position(), - - start_pos = { - top : pos.top, - left : pos.left, - width : wrap.width(), - height : wrap.height() - }; - - equal = (start_pos.width == final_pos.width && start_pos.height == final_pos.height); - - content.fadeTo(currentOpts.changeFade, 0.3, function() { - var finish_resizing = function() { - content.html( tmp.contents() ).fadeTo(currentOpts.changeFade, 1, _finish); - }; - - $.event.trigger('fancybox-change'); - - content - .empty() - .removeAttr('filter') - .css({ - 'border-width' : currentOpts.padding, - 'width' : final_pos.width - currentOpts.padding * 2, - 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2 - }); - - if (equal) { - finish_resizing(); - - } else { - fx.prop = 0; - - $(fx).animate({prop: 1}, { - duration : currentOpts.changeSpeed, - easing : currentOpts.easingChange, - step : _draw, - complete : finish_resizing - }); - } - }); - - return; - } - - wrap.removeAttr("style"); - - content.css('border-width', currentOpts.padding); - - if (currentOpts.transitionIn == 'elastic') { - start_pos = _get_zoom_from(); - - content.html( tmp.contents() ); - - wrap.show(); - - if (currentOpts.opacity) { - final_pos.opacity = 0; - } - - fx.prop = 0; - - $(fx).animate({prop: 1}, { - duration : currentOpts.speedIn, - easing : currentOpts.easingIn, - step : _draw, - complete : _finish - }); - - return; - } - - if (currentOpts.titlePosition == 'inside' && titleHeight > 0) { - title.show(); - } - - content - .css({ - 'width' : final_pos.width - currentOpts.padding * 2, - 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2 - }) - .html( tmp.contents() ); - - wrap - .css(final_pos) - .fadeIn( currentOpts.transitionIn == 'none' ? 0 : currentOpts.speedIn, _finish ); - }, - - _format_title = function(title) { - if (title && title.length) { - if (currentOpts.titlePosition == 'float') { - return '
' + title + '
'; - } - - return '
' + title + '
'; - } - - return false; - }, - - _process_title = function() { - titleStr = currentOpts.title || ''; - titleHeight = 0; - - title - .empty() - .removeAttr('style') - .removeClass(); - - if (currentOpts.titleShow === false) { - title.hide(); - return; - } - - titleStr = $.isFunction(currentOpts.titleFormat) ? currentOpts.titleFormat(titleStr, currentArray, currentIndex, currentOpts) : _format_title(titleStr); - - if (!titleStr || titleStr === '') { - title.hide(); - return; - } - - title - .addClass('fancybox-title-' + currentOpts.titlePosition) - .html( titleStr ) - .appendTo( 'body' ) - .show(); - - switch (currentOpts.titlePosition) { - case 'inside': - title - .css({ - 'width' : final_pos.width - (currentOpts.padding * 2), - 'marginLeft' : currentOpts.padding, - 'marginRight' : currentOpts.padding - }); - - titleHeight = title.outerHeight(true); - - title.appendTo( outer ); - - final_pos.height += titleHeight; - break; - - case 'over': - title - .css({ - 'marginLeft' : currentOpts.padding, - 'width' : final_pos.width - (currentOpts.padding * 2), - 'bottom' : currentOpts.padding - }) - .appendTo( outer ); - break; - - case 'float': - title - .css('left', parseInt((title.width() - final_pos.width - 40)/ 2, 10) * -1) - .appendTo( wrap ); - break; - - default: - title - .css({ - 'width' : final_pos.width - (currentOpts.padding * 2), - 'paddingLeft' : currentOpts.padding, - 'paddingRight' : currentOpts.padding - }) - .appendTo( wrap ); - break; - } - - title.hide(); - }, - - _set_navigation = function() { - if (currentOpts.enableEscapeButton || currentOpts.enableKeyboardNav) { - $(document).bind('keydown.fb', function(e) { - if (e.keyCode == 27 && currentOpts.enableEscapeButton) { - e.preventDefault(); - $.fancybox.close(); - - } else if ((e.keyCode == 37 || e.keyCode == 39) && currentOpts.enableKeyboardNav && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'SELECT') { - e.preventDefault(); - $.fancybox[ e.keyCode == 37 ? 'prev' : 'next'](); - } - }); - } - - if (!currentOpts.showNavArrows) { - nav_left.hide(); - nav_right.hide(); - return; - } - - if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex !== 0) { - nav_left.show(); - } - - if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex != (currentArray.length -1)) { - nav_right.show(); - } - }, - - _finish = function () { - if (!$.support.opacity) { - content.get(0).style.removeAttribute('filter'); - wrap.get(0).style.removeAttribute('filter'); - } - - if (selectedOpts.autoDimensions) { - content.css('height', 'auto'); - } - - wrap.css('height', 'auto'); - - if (titleStr && titleStr.length) { - title.show(); - } - - if (currentOpts.showCloseButton) { - close.show(); - } - - _set_navigation(); - - if (currentOpts.hideOnContentClick) { - content.bind('click', $.fancybox.close); - } - - if (currentOpts.hideOnOverlayClick) { - overlay.bind('click', $.fancybox.close); - } - - $(window).bind("resize.fb", $.fancybox.resize); - - if (currentOpts.centerOnScroll) { - $(window).bind("scroll.fb", $.fancybox.center); - } - - if (currentOpts.type == 'iframe') { - $('').appendTo(content); - } - - wrap.show(); - - busy = false; - - $.fancybox.center(); - - currentOpts.onComplete(currentArray, currentIndex, currentOpts); - - _preload_images(); - }, - - _preload_images = function() { - var href, - objNext; - - if ((currentArray.length -1) > currentIndex) { - href = currentArray[ currentIndex + 1 ].href; - - if (typeof href !== 'undefined' && href.match(imgRegExp)) { - objNext = new Image(); - objNext.src = href; - } - } - - if (currentIndex > 0) { - href = currentArray[ currentIndex - 1 ].href; - - if (typeof href !== 'undefined' && href.match(imgRegExp)) { - objNext = new Image(); - objNext.src = href; - } - } - }, - - _draw = function(pos) { - var dim = { - width : parseInt(start_pos.width + (final_pos.width - start_pos.width) * pos, 10), - height : parseInt(start_pos.height + (final_pos.height - start_pos.height) * pos, 10), - - top : parseInt(start_pos.top + (final_pos.top - start_pos.top) * pos, 10), - left : parseInt(start_pos.left + (final_pos.left - start_pos.left) * pos, 10) - }; - - if (typeof final_pos.opacity !== 'undefined') { - dim.opacity = pos < 0.5 ? 0.5 : pos; - } - - wrap.css(dim); - - content.css({ - 'width' : dim.width - currentOpts.padding * 2, - 'height' : dim.height - (titleHeight * pos) - currentOpts.padding * 2 - }); - }, - - _get_viewport = function() { - return [ - $(window).width() - (currentOpts.margin * 2), - $(window).height() - (currentOpts.margin * 2), - $(document).scrollLeft() + currentOpts.margin, - $(document).scrollTop() + currentOpts.margin - ]; - }, - - _get_zoom_to = function () { - var view = _get_viewport(), - to = {}, - resize = currentOpts.autoScale, - double_padding = currentOpts.padding * 2, - ratio; - - if (currentOpts.width.toString().indexOf('%') > -1) { - to.width = parseInt((view[0] * parseFloat(currentOpts.width)) / 100, 10); - } else { - to.width = currentOpts.width + double_padding; - } - - if (currentOpts.height.toString().indexOf('%') > -1) { - to.height = parseInt((view[1] * parseFloat(currentOpts.height)) / 100, 10); - } else { - to.height = currentOpts.height + double_padding; - } - - if (resize && (to.width > view[0] || to.height > view[1])) { - if (selectedOpts.type == 'image' || selectedOpts.type == 'swf') { - ratio = (currentOpts.width ) / (currentOpts.height ); - - if ((to.width ) > view[0]) { - to.width = view[0]; - to.height = parseInt(((to.width - double_padding) / ratio) + double_padding, 10); - } - - if ((to.height) > view[1]) { - to.height = view[1]; - to.width = parseInt(((to.height - double_padding) * ratio) + double_padding, 10); - } - - } else { - to.width = Math.min(to.width, view[0]); - to.height = Math.min(to.height, view[1]); - } - } - - to.top = parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - to.height - 40) * 0.5)), 10); - to.left = parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - to.width - 40) * 0.5)), 10); - - return to; - }, - - _get_obj_pos = function(obj) { - var pos = obj.offset(); - - pos.top += parseInt( obj.css('paddingTop'), 10 ) || 0; - pos.left += parseInt( obj.css('paddingLeft'), 10 ) || 0; - - pos.top += parseInt( obj.css('border-top-width'), 10 ) || 0; - pos.left += parseInt( obj.css('border-left-width'), 10 ) || 0; - - pos.width = obj.width(); - pos.height = obj.height(); - - return pos; - }, - - _get_zoom_from = function() { - var orig = selectedOpts.orig ? $(selectedOpts.orig) : false, - from = {}, - pos, - view; - - if (orig && orig.length) { - pos = _get_obj_pos(orig); - - from = { - width : pos.width + (currentOpts.padding * 2), - height : pos.height + (currentOpts.padding * 2), - top : pos.top - currentOpts.padding - 20, - left : pos.left - currentOpts.padding - 20 - }; - - } else { - view = _get_viewport(); - - from = { - width : currentOpts.padding * 2, - height : currentOpts.padding * 2, - top : parseInt(view[3] + view[1] * 0.5, 10), - left : parseInt(view[2] + view[0] * 0.5, 10) - }; - } - - return from; - }, - - _animate_loading = function() { - if (!loading.is(':visible')){ - clearInterval(loadingTimer); - return; - } - - $('div', loading).css('top', (loadingFrame * -40) + 'px'); - - loadingFrame = (loadingFrame + 1) % 12; - }; - - /* - * Public methods - */ - - $.fn.fancybox = function(options) { - if (!$(this).length) { - return this; - } - - $(this) - .data('fancybox', $.extend({}, options, ($.metadata ? $(this).metadata() : {}))) - .unbind('click.fb') - .bind('click.fb', function(e) { - e.preventDefault(); - - if (busy) { - return; - } - - busy = true; - - $(this).blur(); - - selectedArray = []; - selectedIndex = 0; - - var rel = $(this).attr('rel') || ''; - - if (!rel || rel == '' || rel === 'nofollow') { - selectedArray.push(this); - - } else { - selectedArray = $("a[rel=" + rel + "], area[rel=" + rel + "]"); - selectedIndex = selectedArray.index( this ); - } - - _start(); - - return; - }); - - return this; - }; - - $.fancybox = function(obj) { - var opts; - - if (busy) { - return; - } - - busy = true; - opts = typeof arguments[1] !== 'undefined' ? arguments[1] : {}; - - selectedArray = []; - selectedIndex = parseInt(opts.index, 10) || 0; - - if ($.isArray(obj)) { - for (var i = 0, j = obj.length; i < j; i++) { - if (typeof obj[i] == 'object') { - $(obj[i]).data('fancybox', $.extend({}, opts, obj[i])); - } else { - obj[i] = $({}).data('fancybox', $.extend({content : obj[i]}, opts)); - } - } - - selectedArray = jQuery.merge(selectedArray, obj); - - } else { - if (typeof obj == 'object') { - $(obj).data('fancybox', $.extend({}, opts, obj)); - } else { - obj = $({}).data('fancybox', $.extend({content : obj}, opts)); - } - - selectedArray.push(obj); - } - - if (selectedIndex > selectedArray.length || selectedIndex < 0) { - selectedIndex = 0; - } - - _start(); - }; - - $.fancybox.showActivity = function() { - clearInterval(loadingTimer); - - loading.show(); - loadingTimer = setInterval(_animate_loading, 66); - }; - - $.fancybox.hideActivity = function() { - loading.hide(); - }; - - $.fancybox.next = function() { - return $.fancybox.pos( currentIndex + 1); - }; - - $.fancybox.prev = function() { - return $.fancybox.pos( currentIndex - 1); - }; - - $.fancybox.pos = function(pos) { - if (busy) { - return; - } - - pos = parseInt(pos); - - selectedArray = currentArray; - - if (pos > -1 && pos < currentArray.length) { - selectedIndex = pos; - _start(); - - } else if (currentOpts.cyclic && currentArray.length > 1) { - selectedIndex = pos >= currentArray.length ? 0 : currentArray.length - 1; - _start(); - } - - return; - }; - - $.fancybox.cancel = function() { - if (busy) { - return; - } - - busy = true; - - $.event.trigger('fancybox-cancel'); - - _abort(); - - selectedOpts.onCancel(selectedArray, selectedIndex, selectedOpts); - - busy = false; - }; - - // Note: within an iframe use - parent.$.fancybox.close(); - $.fancybox.close = function() { - if (busy || wrap.is(':hidden')) { - return; - } - - busy = true; - - if (currentOpts && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) { - busy = false; - return; - } - - _abort(); - - $(close.add( nav_left ).add( nav_right )).hide(); - - $(content.add( overlay )).unbind(); - - $(window).unbind("resize.fb scroll.fb"); - $(document).unbind('keydown.fb'); - - content.find('iframe').attr('src', isIE6 && /^https/i.test(window.location.href || '') ? 'javascript:void(false)' : 'about:blank'); - - if (currentOpts.titlePosition !== 'inside') { - title.empty(); - } - - wrap.stop(); - - function _cleanup() { - overlay.fadeOut('fast'); - - title.empty().hide(); - wrap.hide(); - - $.event.trigger('fancybox-cleanup'); - - content.empty(); - - currentOpts.onClosed(currentArray, currentIndex, currentOpts); - - currentArray = selectedOpts = []; - currentIndex = selectedIndex = 0; - currentOpts = selectedOpts = {}; - - busy = false; - } - - if (currentOpts.transitionOut == 'elastic') { - start_pos = _get_zoom_from(); - - var pos = wrap.position(); - - final_pos = { - top : pos.top , - left : pos.left, - width : wrap.width(), - height : wrap.height() - }; - - if (currentOpts.opacity) { - final_pos.opacity = 1; - } - - title.empty().hide(); - - fx.prop = 1; - - $(fx).animate({ prop: 0 }, { - duration : currentOpts.speedOut, - easing : currentOpts.easingOut, - step : _draw, - complete : _cleanup - }); - - } else { - wrap.fadeOut( currentOpts.transitionOut == 'none' ? 0 : currentOpts.speedOut, _cleanup); - } - }; - - $.fancybox.resize = function() { - if (overlay.is(':visible')) { - overlay.css('height', $(document).height()); - } - - $.fancybox.center(true); - }; - - $.fancybox.center = function() { - var view, align; - - if (busy) { - return; - } - - align = arguments[0] === true ? 1 : 0; - view = _get_viewport(); - - if (!align && (wrap.width() > view[0] || wrap.height() > view[1])) { - return; - } - - wrap - .stop() - .animate({ - 'top' : parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - content.height() - 40) * 0.5) - currentOpts.padding)), - 'left' : parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - content.width() - 40) * 0.5) - currentOpts.padding)) - }, typeof arguments[0] == 'number' ? arguments[0] : 200); - }; - - $.fancybox.init = function() { - if ($("#fancybox-wrap").length) { - return; - } - - $('body').append( - tmp = $('
'), - loading = $('
'), - overlay = $('
'), - wrap = $('
') - ); - - outer = $('
') - .append('
') - .appendTo( wrap ); - - outer.append( - content = $('
'), - close = $(''), - title = $('
'), - - nav_left = $(''), - nav_right = $('') - ); - - close.click($.fancybox.close); - loading.click($.fancybox.cancel); - - nav_left.click(function(e) { - e.preventDefault(); - $.fancybox.prev(); - }); - - nav_right.click(function(e) { - e.preventDefault(); - $.fancybox.next(); - }); - - if ($.fn.mousewheel) { - wrap.bind('mousewheel.fb', function(e, delta) { - if (busy) { - e.preventDefault(); - - } else if ($(e.target).get(0).clientHeight == 0 || $(e.target).get(0).scrollHeight === $(e.target).get(0).clientHeight) { - e.preventDefault(); - $.fancybox[ delta > 0 ? 'prev' : 'next'](); - } - }); - } - - if (!$.support.opacity) { - wrap.addClass('fancybox-ie'); - } - - if (isIE6) { - loading.addClass('fancybox-ie6'); - wrap.addClass('fancybox-ie6'); - - $('').prependTo(outer); - } - }; - - $.fn.fancybox.defaults = { - padding : 10, - margin : 40, - opacity : false, - modal : false, - cyclic : false, - scrolling : 'auto', // 'auto', 'yes' or 'no' - - width : 560, - height : 340, - - autoScale : true, - autoDimensions : true, - centerOnScroll : false, - - ajax : {}, - swf : { wmode: 'transparent' }, - - hideOnOverlayClick : true, - hideOnContentClick : false, - - overlayShow : true, - overlayOpacity : 0.7, - overlayColor : '#777', - - titleShow : true, - titlePosition : 'float', // 'float', 'outside', 'inside' or 'over' - titleFormat : null, - titleFromAlt : false, - - transitionIn : 'fade', // 'elastic', 'fade' or 'none' - transitionOut : 'fade', // 'elastic', 'fade' or 'none' - - speedIn : 300, - speedOut : 300, - - changeSpeed : 300, - changeFade : 'fast', - - easingIn : 'swing', - easingOut : 'swing', - - showCloseButton : true, - showNavArrows : true, - enableEscapeButton : true, - enableKeyboardNav : true, - - onStart : function(){}, - onCancel : function(){}, - onComplete : function(){}, - onCleanup : function(){}, - onClosed : function(){}, - onError : function(){} - }; - - $(document).ready(function() { - $.fancybox.init(); - }); - +/* + * FancyBox - jQuery Plugin + * Simple and fancy lightbox alternative + * + * Examples and documentation at: http://fancybox.net + * + * Copyright (c) 2008 - 2010 Janis Skarnelis + * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated. + * + * Version: 1.3.4 (11/11/2010) + * Requires: jQuery v1.3+ + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + */ + +;(function($) { + var tmp, loading, overlay, wrap, outer, content, close, title, nav_left, nav_right, + + selectedIndex = 0, selectedOpts = {}, selectedArray = [], currentIndex = 0, currentOpts = {}, currentArray = [], + + ajaxLoader = null, imgPreloader = new Image(), imgRegExp = /\.(jpg|gif|png|bmp|jpeg)(.*)?$/i, swfRegExp = /[^\.]\.(swf)\s*$/i, + + loadingTimer, loadingFrame = 1, + + titleHeight = 0, titleStr = '', start_pos, final_pos, busy = false, fx = $.extend($('
')[0], { prop: 0 }), + + isIE6 = $.browser.msie && $.browser.version < 7 && !window.XMLHttpRequest, + + /* + * Private methods + */ + + _abort = function() { + loading.hide(); + + imgPreloader.onerror = imgPreloader.onload = null; + + if (ajaxLoader) { + ajaxLoader.abort(); + } + + tmp.empty(); + }, + + _error = function() { + if (false === selectedOpts.onError(selectedArray, selectedIndex, selectedOpts)) { + loading.hide(); + busy = false; + return; + } + + selectedOpts.titleShow = false; + + selectedOpts.width = 'auto'; + selectedOpts.height = 'auto'; + + tmp.html( '

The requested content cannot be loaded.
Please try again later.

' ); + + _process_inline(); + }, + + _start = function() { + var obj = selectedArray[ selectedIndex ], + href, + type, + title, + str, + emb, + ret; + + _abort(); + + selectedOpts = $.extend({}, $.fn.fancybox.defaults, (typeof $(obj).data('fancybox') == 'undefined' ? selectedOpts : $(obj).data('fancybox'))); + + ret = selectedOpts.onStart(selectedArray, selectedIndex, selectedOpts); + + if (ret === false) { + busy = false; + return; + } else if (typeof ret == 'object') { + selectedOpts = $.extend(selectedOpts, ret); + } + + title = selectedOpts.title || (obj.nodeName ? $(obj).attr('title') : obj.title) || ''; + + if (obj.nodeName && !selectedOpts.orig) { + selectedOpts.orig = $(obj).children("img:first").length ? $(obj).children("img:first") : $(obj); + } + + if (title === '' && selectedOpts.orig && selectedOpts.titleFromAlt) { + title = selectedOpts.orig.attr('alt'); + } + + href = selectedOpts.href || (obj.nodeName ? $(obj).attr('href') : obj.href) || null; + + if ((/^(?:javascript)/i).test(href) || href == '#') { + href = null; + } + + if (selectedOpts.type) { + type = selectedOpts.type; + + if (!href) { + href = selectedOpts.content; + } + + } else if (selectedOpts.content) { + type = 'html'; + + } else if (href) { + if (href.match(imgRegExp)) { + type = 'image'; + + } else if (href.match(swfRegExp)) { + type = 'swf'; + + } else if ($(obj).hasClass("iframe")) { + type = 'iframe'; + + } else if (href.indexOf("#") === 0) { + type = 'inline'; + + } else { + type = 'ajax'; + } + } + + if (!type) { + _error(); + return; + } + + if (type == 'inline') { + obj = href.substr(href.indexOf("#")); + type = $(obj).length > 0 ? 'inline' : 'ajax'; + } + + selectedOpts.type = type; + selectedOpts.href = href; + selectedOpts.title = title; + + if (selectedOpts.autoDimensions) { + if (selectedOpts.type == 'html' || selectedOpts.type == 'inline' || selectedOpts.type == 'ajax') { + selectedOpts.width = 'auto'; + selectedOpts.height = 'auto'; + } else { + selectedOpts.autoDimensions = false; + } + } + + if (selectedOpts.modal) { + selectedOpts.overlayShow = true; + selectedOpts.hideOnOverlayClick = false; + selectedOpts.hideOnContentClick = false; + selectedOpts.enableEscapeButton = false; + selectedOpts.showCloseButton = false; + } + + selectedOpts.padding = parseInt(selectedOpts.padding, 10); + selectedOpts.margin = parseInt(selectedOpts.margin, 10); + + tmp.css('padding', (selectedOpts.padding + selectedOpts.margin)); + + $('.fancybox-inline-tmp').unbind('fancybox-cancel').bind('fancybox-change', function() { + $(this).replaceWith(content.children()); + }); + + switch (type) { + case 'html' : + tmp.html( selectedOpts.content ); + _process_inline(); + break; + + case 'inline' : + if ( $(obj).parent().is('#fancybox-content') === true) { + busy = false; + return; + } + + $('
') + .hide() + .insertBefore( $(obj) ) + .bind('fancybox-cleanup', function() { + $(this).replaceWith(content.children()); + }).bind('fancybox-cancel', function() { + $(this).replaceWith(tmp.children()); + }); + + $(obj).appendTo(tmp); + + _process_inline(); + break; + + case 'image': + busy = false; + + $.fancybox.showActivity(); + + imgPreloader = new Image(); + + imgPreloader.onerror = function() { + _error(); + }; + + imgPreloader.onload = function() { + busy = true; + + imgPreloader.onerror = imgPreloader.onload = null; + + _process_image(); + }; + + imgPreloader.src = href; + break; + + case 'swf': + selectedOpts.scrolling = 'no'; + + str = ''; + emb = ''; + + $.each(selectedOpts.swf, function(name, val) { + str += ''; + emb += ' ' + name + '="' + val + '"'; + }); + + str += ''; + + tmp.html(str); + + _process_inline(); + break; + + case 'ajax': + busy = false; + + $.fancybox.showActivity(); + + selectedOpts.ajax.win = selectedOpts.ajax.success; + + ajaxLoader = $.ajax($.extend({}, selectedOpts.ajax, { + url : href, + data : selectedOpts.ajax.data || {}, + error : function(XMLHttpRequest, textStatus, errorThrown) { + if ( XMLHttpRequest.status > 0 ) { + _error(); + } + }, + success : function(data, textStatus, XMLHttpRequest) { + var o = typeof XMLHttpRequest == 'object' ? XMLHttpRequest : ajaxLoader; + if (o.status == 200) { + if ( typeof selectedOpts.ajax.win == 'function' ) { + ret = selectedOpts.ajax.win(href, data, textStatus, XMLHttpRequest); + + if (ret === false) { + loading.hide(); + return; + } else if (typeof ret == 'string' || typeof ret == 'object') { + data = ret; + } + } + + tmp.html( data ); + _process_inline(); + } + } + })); + + break; + + case 'iframe': + _show(); + break; + } + }, + + _process_inline = function() { + var + w = selectedOpts.width, + h = selectedOpts.height; + + if (w.toString().indexOf('%') > -1) { + w = parseInt( ($(window).width() - (selectedOpts.margin * 2)) * parseFloat(w) / 100, 10) + 'px'; + + } else { + w = w == 'auto' ? 'auto' : w + 'px'; + } + + if (h.toString().indexOf('%') > -1) { + h = parseInt( ($(window).height() - (selectedOpts.margin * 2)) * parseFloat(h) / 100, 10) + 'px'; + + } else { + h = h == 'auto' ? 'auto' : h + 'px'; + } + + tmp.wrapInner('
'); + + selectedOpts.width = tmp.width(); + selectedOpts.height = tmp.height(); + + _show(); + }, + + _process_image = function() { + selectedOpts.width = imgPreloader.width; + selectedOpts.height = imgPreloader.height; + + $("").attr({ + 'id' : 'fancybox-img', + 'src' : imgPreloader.src, + 'alt' : selectedOpts.title + }).appendTo( tmp ); + + _show(); + }, + + _show = function() { + var pos, equal; + + loading.hide(); + + if (wrap.is(":visible") && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) { + $.event.trigger('fancybox-cancel'); + + busy = false; + return; + } + + busy = true; + + $(content.add( overlay )).unbind(); + + $(window).unbind("resize.fb scroll.fb"); + $(document).unbind('keydown.fb'); + + if (wrap.is(":visible") && currentOpts.titlePosition !== 'outside') { + wrap.css('height', wrap.height()); + } + + currentArray = selectedArray; + currentIndex = selectedIndex; + currentOpts = selectedOpts; + + if (currentOpts.overlayShow) { + overlay.css({ + 'background-color' : currentOpts.overlayColor, + 'opacity' : currentOpts.overlayOpacity, + 'cursor' : currentOpts.hideOnOverlayClick ? 'pointer' : 'auto', + 'height' : $(document).height() + }); + + if (!overlay.is(':visible')) { + if (isIE6) { + $('select:not(#fancybox-tmp select)').filter(function() { + return this.style.visibility !== 'hidden'; + }).css({'visibility' : 'hidden'}).one('fancybox-cleanup', function() { + this.style.visibility = 'inherit'; + }); + } + + overlay.show(); + } + } else { + overlay.hide(); + } + + final_pos = _get_zoom_to(); + + _process_title(); + + if (wrap.is(":visible")) { + $( close.add( nav_left ).add( nav_right ) ).hide(); + + pos = wrap.position(), + + start_pos = { + top : pos.top, + left : pos.left, + width : wrap.width(), + height : wrap.height() + }; + + equal = (start_pos.width == final_pos.width && start_pos.height == final_pos.height); + + content.fadeTo(currentOpts.changeFade, 0.3, function() { + var finish_resizing = function() { + content.html( tmp.contents() ).fadeTo(currentOpts.changeFade, 1, _finish); + }; + + $.event.trigger('fancybox-change'); + + content + .empty() + .removeAttr('filter') + .css({ + 'border-width' : currentOpts.padding, + 'width' : final_pos.width - currentOpts.padding * 2, + 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2 + }); + + if (equal) { + finish_resizing(); + + } else { + fx.prop = 0; + + $(fx).animate({prop: 1}, { + duration : currentOpts.changeSpeed, + easing : currentOpts.easingChange, + step : _draw, + complete : finish_resizing + }); + } + }); + + return; + } + + wrap.removeAttr("style"); + + content.css('border-width', currentOpts.padding); + + if (currentOpts.transitionIn == 'elastic') { + start_pos = _get_zoom_from(); + + content.html( tmp.contents() ); + + wrap.show(); + + if (currentOpts.opacity) { + final_pos.opacity = 0; + } + + fx.prop = 0; + + $(fx).animate({prop: 1}, { + duration : currentOpts.speedIn, + easing : currentOpts.easingIn, + step : _draw, + complete : _finish + }); + + return; + } + + if (currentOpts.titlePosition == 'inside' && titleHeight > 0) { + title.show(); + } + + content + .css({ + 'width' : final_pos.width - currentOpts.padding * 2, + 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2 + }) + .html( tmp.contents() ); + + wrap + .css(final_pos) + .fadeIn( currentOpts.transitionIn == 'none' ? 0 : currentOpts.speedIn, _finish ); + }, + + _format_title = function(title) { + if (title && title.length) { + if (currentOpts.titlePosition == 'float') { + return '
' + title + '
'; + } + + return '
' + title + '
'; + } + + return false; + }, + + _process_title = function() { + titleStr = currentOpts.title || ''; + titleHeight = 0; + + title + .empty() + .removeAttr('style') + .removeClass(); + + if (currentOpts.titleShow === false) { + title.hide(); + return; + } + + titleStr = $.isFunction(currentOpts.titleFormat) ? currentOpts.titleFormat(titleStr, currentArray, currentIndex, currentOpts) : _format_title(titleStr); + + if (!titleStr || titleStr === '') { + title.hide(); + return; + } + + title + .addClass('fancybox-title-' + currentOpts.titlePosition) + .html( titleStr ) + .appendTo( 'body' ) + .show(); + + switch (currentOpts.titlePosition) { + case 'inside': + title + .css({ + 'width' : final_pos.width - (currentOpts.padding * 2), + 'marginLeft' : currentOpts.padding, + 'marginRight' : currentOpts.padding + }); + + titleHeight = title.outerHeight(true); + + title.appendTo( outer ); + + final_pos.height += titleHeight; + break; + + case 'over': + title + .css({ + 'marginLeft' : currentOpts.padding, + 'width' : final_pos.width - (currentOpts.padding * 2), + 'bottom' : currentOpts.padding + }) + .appendTo( outer ); + break; + + case 'float': + title + .css('left', parseInt((title.width() - final_pos.width - 40)/ 2, 10) * -1) + .appendTo( wrap ); + break; + + default: + title + .css({ + 'width' : final_pos.width - (currentOpts.padding * 2), + 'paddingLeft' : currentOpts.padding, + 'paddingRight' : currentOpts.padding + }) + .appendTo( wrap ); + break; + } + + title.hide(); + }, + + _set_navigation = function() { + if (currentOpts.enableEscapeButton || currentOpts.enableKeyboardNav) { + $(document).bind('keydown.fb', function(e) { + if (e.keyCode == 27 && currentOpts.enableEscapeButton) { + e.preventDefault(); + $.fancybox.close(); + + } else if ((e.keyCode == 37 || e.keyCode == 39) && currentOpts.enableKeyboardNav && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'SELECT') { + e.preventDefault(); + $.fancybox[ e.keyCode == 37 ? 'prev' : 'next'](); + } + }); + } + + if (!currentOpts.showNavArrows) { + nav_left.hide(); + nav_right.hide(); + return; + } + + if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex !== 0) { + nav_left.show(); + } + + if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex != (currentArray.length -1)) { + nav_right.show(); + } + }, + + _finish = function () { + if (!$.support.opacity) { + content.get(0).style.removeAttribute('filter'); + wrap.get(0).style.removeAttribute('filter'); + } + + if (selectedOpts.autoDimensions) { + content.css('height', 'auto'); + } + + wrap.css('height', 'auto'); + + if (titleStr && titleStr.length) { + title.show(); + } + + if (currentOpts.showCloseButton) { + close.show(); + } + + _set_navigation(); + + if (currentOpts.hideOnContentClick) { + content.bind('click', $.fancybox.close); + } + + if (currentOpts.hideOnOverlayClick) { + overlay.bind('click', $.fancybox.close); + } + + $(window).bind("resize.fb", $.fancybox.resize); + + if (currentOpts.centerOnScroll) { + $(window).bind("scroll.fb", $.fancybox.center); + } + + if (currentOpts.type == 'iframe') { + $('').appendTo(content); + } + + wrap.show(); + + busy = false; + + $.fancybox.center(); + + currentOpts.onComplete(currentArray, currentIndex, currentOpts); + + _preload_images(); + }, + + _preload_images = function() { + var href, + objNext; + + if ((currentArray.length -1) > currentIndex) { + href = currentArray[ currentIndex + 1 ].href; + + if (typeof href !== 'undefined' && href.match(imgRegExp)) { + objNext = new Image(); + objNext.src = href; + } + } + + if (currentIndex > 0) { + href = currentArray[ currentIndex - 1 ].href; + + if (typeof href !== 'undefined' && href.match(imgRegExp)) { + objNext = new Image(); + objNext.src = href; + } + } + }, + + _draw = function(pos) { + var dim = { + width : parseInt(start_pos.width + (final_pos.width - start_pos.width) * pos, 10), + height : parseInt(start_pos.height + (final_pos.height - start_pos.height) * pos, 10), + + top : parseInt(start_pos.top + (final_pos.top - start_pos.top) * pos, 10), + left : parseInt(start_pos.left + (final_pos.left - start_pos.left) * pos, 10) + }; + + if (typeof final_pos.opacity !== 'undefined') { + dim.opacity = pos < 0.5 ? 0.5 : pos; + } + + wrap.css(dim); + + content.css({ + 'width' : dim.width - currentOpts.padding * 2, + 'height' : dim.height - (titleHeight * pos) - currentOpts.padding * 2 + }); + }, + + _get_viewport = function() { + return [ + $(window).width() - (currentOpts.margin * 2), + $(window).height() - (currentOpts.margin * 2), + $(document).scrollLeft() + currentOpts.margin, + $(document).scrollTop() + currentOpts.margin + ]; + }, + + _get_zoom_to = function () { + var view = _get_viewport(), + to = {}, + resize = currentOpts.autoScale, + double_padding = currentOpts.padding * 2, + ratio; + + if (currentOpts.width.toString().indexOf('%') > -1) { + to.width = parseInt((view[0] * parseFloat(currentOpts.width)) / 100, 10); + } else { + to.width = currentOpts.width + double_padding; + } + + if (currentOpts.height.toString().indexOf('%') > -1) { + to.height = parseInt((view[1] * parseFloat(currentOpts.height)) / 100, 10); + } else { + to.height = currentOpts.height + double_padding; + } + + if (resize && (to.width > view[0] || to.height > view[1])) { + if (selectedOpts.type == 'image' || selectedOpts.type == 'swf') { + ratio = (currentOpts.width ) / (currentOpts.height ); + + if ((to.width ) > view[0]) { + to.width = view[0]; + to.height = parseInt(((to.width - double_padding) / ratio) + double_padding, 10); + } + + if ((to.height) > view[1]) { + to.height = view[1]; + to.width = parseInt(((to.height - double_padding) * ratio) + double_padding, 10); + } + + } else { + to.width = Math.min(to.width, view[0]); + to.height = Math.min(to.height, view[1]); + } + } + + to.top = parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - to.height - 40) * 0.5)), 10); + to.left = parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - to.width - 40) * 0.5)), 10); + + return to; + }, + + _get_obj_pos = function(obj) { + var pos = obj.offset(); + + pos.top += parseInt( obj.css('paddingTop'), 10 ) || 0; + pos.left += parseInt( obj.css('paddingLeft'), 10 ) || 0; + + pos.top += parseInt( obj.css('border-top-width'), 10 ) || 0; + pos.left += parseInt( obj.css('border-left-width'), 10 ) || 0; + + pos.width = obj.width(); + pos.height = obj.height(); + + return pos; + }, + + _get_zoom_from = function() { + var orig = selectedOpts.orig ? $(selectedOpts.orig) : false, + from = {}, + pos, + view; + + if (orig && orig.length) { + pos = _get_obj_pos(orig); + + from = { + width : pos.width + (currentOpts.padding * 2), + height : pos.height + (currentOpts.padding * 2), + top : pos.top - currentOpts.padding - 20, + left : pos.left - currentOpts.padding - 20 + }; + + } else { + view = _get_viewport(); + + from = { + width : currentOpts.padding * 2, + height : currentOpts.padding * 2, + top : parseInt(view[3] + view[1] * 0.5, 10), + left : parseInt(view[2] + view[0] * 0.5, 10) + }; + } + + return from; + }, + + _animate_loading = function() { + if (!loading.is(':visible')){ + clearInterval(loadingTimer); + return; + } + + $('div', loading).css('top', (loadingFrame * -40) + 'px'); + + loadingFrame = (loadingFrame + 1) % 12; + }; + + /* + * Public methods + */ + + $.fn.fancybox = function(options) { + if (!$(this).length) { + return this; + } + + $(this) + .data('fancybox', $.extend({}, options, ($.metadata ? $(this).metadata() : {}))) + .unbind('click.fb') + .bind('click.fb', function(e) { + e.preventDefault(); + + if (busy) { + return; + } + + busy = true; + + $(this).blur(); + + selectedArray = []; + selectedIndex = 0; + + var rel = $(this).attr('rel') || ''; + + if (!rel || rel == '' || rel === 'nofollow') { + selectedArray.push(this); + + } else { + selectedArray = $("a[rel=" + rel + "], area[rel=" + rel + "]"); + selectedIndex = selectedArray.index( this ); + } + + _start(); + + return; + }); + + return this; + }; + + $.fancybox = function(obj) { + var opts; + + if (busy) { + return; + } + + busy = true; + opts = typeof arguments[1] !== 'undefined' ? arguments[1] : {}; + + selectedArray = []; + selectedIndex = parseInt(opts.index, 10) || 0; + + if ($.isArray(obj)) { + for (var i = 0, j = obj.length; i < j; i++) { + if (typeof obj[i] == 'object') { + $(obj[i]).data('fancybox', $.extend({}, opts, obj[i])); + } else { + obj[i] = $({}).data('fancybox', $.extend({content : obj[i]}, opts)); + } + } + + selectedArray = jQuery.merge(selectedArray, obj); + + } else { + if (typeof obj == 'object') { + $(obj).data('fancybox', $.extend({}, opts, obj)); + } else { + obj = $({}).data('fancybox', $.extend({content : obj}, opts)); + } + + selectedArray.push(obj); + } + + if (selectedIndex > selectedArray.length || selectedIndex < 0) { + selectedIndex = 0; + } + + _start(); + }; + + $.fancybox.showActivity = function() { + clearInterval(loadingTimer); + + loading.show(); + loadingTimer = setInterval(_animate_loading, 66); + }; + + $.fancybox.hideActivity = function() { + loading.hide(); + }; + + $.fancybox.next = function() { + return $.fancybox.pos( currentIndex + 1); + }; + + $.fancybox.prev = function() { + return $.fancybox.pos( currentIndex - 1); + }; + + $.fancybox.pos = function(pos) { + if (busy) { + return; + } + + pos = parseInt(pos); + + selectedArray = currentArray; + + if (pos > -1 && pos < currentArray.length) { + selectedIndex = pos; + _start(); + + } else if (currentOpts.cyclic && currentArray.length > 1) { + selectedIndex = pos >= currentArray.length ? 0 : currentArray.length - 1; + _start(); + } + + return; + }; + + $.fancybox.cancel = function() { + if (busy) { + return; + } + + busy = true; + + $.event.trigger('fancybox-cancel'); + + _abort(); + + selectedOpts.onCancel(selectedArray, selectedIndex, selectedOpts); + + busy = false; + }; + + // Note: within an iframe use - parent.$.fancybox.close(); + $.fancybox.close = function() { + if (busy || wrap.is(':hidden')) { + return; + } + + busy = true; + + if (currentOpts && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) { + busy = false; + return; + } + + _abort(); + + $(close.add( nav_left ).add( nav_right )).hide(); + + $(content.add( overlay )).unbind(); + + $(window).unbind("resize.fb scroll.fb"); + $(document).unbind('keydown.fb'); + + content.find('iframe').attr('src', isIE6 && /^https/i.test(window.location.href || '') ? 'javascript:void(false)' : 'about:blank'); + + if (currentOpts.titlePosition !== 'inside') { + title.empty(); + } + + wrap.stop(); + + function _cleanup() { + overlay.fadeOut('fast'); + + title.empty().hide(); + wrap.hide(); + + $.event.trigger('fancybox-cleanup'); + + content.empty(); + + currentOpts.onClosed(currentArray, currentIndex, currentOpts); + + currentArray = selectedOpts = []; + currentIndex = selectedIndex = 0; + currentOpts = selectedOpts = {}; + + busy = false; + } + + if (currentOpts.transitionOut == 'elastic') { + start_pos = _get_zoom_from(); + + var pos = wrap.position(); + + final_pos = { + top : pos.top , + left : pos.left, + width : wrap.width(), + height : wrap.height() + }; + + if (currentOpts.opacity) { + final_pos.opacity = 1; + } + + title.empty().hide(); + + fx.prop = 1; + + $(fx).animate({ prop: 0 }, { + duration : currentOpts.speedOut, + easing : currentOpts.easingOut, + step : _draw, + complete : _cleanup + }); + + } else { + wrap.fadeOut( currentOpts.transitionOut == 'none' ? 0 : currentOpts.speedOut, _cleanup); + } + }; + + $.fancybox.resize = function() { + if (overlay.is(':visible')) { + overlay.css('height', $(document).height()); + } + + $.fancybox.center(true); + }; + + $.fancybox.center = function() { + var view, align; + + if (busy) { + return; + } + + align = arguments[0] === true ? 1 : 0; + view = _get_viewport(); + + if (!align && (wrap.width() > view[0] || wrap.height() > view[1])) { + return; + } + + wrap + .stop() + .animate({ + 'top' : parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - content.height() - 40) * 0.5) - currentOpts.padding)), + 'left' : parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - content.width() - 40) * 0.5) - currentOpts.padding)) + }, typeof arguments[0] == 'number' ? arguments[0] : 200); + }; + + $.fancybox.init = function() { + if ($("#fancybox-wrap").length) { + return; + } + + $('body').append( + tmp = $('
'), + loading = $('
'), + overlay = $('
'), + wrap = $('
') + ); + + outer = $('
') + .append('
') + .appendTo( wrap ); + + outer.append( + content = $('
'), + close = $(''), + title = $('
'), + + nav_left = $(''), + nav_right = $('') + ); + + close.click($.fancybox.close); + loading.click($.fancybox.cancel); + + nav_left.click(function(e) { + e.preventDefault(); + $.fancybox.prev(); + }); + + nav_right.click(function(e) { + e.preventDefault(); + $.fancybox.next(); + }); + + if ($.fn.mousewheel) { + wrap.bind('mousewheel.fb', function(e, delta) { + if (busy) { + e.preventDefault(); + + } else if ($(e.target).get(0).clientHeight == 0 || $(e.target).get(0).scrollHeight === $(e.target).get(0).clientHeight) { + e.preventDefault(); + $.fancybox[ delta > 0 ? 'prev' : 'next'](); + } + }); + } + + if (!$.support.opacity) { + wrap.addClass('fancybox-ie'); + } + + if (isIE6) { + loading.addClass('fancybox-ie6'); + wrap.addClass('fancybox-ie6'); + + $('').prependTo(outer); + } + }; + + $.fn.fancybox.defaults = { + padding : 10, + margin : 40, + opacity : false, + modal : false, + cyclic : false, + scrolling : 'auto', // 'auto', 'yes' or 'no' + + width : 560, + height : 340, + + autoScale : true, + autoDimensions : true, + centerOnScroll : false, + + ajax : {}, + swf : { wmode: 'transparent' }, + + hideOnOverlayClick : true, + hideOnContentClick : false, + + overlayShow : true, + overlayOpacity : 0.7, + overlayColor : '#777', + + titleShow : true, + titlePosition : 'float', // 'float', 'outside', 'inside' or 'over' + titleFormat : null, + titleFromAlt : false, + + transitionIn : 'fade', // 'elastic', 'fade' or 'none' + transitionOut : 'fade', // 'elastic', 'fade' or 'none' + + speedIn : 300, + speedOut : 300, + + changeSpeed : 300, + changeFade : 'fast', + + easingIn : 'swing', + easingOut : 'swing', + + showCloseButton : true, + showNavArrows : true, + enableEscapeButton : true, + enableKeyboardNav : true, + + onStart : function(){}, + onCancel : function(){}, + onComplete : function(){}, + onCleanup : function(){}, + onClosed : function(){}, + onError : function(){} + }; + + $(document).ready(function() { + $.fancybox.init(); + }); + })(jQuery); \ No newline at end of file diff --git a/headphones/classes.py b/headphones/classes.py index cf6534bb..c6b14055 100644 --- a/headphones/classes.py +++ b/headphones/classes.py @@ -1,130 +1,130 @@ -# This file is part of Headphones. -# -# Headphones is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Headphones is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Headphones. If not, see . - -######################################### -## Stolen from Sick-Beard's classes.py ## -######################################### - -import headphones - -import urllib -import datetime - -from common import USER_AGENT - -class HeadphonesURLopener(urllib.FancyURLopener): - version = USER_AGENT - -class AuthURLOpener(HeadphonesURLopener): - """ - URLOpener class that supports http auth without needing interactive password entry. - If the provided username/password don't work it simply fails. - - user: username to use for HTTP auth - pw: password to use for HTTP auth - """ - def __init__(self, user, pw): - self.username = user - self.password = pw - - # remember if we've tried the username/password before - self.numTries = 0 - - # call the base class - urllib.FancyURLopener.__init__(self) - - def prompt_user_passwd(self, host, realm): - """ - Override this function and instead of prompting just give the - username/password that were provided when the class was instantiated. - """ - - # if this is the first try then provide a username/password - if self.numTries == 0: - self.numTries = 1 - return (self.username, self.password) - - # if we've tried before then return blank which cancels the request - else: - return ('', '') - - # this is pretty much just a hack for convenience - def openit(self, url): - self.numTries = 0 - return HeadphonesURLopener.open(self, url) - -class SearchResult: - """ - Represents a search result from an indexer. - """ - - def __init__(self): - self.provider = -1 - - # URL to the NZB/torrent file - self.url = "" - - # used by some providers to store extra info associated with the result - self.extraInfo = [] - - # quality of the release - self.quality = -1 - - # release name - self.name = "" - - def __str__(self): - - if self.provider == None: - return "Invalid provider, unable to print self" - - myString = self.provider.name + " @ " + self.url + "\n" - myString += "Extra Info:\n" - for extra in self.extraInfo: - myString += " " + extra + "\n" - return myString - -class NZBSearchResult(SearchResult): - """ - Regular NZB result with an URL to the NZB - """ - resultType = "nzb" - -class NZBDataSearchResult(SearchResult): - """ - NZB result where the actual NZB XML data is stored in the extraInfo - """ - resultType = "nzbdata" - -class TorrentSearchResult(SearchResult): - """ - Torrent result with an URL to the torrent - """ - resultType = "torrent" - -class Proper: - def __init__(self, name, url, date): - self.name = name - self.url = url - self.date = date - self.provider = None - self.quality = -1 - - self.tvdbid = -1 - self.season = -1 - self.episode = -1 - - def __str__(self): - return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid) +# This file is part of Headphones. +# +# Headphones is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Headphones is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Headphones. If not, see . + +######################################### +## Stolen from Sick-Beard's classes.py ## +######################################### + +import headphones + +import urllib +import datetime + +from common import USER_AGENT + +class HeadphonesURLopener(urllib.FancyURLopener): + version = USER_AGENT + +class AuthURLOpener(HeadphonesURLopener): + """ + URLOpener class that supports http auth without needing interactive password entry. + If the provided username/password don't work it simply fails. + + user: username to use for HTTP auth + pw: password to use for HTTP auth + """ + def __init__(self, user, pw): + self.username = user + self.password = pw + + # remember if we've tried the username/password before + self.numTries = 0 + + # call the base class + urllib.FancyURLopener.__init__(self) + + def prompt_user_passwd(self, host, realm): + """ + Override this function and instead of prompting just give the + username/password that were provided when the class was instantiated. + """ + + # if this is the first try then provide a username/password + if self.numTries == 0: + self.numTries = 1 + return (self.username, self.password) + + # if we've tried before then return blank which cancels the request + else: + return ('', '') + + # this is pretty much just a hack for convenience + def openit(self, url): + self.numTries = 0 + return HeadphonesURLopener.open(self, url) + +class SearchResult: + """ + Represents a search result from an indexer. + """ + + def __init__(self): + self.provider = -1 + + # URL to the NZB/torrent file + self.url = "" + + # used by some providers to store extra info associated with the result + self.extraInfo = [] + + # quality of the release + self.quality = -1 + + # release name + self.name = "" + + def __str__(self): + + if self.provider == None: + return "Invalid provider, unable to print self" + + myString = self.provider.name + " @ " + self.url + "\n" + myString += "Extra Info:\n" + for extra in self.extraInfo: + myString += " " + extra + "\n" + return myString + +class NZBSearchResult(SearchResult): + """ + Regular NZB result with an URL to the NZB + """ + resultType = "nzb" + +class NZBDataSearchResult(SearchResult): + """ + NZB result where the actual NZB XML data is stored in the extraInfo + """ + resultType = "nzbdata" + +class TorrentSearchResult(SearchResult): + """ + Torrent result with an URL to the torrent + """ + resultType = "torrent" + +class Proper: + def __init__(self, name, url, date): + self.name = name + self.url = url + self.date = date + self.provider = None + self.quality = -1 + + self.tvdbid = -1 + self.season = -1 + self.episode = -1 + + def __str__(self): + return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid) diff --git a/headphones/common.py b/headphones/common.py index c195855f..74aa1db8 100644 --- a/headphones/common.py +++ b/headphones/common.py @@ -1,177 +1,177 @@ -# This file is part of Headphones. -# -# Headphones is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Headphones is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Headphones. If not, see . - -''' -Created on Aug 1, 2011 - -@author: Michael -''' -import platform, operator, os, re - -from headphones import version - -#Identify Our Application -USER_AGENT = 'Headphones/-'+version.HEADPHONES_VERSION+' ('+platform.system()+' '+platform.release()+')' - -### Notification Types -NOTIFY_SNATCH = 1 -NOTIFY_DOWNLOAD = 2 - -notifyStrings = {} -notifyStrings[NOTIFY_SNATCH] = "Started Download" -notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished" - -### Release statuses -UNKNOWN = -1 # should never happen -UNAIRED = 1 # releases that haven't dropped yet -SNATCHED = 2 # qualified with quality -WANTED = 3 # releases we don't have but want to get -DOWNLOADED = 4 # qualified with quality -SKIPPED = 5 # releases we don't want -ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats) -IGNORED = 7 # releases that you don't want included in your download stats -SNATCHED_PROPER = 9 # qualified with quality - -class Quality: - - NONE = 0 - B192 = 1<<1 # 2 - VBR = 1<<2 # 4 - B256 = 1<<3 # 8 - B320 = 1<<4 #16 - FLAC = 1<<5 #32 - - # put these bits at the other end of the spectrum, far enough out that they shouldn't interfere - UNKNOWN = 1<<15 - - qualityStrings = {NONE: "N/A", - UNKNOWN: "Unknown", - B192: "MP3 192", - VBR: "MP3 VBR", - B256: "MP3 256", - B320: "MP3 320", - FLAC: "Flac"} - - statusPrefixes = {DOWNLOADED: "Downloaded", - SNATCHED: "Snatched"} - - @staticmethod - def _getStatusStrings(status): - toReturn = {} - for x in Quality.qualityStrings.keys(): - toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")" - return toReturn - - @staticmethod - def combineQualities(anyQualities, bestQualities): - anyQuality = 0 - bestQuality = 0 - if anyQualities: - anyQuality = reduce(operator.or_, anyQualities) - if bestQualities: - bestQuality = reduce(operator.or_, bestQualities) - return anyQuality | (bestQuality<<16) - - @staticmethod - def splitQuality(quality): - anyQualities = [] - bestQualities = [] - for curQual in Quality.qualityStrings.keys(): - if curQual & quality: - anyQualities.append(curQual) - if curQual<<16 & quality: - bestQualities.append(curQual) - - return (anyQualities, bestQualities) - - @staticmethod - def nameQuality(name): - - name = os.path.basename(name) - - # if we have our exact text then assume we put it there - for x in Quality.qualityStrings: - if x == Quality.UNKNOWN: - continue - - regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W' - regex_match = re.search(regex, name, re.I) - if regex_match: - return x - - checkName = lambda list, func: func([re.search(x, name, re.I) for x in list]) - - #TODO: fix quality checking here - if checkName(["mp3", "192"], any) and not checkName(["flac"], all): - return Quality.B192 - elif checkName(["mp3", "256"], any) and not checkName(["flac"], all): - return Quality.B256 - elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all): - return Quality.VBR - elif checkName(["mp3", "320"], any) and not checkName(["flac"], all): - return Quality.B320 - else: - return Quality.UNKNOWN - - @staticmethod - def assumeQuality(name): - - if name.lower().endswith(".mp3"): - return Quality.MP3 - elif name.lower().endswith(".flac"): - return Quality.LOSSLESS - else: - return Quality.UNKNOWN - - @staticmethod - def compositeStatus(status, quality): - return status + 100 * quality - - @staticmethod - def qualityDownloaded(status): - return (status - DOWNLOADED) / 100 - - @staticmethod - def splitCompositeStatus(status): - """Returns a tuple containing (status, quality)""" - for x in sorted(Quality.qualityStrings.keys(), reverse=True): - if status > x*100: - return (status-x*100, x) - - return (Quality.NONE, status) - - @staticmethod - def statusFromName(name, assume=True): - quality = Quality.nameQuality(name) - if assume and quality == Quality.UNKNOWN: - quality = Quality.assumeQuality(name) - return Quality.compositeStatus(DOWNLOADED, quality) - - DOWNLOADED = None - SNATCHED = None - SNATCHED_PROPER = None - -Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()] -Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()] -Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()] - -MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], []) -LOSSLESS = Quality.combineQualities([Quality.FLAC], []) -ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], []) - -qualityPresets = (MP3, LOSSLESS, ANY) -qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)", - LOSSLESS: "Lossless (flac)", - ANY: "Any"} +# This file is part of Headphones. +# +# Headphones is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Headphones is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Headphones. If not, see . + +''' +Created on Aug 1, 2011 + +@author: Michael +''' +import platform, operator, os, re + +from headphones import version + +#Identify Our Application +USER_AGENT = 'Headphones/-'+version.HEADPHONES_VERSION+' ('+platform.system()+' '+platform.release()+')' + +### Notification Types +NOTIFY_SNATCH = 1 +NOTIFY_DOWNLOAD = 2 + +notifyStrings = {} +notifyStrings[NOTIFY_SNATCH] = "Started Download" +notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished" + +### Release statuses +UNKNOWN = -1 # should never happen +UNAIRED = 1 # releases that haven't dropped yet +SNATCHED = 2 # qualified with quality +WANTED = 3 # releases we don't have but want to get +DOWNLOADED = 4 # qualified with quality +SKIPPED = 5 # releases we don't want +ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats) +IGNORED = 7 # releases that you don't want included in your download stats +SNATCHED_PROPER = 9 # qualified with quality + +class Quality: + + NONE = 0 + B192 = 1<<1 # 2 + VBR = 1<<2 # 4 + B256 = 1<<3 # 8 + B320 = 1<<4 #16 + FLAC = 1<<5 #32 + + # put these bits at the other end of the spectrum, far enough out that they shouldn't interfere + UNKNOWN = 1<<15 + + qualityStrings = {NONE: "N/A", + UNKNOWN: "Unknown", + B192: "MP3 192", + VBR: "MP3 VBR", + B256: "MP3 256", + B320: "MP3 320", + FLAC: "Flac"} + + statusPrefixes = {DOWNLOADED: "Downloaded", + SNATCHED: "Snatched"} + + @staticmethod + def _getStatusStrings(status): + toReturn = {} + for x in Quality.qualityStrings.keys(): + toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")" + return toReturn + + @staticmethod + def combineQualities(anyQualities, bestQualities): + anyQuality = 0 + bestQuality = 0 + if anyQualities: + anyQuality = reduce(operator.or_, anyQualities) + if bestQualities: + bestQuality = reduce(operator.or_, bestQualities) + return anyQuality | (bestQuality<<16) + + @staticmethod + def splitQuality(quality): + anyQualities = [] + bestQualities = [] + for curQual in Quality.qualityStrings.keys(): + if curQual & quality: + anyQualities.append(curQual) + if curQual<<16 & quality: + bestQualities.append(curQual) + + return (anyQualities, bestQualities) + + @staticmethod + def nameQuality(name): + + name = os.path.basename(name) + + # if we have our exact text then assume we put it there + for x in Quality.qualityStrings: + if x == Quality.UNKNOWN: + continue + + regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W' + regex_match = re.search(regex, name, re.I) + if regex_match: + return x + + checkName = lambda list, func: func([re.search(x, name, re.I) for x in list]) + + #TODO: fix quality checking here + if checkName(["mp3", "192"], any) and not checkName(["flac"], all): + return Quality.B192 + elif checkName(["mp3", "256"], any) and not checkName(["flac"], all): + return Quality.B256 + elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all): + return Quality.VBR + elif checkName(["mp3", "320"], any) and not checkName(["flac"], all): + return Quality.B320 + else: + return Quality.UNKNOWN + + @staticmethod + def assumeQuality(name): + + if name.lower().endswith(".mp3"): + return Quality.MP3 + elif name.lower().endswith(".flac"): + return Quality.LOSSLESS + else: + return Quality.UNKNOWN + + @staticmethod + def compositeStatus(status, quality): + return status + 100 * quality + + @staticmethod + def qualityDownloaded(status): + return (status - DOWNLOADED) / 100 + + @staticmethod + def splitCompositeStatus(status): + """Returns a tuple containing (status, quality)""" + for x in sorted(Quality.qualityStrings.keys(), reverse=True): + if status > x*100: + return (status-x*100, x) + + return (Quality.NONE, status) + + @staticmethod + def statusFromName(name, assume=True): + quality = Quality.nameQuality(name) + if assume and quality == Quality.UNKNOWN: + quality = Quality.assumeQuality(name) + return Quality.compositeStatus(DOWNLOADED, quality) + + DOWNLOADED = None + SNATCHED = None + SNATCHED_PROPER = None + +Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()] +Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()] +Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()] + +MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], []) +LOSSLESS = Quality.combineQualities([Quality.FLAC], []) +ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], []) + +qualityPresets = (MP3, LOSSLESS, ANY) +qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)", + LOSSLESS: "Lossless (flac)", + ANY: "Any"} diff --git a/headphones/exceptions.py b/headphones/exceptions.py index 47de6d99..ecbaa035 100644 --- a/headphones/exceptions.py +++ b/headphones/exceptions.py @@ -1,41 +1,41 @@ -# This file is part of Headphones. -# -# Headphones is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Headphones is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Headphones. If not, see . - -def ex(e): - """ - Returns a string from the exception text if it exists. - """ - - # sanity check - if not e.args or not e.args[0]: - return "" - - e_message = e.args[0] - - # if fixStupidEncodings doesn't fix it then maybe it's not a string, in which case we'll try printing it anyway - if not e_message: - try: - e_message = str(e.args[0]) - except: - e_message = "" - - return e_message - - -class HeadphonesException(Exception): - "Generic Headphones Exception - should never be thrown, only subclassed" - -class NewzbinAPIThrottled(HeadphonesException): - "Newzbin has throttled us, deal with it" +# This file is part of Headphones. +# +# Headphones is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Headphones is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Headphones. If not, see . + +def ex(e): + """ + Returns a string from the exception text if it exists. + """ + + # sanity check + if not e.args or not e.args[0]: + return "" + + e_message = e.args[0] + + # if fixStupidEncodings doesn't fix it then maybe it's not a string, in which case we'll try printing it anyway + if not e_message: + try: + e_message = str(e.args[0]) + except: + e_message = "" + + return e_message + + +class HeadphonesException(Exception): + "Generic Headphones Exception - should never be thrown, only subclassed" + +class NewzbinAPIThrottled(HeadphonesException): + "Newzbin has throttled us, deal with it" diff --git a/headphones/sab.py b/headphones/sab.py index a577f020..5249e914 100644 --- a/headphones/sab.py +++ b/headphones/sab.py @@ -1,162 +1,162 @@ -# This file is part of Headphones. -# -# Headphones is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Headphones is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Headphones. If not, see . - -##################################### -## Stolen from Sick-Beard's sab.py ## -##################################### - -import urllib, httplib -import datetime - -import headphones - -from lib import MultipartPostHandler -import urllib2, cookielib -import ast - -from headphones.common import USER_AGENT -from headphones import logger -from headphones import notifiers, helpers - -def sendNZB(nzb): - - params = {} - - if headphones.SAB_USERNAME: - params['ma_username'] = headphones.SAB_USERNAME - if headphones.SAB_PASSWORD: - params['ma_password'] = headphones.SAB_PASSWORD - if headphones.SAB_APIKEY: - params['apikey'] = headphones.SAB_APIKEY - if headphones.SAB_CATEGORY: - params['cat'] = headphones.SAB_CATEGORY - - # if it's a normal result we just pass SAB the URL - if nzb.resultType == "nzb": - # for newzbin results send the ID to sab specifically - if nzb.provider.getID() == 'newzbin': - id = nzb.provider.getIDFromURL(nzb.url) - if not id: - logger.info("Unable to send NZB to sab, can't find ID in URL "+str(nzb.url)) - return False - params['mode'] = 'addid' - params['name'] = id - else: - params['mode'] = 'addurl' - params['name'] = nzb.url - - # if we get a raw data result we want to upload it to SAB - elif nzb.resultType == "nzbdata": - # Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler - nzbdata = helpers.latinToAscii(nzb.extraInfo[0]) - params['mode'] = 'addfile' - multiPartParams = {"nzbfile": (nzb.name+".nzb", nzbdata)} - - if not headphones.SAB_HOST.startswith('http'): - headphones.SAB_HOST = 'http://' + headphones.SAB_HOST - - if headphones.SAB_HOST.endswith('/'): - headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1] - - url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params) - - try: - - if nzb.resultType == "nzb": - f = urllib.urlopen(url) - elif nzb.resultType == "nzbdata": - cookies = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), - MultipartPostHandler.MultipartPostHandler) - - req = urllib2.Request(url, - multiPartParams, - headers={'User-Agent': USER_AGENT}) - - f = opener.open(req) - - except (EOFError, IOError), e: - logger.error(u"Unable to connect to SAB with URL: %s" % url) - return False - - except httplib.InvalidURL, e: - logger.error(u"Invalid SAB host, check your config. Current host: %s" % headphones.SAB_HOST) - return False - - except Exception, e: - logger.error(u"Error: " + str(e)) - return False - - if f == None: - logger.info(u"No data returned from SABnzbd, NZB not sent") - return False - - try: - result = f.readlines() - except Exception, e: - logger.info(u"Error trying to get result from SAB, NZB not sent: ") - return False - - if len(result) == 0: - logger.info(u"No data returned from SABnzbd, NZB not sent") - return False - - sabText = result[0].strip() - - logger.info(u"Result text from SAB: " + sabText) - - if sabText == "ok": - logger.info(u"NZB sent to SAB successfully") - return True - elif sabText == "Missing authentication": - logger.info(u"Incorrect username/password sent to SAB, NZB not sent") - return False - else: - logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText) - return False - -def checkConfig(): - - params = { 'mode' : 'get_config', - 'section' : 'misc' - } - - if headphones.SAB_USERNAME: - params['ma_username'] = headphones.SAB_USERNAME - if headphones.SAB_PASSWORD: - params['ma_password'] = headphones.SAB_PASSWORD - if headphones.SAB_APIKEY: - params['apikey'] = headphones.SAB_APIKEY - - if not headphones.SAB_HOST.startswith('http'): - headphones.SAB_HOST = 'http://' + headphones.SAB_HOST - - if headphones.SAB_HOST.endswith('/'): - headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1] - - url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params) - - try: - f = urllib.urlopen(url).read() - except Exception, e: - logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)") - return (0, 0) - - config_options = ast.literal_eval(f) - - replace_spaces = config_options['misc']['replace_spaces'] - replace_dots = config_options['misc']['replace_dots'] - - return (replace_spaces, replace_dots) +# This file is part of Headphones. +# +# Headphones is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Headphones is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Headphones. If not, see . + +##################################### +## Stolen from Sick-Beard's sab.py ## +##################################### + +import urllib, httplib +import datetime + +import headphones + +from lib import MultipartPostHandler +import urllib2, cookielib +import ast + +from headphones.common import USER_AGENT +from headphones import logger +from headphones import notifiers, helpers + +def sendNZB(nzb): + + params = {} + + if headphones.SAB_USERNAME: + params['ma_username'] = headphones.SAB_USERNAME + if headphones.SAB_PASSWORD: + params['ma_password'] = headphones.SAB_PASSWORD + if headphones.SAB_APIKEY: + params['apikey'] = headphones.SAB_APIKEY + if headphones.SAB_CATEGORY: + params['cat'] = headphones.SAB_CATEGORY + + # if it's a normal result we just pass SAB the URL + if nzb.resultType == "nzb": + # for newzbin results send the ID to sab specifically + if nzb.provider.getID() == 'newzbin': + id = nzb.provider.getIDFromURL(nzb.url) + if not id: + logger.info("Unable to send NZB to sab, can't find ID in URL "+str(nzb.url)) + return False + params['mode'] = 'addid' + params['name'] = id + else: + params['mode'] = 'addurl' + params['name'] = nzb.url + + # if we get a raw data result we want to upload it to SAB + elif nzb.resultType == "nzbdata": + # Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler + nzbdata = helpers.latinToAscii(nzb.extraInfo[0]) + params['mode'] = 'addfile' + multiPartParams = {"nzbfile": (nzb.name+".nzb", nzbdata)} + + if not headphones.SAB_HOST.startswith('http'): + headphones.SAB_HOST = 'http://' + headphones.SAB_HOST + + if headphones.SAB_HOST.endswith('/'): + headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1] + + url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params) + + try: + + if nzb.resultType == "nzb": + f = urllib.urlopen(url) + elif nzb.resultType == "nzbdata": + cookies = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), + MultipartPostHandler.MultipartPostHandler) + + req = urllib2.Request(url, + multiPartParams, + headers={'User-Agent': USER_AGENT}) + + f = opener.open(req) + + except (EOFError, IOError), e: + logger.error(u"Unable to connect to SAB with URL: %s" % url) + return False + + except httplib.InvalidURL, e: + logger.error(u"Invalid SAB host, check your config. Current host: %s" % headphones.SAB_HOST) + return False + + except Exception, e: + logger.error(u"Error: " + str(e)) + return False + + if f == None: + logger.info(u"No data returned from SABnzbd, NZB not sent") + return False + + try: + result = f.readlines() + except Exception, e: + logger.info(u"Error trying to get result from SAB, NZB not sent: ") + return False + + if len(result) == 0: + logger.info(u"No data returned from SABnzbd, NZB not sent") + return False + + sabText = result[0].strip() + + logger.info(u"Result text from SAB: " + sabText) + + if sabText == "ok": + logger.info(u"NZB sent to SAB successfully") + return True + elif sabText == "Missing authentication": + logger.info(u"Incorrect username/password sent to SAB, NZB not sent") + return False + else: + logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText) + return False + +def checkConfig(): + + params = { 'mode' : 'get_config', + 'section' : 'misc' + } + + if headphones.SAB_USERNAME: + params['ma_username'] = headphones.SAB_USERNAME + if headphones.SAB_PASSWORD: + params['ma_password'] = headphones.SAB_PASSWORD + if headphones.SAB_APIKEY: + params['apikey'] = headphones.SAB_APIKEY + + if not headphones.SAB_HOST.startswith('http'): + headphones.SAB_HOST = 'http://' + headphones.SAB_HOST + + if headphones.SAB_HOST.endswith('/'): + headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1] + + url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params) + + try: + f = urllib.urlopen(url).read() + except Exception, e: + logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)") + return (0, 0) + + config_options = ast.literal_eval(f) + + replace_spaces = config_options['misc']['replace_spaces'] + replace_dots = config_options['misc']['replace_dots'] + + return (replace_spaces, replace_dots) diff --git a/lib/MultipartPostHandler.py b/lib/MultipartPostHandler.py index 82fa59c6..ef63afa2 100644 --- a/lib/MultipartPostHandler.py +++ b/lib/MultipartPostHandler.py @@ -1,88 +1,88 @@ -#!/usr/bin/python - -#### -# 06/2010 Nic Wolfe -# 02/2006 Will Holcomb -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# - -import urllib -import urllib2 -import mimetools, mimetypes -import os, sys - -# Controls how sequences are uncoded. If true, elements may be given multiple values by -# assigning a sequence. -doseq = 1 - -class MultipartPostHandler(urllib2.BaseHandler): - handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first - - def http_request(self, request): - data = request.get_data() - if data is not None and type(data) != str: - v_files = [] - v_vars = [] - try: - for(key, value) in data.items(): - if type(value) in (file, list, tuple): - v_files.append((key, value)) - else: - v_vars.append((key, value)) - except TypeError: - systype, value, traceback = sys.exc_info() - raise TypeError, "not a valid non-string sequence or mapping object", traceback - - if len(v_files) == 0: - data = urllib.urlencode(v_vars, doseq) - else: - boundary, data = MultipartPostHandler.multipart_encode(v_vars, v_files) - contenttype = 'multipart/form-data; boundary=%s' % boundary - if(request.has_header('Content-Type') - and request.get_header('Content-Type').find('multipart/form-data') != 0): - print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') - request.add_unredirected_header('Content-Type', contenttype) - - request.add_data(data) - return request - - @staticmethod - def multipart_encode(vars, files, boundary = None, buffer = None): - if boundary is None: - boundary = mimetools.choose_boundary() - if buffer is None: - buffer = '' - for(key, value) in vars: - buffer += '--%s\r\n' % boundary - buffer += 'Content-Disposition: form-data; name="%s"' % key - buffer += '\r\n\r\n' + value + '\r\n' - for(key, fd) in files: - - # allow them to pass in a file or a tuple with name & data - if type(fd) == file: - name_in = fd.name - fd.seek(0) - data_in = fd.read() - elif type(fd) in (tuple, list): - name_in, data_in = fd - - filename = os.path.basename(name_in) - contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' - buffer += '--%s\r\n' % boundary - buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename) - buffer += 'Content-Type: %s\r\n' % contenttype - # buffer += 'Content-Length: %s\r\n' % file_size - buffer += '\r\n' + data_in + '\r\n' - buffer += '--%s--\r\n\r\n' % boundary - return boundary, buffer - +#!/usr/bin/python + +#### +# 06/2010 Nic Wolfe +# 02/2006 Will Holcomb +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# + +import urllib +import urllib2 +import mimetools, mimetypes +import os, sys + +# Controls how sequences are uncoded. If true, elements may be given multiple values by +# assigning a sequence. +doseq = 1 + +class MultipartPostHandler(urllib2.BaseHandler): + handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first + + def http_request(self, request): + data = request.get_data() + if data is not None and type(data) != str: + v_files = [] + v_vars = [] + try: + for(key, value) in data.items(): + if type(value) in (file, list, tuple): + v_files.append((key, value)) + else: + v_vars.append((key, value)) + except TypeError: + systype, value, traceback = sys.exc_info() + raise TypeError, "not a valid non-string sequence or mapping object", traceback + + if len(v_files) == 0: + data = urllib.urlencode(v_vars, doseq) + else: + boundary, data = MultipartPostHandler.multipart_encode(v_vars, v_files) + contenttype = 'multipart/form-data; boundary=%s' % boundary + if(request.has_header('Content-Type') + and request.get_header('Content-Type').find('multipart/form-data') != 0): + print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') + request.add_unredirected_header('Content-Type', contenttype) + + request.add_data(data) + return request + + @staticmethod + def multipart_encode(vars, files, boundary = None, buffer = None): + if boundary is None: + boundary = mimetools.choose_boundary() + if buffer is None: + buffer = '' + for(key, value) in vars: + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"' % key + buffer += '\r\n\r\n' + value + '\r\n' + for(key, fd) in files: + + # allow them to pass in a file or a tuple with name & data + if type(fd) == file: + name_in = fd.name + fd.seek(0) + data_in = fd.read() + elif type(fd) in (tuple, list): + name_in, data_in = fd + + filename = os.path.basename(name_in) + contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename) + buffer += 'Content-Type: %s\r\n' % contenttype + # buffer += 'Content-Length: %s\r\n' % file_size + buffer += '\r\n' + data_in + '\r\n' + buffer += '--%s--\r\n\r\n' % boundary + return boundary, buffer + https_request = http_request \ No newline at end of file diff --git a/lib/__init__.py b/lib/__init__.py index d3f5a12f..8b137891 100755 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -1 +1 @@ - + diff --git a/lib/cherrypy/LICENSE.txt b/lib/cherrypy/LICENSE.txt index 8db13fb2..3816f933 100644 --- a/lib/cherrypy/LICENSE.txt +++ b/lib/cherrypy/LICENSE.txt @@ -1,25 +1,25 @@ -Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org) -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of the CherryPy Team nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the CherryPy Team nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/ordereddict.py b/lib/ordereddict.py index 5b0303f5..7242b506 100644 --- a/lib/ordereddict.py +++ b/lib/ordereddict.py @@ -1,127 +1,127 @@ -# Copyright (c) 2009 Raymond Hettinger -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. - -from UserDict import DictMixin - -class OrderedDict(dict, DictMixin): - - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) - - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) - - def __setitem__(self, key, value): - if key not in self: - end = self.__end - curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = reversed(self).next() - else: - key = iter(self).next() - value = self.pop(key) - return key, value - - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def keys(self): - return list(self) - - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: - return False - return True - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from UserDict import DictMixin + +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other From 676fa7199b7ad2a7a94a555d48aaf44ce75a0035 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 6 Aug 2014 17:02:08 +0200 Subject: [PATCH 07/11] Adding auto conversion to lf in gitattributes To make end-of-lines consistent in the future. Quoting http://git-scm.com/docs/gitattributes: When text is set to "auto", the path is marked for automatic end-of-line normalization. If Git decides that the content is text, its line endings are normalized to LF on checkin. --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..176a458f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto From 9b1d6eb6e015e6124bea6d497e1f6f950f3b55db Mon Sep 17 00:00:00 2001 From: David Date: Wed, 6 Aug 2014 17:25:49 +0200 Subject: [PATCH 08/11] gitignore vims .swp files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a8fd5b09..b22d691a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ Thumbs.db #Ignore files generated by PyCharm .idea/* +#Ignore files generated by vi +*.swp + #Ignore files build by Visual Studio *.obj *.exe From 6ce710dbb1f4d74e7ee1d9e3ff3be4c8809a37fc Mon Sep 17 00:00:00 2001 From: David Date: Wed, 6 Aug 2014 17:50:17 +0200 Subject: [PATCH 09/11] Cleaning up redundant code and long lines --- headphones/postprocessor.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index ddff3461..94b88700 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -421,24 +421,27 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, logger.info(u'Post-processing for %s - %s complete' % (release['ArtistName'], release['AlbumTitle'])) + pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] + statusmessage = "Download and Postprocessing completed" + if headphones.GROWL_ENABLED: - pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] logger.info(u"Growl request") growl = notifiers.GROWL() - growl.notify(pushmessage,"Download and Postprocessing completed") + growl.notify(pushmessage, statusmessage) if headphones.PROWL_ENABLED: - pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] logger.info(u"Prowl request") prowl = notifiers.PROWL() - prowl.notify(pushmessage,"Download and Postprocessing completed") + prowl.notify(pushmessage, statusmessage) if headphones.XBMC_ENABLED: xbmc = notifiers.XBMC() if headphones.XBMC_UPDATE: xbmc.update() if headphones.XBMC_NOTIFY: - xbmc.notify(release['ArtistName'], release['AlbumTitle'], album_art_path) + xbmc.notify(release['ArtistName'], + release['AlbumTitle'], + album_art_path) if headphones.LMS_ENABLED: lms = notifiers.LMS() @@ -449,17 +452,18 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, if headphones.PLEX_UPDATE: plex.update() if headphones.PLEX_NOTIFY: - plex.notify(release['ArtistName'], release['AlbumTitle'], album_art_path) + plex.notify(release['ArtistName'], + release['AlbumTitle'], + album_art_path) if headphones.NMA_ENABLED: nma = notifiers.NMA() nma.notify(release['ArtistName'], release['AlbumTitle']) if headphones.PUSHALOT_ENABLED: - pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] logger.info(u"Pushalot request") pushalot = notifiers.PUSHALOT() - pushalot.notify(pushmessage,"Download and Postprocessing completed") + pushalot.notify(pushmessage, statusmessage) if headphones.SYNOINDEX_ENABLED: syno = notifiers.Synoindex() @@ -467,19 +471,16 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, syno.notify(albumpath) if headphones.PUSHOVER_ENABLED: - pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] logger.info(u"Pushover request") pushover = notifiers.PUSHOVER() - pushover.notify(pushmessage,"Headphones") + pushover.notify(pushmessage, "Headphones") if headphones.PUSHBULLET_ENABLED: - pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] logger.info(u"PushBullet request") pushbullet = notifiers.PUSHBULLET() pushbullet.notify(pushmessage, "Download and Postprocessing completed") if headphones.TWITTER_ENABLED: - pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] logger.info(u"Sending Twitter notification") twitter = notifiers.TwitterNotifier() twitter.notify_download(pushmessage) @@ -487,13 +488,15 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, if headphones.OSX_NOTIFY_ENABLED: logger.info(u"Sending OS X notification") osx_notify = notifiers.OSX_NOTIFY() - osx_notify.notify(release['ArtistName'], release['AlbumTitle'], "Download and Postprocessing completed") + osx_notify.notify(release['ArtistName'], + release['AlbumTitle'], + statusmessage) if headphones.BOXCAR_ENABLED: - pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] logger.info(u"Sending Boxcar2 notification") boxcar = notifiers.BOXCAR() - boxcar.notify('Headphones processed: ' + pushmessage, "Download and Postprocessing completed", release['AlbumID']) + boxcar.notify('Headphones processed: ' + pushmessage, + statusmessage, release['AlbumID']) if headphones.MPC_ENABLED: mpc = notifiers.MPC() From 29ae400b801d86a781480be0a947f768c69e087b Mon Sep 17 00:00:00 2001 From: David Date: Wed, 6 Aug 2014 19:01:01 +0200 Subject: [PATCH 10/11] small typo: ebedding -> embedding --- headphones/postprocessor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index 94b88700..b824fcff 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -518,7 +518,7 @@ def embedAlbumArt(artwork, downloaded_track_list): f.art = artwork f.save() except Exception, e: - logger.error(u'Error ebedding album art to: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))) + logger.error(u'Error embedding album art to: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))) continue def addAlbumArt(artwork, albumpath, release): From c95bb9d29c439da1ce27f2f2ed4997f418c74589 Mon Sep 17 00:00:00 2001 From: piejanssens Date: Wed, 6 Aug 2014 19:59:59 +0200 Subject: [PATCH 11/11] Check for album's with artist in title Fixes #1788 and #1666 --- headphones/searcher.py | 51 +++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/headphones/searcher.py b/headphones/searcher.py index 13f571ac..ef889d5c 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -29,7 +29,7 @@ import string import shutil import requests import subprocess - +import unicodedata import headphones from headphones.common import USER_AGENT @@ -147,6 +147,11 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False): if data and bestqual: send_to_downloader(data, bestqual, album) +def removeDisallowedFilenameChars(filename): + validFilenameChars = "-_.() %s%s" % (string.ascii_letters, string.digits) + cleanedFilename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').lower() + return ''.join(c for c in cleanedFilename if c in validFilenameChars) + def more_filtering(results, album, albumlength, new): low_size_limit = None @@ -174,33 +179,39 @@ def more_filtering(results, album, albumlength, new): if headphones.PREFERRED_BITRATE_ALLOW_LOSSLESS: allow_lossless = True - if low_size_limit or high_size_limit or new: + newlist = [] - newlist = [] + for result in results: - for result in results: + normalizedAlbumArtist = removeDisallowedFilenameChars(album['ArtistName']) + normalizedAlbumTitle = removeDisallowedFilenameChars(album['AlbumTitle']) + normalizedResultTitle = removeDisallowedFilenameChars(result[0]); + artistTitleCount = normalizedResultTitle.count(normalizedAlbumArtist) - if low_size_limit and (int(result[1]) < low_size_limit): - logger.info("%s from %s is too small for this album - not considering it. (Size: %s, Minsize: %s)", result[0], result[3], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(low_size_limit)) + if normalizedAlbumArtist in normalizedAlbumTitle and artistTitleCount < 2: + continue + + if low_size_limit and (int(result[1]) < low_size_limit): + logger.info("%s from %s is too small for this album - not considering it. (Size: %s, Minsize: %s)", result[0], result[3], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(low_size_limit)) + continue + + if high_size_limit and (int(result[1]) > high_size_limit): + logger.info("%s from %s is too large for this album - not considering it. (Size: %s, Maxsize: %s)", result[0], result[3], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(high_size_limit)) + + # Keep lossless results if there are no good lossy matches + if not (allow_lossless and 'flac' in result[0].lower()): continue - if high_size_limit and (int(result[1]) > high_size_limit): - logger.info("%s from %s is too large for this album - not considering it. (Size: %s, Maxsize: %s)", result[0], result[3], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(high_size_limit)) + if new: + alreadydownloaded = myDB.select('SELECT * from snatched WHERE URL=?', [result[2]]) - # Keep lossless results if there are no good lossy matches - if not (allow_lossless and 'flac' in result[0].lower()): - continue + if len(alreadydownloaded): + logger.info('%s has already been downloaded from %s. Skipping.' % (result[0], result[3])) + continue - if new: - alreadydownloaded = myDB.select('SELECT * from snatched WHERE URL=?', [result[2]]) + newlist.append(result) - if len(alreadydownloaded): - logger.info('%s has already been downloaded from %s. Skipping.' % (result[0], result[3])) - continue - - newlist.append(result) - - results = newlist + results = newlist return results