From a1bc3617a45dbbd83cf6330f3d3a90ba1d720673 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 14:03:56 +0200 Subject: [PATCH 01/13] More logging + proper exception catching. --- headphones/librarysync.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/headphones/librarysync.py b/headphones/librarysync.py index 0fdd98c8..49a147cc 100644 --- a/headphones/librarysync.py +++ b/headphones/librarysync.py @@ -15,10 +15,10 @@ import os import glob - -from beets.mediafile import MediaFile - import headphones + +from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError + from headphones import db, logger, helpers, importer, lastfm # You can scan a single directory and append it to the current library by specifying append=True, ArtistID & ArtistName @@ -84,8 +84,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal for directory in d[:]: if directory.startswith("."): d.remove(directory) - for files in f: + for files in f: # MEDIA_FORMATS = music file extensions, e.g. mp3, flac, etc if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): @@ -104,9 +104,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal # Try to read the metadata try: f = MediaFile(song) - - except: - logger.error('Cannot read file: ' + unicode_song_path) + except (FileTypeError, UnreadableFileError): + logger.error("Cannot read file media file '%s'. It may be corrupted or not a media file.", unicode_song_path) continue # Grab the bitrates for the auto detect bit rate option From ca084f1216f9182ca356c683026784e469bedba9 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 22:18:35 +0200 Subject: [PATCH 02/13] It's more than that :-) --- Headphones.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Headphones.py b/Headphones.py index 41802d70..5d83fc68 100755 --- a/Headphones.py +++ b/Headphones.py @@ -63,7 +63,7 @@ def main(): headphones.SYS_ENCODING = 'UTF-8' # Set up and gather command line arguments - parser = argparse.ArgumentParser(description='Music add-on for SABnzbd+') + parser = argparse.ArgumentParser(description='Music add-on for SABnzbd+, Transmission and more.') parser.add_argument('-v', '--verbose', action='store_true', help='Increase console logging verbosity') parser.add_argument('-q', '--quiet', action='store_true', help='Turn off console logging') From 2e6c8e6537cb52612441b3b5b9912e1eac7eb6c7 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 22:26:20 +0200 Subject: [PATCH 03/13] Code improvements to the cache * Catch the right exceptions where possible * Move class vars to instance vars * Prefer 'with' statement for files --- headphones/cache.py | 74 +++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/headphones/cache.py b/headphones/cache.py index 0194600e..70d851e2 100644 --- a/headphones/cache.py +++ b/headphones/cache.py @@ -42,24 +42,22 @@ class Cache(object): path_to_art_cache = os.path.join(headphones.CACHE_DIR, 'artwork') - id = None - id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed - query_type = None # 'artwork','thumb' or 'info' - set automatically - - artwork_files = [] - thumb_files = [] - - artwork_errors = False - artwork_url = None - - thumb_errors = False - thumb_url = None - - info_summary = None - info_content = None - def __init__(self): - pass + self.id = None + self.id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed + self.query_type = None # 'artwork','thumb' or 'info' - set automatically + + self.artwork_files = [] + self.thumb_files = [] + + self.artwork_errors = False + self.artwork_url = None + + self.thumb_errors = False + self.thumb_url = None + + self.info_summary = None + self.info_content = None def _findfilesstartingwith(self,pattern,folder): files = [] @@ -125,9 +123,9 @@ class Cache(object): return thumb_url def get_artwork_from_cache(self, ArtistID=None, AlbumID=None): - ''' + """ Pass a musicbrainz id to this function (either ArtistID or AlbumID) - ''' + """ self.query_type = 'artwork' @@ -151,9 +149,9 @@ class Cache(object): return None def get_thumb_from_cache(self, ArtistID=None, AlbumID=None): - ''' + """ Pass a musicbrainz id to this function (either ArtistID or AlbumID) - ''' + """ self.query_type = 'thumb' @@ -215,7 +213,7 @@ class Cache(object): try: image_url = data['artist']['image'][-1]['#text'] - except Exception: + except (KeyError, IndexError): logger.debug('No artist image found') image_url = None @@ -233,7 +231,7 @@ class Cache(object): try: image_url = data['album']['image'][-1]['#text'] - except Exception: + except (KeyError, IndexError): logger.debug('No album image found on last.fm') image_url = None @@ -260,17 +258,17 @@ class Cache(object): try: self.info_summary = data['artist']['bio']['summary'] - except Exception: + except KeyError: logger.debug('No artist bio summary found') self.info_summary = None try: self.info_content = data['artist']['bio']['content'] - except Exception: + except KeyError: logger.debug('No artist bio found') self.info_content = None try: image_url = data['artist']['image'][-1]['#text'] - except Exception: + except KeyError: logger.debug('No artist image found') image_url = None @@ -288,17 +286,17 @@ class Cache(object): try: self.info_summary = data['album']['wiki']['summary'] - except Exception: + except KeyError: logger.debug('No album summary found') self.info_summary = None try: self.info_content = data['album']['wiki']['content'] - except Exception: + except KeyError: logger.debug('No album infomation found') self.info_content = None try: image_url = data['album']['image'][-1]['#text'] - except Exception: + except KeyError: logger.debug('No album image link found') image_url = None @@ -358,10 +356,9 @@ class Cache(object): artwork_path = os.path.join(self.path_to_art_cache, self.id + '.' + helpers.today() + ext) try: - f = open(artwork_path, 'wb') - f.write(artwork) - f.close() - except Exception, e: + with open(artwork_path, 'wb') as f: + f.write(artwork) + except IOError as e: logger.error('Unable to write to the cache dir: %s', e) self.artwork_errors = True self.artwork_url = image_url @@ -375,7 +372,7 @@ class Cache(object): if not os.path.isdir(self.path_to_art_cache): try: os.makedirs(self.path_to_art_cache) - except Exception, e: + except Exception as e: logger.error('Unable to create artwork cache dir. Error: %s' + e) self.thumb_errors = True self.thumb_url = thumb_url @@ -384,17 +381,16 @@ class Cache(object): for thumb_file in self.thumb_files: try: os.remove(thumb_file) - except: + except Exception as e: logger.error('Error deleting file from the cache: %s', thumb_file) ext = os.path.splitext(image_url)[1] thumb_path = os.path.join(self.path_to_art_cache, 'T_' + self.id + '.' + helpers.today() + ext) try: - f = open(thumb_path, 'wb') - f.write(artwork) - f.close() - except Exception, e: + with open(thumb_path, 'wb') as f: + f.write(artwork) + except IOError as e: logger.error('Unable to write to the cache dir: %s', e) self.thumb_errors = True self.thumb_url = image_url From e69cf3942e003812fc1b7e0b517ef703a6401ccd Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 22:36:30 +0200 Subject: [PATCH 04/13] Increased logging for Last.FM requests --- headphones/lastfm.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/headphones/lastfm.py b/headphones/lastfm.py index fdb456b3..bb752e96 100644 --- a/headphones/lastfm.py +++ b/headphones/lastfm.py @@ -30,8 +30,8 @@ def request_lastfm(method, **kwargs): Call a Last.FM API method. Automatically sets the method and API key. Method will return the result if no error occured. - By default, this method will request the JSON format, since it is lighter - than XML. + By default, this method will request the JSON format, since it is more + lightweight than XML. """ # Prepare request @@ -41,6 +41,8 @@ def request_lastfm(method, **kwargs): # Send request logger.debug("Calling Last.FM method: %s", method) + logger.debug("Last.FM call parameters: %s", kwargs) + data = request.request_json(ENTRY_POINT, timeout=TIMEOUT, params=kwargs) # Parse response and check for errors. @@ -49,7 +51,7 @@ def request_lastfm(method, **kwargs): return if "error" in data: - logger.debug("Last.FM returned an error: %s", data["message"]) + logger.error("Last.FM returned an error: %s", data["message"]) return return data From 69268b7d2149e843aee6954e5cfa76fea9796dd4 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 22:41:07 +0200 Subject: [PATCH 05/13] Proper line breaks for long lines --- headphones/request.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/headphones/request.py b/headphones/request.py index 1d4f6737..f5e8b5d9 100644 --- a/headphones/request.py +++ b/headphones/request.py @@ -54,7 +54,7 @@ def request_response(url, method="get", auto_raise=True, try: response.raise_for_status() except: - logger.debug("Response status code %d is not white " + + logger.debug("Response status code %d is not white " \ "listed, raised exception", response.status_code) raise elif auto_raise: @@ -62,17 +62,17 @@ def request_response(url, method="get", auto_raise=True, return response except requests.ConnectionError: - logger.error("Unable to connect to remote host. Check if the remote " + + logger.error("Unable to connect to remote host. Check if the remote " \ "host is up and running.") except requests.Timeout: - logger.error("Request timed out. The remote host did not respeond " + + logger.error("Request timed out. The remote host did not respeond " \ "timely.") except requests.HTTPError as e: if e.response is not None: if e.response.status_code >= 500: cause = "remote server error" elif e.response.status_code >= 400: - cause = "local request error" + cause = "local client error" else: # I don't think we will end up here, but for completeness cause = "unknown" From 3a03d940a1b9a8f2b97dbf77ab9d5161430abda2 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 22:54:08 +0200 Subject: [PATCH 06/13] Code improvements. Catch proper exceptions --- headphones/importer.py | 121 +++++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/headphones/importer.py b/headphones/importer.py index f082e4c0..59cba8b8 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -13,24 +13,26 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from lib.pyItunes import * -import time -import threading -import os -from beets.mediafile import MediaFile - -import headphones from headphones import logger, helpers, db, mb, lastfm -blacklisted_special_artist_names = ['[anonymous]','[data]','[no artist]','[traditional]','[unknown]','Various Artists'] -blacklisted_special_artists = ['f731ccc4-e22a-43af-a747-64213329e088','33cf029c-63b0-41a0-9855-be2a3665fb3b',\ - '314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc','eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',\ - '9be7f096-97ec-4615-8957-8d40b5dcbc41','125ec42a-7229-4250-afc5-e057484327fe',\ - '89ad4ac3-39f7-470e-963a-56509c546377'] +from beets.mediafile import MediaFile +import os +import time +import threading +import headphones + +blacklisted_special_artist_names = ['[anonymous]', '[data]', '[no artist]', + '[traditional]','[unknown]','Various Artists'] +blacklisted_special_artists = ['f731ccc4-e22a-43af-a747-64213329e088', + '33cf029c-63b0-41a0-9855-be2a3665fb3b', + '314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc', + 'eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61', + '9be7f096-97ec-4615-8957-8d40b5dcbc41', + '125ec42a-7229-4250-afc5-e057484327fe', + '89ad4ac3-39f7-470e-963a-56509c546377'] def is_exists(artistid): - myDB = db.DBConnection() # See if the artist is already in the database @@ -56,7 +58,7 @@ def artistlist_to_mbids(artistlist, forced=False): if not isinstance(artist, unicode): try: artist = artist.decode('utf-8', 'replace') - except: + except Exception: logger.warn("Unable to convert artist to unicode so cannot do a database lookup") continue @@ -68,7 +70,6 @@ def artistlist_to_mbids(artistlist, forced=False): try: artistid = results[0]['id'] - except IndexError: logger.info('MusicBrainz query turned up no matches for: %s' % artist) continue @@ -130,11 +131,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): # We need the current minimal info in the database instantly # so we don't throw a 500 error when we redirect to the artistPage - controlValueDict = {"ArtistID": artistid} # Don't replace a known artist name with an "Artist ID" placeholder - dbartist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [artistid]).fetchone() # Only modify the Include Extras stuff if it's a new artist. We need it early so we know what to fetch @@ -183,8 +182,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): myDB.upsert("artists", newValueDict, controlValueDict) - # See if we need to grab extras. Artist specific extras take precedence over global option - # Global options are set when adding a new artist + # See if we need to grab extras. Artist specific extras take precedence + # over global option. Global options are set when adding a new artist myDB = db.DBConnection() try: @@ -233,7 +232,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone() if not forcefull: - new_release_group = False try: @@ -244,12 +242,10 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): 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) @@ -263,33 +259,35 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): else: release_date = today if helpers.get_age(today) - helpers.get_age(release_date) < pause_delta: - logger.info("[%s] Now updating: %s (Release Date <%s Days) " % (artist['artist_name'], rg['title'], pause_delta)) + logger.info("[%s] Now updating: %s (Release Date <%s Days)", artist['artist_name'], rg['title'], pause_delta) new_releases = mb.get_new_releases(rgid,includeExtras,True) else: - logger.info("[%s] Skipping: %s (Release Date >%s Days)" % (artist['artist_name'], rg['title'], pause_delta)) + logger.info("[%s] Skipping: %s (Release Date >%s Days)", artist['artist_name'], rg['title'], pause_delta) skip_log = 1 new_releases = 0 if force_repackage == 1: new_releases = -1 - logger.info('[%s] Forcing repackage of %s (Release Group Removed)' % (artist['artist_name'], al_title)) + logger.info('[%s] Forcing repackage of %s (Release Group Removed)', artist['artist_name'], al_title) else: new_releases = new_releases else: - logger.info("[%s] Now adding/updating: %s (Comprehensive Force)" % (artist['artist_name'], rg['title'])) + logger.info("[%s] Now adding/updating: %s (Comprehensive Force)", artist['artist_name'], rg['title']) new_releases = mb.get_new_releases(rgid,includeExtras,forcefull) if new_releases != 0: - #Dump existing hybrid release since we're repackaging/replacing it + # Dump existing hybrid release since we're repackaging/replacing it myDB.action("DELETE from albums WHERE ReleaseID=?", [rg['id']]) myDB.action("DELETE from allalbums WHERE ReleaseID=?", [rg['id']]) myDB.action("DELETE from tracks WHERE ReleaseID=?", [rg['id']]) myDB.action("DELETE from alltracks WHERE ReleaseID=?", [rg['id']]) myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rg['id']]) + # This will be used later to build a hybrid release fullreleaselist = [] - #Search for releases within a release group + # Search for releases within a release group find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?", [rg['id']]) + # Build the dictionary for the fullreleaselist for items in find_hybrid_releases: if items['ReleaseID'] != rg['id']: #don't include hybrid information, since that's what we're replacing @@ -320,14 +318,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): newValueDict['Tracks'] = hybrid_track_array fullreleaselist.append(newValueDict) - #print fullreleaselist - # Basically just do the same thing again for the hybrid release # This may end up being called with an empty fullreleaselist try: hybridrelease = getHybridRelease(fullreleaselist) logger.info('[%s] Packaging %s releases into hybrid title' % (artist['artist_name'], rg['title'])) - except Exception, e: + except Exception as e: errors = True logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (artist['artist_name'],rg['title'],e)) continue @@ -387,7 +383,6 @@ 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: @@ -445,7 +440,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): continue for track in tracks: - controlValueDict = {"TrackID": track['TrackID'], "AlbumID": rg['id']} @@ -569,7 +563,7 @@ def addReleaseById(rid, rgid=None): logger.debug("Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID") try: release_dict = mb.getRelease(rid) - except Exception, e: + except Exception as e: logger.info('Unable to get release information for Release %s: %s', rid, e) if status == 'Loading': myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid]) @@ -639,7 +633,6 @@ def addReleaseById(rid, rgid=None): myDB.action('INSERT INTO releases VALUES( ?, ?)', [rid, release_dict['rgid']]) for track in release_dict['tracks']: - cleanname = helpers.cleanName(release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title']) controlValueDict = {"TrackID": track['id'], @@ -676,7 +669,7 @@ def addReleaseById(rid, rgid=None): newValueDict = {"Status": "Wanted"} myDB.upsert("albums", newValueDict, controlValueDict) - #start a search for the album + # Start a search for the album import searcher searcher.searchforalbum(rgid, False) elif not rg_exists and not release_dict: @@ -718,39 +711,43 @@ def updateFormat(): def getHybridRelease(fullreleaselist): """ - Returns a dictionary of best group of tracks from the list of releases & earliest release date + Returns a dictionary of best group of tracks from the list of releases and + earliest release date """ + if len(fullreleaselist) == 0: - raise Exception("getHybridRelease was called with an empty fullreleaselist") + raise ValueError("Empty fullreleaselist") + sortable_release_list = [] + formats = { + '2xVinyl': '2', + 'Vinyl': '2', + 'CD': '0', + 'Cassette': '3', + '2xCD': '1', + 'Digital Media': '0' + } + + countries = { + 'US': '0', + 'GB': '1', + 'JP': '2', + } + for release in fullreleaselist: - - formats = { - '2xVinyl': '2', - 'Vinyl': '2', - 'CD': '0', - 'Cassette': '3', - '2xCD': '1', - 'Digital Media': '0' - } - - countries = { - 'US': '0', - 'GB': '1', - 'JP': '2', - } - + # Find values for format and country try: format = int(formats[release['Format']]) - except: + except (ValueError, KeyError): format = 3 try: country = int(countries[release['Country']]) - except: + except (ValueError, KeyError): country = 3 + # Create record release_dict = { 'hasasin': bool(release['AlbumASIN']), 'asin': release['AlbumASIN'], @@ -760,15 +757,19 @@ def getHybridRelease(fullreleaselist): 'format': format, 'country': country, 'tracks': release['Tracks'] - } + } sortable_release_list.append(release_dict) - #necessary to make dates that miss the month and/or day show up after full dates + # Necessary to make dates that miss the month and/or day show up after full + # dates def getSortableReleaseDate(releaseDate): + # Change this value to change the sorting behaviour of none, returning + # 'None' will put it at the top which was normal behaviour for pre-ngs + # versions if releaseDate == None: - return 'None';#change this value to change the sorting behaviour of none, returning 'None' will put it at the top - #which was normal behaviour for pre-ngs versions + return 'None'; + if releaseDate.count('-') == 2: return releaseDate elif releaseDate.count('-') == 1: From 51dfeb819052c60f193096b520160af39dffe99f Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 22:54:45 +0200 Subject: [PATCH 07/13] myDB already available. Seems unneccesary to open another connection --- headphones/importer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/headphones/importer.py b/headphones/importer.py index 59cba8b8..3954366e 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -184,8 +184,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): # See if we need to grab extras. Artist specific extras take precedence # over global option. Global options are set when adding a new artist - myDB = db.DBConnection() - try: db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', [artistid]).fetchone() includeExtras = db_artist['IncludeExtras'] From 34b9480a1be09c912c2e5c2e1fbeb37dfd8cd3ea Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 22:58:05 +0200 Subject: [PATCH 08/13] pyItunes is not used anymore. --- lib/pyItunes/Library.py | 41 ---------------------------- lib/pyItunes/Song.py | 46 -------------------------------- lib/pyItunes/XMLLibraryParser.py | 42 ----------------------------- lib/pyItunes/__init__.py | 3 --- 4 files changed, 132 deletions(-) delete mode 100644 lib/pyItunes/Library.py delete mode 100644 lib/pyItunes/Song.py delete mode 100644 lib/pyItunes/XMLLibraryParser.py delete mode 100644 lib/pyItunes/__init__.py diff --git a/lib/pyItunes/Library.py b/lib/pyItunes/Library.py deleted file mode 100644 index 3118fb7b..00000000 --- a/lib/pyItunes/Library.py +++ /dev/null @@ -1,41 +0,0 @@ -from lib.pyItunes.Song import Song -import time -class Library: - def __init__(self,dictionary): - self.songs = self.parseDictionary(dictionary) - - def parseDictionary(self,dictionary): - songs = [] - format = "%Y-%m-%dT%H:%M:%SZ" - for song,attributes in dictionary.iteritems(): - s = Song() - s.name = attributes.get('Name') - s.artist = attributes.get('Artist') - s.album_artist = attributes.get('Album Aritst') - s.composer = attributes.get('Composer') - s.album = attributes.get('Album') - s.genre = attributes.get('Genre') - s.kind = attributes.get('Kind') - if attributes.get('Size'): - s.size = int(attributes.get('Size')) - s.total_time = attributes.get('Total Time') - s.track_number = attributes.get('Track Number') - if attributes.get('Year'): - s.year = int(attributes.get('Year')) - if attributes.get('Date Modified'): - s.date_modified = time.strptime(attributes.get('Date Modified'),format) - if attributes.get('Date Added'): - s.date_added = time.strptime(attributes.get('Date Added'),format) - if attributes.get('Bit Rate'): - s.bit_rate = int(attributes.get('Bit Rate')) - if attributes.get('Sample Rate'): - s.sample_rate = int(attributes.get('Sample Rate')) - s.comments = attributes.get("Comments ") - if attributes.get('Rating'): - s.rating = int(attributes.get('Rating')) - if attributes.get('Play Count'): - s.play_count = int(attributes.get('Play Count')) - if 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 deleted file mode 100644 index c59759e6..00000000 --- a/lib/pyItunes/Song.py +++ /dev/null @@ -1,46 +0,0 @@ -class Song: - """ - Song Attributes: - name (String) - artist (String) - album_arist (String) - composer = None (String) - album = None (String) - genre = None (String) - kind = None (String) - size = None (Integer) - total_time = None (Integer) - track_number = None (Integer) - year = None (Integer) - date_modified = None (Time) - date_added = None (Time) - bit_rate = None (Integer) - sample_rate = None (Integer) - comments = None (String) - rating = None (Integer) - album_rating = None (Integer) - play_count = None (Integer) - location = None (String) - """ - name = None - artist = None - album_arist = None - composer = None - album = None - genre = None - kind = None - size = None - total_time = None - track_number = None - year = None - date_modified = None - date_added = None - bit_rate = None - sample_rate = None - comments = None - rating = None - 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 deleted file mode 100644 index 6824aee7..00000000 --- a/lib/pyItunes/XMLLibraryParser.py +++ /dev/null @@ -1,42 +0,0 @@ -import re -class XMLLibraryParser: - def __init__(self,xmlLibrary): - f = open(xmlLibrary) - 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") - cleanValue = u.encode("ascii","xmlcharrefreplace") - return cleanValue - - def keyAndRestOfLine(self,line): - rawkey = re.search('(.*?)',line).group(0) - key = re.sub("","",rawkey) - restOfLine = re.sub(".*?","",line).strip() - return key,restOfLine - - def parser(self,lines): - dicts = 0 - songs = {} - inSong = False - for line in lines: - if re.search('',line): - dicts += 1 - if re.search('',line): - dicts -= 1 - inSong = False - songs[songkey] = temp - if dicts == 2 and re.search('(.*?)',line): - rawkey = re.search('(.*?)',line).group(0) - songkey = re.sub("","",rawkey) - inSong = True - temp = {} - if dicts == 3 and re.search('(.*?)',line): - key,restOfLine = self.keyAndRestOfLine(line) - temp[key] = self.getValue(restOfLine) - if len(songs) > 0 and dicts < 2: - return songs - return songs \ No newline at end of file diff --git a/lib/pyItunes/__init__.py b/lib/pyItunes/__init__.py deleted file mode 100644 index eb66d826..00000000 --- a/lib/pyItunes/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from lib.pyItunes.XMLLibraryParser import XMLLibraryParser -from lib.pyItunes.Library import Library -from lib.pyItunes.Song import Song \ No newline at end of file From 43efca9d0494df49ee4104aa100c1ffd6238fb54 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 22:58:19 +0200 Subject: [PATCH 09/13] Code improvements. --- headphones/importer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/headphones/importer.py b/headphones/importer.py index 3954366e..79d52c1d 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -190,10 +190,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): except IndexError: includeExtras = False - #Clean all references to release group in dB that are no longer referenced from the musicbrainz refresh + # Clean all references to release group in dB that are no longer referenced + # from the musicbrainz refresh group_list = [] force_repackage = 0 - #Don't nuke the database if there's a MusicBrainz error + + # Don't nuke the database if there's a MusicBrainz error if len(artist['releasegroups']) != 0: for groups in artist['releasegroups']: group_list.append(groups['id']) From 8ec1808313d68a58cfd62ff86626e7cdae6df00c Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 14 Sep 2014 23:14:29 +0200 Subject: [PATCH 10/13] Another batch of improvements: * Prefer 'with' statements for open * Catch proper exceptions * Remove ex() method, there was only one use and we catch thousands of exceptions. --- headphones/exceptions.py | 29 ++++++-------------------- headphones/helpers.py | 8 +++++--- headphones/notifiers.py | 9 ++++---- headphones/postprocessor.py | 17 ++++++++-------- headphones/searcher_rutracker.py | 35 +++++++++++++++----------------- headphones/transmission.py | 5 ++--- headphones/versioncheck.py | 22 ++++++++------------ headphones/webstart.py | 2 +- 8 files changed, 53 insertions(+), 74 deletions(-) diff --git a/headphones/exceptions.py b/headphones/exceptions.py index ecbaa035..a1e62f1a 100644 --- a/headphones/exceptions.py +++ b/headphones/exceptions.py @@ -13,29 +13,12 @@ # 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" + """ + Generic Headphones Exception - should never be thrown, only subclassed + """ class NewzbinAPIThrottled(HeadphonesException): - "Newzbin has throttled us, deal with it" + """ + Newzbin has throttled us, deal with it + """ diff --git a/headphones/helpers.py b/headphones/helpers.py index 30ab3caf..c6089cfd 100644 --- a/headphones/helpers.py +++ b/headphones/helpers.py @@ -603,9 +603,11 @@ def create_https_certificates(ssl_cert, ssl_key): # Save the key and certificate to disk try: - open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) - open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) - except Exception, e: + with open(ssl_key, 'w') as f: + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) + with open(ssl_cert, 'w') as f: + f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + except IOError as e: logger.error("Error creating SSL key and certificate: %s", e) return False diff --git a/headphones/notifiers.py b/headphones/notifiers.py index 52f0da28..39f82bd3 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -14,7 +14,6 @@ # along with Headphones. If not, see . from headphones import logger, helpers, common, request -from headphones.exceptions import ex from xml.dom import minidom from httplib import HTTPSConnection @@ -90,7 +89,9 @@ class GROWL: # Send it, including an image image_file = os.path.join(str(headphones.PROG_DIR), 'data/images/headphoneslogo.png') - image = open(image_file, 'rb').read() + + with open(image_file, 'rb') as f: + image = f.read() try: growl.notify( @@ -249,8 +250,8 @@ class XBMC: if not request: raise Exception - except: - logger.warn('Error sending notification request to XBMC') + except Exception: + logger.error('Error sending notification request to XBMC') class LMS: diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index 7fce5d30..9e88997a 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -569,25 +569,27 @@ def addAlbumArt(artwork, albumpath, release): album_art_name = album_art_name.replace(".", "_", 1) try: - file = open(os.path.join(albumpath, album_art_name), 'wb') - file.write(artwork) - file.close() - except Exception, e: - logger.error('Error saving album art: %s' % str(e)) + with open(os.path.join(albumpath, album_art_name), 'wb') as f: + f.write(artwork) + except IOError as e: + logger.error('Error saving album art: %s', e) return def cleanupFiles(albumpath): logger.info('Cleaning up files') + for r,d,f in os.walk(albumpath): for files in f: if not any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): logger.debug('Removing: %s' % files) try: os.remove(os.path.join(r, files)) - except Exception, e: + except Exception as e: logger.error(u'Could not remove file: %s. Error: %s' % (files.decode(headphones.SYS_ENCODING, 'replace'), e)) def renameNFO(albumpath): + logger.info('Renaming NFO') + for r,d,f in os.walk(albumpath): for file in f: if file.lower().endswith('.nfo'): @@ -595,11 +597,10 @@ def renameNFO(albumpath): try: new_file_name = os.path.join(r, file)[:-3] + 'orig.nfo' os.rename(os.path.join(r, file), new_file_name) - except Exception, e: + except Exception as e: logger.error(u'Could not rename file: %s. Error: %s' % (os.path.join(r, file).decode(headphones.SYS_ENCODING, 'replace'), e)) def moveFiles(albumpath, release, tracks): - try: year = release['ReleaseDate'][:4] except TypeError: diff --git a/headphones/searcher_rutracker.py b/headphones/searcher_rutracker.py index 2523bc04..d2c77c13 100644 --- a/headphones/searcher_rutracker.py +++ b/headphones/searcher_rutracker.py @@ -51,7 +51,7 @@ class Rutracker(): try: self.opener.open("http://login.rutracker.org/forum/login.php", params) - except : + except Exception: pass # Check if we're logged in @@ -286,17 +286,17 @@ class Rutracker(): else: tempdir = mkdtemp(suffix='_rutracker_torrents') download_path = os.path.join(tempdir, torrent_name) - fp = open (download_path, 'wb') - fp.write (torrent) - fp.close () + + with open(download_path, 'wb') as f: + f.write(torrent) os.umask(prev) # Add file to utorrent if headphones.TORRENT_DOWNLOADER == 2: self.utorrent_add_file(download_path) - except Exception, e: - logger.error('Error getting torrent: %s' % e) + except Exception as e: + logger.error('Error getting torrent: %s', e) return False return download_path, tor_hash @@ -322,9 +322,10 @@ class Rutracker(): try: r = session.get(url + 'token.html') - except: - logger.debug('Error getting token') + except Exception: + logger.exception('Error getting token') return + if r.status_code == '401': logger.debug('Error reaching utorrent') return @@ -336,15 +337,11 @@ class Rutracker(): session.params = {'token': regex.group(1)} - params = {'action': 'add-file'} - f = open(filename, 'rb') - files = {'torrent_file': f} - - try: - session.post(url, params=params, files=files) - except: - logger.debug('Error adding file to utorrent') - return - finally: - f.close() + with open(filename, 'rb') as f: + try: + session.post(url, params={'action': 'add-file'}, + files={'torrent_file': f}) + except Exception: + logger.exception('Error adding file to utorrent') + return diff --git a/headphones/transmission.py b/headphones/transmission.py index 046ccaa0..3a687a73 100644 --- a/headphones/transmission.py +++ b/headphones/transmission.py @@ -31,9 +31,8 @@ def addTorrent(link): method = 'torrent-add' if link.endswith('.torrent'): - f = open(link,'rb') - metainfo = str(base64.b64encode(f.read())) - f.close() + with open(link, 'rb') as f: + metainfo = str(base64.b64encode(f.read())) arguments = {'metainfo': metainfo, 'download-dir':headphones.DOWNLOAD_TORRENT_DIR} else: arguments = {'filename': link, 'download-dir': headphones.DOWNLOAD_TORRENT_DIR} diff --git a/headphones/versioncheck.py b/headphones/versioncheck.py index 4956dd55..236c590d 100644 --- a/headphones/versioncheck.py +++ b/headphones/versioncheck.py @@ -21,7 +21,6 @@ import headphones import subprocess from headphones import logger, version, request -from headphones.exceptions import ex def runGit(args): @@ -63,7 +62,6 @@ def runGit(args): def getVersion(): if version.HEADPHONES_VERSION.startswith('win32build'): - headphones.INSTALL_TYPE = 'win' # Don't have a way to update exe yet, but don't want to set VERSION to None @@ -109,9 +107,8 @@ def getVersion(): if not os.path.isfile(version_file): return None, 'master' - fp = open(version_file, 'r') - current_version = fp.read().strip(' \n\r') - fp.close() + with open(version_file, 'r') as f: + current_version = f.read().strip(' \n\r') if current_version: return current_version, headphones.GIT_BRANCH @@ -199,9 +196,8 @@ def update(): tar_download_path = os.path.join(headphones.PROG_DIR, download_name) # Save tar to disk - f = open(tar_download_path, 'wb') - f.write(data) - f.close() + with open(tar_download_path, 'wb') as f: + f.write(data) # Extract the tar to update folder logger.info('Extracting file: ' + tar_download_path) @@ -233,9 +229,9 @@ def update(): # Update version.txt try: - ver_file = open(version_path, 'w') - ver_file.write(str(headphones.LATEST_VERSION)) - ver_file.close() - except IOError, e: - logger.error("Unable to write current version to version.txt, update not complete: "+ex(e)) + with open(version_path, 'w') as f: + f.write(str(headphones.LATEST_VERSION)) + except IOError as e: + logger.error("Unable to write current version to version.txt, " \ + "update not complete: ", e) return diff --git a/headphones/webstart.py b/headphones/webstart.py index 6ab8213c..f6ac65ae 100644 --- a/headphones/webstart.py +++ b/headphones/webstart.py @@ -114,7 +114,7 @@ def initialize(options=None): # Prevent time-outs cherrypy.engine.timeout_monitor.unsubscribe() - cherrypy.tree.mount(WebInterface(), options['http_root'], config = conf) + cherrypy.tree.mount(WebInterface(), options['http_root'], config=conf) try: cherrypy.process.servers.check_port(options['http_host'], options['http_port']) From 21b3d992492efd36e676945e4672e95f13ced4f2 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 15 Sep 2014 00:40:32 +0200 Subject: [PATCH 11/13] Debug JSON responses --- headphones/request.py | 61 +++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/headphones/request.py b/headphones/request.py index f5e8b5d9..6cc0f7f2 100644 --- a/headphones/request.py +++ b/headphones/request.py @@ -80,25 +80,9 @@ def request_response(url, method="get", auto_raise=True, logger.error("Request raise HTTP error with status code %d (%s).", e.response.status_code, cause) - # Some servers return extra information in the result. Try to parse - # it for debugging purpose. Messages are limited to 150 characters, - # since it may return the whole page in case of normal web page URLs + # Debug response if headphones.VERBOSE: - if e.response.headers.get("content-type") == "text/html": - soup = BeautifulSoup(e.response.content, "html5lib") - - message = soup.find("body") - message = message.text if message else soup.text - message = message.strip() - else: - message = e.response.content.strip() - - if message: - # Truncate message if it is too long. - if len(message) > 150: - message = message[:150] + "..." - - logger.debug("Server responded with message: %s", message) + server_message(e.response) else: logger.error("Request raised HTTP error.") except requests.RequestException as e: @@ -144,12 +128,16 @@ def request_json(url, **kwargs): result = response.json() if validator and not validator(result): - logger.error("JSON validation result vailed") + logger.error("JSON validation result failed") else: return result except ValueError: logger.error("Response returned invalid JSON data") + # Debug response + if headphones.VERBOSE: + server_message(response) + def request_content(url, **kwargs): """ Wrapper for `request_response', which will return the raw content. @@ -168,4 +156,37 @@ def request_feed(url, **kwargs): response = request_response(url, **kwargs) if response is not None: - return feedparser.parse(response.content) \ No newline at end of file + return feedparser.parse(response.content) + +def server_message(response): + """ + Extract server message from response and log in to logger with DEBUG level. + + Some servers return extra information in the result. Try to parse it for + debugging purpose. Messages are limited to 150 characters, since it may + return the whole page in case of normal web page URLs + """ + + message = None + + # First attempt is to 'read' the response as HTML + if response.headers.get("content-type") == "text/html": + try: + soup = BeautifulSoup(response.content, "html5lib") + except Exception: + pass + + message = soup.find("body") + message = message.text if message else soup.text + message = message.strip() + + # Second attempt is to just take the response + if message is None: + message = response.content.strip() + + if message: + # Truncate message if it is too long. + if len(message) > 150: + message = message[:150] + "..." + + logger.debug("Server responded with message: %s", message) \ No newline at end of file From 638baf564d40d83b25b377dc3cb6b63ef4abf442 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 15 Sep 2014 00:54:30 +0200 Subject: [PATCH 12/13] Convert old-style notifier classes to new-style classes --- headphones/notifiers.py | 76 ++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/headphones/notifiers.py b/headphones/notifiers.py index 39f82bd3..98f83737 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -39,7 +39,10 @@ try: except ImportError: from cgi import parse_qsl -class GROWL: +class GROWL(object): + """ + Growl notifications, for OS X + """ def __init__(self): self.enabled = headphones.GROWL_ENABLED @@ -117,10 +120,10 @@ class GROWL: self.notify('ZOMG Lazors Pewpewpew!', 'Test Message') -class PROWL: - - keys = [] - priority = [] +class PROWL(object): + """ + Prowl notifications. + """ def __init__(self): self.enabled = headphones.PROWL_ENABLED @@ -164,25 +167,29 @@ class PROWL: return def test(self, keys, priority): - self.enabled = True self.keys = keys self.priority = priority self.notify('ZOMG Lazors Pewpewpew!', 'Test Message') -class MPC: +class MPC(object): + """ + MPC library update + """ def __init__(self): pass def notify( self ): - subprocess.call( ["mpc", "update"] ) -class XBMC: +class XBMC(object): + """ + XBMC notifications + """ def __init__(self): @@ -253,12 +260,12 @@ class XBMC: except Exception: logger.error('Error sending notification request to XBMC') -class LMS: - -#Class for updating a Logitech Media Server +class LMS(object): + """ + Class for updating a Logitech Media Server + """ def __init__(self): - self.hosts = headphones.LMS_HOST def _sendjson(self, host): @@ -294,8 +301,7 @@ class LMS: if not request: logger.warn('Error sending rescan request to LMS') -class Plex: - +class Plex(object): def __init__(self): self.server_hosts = headphones.PLEX_SERVER_HOST @@ -381,7 +387,7 @@ class Plex: except: logger.warn('Error sending notification request to Plex Media Server') -class NMA: +class NMA(object): def notify(self, artist=None, album=None, snatched=None): title = 'Headphones' api = headphones.NMA_APIKEY @@ -417,7 +423,7 @@ class NMA: else: return True -class PUSHBULLET: +class PUSHBULLET(object): def __init__(self): self.apikey = headphones.PUSHBULLET_APIKEY @@ -470,7 +476,7 @@ class PUSHBULLET: self.notify('Main Screen Activate', 'Test Message') -class PUSHALOT: +class PUSHALOT(object): def notify(self, message, event): if not headphones.PUSHALOT_ENABLED: @@ -509,7 +515,7 @@ class PUSHALOT: logger.info(u"Pushalot notification failed.") return False -class Synoindex: +class Synoindex(object): def __init__(self, util_loc='/usr/syno/bin/synoindex'): self.util_loc = util_loc @@ -545,19 +551,17 @@ class Synoindex: for path in path_list: self.notify(path) -class PUSHOVER: - - application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz" - keys = [] - priority = [] +class PUSHOVER(object): def __init__(self): self.enabled = headphones.PUSHOVER_ENABLED self.keys = headphones.PUSHOVER_KEYS self.priority = headphones.PUSHOVER_PRIORITY + if headphones.PUSHOVER_APITOKEN: self.application_token = headphones.PUSHOVER_APITOKEN - pass + else: + self.application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz" def conf(self, options): return cherrypy.config['config'].get('Pushover', options) @@ -599,23 +603,23 @@ class PUSHOVER: return def test(self, keys, priority): - self.enabled = True self.keys = keys self.priority = priority self.notify('Main Screen Activate', 'Test Message') -class TwitterNotifier: - - consumer_key = "oYKnp2ddX5gbARjqX8ZAAg" - consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk" +class TwitterNotifier(object): REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' + def __init__(self): + self.consumer_key = "oYKnp2ddX5gbARjqX8ZAAg" + self.consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk" + def notify_snatch(self, title): if headphones.TWITTER_ONSNATCH: self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH]+': '+title+' at '+helpers.now()) @@ -709,11 +713,7 @@ class TwitterNotifier: return self._send_tweet(prefix+": "+message) -notifier = TwitterNotifier - -class OSX_NOTIFY: - - objc = None +class OSX_NOTIFY(object): def __init__(self): try: @@ -768,14 +768,12 @@ class OSX_NOTIFY: def swizzled_bundleIdentifier(self, original, swizzled): return 'ade.headphones.osxnotify' -class BOXCAR: +class BOXCAR(object): def __init__(self): - self.url = 'https://new.boxcar.io/api/notifications' def notify(self, title, message, rgid=None): - try: if rgid: message += '

