Merge remote-tracking branch 'upstream/develop' into feature/refactor_config

Conflicts:
	headphones/postprocessor.py
This commit is contained in:
Jesse Mullan
2014-10-25 19:14:14 -07:00
7 changed files with 825 additions and 120 deletions

View File

@@ -14,7 +14,7 @@
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
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'],

View File

@@ -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

664
headphones/cuesplit.py Executable file
View File

@@ -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 <http://www.gnu.org/licenses/>.
# 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

View File

@@ -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<name>.*?)\s\-\s(?P<album>.*?)\s\[(?P<year>.*?)\]', re.VERBOSE)
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s[\[\(](?P<year>.*?)[\]\)]', 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<name>.*?)\s\-\s(?P<album>.*?)\s\((?P<year>\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<timestamp>.*?)\s\-\s(?P<level>.*?)\s*\:\:\s(?P<thread>.*?)\s\:\s(?P<message>.*)', re.VERBOSE)

View File

@@ -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(),

82
headphones/postprocessor.py Normal file → Executable file
View File

@@ -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:

View File

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