diff --git a/headphones/cache.py b/headphones/cache.py index cee70ff0..49e9023e 100644 --- a/headphones/cache.py +++ b/headphones/cache.py @@ -22,6 +22,7 @@ from headphones import db, helpers, logger, lastfm, request LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4" + class Cache(object): """ This class deals with getting, storing and serving up artwork (album @@ -92,7 +93,6 @@ class Cache(object): return days_old - def _is_current(self, filename=None, date=None): if filename: @@ -431,6 +431,7 @@ class Cache(object): self.thumb_errors = True self.thumb_url = image_url + def getArtwork(ArtistID=None, AlbumID=None): c = Cache() @@ -445,6 +446,7 @@ def getArtwork(ArtistID=None, AlbumID=None): artwork_file = os.path.basename(artwork_path) return "cache/artwork/" + artwork_file + def getThumb(ArtistID=None, AlbumID=None): c = Cache() @@ -459,6 +461,7 @@ def getThumb(ArtistID=None, AlbumID=None): thumbnail_file = os.path.basename(artwork_path) return "cache/artwork/" + thumbnail_file + def getInfo(ArtistID=None, AlbumID=None): c = Cache() @@ -467,6 +470,7 @@ def getInfo(ArtistID=None, AlbumID=None): return info_dict + def getImageLinks(ArtistID=None, AlbumID=None): c = Cache() diff --git a/headphones/classes.py b/headphones/classes.py index c6b14055..acb29f86 100644 --- a/headphones/classes.py +++ b/headphones/classes.py @@ -24,9 +24,11 @@ import datetime from common import USER_AGENT + class HeadphonesURLopener(urllib.FancyURLopener): version = USER_AGENT + class AuthURLOpener(HeadphonesURLopener): """ URLOpener class that supports http auth without needing interactive password entry. @@ -35,6 +37,7 @@ class AuthURLOpener(HeadphonesURLopener): user: username to use for HTTP auth pw: password to use for HTTP auth """ + def __init__(self, user, pw): self.username = user self.password = pw @@ -65,6 +68,7 @@ class AuthURLOpener(HeadphonesURLopener): self.numTries = 0 return HeadphonesURLopener.open(self, url) + class SearchResult: """ Represents a search result from an indexer. @@ -96,24 +100,28 @@ class SearchResult: myString += " " + extra + "\n" return myString + class NZBSearchResult(SearchResult): """ Regular NZB result with an URL to the NZB """ resultType = "nzb" + class NZBDataSearchResult(SearchResult): """ NZB result where the actual NZB XML data is stored in the extraInfo """ resultType = "nzbdata" + class TorrentSearchResult(SearchResult): """ Torrent result with an URL to the torrent """ resultType = "torrent" + class Proper: def __init__(self, name, url, date): self.name = name diff --git a/headphones/common.py b/headphones/common.py index cadad088..17dd7f54 100644 --- a/headphones/common.py +++ b/headphones/common.py @@ -44,6 +44,7 @@ ARCHIVED = 6 # releases that you don't have locally (counts toward download comp IGNORED = 7 # releases that you don't want included in your download stats SNATCHED_PROPER = 9 # qualified with quality + class Quality: NONE = 0 diff --git a/headphones/config.py b/headphones/config.py index 2023e7a1..0fad26b9 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -4,6 +4,7 @@ import os import re from configobj import ConfigObj + def bool_int(value): """ Casts a config value into a 0 or 1 @@ -230,6 +231,7 @@ _config_definitions = { 'XLDPROFILE': (str, 'General', '') } + class Config(object): """ Wraps access to particular values in a config file """ diff --git a/headphones/cuesplit.py b/headphones/cuesplit.py index 8bf5d016..3becab7d 100755 --- a/headphones/cuesplit.py +++ b/headphones/cuesplit.py @@ -69,6 +69,7 @@ WAVE_FILE_TYPE_BY_EXTENSION = { #SHNTOOL_COMPATIBLE = ('Waveform Audio', 'WavPack', 'Free Lossless Audio Codec') SHNTOOL_COMPATIBLE = ('Free Lossless Audio Codec') + def check_splitter(command): '''Check xld or shntools installed''' try: @@ -82,6 +83,7 @@ def check_splitter(command): return False return True + def split_baby(split_file, split_cmd): '''Let's split baby''' logger.info('Splitting %s...', split_file.decode(headphones.SYS_ENCODING, 'replace')) @@ -115,6 +117,7 @@ def split_baby(split_file, split_cmd): logger.info('Split success %s', split_file.decode(headphones.SYS_ENCODING, 'replace')) return True + def check_list(list, ignore=0): '''Checks a list for None elements. If list have None (after ignore index) then it should pass only if all elements are None threreafter. Returns a tuple without the None entries.''' @@ -146,12 +149,14 @@ def check_list(list, ignore=0): return tuple(list1+list2) + def trim_cue_entry(string): '''Removes leading and trailing "s.''' if string[0] == '"' and string[-1] == '"': string = string[1:-1] return string + def int_to_str(value, length=2): '''Converts integer to string eg 3 to "03"''' try: @@ -164,6 +169,7 @@ def int_to_str(value, length=2): content = '0' + content return content + def split_file_list(ext=None): file_list = [None for m in range(100)] if ext and ext[0] != '.': @@ -260,6 +266,7 @@ class Directory: else: self.content.append(File(self.path + os.sep + i)) + class File: def __init__(self, path): self.path = path @@ -285,6 +292,7 @@ class File: return content + class CueFile(File): def __init__(self, path): @@ -434,6 +442,7 @@ class CueFile(File): content += '\n' return content + class MetaFile(File): def __init__(self, path): File.__init__(self, path) @@ -498,6 +507,7 @@ class MetaFile(File): '''Returns tracks count''' return len(self.content['tracks']) - self.content['tracks'].count(None) + class WaveFile(File): def __init__(self, path, track_nr=None): File.__init__(self, path) @@ -537,6 +547,7 @@ class WaveFile(File): if self.type == 'Free Lossless Audio Codec': return FLAC(self.name) + def split(albumpath): os.chdir(albumpath) diff --git a/headphones/db.py b/headphones/db.py index 9c07db9b..c943b66f 100644 --- a/headphones/db.py +++ b/headphones/db.py @@ -28,10 +28,12 @@ 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 if not headphones.CONFIG.CACHE_SIZEMB: @@ -39,6 +41,7 @@ def getCacheSize(): return 0 return int(headphones.CONFIG.CACHE_SIZEMB) + class DBConnection: def __init__(self, filename="headphones.db"): diff --git a/headphones/exceptions.py b/headphones/exceptions.py index a1e62f1a..5d0ddf52 100644 --- a/headphones/exceptions.py +++ b/headphones/exceptions.py @@ -13,11 +13,13 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . + class HeadphonesException(Exception): """ Generic Headphones Exception - should never be thrown, only subclassed """ + class NewzbinAPIThrottled(HeadphonesException): """ Newzbin has throttled us, deal with it diff --git a/headphones/getXldProfile.py b/headphones/getXldProfile.py index ec0e0e5e..b17e2244 100755 --- a/headphones/getXldProfile.py +++ b/headphones/getXldProfile.py @@ -5,6 +5,7 @@ import xml.parsers.expat as expat import commands from headphones import logger + def getXldProfile(xldProfile): xldProfileNotFound = xldProfile expandedPath = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist') diff --git a/headphones/helpers.py b/headphones/helpers.py index 92288fe1..ebd5a24e 100644 --- a/headphones/helpers.py +++ b/headphones/helpers.py @@ -31,6 +31,7 @@ RE_FEATURING = re.compile(r"[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&|vs\.") RE_CD_ALBUM = re.compile(r"\(?((CD|disc)\s*[0-9]+)\)?", re.I) 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] @@ -44,12 +45,14 @@ def multikeysort(items, columns): return sorted(items, cmp=comparer) + def checked(variable): if variable: return 'Checked' else: return '' + def radio(variable, pos): if variable == pos: @@ -57,6 +60,7 @@ def radio(variable, pos): else: return '' + def latinToAscii(unicrap): """ From couch potato @@ -98,6 +102,7 @@ def latinToAscii(unicrap): r += str(i) return r + def convert_milliseconds(ms): seconds = ms/1000 @@ -109,6 +114,7 @@ def convert_milliseconds(ms): return minutes + def convert_seconds(s): gmtime = time.gmtime(s) @@ -119,15 +125,18 @@ def convert_seconds(s): return minutes + def today(): today = datetime.date.today() yyyymmdd = datetime.date.isoformat(today) return yyyymmdd + def now(): now = datetime.datetime.now() return now.strftime("%Y-%m-%d %H:%M:%S") + def get_age(date): try: @@ -142,17 +151,20 @@ def get_age(date): return days_old + def bytes_to_mb(bytes): mb = int(bytes)/1048576 size = '%.1f MB' % mb return size + def mb_to_bytes(mb_str): result = re.search('^(\d+(?:\.\d+)?)\s?(?:mb)?', mb_str, flags=re.I) if result: return int(float(result.group(1))*1048576) + def piratesize(size): split = size.split(" ") factor = float(split[0]) @@ -170,6 +182,7 @@ def piratesize(size): return size + def replace_all(text, dic, normalize=False): if not text: @@ -187,6 +200,7 @@ def replace_all(text, dic, normalize=False): text = text.replace(i, j) return text + def replace_illegal_chars(string, type="file"): if type == "file": string = re.sub('[\?"*:|<>/]', '_', string) @@ -195,6 +209,7 @@ def replace_illegal_chars(string, type="file"): return string + def cleanName(string): pass1 = latinToAscii(string).lower() @@ -202,6 +217,7 @@ def cleanName(string): return out_string + def cleanTitle(title): title = re.sub('[\.\-\/\_]', ' ', title).lower() @@ -213,6 +229,7 @@ def cleanTitle(title): return title + def split_path(f): """ Split a path into components, starting with the drive letter (if any). Given @@ -244,6 +261,7 @@ def split_path(f): # Done return components + def expand_subfolders(f): """ Try to expand a given folder and search for subfolders containing media @@ -310,6 +328,7 @@ def expand_subfolders(f): logger.debug("Expanded subfolders in folder: %s", media_folders) return media_folders + def extract_data(s): s = s.replace('_', ' ') @@ -337,6 +356,7 @@ def extract_data(s): else: return (None, None, None) + def extract_metadata(f): """ Scan all files in the given directory and decide on an artist, album and @@ -435,6 +455,7 @@ def extract_metadata(f): return (None, None, None) + def get_downloaded_track_list(albumpath): """ Return a list of audio files for the given directory. @@ -449,6 +470,7 @@ def get_downloaded_track_list(albumpath): return downloaded_track_list + def preserve_torrent_direcory(albumpath): """ Copy torrent directory to headphones-modified to keep files for seeding. @@ -465,6 +487,7 @@ def preserve_torrent_direcory(albumpath): ". Not continuing. Error: " + str(e)) return None + def cue_split(albumpath): """ Attempts to check and split audio files by a cue for the given directory. @@ -504,6 +527,7 @@ def cue_split(albumpath): return False + def extract_logline(s): # Default log format pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s*\:\:\s(?P.*?)\s\:\s(?P.*)', re.VERBOSE) @@ -517,6 +541,7 @@ def extract_logline(s): else: return None + def extract_song_data(s): #headphones default format @@ -548,6 +573,7 @@ def extract_song_data(s): logger.info("Couldn't parse %s into a valid Newbin format", s) return (name, album, year) + def smartMove(src, dest, delete=True): from headphones import logger @@ -588,11 +614,15 @@ def smartMove(src, dest, delete=True): # TODO: Grab config values from sab to know when these options are checked. For now we'll just iterate through all combinations + def sab_replace_dots(name): return name.replace('.', ' ') + + def sab_replace_spaces(name): return name.replace(' ', '_') + def sab_sanitize_foldername(name): """ Return foldername with dodgy chars converted to safe ones Remove any leading and trailing dot and space characters @@ -634,12 +664,14 @@ def sab_sanitize_foldername(name): 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): """ Stolen from SickBeard (http://github.com/midgetspy/Sick-Beard): diff --git a/headphones/importer.py b/headphones/importer.py index 5f274a08..8f16e7e5 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -32,6 +32,7 @@ blacklisted_special_artists = ['f731ccc4-e22a-43af-a747-64213329e088', '125ec42a-7229-4250-afc5-e057484327fe', '89ad4ac3-39f7-470e-963a-56509c546377'] + def is_exists(artistid): myDB = db.DBConnection() @@ -52,7 +53,6 @@ def artistlist_to_mbids(artistlist, forced=False): if not artist and not (artist == ' '): continue - # If adding artists through Manage New Artists, they're coming through as non-unicode (utf-8?) # and screwing everything up if not isinstance(artist, unicode): @@ -105,12 +105,14 @@ def artistlist_to_mbids(artistlist, forced=False): except Exception as e: logger.warn('Failed to update arist information from Last.fm: %s' % e) + def addArtistIDListToDB(artistidlist): # Used to add a list of artist IDs to the database in a single thread logger.debug("Importer: Adding artist ids %s" % artistidlist) for artistid in artistidlist: addArtisttoDB(artistid) + def addArtisttoDB(artistid, extrasonly=False, forcefull=False): # Putting this here to get around the circular import. We're using this to update thumbnails for artist/albums @@ -172,7 +174,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): else: sortname = artist['artist_name'] - logger.info(u"Now adding/updating: " + artist['artist_name']) controlValueDict = {"ArtistID": artistid} newValueDict = {"ArtistName": artist['artist_name'], @@ -240,7 +241,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): check_release_date = None new_release_group = True - if new_release_group: logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title'])) new_releases = mb.get_new_releases(rgid, includeExtras) @@ -504,6 +504,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): for album_search in album_searches: searcher.searchforalbum(albumid=album_search) + def finalize_update(artistid, artistname, errors=False): # Moving this little bit to it's own function so we can update have tracks & latest album when deleting extras @@ -533,6 +534,7 @@ def finalize_update(artistid, artistname, errors=False): myDB.upsert("artists", newValueDict, controlValueDict) + def addReleaseById(rid, rgid=None): myDB = db.DBConnection() @@ -689,6 +691,7 @@ def addReleaseById(rid, rgid=None): else: logger.info('Release ' + str(rid) + " already exists in the database!") + def updateFormat(): myDB = db.DBConnection() tracks = myDB.select('SELECT * from tracks WHERE Location IS NOT NULL and Format IS NULL') @@ -718,6 +721,7 @@ def updateFormat(): myDB.upsert("have", newValueDict, controlValueDict) logger.info('Finished finding media format for %s files' % len(havetracks)) + def getHybridRelease(fullreleaselist): """ Returns a dictionary of best group of tracks from the list of releases and diff --git a/headphones/lastfm.py b/headphones/lastfm.py index 9a3a24ab..c97c019e 100644 --- a/headphones/lastfm.py +++ b/headphones/lastfm.py @@ -30,6 +30,7 @@ API_KEY = "395e6ec6bb557382fc41fde867bce66f" # Required for API request limit lock = threading.Lock() + def request_lastfm(method, **kwargs): """ Call a Last.FM API method. Automatically sets the method and API key. Method @@ -62,6 +63,7 @@ def request_lastfm(method, **kwargs): return data + def getSimilar(): myDB = db.DBConnection() results = myDB.select("SELECT ArtistID from artists ORDER BY HaveTracks DESC") @@ -107,6 +109,7 @@ def getSimilar(): logger.debug("Inserted %d artists into Last.FM tag cloud", len(top_list)) + def getArtists(): myDB = db.DBConnection() results = myDB.select("SELECT ArtistID from artists") @@ -136,6 +139,7 @@ def getArtists(): logger.info("Imported %d new artists from Last.FM", len(artistlist)) + def getTagTopArtists(tag, limit=50): myDB = db.DBConnection() results = myDB.select("SELECT ArtistID from artists") diff --git a/headphones/librarysync.py b/headphones/librarysync.py index e4a94332..0a0c8e28 100644 --- a/headphones/librarysync.py +++ b/headphones/librarysync.py @@ -22,9 +22,10 @@ from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError from headphones import db, logger, helpers, importer, lastfm # You can scan a single directory and append it to the current library by specifying append=True, ArtistID & ArtistName -def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=False): +def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=False): + if cron and not headphones.CONFIG.LIBRARYSCAN: return @@ -180,7 +181,6 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal file_count+=1 - # 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')+"%"]) @@ -293,7 +293,6 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) - if not append: logger.info('Updating scanned artist track counts') @@ -343,6 +342,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal logger.info('Library scan complete') #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') diff --git a/headphones/logger.py b/headphones/logger.py index ad1e46af..781d5402 100644 --- a/headphones/logger.py +++ b/headphones/logger.py @@ -39,6 +39,7 @@ logger = logging.getLogger("headphones") # Global queue for multiprocessing logging queue = None + class LogListHandler(logging.Handler): """ Log handler for Web UI. @@ -50,6 +51,7 @@ class LogListHandler(logging.Handler): headphones.LOG_LIST.insert(0, (helpers.now(), message, record.levelname, record.threadName)) + @contextlib.contextmanager def listener(): """ @@ -85,6 +87,7 @@ def listener(): finally: queue_listener.stop() + def initMultiprocessing(): """ Remove all handlers and add QueueHandler on top. This should only be called @@ -108,6 +111,7 @@ def initMultiprocessing(): # Change current thread name for log record threading.current_thread().name = multiprocessing.current_process().name + def initLogger(console=False, verbose=False): """ Setup logging for Headphones. It uses the logger instance with the name @@ -163,6 +167,7 @@ def initLogger(console=False, verbose=False): # Install exception hooks initHooks() + def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True): """ This method installs exception catching mechanisms. Any exception caught diff --git a/headphones/lyrics.py b/headphones/lyrics.py index ea8458ee..ea6fcd8f 100644 --- a/headphones/lyrics.py +++ b/headphones/lyrics.py @@ -18,6 +18,7 @@ import htmlentitydefs from headphones import logger, request + def getLyrics(artist, song): params = { "artist": artist.encode('utf-8'), @@ -60,6 +61,7 @@ def getLyrics(artist, song): return lyrics + def convert_html_entities(s): matches = re.findall("&#\d+;", s) if len(matches) > 0: diff --git a/headphones/mb.py b/headphones/mb.py index 135567a6..8985d9d0 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -32,6 +32,8 @@ mb_lock = threading.Lock() # Quick fix to add mirror switching on the fly. Need to probably return the mbhost & mbport that's # being used, so we can send those values to the log + + def startmb(): mbuser = None @@ -73,6 +75,7 @@ def startmb(): return True + def findArtist(name, limit=1): with mb_lock: @@ -123,6 +126,7 @@ def findArtist(name, limit=1): }) return artistlist + def findRelease(name, limit=1, artist=None): with mb_lock: @@ -201,6 +205,7 @@ def findRelease(name, limit=1, artist=None): }) return releaselist + def getArtist(artistid, extrasonly=False): with mb_lock: @@ -247,7 +252,6 @@ def getArtist(artistid, extrasonly=False): # if 'end' in artist['life-span']: # artist_dict['artist_enddate'] = unicode(artist['life-span']['end']) - releasegroups = [] if not extrasonly: @@ -321,6 +325,7 @@ def getArtist(artistid, extrasonly=False): return artist_dict + def getReleaseGroup(rgid): """ Returns a list of releases in a release group @@ -342,6 +347,7 @@ def getReleaseGroup(rgid): else: return releaseGroup['release-list'] + def getRelease(releaseid, include_artist_info=True): """ Deep release search to get track info @@ -377,7 +383,6 @@ def getRelease(releaseid, include_artist_info=True): except: release['country'] = u'Unknown' - if include_artist_info: if 'release-group' in results: @@ -404,6 +409,7 @@ def getRelease(releaseid, include_artist_info=True): return release + def get_new_releases(rgid, includeExtras=False, forcefull=False): myDB = db.DBConnection() @@ -486,7 +492,6 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False): logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.') return False - release['ReleaseCountry'] = unicode(releasedata['country']) if 'country' in releasedata else u'Unknown' #assuming that the list will contain media and that the format will be consistent try: @@ -570,6 +575,7 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False): return num_new_releases + def getTracksFromRelease(release): totalTracks = 1 tracks = [] @@ -590,6 +596,8 @@ def getTracksFromRelease(release): return tracks # Used when there is a disambiguation + + def findArtistbyAlbum(name): myDB = db.DBConnection() @@ -613,7 +621,6 @@ def findArtistbyAlbum(name): logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) time.sleep(5) - if not results: return False @@ -631,10 +638,9 @@ def findArtistbyAlbum(name): #artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id'] #artist_dict['score'] = int(releaseGroup['ext:score']) - - return artist_dict + def findAlbumID(artist=None, album=None): results = None diff --git a/headphones/music_encoder.py b/headphones/music_encoder.py index 9fa521b0..2bd1113b 100644 --- a/headphones/music_encoder.py +++ b/headphones/music_encoder.py @@ -30,6 +30,7 @@ if headphones.CONFIG.ENCODER == 'xld': else: XLD = False + def encode(albumPath): # Return if xld details not found @@ -226,6 +227,7 @@ def encode(albumPath): return musicFinalFiles + def command_map(args): """ Wrapper for the '[multiprocessing.]map()' method, to unpack the arguments @@ -243,6 +245,7 @@ def command_map(args): logger.exception("Encoder raised an exception.") return False + def command(encoder, musicSource, musicDest, albumPath): """ Encode a given music file with a certain encoder. Returns True on success, @@ -357,6 +360,7 @@ def command(encoder, musicSource, musicDest, albumPath): return encoded + def getTimeEncode(start): seconds =int(time.time()-start) hours = seconds / 3600 diff --git a/headphones/notifiers.py b/headphones/notifiers.py index 75a4f670..fc81f62e 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -39,6 +39,7 @@ try: except ImportError: from cgi import parse_qsl + class GROWL(object): """ Growl notifications, for OS X. @@ -124,6 +125,7 @@ class GROWL(object): self.notify('ZOMG Lazors Pewpewpew!', 'Test Message') + class PROWL(object): """ Prowl notifications. @@ -177,6 +179,7 @@ class PROWL(object): self.notify('ZOMG Lazors Pewpewpew!', 'Test Message') + class MPC(object): """ MPC library update @@ -264,6 +267,7 @@ class XBMC(object): except Exception: logger.error('Error sending notification request to XBMC') + class LMS(object): """ Class for updating a Logitech Media Server @@ -305,6 +309,7 @@ class LMS(object): if not request: logger.warn('Error sending rescan request to LMS') + class Plex(object): def __init__(self): @@ -391,6 +396,7 @@ class Plex(object): except: logger.warn('Error sending notification request to Plex Media Server') + class NMA(object): def notify(self, artist=None, album=None, snatched=None): title = 'Headphones' @@ -427,6 +433,7 @@ class NMA(object): else: return True + class PUSHBULLET(object): def __init__(self): @@ -480,6 +487,7 @@ class PUSHBULLET(object): self.notify('Main Screen Activate', 'Test Message') + class PUSHALOT(object): def notify(self, message, event): @@ -519,6 +527,7 @@ class PUSHALOT(object): logger.info(u"Pushalot notification failed.") return False + class Synoindex(object): def __init__(self, util_loc='/usr/syno/bin/synoindex'): self.util_loc = util_loc @@ -555,6 +564,7 @@ class Synoindex(object): for path in path_list: self.notify(path) + class PUSHOVER(object): def __init__(self): @@ -613,6 +623,7 @@ class PUSHOVER(object): self.notify('Main Screen Activate', 'Test Message') + class TwitterNotifier(object): REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' @@ -689,7 +700,6 @@ class TwitterNotifier(object): headphones.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret'] return True - def _send_tweet(self, message=None): username=self.consumer_key @@ -717,6 +727,7 @@ class TwitterNotifier(object): return self._send_tweet(prefix+": "+message) + class OSX_NOTIFY(object): def __init__(self): @@ -727,6 +738,7 @@ class OSX_NOTIFY(object): def swizzle(self, cls, SEL, func): old_IMP = cls.instanceMethodForSelector_(SEL) + def wrapper(self, *args, **kwargs): return func(self, old_IMP, *args, **kwargs) new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector, @@ -772,6 +784,7 @@ class OSX_NOTIFY(object): def swizzled_bundleIdentifier(self, original, swizzled): return 'ade.headphones.osxnotify' + class BOXCAR(object): def __init__(self): @@ -798,6 +811,7 @@ class BOXCAR(object): logger.warn('Error sending Boxcar2 Notification: %s' % e) return False + class SubSonicNotifier(object): def __init__(self): diff --git a/headphones/nzbget.py b/headphones/nzbget.py index f892caf1..80ce36db 100644 --- a/headphones/nzbget.py +++ b/headphones/nzbget.py @@ -19,7 +19,6 @@ # along with Sick Beard. If not, see . - import httplib import datetime @@ -32,6 +31,7 @@ import xmlrpclib from headphones import logger + def sendNZB(nzb): addToTop = False @@ -48,7 +48,6 @@ def sendNZB(nzb): nzbgetXMLrpc = 'http://' + nzbgetXMLrpc headphones.CONFIG.NZBGET_HOST.replace('http://', '', 1) - url = nzbgetXMLrpc % {"host": headphones.CONFIG.NZBGET_HOST, "username": headphones.CONFIG.NZBGET_USERNAME, "password": headphones.CONFIG.NZBGET_PASSWORD} nzbGetRPC = xmlrpclib.ServerProxy(url) diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index 18b19b69..ef2b7f0f 100755 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -32,6 +32,7 @@ from headphones import logger, helpers, request, mb, music_encoder postprocessor_lock = threading.Lock() + def checkFolder(): with postprocessor_lock: @@ -57,6 +58,7 @@ def checkFolder(): else: logger.info("No folder name found for " + album['Title']) + def verify(albumid, albumpath, Kind=None, forced=False): myDB = db.DBConnection() @@ -276,6 +278,7 @@ def verify(albumid, albumpath, Kind=None, forced=False): else: logger.info(u"Already marked as unprocessed: " + albumpath.decode(headphones.SYS_ENCODING, 'replace')) + def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None): logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle'])) @@ -500,6 +503,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, mpc = notifiers.MPC() mpc.notify() + def embedAlbumArt(artwork, downloaded_track_list): logger.info('Embedding album art') @@ -519,6 +523,7 @@ def embedAlbumArt(artwork, downloaded_track_list): logger.error(u'Error embedding album art to: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))) continue + def addAlbumArt(artwork, albumpath, release): logger.info('Adding album art to folder') @@ -552,6 +557,7 @@ def addAlbumArt(artwork, albumpath, release): logger.error('Error saving album art: %s', e) return + def cleanupFiles(albumpath): logger.info('Cleaning up files') @@ -564,6 +570,7 @@ def cleanupFiles(albumpath): except Exception as e: logger.error(u'Could not remove file: %s. Error: %s' % (files.decode(headphones.SYS_ENCODING, 'replace'), e)) + def renameNFO(albumpath): logger.info('Renaming NFO') @@ -577,6 +584,7 @@ def renameNFO(albumpath): except Exception as e: logger.error(u'Could not rename file: %s. Error: %s' % (os.path.join(r, file).decode(headphones.SYS_ENCODING, 'replace'), e)) + def moveFiles(albumpath, release, tracks): logger.info("Moving files: %s" % albumpath) try: @@ -809,6 +817,7 @@ def moveFiles(albumpath, release, tracks): return destination_paths + def correctMetadata(albumid, release, downloaded_track_list): logger.info('Preparing to write metadata to tracks....') @@ -862,6 +871,7 @@ def correctMetadata(albumid, release, downloaded_track_list): except Exception, e: logger.warn("Error writing metadata to '%s': %s", item.path.decode(headphones.SYS_ENCODING, 'replace'), str(e)) + def embedLyrics(downloaded_track_list): logger.info('Adding lyrics') @@ -909,6 +919,7 @@ def embedLyrics(downloaded_track_list): else: logger.debug('No lyrics found for track: %s', item.title) + def renameFiles(albumpath, downloaded_track_list, release): logger.info('Renaming files') try: @@ -975,7 +986,6 @@ def renameFiles(albumpath, downloaded_track_list, release): new_file_name = helpers.replace_all(headphones.CONFIG.FILE_FORMAT.strip(), values).replace('/', '_') + ext - new_file_name = helpers.replace_illegal_chars(new_file_name).encode(headphones.SYS_ENCODING, 'replace') if headphones.CONFIG.FILE_UNDERSCORES: @@ -997,6 +1007,7 @@ def renameFiles(albumpath, downloaded_track_list, release): logger.error('Error renaming file: %s. Error: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e) continue + def updateFilePermissions(albumpaths): for folder in albumpaths: @@ -1010,6 +1021,7 @@ def updateFilePermissions(albumpaths): logger.error("Could not change permissions for file: %s", full_path) continue + def renameUnprocessedFolder(albumpath): i = 0 @@ -1026,6 +1038,7 @@ def renameUnprocessedFolder(albumpath): os.rename(albumpath, new_folder_name) return + def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None): if album_dir: diff --git a/headphones/request.py b/headphones/request.py index 5e0c6c62..3367428e 100644 --- a/headphones/request.py +++ b/headphones/request.py @@ -27,6 +27,7 @@ import collections # Dictionary with last request times, for rate limiting. last_requests = collections.defaultdict(int) + def request_response(url, method="get", auto_raise=True, whitelist_status_code=None, rate_limit=None, **kwargs): """ @@ -125,6 +126,7 @@ def request_response(url, method="get", auto_raise=True, except requests.RequestException as e: logger.error("Request raised exception: %s", e) + def request_soup(url, **kwargs): """ Wrapper for `request_response', which will return a BeatifulSoup object if @@ -137,6 +139,7 @@ def request_soup(url, **kwargs): if response is not None: return BeautifulSoup(response.content, parser) + def request_minidom(url, **kwargs): """ Wrapper for `request_response', which will return a Minidom object if no @@ -148,6 +151,7 @@ def request_minidom(url, **kwargs): if response is not None: return minidom.parseString(response.content) + def request_json(url, **kwargs): """ Wrapper for `request_response', which will decode the response as JSON @@ -175,6 +179,7 @@ def request_json(url, **kwargs): if headphones.VERBOSE: server_message(response) + def request_content(url, **kwargs): """ Wrapper for `request_response', which will return the raw content. @@ -185,6 +190,7 @@ def request_content(url, **kwargs): if response is not None: return response.content + def request_feed(url, **kwargs): """ Wrapper for `request_response', which will return a feed object. @@ -195,6 +201,7 @@ def request_feed(url, **kwargs): if response is not None: return feedparser.parse(response.content) + def server_message(response): """ Extract server message from response and log in to logger with DEBUG level. diff --git a/headphones/sab.py b/headphones/sab.py index 80a09f2c..1a67c028 100644 --- a/headphones/sab.py +++ b/headphones/sab.py @@ -30,6 +30,7 @@ from headphones.common import USER_AGENT from headphones import logger from headphones import notifiers, helpers + def sendNZB(nzb): params = {} @@ -127,6 +128,7 @@ def sendNZB(nzb): logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText) return False + def checkConfig(): params = { 'mode': 'get_config', diff --git a/headphones/searcher.py b/headphones/searcher.py index b64284ee..83811d01 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -54,6 +54,7 @@ gazelle = None # RUtracker search object rutracker = rutrackersearch.Rutracker() + def fix_url(s, charset="utf-8"): """ Fix the URL so it is proper formatted and encoded. @@ -68,6 +69,7 @@ def fix_url(s, charset="utf-8"): return urlparse.urlunsplit((scheme, netloc, path, qs, anchor)) + def torrent_to_file(target_file, data): """ Write torrent data to file, and change permissions accordingly. Will return @@ -94,6 +96,7 @@ def torrent_to_file(target_file, data): # Done return True + def read_torrent_name(torrent_file, default_name=None): """ Read the torrent file and return the torrent name. If the torrent name @@ -123,6 +126,7 @@ def read_torrent_name(torrent_file, default_name=None): # Return default return default_name + def calculate_torrent_hash(link, data=None): """ Calculate the torrent hash from a magnet link or data. @@ -141,6 +145,7 @@ def calculate_torrent_hash(link, data=None): return torrent_hash + def get_seed_ratio(provider): """ Return the seed ratio for the specified provider, if applicable. Defaults to @@ -170,6 +175,7 @@ def get_seed_ratio(provider): return seed_ratio + def searchforalbum(albumid=None, new=False, losslessOnly=False, choose_specific_download=False): myDB = db.DBConnection() @@ -204,6 +210,7 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False, choose_specific_ 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) @@ -249,7 +256,6 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False): results = nzb_results + torrent_results - if choose_specific_download: return results @@ -264,11 +270,13 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False): if data and bestqual: send_to_downloader(data, bestqual, album) + def removeDisallowedFilenameChars(filename): validFilenameChars = "-_.() %s%s" % (string.ascii_letters, string.digits) cleanedFilename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').lower() return ''.join(c for c in cleanedFilename if c in validFilenameChars) + def more_filtering(results, album, albumlength, new): low_size_limit = None @@ -332,6 +340,7 @@ def more_filtering(results, album, albumlength, new): return results + def sort_search_results(resultlist, album, new, albumlength): if new and not len(resultlist): @@ -401,6 +410,7 @@ def sort_search_results(resultlist, album, new, albumlength): return finallist + def get_year_from_release_date(release_date): try: @@ -410,6 +420,7 @@ def get_year_from_release_date(release_date): return year + def searchNZB(album, new=False, losslessOnly=False, albumlength=None): albumid = album['AlbumID'] @@ -678,6 +689,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None): return results + 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])) @@ -920,6 +932,7 @@ def send_to_downloader(data, bestqual, album): boxcar = notifiers.BOXCAR() boxcar.notify('Headphones snatched: ' + title, b2msg, rgid) + def verifyresult(title, artistterm, term, lossless): title = re.sub('[\.\-\/\_]', ' ', title) @@ -985,6 +998,7 @@ def verifyresult(title, artistterm, term, lossless): return True + def searchTorrent(album, new=False, losslessOnly=False, albumlength=None): global gazelle # persistent what.cd api object to reduce number of login attempts @@ -1057,7 +1071,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None): return proxy_url - if headphones.CONFIG.KAT: provider = "Kick Ass Torrents" ka_term = term.replace("!", "") @@ -1446,6 +1459,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None): return results # THIS IS KIND OF A MESS AND PROBABLY NEEDS TO BE CLEANED UP + + def preprocess(resultlist): for result in resultlist: diff --git a/headphones/searcher_rutracker.py b/headphones/searcher_rutracker.py index 5e2f3c62..815cd903 100644 --- a/headphones/searcher_rutracker.py +++ b/headphones/searcher_rutracker.py @@ -20,6 +20,7 @@ import urllib import re import os + class Rutracker(): logged_in = False diff --git a/headphones/torrentfinished.py b/headphones/torrentfinished.py index dcea9c68..38b5b30b 100644 --- a/headphones/torrentfinished.py +++ b/headphones/torrentfinished.py @@ -20,6 +20,8 @@ from headphones import db, utorrent, transmission, logger postprocessor_lock = threading.Lock() # Remove Torrent + data if Post Processed and finished Seeding + + def checkTorrentFinished(): logger.info("Checking if any torrents have finished seeding and can be removed") diff --git a/headphones/transmission.py b/headphones/transmission.py index 2d42f227..4a5897f9 100644 --- a/headphones/transmission.py +++ b/headphones/transmission.py @@ -27,6 +27,7 @@ import headphones # TODO: Store the session id so we don't need to make 2 calls # Store torrent id so we can check up on it + def addTorrent(link): method = 'torrent-add' @@ -60,6 +61,7 @@ def addTorrent(link): logger.info('Transmission returned status %s' % response['result']) return False + def getTorrentFolder(torrentid): method = 'torrent-get' arguments = { 'ids': torrentid, 'fields': ['name', 'percentDone']} @@ -80,6 +82,7 @@ def getTorrentFolder(torrentid): return torrent_folder_name + def setSeedRatio(torrentid, ratio): method = 'torrent-set' if ratio != 0: @@ -91,6 +94,7 @@ def setSeedRatio(torrentid, ratio): if not response: return False + def removeTorrent(torrentid, remove_data = False): method = 'torrent-get' @@ -120,6 +124,7 @@ def removeTorrent(torrentid, remove_data = False): return False + def torrentAction(method, arguments): host = headphones.CONFIG.TRANSMISSION_HOST diff --git a/headphones/updater.py b/headphones/updater.py index 30b9f057..4b1e8910 100644 --- a/headphones/updater.py +++ b/headphones/updater.py @@ -17,6 +17,7 @@ import headphones from headphones import logger, db, importer + def dbUpdate(forcefull=False): myDB = db.DBConnection() diff --git a/headphones/utorrent.py b/headphones/utorrent.py index 8410a4d4..e705678d 100644 --- a/headphones/utorrent.py +++ b/headphones/utorrent.py @@ -21,6 +21,7 @@ import headphones from headphones import logger from collections import namedtuple + class utorrentclient(object): TOKEN_REGEX = "" @@ -156,12 +157,14 @@ class utorrentclient(object): logger.debug('URL: ' + str(url)) logger.debug('uTorrent webUI raised the following error: ' + str(err)) + def labelTorrent(hash): label = headphones.CONFIG.UTORRENT_LABEL uTorrentClient = utorrentclient() if label: uTorrentClient.setprops(hash, 'label', label) + def removeTorrent(hash, remove_data = False): uTorrentClient = utorrentclient() status, torrentList = uTorrentClient.list() @@ -177,6 +180,7 @@ def removeTorrent(hash, remove_data = False): return False return False + def setSeedRatio(hash, ratio): uTorrentClient = utorrentclient() uTorrentClient.setprops(hash, 'seed_override', '1') @@ -186,6 +190,7 @@ def setSeedRatio(hash, ratio): # TODO passing -1 should be unlimited uTorrentClient.setprops(hash, 'seed_ratio', -10) + def dirTorrent(hash, cacheid=None, return_name=None): uTorrentClient = utorrentclient() @@ -212,6 +217,7 @@ def dirTorrent(hash, cacheid=None, return_name=None): return None, None + def addTorrent(link, hash): uTorrentClient = utorrentclient() @@ -243,6 +249,7 @@ def addTorrent(link, hash): labelTorrent(hash) return os.path.basename(os.path.normpath(torrent_folder)) + def getSettingsDirectories(): uTorrentClient = utorrentclient() settings = uTorrentClient.get_settings() diff --git a/headphones/versioncheck.py b/headphones/versioncheck.py index f8d735d5..8eb9bf0c 100644 --- a/headphones/versioncheck.py +++ b/headphones/versioncheck.py @@ -22,6 +22,7 @@ import subprocess from headphones import logger, version, request + def runGit(args): if headphones.CONFIG.GIT_PATH: @@ -59,6 +60,7 @@ def runGit(args): return (output, err) + def getVersion(): if version.HEADPHONES_VERSION.startswith('win32build'): @@ -115,6 +117,7 @@ def getVersion(): else: return None, 'master' + def checkGithub(): headphones.COMMITS_BEHIND = 0 @@ -161,6 +164,7 @@ def checkGithub(): return headphones.LATEST_VERSION + def update(): if headphones.INSTALL_TYPE == 'win': logger.info('Windows .exe updating not supported yet.') diff --git a/headphones/webserve.py b/headphones/webserve.py index 3cb54991..c9412c4e 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -37,6 +37,7 @@ except ImportError: # Python 2.6.x fallback, from libs from ordereddict import OrderedDict + def serve_template(templatename, **kwargs): interface_dir = os.path.join(str(headphones.PROG_DIR), 'data/interfaces/') @@ -50,6 +51,7 @@ def serve_template(templatename, **kwargs): except: return exceptions.html_error_template().render() + class WebInterface(object): def index(self): @@ -102,7 +104,6 @@ class WebInterface(object): return serve_template(templatename="artist.html", title=artist['ArtistName'], artist=artist, albums=albums, extras=extras_dict) artistPage.exposed = True - def albumPage(self, AlbumID): myDB = db.DBConnection() album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone() @@ -132,7 +133,6 @@ class WebInterface(object): return serve_template(templatename="album.html", title=title, album=album, tracks=tracks, description=description) albumPage.exposed = True - def search(self, name, type): if len(name) == 0: raise cherrypy.HTTPRedirect("home") @@ -472,7 +472,6 @@ class WebInterface(object): 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) manageUnmatched.exposed = True @@ -782,7 +781,6 @@ class WebInterface(object): totalcount = 0 myDB = db.DBConnection() - sortcolumn = 'ArtistSortName' sortbyhavepercent = False if iSortCol_0 == '2': @@ -809,7 +807,6 @@ class WebInterface(object): if sortcolumn == 'ReleaseDate': filtered.reverse() - artists = filtered[iDisplayStart:(iDisplayStart+iDisplayLength)] rows = [] for artist in artists: @@ -840,7 +837,6 @@ class WebInterface(object): rows.append(row) - dict = {'iTotalDisplayRecords': len(filtered), 'iTotalRecords': totalcount, 'aaData': rows, @@ -1362,6 +1358,7 @@ class WebInterface(object): return msg osxnotifyregister.exposed = True + class Artwork(object): def index(self): return "Artwork" @@ -1400,6 +1397,7 @@ class Artwork(object): def index(self): return "Here be thumbs" index.exposed = True + def default(self, ArtistOrAlbum="", ID=None): from headphones import cache ArtistID = None diff --git a/headphones/webstart.py b/headphones/webstart.py index 7010a83f..932ef3d2 100644 --- a/headphones/webstart.py +++ b/headphones/webstart.py @@ -22,6 +22,7 @@ from headphones import logger from headphones.webserve import WebInterface from headphones.helpers import create_https_certificates + def initialize(options=None): if options is None: options = {} @@ -111,7 +112,6 @@ def initialize(options=None): }) conf['/api'] = { 'tools.auth_basic.on': False } - # Prevent time-outs cherrypy.engine.timeout_monitor.unsubscribe() cherrypy.tree.mount(WebInterface(), str(options['http_root']), config=conf)