From 608ccb2c0a5081a85d90d25f1017adcb9fce8590 Mon Sep 17 00:00:00 2001 From: Ade Date: Thu, 25 Oct 2012 11:19:18 +1300 Subject: [PATCH] XLD encoding + bonus post processing xld cue splitting. Currently uses Advanced Encoding Options to determine xld and the xld profile to use, e.g xld Apple Lossless. Would like to use congig instead but can't figure out how to do it --- headphones/getXldProfile.py | 181 ++++++++++++++++++++++++++++++++++++ headphones/music_encoder.py | 65 +++++++++++-- headphones/postprocessor.py | 77 ++++++++++++++- 3 files changed, 314 insertions(+), 9 deletions(-) create mode 100755 headphones/getXldProfile.py diff --git a/headphones/getXldProfile.py b/headphones/getXldProfile.py new file mode 100755 index 00000000..e9d27015 --- /dev/null +++ b/headphones/getXldProfile.py @@ -0,0 +1,181 @@ +import os.path +import plistlib +import sys +import xml.parsers.expat as expat +import commands +from headphones import logger + +def getXldProfile(xldProfile): + xldProfileNotFound = xldProfile + expandedPath = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist') + try: + preferences = plistlib.Plist.fromFile(expandedPath) + except (expat.ExpatError): + os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath ) + try: + preferences = plistlib.Plist.fromFile(expandedPath) + except (ImportError): + os.system("/usr/bin/plutil -convert binary1 %s" % expandedPath ) + logger.info('The plist at "%s" has a date in it, and therefore is not useable.' % expandedPath) + return(xldProfileNotFound, None, None) + except (ImportError): + logger.info('The plist at "%s" has a date in it, and therefore is not useable.' % expandedPath) + except: + logger.info('Unexpected error:', sys.exc_info()[0]) + return(xldProfileNotFound, None, None) + + xldProfile = xldProfile.lower() + profiles = preferences.get('Profiles') + + for profile in profiles: + + profilename = profile.get('XLDProfileManager_ProfileName') + xldProfileForCmd = profilename + profilename = profilename.lower() + xldFormat = None + xldBitrate = None + + if profilename == xldProfile: + + OutputFormatName = profile.get('OutputFormatName') + ShortDesc = profile.get('ShortDesc') + + # Determine format and bitrate + + if OutputFormatName == 'WAV': + xldFormat = 'wav' + + elif OutputFormatName == 'AIFF': + xldFormat = 'aiff' + + elif 'PCM' in OutputFormatName: + xldFormat = 'pcm' + + elif OutputFormatName == 'Wave64': + xldFormat = 'w64' + + elif OutputFormatName == 'MPEG-4 AAC': + xldFormat = 'm4a' + if 'CBR' in ShortDesc or 'ABR' in ShortDesc or 'CVBR' in ShortDesc: + xldBitrate = int(profile.get('XLDAacOutput2_Bitrate')) + elif 'TVBR' in ShortDesc: + XLDAacOutput2_VBRQuality = int(profile.get('XLDAacOutput2_VBRQuality')) + if XLDAacOutput2_VBRQuality > 122: + xldBitrate = 320 + elif XLDAacOutput2_VBRQuality > 113 and XLDAacOutput2_VBRQuality <= 122: + xldBitrate = 285 + elif XLDAacOutput2_VBRQuality > 104 and XLDAacOutput2_VBRQuality <= 113: + xldBitrate = 255 + elif XLDAacOutput2_VBRQuality > 95 and XLDAacOutput2_VBRQuality <= 104: + xldBitrate = 225 + elif XLDAacOutput2_VBRQuality > 86 and XLDAacOutput2_VBRQuality <= 95: + xldBitrate = 195 + elif XLDAacOutput2_VBRQuality > 77 and XLDAacOutput2_VBRQuality <= 86: + xldBitrate = 165 + elif XLDAacOutput2_VBRQuality > 68 and XLDAacOutput2_VBRQuality <= 77: + xldBitrate = 150 + elif XLDAacOutput2_VBRQuality > 58 and XLDAacOutput2_VBRQuality <= 68: + xldBitrate = 135 + elif XLDAacOutput2_VBRQuality > 49 and XLDAacOutput2_VBRQuality <= 58: + xldBitrate = 115 + elif XLDAacOutput2_VBRQuality > 40 and XLDAacOutput2_VBRQuality <= 49: + xldBitrate = 105 + elif XLDAacOutput2_VBRQuality > 31 and XLDAacOutput2_VBRQuality <= 40: + xldBitrate = 95 + elif XLDAacOutput2_VBRQuality > 22 and XLDAacOutput2_VBRQuality <= 31: + xldBitrate = 80 + elif XLDAacOutput2_VBRQuality > 13 and XLDAacOutput2_VBRQuality <= 22: + xldBitrate = 75 + elif XLDAacOutput2_VBRQuality > 4 and XLDAacOutput2_VBRQuality <= 13: + xldBitrate = 45 + elif XLDAacOutput2_VBRQuality >= 0 and XLDAacOutput2_VBRQuality <= 4: + xldBitrate = 40 + + elif OutputFormatName == 'Apple Lossless': + xldFormat = 'm4a' + + elif OutputFormatName == 'FLAC': + if 'ogg' in ShortDesc: + xldFormat = 'oga' + else: + xldFormat = 'flac' + + elif OutputFormatName == 'MPEG-4 HE-AAC': + xldFormat = 'm4a' + xldBitrate = int(profile.get('Bitrate')) + + elif OutputFormatName == 'LAME MP3': + xldFormat = 'mp3' + if 'VBR' in ShortDesc: + VbrQuality = float(profile.get('VbrQuality')) + if VbrQuality < 1: + xldBitrate = 260 + elif VbrQuality >= 1 and VbrQuality < 2: + xldBitrate = 250 + elif VbrQuality >= 2 and VbrQuality < 3: + xldBitrate = 210 + elif VbrQuality >= 3 and VbrQuality < 4: + xldBitrate = 195 + elif VbrQuality >= 4 and VbrQuality < 5: + xldBitrate = 185 + elif VbrQuality >= 5 and VbrQuality < 6: + xldBitrate = 150 + elif VbrQuality >= 6 and VbrQuality < 7: + xldBitrate = 130 + elif VbrQuality >= 7 and VbrQuality < 8: + xldBitrate = 120 + elif VbrQuality >= 8 and VbrQuality < 9: + xldBitrate = 105 + elif VbrQuality >= 9: + xldBitrate = 85 + elif 'CBR' in ShortDesc: + xldBitrate = int(profile.get('Bitrate')) + elif 'ABR' in ShortDesc: + xldBitrate = int(profile.get('AbrBitrate')) + + elif OutputFormatName == 'Opus': + xldFormat = 'opus' + xldBitrate = int(profile.get('XLDOpusOutput_Bitrate')) + + elif OutputFormatName == 'Ogg Vorbis': + xldFormat = 'ogg' + XLDVorbisOutput_Quality = float(profile.get('XLDVorbisOutput_Quality')) + if XLDVorbisOutput_Quality <= -2: + xldBitrate = 32 + elif XLDVorbisOutput_Quality > -2 and XLDVorbisOutput_Quality <= -1: + xldBitrate = 48 + elif XLDVorbisOutput_Quality > -1 and XLDVorbisOutput_Quality <= 0: + xldBitrate = 64 + elif XLDVorbisOutput_Quality > 0 and XLDVorbisOutput_Quality <= 1: + xldBitrate = 80 + elif XLDVorbisOutput_Quality > 1 and XLDVorbisOutput_Quality <= 2: + xldBitrate = 96 + elif XLDVorbisOutput_Quality > 2 and XLDVorbisOutput_Quality <= 3: + xldBitrate = 112 + elif XLDVorbisOutput_Quality > 3 and XLDVorbisOutput_Quality <= 4: + xldBitrate = 128 + elif XLDVorbisOutput_Quality > 4 and XLDVorbisOutput_Quality <= 5: + xldBitrate = 160 + elif XLDVorbisOutput_Quality > 5 and XLDVorbisOutput_Quality <= 6: + xldBitrate = 192 + elif XLDVorbisOutput_Quality > 6 and XLDVorbisOutput_Quality <= 7: + xldBitrate = 224 + elif XLDVorbisOutput_Quality > 7 and XLDVorbisOutput_Quality <= 8: + xldBitrate = 256 + elif XLDVorbisOutput_Quality > 8 and XLDVorbisOutput_Quality <= 9: + xldBitrate = 320 + elif XLDVorbisOutput_Quality > 9: + xldBitrate = 500 + + elif OutputFormatName == 'WavPack': + xldFormat = 'wv' + if ShortDesc != 'normal': + xldBitrate = int(profile.get('XLDWavpackOutput_BitRate')) + + # Lossless + if xldFormat and not xldBitrate: + xldBitrate = 500 + + return(xldProfileForCmd, xldFormat, xldBitrate) + + return(xldProfileNotFound, None, None) \ No newline at end of file diff --git a/headphones/music_encoder.py b/headphones/music_encoder.py index 04dec8f5..8a66228e 100644 --- a/headphones/music_encoder.py +++ b/headphones/music_encoder.py @@ -27,7 +27,26 @@ try: except ImportError: import lib.argparse as argparse +# xld + +if headphones.ADVANCEDENCODER.lower().startswith('xld'): + XLDPROFILE = headphones.ADVANCEDENCODER[4:] + import getXldProfile + XLD = True +else: + XLD = False + def encode(albumPath): + + # Return if xld details not found + + if XLD: + global xldProfile + (xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(XLDPROFILE) + if not xldFormat: + logger.error(u'Details for xld profile "%s" not found, will not be reencoded' % (xldProfile)) + return None + tempDirEncode=os.path.join(albumPath,"temp") musicFiles=[] musicFinalFiles=[] @@ -46,26 +65,48 @@ def encode(albumPath): for r,d,f in os.walk(albumPath): for music in f: if any(music.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): + + if not XLD: + encoderFormat = headphones.ENCODEROUTPUTFORMAT.encode(headphones.SYS_ENCODING) + else: + xldMusicFile = os.path.join(r, music) + xldInfoMusic = MediaFile(xldMusicFile) + encoderFormat = xldFormat + if (headphones.ENCODERLOSSLESS): - if (music.lower().endswith('.flac')): + ext = os.path.normpath(os.path.splitext(music)[1].lstrip(".")).lower() + if not XLD and ext == 'flac' or XLD and (ext != xldFormat and (xldInfoMusic.bitrate / 1000 > 500)): musicFiles.append(os.path.join(r, music)) - musicTemp = os.path.normpath(os.path.splitext(music)[0]+'.'+headphones.ENCODEROUTPUTFORMAT.encode(headphones.SYS_ENCODING)) + musicTemp = os.path.normpath(os.path.splitext(music)[0] + '.' + encoderFormat) musicTempFiles.append(os.path.join(tempDirEncode, musicTemp)) else: logger.debug('Music "%s" is already encoded' % (music)) else: musicFiles.append(os.path.join(r, music)) - musicTemp = os.path.normpath(os.path.splitext(music)[0]+'.'+headphones.ENCODEROUTPUTFORMAT.encode(headphones.SYS_ENCODING)) + musicTemp = os.path.normpath(os.path.splitext(music)[0] + '.' + encoderFormat) musicTempFiles.append(os.path.join(tempDirEncode, musicTemp)) - - if headphones.ENCODER=='lame': + + if XLD: + if headphones.ENCODERFOLDER: + encoder = os.path.join(headphones.ENCODERFOLDER.encode(headphones.SYS_ENCODING), 'xld') + else: + encoder = os.path.join('/Applications', 'xld') + elif headphones.ENCODER=='lame': encoder=os.path.join(headphones.ENCODERFOLDER.encode(headphones.SYS_ENCODING),'lame') elif headphones.ENCODER=='ffmpeg': encoder=os.path.join(headphones.ENCODERFOLDER.encode(headphones.SYS_ENCODING),'ffmpeg') + i=0 for music in musicFiles: infoMusic=MediaFile(music) - if headphones.ENCODER == 'lame': + + if XLD: + if xldBitrate and (infoMusic.bitrate / 1000 <= xldBitrate): + logger.info('Music "%s" has bitrate <= "%skbit", will not be reencoded' % (music.decode(headphones.SYS_ENCODING, 'replace'), xldBitrate)) + else: + command(encoder,music,musicTempFiles[i],albumPath) + ifencoded=1 + elif headphones.ENCODER == 'lame': if not any(music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + x) for x in ["mp3", "wav"]): logger.warn(u'Lame cant encode "%s" format for "%s", use ffmpeg' % (os.path.splitext(music)[1].decode(headphones.SYS_ENCODING, 'replace'),music.decode(headphones.SYS_ENCODING, 'replace'))) else: @@ -106,7 +147,17 @@ def command(encoder,musicSource,musicDest,albumPath): return_code=1 cmd='' startMusicTime=time.time() - if headphones.ENCODER == 'lame': + + if XLD: + xldDestDir = os.path.split(musicDest)[0] + cmd = encoder + cmd = cmd + ' "' + musicSource + '"' + cmd = cmd + ' --profile' + cmd = cmd + ' "' + xldProfile + '"' + cmd = cmd + ' -o' + cmd = cmd + ' "' + xldDestDir + '"' + + elif headphones.ENCODER == 'lame': if headphones.ADVANCEDENCODER =='': cmd=encoder + ' -h' if headphones.ENCODERVBRCBR=='cbr': diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index ecabb271..5f79009b 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -28,6 +28,15 @@ from lib.beets.mediafile import MediaFile import headphones from headphones import db, albumart, librarysync, lyrics, logger, helpers +# xld + +if headphones.ADVANCEDENCODER.lower().startswith('xld'): + XLDPROFILE = headphones.ADVANCEDENCODER[4:] + import getXldProfile + XLD = True +else: + XLD = False + postprocessor_lock = threading.Lock() def checkFolder(): @@ -147,12 +156,73 @@ def verify(albumid, albumpath): tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid]) 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 + # use xld to split cue + + if XLD and headphones.MUSIC_ENCODER and downloaded_cuecount and downloaded_cuecount >= len(downloaded_track_list): + + (xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(XLDPROFILE) + if not xldFormat: + logger.info(u'Details for xld profile "%s" not found, cannot split cue' % (xldProfile)) + else: + if headphones.ENCODERFOLDER: + xldencoder = os.path.join(headphones.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)) + # test #1: metadata - usually works logger.debug('Verifying metadata...') @@ -233,9 +303,12 @@ def verify(albumid, albumpath): def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list): logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle'])) - #start enconding + #start encoding if headphones.MUSIC_ENCODER: downloaded_track_list=music_encoder.encode(albumpath) + + if not downloaded_track_list: + return album_art_path = albumart.getAlbumArt(albumid)