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'}