From 998458ce5e601b0897a422e5cf4685d1608e44fa Mon Sep 17 00:00:00 2001 From: Miquel Adrover Date: Tue, 21 Jan 2014 17:44:59 +0100 Subject: [PATCH 1/5] New features: - Added extended options --playback --mp3 quality --library --otuput format - Check for existing songs in library --- README.md | 63 ++++++++++---------- jbripper.py | 168 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 170 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 8f55330..7d3ec9d 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,45 @@ spotifyripper ============= -small ripper script for spotify (rips playlists to mp3 and includes ID3 tags) +small ripper script for spotify (rips playlists to mp3 and includes ID3 tags and album covers) note that stream ripping violates the ToC's of libspotify! -usage ------ - ./jbripper.py [username] [password] [spotify_url] - -examples --------- - "./jbripper.py user pass spotify:track:52xaypL0Kjzk0ngwv3oBPR" creates "Beat It.mp3" file - "./jbripper.py user pass spotify:user:[user]:playlist:7HC9PMdSbwGBBn3EVTaCNx rips entire playlist +usage: jbripper [-h] -u USER -p PASSWORD -U URL [-l [LIBRARY]] [-P] [-V VBR] + [-f | -d] + +Rip Spotify songs + +optional arguments: + -h, --help show this help message and exit + -u USER, --user USER spotify user + -p PASSWORD, --password PASSWORD + spotify password + -U URL, --url URL spotify url + -l [LIBRARY], --library [LIBRARY] + music library path + -P, --playback set if you want to listen to the tracks that are currently ripped (start with "padsp ./jbripper.py ..." if using pulse audio) + -V VBR, --vbr VBR Lame VBR quality setting. Equivalent to Lame -V parameter. Default 0 + -f, --file Save output mp3 file with the following format: "Artist - Song - [ Album ].mp3" (default) + -d, --directory Save output mp3 to a directory with the following format: "Artist/Album/Song.mp3" + +Example usage: + rip a single file: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR + rip entire playlist: ./jbripper.py -u user -p password -U spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 + check if file exists before ripping: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR -l ~/Music features --------- -* real-time VBR ripping from spotify PCM stream - -* writes id3 tags (including album covers) - -* creates files and directories based on the following structure artist/album/song.mp3 + - real-time VBR ripping from spotify PCM stream + - writes id3 tags (including album cover) prerequisites: --------------- -* libspotify (download at https://developer.spotify.com/technologies/libspotify/) - -* pyspotify (sudo pip install -U pyspotify, requires python-dev) - -* spotify binary appkey (download at developer.spotify.com and copy to wd, requires premium!) - -* lame (sudo apt-get install lame) - -* eyeD3 (pip install eyeD3) + - libspotify (download at https://developer.spotify.com/technologies/libspotify/) + - pyspotify (sudo pip install -U pyspotify) + - spotify appkey (download at developer.spotify.com, requires premium!) + - jukebox.py (pyspotify example) + - lame + - eyeD3 (pip install eyeD3) TODO ----- -- [ ] skip exisiting track (avoid / completed tracks / completed = successful id3) -- [ ] detect if other spotify instance is interrupting -- [ ] add album supprt : spotify:album:1UnRYaeCev9JVKEHWBEgHe - +- detect if other spotify instance is interrupting +. add album supprt : spotify:album:1UnRYaeCev9JVKEHWBEgHe \ No newline at end of file diff --git a/jbripper.py b/jbripper.py index d6fa86d..014969f 100755 --- a/jbripper.py +++ b/jbripper.py @@ -4,16 +4,25 @@ from subprocess import call, Popen, PIPE from spotify import Link, Image from jukebox import Jukebox, container_loaded -import os, sys +import os, sys, argparse import threading import time +import re -playback = False # set if you want to listen to the tracks that are currently ripped (start with "padsp ./jbripper.py ..." if using pulse audio) +#Music library imports +import fnmatch +import eyed3 +import collections + +#playback = False # set if you want to listen to the tracks that are currently ripped (start with "padsp ./jbripper.py ..." if using pulse audio) pipe = None ripping = False end_of_track = threading.Event() +musiclibrary = None +args = None + def printstr(str): # print without newline sys.stdout.write(str) sys.stdout.flush() @@ -24,12 +33,23 @@ def shell(cmdline): # execute shell commands (unicode support) def rip_init(session, track): global pipe, ripping num_track = "%02d" % (track.index(),) - mp3file = track.name()+".mp3" - directory = os.getcwd() + "/" + track.artists()[0].name() + "/" + track.album().name() + "/" + artist = artist = ', '.join(a.name() for a in track.artists()) + album = track.album().name() + title = track.name() + + if args.directory is True: + directory = os.getcwd() + "/" + artist + "/" + album + "/" + mp3file = title+".mp3" + else: + directory = os.getcwd() + "/" + mp3file = artist + " - " + title + " - [ " + album + " ].mp3" + #Removing invalid file characters + mp3file = re.sub(r'[\\/:"*?<>|]+', ' ', mp3file) + if not os.path.exists(directory): os.makedirs(directory) - printstr("ripping " + mp3file + " ...") - p = Popen("lame --silent -V2 -h -r - \""+ directory + mp3file+"\"", stdin=PIPE, shell=True) + printstr("ripping " + directory + mp3file + " ...\n") + p = Popen("lame --silent -V" + args.vbr + " -h -r - \""+ directory + mp3file +"\"", stdin=PIPE, shell=True) pipe = p.stdin ripping = True @@ -37,8 +57,11 @@ def rip_terminate(session, track): global ripping if pipe is not None: print(' done!') + #Avoid concurrent operation exceptions + if args.playback: + time.sleep(1) pipe.close() - ripping = False + ripping = False def rip(session, frames, frame_size, num_frames, sample_type, sample_rate, channels): if ripping: @@ -47,12 +70,19 @@ def rip(session, frames, frame_size, num_frames, sample_type, sample_rate, chann def rip_id3(session, track): # write ID3 data num_track = "%02d" % (track.index(),) - mp3file = track.name()+".mp3" - artist = track.artists()[0].name() + artist = artist = ', '.join(a.name() for a in track.artists()) album = track.album().name() title = track.name() year = track.album().year() - directory = os.getcwd() + "/" + track.artists()[0].name() + "/" + track.album().name() + "/" + + if args.directory is True: + directory = os.getcwd() + "/" + artist + "/" + album + "/" + mp3file = title+".mp3" + else: + directory = os.getcwd() + "/" + mp3file = artist + " - " + title + " - [ " + album + " ].mp3" + #Removing invalid file characters + mp3file = re.sub(r'[\\/:"*?<>|]+', ' ', mp3file) # download cover image = session.image_create(track.album().cover()) @@ -73,9 +103,60 @@ def rip_id3(session, track): # write ID3 data " -Q " + \ " \"" + directory + mp3file + "\"" shell(cmd) - + print directory + mp3file + " written" # delete cover - shell("rm -f cover.jpg") + shell("rm -f cover.jpg") + + +def library_scan(path): + + print "Scanning " + path + count = 0 + tree = lambda: collections.defaultdict(tree) + musiclibrary = tree() + for root, dirnames, filenames in os.walk(path): + for filename in fnmatch.filter(filenames, '*.mp3'): + filepath = os.path.join(root, filename ) + try: + audiofile = eyed3.load(filepath) + try: + artist=audiofile.tag.artist + except AttributeError: + artist="" + try: + album=audiofile.tag.album + except AttributeError: + album="" + try: + title=audiofile.tag.title + except AttributeError: + title="" + + musiclibrary[artist][album][title]=filepath + count += 1 + + except Exception, e: + print "Error loading " + filepath + print e + print str(count) + " mp3 files found" + return musiclibrary + +def library_track_exists(track): + if musiclibrary == None: + return False + + artist = artist = ', '.join(a.name() for a in track.artists()) + album = track.album().name() + title = track.name() + + filepath = musiclibrary[artist][album][title] + if filepath == {}: + return False + else: + print "Skipping. Track found at " + filepath + return True + + class RipperThread(threading.Thread): def __init__(self, ripper): @@ -88,7 +169,7 @@ def run(self): container_loaded.clear() # create track iterator - link = Link.from_string(sys.argv[3]) + link = Link.from_string(args.url[0]) if link.type() == Link.LINK_TRACK: track = link.as_track() itrack = iter([track]) @@ -102,19 +183,26 @@ def run(self): # ripping loop session = self.ripper.session + count = 0 for track in itrack: - - self.ripper.load_track(track) + count += 1 + print "Track " + str(count) + if track.availability() != 1: + print 'Skipping. Track not available' + else: + self.ripper.load_track(track) - rip_init(session, track) + if not library_track_exists(track): - self.ripper.play() + rip_init(session, track) - end_of_track.wait() - end_of_track.clear() # TODO check if necessary + self.ripper.play() - rip_terminate(session, track) - rip_id3(session, track) + end_of_track.wait() + end_of_track.clear() # TODO check if necessary + + rip_terminate(session, track) + rip_id3(session, track) self.ripper.disconnect() @@ -126,7 +214,8 @@ def __init__(self, *a, **kw): def music_delivery_safe(self, session, frames, frame_size, num_frames, sample_type, sample_rate, channels): rip(session, frames, frame_size, num_frames, sample_type, sample_rate, channels) - if playback: + #if playback: + if args.playback: return Jukebox.music_delivery_safe(self, session, frames, frame_size, num_frames, sample_type, sample_rate, channels) else: return num_frames @@ -137,12 +226,29 @@ def end_of_track(self, session): if __name__ == '__main__': - if len(sys.argv) >= 3: - ripper = Ripper(sys.argv[1],sys.argv[2]) # login - ripper.connect() - else: - print "usage : \n" - print " ./jbripper.py [username] [password] [spotify_url]" - print "example : \n" - print " ./jbripper.py user pass spotify:track:52xaypL0Kjzk0ngwv3oBPR - for a single file" - print " ./jbripper.py user pass spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 - rips entire playlist" + + parser = argparse.ArgumentParser(prog='jbripper', + description='Rip Spotify songs', + formatter_class=argparse.RawTextHelpFormatter, + epilog='''Example usage: + rip a single file: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR + rip entire playlist: ./jbripper.py -u user -p password -U spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 + check if file exists before ripping: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR -l ~/Music + ''') + parser.add_argument('-u','--user', nargs=1, required=True, help='spotify user') + parser.add_argument('-p','--password', nargs=1, required=True, help='spotify password') + parser.add_argument('-U','--url', nargs=1, required=True, help='spotify url') + parser.add_argument('-l', '--library', nargs='?', help='music library path') + parser.add_argument('-P', '--playback', action="store_true", help='set if you want to listen to the tracks that are currently ripped (start with "padsp ./jbripper.py ..." if using pulse audio)') + parser.add_argument('-V', '--vbr', default="0", help='Lame VBR quality setting. Equivalent to Lame -V parameter. Default 0') + group = parser.add_mutually_exclusive_group(required=False) + group.add_argument('-f', '--file', default=True, action="store_true", help='Save output mp3 file with the following format: "Artist - Song - [ Album ].mp3" (default)') + group.add_argument('-d', '--directory', default=False, action="store_true", help='Save output mp3 to a directory with the following format: "Artist/Album/Song.mp3"') + + args = parser.parse_args() + #print args + if args.library != None: + musiclibrary = library_scan(args.library) + ripper = Ripper(args.user[0],args.password[0]) # login + ripper.connect() + \ No newline at end of file From 62663341f0a6916d8df594a1cad6aad6b572648d Mon Sep 17 00:00:00 2001 From: Miquel Adrover Date: Tue, 21 Jan 2014 17:51:11 +0100 Subject: [PATCH 2/5] corrected README.md --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7d3ec9d..c84da25 100644 --- a/README.md +++ b/README.md @@ -5,28 +5,28 @@ small ripper script for spotify (rips playlists to mp3 and includes ID3 tags and note that stream ripping violates the ToC's of libspotify! -usage: jbripper [-h] -u USER -p PASSWORD -U URL [-l [LIBRARY]] [-P] [-V VBR] - [-f | -d] - -Rip Spotify songs - -optional arguments: - -h, --help show this help message and exit - -u USER, --user USER spotify user - -p PASSWORD, --password PASSWORD - spotify password - -U URL, --url URL spotify url - -l [LIBRARY], --library [LIBRARY] - music library path - -P, --playback set if you want to listen to the tracks that are currently ripped (start with "padsp ./jbripper.py ..." if using pulse audio) - -V VBR, --vbr VBR Lame VBR quality setting. Equivalent to Lame -V parameter. Default 0 - -f, --file Save output mp3 file with the following format: "Artist - Song - [ Album ].mp3" (default) - -d, --directory Save output mp3 to a directory with the following format: "Artist/Album/Song.mp3" - -Example usage: - rip a single file: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR - rip entire playlist: ./jbripper.py -u user -p password -U spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 - check if file exists before ripping: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR -l ~/Music + usage: jbripper [-h] -u USER -p PASSWORD -U URL [-l [LIBRARY]] [-P] [-V VBR] + [-f | -d] + + Rip Spotify songs + + optional arguments: + -h, --help show this help message and exit + -u USER, --user USER spotify user + -p PASSWORD, --password PASSWORD + spotify password + -U URL, --url URL spotify url + -l [LIBRARY], --library [LIBRARY] + music library path + -P, --playback set if you want to listen to the tracks that are currently ripped (start with "padsp ./jbripper.py ..." if using pulse audio) + -V VBR, --vbr VBR Lame VBR quality setting. Equivalent to Lame -V parameter. Default 0 + -f, --file Save output mp3 file with the following format: "Artist - Song - [ Album ].mp3" (default) + -d, --directory Save output mp3 to a directory with the following format: "Artist/Album/Song.mp3" + + Example usage: + rip a single file: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR + rip entire playlist: ./jbripper.py -u user -p password -U spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 + check if file exists before ripping: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR -l ~/Music features - real-time VBR ripping from spotify PCM stream From 316aa7d09fb5169441cee0f658ce6ac89004d64b Mon Sep 17 00:00:00 2001 From: Miquel Adrover Date: Tue, 21 Jan 2014 17:53:33 +0100 Subject: [PATCH 3/5] corrected README.md --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c84da25..38f66e1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ small ripper script for spotify (rips playlists to mp3 and includes ID3 tags and note that stream ripping violates the ToC's of libspotify! - usage: jbripper [-h] -u USER -p PASSWORD -U URL [-l [LIBRARY]] [-P] [-V VBR] +Usage: + + jbripper [-h] -u USER -p PASSWORD -U URL [-l [LIBRARY]] [-P] [-V VBR] [-f | -d] Rip Spotify songs @@ -28,11 +30,11 @@ note that stream ripping violates the ToC's of libspotify! rip entire playlist: ./jbripper.py -u user -p password -U spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 check if file exists before ripping: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR -l ~/Music -features +features: - real-time VBR ripping from spotify PCM stream - writes id3 tags (including album cover) -prerequisites: +prerequisites: - libspotify (download at https://developer.spotify.com/technologies/libspotify/) - pyspotify (sudo pip install -U pyspotify) - spotify appkey (download at developer.spotify.com, requires premium!) @@ -40,6 +42,6 @@ prerequisites: - lame - eyeD3 (pip install eyeD3) -TODO +TODO: - detect if other spotify instance is interrupting -. add album supprt : spotify:album:1UnRYaeCev9JVKEHWBEgHe \ No newline at end of file +- add album supprt : spotify:album:1UnRYaeCev9JVKEHWBEgHe \ No newline at end of file From f28815d678fee39de305ae52acde107dcbfb4d60 Mon Sep 17 00:00:00 2001 From: Miquel Adrover Date: Tue, 21 Jan 2014 17:55:54 +0100 Subject: [PATCH 4/5] README.md updated --- README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 38f66e1..d1829a5 100644 --- a/README.md +++ b/README.md @@ -31,17 +31,20 @@ Usage: check if file exists before ripping: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR -l ~/Music features: - - real-time VBR ripping from spotify PCM stream - - writes id3 tags (including album cover) -prerequisites: - - libspotify (download at https://developer.spotify.com/technologies/libspotify/) - - pyspotify (sudo pip install -U pyspotify) - - spotify appkey (download at developer.spotify.com, requires premium!) - - jukebox.py (pyspotify example) - - lame - - eyeD3 (pip install eyeD3) +- real-time VBR ripping from spotify PCM stream +- writes id3 tags (including album cover) + +prerequisites: + +- libspotify (download at https://developer.spotify.com/technologies/libspotify/) +- pyspotify (sudo pip install -U pyspotify) +- spotify appkey (download at developer.spotify.com, requires premium!) +- jukebox.py (pyspotify example) +- lame +- eyeD3 (pip install eyeD3) TODO: + - detect if other spotify instance is interrupting - add album supprt : spotify:album:1UnRYaeCev9JVKEHWBEgHe \ No newline at end of file From 4db37da8fc9146df1894cd4ac6ab2a6b90ee8809 Mon Sep 17 00:00:00 2001 From: Miquel Adrover Date: Tue, 21 Jan 2014 18:01:08 +0100 Subject: [PATCH 5/5] README.md updated --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d1829a5..ea4bb0a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ small ripper script for spotify (rips playlists to mp3 and includes ID3 tags and note that stream ripping violates the ToC's of libspotify! Usage: +-------- jbripper [-h] -u USER -p PASSWORD -U URL [-l [LIBRARY]] [-P] [-V VBR] [-f | -d] @@ -15,7 +16,7 @@ Usage: optional arguments: -h, --help show this help message and exit -u USER, --user USER spotify user - -p PASSWORD, --password PASSWORD + -p PASSWORD, --password password spotify password -U URL, --url URL spotify url -l [LIBRARY], --library [LIBRARY] @@ -31,20 +32,23 @@ Usage: check if file exists before ripping: ./jbripper.py -u user -p password -U spotify:track:52xaypL0Kjzk0ngwv3oBPR -l ~/Music features: +---------- - real-time VBR ripping from spotify PCM stream - writes id3 tags (including album cover) +- Check for existing songs prerequisites: +--------------- - libspotify (download at https://developer.spotify.com/technologies/libspotify/) - pyspotify (sudo pip install -U pyspotify) - spotify appkey (download at developer.spotify.com, requires premium!) - jukebox.py (pyspotify example) -- lame +- lame (sudo apt-get install lame) - eyeD3 (pip install eyeD3) TODO: - -- detect if other spotify instance is interrupting -- add album supprt : spotify:album:1UnRYaeCev9JVKEHWBEgHe \ No newline at end of file +------ +- [ ] detect if other spotify instance is interrupting +- [ ] add album supprt : spotify:album:1UnRYaeCev9JVKEHWBEgHe \ No newline at end of file