From 738f0fd6af09dbd3f1854472ef16b4fcd0d4805b Mon Sep 17 00:00:00 2001 From: Remy Date: Wed, 27 Jul 2011 00:38:15 -0700 Subject: [PATCH] Post Processing! Very beta, but improvements coming soon. Also: specify log dir, automatically add extras for artists, release group selection process improvements --- headphones/__init__.py | 45 ++++++--- headphones/albumart.py | 10 ++ headphones/importer.py | 14 ++- headphones/mb.py | 66 ++++++++++--- headphones/postprocessor.py | 190 ++++++++++++++++++++++++++++++++++++ headphones/searcher.py | 10 +- headphones/templates.py | 47 +++++++-- headphones/webserve.py | 19 +++- 8 files changed, 353 insertions(+), 48 deletions(-) create mode 100644 headphones/albumart.py create mode 100644 headphones/postprocessor.py diff --git a/headphones/__init__.py b/headphones/__init__.py index 6c769da7..673148fd 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -11,7 +11,7 @@ from lib.configobj import ConfigObj import cherrypy -from headphones import updater, searcher, importer, versioncheck, logger +from headphones import updater, searcher, importer, versioncheck, logger, postprocessor FULL_PATH = None PROG_DIR = None @@ -52,13 +52,14 @@ LATEST_VERSION = None COMMITS_BEHIND = None MUSIC_DIR = None +DESTINATION_DIR = None FOLDER_FORMAT = None FILE_FORMAT = None PATH_TO_XML = None PREFERRED_QUALITY = None PREFERRED_BITRATE = None DETECT_BITRATE = False -FLAC_TO_MP3 = False +CORRECT_METADATA = False MOVE_FILES = False RENAME_FILES = False CLEANUP_FILES = False @@ -67,6 +68,7 @@ DOWNLOAD_DIR = None BLACKHOLE = None BLACKHOLE_DIR = None USENET_RETENTION = None +INCLUDE_EXTRAS = False NZB_SEARCH_INTERVAL = 360 LIBRARYSCAN_INTERVAL = 60 @@ -141,8 +143,8 @@ def initialize(): global __INITIALIZED__, FULL_PATH, PROG_DIR, QUIET, DAEMON, DATA_DIR, CONFIG_FILE, CFG, LOG_DIR, CACHE_DIR, \ HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, LAUNCH_BROWSER, GIT_PATH, \ - CURRENT_VERSION, LATEST_VERSION, MUSIC_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, \ - FLAC_TO_MP3, MOVE_FILES, RENAME_FILES, FOLDER_FORMAT, FILE_FORMAT, CLEANUP_FILES, \ + CURRENT_VERSION, LATEST_VERSION, MUSIC_DIR, DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, \ + CORRECT_METADATA, MOVE_FILES, RENAME_FILES, FOLDER_FORMAT, FILE_FORMAT, CLEANUP_FILES, INCLUDE_EXTRAS, \ ADD_ALBUM_ART, DOWNLOAD_DIR, BLACKHOLE, BLACKHOLE_DIR, USENET_RETENTION, NZB_SEARCH_INTERVAL, \ LIBRARYSCAN_INTERVAL, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \ NZBMATRIX, NZBMATRIX_USERNAME, NZBMATRIX_APIKEY, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, \ @@ -173,22 +175,25 @@ def initialize(): HTTP_ROOT = check_setting_str(CFG, 'General', 'http_root', '/') LAUNCH_BROWSER = bool(check_setting_int(CFG, 'General', 'launch_browser', 1)) GIT_PATH = check_setting_str(CFG, 'General', 'git_path', '') + LOG_DIR = check_setting_str(CFG, 'General', 'log_dir', '') MUSIC_DIR = check_setting_str(CFG, 'General', 'music_dir', '') + DESTINATION_DIR = check_setting_str(CFG, 'General', 'destination_dir', '') PREFERRED_QUALITY = check_setting_int(CFG, 'General', 'preferred_quality', 0) PREFERRED_BITRATE = check_setting_int(CFG, 'General', 'preferred_bitrate', '') DETECT_BITRATE = bool(check_setting_int(CFG, 'General', 'detect_bitrate', 0)) - FLAC_TO_MP3 = bool(check_setting_int(CFG, 'General', 'flac_to_mp3', 0)) + CORRECT_METADATA = bool(check_setting_int(CFG, 'General', 'correct_metadata', 0)) MOVE_FILES = bool(check_setting_int(CFG, 'General', 'move_files', 0)) RENAME_FILES = bool(check_setting_int(CFG, 'General', 'rename_files', 0)) - FOLDER_FORMAT = check_setting_str(CFG, 'General', 'folder_format', '%artist/%album/%track') - FILE_FORMAT = check_setting_str(CFG, 'General', 'file_format', '%tracknumber %artist - %album - %title') + FOLDER_FORMAT = check_setting_str(CFG, 'General', 'folder_format', 'artist/album [year]') + FILE_FORMAT = check_setting_str(CFG, 'General', 'file_format', 'tracknumber artist - album [year]- title') CLEANUP_FILES = bool(check_setting_int(CFG, 'General', 'cleanup_files', 0)) ADD_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'add_album_art', 0)) DOWNLOAD_DIR = check_setting_str(CFG, 'General', 'download_dir', '') BLACKHOLE = bool(check_setting_int(CFG, 'General', 'blackhole', 0)) BLACKHOLE_DIR = check_setting_str(CFG, 'General', 'blackhole_dir', '') USENET_RETENTION = check_setting_int(CFG, 'General', 'usenet_retention', '') + INCLUDE_EXTRAS = bool(check_setting_int(CFG, 'General', 'include_extras', 0)) NZB_SEARCH_INTERVAL = check_setting_int(CFG, 'General', 'nzb_search_interval', 360) LIBRARYSCAN_INTERVAL = check_setting_int(CFG, 'General', 'libraryscan_interval', 180) @@ -211,8 +216,9 @@ def initialize(): NZBSORG_UID = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_uid', '') NZBSORG_HASH = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_hash', '') - # Put the log dir in the data dir for now - LOG_DIR = os.path.join(DATA_DIR, 'logs') + if not LOG_DIR: + LOG_DIR = os.path.join(DATA_DIR, 'logs') + if not os.path.exists(LOG_DIR): try: os.makedirs(LOG_DIR) @@ -223,6 +229,11 @@ def initialize(): # Start the logger, silence console logging if we need to logger.headphones_log.initLogger(quiet=QUIET) + # Update some old config code: + if FOLDER_FORMAT == '%artist/%album/%track': + FOLDER_FORMAT = 'artist/album [year]' + if FILE_FORMAT == '%tracknumber %artist - %album - %title': + FILE_FORMAT = 'tracknumber artist - album - title' # Put the cache dir in the data dir for now CACHE_DIR = os.path.join(DATA_DIR, 'cache') @@ -232,8 +243,6 @@ def initialize(): except OSError: logger.error('Could not create cache dir. Check permissions of datadir: ' + DATA_DIR) - - # Initialize the database logger.info('Checking to see if the database has all tables....') try: @@ -318,13 +327,15 @@ def config_write(): new_config['General']['http_password'] = HTTP_PASSWORD new_config['General']['http_root'] = HTTP_ROOT new_config['General']['launch_browser'] = int(LAUNCH_BROWSER) + new_config['General']['log_dir'] = LOG_DIR new_config['General']['git_path'] = GIT_PATH new_config['General']['music_dir'] = MUSIC_DIR + new_config['General']['destination_dir'] = DESTINATION_DIR new_config['General']['preferred_quality'] = PREFERRED_QUALITY new_config['General']['preferred_bitrate'] = PREFERRED_BITRATE new_config['General']['detect_bitrate'] = int(DETECT_BITRATE) - new_config['General']['flac_to_mp3'] = int(FLAC_TO_MP3) + new_config['General']['correct_metadata'] = int(CORRECT_METADATA) new_config['General']['move_files'] = int(MOVE_FILES) new_config['General']['rename_files'] = int(RENAME_FILES) new_config['General']['folder_format'] = FOLDER_FORMAT @@ -335,6 +346,7 @@ def config_write(): new_config['General']['blackhole'] = int(BLACKHOLE) new_config['General']['blackhole_dir'] = BLACKHOLE_DIR new_config['General']['usenet_retention'] = USENET_RETENTION + new_config['General']['include_extras'] = int(INCLUDE_EXTRAS) new_config['General']['nzb_search_interval'] = NZB_SEARCH_INTERVAL new_config['General']['libraryscan_interval'] = LIBRARYSCAN_INTERVAL @@ -376,6 +388,8 @@ def start(): SCHED.add_interval_job(searcher.searchNZB, minutes=NZB_SEARCH_INTERVAL) SCHED.add_interval_job(importer.scanMusic, minutes=LIBRARYSCAN_INTERVAL) SCHED.add_interval_job(versioncheck.checkGithub, minutes=300) + SCHED.add_interval_job(postprocessor.checkFolder, minutes=5) + postprocessor.checkFolder() SCHED.start() @@ -388,7 +402,7 @@ def dbcheck(): c.execute('CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER)') c.execute('CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT, Type TEXT)') c.execute('CREATE TABLE IF NOT EXISTS tracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT)') - c.execute('CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT)') + c.execute('CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT)') c.execute('CREATE TABLE IF NOT EXISTS have (ArtistName TEXT, AlbumTitle TEXT, TrackNumber TEXT, TrackTitle TEXT, TrackLength TEXT, BitRate TEXT, Genre TEXT, Date TEXT, TrackID TEXT)') try: @@ -425,6 +439,11 @@ def dbcheck(): c.execute('SELECT Type from albums') except sqlite3.OperationalError: c.execute('ALTER TABLE albums ADD COLUMN Type TEXT DEFAULT "Album"') + + try: + c.execute('SELECT FolderName from snatched') + except sqlite3.OperationalError: + c.execute('ALTER TABLE snatched ADD COLUMN FolderName TEXT') conn.commit() c.close() diff --git a/headphones/albumart.py b/headphones/albumart.py new file mode 100644 index 00000000..ace34556 --- /dev/null +++ b/headphones/albumart.py @@ -0,0 +1,10 @@ +from headphones import db + +def getAlbumArt(albumid): + + myDB = db.DBConnection() + asin = myDB.action('SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0] + + url = 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % asin + + return url \ No newline at end of file diff --git a/headphones/importer.py b/headphones/importer.py index 721af6ac..f9a85d48 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -4,7 +4,7 @@ import os from lib.beets.mediafile import MediaFile import headphones -from headphones import logger, helpers, db, mb +from headphones import logger, helpers, db, mb, albumart various_artists_mbid = '89ad4ac3-39f7-470e-963a-56509c546377' @@ -158,6 +158,9 @@ def addArtisttoDB(artistid, extrasonly=False): "DateAdded": helpers.today(), "Status": "Loading"} + if headphones.INCLUDE_EXTRAS: + newValueDict['IncludeExtras'] = 1 + myDB.upsert("artists", newValueDict, controlValueDict) for rg in artist['releasegroups']: @@ -170,7 +173,8 @@ def addArtisttoDB(artistid, extrasonly=False): try: release_dict = mb.getReleaseGroup(rgid) except Exception, e: - logger.info('Unable to get release information for %s - it may not be a valid release group' % rg['title']) + logger.info('Unable to get release information for %s - it may not be a valid release group \ + (or it might just not be tagged right in MusicBrainz)' % rg['title']) continue if not release_dict: @@ -187,7 +191,7 @@ def addArtisttoDB(artistid, extrasonly=False): if len(rg_exists): - newValueDict = {"AlbumASIN": release['asin'], + newValueDict = {"AlbumASIN": release_dict['asin'], "ReleaseDate": release_dict['releasedate'], } @@ -196,7 +200,7 @@ def addArtisttoDB(artistid, extrasonly=False): newValueDict = {"ArtistID": artistid, "ArtistName": artist['artist_name'], "AlbumTitle": rg['title'], - "AlbumASIN": release['asin'], + "AlbumASIN": release_dict['asin'], "ReleaseDate": release_dict['releasedate'], "DateAdded": helpers.today(), "Type": rg['type'] @@ -219,7 +223,7 @@ def addArtisttoDB(artistid, extrasonly=False): newValueDict = {"ArtistID": artistid, "ArtistName": artist['artist_name'], "AlbumTitle": rg['title'], - "AlbumASIN": release['asin'], + "AlbumASIN": release_dict['asin'], "TrackTitle": track['title'], "TrackDuration": track['duration'], } diff --git a/headphones/mb.py b/headphones/mb.py index 20cf0d9d..0af3174e 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -9,8 +9,9 @@ import lib.musicbrainz2.utils as u from lib.musicbrainz2.webservice import WebServiceError +import headphones from headphones import logger, db -from headphones.helpers import multikeysort +from headphones.helpers import multikeysort, replace_all q = ws.Query() mb_lock = threading.Lock() @@ -83,7 +84,7 @@ def getArtist(artistid, extrasonly=False): artist = q.getArtistById(artistid, inc) break except WebServiceError, e: - logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s. Sleeping 10 seconds' % artistid) + logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s. Sleeping 5 seconds' % artistid) attempt += 1 time.sleep(5) @@ -120,7 +121,7 @@ def getArtist(artistid, extrasonly=False): except IndexError: includeExtras = False - if includeExtras: + if includeExtras or headphones.INCLUDE_EXTRAS: includes = [m.Release.TYPE_COMPILATION, m.Release.TYPE_REMIX, m.Release.TYPE_SINGLE, m.Release.TYPE_LIVE, m.Release.TYPE_EP] for include in includes: inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, include), releaseGroups=True) @@ -134,7 +135,7 @@ def getArtist(artistid, extrasonly=False): artist = q.getArtistById(artistid, inc) break except WebServiceError, e: - logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s. Sleeping 10 seconds' % artistid) + logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s. Sleeping 5 seconds' % artistid) attempt += 1 time.sleep(5) @@ -161,6 +162,7 @@ def getReleaseGroup(rgid): with mb_lock: releaselist = [] + asinlist = [] inc = ws.ReleaseGroupIncludes(releases=True) releaseGroup = None @@ -172,7 +174,7 @@ def getReleaseGroup(rgid): releaseGroup = q.getReleaseGroupById(rgid, inc) break except WebServiceError, e: - logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed. Sleeping 10 seconds' % rgid) + logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed. Sleeping 5 seconds' % rgid) attempt += 1 time.sleep(5) @@ -194,28 +196,66 @@ def getReleaseGroup(rgid): releaseResult = q.getReleaseById(release.id, inc) break except WebServiceError, e: - logger.warn('Attempt to retrieve release information for %s from MusicBrainz failed: %s. Sleeping 10 seconds' % (releaseResult.title, e)) + logger.warn('Attempt to retrieve release information for %s from MusicBrainz failed: %s. Sleeping 5 seconds' % (releaseResult.title, e)) attempt += 1 time.sleep(5) if not releaseResult: continue + if releaseResult.title.lower() != releaseGroup.title.lower(): + continue + time.sleep(1) + formats = { + '2xVinyl': '2', + 'Vinyl': '2', + 'CD': '0', + 'Cassette': '3', + '2xCD': '1', + 'Digital Media': '0' + } + + country = { + 'US': '0', + 'GB': '1', + 'JP': '1', + } + + + try: + format = int(replace_all(u.extractFragment(releaseResult.releaseEvents[0].format), formats)) + except: + format = 3 + + try: + country = int(replace_all(releaseResult.releaseEvents[0].country, country)) + except: + country = 2 + release_dict = { - 'asin': bool(releaseResult.asin), + 'hasasin': bool(releaseResult.asin), + 'asin': releaseResult.asin, 'tracks': len(releaseResult.getTracks()), 'releaseid': u.extractUuid(releaseResult.id), - 'releasedate': releaseResult.getEarliestReleaseDate() + 'releasedate': releaseResult.getEarliestReleaseDate(), + 'format': format, + 'country': country } - + releaselist.append(release_dict) + + if releaseResult.asin: + asinlist.append(releaseResult.asin) - a = multikeysort(releaselist, ['-asin', '-tracks']) + a = multikeysort(releaselist, ['-hasasin', 'country', 'format', 'tracks']) release_dict = {'releaseid' :a[0]['releaseid'], - 'releasedate' : releaselist[0]['releasedate']} + 'releasedate' : releaselist[0]['releasedate'], + 'trackcount' : a[0]['tracks'], + 'asin' : a[0]['asin'] + } return release_dict @@ -237,7 +277,7 @@ def getRelease(releaseid): results = q.getReleaseById(releaseid, inc) break except WebServiceError, e: - logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed: %s. SLeeping 10 seconds' % (releaseid, e)) + logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed: %s. SLeeping 5 seconds' % (releaseid, e)) attempt += 1 time.sleep(5) @@ -286,7 +326,7 @@ def findArtistbyAlbum(name): results = q.getReleaseGroups(f) break except WebServiceError, e: - logger.warn('Attempt to query MusicBrainz for %s failed: %s. Sleeping 10 seconds.' % (name, e)) + logger.warn('Attempt to query MusicBrainz for %s failed: %s. Sleeping 5 seconds.' % (name, e)) attempt += 1 time.sleep(5) diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py new file mode 100644 index 00000000..2d5997c1 --- /dev/null +++ b/headphones/postprocessor.py @@ -0,0 +1,190 @@ +import os +import time + +import urllib, shutil + +from lib.beets.mediafile import MediaFile +import lib.musicbrainz2.webservice as ws + +import headphones +from headphones import db, albumart, logger, helpers + +def checkFolder(): + + myDB = db.DBConnection() + snatched = myDB.select('SELECT * from snatched WHERE Status="Snatched" or Status="Unprocessed"') + + for album in snatched: + + if album['FolderName']: + + album_path = os.path.join(headphones.DOWNLOAD_DIR, album['FolderName']) + + if os.path.exists(album_path): + verify(album['AlbumID'], album_path) + +def verify(albumid, albumpath): + + myDB = db.DBConnection() + release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone() + tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid]) + + downloaded_track_list = [] + for r,d,f in os.walk(albumpath): + for files in f: + if any(files.endswith(x) for x in (".mp3", ".flac", ".aac", ".ogg", ".ape")): + downloaded_track_list.append(os.path.join(r, files)) + + # test #1: filenames + + + # test #2: metadata. + for downloaded_track in downloaded_track_list: + try: + f = MediaFile(downloaded_track) + except: + continue + if helpers.latinToAscii(f.artist.lower()).encode('UTF-8') == helpers.latinToAscii(release['ArtistName'].lower()).encode('UTF-8') and helpers.latinToAscii(f.album.lower()).encode('UTF-8') == helpers.latinToAscii(release['AlbumTitle'].lower()).encode('UTF-8'): + doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list) + return + + # test #3: number of songs and duration + db_track_duration = 0 + downloaded_track_duration = 0 + + for track in tracks: + try: + db_track_duration += track['TrackDuration']/1000 + except: + downloaded_track_duration = False + break + + for downloaded_track in downloaded_track_list: + try: + f = MediaFile(downloaded_track) + downloaded_track_duration += f.length + except: + downloaded_track_duration = False + break + + if downloaded_track_duration and db_track_duration: + delta = abs(downloaded_track_duration - db_track_duration) + if len(tracks) == len(downloaded_track_list) and delta < 100: + doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list) + return + + logger.warn('Could not identify album: %s. It may not be the intended album.' % albumpath) + myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE AlbumID=?', [albumid]) + +def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list): + + logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle'])) + + if headphones.ADD_ALBUM_ART: + addAlbumArt(albumid, downloaded_track_list) + + if headphones.CLEANUP_FILES: + cleanupFiles(albumpath) + + if headphones.CORRECT_METADATA: + correctMetadata(albumid, release, downloaded_track_list) + + if headphones.RENAME_FILES: + renameFiles(albumpath, downloaded_track_list, release) + + if headphones.MOVE_FILES and headphones.DESTINATION_DIR: + moveFiles(albumpath, release, tracks) + + myDB = db.DBConnection() + myDB.action('UPDATE albums SET status = "Downloaded" WHERE AlbumID=?', [albumid]) + myDB.action('UPDATE snatched SET status = "Processed" WHERE AlbumID=?', [albumid]) + logger.info('Post-processing for %s - %s complete' % (release['ArtistName'], release['AlbumTitle'])) + + +def addAlbumArt(albumid, downloaded_track_list): + + album_art_path = albumart.getAlbumArt(albumid) + + artwork = urllib.urlopen(album_art_path).read() + + for downloaded_track in downloaded_track_list: + try: + f = MediaFile(downloaded_track) + except: + continue + + f.art = artwork + f.save() + +def cleanupFiles(albumpath): + + for r,d,f in os.walk(albumpath): + for files in f: + if not any(files.endswith(x) for x in (".mp3", ".flac", ".aac", ".ogg", ".ape", ".m4a")): + os.remove(os.path.join(r, files)) + +def moveFiles(albumpath, release, tracks): + + try: + year = release['ReleaseDate'][:4] + except TypeError: + year = '' + + values = { 'artist': release['ArtistName'], + 'album': release['AlbumTitle'], + 'year': year + } + + + folder = helpers.replace_all(headphones.FOLDER_FORMAT, values) + + destination_path = os.path.join(headphones.DESTINATION_DIR, folder) + + try: + os.makedirs(destination_path) + except Exception, e: + logger.error('Could not create folder for %s. Not moving' % release['AlbumName']) + return + + for r,d,f in os.walk(albumpath): + for files in f: + shutil.move(os.path.join(r, files), destination_path) + + try: + os.rmdir(albumpath) + except Exception, e: + logger.error('Could not remove directory: %s. %s' % (albumpath, e)) + +def correctMetadata(albumid, release, downloaded_track_list): + + pass + +def renameFiles(albumpath, downloaded_track_list, release): + + try: + year = release['ReleaseDate'][:4] + except TypeError: + year = '' + # Until tagging works better I'm going to rely on the already provided metadata + for downloaded_track in downloaded_track_list: + try: + f = MediaFile(downloaded_track) + except: + continue + + tracknumber = '%02d' % f.track + + values = { 'tracknumber': tracknumber, + 'title': f.title, + 'artist': release['ArtistName'], + 'album': release['AlbumTitle'], + 'year': year + } + + ext = os.path.splitext(downloaded_track)[1] + + new_file_name = helpers.replace_all(headphones.FILE_FORMAT, values).replace('/','_') + ext + + new_file = os.path.join(albumpath, new_file_name) + + shutil.move(downloaded_track, new_file) \ No newline at end of file diff --git a/headphones/searcher.py b/headphones/searcher.py index 019b98ef..6a6d1a13 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -244,7 +244,9 @@ def searchNZB(albumid=None, new=False): logger.info(u"Found best result: %s (%s) - %s" % (bestqual[0], bestqual[2], helpers.bytes_to_mb(bestqual[1]))) + downloadurl = bestqual[2] + nzb_folder_name = '%s - %s [%s]' % (helpers.latinToAscii(albums[0]).encode('UTF-8'), helpers.latinToAscii(albums[1]).encode('UTF-8'), year) if headphones.SAB_HOST and not headphones.BLACKHOLE: linkparams = {} @@ -262,7 +264,7 @@ def searchNZB(albumid=None, new=False): linkparams["name"] = downloadurl - linkparams["nzbname"] = ('%s - %s [%s]' % (helpers.latinToAscii(albums[0]).encode('UTF-8'), helpers.latinToAscii(albums[1]).encode('UTF-8'), year)) + linkparams["nzbname"] = nzb_folder_name saburl = 'http://' + headphones.SAB_HOST + '/sabnzbd/api?' + urllib.urlencode(linkparams) logger.info(u"Sending link to SABNZBD: " + saburl) @@ -275,12 +277,12 @@ def searchNZB(albumid=None, new=False): break myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [albums[2]]) - myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched"]) + myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched", nzb_folder_name]) elif headphones.BLACKHOLE: - nzb_name = ('%s - %s [%s].nzb' % (albums[0], albums[1], year)) + nzb_name = nzb_folder_name + '.nzb' download_path = os.path.join(headphones.BLACKHOLE_DIR, nzb_name) try: @@ -290,4 +292,4 @@ def searchNZB(albumid=None, new=False): break myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [albums[2]]) - myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched"]) \ No newline at end of file + myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched", nzb_folder_name]) \ No newline at end of file diff --git a/headphones/templates.py b/headphones/templates.py index 20eef8f5..bace7097 100644 --- a/headphones/templates.py +++ b/headphones/templates.py @@ -62,7 +62,8 @@ configform = form = ''' Web Interface | Download Settings | Search Providers | - Quality & Post Processing + Quality & Post Processing | + Advanced Settings
@@ -152,7 +153,7 @@ configform = form = '''

Music Download Directory:


Full path to the directory where SAB downloads your music
- i.e. Downloads/music or /Users/name/Downloads/music
+ i.e. /Users/name/Downloads/music @@ -277,10 +278,10 @@ configform = form = ''' Post-Processing:

- Move downloads to Music Folder
- Convert lossless to mp3
- Rename & add metadata
- Delete leftover files
+ Move downloads to Destination Folder
+ Rename files
+ Correct metadata
+ Delete leftover files (.m3u, .nfo, .sfv, .nzb, etc.)
Add album art

@@ -290,12 +291,42 @@ configform = form = '''
-

Path to Music folder:
+

Path to Destination folder:

i.e. /Users/name/Music/iTunes or /Volumes/share/music

+ + +

Advanced Settings

+ + + + + +
+ Renaming Options: +
+ +

Folder Format:
+
+ Use: artist, album and year +

+ +

File Format:
+
+ Use: tracknumber, title, artist, album and year +

+
+ Miscellaneous: +

+ Automatically Include Extras When Adding an Artist
+ Extras includes: EPs, Compilations, Live Albums, Remix Albums and Singles +

+

Log Directory:
+

+
@@ -346,7 +377,7 @@ def displayAlbums(ArtistID, Type=None): elif results[i][3] == 'Wanted': newStatus = '''%s[skip]''' % (results[i][3], results[i][2], ArtistID) elif results[i][3] == 'Downloaded': - newStatus = '''%s[retry]''' % (results[i][3], results[i][2], ArtistID) + newStatus = '''%s[retry][new]''' % (results[i][3], results[i][2], ArtistID, results[i][2], ArtistID) elif results[i][3] == 'Snatched': newStatus = '''%s[retry][new]''' % (results[i][3], results[i][2], ArtistID, results[i][2], ArtistID) else: diff --git a/headphones/webserve.py b/headphones/webserve.py index 95a09dbd..44cc32e2 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -550,11 +550,15 @@ class WebInterface(object): headphones.PREFERRED_BITRATE, checked(headphones.DETECT_BITRATE), checked(headphones.MOVE_FILES), - checked(headphones.FLAC_TO_MP3), checked(headphones.RENAME_FILES), + checked(headphones.CORRECT_METADATA), checked(headphones.CLEANUP_FILES), checked(headphones.ADD_ALBUM_ART), - headphones.MUSIC_DIR + headphones.DESTINATION_DIR, + headphones.FOLDER_FORMAT, + headphones.FILE_FORMAT, + checked(headphones.INCLUDE_EXTRAS), + headphones.LOG_DIR )) page.append(templates._footer % headphones.CURRENT_VERSION) return page @@ -565,7 +569,8 @@ class WebInterface(object): def configUpdate(self, http_host='0.0.0.0', http_username=None, http_port=8181, http_password=None, launch_browser=0, sab_host=None, sab_username=None, sab_apikey=None, sab_password=None, sab_category=None, download_dir=None, blackhole=0, blackhole_dir=None, usenet_retention=None, nzbmatrix=0, nzbmatrix_username=None, nzbmatrix_apikey=None, newznab=0, newznab_host=None, newznab_apikey=None, - nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, flac_to_mp3=0, move_files=0, music_dir=None, rename_files=0, cleanup_files=0, add_album_art=0): + nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, move_files=0, + rename_files=0, correct_metadata=0, cleanup_files=0, add_album_art=0, destination_dir=None, folder_format=None, file_format=None, include_extras=0, log_dir=None): headphones.HTTP_HOST = http_host headphones.HTTP_PORT = http_port @@ -593,12 +598,16 @@ class WebInterface(object): headphones.PREFERRED_QUALITY = int(preferred_quality) headphones.PREFERRED_BITRATE = preferred_bitrate headphones.DETECT_BITRATE = detect_bitrate - headphones.FLAC_TO_MP3 = flac_to_mp3 headphones.MOVE_FILES = move_files - headphones.MUSIC_DIR = music_dir + headphones.CORRECT_METADATA = correct_metadata headphones.RENAME_FILES = rename_files headphones.CLEANUP_FILES = cleanup_files headphones.ADD_ALBUM_ART = add_album_art + headphones.DESTINATION_DIR = destination_dir + headphones.FOLDER_FORMAT = folder_format + headphones.FILE_FORMAT = file_format + headphones.INCLUDE_EXTRAS = include_extras + headphones.LOG_DIR = log_dir headphones.config_write()