MusicBrainz' % rgid @@ -792,6 +790,6 @@ class BOXCAR: handle.close() return True - except urllib2.URLError, e: + except urllib2.URLError as e: logger.warn('Error sending Boxcar2 Notification: %s' % e) return False From f4b13533ce2f8a3a1326126dca8a132644c700dd Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 15 Sep 2014 01:29:40 +0200 Subject: [PATCH 13/13] Add support for Subsonic library update. Fixes #1864 --- data/interfaces/default/config.html | 40 ++++++++++++++++++++++++++++- headphones/__init__.py | 17 +++++++++++- headphones/notifiers.py | 19 ++++++++++++++ headphones/postprocessor.py | 5 ++++ headphones/webserve.py | 10 +++++++- 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index cb3aa513..46f7eedc 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -822,8 +822,26 @@ +
+

Subsonic

+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ - +

Synology NAS

@@ -1658,6 +1676,26 @@ } }); + if ($("#subsonic").is(":checked")) + { + $("#subsonicoptions").show(); + } + else + { + $("#subsonicoptions").hide(); + } + + $("#subsonic").click(function(){ + if ($("#subsonic").is(":checked")) + { + $("#subsonicoptions").slideDown(); + } + else + { + $("#subsonicoptions").slideUp(); + } + }); + if ($("#songkick").is(":checked")) { $("#songkickoptions").show(); diff --git a/headphones/__init__.py b/headphones/__init__.py index 80635061..d5bcb6e7 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -283,6 +283,10 @@ OSX_NOTIFY_APP = None BOXCAR_ENABLED = False BOXCAR_ONSNATCH = False BOXCAR_TOKEN = None +SUBSONIC_ENABLED = False +SUBSONIC_HOST = None +SUBSONIC_USERNAME = None +SUBSONIC_PASSWORD = None MIRRORLIST = ["musicbrainz.org","headphones","custom"] MIRROR = None CUSTOMHOST = None @@ -371,7 +375,7 @@ def initialize(): XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \ PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, LOSSLESS_BITRATE_FROM, LOSSLESS_BITRATE_TO, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \ PLEX_ENABLED, PLEX_SERVER_HOST, PLEX_CLIENT_HOST, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE, PLEX_NOTIFY, PUSHALOT_ENABLED, PUSHALOT_APIKEY, \ - PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED, VERIFY_SSL_CERT + PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED, SUBSONIC_ENABLED, SUBSONIC_HOST, SUBSONIC_USERNAME, SUBSONIC_PASSWORD, VERIFY_SSL_CERT if __INITIALIZED__: @@ -627,6 +631,11 @@ def initialize(): BOXCAR_ONSNATCH = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_onsnatch', 0)) BOXCAR_TOKEN = check_setting_str(CFG, 'Boxcar', 'boxcar_token', '') + SUBSONIC_ENABLED = bool(check_setting_int(CFG, 'Subsonic', 'subsonic_enabled', 0)) + SUBSONIC_HOST = check_setting_str(CFG, 'Subsonic', 'subsonic_host', '') + SUBSONIC_USERNAME = check_setting_str(CFG, 'Subsonic', 'subsonic_username', '') + SUBSONIC_PASSWORD = check_setting_str(CFG, 'Subsonic', 'subsonic_password', '') + SONGKICK_ENABLED = bool(check_setting_int(CFG, 'Songkick', 'songkick_enabled', 1)) SONGKICK_APIKEY = check_setting_str(CFG, 'Songkick', 'songkick_apikey', 'nd1We7dFW2RqxPw8') SONGKICK_LOCATION = check_setting_str(CFG, 'Songkick', 'songkick_location', '') @@ -1071,6 +1080,12 @@ def config_write(): new_config['Boxcar']['boxcar_onsnatch'] = int(BOXCAR_ONSNATCH) new_config['Boxcar']['boxcar_token'] = BOXCAR_TOKEN + new_config['Subsonic'] = {} + new_config['Subsonic']['subsonic_enabled'] = int(SUBSONIC_ENABLED) + new_config['Subsonic']['subsonic_host'] = SUBSONIC_HOST + new_config['Subsonic']['subsonic_username'] = SUBSONIC_USERNAME + new_config['Subsonic']['subsonic_password'] = SUBSONIC_PASSWORD + new_config['Songkick'] = {} new_config['Songkick']['songkick_enabled'] = int(SONGKICK_ENABLED) new_config['Songkick']['songkick_apikey'] = SONGKICK_APIKEY diff --git a/headphones/notifiers.py b/headphones/notifiers.py index 98f83737..b5749e7c 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -793,3 +793,22 @@ class BOXCAR(object): except urllib2.URLError as e: logger.warn('Error sending Boxcar2 Notification: %s' % e) return False + +class SubSonicNotifier(object): + + def __init__(self): + self.host = headphones.SUBSONIC_HOST + self.username = headphones.SUBSONIC_USERNAME + self.password = headphones.SUBSONIC_PASSWORD + + def notify(self, albumpaths): + # Correct URL + if not self.host.lower().startswith("http"): + self.host = "http://" + self.host + + if not self.host.lower().endswith("/"): + self.host = self.host + "/" + + # Invoke request + request.request_response(self.host + "musicFolderSettings.view?scanNow", + auth=(self.username, self.password)) \ No newline at end of file diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index 9e88997a..212f7055 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -519,6 +519,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, boxcar.notify('Headphones processed: ' + pushmessage, statusmessage, release['AlbumID']) + if headphones.SUBSONIC_ENABLED: + logger.info(u"Sending Subsonic update") + subsonic = notifiers.SubSonicNotifier() + subsonic.notify(albumpaths) + if headphones.MPC_ENABLED: mpc = notifiers.MPC() mpc.notify() diff --git a/headphones/webserve.py b/headphones/webserve.py index b4811e9d..89cc720d 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1124,6 +1124,10 @@ class WebInterface(object): "pushbullet_onsnatch": checked(headphones.PUSHBULLET_ONSNATCH), "pushbullet_apikey": headphones.PUSHBULLET_APIKEY, "pushbullet_deviceid": headphones.PUSHBULLET_DEVICEID, + "subsonic_enabled": checked(headphones.SUBSONIC_ENABLED), + "subsonic_host": headphones.SUBSONIC_HOST, + "subsonic_username": headphones.SUBSONIC_USERNAME, + "subsonic_password": headphones.SUBSONIC_PASSWORD, "twitter_enabled": checked(headphones.TWITTER_ENABLED), "twitter_onsnatch": checked(headphones.TWITTER_ONSNATCH), "osx_notify_enabled": checked(headphones.OSX_NOTIFY_ENABLED), @@ -1181,7 +1185,7 @@ class WebInterface(object): rutracker=0, rutracker_user=None, rutracker_password=None, rutracker_ratio=None, rename_files=0, correct_metadata=0, cleanup_files=0, keep_nfo=0, add_album_art=0, album_art_format=None, embed_album_art=0, embed_lyrics=0, replace_existing_folders=False, destination_dir=None, lossless_destination_dir=None, folder_format=None, file_format=None, file_underscores=0, include_extras=0, single=0, ep=0, compilation=0, soundtrack=0, live=0, remix=0, spokenword=0, audiobook=0, other=0, djmix=0, mixtape_street=0, broadcast=0, interview=0, demo=0, autowant_upcoming=False, autowant_all=False, keep_torrent_files=False, prefer_torrents=0, open_magnet_links=0, interface=None, log_dir=None, cache_dir=None, music_encoder=0, encoder=None, xldprofile=None, - bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0, + bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0, subsonic_enabled=False, subsonic_host=None, subsonic_username=None, subsonic_password=None, delete_lossless_files=0, growl_enabled=0, growl_onsnatch=0, growl_host=None, growl_password=None, prowl_enabled=0, prowl_onsnatch=0, prowl_keys=None, prowl_priority=0, xbmc_enabled=0, xbmc_host=None, xbmc_username=None, xbmc_password=None, xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, pushalot_enabled=False, pushalot_apikey=None, pushalot_onsnatch=0, synoindex_enabled=False, lms_enabled=0, lms_host=None, pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0, @@ -1350,6 +1354,10 @@ class WebInterface(object): headphones.PUSHBULLET_ONSNATCH = pushbullet_onsnatch headphones.PUSHBULLET_APIKEY = pushbullet_apikey headphones.PUSHBULLET_DEVICEID = pushbullet_deviceid + headphones.SUBSONIC_ENABLED = subsonic_enabled + headphones.SUBSONIC_HOST = subsonic_host + headphones.SUBSONIC_USERNAME = subsonic_username + headphones.SUBSONIC_PASSWORD = subsonic_password headphones.SONGKICK_ENABLED = songkick_enabled headphones.SONGKICK_APIKEY = songkick_apikey headphones.SONGKICK_LOCATION = songkick_location