diff --git a/headphones/albumswitcher.py b/headphones/albumswitcher.py index f85508ef..8ac8ed84 100644 --- a/headphones/albumswitcher.py +++ b/headphones/albumswitcher.py @@ -14,7 +14,7 @@ # along with Headphones. If not, see . import headphones -from headphones import db, logger +from headphones import db, logger, cache def switch(AlbumID, ReleaseID): ''' @@ -42,6 +42,11 @@ def switch(AlbumID, ReleaseID): myDB.upsert("albums", newValueDict, controlValueDict) + # Update cache + c = cache.Cache() + c.remove_from_cache(AlbumID=AlbumID) + c.get_artwork_from_cache(AlbumID=AlbumID) + for track in newtrackdata: controlValueDict = {"TrackID": track['TrackID'], diff --git a/headphones/cache.py b/headphones/cache.py index 7b0c92e5..a003e494 100644 --- a/headphones/cache.py +++ b/headphones/cache.py @@ -242,6 +242,36 @@ class Cache(object): return {'artwork' : image_url, 'thumbnail' : thumb_url } + def remove_from_cache(self, ArtistID=None, AlbumID=None): + """ + Pass a musicbrainz id to this function (either ArtistID or AlbumID) + """ + + if ArtistID: + self.id = ArtistID + self.id_type = 'artist' + else: + self.id = AlbumID + self.id_type = 'album' + + self.query_type = 'artwork' + + if self._exists('artwork'): + for artwork_file in self.artwork_files: + try: + os.remove(artwork_file) + except: + logger.warn('Error deleting file from the cache: %s', artwork_file) + + self.query_type = 'thumb' + + if self._exists('thumb'): + for thumb_file in self.thumb_files: + try: + os.remove(thumb_file) + except Exception as e: + logger.warn('Error deleting file from the cache: %s', thumb_file) + def _update_cache(self): ''' Since we call the same url for both info and artwork, we'll update both at the same time @@ -249,6 +279,7 @@ class Cache(object): myDB = db.DBConnection() # Since lastfm uses release ids rather than release group ids for albums, we have to do a artist + album search for albums + # Exception is when adding albums manually, then we should use release id if self.id_type == 'artist': data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY) @@ -278,8 +309,13 @@ class Cache(object): else: - dbartist = myDB.action('SELECT ArtistName, AlbumTitle FROM albums WHERE AlbumID=?', [self.id]).fetchone() - data = lastfm.request_lastfm("album.getinfo", artist=dbartist['ArtistName'], album=dbartist['AlbumTitle'], api_key=LASTFM_API_KEY) + 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) + if not data: + 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) if not data: return diff --git a/headphones/cuesplit.py b/headphones/cuesplit.py new file mode 100755 index 00000000..fc7f3ea4 --- /dev/null +++ b/headphones/cuesplit.py @@ -0,0 +1,664 @@ +# This file is part of Headphones. +# +# Headphones is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Headphones is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Headphones. If not, see . + +# Most of this lifted from here: https://github.com/SzieberthAdam/gneposis-cdgrab + +import os +import sys +import re +import shutil +import commands +import subprocess +import time +import copy +import glob + +import headphones +from headphones import logger +from mutagen.flac import FLAC + +CUE_HEADER = { + 'genre': '^REM GENRE (.+?)$', + 'date': '^REM DATE (.+?)$', + 'discid': '^REM DISCID (.+?)$', + 'comment': '^REM COMMENT (.+?)$', + 'catalog': '^CATALOG (.+?)$', + 'artist': '^PERFORMER (.+?)$', + 'title': '^TITLE (.+?)$', + 'file': '^FILE (.+?) WAVE$', + 'accurateripid': '^REM ACCURATERIPID (.+?)$' +} + +CUE_TRACK = 'TRACK (\d\d) AUDIO$' + +CUE_TRACK_INFO = { + 'artist': 'PERFORMER (.+?)$', + 'title': 'TITLE (.+?)$', + 'isrc': 'ISRC (.+?)$', + 'index': 'INDEX (\d\d) (.+?)$' +} + +ALBUM_META_FILE_NAME = 'album.dat' +SPLIT_FILE_NAME = 'split.dat' + +ALBUM_META_ALBUM_BY_CUE = ('artist', 'title', 'date', 'genre') + +HTOA_LENGTH_TRIGGER = 3 + +WAVE_FILE_TYPE_BY_EXTENSION = { + '.wav': 'Waveform Audio', + '.wv': 'WavPack', + '.ape': "Monkey's Audio", + '.m4a': 'Apple Lossless', + '.flac': 'Free Lossless Audio Codec' +} + +# TODO: Only alow flac for now +#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: + env = os.environ.copy() + if 'xld' in command: + env['PATH'] += os.pathsep + '/Applications' + devnull = open(os.devnull) + subprocess.Popen([command], stdout=devnull, stderr=devnull, env=env).communicate() + except OSError as e: + if e.errno == os.errno.ENOENT: + 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')) + logger.debug(subprocess.list2cmdline(split_cmd)) + + # Prevent Windows from opening a terminal window + startupinfo = None + + if headphones.SYS_PLATFORM == "win32": + startupinfo = subprocess.STARTUPINFO() + try: + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + except AttributeError: + startupinfo.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW + + env = os.environ.copy() + if 'xld' in split_cmd: + env['PATH'] += os.pathsep + '/Applications' + + process = subprocess.Popen(split_cmd, startupinfo=startupinfo, + + stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=env) + stdout, stderr = process.communicate() + if process.returncode: + logger.error('Split failed for %s', split_file.decode(headphones.SYS_ENCODING, 'replace')) + out = stdout if stdout else stderr + logger.error('Error details: %s', out.decode(headphones.SYS_ENCODING, 'replace')) + return False + else: + 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.''' + + if ignore: + try: + list[int(ignore)] + except: + raise ValueError('non-integer ignore index or ignore index not in list') + + list1 = list[:ignore] + list2 = list[ignore:] + + try: + first_none = list2.index(None) + except: + return tuple(list1 + list2) + + for i in range(first_none, len(list2)): + if list2[i]: + raise ValueError('non-None entry after None entry in list at index {0}'.format(i)) + + while True: + list2.remove(None) + try: + list2.index(None) + except: + break + + 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: + int(value) + except: + raise ValueError('expected an integer value') + + content = str(value) + while len(content) < length: + content = '0' + content + return content + +def split_file_list(ext=None): + file_list = [None for m in range(100)] + if ext and ext[0] != '.': + ext = '.' + ext + for f in os.listdir('.'): + if f[:11] == 'split-track': + if (ext and ext == os.path.splitext(f)[-1]) or not ext: + filename_parser = re.search('split-track(\d\d)', f) + track_nr = int(filename_parser.group(1)) + if cue.htoa() and not os.path.exists('split-track00'+ext): + track_nr -= 1 + file_list[track_nr] = WaveFile(f, track_nr=track_nr) + return check_list(file_list, ignore=1) + + +class Directory: + def __init__(self, path): + self.path = path + self.name = os.path.split(self.path)[-1] + self.content = [] + self.update() + + def filter(self, classname): + content = [] + for c in self.content: + if c.__class__.__name__ == classname: + content.append(c) + return content + + def tracks(self, ext=None, split=False): + content = [] + for c in self.content: + ext_match = False + if c.__class__.__name__ == 'WaveFile': + if not ext or (ext and ext == c.name_ext): + ext_match = True + if ext_match and c.track_nr: + if not split or (split and c.split_file): + content.append(c) + return content + + def update(self): + def check_match(filename): + for i in self.content: + if i.name == filename: + return True + return False + + def identify_track_number(filename): + if 'split-track' in filename: + search = re.search('split-track(\d\d)', filename) + if search: + n = int(search.group(1)) + if n: + return n + for n in range(0,100): + search = re.search(int_to_str(n), filename) + if search: + # TODO: not part of other value such as year + return n + + list_dir = glob.glob(os.path.join(self.path, '*')) + + # TODO: for some reason removes only one file + rem_list = [] + for i in self.content: + if i.name not in list_dir: + rem_list.append(i) + for i in rem_list: + self.content.remove(i) + + for i in list_dir: + if not check_match(i): + # music file + if os.path.splitext(i)[-1] in WAVE_FILE_TYPE_BY_EXTENSION.keys(): + track_nr = identify_track_number(i) + if track_nr: + self.content.append(WaveFile(self.path + os.sep + i, track_nr=track_nr)) + else: + self.content.append(WaveFile(self.path + os.sep + i)) + + # cue file + elif os.path.splitext(i)[-1] == '.cue': + self.content.append(CueFile(self.path + os.sep + i)) + + # meta file + elif i == ALBUM_META_FILE_NAME: + self.content.append(MetaFile(self.path + os.sep + i)) + + # directory + elif os.path.isdir(i): + self.content.append(Directory(self.path + os.sep + i)) + + else: + self.content.append(File(self.path + os.sep + i)) + +class File: + def __init__(self, path): + self.path = path + self.name = os.path.split(self.path)[-1] + + self.name_name = ''.join(os.path.splitext(self.name)[:-1]) + self.name_ext = os.path.splitext(self.name)[-1] + self.split_file = True if self.name_name[:11] == 'split-track' else False + + def get_name(self, ext=True, cmd=False): + + if ext == True: + content = self.name + elif ext == False: + content = self.name_name + elif ext[0] == '.': + content = self.name_name + ext + else: + raise ValueError('ext parameter error') + + if cmd: + content = content.replace(' ', '\ ') + + return content + +class CueFile(File): + def __init__(self, path): + + def header_parser(): + global line_content + c = self.content.splitlines() + header_dict = {} + #remaining_headers = CUE_HEADER + remaining_headers = copy.copy(CUE_HEADER) + line_index = 0 + match = True + while match: + match = False + saved_match = None + line_content = c[line_index] + for e in remaining_headers: + search_result = re.search(remaining_headers[e], line_content, re.I) + if search_result: + search_content = trim_cue_entry(search_result.group(1)) + header_dict[e] = search_content + saved_match = e + match = True + line_index += 1 + if saved_match: + del remaining_headers[saved_match] + return header_dict, line_index + + def track_parser(start_line): + c = self.content.splitlines() + line_index = start_line + 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)) + track_nr = int(search_result.group(1)) + line_index += 1 + next_track = False + track_meta = {} + # we make room for future indexes + track_meta['index'] = [None for m in range(100)] + + while not next_track: + if line_index < len(c): + line_content = c[line_index] + + artist_search = re.search(CUE_TRACK_INFO['artist'], line_content, re.I) + title_search = re.search(CUE_TRACK_INFO['title'], line_content, re.I) + isrc_search = re.search(CUE_TRACK_INFO['isrc'], line_content, re.I) + index_search = re.search(CUE_TRACK_INFO['index'], line_content, re.I) + + if artist_search: + if trim_cue_entry(artist_search.group(1)) != self.header['artist']: + track_meta['artist'] = trim_cue_entry(artist_search.group(1)) + line_index += 1 + elif title_search: + track_meta['title'] = trim_cue_entry(title_search.group(1)) + line_index += 1 + elif isrc_search: + track_meta['isrc'] = trim_cue_entry(isrc_search.group(1)) + line_index += 1 + elif index_search: + track_meta['index'][int(index_search.group(1))] = index_search.group(2) + line_index += 1 + elif re.search(CUE_TRACK, line_content, re.I): + next_track = True + elif line_index == len(c)-1 and not line_content: + # last line is empty + line_index += 1 + elif re.search('FLAGS DCP$', line_content, re.I): + track_meta['dcpflag'] = True + line_index += 1 + else: + raise ValueError('unknown entry in track error, line {0}'.format(line_index+1)) + else: + next_track = True + + track_meta['index'] = check_list(track_meta['index'], ignore=1) + + return track_nr, track_meta, line_index + + File.__init__(self, path) + + try: + with open(self.name) as cue_file: + self.content = cue_file.read() + except: + self.content = None + + if not self.content: + try: + with open(self.name, encoding="cp1252") as cue_file: + self.content = cue_file.read() + except: + raise ValueError('Cant encode CUE Sheet.') + + if self.content[0] == '\ufeff': + self.content = self.content[1:] + + header = header_parser() + + self.header = header[0] + + line_index = header[1] + + # we make room for tracks + tracks = [None for m in range(100)] + + while line_index < len(self.content.splitlines()): + parsed_track = track_parser(line_index) + line_index = parsed_track[2] + tracks[parsed_track[0]] = parsed_track[1] + + self.tracks = check_list(tracks, ignore=1) + + def get_meta(self): + content = '' + for i in ALBUM_META_ALBUM_BY_CUE: + if self.header.get(i): + content += i + '\t' + self.header[i] + '\n' + else: + content += i + '\t' + '\n' + + 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' + if self.tracks[i].get('title'): + content += 'track'+int_to_str(i) + 'title' + '\t' + self.tracks[i].get('title') + '\n' + return content + + def htoa(self): + '''Returns true if Hidden Track exists.''' + if int(self.tracks[1]['index'][1][-5:-3]) >= HTOA_LENGTH_TRIGGER: + return True + return False + + def breakpoints(self): + '''Returns track break points. Identical as CUETools' cuebreakpoints, with the exception of my standards for HTOA.''' + content = '' + for t in range(len(self.tracks)): + if t == 1 and not self.htoa(): + content += '' + elif t >= 1: + t_index = self.tracks[t]['index'] + content += t_index[1] + if (t < len(self.tracks) - 1): + content += '\n' + return content + +class MetaFile(File): + def __init__(self, path): + File.__init__(self, path) + with open(self.path) as meta_file: + self.rawcontent = meta_file.read() + + content = {} + content['tracks'] = [None for m in range(100)] + + for l in self.rawcontent.splitlines(): + parsed_line = re.search('^(.+?)\t(.+?)$', l) + if parsed_line: + if parsed_line.group(1)[:5] == 'track': + parsed_track = re.search('^track(\d\d)(.+?)$', parsed_line.group(1)) + if not parsed_track: + 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) + else: + content[parsed_line.group(1)] = parsed_line.group(2) + + content['tracks'] = check_list(content['tracks'], ignore=1) + + self.content = content + + def flac_tags(self, track_nr): + common_tags = dict() + freeform_tags = dict() + + # common flac tags + common_tags['artist'] = self.content['artist'] + common_tags['album'] = self.content['title'] + common_tags['title'] = self.content['tracks'][track_nr]['title'] + common_tags['date'] = self.content['date'] + common_tags['tracknumber'] = str(track_nr) + common_tags['genre'] = meta.content['genre'] + common_tags['tracktotal'] = str(len(self.content['tracks'])-1) + + #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'] + ')' + return artist, album + + def complete(self): + '''Check MetaFile for containing all data''' + self.__init__(self.path) + for l in self.rawcontent.splitlines(): + if re.search('^[0-9A-Za-z]+?\t$', l): + return False + return True + + def count_tracks(self): + '''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) + + self.track_nr = track_nr + self.type = WAVE_FILE_TYPE_BY_EXTENSION[self.name_ext] + + def filename(self, ext=None, cmd=False): + title = meta.content['tracks'][self.track_nr]['title'] + + if ext: + if ext[0] != '.': + ext = '.' + ext + else: + ext = self.name_ext + + f_name = int_to_str(self.track_nr) + ' - ' + title + ext + + if cmd: + f_name = f_name.replace(' ', '\ ') + + f_name = f_name.replace('!', '') + f_name = f_name.replace('?', '') + f_name = f_name.replace('/', ';') + + return f_name + + def tag(self): + if self.type == 'Free Lossless Audio Codec': + f = FLAC(self.name) + tags = meta.flac_tags(self.track_nr) + for t in tags[0]: + f[t] = tags[0][t] + f.save() + + def mutagen(self): + if self.type == 'Free Lossless Audio Codec': + return FLAC(self.name) + +def split(albumpath): + + os.chdir(albumpath) + base_dir = Directory(os.getcwd()) + cue = None + wave = None + + # determining correct cue file + # if perfect match found + for _cue in base_dir.filter('CueFile'): + for _wave in base_dir.filter('WaveFile'): + if _cue.header['file'] == _wave.name: + logger.info('CUE Sheet found: {0}'.format(_cue.name)) + logger.info('Music file found: {0}'.format(_wave.name)) + cue = _cue + wave = _wave + # if no perfect match found then try without extensions + if not cue and not wave: + logger.info('No match for music files, trying to match without extensions...') + for _cue in base_dir.filter('CueFile'): + for _wave in base_dir.filter('WaveFile'): + if ''.join(os.path.splitext(_cue.header['file'])[:-1]) == _wave.name_name: + logger.info('Possible CUE Sheet found: {0}'.format(_cue.name)) + logger.info('CUE Sheet refers music file: {0}'.format(_cue.header['file'])) + logger.info('Possible Music file found: {0}'.format(_wave.name)) + cue = _cue + wave = _wave + cue.header['file'] = wave.name + # if still no match then raise an exception + if not cue and not wave: + raise ValueError('No music file match found!') + + # Split with xld or shntool + splitter = 'shntool' + xldprofile = None + + # use xld profile to split cue + if headphones.ENCODER == 'xld' and headphones.MUSIC_ENCODER and headphones.XLDPROFILE: + import getXldProfile + xldprofile, xldformat, _ = getXldProfile.getXldProfile(headphones.XLDPROFILE) + if not xldformat: + raise ValueError('Details for xld profile "%s" not found, cannot split cue' % (xldprofile)) + else: + if headphones.ENCODERFOLDER: + splitter = os.path.join(headphones.ENCODERFOLDER, 'xld') + else: + splitter = 'xld' + # use standard xld command to split cue + elif sys.platform == 'darwin': + splitter = 'xld' + if not check_splitter(splitter): + splitter = 'shntool' + + if splitter == 'shntool' and not check_splitter(splitter): + raise ValueError('Command not found, ensure shntools with FLAC or xld (OS X) installed') + + # Determine if file can be split (only flac allowed for shntools) + if 'xld' in splitter and wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys() or \ + wave.type not in SHNTOOL_COMPATIBLE: + raise ValueError('Cannot split, audio file has unsupported extension') + + # generate temporary metafile describing the cue + if not base_dir.filter('MetaFile'): + with open(ALBUM_META_FILE_NAME, mode='w') as meta_file: + meta_file.write(cue.get_meta()) + base_dir.content.append(MetaFile(os.path.abspath(ALBUM_META_FILE_NAME))) + # check metafile for completeness + if not base_dir.filter('MetaFile'): + raise ValueError('Meta file {0} missing!'.format(ALBUM_META_FILE_NAME)) + else: + global meta + meta = base_dir.filter('MetaFile')[0] + + # Split with xld + if 'xld' in splitter: + cmd = [splitter] + cmd.extend([wave.name]) + cmd.extend(['-c']) + cmd.extend([cue.name]) + if xldprofile: + cmd.extend(['--profile']) + cmd.extend([xldprofile]) + else: + cmd.extend(['-f']) + cmd.extend(['flac']) + cmd.extend(['-o']) + cmd.extend([base_dir.path]) + split = split_baby(wave.name, cmd) + else: + # Split with shntool + with open(SPLIT_FILE_NAME, mode='w') as split_file: + split_file.write(cue.breakpoints()) + + cmd = ['shntool'] + cmd.extend(['split']) + cmd.extend(['-f']) + cmd.extend([SPLIT_FILE_NAME]) + cmd.extend(['-o']) + cmd.extend(['flac']) + cmd.extend([wave.name]) + split = split_baby(wave.name, cmd) + os.remove(SPLIT_FILE_NAME) + base_dir.update() + + # tag FLAC files + if split and meta.count_tracks() == len(base_dir.tracks(ext='.flac', split=True)): + for t in base_dir.tracks(ext='.flac', split=True): + logger.info('Tagging {0}...'.format(t.name)) + t.tag() + + # rename FLAC files + if split and meta.count_tracks() == len(base_dir.tracks(ext='.flac', split=True)): + for t in base_dir.tracks(ext='.flac', split=True): + if t.name != t.filename(): + logger.info('Renaming {0} to {1}...'.format(t.name, t.filename())) + os.rename(t.name, t.filename()) + + os.remove(ALBUM_META_FILE_NAME) + + if not split: + raise ValueError('Failed to split, check logs') + else: + # Rename original file + os.rename(wave.name, wave.name + '.original') + return True + + diff --git a/headphones/helpers.py b/headphones/helpers.py index 6b2fa078..c25fc5ec 100644 --- a/headphones/helpers.py +++ b/headphones/helpers.py @@ -222,7 +222,7 @@ def split_path(f): components = [] drive, path = os.path.splitdrive(f) - # Stip the folder from the path, iterate until nothing is left + # Strip the folder from the path, iterate until nothing is left while True: path, folder = os.path.split(path) @@ -315,18 +315,9 @@ def extract_data(s): s = s.replace('_', ' ') #headphones default format - pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s\[(?P.*?)\]', re.VERBOSE) + pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s[\[\(](?P.*?)[\]\)]', re.VERBOSE) match = pattern.match(s) - if match: - name = match.group("name") - album = match.group("album") - year = match.group("year") - return (name, album, year) - - #newzbin default format - pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s\((?P\d+?\))', re.VERBOSE) - match = pattern.match(s) if match: name = match.group("name") album = match.group("album") @@ -444,6 +435,75 @@ def extract_metadata(f): return (None, None, None) +def get_downloaded_track_list(albumpath): + """ + Return a list of audio files for the given directory. + """ + downloaded_track_list = [] + + for root, dirs, files in os.walk(albumpath): + for _file in files: + extension = os.path.splitext(_file)[1].lower()[1:] + if extension in headphones.MEDIA_FORMATS: + downloaded_track_list.append(os.path.join(root, _file)) + + return downloaded_track_list + +def preserve_torrent_direcory(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") + try: + shutil.copytree(albumpath, new_folder) + return new_folder + except Exception, e: + logger.warn("Cannot copy/move files to temp folder: " + \ + new_folder.decode(headphones.SYS_ENCODING, 'replace') + \ + ". 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. + """ + # Walk directory and scan all media files + count = 0 + cue_count = 0 + cue_dirs = [] + + for root, dirs, files in os.walk(albumpath): + for _file in files: + extension = os.path.splitext(_file)[1].lower()[1:] + if extension in headphones.MEDIA_FORMATS: + count += 1 + elif extension == 'cue': + cue_count += 1 + if root not in cue_dirs: + cue_dirs.append(root) + + # Split cue + if cue_count and cue_count >= count and cue_dirs: + + from headphones import logger, cuesplit + logger.info("Attempting to split audio files by cue") + + cwd = os.getcwd() + for cue_dir in cue_dirs: + try: + cuesplit.split(cue_dir) + except Exception, e: + os.chdir(cwd) + logger.warn("Cue not split: " + str(e)) + return False + + os.chdir(cwd) + return True + + 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) diff --git a/headphones/importer.py b/headphones/importer.py index 7d201353..3590772b 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -618,7 +618,7 @@ def addReleaseById(rid, rgid=None): newValueDict = {"ArtistID": release_dict['artist_id'], "ReleaseID": rgid, "ArtistName": release_dict['artist_name'], - "AlbumTitle": 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(), diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py old mode 100644 new mode 100755 index f11a8abc..ff89c300 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -178,66 +178,18 @@ def verify(albumid, albumpath, Kind=None, forced=False): logger.info("Looks like " + os.path.basename(albumpath).decode(headphones.SYS_ENCODING, 'replace') + " isn't complete yet. Will try again on the next run") return - - # use xld to split cue - - if headphones.CONFIG.ENCODER == 'xld' and headphones.CONFIG.MUSIC_ENCODER and downloaded_cuecount and downloaded_cuecount >= len(downloaded_track_list): - - import getXldProfile - - (xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE) - if not xldFormat: - logger.info(u'Details for xld profile "%s" not found, cannot split cue' % (xldProfile)) + # Split cue + if downloaded_cuecount and downloaded_cuecount >= len(downloaded_track_list): + if headphones.CONFIG.KEEP_TORRENT_FILES and Kind=="torrent": + albumpath = helpers.preserve_torrent_direcory(albumpath) + if albumpath and helpers.cue_split(albumpath): + downloaded_track_list = helpers.get_downloaded_track_list(albumpath) else: - if headphones.CONFIG.ENCODERFOLDER: - xldencoder = os.path.join(headphones.CONFIG.ENCODERFOLDER, 'xld') - else: - xldencoder = os.path.join('/Applications','xld') - - for r,d,f in os.walk(albumpath): - xldfolder = r - xldfile = '' - xldcue = '' - for file in f: - if any(file.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS) and not xldfile: - xldfile = os.path.join(r, file) - elif file.lower().endswith('.cue') and not xldcue: - xldcue = os.path.join(r, file) - - if xldfile and xldcue and xldfolder: - xldcmd = xldencoder - xldcmd = xldcmd + ' "' + xldfile + '"' - xldcmd = xldcmd + ' -c' - xldcmd = xldcmd + ' "' + xldcue + '"' - xldcmd = xldcmd + ' --profile' - xldcmd = xldcmd + ' "' + xldProfile + '"' - xldcmd = xldcmd + ' -o' - xldcmd = xldcmd + ' "' + xldfolder + '"' - logger.info(u"Cue found, splitting file " + xldfile.decode(headphones.SYS_ENCODING, 'replace')) - logger.debug(xldcmd) - os.system(xldcmd) - - # count files, should now be more than original if xld successfully split - - new_downloaded_track_list_count = 0 - for r,d,f in os.walk(albumpath): - for file in f: - if any(file.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): - new_downloaded_track_list_count += 1 - - if new_downloaded_track_list_count > len(downloaded_track_list): - - # rename original unsplit files - for downloaded_track in downloaded_track_list: - os.rename(downloaded_track, downloaded_track + '.original') - - #reload - - downloaded_track_list = [] - for r,d,f in os.walk(albumpath): - for file in f: - if any(file.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): - downloaded_track_list.append(os.path.join(r, file)) + myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid]) + processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath) + if not processed: + renameUnprocessedFolder(albumpath) + return # test #1: metadata - usually works logger.debug('Verifying metadata...') @@ -328,7 +280,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle'])) # Check to see if we're preserving the torrent dir - if headphones.CONFIG.KEEP_TORRENT_FILES and Kind=="torrent": + if headphones.CONFIG.KEEP_TORRENT_FILES and Kind=="torrent" and 'headphones-modified' not in albumpath: 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: @@ -343,14 +295,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, # Could probably just throw in the "headphones-modified" folder, # but this is good to make sure we're not counting files that may have failed to move downloaded_track_list = [] - downloaded_cuecount = 0 for r,d,f in os.walk(albumpath): for files in f: if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): downloaded_track_list.append(os.path.join(r, files)) - elif files.lower().endswith('.cue'): - downloaded_cuecount += 1 # Check if files are valid media files and are writeable, before the steps # below are executed. This simplifies errors and prevents unfinished steps. @@ -1179,6 +1128,13 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None): except Exception as e: name = album = year = None + # Check if there's a cue to split + if not name and not album and helpers.cue_split(folder): + try: + name, album, year = helpers.extract_metadata(folder) + except Exception as e: + 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() if release: diff --git a/headphones/webserve.py b/headphones/webserve.py index f561c603..a225c767 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -185,6 +185,9 @@ class WebInterface(object): 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() + c.remove_from_cache(AlbumID=album['AlbumID']) importer.finalize_update(ArtistID, ArtistName) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) removeExtras.exposed = True @@ -207,7 +210,7 @@ class WebInterface(object): raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) resumeArtist.exposed = True - def deleteArtist(self, ArtistID): + def removeArtist(self, ArtistID): logger.info(u"Deleting all traces of artist: " + ArtistID) myDB = db.DBConnection() namecheck = myDB.select('SELECT ArtistName from artists where ArtistID=?', [ArtistID]) @@ -215,23 +218,29 @@ class WebInterface(object): artistname=name['ArtistName'] myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID]) - rgids = myDB.select('SELECT DISTINCT ReleaseGroupID FROM albums JOIN releases ON AlbumID = ReleaseGroupID WHERE ArtistID=?', [ArtistID]) + 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]) for rgid in rgids: - myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']]) - myDB.action('DELETE from have WHERE Matched=?', [rgid['ReleaseGroupID']]) + albumid = rgid['AlbumID'] + myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [albumid]) + myDB.action('DELETE from have WHERE Matched=?', [albumid]) + c.remove_from_cache(AlbumID=albumid) + myDB.action('DELETE from descriptions WHERE ReleaseGroupID=?', [albumid]) myDB.action('DELETE from albums WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from tracks WHERE ArtistID=?', [ArtistID]) - rgids = myDB.select('SELECT DISTINCT ReleaseGroupID FROM allalbums JOIN releases ON AlbumID = ReleaseGroupID WHERE ArtistID=?', [ArtistID]) - for rgid in rgids: - myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']]) - myDB.action('DELETE from have WHERE Matched=?', [rgid['ReleaseGroupID']]) - myDB.action('DELETE from allalbums WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from alltracks WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from have WHERE ArtistName=?', [artistname]) + c.remove_from_cache(ArtistID=ArtistID) + myDB.action('DELETE from descriptions WHERE ArtistID=?', [ArtistID]) myDB.action('INSERT OR REPLACE into blacklist VALUES (?)', [ArtistID]) + + def deleteArtist(self, ArtistID): + self.removeArtist(ArtistID) raise cherrypy.HTTPRedirect("home") deleteArtist.exposed = True @@ -240,23 +249,7 @@ class WebInterface(object): myDB = db.DBConnection() emptyArtistIDs = [row['ArtistID'] for row in myDB.select("SELECT ArtistID FROM artists WHERE LatestAlbum IS NULL")] for ArtistID in emptyArtistIDs: - logger.info(u"Deleting all traces of artist: " + ArtistID) - myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID]) - - rgids = myDB.select('SELECT DISTINCT ReleaseGroupID FROM albums JOIN releases ON AlbumID = ReleaseGroupID WHERE ArtistID=?', [ArtistID]) - for rgid in rgids: - myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']]) - - myDB.action('DELETE from albums WHERE ArtistID=?', [ArtistID]) - myDB.action('DELETE from tracks WHERE ArtistID=?', [ArtistID]) - - rgids = myDB.select('SELECT DISTINCT ReleaseGroupID FROM allalbums JOIN releases ON AlbumID = ReleaseGroupID WHERE ArtistID=?', [ArtistID]) - for rgid in rgids: - myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']]) - - myDB.action('DELETE from allalbums WHERE ArtistID=?', [ArtistID]) - myDB.action('DELETE from alltracks WHERE ArtistID=?', [ArtistID]) - myDB.action('INSERT OR REPLACE into blacklist VALUES (?)', [ArtistID]) + self.removeArtist(ArtistID) deleteEmptyArtists.exposed = True def refreshArtist(self, ArtistID): @@ -388,6 +381,12 @@ class WebInterface(object): myDB.action('DELETE from allalbums WHERE AlbumID=?', [AlbumID]) myDB.action('DELETE from alltracks WHERE AlbumID=?', [AlbumID]) myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [AlbumID]) + myDB.action('DELETE from descriptions WHERE ReleaseGroupID=?', [AlbumID]) + + from headphones import cache + c = cache.Cache() + c.remove_from_cache(AlbumID=AlbumID) + if ArtistID: raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) else: @@ -641,22 +640,7 @@ class WebInterface(object): artistsToAdd = [] for ArtistID in args: if action == 'delete': - myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID]) - - rgids = myDB.select('SELECT DISTINCT ReleaseGroupID FROM albums JOIN releases ON AlbumID = ReleaseGroupID WHERE ArtistID=?', [ArtistID]) - for rgid in rgids: - myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']]) - - myDB.action('DELETE from albums WHERE ArtistID=?', [ArtistID]) - myDB.action('DELETE from tracks WHERE ArtistID=?', [ArtistID]) - - rgids = myDB.select('SELECT DISTINCT ReleaseGroupID FROM allalbums JOIN releases ON AlbumID = ReleaseGroupID WHERE ArtistID=?', [ArtistID]) - for rgid in rgids: - myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']]) - - myDB.action('DELETE from allalbums WHERE ArtistID=?', [ArtistID]) - myDB.action('DELETE from alltracks WHERE ArtistID=?', [ArtistID]) - myDB.action('INSERT OR REPLACE into blacklist VALUES (?)', [ArtistID]) + self.removeArtist(ArtistID) elif action == 'pause': controlValueDict = {'ArtistID': ArtistID} newValueDict = {'Status': 'Paused'}