diff --git a/headphones/__init__.py b/headphones/__init__.py index 52630d5f..eb48476d 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -16,21 +16,21 @@ # NZBGet support added by CurlyMo as a part of # XBian - XBMC on the Raspberry Pi -import os import sys import subprocess import threading import webbrowser import sqlite3 -import cherrypy import datetime +import os +import cherrypy from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger - from headphones import versioncheck, logger import headphones.config + # (append new extras to the end) POSSIBLE_EXTRAS = [ "single", @@ -94,7 +94,6 @@ UMASK = None def initialize(config_file): - with INIT_LOCK: global CONFIG @@ -131,11 +130,11 @@ def initialize(config_file): if not QUIET: sys.stderr.write("Unable to create the log directory. " \ - "Logging to screen only.\n") + "Logging to screen only.\n") # Start the logger, disable console if needed logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR, - verbose=VERBOSE) + verbose=VERBOSE) if not CONFIG.CACHE_DIR: # Put the cache dir in the data dir for now @@ -246,7 +245,6 @@ def daemonize(): def launch_browser(host, port, root): - if host == '0.0.0.0': host = 'localhost' @@ -287,17 +285,19 @@ def initialize_scheduler(): hours = CONFIG.UPDATE_DB_INTERVAL schedule_job(updater.dbUpdate, 'MusicBrainz Update', hours=hours, minutes=0) - #Update check + # Update check if CONFIG.CHECK_GITHUB: if CONFIG.CHECK_GITHUB_INTERVAL: minutes = CONFIG.CHECK_GITHUB_INTERVAL else: minutes = 0 - schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, minutes=minutes) + schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, + minutes=minutes) # Remove Torrent + data if Post Processed and finished Seeding minutes = CONFIG.TORRENT_REMOVAL_INTERVAL - schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0, minutes=minutes) + schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0, + minutes=minutes) # Start scheduler if start_jobs and len(SCHED.get_jobs()): @@ -306,8 +306,8 @@ def initialize_scheduler(): except Exception as e: logger.info(e) - # Debug - #SCHED.print_jobs() + # Debug + # SCHED.print_jobs() def schedule_job(function, name, hours=0, minutes=0): @@ -334,7 +334,6 @@ def schedule_job(function, name, hours=0, minutes=0): def start(): - global started if _INITIALIZED: @@ -349,7 +348,6 @@ def sig_handler(signum=None, frame=None): def dbcheck(): - conn = sqlite3.connect(DB_FILE) c = conn.cursor() c.execute( @@ -609,7 +607,6 @@ def dbcheck(): def shutdown(restart=False, update=False): - cherrypy.engine.exit() SCHED.shutdown(wait=False) diff --git a/headphones/albumswitcher.py b/headphones/albumswitcher.py index 1edf1937..d4d05aa1 100644 --- a/headphones/albumswitcher.py +++ b/headphones/albumswitcher.py @@ -53,7 +53,6 @@ def switch(AlbumID, ReleaseID): c.get_artwork_from_cache(AlbumID=AlbumID) for track in newtrackdata: - controlValueDict = {"TrackID": track['TrackID'], "AlbumID": AlbumID} @@ -79,15 +78,18 @@ def switch(AlbumID, ReleaseID): 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.CONFIG.ALBUM_COMPLETION_PCT / 100.0)): + if oldalbumdata['Status'] == 'Skipped' and ((have_track_count / float(total_track_count)) >= ( + headphones.CONFIG.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']])) + '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']])) + 'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', + [newalbumdata['ArtistID']])) controlValueDict = {"ArtistID": newalbumdata['ArtistID']} diff --git a/headphones/api.py b/headphones/api.py index 6a51f55d..b5f2e26a 100644 --- a/headphones/api.py +++ b/headphones/api.py @@ -13,21 +13,25 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from headphones import db, mb, updater, importer, searcher, cache, postprocessor, versioncheck, logger - -import headphones import json -cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSnatched', 'getSimilar', 'getHistory', 'getLogs', - 'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist', - 'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'forceActiveArtistsUpdate', - 'getVersion', 'checkGithub', 'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', +from headphones import db, mb, updater, importer, searcher, cache, postprocessor, versioncheck, \ + logger +import headphones + +cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSnatched', + 'getSimilar', 'getHistory', 'getLogs', + 'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', + 'refreshArtist', + 'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', + 'forceActiveArtistsUpdate', + 'getVersion', 'checkGithub', 'shutdown', 'restart', 'update', 'getArtistArt', + 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb', 'clearLogs', 'choose_specific_download', 'download_specific_release'] class Api(object): - def __init__(self): self.apikey = None @@ -170,7 +174,7 @@ class Api(object): self.data = self._dic_from_query( "SELECT * from albums WHERE Status='Snatched'") return - + def _getSimilar(self, **kwargs): self.data = self._dic_from_query('SELECT * from lastfmcloud') return @@ -432,7 +436,6 @@ class Api(object): results_as_dicts = [] for result in results: - result_dict = { 'title': result[0], 'size': result[1], diff --git a/headphones/cache.py b/headphones/cache.py index 1089a71d..166d5577 100644 --- a/headphones/cache.py +++ b/headphones/cache.py @@ -14,8 +14,8 @@ # along with Headphones. If not, see . import os -import headphones +import headphones from headphones import db, helpers, logger, lastfm, request LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4" @@ -45,8 +45,8 @@ class Cache(object): def __init__(self): 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.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 = [] @@ -182,13 +182,18 @@ class Cache(object): if ArtistID: self.id = ArtistID self.id_type = 'artist' - db_info = myDB.action('SELECT Summary, Content, LastUpdated FROM descriptions WHERE ArtistID=?', [self.id]).fetchone() + db_info = myDB.action( + 'SELECT Summary, Content, LastUpdated FROM descriptions WHERE ArtistID=?', + [self.id]).fetchone() else: self.id = AlbumID self.id_type = 'album' - db_info = myDB.action('SELECT Summary, Content, LastUpdated FROM descriptions WHERE ReleaseGroupID=?', [self.id]).fetchone() + db_info = myDB.action( + 'SELECT Summary, Content, LastUpdated FROM descriptions WHERE ReleaseGroupID=?', + [self.id]).fetchone() - if not db_info or not db_info['LastUpdated'] or not self._is_current(date=db_info['LastUpdated']): + if not db_info or not db_info['LastUpdated'] or not self._is_current( + date=db_info['LastUpdated']): self._update_cache() info_dict = {'Summary': self.info_summary, 'Content': self.info_content} @@ -309,13 +314,19 @@ class Cache(object): logger.debug('No artist thumbnail image found') else: - dbalbum = myDB.action('SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?', [self.id]).fetchone() + dbalbum = myDB.action( + 'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?', + [self.id]).fetchone() if dbalbum['ReleaseID'] != self.id: - data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'], api_key=LASTFM_API_KEY) + data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'], + api_key=LASTFM_API_KEY) if not data: - data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY) + data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], + album=dbalbum['AlbumTitle'], + api_key=LASTFM_API_KEY) else: - data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY) + data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], + album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY) if not data: return @@ -357,7 +368,8 @@ class Cache(object): # Save the image URL to the database if image_url: if self.id_type == 'artist': - myDB.action('UPDATE artists SET ArtworkURL=? WHERE ArtistID=?', [image_url, self.id]) + myDB.action('UPDATE artists SET ArtworkURL=? WHERE ArtistID=?', + [image_url, self.id]) else: myDB.action('UPDATE albums SET ArtworkURL=? WHERE AlbumID=?', [image_url, self.id]) @@ -378,7 +390,8 @@ class Cache(object): if not os.path.isdir(self.path_to_art_cache): try: os.makedirs(self.path_to_art_cache) - os.chmod(self.path_to_art_cache, int(headphones.CONFIG.FOLDER_PERMISSIONS, 8)) + os.chmod(self.path_to_art_cache, + int(headphones.CONFIG.FOLDER_PERMISSIONS, 8)) except OSError as e: logger.error('Unable to create artwork cache dir. Error: %s', e) self.artwork_errors = True @@ -393,7 +406,8 @@ class Cache(object): ext = os.path.splitext(image_url)[1] - artwork_path = os.path.join(self.path_to_art_cache, self.id + '.' + helpers.today() + ext) + artwork_path = os.path.join(self.path_to_art_cache, + self.id + '.' + helpers.today() + ext) try: with open(artwork_path, 'wb') as f: f.write(artwork) @@ -406,7 +420,8 @@ class Cache(object): # Grab the thumbnail as well if we're getting the full artwork (as long # as it's missing/outdated. - if thumb_url and self.query_type in ['thumb', 'artwork'] and not (self.thumb_files and self._is_current(self.thumb_files[0])): + if thumb_url and self.query_type in ['thumb', 'artwork'] and not ( + self.thumb_files and self._is_current(self.thumb_files[0])): artwork = request.request_content(thumb_url, timeout=20) if artwork: @@ -414,7 +429,8 @@ class Cache(object): if not os.path.isdir(self.path_to_art_cache): try: os.makedirs(self.path_to_art_cache) - os.chmod(self.path_to_art_cache, int(headphones.CONFIG.FOLDER_PERMISSIONS, 8)) + os.chmod(self.path_to_art_cache, + int(headphones.CONFIG.FOLDER_PERMISSIONS, 8)) except OSError as e: logger.error('Unable to create artwork cache dir. Error: %s' + e) self.thumb_errors = True @@ -429,7 +445,8 @@ class Cache(object): ext = os.path.splitext(image_url)[1] - thumb_path = os.path.join(self.path_to_art_cache, 'T_' + self.id + '.' + helpers.today() + ext) + thumb_path = os.path.join(self.path_to_art_cache, + 'T_' + self.id + '.' + helpers.today() + ext) try: with open(thumb_path, 'wb') as f: f.write(artwork) diff --git a/headphones/classes.py b/headphones/classes.py index 96315ba7..6015a0f2 100644 --- a/headphones/classes.py +++ b/headphones/classes.py @@ -13,9 +13,9 @@ # 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 ## -######################################### +####################################### +# Stolen from Sick-Beard's classes.py # +####################################### import urllib @@ -133,4 +133,5 @@ class Proper: self.episode = -1 def __str__(self): - return str(self.date) + " " + self.name + " " + str(self.season) + "x" + str(self.episode) + " of " + str(self.tvdbid) + 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 02bfc3a2..0a33bde7 100644 --- a/headphones/common.py +++ b/headphones/common.py @@ -20,15 +20,16 @@ Created on Aug 1, 2011 ''' import platform import operator + import os import re - from headphones import version -#Identify Our Application + +# Identify Our Application USER_AGENT = 'Headphones/-' + version.HEADPHONES_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')' -### Notification Types +# Notification Types NOTIFY_SNATCH = 1 NOTIFY_DOWNLOAD = 2 @@ -36,26 +37,25 @@ 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 +# 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 + 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 @@ -75,7 +75,8 @@ class Quality: def _getStatusStrings(status): toReturn = {} for x in Quality.qualityStrings.keys(): - toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + Quality.qualityStrings[x] + ")" + toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + \ + Quality.qualityStrings[x] + ")" return toReturn @staticmethod @@ -103,6 +104,9 @@ class Quality: @staticmethod def nameQuality(name): + def checkName(list, func): + return func([re.search(x, name, re.I) for x in list]) + name = os.path.basename(name) # if we have our exact text then assume we put it there @@ -115,9 +119,7 @@ class Quality: 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 + # 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): @@ -131,7 +133,6 @@ class Quality: @staticmethod def assumeQuality(name): - if name.lower().endswith(".mp3"): return Quality.MP3 elif name.lower().endswith(".flac"): @@ -167,13 +168,16 @@ class Quality: 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()] +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], []) +ANY = Quality.combineQualities( + [Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], []) qualityPresets = (MP3, LOSSLESS, ANY) qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)", diff --git a/headphones/config.py b/headphones/config.py index 97cc5e6c..85f49799 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -1,7 +1,8 @@ -import headphones.logger import itertools + import os import re +import headphones.logger from configobj import ConfigObj @@ -14,6 +15,7 @@ def bool_int(value): value = 0 return int(bool(value)) + _CONFIG_DEFINITIONS = { 'ADD_ALBUM_ART': (int, 'General', 0), 'ADVANCEDENCODER': (str, 'General', ''), @@ -254,7 +256,7 @@ _CONFIG_DEFINITIONS = { 'UTORRENT_PASSWORD': (str, 'uTorrent', ''), 'UTORRENT_USERNAME': (str, 'uTorrent', ''), 'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1), - 'WAIT_UNTIL_RELEASE_DATE' : (int, 'General', 0), + 'WAIT_UNTIL_RELEASE_DATE': (int, 'General', 0), 'WAFFLES': (int, 'Waffles', 0), 'WAFFLES_PASSKEY': (str, 'Waffles', ''), 'WAFFLES_RATIO': (str, 'Waffles', ''), @@ -272,6 +274,7 @@ _CONFIG_DEFINITIONS = { 'XLDPROFILE': (str, 'General', '') } + # pylint:disable=R0902 # it might be nice to refactor for fewer instance variables class Config(object): @@ -348,7 +351,7 @@ class Config(object): """ Return the extra newznab tuples """ extra_newznabs = list( itertools.izip(*[itertools.islice(self.EXTRA_NEWZNABS, i, None, 3) - for i in range(3)]) + for i in range(3)]) ) return extra_newznabs @@ -367,7 +370,7 @@ class Config(object): """ Return the extra torznab tuples """ extra_torznabs = list( itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 3) - for i in range(3)]) + for i in range(3)]) ) return extra_torznabs diff --git a/headphones/cuesplit.py b/headphones/cuesplit.py index 55112368..6d7ae7a0 100755 --- a/headphones/cuesplit.py +++ b/headphones/cuesplit.py @@ -15,13 +15,13 @@ # Most of this lifted from here: https://github.com/SzieberthAdam/gneposis-cdgrab -import os import sys -import re import subprocess import copy import glob +import os +import re import headphones from headphones import logger from mutagen.flac import FLAC @@ -62,7 +62,7 @@ WAVE_FILE_TYPE_BY_EXTENSION = { '.flac': 'Free Lossless Audio Codec' } -#SHNTOOL_COMPATIBLE = ("Free Lossless Audio Codec", "Waveform Audio", "Monkey's Audio") +# SHNTOOL_COMPATIBLE = ("Free Lossless Audio Codec", "Waveform Audio", "Monkey's Audio") # TODO: Make this better! # this module-level variable is bad. :( @@ -288,7 +288,7 @@ class CueFile(File): global line_content c = self.content.splitlines() header_dict = {} - #remaining_headers = CUE_HEADER + # remaining_headers = CUE_HEADER remaining_headers = copy.copy(CUE_HEADER) line_index = 0 match = True @@ -314,7 +314,8 @@ class CueFile(File): line_content = c[line_index] search_result = re.search(CUE_TRACK, line_content, re.I) if not search_result: - raise ValueError('inconsistent CUE sheet, TRACK expected at line {0}'.format(line_index + 1)) + raise ValueError( + 'inconsistent CUE sheet, TRACK expected at line {0}'.format(line_index + 1)) track_nr = int(search_result.group(1)) line_index += 1 next_track = False @@ -353,7 +354,8 @@ class CueFile(File): track_meta['dcpflag'] = True line_index += 1 else: - raise ValueError('unknown entry in track error, line {0}'.format(line_index + 1)) + raise ValueError( + 'unknown entry in track error, line {0}'.format(line_index + 1)) else: next_track = True @@ -371,8 +373,8 @@ class CueFile(File): if not self.content: try: - with open(self.name, encoding="cp1252") as cue_file: - self.content = cue_file.read() + with open(self.name, encoding="cp1252") as cue_file: + self.content = cue_file.read() except: raise ValueError('Cant encode CUE Sheet.') @@ -406,9 +408,11 @@ class CueFile(File): for i in range(len(self.tracks)): if self.tracks[i]: if self.tracks[i].get('artist'): - content += 'track' + int_to_str(i) + 'artist' + '\t' + self.tracks[i].get('artist') + '\n' + content += 'track' + int_to_str(i) + 'artist' + '\t' + self.tracks[i].get( + 'artist') + '\n' if self.tracks[i].get('title'): - content += 'track' + int_to_str(i) + 'title' + '\t' + self.tracks[i].get('title') + '\n' + content += 'track' + int_to_str(i) + 'title' + '\t' + self.tracks[i].get( + 'title') + '\n' return content def htoa(self): @@ -449,7 +453,8 @@ class MetaFile(File): raise ValueError('Syntax error in album meta file') if not content['tracks'][int(parsed_track.group(1))]: content['tracks'][int(parsed_track.group(1))] = dict() - content['tracks'][int(parsed_track.group(1))][parsed_track.group(2)] = parsed_line.group(2) + content['tracks'][int(parsed_track.group(1))][ + parsed_track.group(2)] = parsed_line.group(2) else: content[parsed_line.group(1)] = parsed_line.group(2) @@ -472,15 +477,16 @@ class MetaFile(File): if 'genre' in CUE_META.content: common_tags['genre'] = CUE_META.content['genre'] - #freeform tags - #freeform_tags['country'] = self.content['country'] - #freeform_tags['releasedate'] = self.content['releasedate'] + # freeform tags + # freeform_tags['country'] = self.content['country'] + # freeform_tags['releasedate'] = self.content['releasedate'] return common_tags, freeform_tags def folders(self): artist = self.content['artist'] - album = self.content['date'] + ' - ' + self.content['title'] + ' (' + self.content['label'] + ' - ' + self.content['catalog'] + ')' + album = self.content['date'] + ' - ' + self.content['title'] + ' (' + self.content[ + 'label'] + ' - ' + self.content['catalog'] + ')' return artist, album def complete(self): @@ -535,6 +541,7 @@ class WaveFile(File): if self.type == 'Free Lossless Audio Codec': return FLAC(self.name) + def split(albumpath): global CUE_META os.chdir(albumpath) @@ -577,7 +584,8 @@ def split(albumpath): import getXldProfile xldprofile, xldformat, _ = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE) if not xldformat: - raise ValueError('Details for xld profile "%s" not found, cannot split cue' % (xldprofile)) + raise ValueError( + 'Details for xld profile "%s" not found, cannot split cue' % (xldprofile)) else: if headphones.CONFIG.ENCODERFOLDER: splitter = os.path.join(headphones.CONFIG.ENCODERFOLDER, 'xld') @@ -590,7 +598,7 @@ def split(albumpath): splitter = 'shntool' if splitter == 'shntool' and not check_splitter(splitter): - raise ValueError('Command not found, ensure shntool or xld installed') + raise ValueError('Command not found, ensure shntool or xld installed') # Determine if file can be split if wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys(): diff --git a/headphones/db.py b/headphones/db.py index 02d003e9..8d864aba 100644 --- a/headphones/db.py +++ b/headphones/db.py @@ -13,44 +13,41 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -##################################### -## Stolen from Sick-Beard's db.py ## -##################################### +################################### +# Stolen from Sick-Beard's db.py # +################################### from __future__ import with_statement -import os import sqlite3 +import os import headphones - 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 + # this will protect against typecasting problems produced by empty string and None settings if not headphones.CONFIG.CACHE_SIZEMB: - #sqlite will work with this (very slowly) + # sqlite will work with this (very slowly) return 0 return int(headphones.CONFIG.CACHE_SIZEMB) 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 + # don't wait for the disk to finish writing self.connection.execute("PRAGMA synchronous = OFF") - #journal disabled since we never do rollbacks + # journal disabled since we never do rollbacks self.connection.execute("PRAGMA journal_mode = %s" % headphones.CONFIG.JOURNAL_MODE) - #64mb of cache memory,probably need to make it user configurable + # 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 @@ -92,17 +89,20 @@ class DBConnection: def upsert(self, tableName, valueDict, keyDict): + def genParams(myDict): + return [x + " = ?" for x in myDict.keys()] + changesBefore = self.connection.total_changes - genParams = lambda myDict: [x + " = ?" for x in myDict.keys()] - - update_query = "UPDATE " + tableName + " SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict)) + update_query = "UPDATE " + tableName + " SET " + ", ".join( + genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict)) self.action(update_query, valueDict.values() + keyDict.values()) if self.connection.total_changes == changesBefore: insert_query = ( - "INSERT INTO " + tableName + " (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + + "INSERT INTO " + tableName + " (" + ", ".join( + valueDict.keys() + keyDict.keys()) + ")" + " VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")" ) try: diff --git a/headphones/getXldProfile.py b/headphones/getXldProfile.py index 181751a7..6fd1ce2e 100755 --- a/headphones/getXldProfile.py +++ b/headphones/getXldProfile.py @@ -1,29 +1,29 @@ import os.path + import biplist from headphones import logger def getXldProfile(xldProfile): - xldProfileNotFound = xldProfile expanded = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist') if not os.path.isfile(expanded): logger.warn("Could not find xld preferences at: %s", expanded) - return(xldProfileNotFound, None, None) + return (xldProfileNotFound, None, None) # Get xld preferences plist try: preferences = biplist.readPlist(expanded) except (biplist.InvalidPlistException, biplist.NotBinaryPlistException), e: logger.error("Error reading xld preferences plist: %s", e) - return(xldProfileNotFound, None, None) + return (xldProfileNotFound, None, None) if not isinstance(preferences, dict): logger.error("Error reading xld preferences plist, not a dict: %r", preferences) - return(xldProfileNotFound, None, None) + return (xldProfileNotFound, None, None) - profiles = preferences.get('Profiles', []) # pylint:disable=E1103 + profiles = preferences.get('Profiles', []) # pylint:disable=E1103 xldProfile = xldProfile.lower() for profile in profiles: @@ -175,6 +175,6 @@ def getXldProfile(xldProfile): if xldFormat and not xldBitrate: xldBitrate = 400 - return(xldProfileForCmd, xldFormat, xldBitrate) + return (xldProfileForCmd, xldFormat, xldBitrate) - return(xldProfileNotFound, None, None) + return (xldProfileNotFound, None, None) diff --git a/headphones/helpers.py b/headphones/helpers.py index 55a8778a..b81ab4ff 100644 --- a/headphones/helpers.py +++ b/headphones/helpers.py @@ -13,19 +13,19 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError - from operator import itemgetter - import unicodedata -import headphones import datetime -import fnmatch import shutil import time import sys + +import fnmatch import re import os +from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError +import headphones + # Modified from https://github.com/Verrus/beets-plugin-featInTitle RE_FEATURING = re.compile(r"[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&|vs\.") @@ -35,7 +35,9 @@ RE_CD = re.compile(r"^(CD|dics)\s*[0-9]+$", re.I) def multikeysort(items, columns): - comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns] + comparers = [ + ((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) + for col in columns] def comparer(left, right): for fn, mult in comparers: @@ -56,7 +58,6 @@ def checked(variable): def radio(variable, pos): - if variable == pos: return 'Checked' else: @@ -107,7 +108,6 @@ def latinToAscii(unicrap): def convert_milliseconds(ms): - seconds = ms / 1000 gmtime = time.gmtime(seconds) if seconds > 3600: @@ -119,7 +119,6 @@ def convert_milliseconds(ms): def convert_seconds(s): - gmtime = time.gmtime(s) if s > 3600: minutes = time.strftime("%H:%M:%S", gmtime) @@ -141,7 +140,6 @@ def now(): def get_age(date): - try: split_date = date.split('-') except: @@ -149,14 +147,13 @@ def get_age(date): try: days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2]) - except (IndexError,ValueError): + except (IndexError, ValueError): days_old = False return days_old def bytes_to_mb(bytes): - mb = int(bytes) / 1048576 size = '%.1f MB' % mb return size @@ -172,7 +169,7 @@ def piratesize(size): split = size.split(" ") factor = float(split[0]) unit = split[1].upper() - + if unit == 'MIB': size = factor * 1048576 elif unit == 'MB': @@ -194,7 +191,6 @@ def piratesize(size): def replace_all(text, dic, normalize=False): - if not text: return '' @@ -221,15 +217,14 @@ def replace_illegal_chars(string, type="file"): def cleanName(string): - pass1 = latinToAscii(string).lower() - out_string = re.sub('[\.\-\/\!\@\#\$\%\^\&\*\(\)\+\-\"\'\,\;\:\[\]\{\}\<\>\=\_]', '', pass1).encode('utf-8') + out_string = re.sub('[\.\-\/\!\@\#\$\%\^\&\*\(\)\+\-\"\'\,\;\:\[\]\{\}\<\>\=\_]', '', + pass1).encode('utf-8') return out_string def cleanTitle(title): - title = re.sub('[\.\-\/\_]', ' ', title).lower() # Strip out extra whitespace @@ -312,16 +307,22 @@ def expand_subfolders(f): difference = max(path_depths) - min(path_depths) if difference > 0: - logger.info("Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.", len(media_folders), difference) + logger.info( + "Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.", + len(media_folders), difference) # While already failed, advice the user what he could try. We assume the # directory may contain separate CD's and maybe some extra's. The # structure may look like X albums at same depth, and (one or more) # extra folders with a higher depth. - extra_media_folders = [media_folder[:min(path_depths)] for media_folder in media_folders if len(media_folder) > min(path_depths)] - extra_media_folders = list(set([os.path.join(*media_folder) for media_folder in extra_media_folders])) + extra_media_folders = [media_folder[:min(path_depths)] for media_folder in media_folders if + len(media_folder) > min(path_depths)] + extra_media_folders = list( + set([os.path.join(*media_folder) for media_folder in extra_media_folders])) - logger.info("Please look at the following folder(s), since they cause the depth difference: %s", extra_media_folders) + logger.info( + "Please look at the following folder(s), since they cause the depth difference: %s", + extra_media_folders) return # Convert back to paths and remove duplicates, which may be there after @@ -368,7 +369,7 @@ def path_filter_patterns(paths, patterns, root=None): for path in paths[:]: if path_match_patterns(path, patterns): logger.debug("Path ignored by pattern: %s", - os.path.join(root or "", path)) + os.path.join(root or "", path)) ignored += 1 paths.remove(path) @@ -378,11 +379,11 @@ def path_filter_patterns(paths, patterns, root=None): def extract_data(s): - s = s.replace('_', ' ') - #headphones default format - pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s[\[\(](?P.*?)[\]\)]', re.VERBOSE) + # headphones default format + pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s[\[\(](?P.*?)[\]\)]', + re.VERBOSE) match = pattern.match(s) if match: @@ -391,7 +392,7 @@ def extract_data(s): year = match.group("year") return (name, album, year) - #Gonna take a guess on this one - might be enough to search on mb + # Gonna take a guess on this one - might be enough to search on mb pat = re.compile(r"(?P.*?)\s*-\s*(?P[^\[(-]*)") match = pat.match(s) @@ -468,7 +469,8 @@ def extract_metadata(f): old_album = new_albums[index] new_albums[index] = RE_CD_ALBUM.sub("", album).strip() - logger.debug("Stripped albumd number identifier: %s -> %s", old_album, new_albums[index]) + logger.debug("Stripped albumd number identifier: %s -> %s", old_album, + new_albums[index]) # Remove duplicates new_albums = list(set(new_albums)) @@ -498,7 +500,8 @@ def extract_metadata(f): return (artist, albums[0], years[0]) # Not sure what to do here. - logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists), len(albums), len(years)) + logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists), + len(albums), len(years)) logger.debug("Artists: %s, Albums: %s, Years: %s", artists, albums, years) return (None, None, None) @@ -524,8 +527,10 @@ def preserve_torrent_directory(albumpath): Copy torrent directory to headphones-modified to keep files for seeding. """ from headphones import logger - new_folder = os.path.join(albumpath, 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace')) - logger.info("Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding") + new_folder = os.path.join(albumpath, + 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace')) + logger.info( + "Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding") try: shutil.copytree(albumpath, new_folder) return new_folder @@ -578,7 +583,9 @@ def cue_split(albumpath): def extract_logline(s): # Default log format - pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s*\:\:\s(?P.*?)\s\:\s(?P.*)', re.VERBOSE) + pattern = re.compile( + r'(?P.*?)\s\-\s(?P.*?)\s*\:\:\s(?P.*?)\s\:\s(?P.*)', + re.VERBOSE) match = pattern.match(s) if match: timestamp = match.group("timestamp") @@ -593,7 +600,7 @@ def extract_logline(s): def extract_song_data(s): from headphones import logger - #headphones default format + # headphones default format pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s\[(?P.*?)\]', re.VERBOSE) match = pattern.match(s) @@ -605,7 +612,7 @@ def extract_song_data(s): else: logger.info("Couldn't parse %s into a valid default format", s) - #newzbin default format + # newzbin default format pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s\((?P\d+?\))', re.VERBOSE) match = pattern.match(s) if match: @@ -619,7 +626,6 @@ def extract_song_data(s): def smartMove(src, dest, delete=True): - from headphones import logger source_dir = os.path.dirname(src) @@ -640,7 +646,8 @@ def smartMove(src, dest, delete=True): os.rename(src, os.path.join(source_dir, newfile)) filename = newfile except Exception as e: - logger.warn('Error renaming %s: %s', src.decode(headphones.SYS_ENCODING, 'replace'), e) + logger.warn('Error renaming %s: %s', + src.decode(headphones.SYS_ENCODING, 'replace'), e) break try: @@ -650,7 +657,9 @@ def smartMove(src, dest, delete=True): shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename)) return True except Exception as e: - logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'), e) + logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'), + e) + def walk_directory(basedir, followlinks=True): """ @@ -672,8 +681,8 @@ def walk_directory(basedir, followlinks=True): real_path = os.path.abspath(os.readlink(path)) if real_path in traversed: - logger.debug("Skipping '%s' since it is a symlink to "\ - "'%s', which is already visited.", path, real_path) + logger.debug("Skipping '%s' since it is a symlink to " \ + "'%s', which is already visited.", path, real_path) else: traversed.append(real_path) @@ -689,8 +698,9 @@ def walk_directory(basedir, followlinks=True): for result in _inner(*args): yield result + ######################### -#Sab renaming functions # +# Sab renaming functions # ######################### # TODO: Grab config values from sab to know when these options are checked. For now we'll just iterate through all combinations @@ -739,18 +749,20 @@ def sab_sanitize_foldername(name): if not name: name = 'unknown' - #maxlen = cfg.folder_max_length() - #if len(name) > maxlen: + # maxlen = cfg.folder_max_length() + # if len(name) > maxlen: # name = name[:maxlen] return name + def split_string(mystring, splitvar=','): mylist = [] for each_word in mystring.split(splitvar): mylist.append(each_word.strip()) return mylist + def create_https_certificates(ssl_cert, ssl_key): """ Create a pair of self-signed HTTPS certificares and store in them in @@ -768,11 +780,13 @@ def create_https_certificates(ssl_cert, ssl_key): # Create the CA Certificate cakey = createKeyPair(TYPE_RSA, 2048) careq = createCertRequest(cakey, CN="Certificate Authority") - cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years + cacert = createCertificate(careq, (careq, cakey), serial, + (0, 60 * 60 * 24 * 365 * 10)) # ten years pkey = createKeyPair(TYPE_RSA, 2048) req = createCertRequest(pkey, CN="Headphones") - cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years + cert = createCertificate(req, (cacert, cakey), serial, + (0, 60 * 60 * 24 * 365 * 10)) # ten years # Save the key and certificate to disk try: diff --git a/headphones/importer.py b/headphones/importer.py index 81b6bf2a..3cc7f141 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -13,39 +13,39 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from headphones import logger, helpers, db, mb, lastfm, metacritic - -from beets.mediafile import MediaFile - import time + +from headphones import logger, helpers, db, mb, lastfm, metacritic +from beets.mediafile import MediaFile import headphones blacklisted_special_artist_names = ['[anonymous]', '[data]', '[no artist]', - '[traditional]', '[unknown]', 'Various Artists'] + '[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'] + '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 - artistlist = myDB.select('SELECT ArtistID, ArtistName from artists WHERE ArtistID=?', [artistid]) + artistlist = myDB.select('SELECT ArtistID, ArtistName from artists WHERE ArtistID=?', + [artistid]) if any(artistid in x for x in artistlist): - logger.info(artistlist[0][1] + u" is already in the database. Updating 'have tracks', but not artist information") + logger.info(artistlist[0][ + 1] + u" is already in the database. Updating 'have tracks', but not artist information") return True else: return False def artistlist_to_mbids(artistlist, forced=False): - for artist in artistlist: if not artist and artist != ' ': @@ -77,9 +77,12 @@ def artistlist_to_mbids(artistlist, forced=False): myDB = db.DBConnection() if not forced: - bl_artist = myDB.action('SELECT * FROM blacklist WHERE ArtistID=?', [artistid]).fetchone() + bl_artist = myDB.action('SELECT * FROM blacklist WHERE ArtistID=?', + [artistid]).fetchone() if bl_artist or artistid in blacklisted_special_artists: - logger.info("Artist ID for '%s' is either blacklisted or Various Artists. To add artist, you must do it manually (Artist ID: %s)" % (artist, artistid)) + logger.info( + "Artist ID for '%s' is either blacklisted or Various Artists. To add artist, you must do it manually (Artist ID: %s)" % ( + artist, artistid)) continue # Add to database if it doesn't exist @@ -88,7 +91,9 @@ def artistlist_to_mbids(artistlist, forced=False): # Just update the tracks if it does else: - havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist])) + havetracks = len( + myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len( + myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist])) myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artistid]) # Delete it from the New Artists if the request came from there @@ -112,7 +117,6 @@ def addArtistIDListToDB(artistidlist): def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): - # Putting this here to get around the circular import. We're using this to update thumbnails for artist/albums from headphones import cache @@ -142,7 +146,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): "Status": "Loading", "IncludeExtras": headphones.CONFIG.INCLUDE_EXTRAS, "Extras": headphones.CONFIG.EXTRAS} - if type=="series": + if type == "series": newValueDict['Type'] = "series" else: newValueDict = {"Status": "Loading"} @@ -151,7 +155,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): myDB.upsert("artists", newValueDict, controlValueDict) - if type=="series": + if type == "series": artist = mb.getSeries(artistid) else: artist = mb.getArtist(artistid, extrasonly) @@ -159,7 +163,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): if artist and artist.get('artist_name') in blacklisted_special_artist_names: logger.warn('Cannot import blocked special purpose artist: %s' % artist.get('artist_name')) myDB.action('DELETE from artists WHERE ArtistID=?', [artistid]) - #in case it's already in the db + # in case it's already in the db myDB.action('DELETE from albums WHERE ArtistID=?', [artistid]) myDB.action('DELETE from tracks WHERE ArtistID=?', [artistid]) return @@ -168,7 +172,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): logger.warn("Error fetching artist info. ID: " + artistid) if dbartist is None: newValueDict = {"ArtistName": "Fetch failed, try refreshing. (%s)" % (artistid), - "Status": "Active"} + "Status": "Active"} else: newValueDict = {"Status": "Active"} myDB.upsert("artists", newValueDict, controlValueDict) @@ -191,7 +195,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="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 try: - db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', [artistid]).fetchone() + db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', + [artistid]).fetchone() includeExtras = db_artist['IncludeExtras'] except IndexError: includeExtras = False @@ -206,9 +211,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): for groups in artist['releasegroups']: group_list.append(groups['id']) if not extrasonly: - remove_missing_groups_from_albums = myDB.select("SELECT AlbumID FROM albums WHERE ArtistID=?", [artistid]) + remove_missing_groups_from_albums = myDB.select( + "SELECT AlbumID FROM albums WHERE ArtistID=?", [artistid]) else: - remove_missing_groups_from_albums = myDB.select('SELECT AlbumID FROM albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"', [artistid]) + remove_missing_groups_from_albums = myDB.select( + 'SELECT AlbumID FROM albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"', + [artistid]) for items in remove_missing_groups_from_albums: if items['AlbumID'] not in group_list: # Remove all from albums/tracks that aren't in release groups @@ -217,12 +225,16 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): myDB.action("DELETE FROM tracks WHERE AlbumID=?", [items['AlbumID']]) myDB.action("DELETE FROM alltracks WHERE AlbumID=?", [items['AlbumID']]) myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [items['AlbumID']]) - logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz refresh" % (artist['artist_name'], items['AlbumID'])) + logger.info( + "[%s] Removing all references to release group %s to reflect MusicBrainz refresh" % ( + artist['artist_name'], items['AlbumID'])) if not extrasonly: force_repackage = 1 else: if not extrasonly: - logger.info("[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" % artist['artist_name']) + logger.info( + "[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" % + artist['artist_name']) # Then search for releases within releasegroups, if releases don't exist, then remove from allalbums/alltracks album_searches = [] @@ -232,7 +244,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): today = helpers.today() rgid = rg['id'] skip_log = 0 - #Make a user configurable variable to skip update of albums with release dates older than this date (in days) + # Make a user configurable variable to skip update of albums with release dates older than this date (in days) pause_delta = headphones.CONFIG.MB_IGNORE_AGE rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone() @@ -247,12 +259,14 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): new_release_group = True if new_release_group: - logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title'])) + 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'])) + logger.info("[%s] Now updating: %s (No Release Date)" % ( + artist['artist_name'], rg['title'])) new_releases = mb.get_new_releases(rgid, includeExtras, True) else: if len(check_release_date) == 10: @@ -264,20 +278,24 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): 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: @@ -291,23 +309,26 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): # This will be used later to build a hybrid release fullreleaselist = [] # Search for releases within a release group - find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?", [rg['id']]) + 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 + if items['ReleaseID'] != rg[ + 'id']: # don't include hybrid information, since that's what we're replacing hybrid_release_id = items['ReleaseID'] newValueDict = {"ArtistID": items['ArtistID'], - "ArtistName": items['ArtistName'], - "AlbumTitle": items['AlbumTitle'], - "AlbumID": items['AlbumID'], - "AlbumASIN": items['AlbumASIN'], - "ReleaseDate": items['ReleaseDate'], - "Type": items['Type'], - "ReleaseCountry": items['ReleaseCountry'], - "ReleaseFormat": items['ReleaseFormat'] - } - find_hybrid_tracks = myDB.action("SELECT * from alltracks WHERE ReleaseID=?", [hybrid_release_id]) + "ArtistName": items['ArtistName'], + "AlbumTitle": items['AlbumTitle'], + "AlbumID": items['AlbumID'], + "AlbumASIN": items['AlbumASIN'], + "ReleaseDate": items['ReleaseDate'], + "Type": items['Type'], + "ReleaseCountry": items['ReleaseCountry'], + "ReleaseFormat": items['ReleaseFormat'] + } + find_hybrid_tracks = myDB.action("SELECT * from alltracks WHERE ReleaseID=?", + [hybrid_release_id]) totalTracks = 1 hybrid_track_array = [] for hybrid_tracks in find_hybrid_tracks: @@ -315,9 +336,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): 'number': hybrid_tracks['TrackNumber'], 'title': hybrid_tracks['TrackTitle'], 'id': hybrid_tracks['TrackID'], - #'url': hybrid_tracks['TrackURL'], + # 'url': hybrid_tracks['TrackURL'], 'duration': hybrid_tracks['TrackDuration'] - }) + }) totalTracks += 1 newValueDict['ReleaseID'] = hybrid_release_id newValueDict['Tracks'] = hybrid_track_array @@ -327,10 +348,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): # 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'])) + logger.info('[%s] Packaging %s releases into hybrid title' % ( + artist['artist_name'], rg['title'])) except Exception as e: errors = True - logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (artist['artist_name'], rg['title'], e)) + logger.warn('[%s] Unable to get hybrid release information for %s: %s' % ( + artist['artist_name'], rg['title'], e)) continue # Use the ReleaseGroupID as the ReleaseID for the hybrid release to differentiate it @@ -345,13 +368,14 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): "AlbumASIN": hybridrelease['AlbumASIN'], "ReleaseDate": hybridrelease['ReleaseDate'], "Type": rg['type'] - } + } myDB.upsert("allalbums", newValueDict, controlValueDict) for track in hybridrelease['Tracks']: - cleanname = helpers.cleanName(artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title']) + cleanname = helpers.cleanName( + artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title']) controlValueDict = {"TrackID": track['id'], "ReleaseID": rg['id']} @@ -365,25 +389,29 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): "TrackDuration": track['duration'], "TrackNumber": track['number'], "CleanName": cleanname - } + } - match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone() + match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', + [cleanname]).fetchone() if not match: - match = myDB.action('SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [artist['artist_name'], rg['title'], track['title']]).fetchone() - #if not match: - #match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone() + match = myDB.action( + 'SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', + [artist['artist_name'], rg['title'], track['title']]).fetchone() + # if not match: + # match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone() if match: newValueDict['Location'] = match['Location'] newValueDict['BitRate'] = match['BitRate'] newValueDict['Format'] = match['Format'] - #myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']]) - myDB.action('UPDATE have SET Matched=? WHERE Location=?', (rg['id'], match['Location'])) + # myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']]) + myDB.action('UPDATE have SET Matched=? WHERE Location=?', + (rg['id'], match['Location'])) myDB.upsert("alltracks", newValueDict, controlValueDict) # Delete matched tracks from the have table - #myDB.action('DELETE from have WHERE Matched="True"') + # myDB.action('DELETE from have WHERE Matched="True"') # 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) @@ -408,7 +436,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): "Type": album['Type'], "ReleaseCountry": album['ReleaseCountry'], "ReleaseFormat": album['ReleaseFormat'] - } + } if rg_exists: newValueDict['DateAdded'] = rg_exists['DateAdded'] @@ -425,14 +453,17 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): newValueDict['Status'] = "Wanted" # Sometimes "new" albums are added to musicbrainz after their release date, so let's try to catch these # The first test just makes sure we have year-month-day - elif helpers.get_age(album['ReleaseDate']) and helpers.get_age(today) - helpers.get_age(album['ReleaseDate']) < 21 and headphones.CONFIG.AUTOWANT_UPCOMING: + elif helpers.get_age(album['ReleaseDate']) and helpers.get_age( + today) - helpers.get_age( + album['ReleaseDate']) < 21 and headphones.CONFIG.AUTOWANT_UPCOMING: newValueDict['Status'] = "Wanted" else: newValueDict['Status'] = "Skipped" myDB.upsert("albums", newValueDict, controlValueDict) - tracks = myDB.action('SELECT * from alltracks WHERE ReleaseID=?', [releaseid]).fetchall() + tracks = myDB.action('SELECT * from alltracks WHERE ReleaseID=?', + [releaseid]).fetchall() # This is used to see how many tracks you have from an album - to # mark it as downloaded. Default is 80%, can be set in config as @@ -441,7 +472,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): if total_track_count == 0: logger.warning("Total track count is zero for Release ID " + - "'%s', skipping.", releaseid) + "'%s', skipping.", releaseid) continue for track in tracks: @@ -449,35 +480,43 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): "AlbumID": rg['id']} newValueDict = {"ArtistID": track['ArtistID'], - "ArtistName": track['ArtistName'], - "AlbumTitle": track['AlbumTitle'], - "AlbumASIN": track['AlbumASIN'], - "ReleaseID": track['ReleaseID'], - "TrackTitle": track['TrackTitle'], - "TrackDuration": track['TrackDuration'], - "TrackNumber": track['TrackNumber'], - "CleanName": track['CleanName'], - "Location": track['Location'], - "Format": track['Format'], - "BitRate": track['BitRate'] - } + "ArtistName": track['ArtistName'], + "AlbumTitle": track['AlbumTitle'], + "AlbumASIN": track['AlbumASIN'], + "ReleaseID": track['ReleaseID'], + "TrackTitle": track['TrackTitle'], + "TrackDuration": track['TrackDuration'], + "TrackNumber": track['TrackNumber'], + "CleanName": track['CleanName'], + "Location": track['Location'], + "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 - have_track_count = len(myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [rg['id']])) + have_track_count = len( + myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', + [rg['id']])) marked_as_downloaded = False if rg_exists: - if rg_exists['Status'] == 'Skipped' and ((have_track_count / float(total_track_count)) >= (headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)): - myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', rg['id']]) + if rg_exists['Status'] == 'Skipped' and ( + (have_track_count / float(total_track_count)) >= ( + headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)): + myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', + ['Downloaded', rg['id']]) marked_as_downloaded = True else: - if (have_track_count / float(total_track_count)) >= (headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0): - myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', rg['id']]) + if (have_track_count / float(total_track_count)) >= ( + headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0): + myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', + ['Downloaded', rg['id']]) marked_as_downloaded = True - logger.info(u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title'])) + logger.info( + u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title'])) cache.getThumb(AlbumID=rg['id']) # Start a search for the album if it's new, hasn't been marked as @@ -487,7 +526,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): album_searches.append(rg['id']) else: if skip_log == 0: - logger.info(u"[%s] No new releases, so no changes made to %s" % (artist['artist_name'], rg['title'])) + logger.info(u"[%s] No new releases, so no changes made to %s" % ( + artist['artist_name'], rg['title'])) time.sleep(3) finalize_update(artistid, artist['artist_name'], errors) @@ -499,7 +539,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): metacritic.update(artistid, artist['artist_name'], artist['releasegroups']) if errors: - logger.info("[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (artist['artist_name'], artist['artist_name'])) + logger.info( + "[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % ( + artist['artist_name'], artist['artist_name'])) else: myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']]) logger.info(u"Updating complete for: %s" % artist['artist_name']) @@ -518,10 +560,18 @@ def finalize_update(artistid, artistname, errors=False): myDB = db.DBConnection() - latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone() - totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [artistid])) - #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 ?', [artist['artist_name']])) - 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])) + latestalbum = myDB.action( + 'SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', + [artistid]).fetchone() + totaltracks = len(myDB.select( + 'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', + [artistid])) + # 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 ?', [artist['artist_name']])) + 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])) controlValueDict = {"ArtistID": artistid} @@ -544,7 +594,6 @@ def finalize_update(artistid, artistname, errors=False): def addReleaseById(rid, rgid=None): - myDB = db.DBConnection() # Create minimum info upfront if added from searchresults @@ -563,14 +612,18 @@ def addReleaseById(rid, rgid=None): rgid = None artistid = None release_dict = None - results = myDB.select("SELECT albums.ArtistID, releases.ReleaseGroupID from releases, albums WHERE releases.ReleaseID=? and releases.ReleaseGroupID=albums.AlbumID LIMIT 1", [rid]) + results = myDB.select( + "SELECT albums.ArtistID, releases.ReleaseGroupID from releases, albums WHERE releases.ReleaseID=? and releases.ReleaseGroupID=albums.AlbumID LIMIT 1", + [rid]) for result in results: rgid = result['ReleaseGroupID'] artistid = result['ArtistID'] - logger.debug("Found a cached releaseid : releasegroupid relationship: " + rid + " : " + rgid) + logger.debug( + "Found a cached releaseid : releasegroupid relationship: " + rid + " : " + rgid) if not rgid: - #didn't find it in the cache, get the information from MB - logger.debug("Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID") + # didn't find it in the cache, get the information from MB + logger.debug( + "Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID") try: release_dict = mb.getRelease(rid) except Exception as e: @@ -587,10 +640,10 @@ def addReleaseById(rid, rgid=None): rgid = release_dict['rgid'] artistid = release_dict['artist_id'] - #we don't want to make more calls to MB here unless we have to, could be happening quite a lot + # we don't want to make more calls to MB here unless we have to, could be happening quite a lot rg_exists = myDB.select("SELECT * from albums WHERE AlbumID=?", [rgid]) - #make sure the artist exists since I don't know what happens later if it doesn't + # make sure the artist exists since I don't know what happens later if it doesn't artist_exists = myDB.select("SELECT * from artists WHERE ArtistID=?", [artistid]) if not artist_exists and release_dict: @@ -599,7 +652,8 @@ def addReleaseById(rid, rgid=None): else: sortname = release_dict['artist_name'] - logger.info(u"Now manually adding: " + release_dict['artist_name'] + " - with status Paused") + logger.info( + u"Now manually adding: " + release_dict['artist_name'] + " - with status Paused") controlValueDict = {"ArtistID": release_dict['artist_id']} newValueDict = {"ArtistName": release_dict['artist_name'], "ArtistSortName": sortname, @@ -624,13 +678,14 @@ def addReleaseById(rid, rgid=None): myDB.upsert("artists", newValueDict, controlValueDict) elif not artist_exists and not release_dict: - logger.error("Artist does not exist in the database and did not get a valid response from MB. Skipping release.") + logger.error( + "Artist does not exist in the database and did not get a valid response from MB. Skipping release.") if status == 'Loading': myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid]) return - if not rg_exists and release_dict or status == 'Loading' and release_dict: #it should never be the case that we have an rg and not the artist - #but if it is this will fail + if not rg_exists and release_dict or status == 'Loading' and release_dict: # it should never be the case that we have an rg and not the artist + # but if it is this will fail logger.info(u"Now adding-by-id album (" + release_dict['title'] + ") from id: " + rgid) controlValueDict = {"AlbumID": rgid} if status != 'Loading': @@ -639,7 +694,8 @@ def addReleaseById(rid, rgid=None): newValueDict = {"ArtistID": release_dict['artist_id'], "ReleaseID": rgid, "ArtistName": release_dict['artist_name'], - "AlbumTitle": release_dict['title'] if 'title' in release_dict else release_dict['rg_title'], + "AlbumTitle": release_dict['title'] if 'title' in release_dict else + release_dict['rg_title'], "AlbumASIN": release_dict['asin'], "ReleaseDate": release_dict['date'], "DateAdded": helpers.today(), @@ -650,41 +706,48 @@ def addReleaseById(rid, rgid=None): myDB.upsert("albums", newValueDict, controlValueDict) - #keep a local cache of these so that external programs that are adding releasesByID don't hammer MB + # keep a local cache of these so that external programs that are adding releasesByID don't hammer MB 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']) + cleanname = helpers.cleanName( + release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title']) controlValueDict = {"TrackID": track['id'], "AlbumID": rgid} newValueDict = {"ArtistID": release_dict['artist_id'], - "ArtistName": release_dict['artist_name'], - "AlbumTitle": release_dict['rg_title'], - "AlbumASIN": release_dict['asin'], - "TrackTitle": track['title'], - "TrackDuration": track['duration'], - "TrackNumber": track['number'], - "CleanName": cleanname - } + "ArtistName": release_dict['artist_name'], + "AlbumTitle": release_dict['rg_title'], + "AlbumASIN": release_dict['asin'], + "TrackTitle": track['title'], + "TrackDuration": track['duration'], + "TrackNumber": track['number'], + "CleanName": cleanname + } - match = myDB.action('SELECT Location, BitRate, Format, Matched from have WHERE CleanName=?', [cleanname]).fetchone() + match = myDB.action( + 'SELECT Location, BitRate, Format, Matched from have WHERE CleanName=?', + [cleanname]).fetchone() if not match: - match = myDB.action('SELECT Location, BitRate, Format, Matched from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [release_dict['artist_name'], release_dict['rg_title'], track['title']]).fetchone() + match = myDB.action( + 'SELECT Location, BitRate, Format, Matched from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', + [release_dict['artist_name'], release_dict['rg_title'], + track['title']]).fetchone() - #if not match: - #match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone() + # if not match: + # match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone() if match: newValueDict['Location'] = match['Location'] newValueDict['BitRate'] = match['BitRate'] newValueDict['Format'] = match['Format'] - #myDB.action('DELETE from have WHERE Location=?', [match['Location']]) + # myDB.action('DELETE from have WHERE Location=?', [match['Location']]) # If the album has been scanned before adding the release it will be unmatched, update to matched if match['Matched'] == 'Failed': - myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release_dict['rgid'], match['Location'])) + myDB.action('UPDATE have SET Matched=? WHERE Location=?', + (release_dict['rgid'], match['Location'])) myDB.upsert("tracks", newValueDict, controlValueDict) @@ -703,7 +766,8 @@ def addReleaseById(rid, rgid=None): searcher.searchforalbum(rgid, False) elif not rg_exists and not release_dict: - logger.error("ReleaseGroup does not exist in the database and did not get a valid response from MB. Skipping release.") + logger.error( + "ReleaseGroup does not exist in the database and did not get a valid response from MB. Skipping release.") if status == 'Loading': myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid]) return @@ -811,11 +875,13 @@ def getHybridRelease(fullreleaselist): sortable_release_list.sort(key=lambda x: getSortableReleaseDate(x['releasedate'])) - average_tracks = sum(x['trackscount'] for x in sortable_release_list) / float(len(sortable_release_list)) + average_tracks = sum(x['trackscount'] for x in sortable_release_list) / float( + len(sortable_release_list)) for item in sortable_release_list: item['trackscount_delta'] = abs(average_tracks - item['trackscount']) - a = helpers.multikeysort(sortable_release_list, ['-hasasin', 'country', 'format', 'trackscount_delta']) + a = helpers.multikeysort(sortable_release_list, + ['-hasasin', 'country', 'format', 'trackscount_delta']) release_dict = {'ReleaseDate': sortable_release_list[0]['releasedate'], 'Tracks': a[0]['tracks'], diff --git a/headphones/lastfm.py b/headphones/lastfm.py index 720c8dd9..89ab7448 100644 --- a/headphones/lastfm.py +++ b/headphones/lastfm.py @@ -14,15 +14,14 @@ # along with Headphones. If not, see . import random -import headphones -import headphones.lock - -from headphones import db, logger, request - from collections import defaultdict -TIMEOUT = 60.0 # seconds -REQUEST_LIMIT = 1.0 / 5 # seconds +import headphones +import headphones.lock +from headphones import db, logger, request + +TIMEOUT = 60.0 # seconds +REQUEST_LIMIT = 1.0 / 5 # seconds ENTRY_POINT = "http://ws.audioscrobbler.com/2.0/" API_KEY = "395e6ec6bb557382fc41fde867bce66f" diff --git a/headphones/librarysync.py b/headphones/librarysync.py index 17381d80..35247442 100644 --- a/headphones/librarysync.py +++ b/headphones/librarysync.py @@ -14,17 +14,16 @@ # along with Headphones. If not, see . import os + 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 and ArtistName. def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, - cron=False, artistScan=False): - + cron=False, artistScan=False): if cron and not headphones.CONFIG.LIBRARYSCAN: return @@ -40,7 +39,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, 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')) + logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, + 'replace')) return myDB = db.DBConnection() @@ -50,13 +50,16 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, if not append: # Clean up bad filepaths - tracks = myDB.select('SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL') + tracks = myDB.select( + 'SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL') for track in tracks: encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace') 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']]) + 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') @@ -67,7 +70,9 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, # Make sure deleted files get accounted for when updating artist track counts new_artists.append(track['ArtistName']) myDB.action('DELETE FROM have WHERE Location=?', [track['Location']]) - logger.info('File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(headphones.SYS_ENCODING, 'replace')) + logger.info( + 'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode( + headphones.SYS_ENCODING, 'replace')) bitrates = [] song_list = [] @@ -89,9 +94,14 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, latest_subdirectory.append(subdirectory) if file_count == 0 and r.replace(dir, '') != '': - logger.info("[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace'))) - elif latest_subdirectory[file_count] != latest_subdirectory[file_count - 1] and file_count != 0: - logger.info("[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace'))) + logger.info("[%s] Now scanning subdirectory %s" % ( + dir.decode(headphones.SYS_ENCODING, 'replace'), + subdirectory.decode(headphones.SYS_ENCODING, 'replace'))) + elif latest_subdirectory[file_count] != latest_subdirectory[ + file_count - 1] and file_count != 0: + logger.info("[%s] Now scanning subdirectory %s" % ( + dir.decode(headphones.SYS_ENCODING, 'replace'), + subdirectory.decode(headphones.SYS_ENCODING, 'replace'))) song = os.path.join(r, files) @@ -102,10 +112,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, try: f = MediaFile(song) except (FileTypeError, UnreadableFileError): - logger.warning("Cannot read media file '%s', skipping. It may be corrupted or not a media file.", unicode_song_path) + logger.warning( + "Cannot read media file '%s', skipping. It may be corrupted or not a media file.", + unicode_song_path) continue except IOError: - logger.warning("Cannnot read media file '%s', skipping. Does the file exists?", unicode_song_path) + logger.warning("Cannnot read media file '%s', skipping. Does the file exists?", + unicode_song_path) continue # Grab the bitrates for the auto detect bit rate option @@ -131,45 +144,52 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, controlValueDict = {'Location': unicode_song_path} newValueDict = {'TrackID': f.mb_trackid, - #'ReleaseID' : f.mb_albumid, - 'ArtistName': f_artist, - 'AlbumTitle': f.album, - 'TrackNumber': f.track, - 'TrackLength': f.length, - 'Genre': f.genre, - 'Date': f.date, - 'TrackTitle': f.title, - 'BitRate': f.bitrate, - 'Format': f.format, - 'CleanName': CleanName - } + # 'ReleaseID' : f.mb_albumid, + 'ArtistName': f_artist, + 'AlbumTitle': f.album, + 'TrackNumber': f.track, + 'TrackLength': f.length, + 'Genre': f.genre, + 'Date': f.date, + 'TrackTitle': f.title, + 'BitRate': f.bitrate, + '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. + # 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. if not check_exist_song: - #This is a new track + # This is a new track if f_artist: new_artists.append(f_artist) myDB.upsert("have", newValueDict, controlValueDict) new_song_count += 1 else: - if check_exist_song['ArtistName'] != f_artist or check_exist_song['AlbumTitle'] != f.album or check_exist_song['TrackTitle'] != f.title: - #Important track metadata has been modified, need to run matcher again + if check_exist_song['ArtistName'] != f_artist or check_exist_song[ + 'AlbumTitle'] != f.album or check_exist_song['TrackTitle'] != f.title: + # Important track metadata has been modified, need to run matcher again if f_artist and f_artist != check_exist_song['ArtistName']: new_artists.append(f_artist) - elif f_artist and f_artist == check_exist_song['ArtistName'] and check_exist_song['Matched'] != "Ignored": + elif f_artist and f_artist == check_exist_song['ArtistName'] and \ + check_exist_song['Matched'] != "Ignored": new_artists.append(f_artist) else: continue newValueDict['Matched'] = None myDB.upsert("have", newValueDict, controlValueDict) - myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path]) - myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path]) + myDB.action( + 'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', + [None, None, None, unicode_song_path]) + myDB.action( + 'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', + [None, None, None, unicode_song_path]) new_song_count += 1 else: - #This track information hasn't changed + # This track information hasn't changed if f_artist and check_exist_song['Matched'] != "Ignored": new_artists.append(f_artist) @@ -177,9 +197,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, # Now we start track matching logger.info("%s new/modified songs found and added to the database" % new_song_count) - 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....") + 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 @@ -205,22 +229,24 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, 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. + # 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() + 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() have_updated = False if track: controlValueDict = {'ArtistName': track['ArtistName'], - 'AlbumTitle': track['AlbumTitle'], - 'TrackTitle': track['TrackTitle']} + 'AlbumTitle': track['AlbumTitle'], + 'TrackTitle': track['TrackTitle']} newValueDict = {'Location': song['Location'], - 'BitRate': song['BitRate'], - 'Format': song['Format']} + 'BitRate': song['BitRate'], + 'Format': song['Format']} myDB.upsert("tracks", newValueDict, controlValueDict) controlValueDict2 = {'Location': song['Location']} @@ -228,12 +254,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, myDB.upsert("have", newValueDict2, controlValueDict2) have_updated = True else: - track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() + track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', + [song['CleanName']]).fetchone() if track: controlValueDict = {'CleanName': track['CleanName']} newValueDict = {'Location': song['Location'], - 'BitRate': song['BitRate'], - 'Format': song['Format']} + 'BitRate': song['BitRate'], + 'Format': song['Format']} myDB.upsert("tracks", newValueDict, controlValueDict) controlValueDict2 = {'Location': song['Location']} @@ -246,26 +273,30 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, myDB.upsert("have", newValueDict2, controlValueDict2) have_updated = True - 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'], - 'TrackTitle': alltrack['TrackTitle']} + 'AlbumTitle': alltrack['AlbumTitle'], + 'TrackTitle': alltrack['TrackTitle']} newValueDict = {'Location': song['Location'], - 'BitRate': song['BitRate'], - 'Format': song['Format']} + 'BitRate': song['BitRate'], + 'Format': song['Format']} myDB.upsert("alltracks", newValueDict, controlValueDict) controlValueDict2 = {'Location': song['Location']} newValueDict2 = {'Matched': alltrack['AlbumID']} myDB.upsert("have", newValueDict2, controlValueDict2) else: - alltrack = myDB.action('SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() + alltrack = myDB.action( + 'SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', + [song['CleanName']]).fetchone() if alltrack: controlValueDict = {'CleanName': alltrack['CleanName']} newValueDict = {'Location': song['Location'], - 'BitRate': song['BitRate'], - 'Format': song['Format']} + 'BitRate': song['BitRate'], + 'Format': song['Format']} myDB.upsert("alltracks", newValueDict, controlValueDict) controlValueDict2 = {'Location': song['Location']} @@ -283,9 +314,10 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, 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']]) + #######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')) + logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, + 'replace')) if not append or artistScan: logger.info('Updating scanned artist track counts') @@ -294,29 +326,32 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, 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 = [ x for x in unique_artists if helpers.cleanName(x).lower() not in [ helpers.cleanName(y[0]).lower() for y in current_artists + ] ] - ] artists_checked = [ x for x in unique_artists if helpers.cleanName(x).lower() in [ helpers.cleanName(y[0]).lower() for y 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])) + 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 # (can fix by getting rid of second len statement) @@ -330,7 +365,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, importer.artistlist_to_mbids(artist_list) else: logger.info('To add these artists, go to Manage->Manage New Artists') - #myDB.action('DELETE from newartists') + # myDB.action('DELETE from newartists') for artist in artist_list: myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist]) @@ -341,7 +376,11 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, # 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])) + 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]) if not append: @@ -352,18 +391,21 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, logger.info('Library scan complete') -#ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED + +# ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED def update_album_status(AlbumID=None): myDB = db.DBConnection() logger.info('Counting matched tracks to mark albums as skipped/downloaded') if AlbumID: - album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums WHERE AlbumID=?', [AlbumID]) + album_status_updater = myDB.action( + 'SELECT AlbumID, AlbumTitle, Status from albums WHERE AlbumID=?', [AlbumID]) else: album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums') for album in album_status_updater: - track_counter = myDB.action('SELECT Location from tracks where AlbumID=?', [album['AlbumID']]) + track_counter = myDB.action('SELECT Location from tracks where AlbumID=?', + [album['AlbumID']]) total_tracks = 0 have_tracks = 0 for track in track_counter: @@ -383,7 +425,7 @@ def update_album_status(AlbumID=None): # 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 # recent, but matched to less than 80%. It would go Downloaded->Skipped->Wanted->Downloaded->Skipped->Wanted->etc.... - #else: + # else: # if album['Status'] == "Skipped" or album['Status'] == "Downloaded": # new_album_status = "Skipped" # else: diff --git a/headphones/lock.py b/headphones/lock.py index 1ec3f2c8..8d385738 100644 --- a/headphones/lock.py +++ b/headphones/lock.py @@ -2,11 +2,12 @@ Locking-related classes """ -import headphones.logger import time import threading import Queue +import headphones.logger + class TimedLock(object): """ diff --git a/headphones/logger.py b/headphones/logger.py index 84c08df3..b01e880f 100644 --- a/headphones/logger.py +++ b/headphones/logger.py @@ -13,24 +13,24 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from headphones import helpers - -from logutils.queue import QueueHandler, QueueListener from logging import handlers - import multiprocessing import contextlib -import headphones import threading import traceback import logging import errno import sys + import os +from headphones import helpers +from logutils.queue import QueueHandler, QueueListener +import headphones + # These settings are for file logging only FILENAME = "headphones.log" -MAX_SIZE = 1000000 # 1 MB +MAX_SIZE = 1000000 # 1 MB MAX_FILES = 5 # Headphones logger @@ -39,6 +39,7 @@ logger = logging.getLogger("headphones") # Global queue for multiprocessing logging queue = None + class LogListHandler(logging.Handler): """ Log handler for Web UI. @@ -71,8 +72,8 @@ def listener(): # http://stackoverflow.com/questions/2009278 for more information. if e.errno == errno.EACCES: logger.warning("Multiprocess logging disabled, because " - "current user cannot map shared memory. You won't see any" \ - "logging generated by the worker processed.") + "current user cannot map shared memory. You won't see any" \ + "logging generated by the worker processed.") # Multiprocess logging may be disabled. if not queue: @@ -149,8 +150,10 @@ def initLogger(console=False, log_dir=False, verbose=False): if log_dir: filename = os.path.join(log_dir, FILENAME) - file_formatter = logging.Formatter('%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S') - file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES) + file_formatter = logging.Formatter( + '%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S') + file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, + backupCount=MAX_FILES) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) @@ -158,7 +161,8 @@ def initLogger(console=False, log_dir=False, verbose=False): # Setup console logger if console: - console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S') + console_formatter = logging.Formatter( + '%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S') console_handler = logging.StreamHandler() console_handler.setFormatter(console_formatter) console_handler.setLevel(logging.DEBUG) @@ -212,11 +216,13 @@ def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True raise except: excepthook(*sys.exc_info()) + self.run = new_run # Monkey patch the run() by monkey patching the __init__ method threading.Thread.__init__ = new_init + # Expose logger methods info = logger.info warn = logger.warn diff --git a/headphones/lyrics.py b/headphones/lyrics.py index 066b97c5..c0f45ed5 100644 --- a/headphones/lyrics.py +++ b/headphones/lyrics.py @@ -13,18 +13,17 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -import re import htmlentitydefs +import re from headphones import logger, request def getLyrics(artist, song): - params = {"artist": artist.encode('utf-8'), - "song": song.encode('utf-8'), - "fmt": 'xml' - } + "song": song.encode('utf-8'), + "fmt": 'xml' + } url = 'http://lyrics.wikia.com/api.php' data = request.request_minidom(url, params=params) @@ -46,10 +45,13 @@ def getLyrics(artist, song): logger.warn('Error fetching lyrics from: %s' % lyricsurl) return - m = re.compile('''
.*?
(.*?) %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), new_file_name.decode(headphones.SYS_ENCODING, 'replace')) + logger.debug('Renaming %s ---> %s', + downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), + new_file_name.decode(headphones.SYS_ENCODING, 'replace')) try: os.rename(downloaded_track, new_file) except Exception as e: - logger.error('Error renaming file: %s. Error: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e) + logger.error('Error renaming file: %s. Error: %s', + downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e) continue def updateFilePermissions(albumpaths): - for folder in albumpaths: logger.info("Updating file permissions in %s", folder) for r, d, f in os.walk(folder): @@ -1053,7 +1152,8 @@ def updateFilePermissions(albumpaths): logger.error("Could not change permissions for file: %s", full_path) continue else: - logger.debug("Not changing file permissions, since it is disabled: %s", full_path.decode(headphones.SYS_ENCODING, 'replace')) + logger.debug("Not changing file permissions, since it is disabled: %s", + full_path.decode(headphones.SYS_ENCODING, 'replace')) def renameUnprocessedFolder(path, tag): @@ -1076,7 +1176,6 @@ def renameUnprocessedFolder(path, tag): def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_original_folder=False): - logger.info('Force checking download folder for completed downloads') ignored = 0 @@ -1089,9 +1188,11 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig if dir: download_dirs.append(dir.encode(headphones.SYS_ENCODING, 'replace')) if headphones.CONFIG.DOWNLOAD_DIR and not dir: - download_dirs.append(headphones.CONFIG.DOWNLOAD_DIR.encode(headphones.SYS_ENCODING, 'replace')) + download_dirs.append( + headphones.CONFIG.DOWNLOAD_DIR.encode(headphones.SYS_ENCODING, 'replace')) if headphones.CONFIG.DOWNLOAD_TORRENT_DIR and not dir: - download_dirs.append(headphones.CONFIG.DOWNLOAD_TORRENT_DIR.encode(headphones.SYS_ENCODING, 'replace')) + download_dirs.append( + headphones.CONFIG.DOWNLOAD_TORRENT_DIR.encode(headphones.SYS_ENCODING, 'replace')) # If DOWNLOAD_DIR and DOWNLOAD_TORRENT_DIR are the same, remove the duplicate to prevent us from trying to process the same folder twice. download_dirs = list(set(download_dirs)) @@ -1108,7 +1209,8 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig # Scan for subfolders subfolders = os.listdir(download_dir) ignored += helpers.path_filter_patterns(subfolders, - headphones.CONFIG.IGNORED_FOLDERS, root=download_dir) + headphones.CONFIG.IGNORED_FOLDERS, + root=download_dir) for folder in subfolders: path_to_folder = os.path.join(download_dir, folder) @@ -1125,7 +1227,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig if folders: logger.debug('Expanded post processing folders: %s', folders) logger.info('Found %d folders to process (%d ignored).', - len(folders), ignored) + len(folders), ignored) else: logger.info('Found no folders to process. Aborting.') return @@ -1143,15 +1245,23 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig # underscores -> dots (this might be hit or miss since it assumes all # spaces/underscores came from sab replacing values logger.debug('Attempting to find album in the snatched table') - snatched = myDB.action('SELECT AlbumID, Title, Kind, Status from snatched WHERE FolderName LIKE ?', [folder_basename]).fetchone() + snatched = myDB.action( + 'SELECT AlbumID, Title, Kind, Status from snatched WHERE FolderName LIKE ?', + [folder_basename]).fetchone() if snatched: - if headphones.CONFIG.KEEP_TORRENT_FILES and snatched['Kind'] == 'torrent' and snatched['Status'] == 'Processed': - logger.info('%s is a torrent folder being preserved for seeding and has already been processed. Skipping.', folder_basename) + if headphones.CONFIG.KEEP_TORRENT_FILES and snatched['Kind'] == 'torrent' and snatched[ + 'Status'] == 'Processed': + logger.info( + '%s is a torrent folder being preserved for seeding and has already been processed. Skipping.', + folder_basename) continue else: - logger.info('Found a match in the database: %s. Verifying to make sure it is the correct album', snatched['Title']) - verify(snatched['AlbumID'], folder, snatched['Kind'], keep_original_folder=keep_original_folder) + logger.info( + 'Found a match in the database: %s. Verifying to make sure it is the correct album', + snatched['Title']) + verify(snatched['AlbumID'], folder, snatched['Kind'], + keep_original_folder=keep_original_folder) continue # Attempt 2: strip release group id from filename @@ -1165,13 +1275,19 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig if rgid: rgid = possible_rgid - release = myDB.action('SELECT ArtistName, AlbumTitle, AlbumID from albums WHERE AlbumID=?', [rgid]).fetchone() + release = myDB.action( + 'SELECT ArtistName, AlbumTitle, AlbumID from albums WHERE AlbumID=?', + [rgid]).fetchone() if release: - logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle']) - verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder) + logger.info( + 'Found a match in the database: %s - %s. Verifying to make sure it is the correct album', + release['ArtistName'], release['AlbumTitle']) + verify(release['AlbumID'], folder, forced=True, + keep_original_folder=keep_original_folder) continue else: - logger.info('Found a (possibly) valid Musicbrainz release group id in album folder name.') + logger.info( + 'Found a (possibly) valid Musicbrainz release group id in album folder name.') verify(rgid, folder, forced=True) continue @@ -1184,13 +1300,18 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig name = album = year = None if name and album: - release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone() + release = myDB.action( + 'SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', + [name, album]).fetchone() if release: - logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle']) + logger.info( + 'Found a match in the database: %s - %s. Verifying to make sure it is the correct album', + release['ArtistName'], release['AlbumTitle']) verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder) continue else: - logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album) + logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, + album) try: rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album)) except: @@ -1219,13 +1340,18 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig name = album = None if name and album: - release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone() + release = myDB.action( + 'SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', + [name, album]).fetchone() if release: - logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle']) + logger.info( + 'Found a match in the database: %s - %s. Verifying to make sure it is the correct album', + release['ArtistName'], release['AlbumTitle']) verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder) continue else: - logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album) + logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, + album) try: rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album)) except: @@ -1243,13 +1369,18 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig logger.debug('Attempt to extract album name by assuming it is the folder name') if '-' not in folder_basename: - release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder_basename]).fetchone() + release = myDB.action( + 'SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', + [folder_basename]).fetchone() if release: - logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle']) + logger.info( + 'Found a match in the database: %s - %s. Verifying to make sure it is the correct album', + release['ArtistName'], release['AlbumTitle']) verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder) continue else: - logger.info('Querying MusicBrainz for the release group id for: %s', folder_basename) + logger.info('Querying MusicBrainz for the release group id for: %s', + folder_basename) try: rgid = mb.findAlbumID(album=helpers.latinToAscii(folder_basename)) except: @@ -1264,6 +1395,6 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig # Fail here logger.info("Couldn't parse '%s' into any valid format. If adding " \ - "albums from another source, they must be in an 'Artist - Album " \ - "[Year]' format, or end with the musicbrainz release group id.", - folder_basename) + "albums from another source, they must be in an 'Artist - Album " \ + "[Year]' format, or end with the musicbrainz release group id.", + folder_basename) diff --git a/headphones/request.py b/headphones/request.py index a3493f79..0fa843f7 100644 --- a/headphones/request.py +++ b/headphones/request.py @@ -13,16 +13,16 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from headphones import logger - from xml.dom import minidom -from bs4 import BeautifulSoup +import collections +from bs4 import BeautifulSoup import requests +from headphones import logger import feedparser import headphones import headphones.lock -import collections + # Disable SSL certificate warnings. We have our own handling requests.packages.urllib3.disable_warnings() @@ -211,7 +211,7 @@ def server_message(response): # First attempt is to 'read' the response as HTML if response.headers.get("content-type") and \ - "text/html" in response.headers.get("content-type"): + "text/html" in response.headers.get("content-type"): try: soup = BeautifulSoup(response.content, "html5lib") except Exception: diff --git a/headphones/rutracker.py b/headphones/rutracker.py index f106e133..8395fbf3 100644 --- a/headphones/rutracker.py +++ b/headphones/rutracker.py @@ -1,19 +1,17 @@ #!/usr/bin/env python import urllib -import requests as requests -from urlparse import urlparse -from bs4 import BeautifulSoup - -import os import time -import re +from urlparse import urlparse +import re +import requests as requests +from bs4 import BeautifulSoup import headphones from headphones import logger -class Rutracker(object): +class Rutracker(object): def __init__(self): self.session = requests.session() self.timeout = 60 @@ -58,7 +56,8 @@ class Rutracker(object): self.loggedin = True logger.info("Successfully logged in to rutracker") else: - logger.error("Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later") + logger.error( + "Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later") self.loggedin = False return self.loggedin except Exception as e: @@ -111,7 +110,7 @@ class Rutracker(object): soup = BeautifulSoup(r.content, 'html5lib') # Debug - #logger.debug (soup.prettify()) + # logger.debug (soup.prettify()) # Check if still logged in if not self.still_logged_in(soup): @@ -130,7 +129,8 @@ class Rutracker(object): return None minimumseeders = int(headphones.CONFIG.NUMBEROFSEEDERS) - 1 - for item in zip(i.find_all(class_='hl-tags'),i.find_all(class_='dl-stub'),i.find_all(class_='seedmed')): + for item in zip(i.find_all(class_='hl-tags'), i.find_all(class_='dl-stub'), + i.find_all(class_='seedmed')): title = item[0].get_text() url = item[1].get('href') size_formatted = item[1].get_text()[:-2] @@ -149,12 +149,15 @@ class Rutracker(object): if size < self.maxsize and minimumseeders < int(seeds): logger.info('Found %s. Size: %s' % (title, size_formatted)) - #Torrent topic page - torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t'] + # Torrent topic page + torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])[ + 't'] topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id rulist.append((title, size, topicurl, 'rutracker.org', 'torrent', True)) else: - logger.info("%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)" % (title, size, int(seeds))) + logger.info( + "%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)" % ( + title, size, int(seeds))) if not rulist: logger.info("No valid results found from rutracker") @@ -165,7 +168,6 @@ class Rutracker(object): logger.error("An unknown error occurred in the rutracker parser: %s" % e) return None - def get_torrent_data(self, url): """ return the .torrent data @@ -176,14 +178,14 @@ class Rutracker(object): cookie = {'bb_dl': torrent_id} try: headers = {'Referer': url} - r = self.session.post(url=downloadurl, cookies=cookie, headers=headers, timeout=self.timeout) + r = self.session.post(url=downloadurl, cookies=cookie, headers=headers, + timeout=self.timeout) return r.content except Exception as e: logger.error('Error getting torrent: %s', e) return False - - #TODO get this working in utorrent.py + # TODO get this working in utorrent.py def utorrent_add_file(self, data): host = headphones.CONFIG.UTORRENT_HOST @@ -197,7 +199,8 @@ class Rutracker(object): base_url = host url = base_url + '/gui/' - self.session.auth = (headphones.CONFIG.UTORRENT_USERNAME, headphones.CONFIG.UTORRENT_PASSWORD) + self.session.auth = ( + headphones.CONFIG.UTORRENT_USERNAME, headphones.CONFIG.UTORRENT_PASSWORD) try: r = self.session.get(url + 'token.html') @@ -221,4 +224,3 @@ class Rutracker(object): self.session.post(url, params={'action': 'add-file'}, files=files) except Exception as e: logger.exception('Error adding file to utorrent %s', e) - diff --git a/headphones/sab.py b/headphones/sab.py index 79a67a0e..fa7c3309 100644 --- a/headphones/sab.py +++ b/headphones/sab.py @@ -13,26 +13,24 @@ # 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 ## -##################################### +################################### +# Stolen from Sick-Beard's sab.py # +################################### -import MultipartPostHandler -import headphones import cookielib -import httplib +import headphones from headphones.common import USER_AGENT from headphones import logger, helpers, request def sab_api_call(request_type=None, params={}, **kwargs): - if not headphones.CONFIG.SAB_HOST.startswith('http'): headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST if headphones.CONFIG.SAB_HOST.endswith('/'): - headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1] + headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[ + 0:len(headphones.CONFIG.SAB_HOST) - 1] url = headphones.CONFIG.SAB_HOST + "/" + "api?" @@ -42,11 +40,11 @@ def sab_api_call(request_type=None, params={}, **kwargs): params['ma_password'] = headphones.CONFIG.SAB_PASSWORD if headphones.CONFIG.SAB_APIKEY: params['apikey'] = headphones.CONFIG.SAB_APIKEY - - if request_type=='send_nzb' and headphones.CONFIG.SAB_CATEGORY: + + if request_type == 'send_nzb' and headphones.CONFIG.SAB_CATEGORY: params['cat'] = headphones.CONFIG.SAB_CATEGORY - params['output']='json' + params['output'] = 'json' response = request.request_json(url, params=params, **kwargs) @@ -57,8 +55,8 @@ def sab_api_call(request_type=None, params={}, **kwargs): logger.debug("Successfully connected to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST) return response -def sendNZB(nzb): +def sendNZB(nzb): params = {} # if it's a normal result we just pass SAB the URL if nzb.resultType == "nzb": @@ -87,7 +85,8 @@ def sendNZB(nzb): response = sab_api_call('send_nzb', params=params) elif nzb.resultType == "nzbdata": cookies = cookielib.CookieJar() - response = sab_api_call('send_nzb', params=params, method="post", files=files, cookies=cookies, headers=headers) + response = sab_api_call('send_nzb', params=params, method="post", files=files, + cookies=cookies, headers=headers) if not response: logger.info(u"No data returned from SABnzbd, NZB not sent") @@ -102,15 +101,15 @@ def sendNZB(nzb): def checkConfig(): - params = {'mode': 'get_config', - 'section': 'misc', - } + 'section': 'misc', + } config_options = sab_api_call(params=params) - + if not config_options: - logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)") + logger.warn( + "Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)") return (0, 0) replace_spaces = config_options['config']['misc']['replace_spaces'] diff --git a/headphones/searcher.py b/headphones/searcher.py index 93451107..aa07442b 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -15,35 +15,32 @@ # NZBGet support added by CurlyMo as a part of XBian - XBMC on the Raspberry Pi -import urllib -import urlparse -from pygazelle import api as gazelleapi -from pygazelle import encoding as gazelleencoding -from pygazelle import format as gazelleformat from base64 import b16encode, b32decode from hashlib import sha1 - -import os -import re import string -import shutil import random import urllib import datetime -import headphones import subprocess import unicodedata +import urlparse +import os +import re +from pygazelle import api as gazelleapi +from pygazelle import encoding as gazelleencoding +from pygazelle import format as gazelleformat +import headphones from headphones.common import USER_AGENT from headphones import logger, db, helpers, classes, sab, nzbget, request from headphones import utorrent, transmission, notifiers, rutracker - from bencode import bencode, bdecode + # Magnet to torrent services, for Black hole. Stolen from CouchPotato. TORRENT_TO_MAGNET_SERVICES = [ - #'https://zoink.it/torrent/%s.torrent', - #'http://torrage.com/torrent/%s.torrent', + # 'https://zoink.it/torrent/%s.torrent', + # 'http://torrage.com/torrent/%s.torrent', 'https://torcache.net/torrent/%s.torrent', ] @@ -123,10 +120,10 @@ def read_torrent_name(torrent_file, default_name=None): except KeyError: if default_name: logger.warning("Couldn't get name from torrent file: %s. " \ - "Defaulting to '%s'", e, default_name) + "Defaulting to '%s'", e, default_name) else: logger.warning("Couldn't get name from torrent file: %s. No " \ - "default given", e) + "default given", e) # Return default return default_name @@ -147,7 +144,7 @@ def calculate_torrent_hash(link, data=None): torrent_hash = sha1(bencode(info)).hexdigest() else: raise ValueError("Cannot calculate torrent hash without magnet link " \ - "or data") + "or data") return torrent_hash.upper() @@ -173,7 +170,7 @@ def get_seed_ratio(provider): elif provider == 'Mininova': seed_ratio = headphones.CONFIG.MININOVA_RATIO elif provider == 'Strike': - seed_ratio = headphones.CONFIG.STRIKE_RATIO + seed_ratio = headphones.CONFIG.STRIKE_RATIO else: seed_ratio = None @@ -187,13 +184,13 @@ def get_seed_ratio(provider): def searchforalbum(albumid=None, new=False, losslessOnly=False, - choose_specific_download=False): - + choose_specific_download=False): logger.info('Searching for wanted albums') myDB = db.DBConnection() if not albumid: - results = myDB.select('SELECT * from albums WHERE Status="Wanted" OR Status="Wanted Lossless"') + results = myDB.select( + 'SELECT * from albums WHERE Status="Wanted" OR Status="Wanted Lossless"') for album in results: @@ -205,11 +202,13 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False, try: release_date = datetime.datetime.strptime(album['ReleaseDate'], "%Y-%m-%d") except: - logger.warn("No valid date for: %s. Skipping automatic search" % album['AlbumTitle']) + logger.warn( + "No valid date for: %s. Skipping automatic search" % album['AlbumTitle']) continue if release_date > datetime.datetime.today(): - logger.info("Skipping: %s. Waiting for release date of: %s" % (album['AlbumTitle'], album['ReleaseDate'])) + logger.info("Skipping: %s. Waiting for release date of: %s" % ( + album['AlbumTitle'], album['ReleaseDate'])) continue new = True @@ -217,7 +216,8 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False, if album['Status'] == "Wanted Lossless": losslessOnly = True - logger.info('Searching for "%s - %s" since it is marked as wanted' % (album['ArtistName'], album['AlbumTitle'])) + logger.info('Searching for "%s - %s" since it is marked as wanted' % ( + album['ArtistName'], album['AlbumTitle'])) do_sorted_search(album, new, losslessOnly) elif albumid and choose_specific_download: @@ -228,21 +228,25 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False, else: album = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone() - logger.info('Searching for "%s - %s" since it was marked as wanted' % (album['ArtistName'], album['AlbumTitle'])) + logger.info('Searching for "%s - %s" since it was marked as wanted' % ( + album['ArtistName'], album['AlbumTitle'])) do_sorted_search(album, new, losslessOnly) logger.info('Search for wanted albums complete') def do_sorted_search(album, new, losslessOnly, choose_specific_download=False): - - NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER or headphones.CONFIG.NEWZNAB or headphones.CONFIG.NZBSORG or headphones.CONFIG.OMGWTFNZBS) - NZB_DOWNLOADERS = (headphones.CONFIG.SAB_HOST or headphones.CONFIG.BLACKHOLE_DIR or headphones.CONFIG.NZBGET_HOST) - TORRENT_PROVIDERS = (headphones.CONFIG.TORZNAB or headphones.CONFIG.KAT or headphones.CONFIG.PIRATEBAY or headphones.CONFIG.OLDPIRATEBAY or headphones.CONFIG.MININOVA or headphones.CONFIG.WAFFLES or headphones.CONFIG.RUTRACKER or headphones.CONFIG.WHATCD or headphones.CONFIG.STRIKE) + NZB_PROVIDERS = ( + headphones.CONFIG.HEADPHONES_INDEXER or headphones.CONFIG.NEWZNAB or headphones.CONFIG.NZBSORG or headphones.CONFIG.OMGWTFNZBS) + NZB_DOWNLOADERS = ( + headphones.CONFIG.SAB_HOST or headphones.CONFIG.BLACKHOLE_DIR or headphones.CONFIG.NZBGET_HOST) + TORRENT_PROVIDERS = ( + headphones.CONFIG.TORZNAB or headphones.CONFIG.KAT or headphones.CONFIG.PIRATEBAY or headphones.CONFIG.OLDPIRATEBAY or headphones.CONFIG.MININOVA or headphones.CONFIG.WAFFLES or headphones.CONFIG.RUTRACKER or headphones.CONFIG.WHATCD or headphones.CONFIG.STRIKE) results = [] myDB = db.DBConnection() - albumlength = myDB.select('SELECT sum(TrackDuration) from tracks WHERE AlbumID=?', [album['AlbumID']])[0][0] + albumlength = \ + myDB.select('SELECT sum(TrackDuration) from tracks WHERE AlbumID=?', [album['AlbumID']])[0][0] if headphones.CONFIG.PREFER_TORRENTS == 0 and not choose_specific_download: @@ -269,7 +273,8 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False): nzb_results = searchNZB(album, new, losslessOnly, albumlength, choose_specific_download) if TORRENT_PROVIDERS: - torrent_results = searchTorrent(album, new, losslessOnly, albumlength, choose_specific_download) + torrent_results = searchTorrent(album, new, losslessOnly, albumlength, + choose_specific_download) if not nzb_results: nzb_results = [] @@ -283,7 +288,7 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False): return results # Filter all results that do not comply - results = [ result for result in results if result[5] ] + results = [result for result in results if result[5]] # Sort the remaining results sorted_search_results = sort_search_results(results, album, new, albumlength) @@ -305,14 +310,14 @@ def removeDisallowedFilenameChars(filename): def more_filtering(results, album, albumlength, new): - low_size_limit = None high_size_limit = None allow_lossless = False myDB = db.DBConnection() # Lossless - ignore results if target size outside bitrate range - if headphones.CONFIG.PREFERRED_QUALITY == 3 and albumlength and (headphones.CONFIG.LOSSLESS_BITRATE_FROM or headphones.CONFIG.LOSSLESS_BITRATE_TO): + if headphones.CONFIG.PREFERRED_QUALITY == 3 and albumlength and ( + headphones.CONFIG.LOSSLESS_BITRATE_FROM or headphones.CONFIG.LOSSLESS_BITRATE_TO): if headphones.CONFIG.LOSSLESS_BITRATE_FROM: low_size_limit = albumlength / 1000 * int(headphones.CONFIG.LOSSLESS_BITRATE_FROM) * 128 if headphones.CONFIG.LOSSLESS_BITRATE_TO: @@ -325,9 +330,11 @@ def more_filtering(results, album, albumlength, new): targetsize = albumlength / 1000 * int(headphones.CONFIG.PREFERRED_BITRATE) * 128 logger.info('Target size: %s' % helpers.bytes_to_mb(targetsize)) if headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER: - low_size_limit = targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER) / 100 + low_size_limit = targetsize * int( + headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER) / 100 if headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER: - high_size_limit = targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER) / 100 + high_size_limit = targetsize * int( + headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER) / 100 if headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS: allow_lossless = True @@ -335,22 +342,18 @@ def more_filtering(results, album, albumlength, new): for result in results: - normalizedAlbumArtist = removeDisallowedFilenameChars(album['ArtistName']) - normalizedAlbumTitle = removeDisallowedFilenameChars(album['AlbumTitle']) - normalizedResultTitle = removeDisallowedFilenameChars(result[0]) - artistTitleCount = normalizedResultTitle.count(normalizedAlbumArtist) - - # WHAT DOES THIS DO? - #if normalizedAlbumArtist in normalizedAlbumTitle and artistTitleCount < 2: - # logger.info("Removing %s from %s" % (result[0], result[3])) - # 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)) + 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)) + 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()): @@ -360,7 +363,8 @@ def more_filtering(results, album, albumlength, new): alreadydownloaded = myDB.select('SELECT * from snatched WHERE URL=?', [result[2]]) if len(alreadydownloaded): - logger.info('%s has already been downloaded from %s. Skipping.' % (result[0], result[3])) + logger.info( + '%s has already been downloaded from %s. Skipping.' % (result[0], result[3])) continue newlist.append(result) @@ -371,9 +375,9 @@ def more_filtering(results, album, albumlength, new): def sort_search_results(resultlist, album, new, albumlength): - if new and not len(resultlist): - logger.info('No more results found for: %s - %s' % (album['ArtistName'], album['AlbumTitle'])) + logger.info( + 'No more results found for: %s - %s' % (album['ArtistName'], album['AlbumTitle'])) return None # Add a priority if it has any of the preferred words @@ -387,7 +391,8 @@ def sort_search_results(resultlist, album, new, albumlength): if any(word.lower() in result[0].lower() for word in preferred_words): priority = 1 # add a search provider priority (weighted based on position) - i = next((i for i, word in enumerate(preferred_words) if word in result[3].lower()), None) + i = next((i for i, word in enumerate(preferred_words) if word in result[3].lower()), + None) if i is not None: priority += round((len(preferred_words) - i) / float(len(preferred_words)), 2) @@ -401,8 +406,10 @@ def sort_search_results(resultlist, album, new, albumlength): targetsize = albumlength / 1000 * int(headphones.CONFIG.PREFERRED_BITRATE) * 128 if not targetsize: - logger.info('No track information for %s - %s. Defaulting to highest quality' % (album['ArtistName'], album['AlbumTitle'])) - finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True) + logger.info('No track information for %s - %s. Defaulting to highest quality' % ( + album['ArtistName'], album['AlbumTitle'])) + finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), + reverse=True) else: newlist = [] @@ -412,36 +419,43 @@ def sort_search_results(resultlist, album, new, albumlength): # Add lossless results to the "flac list" which we can use if there are no good lossy matches if 'flac' in result[0].lower(): - flac_list.append((result[0], result[1], result[2], result[3], result[4], result[5])) + flac_list.append( + (result[0], result[1], result[2], result[3], result[4], result[5])) continue delta = abs(targetsize - int(result[1])) - newlist.append((result[0], result[1], result[2], result[3], result[4], result[5], delta)) + newlist.append( + (result[0], result[1], result[2], result[3], result[4], result[5], delta)) finallist = sorted(newlist, key=lambda title: (-title[5], title[6])) - if not len(finallist) and len(flac_list) and headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS: - logger.info("Since there were no appropriate lossy matches (and at least one lossless match), going to use lossless instead") - finallist = sorted(flac_list, key=lambda title: (title[5], int(title[1])), reverse=True) + if not len(finallist) and len( + flac_list) and headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS: + logger.info( + "Since there were no appropriate lossy matches (and at least one lossless match), going to use lossless instead") + finallist = sorted(flac_list, key=lambda title: (title[5], int(title[1])), + reverse=True) except Exception: logger.exception('Unhandled exception') - logger.info('No track information for %s - %s. Defaulting to highest quality', album['ArtistName'], album['AlbumTitle']) + logger.info('No track information for %s - %s. Defaulting to highest quality', + album['ArtistName'], album['AlbumTitle']) - finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True) + finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), + reverse=True) else: finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True) if not len(finallist): - logger.info('No appropriate matches found for %s - %s', album['ArtistName'], album['AlbumTitle']) + logger.info('No appropriate matches found for %s - %s', album['ArtistName'], + album['AlbumTitle']) return None return finallist def get_year_from_release_date(release_date): - try: year = release_date[:4] except TypeError: @@ -450,11 +464,13 @@ def get_year_from_release_date(release_date): return year -def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_specific_download=False): +def searchNZB(album, new=False, losslessOnly=False, albumlength=None, + choose_specific_download=False): reldate = album['ReleaseDate'] year = get_year_from_release_date(reldate) - dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ', '"': '', ',': '', '*': '', '.': '', ':': ''} + dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ', '"': '', ',': '', + '*': '', '.': '', ':': ''} cleanalbum = helpers.latinToAscii(helpers.replace_all(album['AlbumTitle'], dic)).strip() cleanartist = helpers.latinToAscii(helpers.replace_all(album['ArtistName'], dic)).strip() @@ -468,7 +484,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe # FLAC usually doesn't have a year for some reason so leave it out. # Various Artist albums might be listed as VA, so I'll leave that out too # Only use the year if the term could return a bunch of different albums, i.e. self-titled albums - if album['ArtistName'] in album['AlbumTitle'] or len(album['ArtistName']) < 4 or len(album['AlbumTitle']) < 4: + if album['ArtistName'] in album['AlbumTitle'] or len(album['ArtistName']) < 4 or len( + album['AlbumTitle']) < 4: term = cleanartist + ' ' + cleanalbum + ' ' + year elif album['ArtistName'] == 'Various Artists': term = cleanalbum + ' ' + year @@ -542,7 +559,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe newznab_hosts = [] if headphones.CONFIG.NEWZNAB_HOST and headphones.CONFIG.NEWZNAB_ENABLED: - newznab_hosts.append((headphones.CONFIG.NEWZNAB_HOST, headphones.CONFIG.NEWZNAB_APIKEY, headphones.CONFIG.NEWZNAB_ENABLED)) + newznab_hosts.append((headphones.CONFIG.NEWZNAB_HOST, headphones.CONFIG.NEWZNAB_APIKEY, + headphones.CONFIG.NEWZNAB_ENABLED)) for newznab_host in headphones.CONFIG.get_extra_newznabs(): if newznab_host[2] == '1' or newznab_host[2] == 1: @@ -575,7 +593,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe categories = categories + ",4050" # Request results - logger.info('Parsing results from %s using search term: %s' % (newznab_host[0],term)) + logger.info('Parsing results from %s using search term: %s' % (newznab_host[0], term)) headers = {'User-Agent': USER_AGENT} params = { @@ -602,13 +620,15 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe title = item.title size = int(item.links[1]['length']) if all(word.lower() in title.lower() for word in term.split()): - logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) + logger.info( + 'Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) resultlist.append((title, size, url, provider, 'nzb', True)) else: logger.info('Skipping %s, not all search term words found' % title) except Exception as e: - logger.exception("An unknown error occurred trying to parse the feed: %s" % e) + logger.exception( + "An unknown error occurred trying to parse the feed: %s" % e) if headphones.CONFIG.NZBSORG: provider = "nzbsorg" @@ -663,7 +683,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: categories = "22,7" else: - categories = "7" + categories = "7" if album['Type'] == 'Other': categories = "29" @@ -708,7 +728,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe # # Also will filter flac & remix albums if not specifically looking for it # This code also checks the ignored words and required words - results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)] + results = [result for result in resultlist if + verifyresult(result[0], artistterm, term, losslessOnly)] # Additional filtering for size etc if results and not choose_specific_download: @@ -718,8 +739,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe def send_to_downloader(data, bestqual, album): - - logger.info(u'Found best result from %s: %s - %s', bestqual[3], bestqual[2], bestqual[0], helpers.bytes_to_mb(bestqual[1])) + logger.info(u'Found best result from %s: %s - %s', bestqual[3], bestqual[2], + bestqual[0], helpers.bytes_to_mb(bestqual[1])) # Get rid of any dodgy chars here so we can prevent sab from renaming our downloads kind = bestqual[4] seed_ratio = None @@ -768,7 +789,10 @@ def send_to_downloader(data, bestqual, album): logger.error('Couldn\'t write NZB file: %s', e) return else: - folder_name = '%s - %s [%s]' % (helpers.latinToAscii(album['ArtistName']).encode('UTF-8').replace('/', '_'), helpers.latinToAscii(album['AlbumTitle']).encode('UTF-8').replace('/', '_'), get_year_from_release_date(album['ReleaseDate'])) + folder_name = '%s - %s [%s]' % ( + helpers.latinToAscii(album['ArtistName']).encode('UTF-8').replace('/', '_'), + helpers.latinToAscii(album['AlbumTitle']).encode('UTF-8').replace('/', '_'), + get_year_from_release_date(album['ReleaseDate'])) # Blackhole if headphones.CONFIG.TORRENT_DOWNLOADER == 0: @@ -783,9 +807,11 @@ def send_to_downloader(data, bestqual, album): if headphones.SYS_PLATFORM == 'win32': os.startfile(bestqual[2]) elif headphones.SYS_PLATFORM == 'darwin': - subprocess.Popen(["open", bestqual[2]], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.Popen(["open", bestqual[2]], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) else: - subprocess.Popen(["xdg-open", bestqual[2]], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.Popen(["xdg-open", bestqual[2]], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) # Gonna just take a guess at this..... Is there a better way to find this out? folder_name = bestqual[0] @@ -810,20 +836,20 @@ def send_to_downloader(data, bestqual, album): return # Extract folder name from torrent folder_name = read_torrent_name(download_path, - bestqual[0]) + bestqual[0]) # Break for loop break else: # No service succeeded logger.warning("Unable to convert magnet with hash " \ - "'%s' into a torrent file.", torrent_hash) + "'%s' into a torrent file.", torrent_hash) return else: logger.error("Cannot save magnet link in blackhole. " \ - "Please switch your torrent downloader to " \ - "Transmission or uTorrent, or allow Headphones " \ - "to open or convert magnet links") + "Please switch your torrent downloader to " \ + "Transmission or uTorrent, or allow Headphones " \ + "to open or convert magnet links") return else: @@ -860,7 +886,7 @@ def send_to_downloader(data, bestqual, album): if seed_ratio is not None: transmission.setSeedRatio(torrentid, seed_ratio) - else:# if headphones.CONFIG.TORRENT_DOWNLOADER == 2: + else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 2: logger.info("Sending torrent to uTorrent") # Add torrent @@ -894,11 +920,16 @@ def send_to_downloader(data, bestqual, album): myDB = db.DBConnection() myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']]) - myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name, kind]) + myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', + [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name, + kind]) # Store the torrent id so we can check later if it's finished seeding and can be removed if seed_ratio is not None and seed_ratio != 0 and torrentid: - myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid, kind]) + myDB.action( + 'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', + [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid, + kind]) # notify artist = album[1] @@ -959,11 +990,11 @@ def send_to_downloader(data, bestqual, album): message = 'Snatched from ' + provider + '. ' + name email.notify(title, message) -def verifyresult(title, artistterm, term, lossless): +def verifyresult(title, artistterm, term, lossless): title = re.sub('[\.\-\/\_]', ' ', title) - #if artistterm != 'Various Artists': + # if artistterm != 'Various Artists': # # if not re.search('^' + re.escape(artistterm), title, re.IGNORECASE): # #logger.info("Removed from results: " + title + " (artist not at string start).") @@ -975,22 +1006,28 @@ def verifyresult(title, artistterm, term, lossless): # logger.info("Removed from results: " + title + " (pre substring result).") # return False - #another attempt to weed out substrings. We don't want "Vol III" when we were looking for "Vol II" + # another attempt to weed out substrings. We don't want "Vol III" when we were looking for "Vol II" # Filter out remix search results (if we're not looking for it) if 'remix' not in term.lower() and 'remix' in title.lower(): - logger.info("Removed %s from results because it's a remix album and we're not looking for a remix album right now.", title) + logger.info( + "Removed %s from results because it's a remix album and we're not looking for a remix album right now.", + title) return False # Filter out FLAC if we're not specifically looking for it - if headphones.CONFIG.PREFERRED_QUALITY == (0 or '0') and 'flac' in title.lower() and not lossless: - logger.info("Removed %s from results because it's a lossless album and we're not looking for a lossless album right now.", title) + if headphones.CONFIG.PREFERRED_QUALITY == ( + 0 or '0') and 'flac' in title.lower() and not lossless: + logger.info( + "Removed %s from results because it's a lossless album and we're not looking for a lossless album right now.", + title) return False if headphones.CONFIG.IGNORED_WORDS: for each_word in helpers.split_string(headphones.CONFIG.IGNORED_WORDS): if each_word.lower() in title.lower(): - logger.info("Removed '%s' from results because it contains ignored word: '%s'", title, each_word) + logger.info("Removed '%s' from results because it contains ignored word: '%s'", + title, each_word) return False if headphones.CONFIG.REQUIRED_WORDS: @@ -1000,17 +1037,22 @@ def verifyresult(title, artistterm, term, lossless): if any(word.lower() in title.lower() for word in or_words): continue else: - logger.info("Removed '%s' from results because it doesn't contain any of the required words in: '%s'", title, str(or_words)) + logger.info( + "Removed '%s' from results because it doesn't contain any of the required words in: '%s'", + title, str(or_words)) return False if each_word.lower() not in title.lower(): - logger.info("Removed '%s' from results because it doesn't contain required word: '%s'", title, each_word) + logger.info( + "Removed '%s' from results because it doesn't contain required word: '%s'", + title, each_word) return False if headphones.CONFIG.IGNORE_CLEAN_RELEASES: - for each_word in ['clean','edited','censored']: + for each_word in ['clean', 'edited', 'censored']: logger.debug("Checking if '%s' is in search result: '%s'", each_word, title) if each_word.lower() in title.lower() and each_word.lower() not in term.lower(): - logger.info("Removed '%s' from results because it contains clean album word: '%s'", title, each_word) + logger.info("Removed '%s' from results because it contains clean album word: '%s'", + title, each_word) return False tokens = re.split('\W', term, re.IGNORECASE | re.UNICODE) @@ -1022,27 +1064,31 @@ def verifyresult(title, artistterm, term, lossless): continue if not re.search('(?:\W|^)+' + token + '(?:\W|$)+', title, re.IGNORECASE | re.UNICODE): cleantoken = ''.join(c for c in token if c not in string.punctuation) - if not not re.search('(?:\W|^)+' + cleantoken + '(?:\W|$)+', title, re.IGNORECASE | re.UNICODE): + if not not re.search('(?:\W|^)+' + cleantoken + '(?:\W|$)+', title, + re.IGNORECASE | re.UNICODE): dic = {'!': 'i', '$': 's'} dumbtoken = helpers.replace_all(token, dic) - if not not re.search('(?:\W|^)+' + dumbtoken + '(?:\W|$)+', title, re.IGNORECASE | re.UNICODE): - logger.info("Removed from results: %s (missing tokens: %s and %s)", title, token, cleantoken) + if not not re.search('(?:\W|^)+' + dumbtoken + '(?:\W|$)+', title, + re.IGNORECASE | re.UNICODE): + logger.info("Removed from results: %s (missing tokens: %s and %s)", title, + token, cleantoken) return False return True -def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose_specific_download=False): +def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, + choose_specific_download=False): global gazelle # persistent what.cd api object to reduce number of login attempts - global ruobj # and rutracker + global ruobj # and rutracker - albumid = album['AlbumID'] reldate = album['ReleaseDate'] year = get_year_from_release_date(reldate) # MERGE THIS WITH THE TERM CLEANUP FROM searchNZB - dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ', '"': '', ',': ' ', '*': ''} + dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ', '"': '', ',': ' ', + '*': ''} semi_cleanalbum = helpers.replace_all(album['AlbumTitle'], dic) cleanalbum = helpers.latinToAscii(semi_cleanalbum) @@ -1059,7 +1105,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose # FLAC usually doesn't have a year for some reason so I'll leave it out # Various Artist albums might be listed as VA, so I'll leave that out too # Only use the year if the term could return a bunch of different albums, i.e. self-titled albums - if album['ArtistName'] in album['AlbumTitle'] or len(album['ArtistName']) < 4 or len(album['AlbumTitle']) < 4: + if album['ArtistName'] in album['AlbumTitle'] or len(album['ArtistName']) < 4 or len( + album['AlbumTitle']) < 4: term = cleanartist + ' ' + cleanalbum + ' ' + year elif album['ArtistName'] == 'Various Artists': term = cleanalbum + ' ' + year @@ -1103,7 +1150,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose torznab_hosts = [] if headphones.CONFIG.TORZNAB_HOST and headphones.CONFIG.TORZNAB_ENABLED: - torznab_hosts.append((headphones.CONFIG.TORZNAB_HOST, headphones.CONFIG.TORZNAB_APIKEY, headphones.CONFIG.TORZNAB_ENABLED)) + torznab_hosts.append((headphones.CONFIG.TORZNAB_HOST, headphones.CONFIG.TORZNAB_APIKEY, + headphones.CONFIG.TORZNAB_ENABLED)) for torznab_host in headphones.CONFIG.get_extra_torznabs(): if torznab_host[2] == '1' or torznab_host[2] == 1: @@ -1125,7 +1173,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose provider = torznab_host[0] # Request results - logger.info('Parsing results from %s using search term: %s' % (torznab_host[0],term)) + logger.info('Parsing results from %s using search term: %s' % (torznab_host[0], term)) headers = {'User-Agent': USER_AGENT} params = { @@ -1152,13 +1200,15 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose title = item.title size = int(item.links[1]['length']) if all(word.lower() in title.lower() for word in term.split()): - logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) + logger.info( + 'Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) resultlist.append((title, size, url, provider, 'torrent', True)) else: logger.info('Skipping %s, not all search term words found' % title) except Exception as e: - logger.exception("An unknown error occurred trying to parse the feed: %s" % e) + logger.exception( + "An unknown error occurred trying to parse the feed: %s" % e) if headphones.CONFIG.KAT: provider = "Kick Ass Torrents" @@ -1185,7 +1235,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose providerurl += " category:music/" # Requesting content - logger.info("Searching %s using term: %s" % (provider,ka_term)) + logger.info("Searching %s using term: %s" % (provider, ka_term)) params = { "field": "seeders", @@ -1194,7 +1244,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose } data = request.request_feed(url=providerurl, params=params, - whitelist_status_code=[404]) + whitelist_status_code=[404]) # Process feed if data: @@ -1214,7 +1264,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose resultlist.append((title, size, url, provider, 'torrent', True)) logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) else: - logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %d)', title, size, int(seeders)) + logger.info( + '%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %d)', + title, size, int(seeders)) except Exception as e: logger.exception("Unhandled exception in the KAT parser") @@ -1243,7 +1295,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose query_items.extend(['format:(%s)' % format, 'size:[0 TO %d]' % maxsize, - '-seeders:0']) # cut out dead torrents + '-seeders:0']) # cut out dead torrents if bitrate: query_items.append('bitrate:"%s"' % bitrate) @@ -1256,8 +1308,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose "passkey": headphones.CONFIG.WAFFLES_PASSKEY, "rss": "1", "c0": "1", - "s": "seeders", # sort by - "d": "desc", # direction + "s": "seeders", # sort by + "d": "desc", # direction "q": " ".join(query_items) } @@ -1281,7 +1333,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose resultlist.append((title, size, url, provider, 'torrent', True)) logger.info('Found %s. Size: %s', title, helpers.bytes_to_mb(size)) except Exception as e: - logger.error(u"An error occurred while trying to parse the response from Waffles.fm: %s", e) + logger.error( + u"An error occurred while trying to parse the response from Waffles.fm: %s", + e) # rutracker.org if headphones.CONFIG.RUTRACKER: @@ -1341,7 +1395,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose if re.search(bitrate, encoding_string, flags=re.I): bitrate_string = encoding_string if bitrate_string not in gazelleencoding.ALL_ENCODINGS: - logger.info(u"Your preferred bitrate is not one of the available What.cd filters, so not using it as a search parameter.") + logger.info( + u"Your preferred bitrate is not one of the available What.cd filters, so not using it as a search parameter.") maxsize = 10000000000 elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless search_formats = [gazelleformat.FLAC, gazelleformat.MP3] @@ -1353,28 +1408,35 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose if not gazelle or not gazelle.logged_in(): try: logger.info(u"Attempting to log in to What.cd...") - gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME, headphones.CONFIG.WHATCD_PASSWORD) + gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME, + headphones.CONFIG.WHATCD_PASSWORD) gazelle._login() except Exception as e: gazelle = None - logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % (e.__class__.__name__, str(e))) + logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % ( + e.__class__.__name__, str(e))) if gazelle and gazelle.logged_in(): logger.info(u"Searching %s..." % provider) all_torrents = [] for search_format in search_formats: if usersearchterm: - all_torrents.extend(gazelle.search_torrents(searchstr=usersearchterm, format=search_format, encoding=bitrate_string)['results']) + all_torrents.extend( + gazelle.search_torrents(searchstr=usersearchterm, format=search_format, + encoding=bitrate_string)['results']) else: all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term, - groupname=semi_clean_album_term, - format=search_format, encoding=bitrate_string)['results']) + groupname=semi_clean_album_term, + format=search_format, + encoding=bitrate_string)['results']) # filter on format, size, and num seeders logger.info(u"Filtering torrents by format, maximum size, and minimum seeders...") - match_torrents = [t for t in all_torrents if t.size <= maxsize and t.seeders >= minimumseeders] + match_torrents = [t for t in all_torrents if + t.size <= maxsize and t.seeders >= minimumseeders] - logger.info(u"Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents)) + logger.info( + u"Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents)) # sort by times d/l'd if not len(match_torrents): @@ -1382,21 +1444,25 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose elif len(match_torrents) > 1: logger.info(u"Found %d matching releases from %s for %s - %s after filtering" % (len(match_torrents), provider, artistterm, albumterm)) - logger.info("Sorting torrents by times snatched and preferred bitrate %s..." % bitrate_string) + logger.info( + "Sorting torrents by times snatched and preferred bitrate %s..." % bitrate_string) match_torrents.sort(key=lambda x: int(x.snatched), reverse=True) if gazelleformat.MP3 in search_formats: # sort by size after rounding to nearest 10MB...hacky, but will favor highest quality - match_torrents.sort(key=lambda x: int(10 * round(x.size / 1024. / 1024. / 10.)), reverse=True) + match_torrents.sort(key=lambda x: int(10 * round(x.size / 1024. / 1024. / 10.)), + reverse=True) if search_formats and None not in search_formats: - match_torrents.sort(key=lambda x: int(search_formats.index(x.format))) # prefer lossless -# if bitrate: -# match_torrents.sort(key=lambda x: re.match("mp3", x.getTorrentDetails(), flags=re.I), reverse=True) -# match_torrents.sort(key=lambda x: str(bitrate) in x.getTorrentFolderName(), reverse=True) - logger.info(u"New order: %s" % ", ".join(repr(torrent) for torrent in match_torrents)) + match_torrents.sort( + key=lambda x: int(search_formats.index(x.format))) # prefer lossless + # if bitrate: + # match_torrents.sort(key=lambda x: re.match("mp3", x.getTorrentDetails(), flags=re.I), reverse=True) + # match_torrents.sort(key=lambda x: str(bitrate) in x.getTorrentFolderName(), reverse=True) + logger.info( + u"New order: %s" % ", ".join(repr(torrent) for torrent in match_torrents)) for torrent in match_torrents: if not torrent.file_path: - torrent.group.update_group_data() # will load the file_path for the individual torrents + torrent.group.update_group_data() # will load the file_path for the individual torrents resultlist.append((torrent.file_path, torrent.size, gazelle.generate_torrent_link(torrent.id), @@ -1415,23 +1481,24 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose providerurl = fix_url("https://thepiratebay.se") # Build URL - providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders + providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders # Pick category for torrents if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: - category = '104' # FLAC + category = '104' # FLAC maxsize = 10000000000 elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: - category = '100' # General audio category + category = '100' # General audio category maxsize = 10000000000 else: - category = '101' # MP3 only + category = '101' # MP3 only maxsize = 300000000 # Request content logger.info("Searching The Pirate Bay using term: %s", tpb_term) - headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'} + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'} data = request.request_soup(url=providerurl + category, headers=headers) # Process content @@ -1463,7 +1530,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose if url.lower().startswith("//"): url = "http:" + url - formatted_size = re.search('Size (.*),', unicode(item)).group(1).replace(u'\xa0', ' ') + formatted_size = re.search('Size (.*),', unicode(item)).group(1).replace( + u'\xa0', ' ') size = helpers.piratesize(formatted_size) if size < maxsize and minimumseeders < seeds and url is not None: @@ -1471,7 +1539,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose logger.info('Found %s. Size: %s' % (title, formatted_size)) else: match = False - logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (title, size, int(seeds))) + logger.info( + '%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % ( + title, size, int(seeds))) resultlist.append((title, size, url, provider, "torrent", match)) except Exception as e: @@ -1493,9 +1563,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose # Requesting content logger.info("Parsing results from Old Pirate Bay") - headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'} + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'} provider_url = fix_url(headphones.CONFIG.OLDPIRATEBAY_URL) + \ - "/search.php?" + urllib.urlencode({"q": tpb_term, "iht": 6}) + "/search.php?" + urllib.urlencode({"q": tpb_term, "iht": 6}) data = request.request_soup(url=provider_url, headers=headers) @@ -1513,7 +1584,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose rightformat = True title = links[1].text seeds = int(item.select("td.seeders-row")[0].text) - url = links[0]["href"] # Magnet link. The actual download link is not based on the URL + url = links[0][ + "href"] # Magnet link. The actual download link is not based on the URL formatted_size = item.select("td.size-row")[0].text size = helpers.piratesize(formatted_size) @@ -1523,11 +1595,14 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose logger.info('Found %s. Size: %s' % (title, formatted_size)) else: match = False - logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (title, size, int(seeds))) + logger.info( + '%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % ( + title, size, int(seeds))) resultlist.append((title, size, url, provider, "torrent", match)) except Exception as e: - logger.error(u"An unknown error occurred in the Old Pirate Bay parser: %s" % e) + logger.error( + u"An unknown error occurred in the Old Pirate Bay parser: %s" % e) # Strike if headphones.CONFIG.STRIKE: @@ -1547,7 +1622,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose logger.info("Searching %s using term: %s" % (provider, s_term)) data = request.request_json(url=providerurl, - whitelist_status_code=[404]) + whitelist_status_code=[404]) if not data or not data.get('torrents'): logger.info("No results found on %s using search term: %s" % (provider, s_term)) @@ -1558,7 +1633,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose seeders = item['seeds'] url = item['magnet_uri'] size = int(item['size']) - subcategory = item['sub_category'] if size < maxsize and minimumseeders < int(seeders): resultlist.append((title, size, url, provider, 'torrent', True)) @@ -1577,15 +1651,15 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: # categories = "7" #music - format = "2" #flac + format = "2" # flac maxsize = 10000000000 elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # categories = "7" #music - format = "10" #mp3+flac + format = "10" # mp3+flac maxsize = 10000000000 else: # categories = "7" #music - format = "8" #mp3 + format = "8" # mp3 maxsize = 300000000 # Requesting content @@ -1614,7 +1688,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose size = int(item.links[1]['length']) if format == "2": torrent = request.request_content(url) - if not torrent or (int(torrent.find(".mp3")) > 0 and int(torrent.find(".flac")) < 1): + if not torrent or (int(torrent.find(".mp3")) > 0 and int( + torrent.find(".flac")) < 1): rightformat = False if rightformat and size < maxsize and minimumseeders < seeds: @@ -1622,16 +1697,19 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) else: match = False - logger.info('%s is larger than the maxsize, the wrong format or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i, Format: %s)' % (title, size, int(seeds), rightformat)) + logger.info( + '%s is larger than the maxsize, the wrong format or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i, Format: %s)' % ( + title, size, int(seeds), rightformat)) resultlist.append((title, size, url, provider, 'torrent', match)) except Exception as e: logger.exception("Unhandled exception in Mininova Parser") - #attempt to verify that this isn't a substring result - #when looking for "Foo - Foo" we don't want "Foobar" - #this should be less of an issue when it isn't a self-titled album so we'll only check vs artist - results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)] + # attempt to verify that this isn't a substring result + # when looking for "Foo - Foo" we don't want "Foobar" + # this should be less of an issue when it isn't a self-titled album so we'll only check vs artist + results = [result for result in resultlist if + verifyresult(result[0], artistterm, term, losslessOnly)] # Additional filtering for size etc if results and not choose_specific_download: @@ -1639,11 +1717,11 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose return results + # THIS IS KIND OF A MESS AND PROBABLY NEEDS TO BE CLEANED UP def preprocess(resultlist): - for result in resultlist: if result[4] == 'torrent': @@ -1651,7 +1729,7 @@ def preprocess(resultlist): if result[3] == 'rutracker.org': return ruobj.get_torrent_data(result[2]), result - #Get out of here if we're using Transmission + # Get out of here if we're using Transmission if headphones.CONFIG.TORRENT_DOWNLOADER == 1: ## if not a magnet link still need the .torrent to generate hash... uTorrent support labeling return True, result # Get out of here if it's a magnet link @@ -1667,13 +1745,15 @@ def preprocess(resultlist): elif result[3] == 'What.cd': headers['User-Agent'] = 'Headphones' elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay": - headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36' + headers[ + 'User-Agent'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36' return request.request_content(url=result[2], headers=headers), result else: headers = {'User-Agent': USER_AGENT} if result[3] == 'headphones': - return request.request_content(url=result[2], headers=headers, auth=(headphones.CONFIG.HPUSER, headphones.CONFIG.HPPASS)), result + return request.request_content(url=result[2], headers=headers, auth=( + headphones.CONFIG.HPUSER, headphones.CONFIG.HPPASS)), result else: return request.request_content(url=result[2], headers=headers), result diff --git a/headphones/torrentfinished.py b/headphones/torrentfinished.py index 24e9a4b0..bc622291 100644 --- a/headphones/torrentfinished.py +++ b/headphones/torrentfinished.py @@ -13,13 +13,14 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from headphones import db, utorrent, transmission, logger - import threading + +from headphones import db, utorrent, transmission, logger import headphones postprocessor_lock = threading.Lock() + def checkTorrentFinished(): """ Remove Torrent + data if Post Processed and finished Seeding @@ -41,6 +42,7 @@ def checkTorrentFinished(): torrent_removed = utorrent.removeTorrent(hash, True) if torrent_removed: - myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?', [albumid]) + myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?', + [albumid]) logger.info("Checking finished torrents completed") diff --git a/headphones/transmission.py b/headphones/transmission.py index ab5ae6c9..3ad0327c 100644 --- a/headphones/transmission.py +++ b/headphones/transmission.py @@ -13,14 +13,15 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from headphones import logger, request - import time import json import base64 import urlparse + +from headphones import logger, request import headphones + # This is just a simple script to send torrents to transmission. The # intention is to turn this into a class where we can check the state # of the download, set the download dir, etc. @@ -96,7 +97,6 @@ def setSeedRatio(torrentid, ratio): def removeTorrent(torrentid, remove_data=False): - method = 'torrent-get' arguments = {'ids': torrentid, 'fields': ['isFinished', 'name']} @@ -118,7 +118,8 @@ def removeTorrent(torrentid, remove_data=False): response = torrentAction(method, arguments) return True else: - logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % name) + logger.info( + '%s has not finished seeding yet, torrent will not be removed, will try again on next run' % name) except: return False @@ -126,7 +127,6 @@ def removeTorrent(torrentid, remove_data=False): def torrentAction(method, arguments): - host = headphones.CONFIG.TRANSMISSION_HOST username = headphones.CONFIG.TRANSMISSION_USERNAME password = headphones.CONFIG.TRANSMISSION_PASSWORD @@ -152,7 +152,7 @@ def torrentAction(method, arguments): # Retrieve session id auth = (username, password) if username and password else None response = request.request_response(host, auth=auth, - whitelist_status_code=[401, 409]) + whitelist_status_code=[401, 409]) if response is None: logger.error("Error gettings Transmission session ID") @@ -162,7 +162,7 @@ def torrentAction(method, arguments): if response.status_code == 401: if auth: logger.error("Username and/or password not accepted by " \ - "Transmission") + "Transmission") else: logger.error("Transmission authorization required") @@ -179,7 +179,7 @@ def torrentAction(method, arguments): data = {'method': method, 'arguments': arguments} response = request.request_json(host, method="POST", data=json.dumps(data), - headers=headers, auth=auth) + headers=headers, auth=auth) print response diff --git a/headphones/updater.py b/headphones/updater.py index b750c013..0dc49e52 100644 --- a/headphones/updater.py +++ b/headphones/updater.py @@ -17,10 +17,10 @@ from headphones import logger, db, importer def dbUpdate(forcefull=False): - myDB = db.DBConnection() - active_artists = myDB.select('SELECT ArtistID, ArtistName from artists WHERE Status="Active" or Status="Loading" order by LastUpdated ASC') + active_artists = myDB.select( + 'SELECT ArtistID, ArtistName from artists WHERE Status="Active" or Status="Loading" order by LastUpdated ASC') logger.info('Starting update for %i active artists', len(active_artists)) for artist in active_artists: diff --git a/headphones/utorrent.py b/headphones/utorrent.py index a826e928..43befbd2 100644 --- a/headphones/utorrent.py +++ b/headphones/utorrent.py @@ -14,26 +14,24 @@ # along with Headphones. If not, see . import urllib +import json +import time +from collections import namedtuple import urllib2 import urlparse import cookielib -import json + import re import os -import time - import headphones - from headphones import logger -from collections import namedtuple class utorrentclient(object): - TOKEN_REGEX = "" UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"]) - def __init__(self, base_url=None, username=None, password=None,): + def __init__(self, base_url=None, username=None, password=None, ): host = headphones.CONFIG.UTORRENT_HOST if not host.startswith('http'): @@ -50,7 +48,7 @@ class utorrentclient(object): self.password = headphones.CONFIG.UTORRENT_PASSWORD self.opener = self._make_opener('uTorrent', self.base_url, self.username, self.password) self.token = self._get_token() - #TODO refresh token, when necessary + # TODO refresh token, when necessary def _make_opener(self, realm, base_url, username, password): """uTorrent API need HTTP Basic Auth and cookie support for token verify.""" @@ -83,7 +81,7 @@ class utorrentclient(object): return self._action(params) def add_url(self, url): - #can receive magnet or normal .torrent link + # can receive magnet or normal .torrent link params = [('action', 'add-url'), ('s', url)] return self._action(params) @@ -187,7 +185,9 @@ def removeTorrent(hash, remove_data=False): uTorrentClient.remove(hash, remove_data) return True else: - logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % torrent[2]) + logger.info( + '%s has not finished seeding yet, torrent will not be removed, will try again on next run' % + torrent[2]) return False return False @@ -203,7 +203,6 @@ def setSeedRatio(hash, ratio): def dirTorrent(hash, cacheid=None, return_name=None): - uTorrentClient = utorrentclient() if not cacheid: @@ -228,19 +227,20 @@ def dirTorrent(hash, cacheid=None, return_name=None): return None, None + def addTorrent(link): uTorrentClient = utorrentclient() uTorrentClient.add_url(link) def getFolder(hash): - uTorrentClient = utorrentclient() # Get Active Directory from settings active_dir, completed_dir = getSettingsDirectories() if not active_dir: - logger.error('Could not get "Put new downloads in:" directory from uTorrent settings, please ensure it is set') + logger.error( + 'Could not get "Put new downloads in:" directory from uTorrent settings, please ensure it is set') return None # Get Torrent Folder Name diff --git a/headphones/versioncheck.py b/headphones/versioncheck.py index ea2a8e3f..30098886 100644 --- a/headphones/versioncheck.py +++ b/headphones/versioncheck.py @@ -13,18 +13,17 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -import re -import os import tarfile import platform -import headphones import subprocess +import re +import os +import headphones from headphones import logger, version, request def runGit(args): - if headphones.CONFIG.GIT_PATH: git_locations = ['"' + headphones.CONFIG.GIT_PATH + '"'] else: @@ -40,7 +39,8 @@ def runGit(args): try: logger.debug('Trying to execute: "' + cmd + '" with shell in ' + headphones.PROG_DIR) - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=headphones.PROG_DIR) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, + cwd=headphones.PROG_DIR) output, err = p.communicate() output = output.strip() @@ -62,7 +62,6 @@ def runGit(args): def getVersion(): - if version.HEADPHONES_VERSION.startswith('win32build'): headphones.INSTALL_TYPE = 'win' @@ -92,7 +91,8 @@ def getVersion(): branch_name = branch_name if not branch_name and headphones.CONFIG.GIT_BRANCH: - logger.error('Could not retrieve branch name from git. Falling back to %s' % headphones.CONFIG.GIT_BRANCH) + logger.error( + 'Could not retrieve branch name from git. Falling back to %s' % headphones.CONFIG.GIT_BRANCH) branch_name = headphones.CONFIG.GIT_BRANCH if not branch_name: logger.error('Could not retrieve branch name from git. Defaulting to master') @@ -123,11 +123,13 @@ def checkGithub(): # Get the latest version available from github logger.info('Retrieving latest version information from GitHub') - url = 'https://api.github.com/repos/%s/headphones/commits/%s' % (headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH) + url = 'https://api.github.com/repos/%s/headphones/commits/%s' % ( + headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH) version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict) if version is None: - logger.warn('Could not get the latest version from GitHub. Are you running a local development version?') + logger.warn( + 'Could not get the latest version from GitHub. Are you running a local development version?') return headphones.CURRENT_VERSION headphones.LATEST_VERSION = version['sha'] @@ -135,7 +137,8 @@ def checkGithub(): # See how many commits behind we are if not headphones.CURRENT_VERSION: - logger.info('You are running an unknown version of Headphones. Run the updater to identify your version') + logger.info( + 'You are running an unknown version of Headphones. Run the updater to identify your version') return headphones.LATEST_VERSION if headphones.LATEST_VERSION == headphones.CURRENT_VERSION: @@ -143,8 +146,10 @@ def checkGithub(): return headphones.LATEST_VERSION logger.info('Comparing currently installed version with latest GitHub version') - url = 'https://api.github.com/repos/%s/headphones/compare/%s...%s' % (headphones.CONFIG.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION) - commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict) + url = 'https://api.github.com/repos/%s/headphones/compare/%s...%s' % ( + headphones.CONFIG.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION) + commits = request.request_json(url, timeout=20, whitelist_status_code=404, + validator=lambda x: type(x) == dict) if commits is None: logger.warn('Could not get commits behind from GitHub.') @@ -158,7 +163,8 @@ def checkGithub(): headphones.COMMITS_BEHIND = 0 if headphones.COMMITS_BEHIND > 0: - logger.info('New version is available. You are %s commits behind' % headphones.COMMITS_BEHIND) + logger.info( + 'New version is available. You are %s commits behind' % headphones.COMMITS_BEHIND) elif headphones.COMMITS_BEHIND == 0: logger.info('Headphones is up to date') @@ -185,7 +191,8 @@ def update(): logger.info('Output: ' + str(output)) else: - tar_download_url = 'https://github.com/%s/headphones/tarball/%s' % (headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH) + tar_download_url = 'https://github.com/%s/headphones/tarball/%s' % ( + headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH) update_dir = os.path.join(headphones.PROG_DIR, 'update') version_path = os.path.join(headphones.PROG_DIR, 'version.txt') @@ -214,7 +221,8 @@ def update(): os.remove(tar_download_path) # Find update dir name - update_dir_contents = [x for x in os.listdir(update_dir) if os.path.isdir(os.path.join(update_dir, x))] + update_dir_contents = [x for x in os.listdir(update_dir) if + os.path.isdir(os.path.join(update_dir, x))] if len(update_dir_contents) != 1: logger.error("Invalid update data, update failed: " + str(update_dir_contents)) return diff --git a/headphones/webserve.py b/headphones/webserve.py index 815c5e9f..cdc503a0 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -15,18 +15,8 @@ # NZBGet support added by CurlyMo as a part of XBian - XBMC on the Raspberry Pi -from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers -from headphones.helpers import checked, radio, today, cleanName - -from mako.lookup import TemplateLookup -from mako import exceptions - from operator import itemgetter - -import headphones import threading -import cherrypy -import urllib2 import hashlib import random import urllib @@ -34,8 +24,16 @@ import json import time import cgi import sys +import urllib2 + import os import re +from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers +from headphones.helpers import checked, radio, today, cleanName +from mako.lookup import TemplateLookup +from mako import exceptions +import headphones +import cherrypy try: # pylint:disable=E0611 @@ -48,7 +46,6 @@ except ImportError: def serve_template(templatename, **kwargs): - interface_dir = os.path.join(str(headphones.PROG_DIR), 'data/interfaces/') template_dir = os.path.join(str(interface_dir), headphones.CONFIG.INTERFACE) @@ -62,7 +59,6 @@ def serve_template(templatename, **kwargs): class WebInterface(object): - @cherrypy.expose def index(self): raise cherrypy.HTTPRedirect("home") @@ -90,7 +86,8 @@ class WebInterface(object): if not artist: raise cherrypy.HTTPRedirect("home") - albums = myDB.select('SELECT * from albums WHERE ArtistID=? order by ReleaseDate DESC', [ArtistID]) + albums = myDB.select('SELECT * from albums WHERE ArtistID=? order by ReleaseDate DESC', + [ArtistID]) # Serve the extras up as a dict to make things easier for new templates (append new extras to the end) extras_list = headphones.POSSIBLE_EXTRAS @@ -109,7 +106,8 @@ class WebInterface(object): extras_dict[extra] = "" i += 1 - return serve_template(templatename="artist.html", title=artist['ArtistName'], artist=artist, albums=albums, extras=extras_dict) + return serve_template(templatename="artist.html", title=artist['ArtistName'], artist=artist, + albums=albums, extras=extras_dict) @cherrypy.expose def albumPage(self, AlbumID): @@ -128,8 +126,10 @@ class WebInterface(object): if not album: raise cherrypy.HTTPRedirect("home") - tracks = myDB.select('SELECT * from tracks WHERE AlbumID=? ORDER BY CAST(TrackNumber AS INTEGER)', [AlbumID]) - description = myDB.action('SELECT * from descriptions WHERE ReleaseGroupID=?', [AlbumID]).fetchone() + tracks = myDB.select( + 'SELECT * from tracks WHERE AlbumID=? ORDER BY CAST(TrackNumber AS INTEGER)', [AlbumID]) + description = myDB.action('SELECT * from descriptions WHERE ReleaseGroupID=?', + [AlbumID]).fetchone() if not album['ArtistName']: title = ' - ' @@ -139,7 +139,8 @@ class WebInterface(object): title = title + "" else: title = title + album['AlbumTitle'] - return serve_template(templatename="album.html", title=title, album=album, tracks=tracks, description=description) + return serve_template(templatename="album.html", title=title, album=album, tracks=tracks, + description=description) @cherrypy.expose def search(self, name, type): @@ -151,7 +152,9 @@ class WebInterface(object): searchresults = mb.findRelease(name, limit=100) else: searchresults = mb.findSeries(name, limit=100) - return serve_template(templatename="searchresults.html", title='Search Results for: "' + cgi.escape(name) + '"', searchresults=searchresults, name=cgi.escape(name), type=type) + return serve_template(templatename="searchresults.html", + title='Search Results for: "' + cgi.escape(name) + '"', + searchresults=searchresults, name=cgi.escape(name), type=type) @cherrypy.expose def addArtist(self, artistid): @@ -162,7 +165,8 @@ class WebInterface(object): @cherrypy.expose def addSeries(self, seriesid): - thread = threading.Thread(target=importer.addArtisttoDB, args=[seriesid, False, False, "series"]) + thread = threading.Thread(target=importer.addArtisttoDB, + args=[seriesid, False, False, "series"]) thread.start() thread.join(1) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % seriesid) @@ -200,12 +204,18 @@ class WebInterface(object): controlValueDict = {'ArtistID': ArtistID} newValueDict = {'IncludeExtras': 0} myDB.upsert("artists", newValueDict, controlValueDict) - extraalbums = myDB.select('SELECT AlbumID from albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"', [ArtistID]) + extraalbums = myDB.select( + 'SELECT AlbumID from albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"', + [ArtistID]) for album in extraalbums: - myDB.action('DELETE from tracks WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']]) - myDB.action('DELETE from albums WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']]) - myDB.action('DELETE from allalbums WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']]) - myDB.action('DELETE from alltracks WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']]) + myDB.action('DELETE from tracks WHERE ArtistID=? AND AlbumID=?', + [ArtistID, album['AlbumID']]) + myDB.action('DELETE from albums WHERE ArtistID=? AND AlbumID=?', + [ArtistID, album['AlbumID']]) + myDB.action('DELETE from allalbums WHERE ArtistID=? AND AlbumID=?', + [ArtistID, album['AlbumID']]) + myDB.action('DELETE from alltracks WHERE ArtistID=? AND AlbumID=?', + [ArtistID, album['AlbumID']]) myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [album['AlbumID']]) from headphones import cache c = cache.Cache() @@ -242,7 +252,9 @@ class WebInterface(object): from headphones import cache c = cache.Cache() - rgids = myDB.select('SELECT AlbumID FROM albums WHERE ArtistID=? UNION SELECT AlbumID FROM allalbums WHERE ArtistID=?', [ArtistID, ArtistID]) + rgids = myDB.select( + 'SELECT AlbumID FROM albums WHERE ArtistID=? UNION SELECT AlbumID FROM allalbums WHERE ArtistID=?', + [ArtistID, ArtistID]) for rgid in rgids: albumid = rgid['AlbumID'] myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [albumid]) @@ -269,17 +281,19 @@ class WebInterface(object): def scanArtist(self, ArtistID): myDB = db.DBConnection() - artist_name = myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID])[0][0] + artist_name = \ + myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID])[0][0] logger.info(u"Scanning artist: %s", artist_name) full_folder_format = headphones.CONFIG.FOLDER_FORMAT folder_format = re.findall(r'(.*?[Aa]rtist?)\.*', full_folder_format)[0] - acceptable_formats = ["$artist","$sortartist","$first/$artist","$first/$sortartist"] + acceptable_formats = ["$artist", "$sortartist", "$first/$artist", "$first/$sortartist"] if not folder_format.lower() in acceptable_formats: - logger.info("Can't determine the artist folder from the configured folder_format. Not scanning") + logger.info( + "Can't determine the artist folder from the configured folder_format. Not scanning") return # Format the folder to match the settings @@ -299,12 +313,12 @@ class WebInterface(object): firstchar = sortname[0] values = {'$Artist': artist, - '$SortArtist': sortname, - '$First': firstchar.upper(), - '$artist': artist.lower(), - '$sortartist': sortname.lower(), - '$first': firstchar.lower(), - } + '$SortArtist': sortname, + '$First': firstchar.upper(), + '$artist': artist.lower(), + '$sortartist': sortname.lower(), + '$first': firstchar.lower(), + } folder = helpers.replace_all(folder_format.strip(), values, normalize=True) @@ -332,14 +346,17 @@ class WebInterface(object): if not os.path.isdir(artistfolder): logger.debug("Cannot find directory: " + artistfolder) continue - threading.Thread(target=librarysync.libraryScan, kwargs={"dir":artistfolder, "artistScan":True, "ArtistID":ArtistID, "ArtistName":artist_name}).start() + threading.Thread(target=librarysync.libraryScan, + kwargs={"dir": artistfolder, "artistScan": True, "ArtistID": ArtistID, + "ArtistName": artist_name}).start() raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) @cherrypy.expose def deleteEmptyArtists(self): logger.info(u"Deleting all empty artists") myDB = db.DBConnection() - emptyArtistIDs = [row['ArtistID'] for row in myDB.select("SELECT ArtistID FROM artists WHERE LatestAlbum IS NULL")] + emptyArtistIDs = [row['ArtistID'] for row in + myDB.select("SELECT ArtistID FROM artists WHERE LatestAlbum IS NULL")] for ArtistID in emptyArtistIDs: self.removeArtist(ArtistID) @@ -371,8 +388,11 @@ class WebInterface(object): if ArtistID: ArtistIDT = ArtistID else: - ArtistIDT = myDB.action('SELECT ArtistID FROM albums WHERE AlbumID=?', [mbid]).fetchone()[0] - myDB.action('UPDATE artists SET TotalTracks=(SELECT COUNT(*) FROM tracks WHERE ArtistID = ? AND AlbumTitle IN (SELECT AlbumTitle FROM albums WHERE Status != "Ignored")) WHERE ArtistID = ?', [ArtistIDT, ArtistIDT]) + ArtistIDT = \ + myDB.action('SELECT ArtistID FROM albums WHERE AlbumID=?', [mbid]).fetchone()[0] + myDB.action( + 'UPDATE artists SET TotalTracks=(SELECT COUNT(*) FROM tracks WHERE ArtistID = ? AND AlbumTitle IN (SELECT AlbumTitle FROM albums WHERE Status != "Ignored")) WHERE ArtistID = ?', + [ArtistIDT, ArtistIDT]) if ArtistID: raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) else: @@ -385,8 +405,10 @@ class WebInterface(object): if action == "ignore": myDB = db.DBConnection() for artist in args: - myDB.action('DELETE FROM newartists WHERE ArtistName=?', [artist.decode(headphones.SYS_ENCODING, 'replace')]) - myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=?', [artist.decode(headphones.SYS_ENCODING, 'replace')]) + myDB.action('DELETE FROM newartists WHERE ArtistName=?', + [artist.decode(headphones.SYS_ENCODING, 'replace')]) + myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=?', + [artist.decode(headphones.SYS_ENCODING, 'replace')]) logger.info("Artist %s removed from new artist list and set to ignored" % artist) raise cherrypy.HTTPRedirect("home") @@ -440,12 +462,12 @@ class WebInterface(object): (data, bestqual) = searcher.preprocess(result) if data and bestqual: - myDB = db.DBConnection() - album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone() - searcher.send_to_downloader(data, bestqual, album) - return json.dumps({'result':'success'}) + myDB = db.DBConnection() + album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone() + searcher.send_to_downloader(data, bestqual, album) + return json.dumps({'result': 'success'}) else: - return json.dumps({'result':'failure'}) + return json.dumps({'result': 'failure'}) @cherrypy.expose def unqueueAlbum(self, AlbumID, ArtistID): @@ -462,10 +484,12 @@ class WebInterface(object): myDB = db.DBConnection() myDB.action('DELETE from have WHERE Matched=?', [AlbumID]) - album = myDB.action('SELECT ArtistID, ArtistName, AlbumTitle from albums where AlbumID=?', [AlbumID]).fetchone() + album = myDB.action('SELECT ArtistID, ArtistName, AlbumTitle from albums where AlbumID=?', + [AlbumID]).fetchone() if album: ArtistID = album['ArtistID'] - myDB.action('DELETE from have WHERE ArtistName=? AND AlbumTitle=?', [album['ArtistName'], album['AlbumTitle']]) + myDB.action('DELETE from have WHERE ArtistName=? AND AlbumTitle=?', + [album['ArtistName'], album['AlbumTitle']]) myDB.action('DELETE from albums WHERE AlbumID=?', [AlbumID]) myDB.action('DELETE from tracks WHERE AlbumID=?', [AlbumID]) @@ -505,9 +529,11 @@ class WebInterface(object): @cherrypy.expose def upcoming(self): myDB = db.DBConnection() - upcoming = myDB.select("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate ASC") + upcoming = myDB.select( + "SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate ASC") wanted = myDB.select("SELECT * from albums WHERE Status='Wanted'") - return serve_template(templatename="upcoming.html", title="Upcoming", upcoming=upcoming, wanted=wanted) + return serve_template(templatename="upcoming.html", title="Upcoming", upcoming=upcoming, + wanted=wanted) @cherrypy.expose def manage(self): @@ -519,7 +545,8 @@ class WebInterface(object): def manageArtists(self): myDB = db.DBConnection() artists = myDB.select('SELECT * from artists order by ArtistSortName COLLATE NOCASE') - return serve_template(templatename="manageartists.html", title="Manage Artists", artists=artists) + return serve_template(templatename="manageartists.html", title="Manage Artists", + artists=artists) @cherrypy.expose def manageAlbums(self, Status=None): @@ -530,87 +557,115 @@ class WebInterface(object): albums = myDB.select('SELECT * from albums WHERE Status=?', [Status]) else: albums = myDB.select('SELECT * from albums') - return serve_template(templatename="managealbums.html", title="Manage Albums", albums=albums) + return serve_template(templatename="managealbums.html", title="Manage Albums", + albums=albums) @cherrypy.expose def manageNew(self): myDB = db.DBConnection() newartists = myDB.select('SELECT * from newartists') - return serve_template(templatename="managenew.html", title="Manage New Artists", newartists=newartists) + return serve_template(templatename="managenew.html", title="Manage New Artists", + newartists=newartists) @cherrypy.expose def manageUnmatched(self): myDB = db.DBConnection() have_album_dictionary = [] headphones_album_dictionary = [] - have_albums = myDB.select('SELECT ArtistName, AlbumTitle, TrackTitle, CleanName from have WHERE Matched = "Failed" GROUP BY AlbumTitle ORDER BY ArtistName') + have_albums = myDB.select( + 'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName from have WHERE Matched = "Failed" GROUP BY AlbumTitle ORDER BY ArtistName') for albums in have_albums: - #Have to skip over manually matched tracks + # Have to skip over manually matched tracks if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']: - original_clean = helpers.cleanName(albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle']) - # else: - # original_clean = None + original_clean = helpers.cleanName( + albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle']) + # else: + # original_clean = None if original_clean == albums['CleanName']: - have_dict = {'ArtistName': albums['ArtistName'], 'AlbumTitle': albums['AlbumTitle']} + have_dict = {'ArtistName': albums['ArtistName'], + 'AlbumTitle': albums['AlbumTitle']} have_album_dictionary.append(have_dict) - headphones_albums = myDB.select('SELECT ArtistName, AlbumTitle from albums ORDER BY ArtistName') + headphones_albums = myDB.select( + 'SELECT ArtistName, AlbumTitle from albums ORDER BY ArtistName') for albums in headphones_albums: if albums['ArtistName'] and albums['AlbumTitle']: - headphones_dict = {'ArtistName': albums['ArtistName'], 'AlbumTitle': albums['AlbumTitle']} + headphones_dict = {'ArtistName': albums['ArtistName'], + 'AlbumTitle': albums['AlbumTitle']} headphones_album_dictionary.append(headphones_dict) - #unmatchedalbums = [f for f in have_album_dictionary if f not in [x for x in headphones_album_dictionary]] + # unmatchedalbums = [f for f in have_album_dictionary if f not in [x for x in headphones_album_dictionary]] - check = set([(cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) for d in headphones_album_dictionary]) - unmatchedalbums = [d for d in have_album_dictionary if (cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) not in check] + check = set( + [(cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) for d in + headphones_album_dictionary]) + unmatchedalbums = [d for d in have_album_dictionary if ( + cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) not in check] - return serve_template(templatename="manageunmatched.html", title="Manage Unmatched Items", unmatchedalbums=unmatchedalbums) + return serve_template(templatename="manageunmatched.html", title="Manage Unmatched Items", + unmatchedalbums=unmatchedalbums) @cherrypy.expose - def markUnmatched(self, action=None, existing_artist=None, existing_album=None, new_artist=None, new_album=None): + def markUnmatched(self, action=None, existing_artist=None, existing_album=None, new_artist=None, + new_album=None): myDB = db.DBConnection() if action == "ignoreArtist": artist = existing_artist - myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND Matched = "Failed"', [artist]) + myDB.action( + 'UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND Matched = "Failed"', + [artist]) elif action == "ignoreAlbum": artist = existing_artist album = existing_album - myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND AlbumTitle=? AND Matched = "Failed"', (artist, album)) + myDB.action( + 'UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND AlbumTitle=? AND Matched = "Failed"', + (artist, album)) elif action == "matchArtist": existing_artist_clean = helpers.cleanName(existing_artist).lower() new_artist_clean = helpers.cleanName(new_artist).lower() if new_artist_clean != existing_artist_clean: - have_tracks = myDB.action('SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?', [existing_artist]) + have_tracks = myDB.action( + 'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?', + [existing_artist]) update_count = 0 for entry in have_tracks: old_clean_filename = entry['CleanName'] if old_clean_filename.startswith(existing_artist_clean): - new_clean_filename = old_clean_filename.replace(existing_artist_clean, new_artist_clean, 1) - myDB.action('UPDATE have SET CleanName=? WHERE ArtistName=? AND CleanName=?', [new_clean_filename, existing_artist, old_clean_filename]) + new_clean_filename = old_clean_filename.replace(existing_artist_clean, + new_artist_clean, 1) + myDB.action( + 'UPDATE have SET CleanName=? WHERE ArtistName=? AND CleanName=?', + [new_clean_filename, existing_artist, old_clean_filename]) controlValueDict = {"CleanName": new_clean_filename} newValueDict = {"Location": entry['Location'], "BitRate": entry['BitRate'], "Format": entry['Format'] } - #Attempt to match tracks with new CleanName - match_alltracks = myDB.action('SELECT CleanName from alltracks WHERE CleanName=?', [new_clean_filename]).fetchone() + # Attempt to match tracks with new CleanName + match_alltracks = myDB.action( + 'SELECT CleanName from alltracks WHERE CleanName=?', + [new_clean_filename]).fetchone() if match_alltracks: myDB.upsert("alltracks", newValueDict, controlValueDict) - match_tracks = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName=?', [new_clean_filename]).fetchone() + match_tracks = myDB.action( + 'SELECT CleanName, AlbumID from tracks WHERE CleanName=?', + [new_clean_filename]).fetchone() if match_tracks: myDB.upsert("tracks", newValueDict, controlValueDict) - myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?', [new_clean_filename]) + myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?', + [new_clean_filename]) update_count += 1 - #This was throwing errors and I don't know why, but it seems to be working fine. - #else: - #logger.info("There was an error modifying Artist %s. This should not have happened" % existing_artist) - logger.info("Manual matching yielded %s new matches for Artist: %s" % (update_count, new_artist)) + # This was throwing errors and I don't know why, but it seems to be working fine. + # else: + # logger.info("There was an error modifying Artist %s. This should not have happened" % existing_artist) + logger.info("Manual matching yielded %s new matches for Artist: %s" % ( + update_count, new_artist)) if update_count > 0: librarysync.update_album_status() else: - logger.info("Artist %s already named appropriately; nothing to modify" % existing_artist) + logger.info( + "Artist %s already named appropriately; nothing to modify" % existing_artist) elif action == "matchAlbum": existing_artist_clean = helpers.cleanName(existing_artist).lower() @@ -620,83 +675,115 @@ class WebInterface(object): existing_clean_string = existing_artist_clean + " " + existing_album_clean new_clean_string = new_artist_clean + " " + new_album_clean if existing_clean_string != new_clean_string: - have_tracks = myDB.action('SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=? AND AlbumTitle=?', (existing_artist, existing_album)) + have_tracks = myDB.action( + 'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=? AND AlbumTitle=?', + (existing_artist, existing_album)) update_count = 0 for entry in have_tracks: old_clean_filename = entry['CleanName'] if old_clean_filename.startswith(existing_clean_string): - new_clean_filename = old_clean_filename.replace(existing_clean_string, new_clean_string, 1) - myDB.action('UPDATE have SET CleanName=? WHERE ArtistName=? AND AlbumTitle=? AND CleanName=?', [new_clean_filename, existing_artist, existing_album, old_clean_filename]) + new_clean_filename = old_clean_filename.replace(existing_clean_string, + new_clean_string, 1) + myDB.action( + 'UPDATE have SET CleanName=? WHERE ArtistName=? AND AlbumTitle=? AND CleanName=?', + [new_clean_filename, existing_artist, existing_album, + old_clean_filename]) controlValueDict = {"CleanName": new_clean_filename} newValueDict = {"Location": entry['Location'], "BitRate": entry['BitRate'], "Format": entry['Format'] } - #Attempt to match tracks with new CleanName - match_alltracks = myDB.action('SELECT CleanName from alltracks WHERE CleanName=?', [new_clean_filename]).fetchone() + # Attempt to match tracks with new CleanName + match_alltracks = myDB.action( + 'SELECT CleanName from alltracks WHERE CleanName=?', + [new_clean_filename]).fetchone() if match_alltracks: myDB.upsert("alltracks", newValueDict, controlValueDict) - match_tracks = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName=?', [new_clean_filename]).fetchone() + match_tracks = myDB.action( + 'SELECT CleanName, AlbumID from tracks WHERE CleanName=?', + [new_clean_filename]).fetchone() if match_tracks: myDB.upsert("tracks", newValueDict, controlValueDict) - myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?', [new_clean_filename]) + myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?', + [new_clean_filename]) album_id = match_tracks['AlbumID'] update_count += 1 - #This was throwing errors and I don't know why, but it seems to be working fine. - #else: - #logger.info("There was an error modifying Artist %s / Album %s with clean name %s" % (existing_artist, existing_album, existing_clean_string)) - logger.info("Manual matching yielded %s new matches for Artist: %s / Album: %s" % (update_count, new_artist, new_album)) + # This was throwing errors and I don't know why, but it seems to be working fine. + # else: + # logger.info("There was an error modifying Artist %s / Album %s with clean name %s" % (existing_artist, existing_album, existing_clean_string)) + logger.info("Manual matching yielded %s new matches for Artist: %s / Album: %s" % ( + update_count, new_artist, new_album)) if update_count > 0: librarysync.update_album_status(album_id) else: - logger.info("Artist %s / Album %s already named appropriately; nothing to modify" % (existing_artist, existing_album)) + logger.info( + "Artist %s / Album %s already named appropriately; nothing to modify" % ( + existing_artist, existing_album)) @cherrypy.expose def manageManual(self): myDB = db.DBConnection() manual_albums = [] - manualalbums = myDB.select('SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have') + manualalbums = myDB.select( + 'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have') for albums in manualalbums: if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']: - original_clean = helpers.cleanName(albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle']) - if albums['Matched'] == "Ignored" or albums['Matched'] == "Manual" or albums['CleanName'] != original_clean: + original_clean = helpers.cleanName( + albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle']) + if albums['Matched'] == "Ignored" or albums['Matched'] == "Manual" or albums[ + 'CleanName'] != original_clean: if albums['Matched'] == "Ignored": album_status = "Ignored" elif albums['Matched'] == "Manual" or albums['CleanName'] != original_clean: album_status = "Matched" - manual_dict = {'ArtistName': albums['ArtistName'], 'AlbumTitle': albums['AlbumTitle'], 'AlbumStatus': album_status} + manual_dict = {'ArtistName': albums['ArtistName'], + 'AlbumTitle': albums['AlbumTitle'], 'AlbumStatus': album_status} if manual_dict not in manual_albums: manual_albums.append(manual_dict) manual_albums_sorted = sorted(manual_albums, key=itemgetter('ArtistName', 'AlbumTitle')) - return serve_template(templatename="managemanual.html", title="Manage Manual Items", manualalbums=manual_albums_sorted) + return serve_template(templatename="managemanual.html", title="Manage Manual Items", + manualalbums=manual_albums_sorted) @cherrypy.expose def markManual(self, action=None, existing_artist=None, existing_album=None): myDB = db.DBConnection() if action == "unignoreArtist": artist = existing_artist - myDB.action('UPDATE have SET Matched="Failed" WHERE ArtistName=? AND Matched="Ignored"', [artist]) + myDB.action('UPDATE have SET Matched="Failed" WHERE ArtistName=? AND Matched="Ignored"', + [artist]) logger.info("Artist: %s successfully restored to unmatched list" % artist) elif action == "unignoreAlbum": artist = existing_artist album = existing_album - myDB.action('UPDATE have SET Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND Matched="Ignored"', (artist, album)) + myDB.action( + 'UPDATE have SET Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND Matched="Ignored"', + (artist, album)) logger.info("Album: %s successfully restored to unmatched list" % album) elif action == "unmatchArtist": artist = existing_artist - update_clean = myDB.select('SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have WHERE ArtistName=?', [artist]) + update_clean = myDB.select( + 'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have WHERE ArtistName=?', + [artist]) update_count = 0 for tracks in update_clean: - original_clean = helpers.cleanName(tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks['TrackTitle']).lower() + original_clean = helpers.cleanName( + tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[ + 'TrackTitle']).lower() album = tracks['AlbumTitle'] track_title = tracks['TrackTitle'] if tracks['CleanName'] != original_clean: - myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', [None, None, None, tracks['CleanName']]) - myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', [None, None, None, tracks['CleanName']]) - myDB.action('UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?', (original_clean, artist, album, track_title)) + myDB.action( + 'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', + [None, None, None, tracks['CleanName']]) + myDB.action( + 'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', + [None, None, None, tracks['CleanName']]) + myDB.action( + 'UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?', + (original_clean, artist, album, track_title)) update_count += 1 if update_count > 0: librarysync.update_album_status() @@ -705,18 +792,29 @@ class WebInterface(object): elif action == "unmatchAlbum": artist = existing_artist album = existing_album - update_clean = myDB.select('SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have WHERE ArtistName=? AND AlbumTitle=?', (artist, album)) + update_clean = myDB.select( + 'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have WHERE ArtistName=? AND AlbumTitle=?', + (artist, album)) update_count = 0 for tracks in update_clean: - original_clean = helpers.cleanName(tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks['TrackTitle']).lower() + original_clean = helpers.cleanName( + tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[ + 'TrackTitle']).lower() track_title = tracks['TrackTitle'] if tracks['CleanName'] != original_clean: - album_id_check = myDB.action('SELECT AlbumID from tracks WHERE CleanName=?', [tracks['CleanName']]).fetchone() + album_id_check = myDB.action('SELECT AlbumID from tracks WHERE CleanName=?', + [tracks['CleanName']]).fetchone() if album_id_check: album_id = album_id_check[0] - myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', [None, None, None, tracks['CleanName']]) - myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', [None, None, None, tracks['CleanName']]) - myDB.action('UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?', (original_clean, artist, album, track_title)) + myDB.action( + 'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', + [None, None, None, tracks['CleanName']]) + myDB.action( + 'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', + [None, None, None, tracks['CleanName']]) + myDB.action( + 'UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?', + (original_clean, artist, album, track_title)) update_count += 1 if update_count > 0: librarysync.update_album_status(album_id) @@ -802,7 +900,9 @@ class WebInterface(object): @cherrypy.expose def forcePostProcess(self, dir=None, album_dir=None, keep_original_folder=False): from headphones import postprocessor - threading.Thread(target=postprocessor.forcePostProcess, kwargs={'dir': dir, 'album_dir': album_dir, 'keep_original_folder':keep_original_folder == 'True'}).start() + threading.Thread(target=postprocessor.forcePostProcess, + kwargs={'dir': dir, 'album_dir': album_dir, + 'keep_original_folder': keep_original_folder == 'True'}).start() raise cherrypy.HTTPRedirect("home") @cherrypy.expose @@ -814,7 +914,8 @@ class WebInterface(object): @cherrypy.expose def history(self): myDB = db.DBConnection() - history = myDB.select('''SELECT AlbumID, Title, Size, URL, DateAdded, Status, Kind, ifnull(FolderName, '?') FolderName FROM snatched WHERE Status NOT LIKE "Seed%" ORDER BY DateAdded DESC''') + history = myDB.select( + '''SELECT AlbumID, Title, Size, URL, DateAdded, Status, Kind, ifnull(FolderName, '?') FolderName FROM snatched WHERE Status NOT LIKE "Seed%" ORDER BY DateAdded DESC''') return serve_template(templatename="history.html", title="History", history=history) @cherrypy.expose @@ -831,13 +932,14 @@ class WebInterface(object): def toggleVerbose(self): headphones.VERBOSE = not headphones.VERBOSE logger.initLogger(console=not headphones.QUIET, - log_dir=headphones.CONFIG.LOG_DIR, verbose=headphones.VERBOSE) + log_dir=headphones.CONFIG.LOG_DIR, verbose=headphones.VERBOSE) logger.info("Verbose toggled, set to %s", headphones.VERBOSE) logger.debug("If you read this message, debug logging is available") raise cherrypy.HTTPRedirect("logs") @cherrypy.expose - def getLog(self, iDisplayStart=0, iDisplayLength=100, iSortCol_0=0, sSortDir_0="desc", sSearch="", **kwargs): + def getLog(self, iDisplayStart=0, iDisplayLength=100, iSortCol_0=0, sSortDir_0="desc", + sSearch="", **kwargs): iDisplayStart = int(iDisplayStart) iDisplayLength = int(iDisplayLength) @@ -845,7 +947,8 @@ class WebInterface(object): if sSearch == "": filtered = headphones.LOG_LIST[::] else: - filtered = [row for row in headphones.LOG_LIST for column in row if sSearch.lower() in column.lower()] + filtered = [row for row in headphones.LOG_LIST for column in row if + sSearch.lower() in column.lower()] sortcolumn = 0 if iSortCol_0 == '1': @@ -864,7 +967,8 @@ class WebInterface(object): }) @cherrypy.expose - def getArtists_json(self, iDisplayStart=0, iDisplayLength=100, sSearch="", iSortCol_0='0', sSortDir_0='asc', **kwargs): + def getArtists_json(self, iDisplayStart=0, iDisplayLength=100, sSearch="", iSortCol_0='0', + sSortDir_0='asc', **kwargs): iDisplayStart = int(iDisplayStart) iDisplayLength = int(iDisplayLength) filtered = [] @@ -885,15 +989,18 @@ class WebInterface(object): filtered = myDB.select(query) totalcount = len(filtered) else: - query = 'SELECT * from artists WHERE ArtistSortName LIKE "%' + sSearch + '%" OR LatestAlbum LIKE "%' + sSearch + '%"' + 'ORDER BY %s COLLATE NOCASE %s' % (sortcolumn, sSortDir_0) + query = 'SELECT * from artists WHERE ArtistSortName LIKE "%' + sSearch + '%" OR LatestAlbum LIKE "%' + sSearch + '%"' + 'ORDER BY %s COLLATE NOCASE %s' % ( + sortcolumn, sSortDir_0) filtered = myDB.select(query) totalcount = myDB.select('SELECT COUNT(*) from artists')[0][0] if sortbyhavepercent: - filtered.sort(key=lambda x: (float(x['HaveTracks']) / x['TotalTracks'] if x['TotalTracks'] > 0 else 0.0, x['HaveTracks'] if x['HaveTracks'] else 0.0), reverse=sSortDir_0 == "asc") + filtered.sort(key=lambda x: ( + float(x['HaveTracks']) / x['TotalTracks'] if x['TotalTracks'] > 0 else 0.0, + x['HaveTracks'] if x['HaveTracks'] else 0.0), reverse=sSortDir_0 == "asc") - #can't figure out how to change the datatables default sorting order when its using an ajax datasource so ill - #just reverse it here and the first click on the "Latest Album" header will sort by descending release date + # can't figure out how to change the datatables default sorting order when its using an ajax datasource so ill + # just reverse it here and the first click on the "Latest Album" header will sort by descending release date if sortcolumn == 'ReleaseDate': filtered.reverse() @@ -901,16 +1008,16 @@ class WebInterface(object): rows = [] for artist in artists: row = {"ArtistID": artist['ArtistID'], - "ArtistName": artist["ArtistName"], - "ArtistSortName": artist["ArtistSortName"], - "Status": artist["Status"], - "TotalTracks": artist["TotalTracks"], - "HaveTracks": artist["HaveTracks"], - "LatestAlbum": "", - "ReleaseDate": "", - "ReleaseInFuture": "False", - "AlbumID": "", - } + "ArtistName": artist["ArtistName"], + "ArtistSortName": artist["ArtistSortName"], + "Status": artist["Status"], + "TotalTracks": artist["TotalTracks"], + "HaveTracks": artist["HaveTracks"], + "LatestAlbum": "", + "ReleaseDate": "", + "ReleaseInFuture": "False", + "AlbumID": "", + } if not row['HaveTracks']: row['HaveTracks'] = 0 @@ -954,9 +1061,9 @@ class WebInterface(object): myDB = db.DBConnection() artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone() artist_json = json.dumps({ - 'ArtistName': artist['ArtistName'], - 'Status': artist['Status'] - }) + 'ArtistName': artist['ArtistName'], + 'Status': artist['Status'] + }) return artist_json @cherrypy.expose @@ -964,9 +1071,9 @@ class WebInterface(object): myDB = db.DBConnection() album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone() album_json = json.dumps({ - 'AlbumTitle': album['AlbumTitle'], - 'ArtistName': album['ArtistName'], - 'Status': album['Status'] + 'AlbumTitle': album['AlbumTitle'], + 'ArtistName': album['ArtistName'], + 'Status': album['Status'] }) return album_json @@ -982,7 +1089,9 @@ class WebInterface(object): myDB.action('DELETE from snatched WHERE Status=?', [type]) else: logger.info(u"Deleting '%s' from history" % title) - myDB.action('DELETE from snatched WHERE Status NOT LIKE "Seed%" AND Title=? AND DateAdded=?', [title, date_added]) + myDB.action( + 'DELETE from snatched WHERE Status NOT LIKE "Seed%" AND Title=? AND DateAdded=?', + [title, date_added]) raise cherrypy.HTTPRedirect("history") @cherrypy.expose @@ -995,7 +1104,7 @@ class WebInterface(object): def forceScan(self, keepmatched=None): myDB = db.DBConnection() ######################################### - #NEED TO MOVE THIS INTO A SEPARATE FUNCTION BEFORE RELEASE + # NEED TO MOVE THIS INTO A SEPARATE FUNCTION BEFORE RELEASE myDB.select('DELETE from Have') logger.info('Removed all entries in local library database') myDB.select('UPDATE alltracks SET Location=NULL, BitRate=NULL, Format=NULL') @@ -1003,7 +1112,8 @@ class WebInterface(object): logger.info('All tracks in library unmatched') myDB.action('UPDATE artists SET HaveTracks=NULL') logger.info('Reset track counts for all artists') - myDB.action('UPDATE albums SET Status="Skipped" WHERE Status="Skipped" OR Status="Downloaded"') + myDB.action( + 'UPDATE albums SET Status="Skipped" WHERE Status="Skipped" OR Status="Downloaded"') logger.info('Marking all unwanted albums as Skipped') try: threading.Thread(target=librarysync.libraryScan).start() @@ -1014,7 +1124,8 @@ class WebInterface(object): @cherrypy.expose def config(self): interface_dir = os.path.join(headphones.PROG_DIR, 'data/interfaces/') - interface_list = [name for name in os.listdir(interface_dir) if os.path.isdir(os.path.join(interface_dir, name))] + interface_list = [name for name in os.listdir(interface_dir) if + os.path.isdir(os.path.join(interface_dir, name))] config = { "http_host": headphones.CONFIG.HTTP_HOST, @@ -1115,7 +1226,8 @@ class WebInterface(object): "preferred_bitrate": headphones.CONFIG.PREFERRED_BITRATE, "preferred_bitrate_high": headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER, "preferred_bitrate_low": headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER, - "preferred_bitrate_allow_lossless": checked(headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS), + "preferred_bitrate_allow_lossless": checked( + headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS), "detect_bitrate": checked(headphones.CONFIG.DETECT_BITRATE), "lossless_bitrate_from": headphones.CONFIG.LOSSLESS_BITRATE_FROM, "lossless_bitrate_to": headphones.CONFIG.LOSSLESS_BITRATE_TO, @@ -1133,7 +1245,7 @@ class WebInterface(object): "embed_album_art": checked(headphones.CONFIG.EMBED_ALBUM_ART), "embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS), "replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS), - "keep_original_folder" : checked(headphones.CONFIG.KEEP_ORIGINAL_FOLDER), + "keep_original_folder": checked(headphones.CONFIG.KEEP_ORIGINAL_FOLDER), "destination_dir": headphones.CONFIG.DESTINATION_DIR, "lossless_destination_dir": headphones.CONFIG.LOSSLESS_DESTINATION_DIR, "folder_format": headphones.CONFIG.FOLDER_FORMAT, @@ -1286,18 +1398,31 @@ class WebInterface(object): # Handle the variable config options. Note - keys with False values aren't getting passed checked_configs = [ - "launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer", "use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled", - "use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay", "use_mininova", "use_waffles", "use_rutracker", - "use_whatcd", "use_strike", "preferred_bitrate_allow_lossless", "detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files", - "rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "embed_album_art", "embed_lyrics", - "replace_existing_folders", "keep_original_folder", "file_underscores", "include_extras", "official_releases_only", - "wait_until_release_date", "autowant_upcoming", "autowant_all", "autowant_manually_added", "do_not_process_unmatched", "keep_torrent_files", "music_encoder", - "encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled", "growl_onsnatch", "prowl_enabled", - "prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled", "plex_enabled", "plex_update", "plex_notify", - "nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch", "synoindex_enabled", "pushover_enabled", - "pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled", "twitter_enabled", "twitter_onsnatch", - "osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", "songkick_enabled", "songkick_filter_enabled", - "mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch", "customauth", "idtag" + "launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer", + "use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled", + "use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay", + "use_mininova", "use_waffles", "use_rutracker", + "use_whatcd", "use_strike", "preferred_bitrate_allow_lossless", "detect_bitrate", + "ignore_clean_releases", "freeze_db", "cue_split", "move_files", + "rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", + "embed_album_art", "embed_lyrics", + "replace_existing_folders", "keep_original_folder", "file_underscores", + "include_extras", "official_releases_only", + "wait_until_release_date", "autowant_upcoming", "autowant_all", + "autowant_manually_added", "do_not_process_unmatched", "keep_torrent_files", + "music_encoder", + "encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled", + "growl_onsnatch", "prowl_enabled", + "prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled", + "plex_enabled", "plex_update", "plex_notify", + "nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch", + "synoindex_enabled", "pushover_enabled", + "pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled", + "twitter_enabled", "twitter_onsnatch", + "osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", + "songkick_enabled", "songkick_filter_enabled", + "mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch", + "customauth", "idtag" ] for checked_config in checked_configs: if checked_config not in kwargs: @@ -1473,9 +1598,11 @@ class WebInterface(object): image_dict = {'artwork': image_url, 'thumbnail': thumb_url} elif AlbumID and (not image_dict['artwork'] or not image_dict['thumbnail']): if not image_dict['artwork']: - image_dict['artwork'] = "http://coverartarchive.org/release/%s/front-500.jpg" % AlbumID + image_dict[ + 'artwork'] = "http://coverartarchive.org/release/%s/front-500.jpg" % AlbumID if not image_dict['thumbnail']: - image_dict['thumbnail'] = "http://coverartarchive.org/release/%s/front-250.jpg" % AlbumID + image_dict[ + 'thumbnail'] = "http://coverartarchive.org/release/%s/front-250.jpg" % AlbumID return json.dumps(image_dict) @@ -1514,7 +1641,8 @@ class WebInterface(object): if result: osx_notify = notifiers.OSX_NOTIFY() osx_notify.notify('Registered', result, 'Success :-)') - logger.info('Registered %s, to re-register a different app, delete this app first' % result) + logger.info( + 'Registered %s, to re-register a different app, delete this app first' % result) else: logger.warn(msg) return msg @@ -1536,7 +1664,8 @@ class WebInterface(object): def testPushbullet(self): logger.info("Testing Pushbullet notifications") pushbullet = notifiers.PUSHBULLET() - pushbullet.notify("it works!") + pushbullet.notify("it works!", "Test message") + class Artwork(object): @cherrypy.expose @@ -1605,4 +1734,6 @@ class Artwork(object): return fp.read() thumbs = Thumbs() + + WebInterface.artwork = Artwork() diff --git a/headphones/webstart.py b/headphones/webstart.py index cf2f4a77..a4ab7113 100644 --- a/headphones/webstart.py +++ b/headphones/webstart.py @@ -13,18 +13,17 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -import os import sys + +import os import cherrypy import headphones - from headphones import logger from headphones.webserve import WebInterface from headphones.helpers import create_https_certificates def initialize(options): - # HTTPS stuff stolen from sickbeard enable_https = options['enable_https'] https_cert = options['https_cert'] @@ -33,15 +32,16 @@ def initialize(options): if enable_https: # If either the HTTPS certificate or key do not exist, try to make # self-signed ones. - if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)): + if not (https_cert and os.path.exists(https_cert)) or not ( + https_key and os.path.exists(https_key)): if not create_https_certificates(https_cert, https_key): logger.warn("Unable to create certificate and key. Disabling " \ - "HTTPS") + "HTTPS") enable_https = False if not (os.path.exists(https_cert) and os.path.exists(https_key)): logger.warn("Disabled HTTPS because of missing certificate and " \ - "key.") + "key.") enable_https = False options_dict = { @@ -63,7 +63,7 @@ def initialize(options): protocol = "http" logger.info("Starting Headphones web server on %s://%s:%d/", protocol, - options['http_host'], options['http_port']) + options['http_host'], options['http_port']) cherrypy.config.update(options_dict) conf = { @@ -99,7 +99,8 @@ def initialize(options): } if options['http_password']: - logger.info("Web server authentication is enabled, username is '%s'", options['http_username']) + logger.info("Web server authentication is enabled, username is '%s'", + options['http_username']) conf['/'].update({ 'tools.auth_basic.on': True, @@ -118,7 +119,8 @@ def initialize(options): cherrypy.process.servers.check_port(str(options['http_host']), options['http_port']) cherrypy.server.start() except IOError: - sys.stderr.write('Failed to start on port: %i. Is something else running?\n' % (options['http_port'])) + sys.stderr.write( + 'Failed to start on port: %i. Is something else running?\n' % (options['http_port'])) sys.exit(1) cherrypy.server.wait()