diff --git a/jbripper.py b/jbripper.py index a56c605..1b81b60 100755 --- a/jbripper.py +++ b/jbripper.py @@ -7,85 +7,314 @@ import os, sys import threading import time +import pprint +from eyed3 import id3 +import eyed3 + +reload(sys) +sys.setdefaultencoding('utf8') + playback = False # set if you want to listen to the tracks that are currently ripped (start with "padsp ./jbripper.py ..." if using pulse audio) -rawpcm = False # also saves a .pcm file with the raw PCM data as delivered by libspotify () +wav = True # also saves a .pcm file with the raw PCM data as delivered by libspotify () +aac = True +mp3 = True +alac = True +fileNameMaxSize=255 # your filesystem's maximum filename size. Linux' Ext4 is 255. filename/filename/filename +defaultgenre = u'☣ UNKNOWN ♺' pcmfile = None pipe = None ripping = False +size = 0 +feedbackchar = "-" +feedbackcharDelay = 0 end_of_track = threading.Event() def printstr(str): # print without newline sys.stdout.write(str) sys.stdout.flush() -def shell(cmdline): # execute shell commands (unicode support) - call(cmdline, shell=True) +def transliterate(str): + transliterated=str + transliterated=transliterated.replace('/',u'/') + transliterated=transliterated.replace('*',u'✱') + transliterated=transliterated.replace('#',u'♯') + transliterated=transliterated.replace(':',u'∶') + transliterated=transliterated.replace('?',u'⁇') + transliterated=transliterated.replace('\\',u'\') + transliterated=transliterated.replace('|',u'│') + transliterated=transliterated.replace('>',u'>') + transliterated=transliterated.replace('<',u'<') + transliterated=transliterated.replace('&',u'&') + return transliterated + + +def unicode_truncate(s, length, encoding='utf-8'): + encoded = s.encode(encoding)[:length] + return encoded.decode(encoding, 'ignore') + +def track_path(track, format): + global fileNameMaxSize + + oalbum=track.album() + num_track = track.index() + year=oalbum.year() + album_artist=transliterate(oalbum.artist().name()) + track_artist=transliterate(u' • '.join([str(x.name()) for x in track.artists()])) + track_name=transliterate(track.name()) + album_name=transliterate(oalbum.name()) + + if (album_artist == track_artist): + track_file="{:02d} {}".format(num_track, track_name) + else: + track_file="{:02d} {} ♫ {}".format(num_track, track_artist, track_name) + + return "{aartist}/{year:04d} • {album}/{ff}/{file}".format( + year=year, ff=format, + aartist = unicode_truncate(album_artist, fileNameMaxSize), + album = unicode_truncate(album_name, fileNameMaxSize-4-2-3), + file = unicode_truncate(track_file, fileNameMaxSize-4)) + + +# Setup all pipes def rip_init(session, track): - global pipe, ripping, pcmfile, rawpcm - num_track = "%02d" % (track.index(),) - mp3file = track.name()+".mp3" - pcmfile = track.name()+".pcm" - directory = os.getcwd() + "/" + track.artists()[0].name() + "/" + track.album().name() + "/" - if not os.path.exists(directory): - os.makedirs(directory) - printstr("ripping " + mp3file + " ...") - p = Popen("lame --silent -V0 -h -r - \""+ directory + mp3file+"\"", stdin=PIPE, shell=True) - pipe = p.stdin - if rawpcm: - pcmfile = open(directory + pcmfile, 'w') + global pipe, ripping, wpipe, size, defaultgenre + + size = 0 + + + pipe = [] + + if mp3: + file_prefix = track_path(track,"mp3") + directory = os.path.dirname(file_prefix) + + if not os.path.exists(directory): + os.makedirs(directory) + + printstr("ripping " + file_prefix + ".mp3 ...\n") + mp3Pipe = Popen(["lame", + "--silent", + "-V2", # VBR slightly less than highest quality + "-m", "s", # plain stereo (no joint stereo) + "-h", # high quality, same as -q 2 + "-r", # input is raw PCM + "--id3v2-utf16", + "--id3v2-only", + "--tg", str(defaultgenre), + "--tt", str(track.name()), + "--ta", u' • '.join([str(x.name()) for x in track.artists()]), + "--tl", str(track.album()), + "--ty", str(track.album().year()), + "--tn", str("%02d" % (track.index(),)), + "--tc", "Spotify PCM + 'lame -V2 -m s -h'", + "-", file_prefix + ".mp3"], + stdin=PIPE) + + pipe.append(mp3Pipe.stdin) + + if wav: + file_prefix = track_path(track,"wav") + directory = os.path.dirname(file_prefix) + + if not os.path.exists(directory): + os.makedirs(directory) + + printstr("ripping " + file_prefix + ".wav ...\n") + + wavPipe=Popen(["ffmpeg", + "-loglevel", "quiet", + "-f", "s16le", + "-ar", "44100", + "-ac", "2", + "-i", "-", + file_prefix + ".wav"], + stdin=PIPE) + + pipe.append(wavPipe.stdin) + + if alac: + file_prefix = track_path(track,"alac") + directory = os.path.dirname(file_prefix) + + if not os.path.exists(directory): + os.makedirs(directory) + + printstr("ripping " + file_prefix + ".m4a (ALAC) ...\n") + + alacPipe=Popen(["ffmpeg", + "-loglevel", "quiet", + "-y", + "-f", "s16le", + "-ar", "44100", + "-ac", "2", + "-i", "-", + + "-acodec", "alac", + + "-metadata", "genre=" + str(defaultgenre), + "-metadata", "title=" + str(track.name()), + "-metadata", "artist=" + u' • '.join([str(x.name()) for x in track.artists()]), + "-metadata", "album=" + str(track.album()), + "-metadata", "album_artist=" + str(track.album().artist().name()), + "-metadata", "date=" + str(track.album().year()), + "-metadata", "track=" + str("%02d" % (track.index(),)), + "-metadata", "encoder=" + "Spotify PCM + 'ffmpeg -acodec alac'", + + file_prefix + ".m4a"], + stdin=PIPE) + + pipe.append(alacPipe.stdin) + ripping = True def rip_terminate(session, track): - global ripping, pipe, pcmfile, rawpcm + global ripping, pipe, defaultgenre + if pipe is not None: + for p in pipe: + p.close() print(' done!') - pipe.close() - if rawpcm: - pcmfile.close() + + + if aac: + wfile_prefix = track_path(track,"wav") + file_prefix = track_path(track,"aac") + directory = os.path.dirname(file_prefix) + + if not os.path.exists(directory): + os.makedirs(directory) + + printstr("ripping " + file_prefix + ".m4a ...\n") + Popen(["faac", + "-w", + "-s", + "-q", str("300"), + "--genre", str(defaultgenre), + "--title", str(track.name()), + "--artist", u' • '.join([str(x.name()) for x in track.artists()]), + "--album", str(track.album()), + "--year", str(track.album().year()), + "--track", str("%02d" % (track.index(),)), +# "--cover-art", str(directory + "/folder.jpg"), + "--comment", "Spotify PCM + 'faac -q 300'", + "-o", file_prefix + ".m4a", + wfile_prefix + ".wav"]) + ripping = False + +# This callback is called for each frame of each track def rip(session, frames, frame_size, num_frames, sample_type, sample_rate, channels): - if ripping: - printstr('.') - pipe.write(frames); - if rawpcm: - pcmfile.write(frames) + global size, feedbackchar, feedbackcharDelay + + sys.stdout.write('\r' + feedbackchar) + sys.stdout.flush() + + if feedbackcharDelay > 5: + feedbackcharDelay = 0 + if feedbackchar == '-': + feedbackchar='\\' + elif feedbackchar == '\\': + feedbackchar='|' + elif feedbackchar == '|': + feedbackchar='/' + elif feedbackchar == '/': + feedbackchar='-' + + feedbackcharDelay += 1 + +# printstr('.') +# printstr("frame_size={}, num_frames={}, sample_type={}, sample_rate={}, channels={}\n".format( +# frame_size, num_frames, sample_type, sample_rate, channels)) + +# pprint.pprint(pipe) + + for p in pipe: + p.write(frames) + +# if ripping: +# pipe.write(frames); +# # printstr(" " + size + " bytes\r") +# if wav: +# wpipe.write(frames) def rip_id3(session, track): # write ID3 data - num_track = "%02d" % (track.index(),) - mp3file = track.name()+".mp3" - artist = track.artists()[0].name() - album = track.album().name() - title = track.name() - year = track.album().year() - directory = os.getcwd() + "/" + track.artists()[0].name() + "/" + track.album().name() + "/" + file_prefix = track_path(track) + mp3file = file_prefix+".mp3" + directory = os.path.dirname(file_prefix) # download cover image = session.image_create(track.album().cover()) + while not image.is_loaded(): # does not work from MainThread! time.sleep(0.1) - fh_cover = open('cover.jpg','wb') + + fh_cover = open(directory + '/folder.jpg','wb') fh_cover.write(image.data()) fh_cover.close() + + oalbum=track.album() + num_track = str("%02d" % (track.index(),)) + year=str(oalbum.year()) + album_artist=transliterate(oalbum.artist().name()) + track_artist=u' • '.join([str(x.name()) for x in track.artists()]) + track_name=track.name() + album_name=oalbum.name() + +# spotify_links="Spotify track: {0}\nSpotify album: {1}".format( +# track.link().url, +# oalbum.link().url +# ) + # write id3 data - cmd = "eyeD3" + \ - " --add-image cover.jpg:FRONT_COVER" + \ - " -t \"" + title + "\"" + \ - " -a \"" + artist + "\"" + \ - " -A \"" + album + "\"" + \ - " -n " + str(num_track) + \ - " -Y " + str(year) + \ - " -Q " + \ - " \"" + directory + mp3file + "\"" - shell(cmd) - - # delete cover - shell("rm -f cover.jpg") +# call(["eyeD3", +# "--add-image", directory + "/folder.jpg:FRONT_COVER", +# "-t", track_name, +# "-a", track_artist, +# "-b", album_artist, +# # "-c", spotify_link, +# "-A", album_name, +# "-n", num_track, +# "-Y", year, +# "--to-v2.3", +# "-Q", +# mp3file +# ]) + + +# id3.ID3_DEFAULT_VERSION = (2, 3, 0) + + audiofile = eyed3.load(mp3file) + audiofile.initTag() + + audiofile.tag.album_artist = album_artist + audiofile.tag.album = album_name + audiofile.tag.release_date = year + audiofile.tag.recording_date = year + audiofile.tag.original_release_date = year + audiofile.tag.artist = track_artist + audiofile.tag.track_num = (num_track, None) + audiofile.tag.title = track_name + audiofile.tag.genre = defaultgenre +# audiofile.tag.comments.set( spotify_links) + + # append image to tags +# audiofile.tag.images.set(3,image.data(), +# u'image/jpeg', +# u'Front Cover' +# ) + + # Save ID3 using version 2.3 for maximum compatibility. UTF-16 is required for 2.3 + audiofile.tag.save(version=(2, 3, 0), encoding='utf16') + + + + class RipperThread(threading.Thread): def __init__(self, ripper): @@ -124,7 +353,7 @@ def run(self): end_of_track.clear() # TODO check if necessary rip_terminate(session, track) - rip_id3(session, track) + # rip_id3(session, track) self.ripper.disconnect()