def main(prog_args=sys.argv[1:]): # in case we changed the location of the settings directory where the # config file lives, we need to parse this argument before we parse # the rest of the arguments (which can overwrite the options in the # config file) settings_parser = argparse.ArgumentParser(add_help=False) settings_parser.add_argument( '-S', '--settings', nargs=1, help='Path to settings, config and temp files directory ' '[Default=~/.spotify-ripper]') args, remaining_argv = settings_parser.parse_known_args(prog_args) init_util_globals(args) # load config file, overwriting any defaults defaults = { "bitrate": "320", "quality": "320", "comp": "10", "vbr": "0", } defaults = load_config(defaults) parser = argparse.ArgumentParser( prog='spotify-ripper', description='Rips Spotify URIs to MP3s with ID3 tags and album covers', parents=[settings_parser], formatter_class=argparse.RawTextHelpFormatter, epilog='''Example usage: rip a single file: spotify-ripper -u user spotify:track:52xaypL0Kjzk0ngwv3oBPR rip entire playlist: spotify-ripper -u user spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 rip a list of URIs: spotify-ripper -u user list_of_uris.txt rip tracks from Spotify's charts: spotify-ripper -l spotify:charts:regional:global:weekly:latest search for tracks to rip: spotify-ripper -l -Q 160 -o "album:Rumours track:'the chain'" ''') # create group to prevent user from using both the -l and -u option is_user_set = defaults.get('user') is not None is_last_set = defaults.get('last') is True if is_user_set or is_last_set: if is_user_set and is_last_set: print("spotify-ripper: error: one of the arguments -u/--user " "-l/--last is required") sys.exit(1) else: group = parser.add_mutually_exclusive_group(required=False) else: group = parser.add_mutually_exclusive_group(required=True) encoding_group = parser.add_mutually_exclusive_group(required=False) # set defaults parser.set_defaults(**defaults) prog_version = pkg_resources.require("spotify-ripper")[0].version parser.add_argument( '-a', '--ascii', action='store_true', help='Convert the file name and the metadata tags to ASCII ' 'encoding [Default=utf-8]') encoding_group.add_argument( '--aac', action='store_true', help='Rip songs to AAC format with FreeAAC instead of MP3') encoding_group.add_argument( '--alac', action='store_true', help='Rip songs to Apple Lossless format instead of MP3') parser.add_argument( '-A', '--ascii-path-only', action='store_true', help='Convert the file name (but not the metadata tags) to ASCII ' 'encoding [Default=utf-8]') parser.add_argument('-b', '--bitrate', help='CBR bitrate [Default=320]') parser.add_argument('-c', '--cbr', action='store_true', help='CBR encoding [Default=VBR]') parser.add_argument( '--comp', default="10", help='compression complexity for FLAC and Opus [Default=Max]') parser.add_argument( '--comment', nargs=1, help='Add custom metadata comment to all songs. Can include ' '{create_time} or {creator} if the URI is a playlist.') parser.add_argument( '--cover-file', nargs=1, help='Save album cover image to file name (e.g "cover.jpg") ' '[Default=embed]') parser.add_argument( '-d', '--directory', nargs=1, help='Base directory where ripped MP3s are saved [Default=cwd]') parser.add_argument('--fail-log', nargs=1, help="Logs the list of track URIs that failed to rip") encoding_group.add_argument( '--flac', action='store_true', help='Rip songs to lossless FLAC encoding instead of MP3') parser.add_argument( '-f', '--format', nargs=1, help='Save songs using this path and filename structure (see README)') parser.add_argument('--flat', action='store_true', help='Save all songs to a single directory ' '(overrides --format option)') parser.add_argument( '--flat-with-index', action='store_true', help='Similar to --flat [-f] but includes the playlist index at ' 'the start of the song file') parser.add_argument( '-g', '--genres', nargs=1, choices=['artist', 'album'], help='Attempt to retrieve genre information from Spotify\'s ' 'Web API [Default=skip]') encoding_group.add_argument( '--id3-v23', action='store_true', help='Store ID3 tags using version v2.3 [Default=v2.4]') parser.add_argument('-k', '--key', nargs=1, help='Path to Spotify application key file ' '[Default=Settings Directory]') group.add_argument('-u', '--user', nargs=1, help='Spotify username') parser.add_argument('-p', '--password', nargs=1, help='Spotify password [Default=ask interactively]') group.add_argument('-l', '--last', action='store_true', help='Use last login credentials') parser.add_argument( '-L', '--log', nargs=1, help='Log in a log-friendly format to a file (use - to log to stdout)') encoding_group.add_argument( '--pcm', action='store_true', help='Saves a .pcm file with the raw PCM data instead of MP3') encoding_group.add_argument( '--mp4', action='store_true', help='Rip songs to MP4/M4A format with Fraunhofer FDK AAC codec ' 'instead of MP3') parser.add_argument('--normalize', action='store_true', help='Normalize volume levels of tracks') parser.add_argument('-na', '--normalized-ascii', action='store_true', help='Convert the file name to normalized ASCII with ' 'unicodedata.normalize (NFKD)') parser.add_argument('-o', '--overwrite', action='store_true', help='Overwrite existing MP3 files [Default=skip]') encoding_group.add_argument( '--opus', action='store_true', help='Rip songs to Opus encoding instead of MP3') parser.add_argument('--playlist-m3u', action='store_true', help='create a m3u file when ripping a playlist') parser.add_argument('--playlist-wpl', action='store_true', help='create a wpl file when ripping a playlist') parser.add_argument( '--playlist-sync', action='store_true', help='Sync playlist songs (rename and remove old songs)') parser.add_argument( '-q', '--vbr', help='VBR quality setting or target bitrate for Opus [Default=0]') parser.add_argument('-Q', '--quality', choices=['160', '320', '96'], help='Spotify stream bitrate preference [Default=320]') parser.add_argument( '--resume-after', help='Resumes script after a certain amount of time has passed ' 'after stopping (e.g. 1h30m). Alternatively, accepts a specific ' 'time in 24hr format to start after (e.g 03:30, 16:15). ' 'Requires --stop-after option to be set') parser.add_argument( '-R', '--replace', nargs="+", required=False, help='pattern to replace the output filename separated by "/". ' 'The following example replaces all spaces with "_" and all "-" ' 'with ".":' ' spotify-ripper --replace " /_" "\-/." uri') parser.add_argument('-s', '--strip-colors', action='store_true', help='Strip coloring from output [Default=colors]') parser.add_argument( '--stereo-mode', choices=['j', 's', 'f', 'd', 'm', 'l', 'r'], help='Advanced stereo settings for Lame MP3 encoder only') parser.add_argument( '--stop-after', help='Stops script after a certain amount of time has passed ' '(e.g. 1h30m). Alternatively, accepts a specific time in 24hr ' 'format to stop after (e.g 03:30, 16:15)') parser.add_argument('-V', '--version', action='version', version=prog_version) encoding_group.add_argument( '--wav', action='store_true', help='Rip songs to uncompressed WAV file instead of MP3') encoding_group.add_argument( '--vorbis', action='store_true', help='Rip songs to Ogg Vorbis encoding instead of MP3') parser.add_argument('-r', '--remove-from-playlist', action='store_true', help='Delete tracks from playlist after successful ' 'ripping [Default=no]') parser.add_argument( '-x', '--exclude-appears-on', action='store_true', help='Exclude albums that an artist \'appears on\' when passing ' 'a Spotify artist URI') parser.add_argument( 'uri', nargs="+", help='One or more Spotify URI(s) (either URI, a file of URIs or a ' 'search query)') args = parser.parse_args(remaining_argv) init_util_globals(args) # kind of a hack to get colorama stripping to work when outputting # to a file instead of stdout. Taken from initialise.py in colorama def wrap_stream(stream, convert, strip, autoreset, wrap): if wrap: wrapper = AnsiToWin32(stream, convert=convert, strip=strip, autoreset=autoreset) if wrapper.should_wrap(): stream = wrapper.stream return stream args.has_log = args.log is not None if args.has_log: if args.log[0] == "-": init(strip=True) else: log_file = open(args.log[0], 'a') sys.stdout = wrap_stream(log_file, None, True, False, True) else: init(strip=True if args.strip_colors else None) if args.ascii_path_only is True: args.ascii = True # unless explicitly told not to, we are going to encode # for utf-8 by default if not args.ascii and sys.version_info < (3, 0): sys.stdout = codecs.getwriter('utf-8')(sys.stdout) # small sanity check on user option if args.user is not None and args.user[0] == "USER": print(Fore.RED + "Please pass your username as --user " + "<YOUR_USER_NAME> instead of --user USER " + "<YOUR_USER_NAME>..." + Fore.RESET) sys.exit(1) if args.wav: args.output_type = "wav" elif args.pcm: args.output_type = "pcm" elif args.flac: args.output_type = "flac" if args.comp == "10": args.comp = "8" elif args.vorbis: args.output_type = "ogg" if args.vbr == "0": args.vbr = "10" elif args.opus: args.output_type = "opus" if args.vbr == "0": args.vbr = "320" elif args.aac: args.output_type = "aac" if args.vbr == "0": args.vbr = "500" elif args.mp4: args.output_type = "m4a" if args.vbr == "0": args.vbr = "5" elif args.alac: args.output_type = "alac.m4a" else: args.output_type = "mp3" # check that encoder tool is available encoders = { "flac": ("flac", "flac"), "aac": ("faac", "faac"), "ogg": ("oggenc", "vorbis-tools"), "opus": ("opusenc", "opus-tools"), "mp3": ("lame", "lame"), "m4a": ("fdkaac", "fdk-aac-encoder"), "alac.m4a": ("avconv", "libav-tools"), } if args.output_type in encoders.keys(): encoder = encoders[args.output_type][0] if which(encoder) is None: print(Fore.RED + "Missing dependency '" + encoder + "'. Please install and add to path..." + Fore.RESET) # assumes OS X or Ubuntu/Debian command_help = ("brew install " if sys.platform == "darwin" else "sudo apt-get install ") print("...try " + Fore.YELLOW + command_help + encoders[args.output_type][1] + Fore.RESET) sys.exit(1) # format string if args.flat: args.format = ["{artist} - {track_name}.{ext}"] elif args.flat_with_index: args.format = ["{idx:3} - {artist} - {track_name}.{ext}"] elif args.format is None: args.format = ["{album_artist}/{album}/{artist} - {track_name}.{ext}"] # print some settings print(Fore.GREEN + "Spotify Ripper - v" + prog_version + Fore.RESET) def encoding_output_str(): if args.output_type == "wav": return "WAV, Stereo 16bit 44100Hz" elif args.output_type == "pcm": return "Raw Headerless PCM, Stereo 16bit 44100Hz" else: if args.output_type == "flac": return "FLAC, Compression Level: " + args.comp elif args.output_type == "alac.m4a": return "Apple Lossless (ALAC)" elif args.output_type == "ogg": codec = "Ogg Vorbis" elif args.output_type == "opus": codec = "Opus" elif args.output_type == "mp3": codec = "MP3" elif args.output_type == "m4a": codec = "MPEG4 AAC" elif args.output_type == "aac": codec = "AAC" else: codec = "Unknown" if args.cbr: return codec + ", CBR " + args.bitrate + " kbps" else: return codec + ", VBR " + args.vbr print(Fore.YELLOW + " Encoding output:\t" + Fore.RESET + encoding_output_str()) print(Fore.YELLOW + " Spotify bitrate:\t" + Fore.RESET + args.quality + " kbps") def unicode_support_str(): if args.ascii_path_only: return "Unicode tags, ASCII file path" elif args.ascii: return "ASCII only" else: return "Yes" # check that --stop-after and --resume-after options are valid if args.stop_after is not None and \ parse_time_str(args.stop_after) is None: print(Fore.RED + "--stop-after option is not valid" + Fore.RESET) sys.exit(1) if args.resume_after is not None and \ parse_time_str(args.resume_after) is None: print(Fore.RED + "--resume-after option is not valid" + Fore.RESET) sys.exit(1) print(Fore.YELLOW + " Unicode support:\t" + Fore.RESET + unicode_support_str()) print(Fore.YELLOW + " Output directory:\t" + Fore.RESET + base_dir()) print(Fore.YELLOW + " Settings directory:\t" + Fore.RESET + settings_dir()) print(Fore.YELLOW + " Format String:\t" + Fore.RESET + args.format[0]) print(Fore.YELLOW + " Overwrite files:\t" + Fore.RESET + ("Yes" if args.overwrite else "No")) # patch a bug when Python 3/MP4 if sys.version_info >= (3, 0) and args.output_type == "m4a": patch_bug_in_mutagen() ripper = Ripper(args) ripper.start() # try to listen for terminal resize events # (needs to be called on main thread) if not args.has_log: ripper.progress.handle_resize() signal.signal(signal.SIGWINCH, ripper.progress.handle_resize) def abort(set_logged_in=False): ripper.abort_rip() if set_logged_in: ripper.ripper_continue.set() ripper.join() sys.exit(1) # login on main thread to catch any KeyboardInterrupt try: if not ripper.login(): print(Fore.RED + "Encountered issue while logging into " "Spotify, aborting..." + Fore.RESET) abort(set_logged_in=True) else: ripper.ripper_continue.set() except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort(set_logged_in=True) # wait for ripping thread to finish try: while ripper.isAlive(): schedule.run_pending() ripper.join(0.1) except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort()
def main(prog_args=sys.argv[1:]): # in case we changed the location of the settings directory where the # config file lives, we need to parse this argument before we parse # the rest of the arguments (which can overwrite the options in the # config file) settings_parser = argparse.ArgumentParser(add_help=False) settings_parser.add_argument( '-S', '--settings', help='Path to settings, config and temp files directory ' '[Default=~/.spotify-ripper]') args, remaining_argv = settings_parser.parse_known_args(prog_args) init_util_globals(args) # load config file, overwriting any defaults defaults = { "bitrate": "320", "quality": "320", "comp": "10", "vbr": "0", "partial_check": "weak", } defaults = load_config(defaults) parser = argparse.ArgumentParser( prog='spotify-ripper', description='Rips Spotify URIs to MP3s with ID3 tags and album covers', parents=[settings_parser], formatter_class=argparse.RawTextHelpFormatter, epilog='''Example usage: rip a single file: spotify-ripper -u user spotify:track:52xaypL0Kjzk0ngwv3oBPR rip entire playlist: spotify-ripper -u user spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 rip a list of URIs: spotify-ripper -u user list_of_uris.txt rip tracks from Spotify's charts: spotify-ripper -l spotify:charts:regional:global:weekly:latest search for tracks to rip: spotify-ripper -l -Q 160 -o "album:Rumours track:'the chain'" ''') # create group to prevent user from using both the -l and -u option is_user_set = defaults.get('user') is not None is_last_set = defaults.get('last') is True if is_user_set or is_last_set: if is_user_set and is_last_set: print("spotify-ripper: error: one of the arguments -u/--user " "-l/--last is required") sys.exit(1) else: group = parser.add_mutually_exclusive_group(required=False) else: group = parser.add_mutually_exclusive_group(required=True) encoding_group = parser.add_mutually_exclusive_group(required=False) # set defaults parser.set_defaults(**defaults) prog_version = pkg_resources.require("spotify-ripper")[0].version parser.add_argument( '-a', '--ascii', action='store_true', help='Convert the file name and the metadata tags to ASCII ' 'encoding [Default=utf-8]') encoding_group.add_argument( '--aac', action='store_true', help='Rip songs to AAC format with FreeAAC instead of MP3') encoding_group.add_argument( '--aiff', action='store_true', help='Rip songs to lossless AIFF encoding instead of MP3') encoding_group.add_argument( '--alac', action='store_true', help='Rip songs to Apple Lossless format instead of MP3') parser.add_argument( '--all-artists', action='store_true', help='Store all artists, rather than just the main artist, in the ' 'track\'s metadata tag') parser.add_argument( '--artist-album-type', help='Only load albums of specified types when passing a Spotify ' 'artist URI [Default=album,single,ep,compilation,appears_on]') parser.add_argument( '--artist-album-market', help='Only load albums with the specified ISO2 country code when ' 'passing a Spotify artist URI. You may get duplicate albums ' 'if not set. [Default=any]') parser.add_argument( '-A', '--ascii-path-only', action='store_true', help='Convert the file name (but not the metadata tags) to ASCII ' 'encoding [Default=utf-8]') parser.add_argument( '-b', '--bitrate', help='CBR bitrate [Default=320]') parser.add_argument( '-c', '--cbr', action='store_true', help='CBR encoding [Default=VBR]') parser.add_argument( '--comp', help='compression complexity for FLAC and Opus [Default=Max]') parser.add_argument( '--comment', help='Set comment metadata tag to all songs. Can include ' 'same tags as --format.') parser.add_argument( '--cover-file', help='Save album cover image to file name (e.g "cover.jpg") ' '[Default=embed]') parser.add_argument( '--cover-file-and-embed', metavar="COVER_FILE", help='Same as --cover-file but embeds the cover image too') parser.add_argument( '-d', '--directory', help='Base directory where ripped MP3s are saved [Default=cwd]') parser.add_argument( '--fail-log', help="Logs the list of track URIs that failed to rip") encoding_group.add_argument( '--flac', action='store_true', help='Rip songs to lossless FLAC encoding instead of MP3') parser.add_argument( '-f', '--format', help='Save songs using this path and filename structure (see README)') parser.add_argument( '--format-case', choices=['upper', 'lower', 'capitalize'], help='Convert all words of the file name to upper-case, lower-case, ' 'or capitalized') parser.add_argument( '--flat', action='store_true', help='Save all songs to a single directory ' '(overrides --format option)') parser.add_argument( '--flat-with-index', action='store_true', help='Similar to --flat [-f] but includes the playlist index at ' 'the start of the song file') parser.add_argument( '-g', '--genres', choices=['artist', 'album'], help='Attempt to retrieve genre information from Spotify\'s ' 'Web API [Default=skip]') parser.add_argument( '--grouping', help='Set grouping metadata tag to all songs. Can include ' 'same tags as --format.') encoding_group.add_argument( '--id3-v23', action='store_true', help='Store ID3 tags using version v2.3 [Default=v2.4]') parser.add_argument( '-k', '--key', help='Path to Spotify application key file ' '[Default=Settings Directory]') group.add_argument( '-u', '--user', help='Spotify username') parser.add_argument( '-p', '--password', help='Spotify password [Default=ask interactively]') group.add_argument( '-l', '--last', action='store_true', help='Use last login credentials') parser.add_argument( '-L', '--log', help='Log in a log-friendly format to a file (use - to log to stdout)') encoding_group.add_argument( '--pcm', action='store_true', help='Saves a .pcm file with the raw PCM data instead of MP3') encoding_group.add_argument( '--mp4', action='store_true', help='Rip songs to MP4/M4A format with Fraunhofer FDK AAC codec ' 'instead of MP3') parser.add_argument( '--normalize', action='store_true', help='Normalize volume levels of tracks') parser.add_argument( '-na', '--normalized-ascii', action='store_true', help='Convert the file name to normalized ASCII with ' 'unicodedata.normalize (NFKD)') parser.add_argument( '-o', '--overwrite', action='store_true', help='Overwrite existing MP3 files [Default=skip]') encoding_group.add_argument( '--opus', action='store_true', help='Rip songs to Opus encoding instead of MP3') parser.add_argument( '--partial-check', choices=['none', 'weak', 'strict'], help='Check for and overwrite partially ripped files. "weak" will ' 'err on the side of not re-ripping the file if it is unsure, ' 'whereas "strict" will re-rip the file [Default=weak]') parser.add_argument( '--play-token-resume', metavar="RESUME_AFTER", help='If the \'play token\' is lost to a different device using ' 'the same Spotify account, the script will wait a speficied ' 'amount of time before restarting. This argument takes the same ' 'values as --resume-after [Default=abort]') parser.add_argument( '--playlist-m3u', action='store_true', help='create a m3u file when ripping a playlist') parser.add_argument( '--playlist-wpl', action='store_true', help='create a wpl file when ripping a playlist') parser.add_argument( '--playlist-sync', action='store_true', help='Sync playlist songs (rename and remove old songs)') parser.add_argument( '--plus-pcm', action='store_true', help='Saves a .pcm file in addition to the encoded file (e.g. mp3)') parser.add_argument( '--plus-wav', action='store_true', help='Saves a .wav file in addition to the encoded file (e.g. mp3)') parser.add_argument( '-q', '--vbr', help='VBR quality setting or target bitrate for Opus [Default=0]') parser.add_argument( '-Q', '--quality', choices=['160', '320', '96'], help='Spotify stream bitrate preference [Default=320]') parser.add_argument( '--remove-offline-cache', action='store_true', help='Remove libspotify\'s offline cache directory after the rip' 'is complete to save disk space') parser.add_argument( '--resume-after', help='Resumes script after a certain amount of time has passed ' 'after stopping (e.g. 1h30m). Alternatively, accepts a specific ' 'time in 24hr format to start after (e.g 03:30, 16:15). ' 'Requires --stop-after option to be set') parser.add_argument( '-R', '--replace', nargs="+", required=False, help='pattern to replace the output filename separated by "/". ' 'The following example replaces all spaces with "_" and all "-" ' 'with ".": spotify-ripper --replace " /_" "\-/." uri') parser.add_argument( '-s', '--strip-colors', action='store_true', help='Strip coloring from output [Default=colors]') parser.add_argument( '--stereo-mode', choices=['j', 's', 'f', 'd', 'm', 'l', 'r'], help='Advanced stereo settings for Lame MP3 encoder only') parser.add_argument( '--stop-after', help='Stops script after a certain amount of time has passed ' '(e.g. 1h30m). Alternatively, accepts a specific time in 24hr ' 'format to stop after (e.g 03:30, 16:15)') parser.add_argument( '-V', '--version', action='version', version=prog_version) encoding_group.add_argument( '--wav', action='store_true', help='Rip songs to uncompressed WAV file instead of MP3') parser.add_argument( '--windows-safe', action='store_true', help='Make filename safe for Windows file system ' '(truncate filename to 255 characters)') encoding_group.add_argument( '--vorbis', action='store_true', help='Rip songs to Ogg Vorbis encoding instead of MP3') parser.add_argument( '-r', '--remove-from-playlist', action='store_true', help='[WARNING: SPOTIFY IS NOT PROPROGATING PLAYLIST CHANGES TO ' 'THEIR SERVERS] Delete tracks from playlist after successful ' 'ripping [Default=no]') parser.add_argument( 'uri', nargs="+", help='One or more Spotify URI(s) (either URI, a file of URIs or a ' 'search query)') args = parser.parse_args(remaining_argv) init_util_globals(args) # kind of a hack to get colorama stripping to work when outputting # to a file instead of stdout. Taken from initialise.py in colorama def wrap_stream(stream, convert, strip, autoreset, wrap): if wrap: wrapper = AnsiToWin32(stream, convert=convert, strip=strip, autoreset=autoreset) if wrapper.should_wrap(): stream = wrapper.stream return stream args.has_log = args.log is not None if args.has_log: if args.log == "-": init(strip=True) else: encoding = "ascii" if args.ascii else "utf-8" log_file = codecs.open(enc_str(args.log), 'a', encoding) sys.stdout = wrap_stream(log_file, None, True, False, True) else: init(strip=True if args.strip_colors else None) if args.ascii_path_only is True: args.ascii = True # unless explicitly told not to, we are going to encode # for utf-8 by default if not args.ascii and sys.version_info < (3, 0): sys.stdout = codecs.getwriter('utf-8')(sys.stdout) # small sanity check on user option if args.user is not None and args.user == "USER": print(Fore.RED + "Please pass your username as --user " + "<YOUR_USER_NAME> instead of --user USER " + "<YOUR_USER_NAME>..." + Fore.RESET) sys.exit(1) # give warning for broken feature if args.remove_from_playlist: print(Fore.RED + "--REMOVE-FROM-PLAYLIST WARNING:") print("SPOTIFY IS NOT PROPROGATING PLAYLIST CHANGES " "TO THEIR SERVERS.") print("YOU WILL NOT SEE ANY CHANGES TO YOUR PLAYLIST ON THE " + "OFFICIAL SPOTIFY DESKTOP OR WEB APP." + Fore.RESET) if args.wav: args.output_type = "wav" elif args.pcm: args.output_type = "pcm" elif args.flac: args.output_type = "flac" if args.comp == "10": args.comp = "8" elif args.vorbis: args.output_type = "ogg" if args.vbr == "0": args.vbr = "9" elif args.opus: args.output_type = "opus" if args.vbr == "0": args.vbr = "320" elif args.aac: args.output_type = "aac" if args.vbr == "0": args.vbr = "500" elif args.mp4: args.output_type = "m4a" if args.vbr == "0": args.vbr = "5" elif args.alac: args.output_type = "alac.m4a" else: args.output_type = "mp3" # check that encoder tool is available encoders = { "flac": ("flac", "flac"), "aiff": ("sox", "sox"), "aac": ("faac", "faac"), "ogg": ("oggenc", "vorbis-tools"), "opus": ("opusenc", "opus-tools"), "mp3": ("lame", "lame"), "m4a": ("fdkaac", "fdk-aac-encoder"), "alac.m4a": ("avconv", "libav-tools"), } if args.output_type in encoders.keys(): encoder = encoders[args.output_type][0] if which(encoder) is None: print(Fore.RED + "Missing dependency '" + encoder + "'. Please install and add to path..." + Fore.RESET) # assumes OS X or Ubuntu/Debian command_help = ("brew install " if sys.platform == "darwin" else "sudo apt-get install ") print("...try " + Fore.YELLOW + command_help + encoders[args.output_type][1] + Fore.RESET) sys.exit(1) # format string if args.flat: args.format = "{artist} - {track_name}.{ext}" elif args.flat_with_index: args.format = "{idx:3} - {artist} - {track_name}.{ext}" elif args.format is None: args.format = "{album_artist}/{album}/{artist} - {track_name}.{ext}" # print some settings print(Fore.GREEN + "Spotify Ripper - v" + prog_version + Fore.RESET) def encoding_output_str(): if args.output_type == "wav": return "WAV, Stereo 16bit 44100Hz" elif args.output_type == "pcm": return "Raw Headerless PCM, Stereo 16bit 44100Hz" else: if args.output_type == "flac": return "FLAC, Compression Level: " + args.comp elif args.output_type == "aiff": return "AIFF" elif args.output_type == "alac.m4a": return "Apple Lossless (ALAC)" elif args.output_type == "ogg": codec = "Ogg Vorbis" elif args.output_type == "opus": codec = "Opus" elif args.output_type == "mp3": codec = "MP3" elif args.output_type == "m4a": codec = "MPEG4 AAC" elif args.output_type == "aac": codec = "AAC" else: codec = "Unknown" if args.cbr: return codec + ", CBR " + args.bitrate + " kbps" else: return codec + ", VBR " + args.vbr print(Fore.YELLOW + " Encoding output:\t" + Fore.RESET + encoding_output_str()) print(Fore.YELLOW + " Spotify bitrate:\t" + Fore.RESET + args.quality + " kbps") def unicode_support_str(): if args.ascii_path_only: return "Unicode tags, ASCII file path" elif args.ascii: return "ASCII only" else: return "Yes" # check that --stop-after and --resume-after options are valid if args.stop_after is not None and \ parse_time_str(args.stop_after) is None: print(Fore.RED + "--stop-after option is not valid" + Fore.RESET) sys.exit(1) if args.resume_after is not None and \ parse_time_str(args.resume_after) is None: print(Fore.RED + "--resume-after option is not valid" + Fore.RESET) sys.exit(1) if args.play_token_resume is not None and \ parse_time_str(args.play_token_resume) is None: print(Fore.RED + "--play_token_resume option is not valid" + Fore.RESET) sys.exit(1) print(Fore.YELLOW + " Unicode support:\t" + Fore.RESET + unicode_support_str()) print(Fore.YELLOW + " Output directory:\t" + Fore.RESET + base_dir()) print(Fore.YELLOW + " Settings directory:\t" + Fore.RESET + settings_dir()) print(Fore.YELLOW + " Format String:\t" + Fore.RESET + args.format) print(Fore.YELLOW + " Overwrite files:\t" + Fore.RESET + ("Yes" if args.overwrite else "No")) # patch a bug when Python 3/MP4 if sys.version_info >= (3, 0) and \ (args.output_type == "m4a" or args.output_type == "alac.m4a"): patch_bug_in_mutagen() ripper = Ripper(args) ripper.start() # try to listen for terminal resize events # (needs to be called on main thread) if not args.has_log: ripper.progress.handle_resize() signal.signal(signal.SIGWINCH, ripper.progress.handle_resize) def hasStdinData(): return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []) def abort(set_logged_in=False): ripper.abort_rip() if set_logged_in: ripper.ripper_continue.set() ripper.join() sys.exit(1) def skip(): if ripper.ripping.is_set(): ripper.skip.set() # check if we were passed a file name or search def check_uri_args(): if len(args.uri) == 1 and path_exists(args.uri[0]): encoding = "ascii" if args.ascii else "utf-8" args.uri = [line.strip() for line in codecs.open(enc_str(args.uri[0]), 'r', encoding) if not line.strip().startswith("#") and len(line.strip()) > 0] elif len(args.uri) == 1 and not args.uri[0].startswith("spotify:"): args.uri = [list(ripper.search_query(args.uri[0]))] # login and uri_parse on main thread to catch any KeyboardInterrupt try: if not ripper.login(): print( Fore.RED + "Encountered issue while logging into " "Spotify, aborting..." + Fore.RESET) abort(set_logged_in=True) else: check_uri_args() ripper.ripper_continue.set() except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort(set_logged_in=True) # wait for ripping thread to finish stdin_settings = termios.tcgetattr(sys.stdin) try: tty.setcbreak(sys.stdin.fileno()) while ripper.isAlive(): schedule.run_pending() # check if the escape button was pressed if hasStdinData(): c = sys.stdin.read(1) if c == '\x1b': skip() ripper.join(0.1) except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort() finally: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, stdin_settings)
def main(prog_args=sys.argv[1:]): # in case we changed the location of the settings directory where the # config file lives, we need to parse this argument before we parse # the rest of the arguments (which can overwrite the options in the # config file) settings_parser = argparse.ArgumentParser(add_help=False) settings_parser.add_argument( '-S', '--settings', help='Path to settings, config and temp files directory ' '[Default=~/.spotify-ripper]') args, remaining_argv = settings_parser.parse_known_args(prog_args) init_util_globals(args) # load config file, overwriting any defaults defaults = { "bitrate": "320", "quality": "320", "comp": "10", "vbr": "0", "partial_check": "weak", } defaults = load_config(defaults) parser = argparse.ArgumentParser( prog='spotify-ripper', description='Rips Spotify URIs to MP3s with ID3 tags and album covers', parents=[settings_parser], formatter_class=argparse.RawTextHelpFormatter, epilog='''Example usage: rip a single file: spotify-ripper -u user spotify:track:52xaypL0Kjzk0ngwv3oBPR rip entire playlist: spotify-ripper -u user spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 rip a list of URIs: spotify-ripper -u user list_of_uris.txt rip tracks from Spotify's charts: spotify-ripper -l spotify:charts:regional:global:weekly:latest search for tracks to rip: spotify-ripper -l -Q 160 -o "album:Rumours track:'the chain'" ''') group = parser.add_mutually_exclusive_group(required=False) encoding_group = parser.add_mutually_exclusive_group(required=False) # set defaults parser.set_defaults(**defaults) prog_version = pkg_resources.require("spotify-ripper")[0].version parser.add_argument( '-a', '--ascii', action='store_true', help='Convert the file name and the metadata tags to ASCII ' 'encoding [Default=utf-8]') encoding_group.add_argument( '--aac', action='store_true', help='Rip songs to AAC format with FreeAAC instead of MP3') encoding_group.add_argument( '--aiff', action='store_true', help='Rip songs to lossless AIFF encoding instead of MP3') encoding_group.add_argument( '--alac', action='store_true', help='Rip songs to Apple Lossless format instead of MP3') parser.add_argument( '--all-artists', action='store_true', help='Store all artists, rather than just the main artist, in the ' 'track\'s metadata tag') parser.add_argument( '--artist-album-type', help='Only load albums of specified types when passing a Spotify ' 'artist URI [Default=album,single,ep,compilation,appears_on]') parser.add_argument( '--artist-album-market', help='Only load albums with the specified ISO2 country code when ' 'passing a Spotify artist URI. You may get duplicate albums ' 'if not set. [Default=any]') parser.add_argument( '-A', '--ascii-path-only', action='store_true', help='Convert the file name (but not the metadata tags) to ASCII ' 'encoding [Default=utf-8]') parser.add_argument('-b', '--bitrate', help='CBR bitrate [Default=320]') parser.add_argument('-c', '--cbr', action='store_true', help='CBR encoding [Default=VBR]') parser.add_argument( '--comp', help='compression complexity for FLAC and Opus [Default=Max]') parser.add_argument( '--comment', help='Set comment metadata tag to all songs. Can include ' 'same tags as --format.') parser.add_argument( '--cover-file', help='Save album cover image to file name (e.g "cover.jpg") ' '[Default=embed]') parser.add_argument( '--cover-file-and-embed', metavar="COVER_FILE", help='Same as --cover-file but embeds the cover image too') parser.add_argument( '-d', '--directory', help='Base directory where ripped MP3s are saved [Default=cwd]') parser.add_argument('--fail-log', help="Logs the list of track URIs that failed to rip") encoding_group.add_argument( '--flac', action='store_true', help='Rip songs to lossless FLAC encoding instead of MP3') parser.add_argument( '-f', '--format', help='Save songs using this path and filename structure (see README)') parser.add_argument( '--format-case', choices=['upper', 'lower', 'capitalize'], help='Convert all words of the file name to upper-case, lower-case, ' 'or capitalized') parser.add_argument('--flat', action='store_true', help='Save all songs to a single directory ' '(overrides --format option)') parser.add_argument( '--flat-with-index', action='store_true', help='Similar to --flat [-f] but includes the playlist index at ' 'the start of the song file') parser.add_argument( '-g', '--genres', choices=['artist', 'album'], help='Attempt to retrieve genre information from Spotify\'s ' 'Web API [Default=skip]') parser.add_argument( '--grouping', help='Set grouping metadata tag to all songs. Can include ' 'same tags as --format.') encoding_group.add_argument( '--id3-v23', action='store_true', help='Store ID3 tags using version v2.3 [Default=v2.4]') parser.add_argument('-k', '--key', help='Path to Spotify application key file ' '[Default=Settings Directory]') group.add_argument('-u', '--user', help='Spotify username') parser.add_argument('-p', '--password', help='Spotify password [Default=ask interactively]') parser.add_argument( '--large-cover-art', action='store_true', help='Attempt to retrieve 640x640 cover art from Spotify\'s Web API ' '[Default=300x300]') group.add_argument('-l', '--last', action='store_true', help='Use last login credentials') parser.add_argument( '-L', '--log', help='Log in a log-friendly format to a file (use - to log to stdout)') encoding_group.add_argument( '--pcm', action='store_true', help='Saves a .pcm file with the raw PCM data instead of MP3') encoding_group.add_argument( '--mp4', action='store_true', help='Rip songs to MP4/M4A format with Fraunhofer FDK AAC codec ' 'instead of MP3') parser.add_argument('--normalize', action='store_true', help='Normalize volume levels of tracks') parser.add_argument('-na', '--normalized-ascii', action='store_true', help='Convert the file name to normalized ASCII with ' 'unicodedata.normalize (NFKD)') parser.add_argument('-o', '--overwrite', action='store_true', help='Overwrite existing MP3 files [Default=skip]') encoding_group.add_argument( '--opus', action='store_true', help='Rip songs to Opus encoding instead of MP3') parser.add_argument( '--partial-check', choices=['none', 'weak', 'strict'], help='Check for and overwrite partially ripped files. "weak" will ' 'err on the side of not re-ripping the file if it is unsure, ' 'whereas "strict" will re-rip the file [Default=weak]') parser.add_argument( '--play-token-resume', metavar="RESUME_AFTER", help='If the \'play token\' is lost to a different device using ' 'the same Spotify account, the script will wait a speficied ' 'amount of time before restarting. This argument takes the same ' 'values as --resume-after [Default=abort]') parser.add_argument('--playlist-m3u', action='store_true', help='create a m3u file when ripping a playlist') parser.add_argument('--playlist-wpl', action='store_true', help='create a wpl file when ripping a playlist') parser.add_argument( '--playlist-sync', action='store_true', help='Sync playlist songs (rename and remove old songs)') parser.add_argument( '--plus-pcm', action='store_true', help='Saves a .pcm file in addition to the encoded file (e.g. mp3)') parser.add_argument( '--plus-wav', action='store_true', help='Saves a .wav file in addition to the encoded file (e.g. mp3)') parser.add_argument( '-q', '--vbr', help='VBR quality setting or target bitrate for Opus [Default=0]') parser.add_argument('-Q', '--quality', choices=['160', '320', '96'], help='Spotify stream bitrate preference [Default=320]') parser.add_argument( '--remove-offline-cache', action='store_true', help='Remove libspotify\'s offline cache directory after the rip' 'is complete to save disk space') parser.add_argument( '--resume-after', help='Resumes script after a certain amount of time has passed ' 'after stopping (e.g. 1h30m). Alternatively, accepts a specific ' 'time in 24hr format to start after (e.g 03:30, 16:15). ' 'Requires --stop-after option to be set') parser.add_argument( '-R', '--replace', nargs="+", required=False, help='pattern to replace the output filename separated by "/". ' 'The following example replaces all spaces with "_" and all "-" ' 'with ".": spotify-ripper --replace " /_" "\-/." uri') parser.add_argument('-s', '--strip-colors', action='store_true', help='Strip coloring from output [Default=colors]') parser.add_argument( '--stereo-mode', choices=['j', 's', 'f', 'd', 'm', 'l', 'r'], help='Advanced stereo settings for Lame MP3 encoder only') parser.add_argument( '--stop-after', help='Stops script after a certain amount of time has passed ' '(e.g. 1h30m). Alternatively, accepts a specific time in 24hr ' 'format to stop after (e.g 03:30, 16:15)') parser.add_argument('-V', '--version', action='version', version=prog_version) encoding_group.add_argument( '--wav', action='store_true', help='Rip songs to uncompressed WAV file instead of MP3') parser.add_argument('--windows-safe', action='store_true', help='Make filename safe for Windows file system ' '(truncate filename to 255 characters)') encoding_group.add_argument( '--vorbis', action='store_true', help='Rip songs to Ogg Vorbis encoding instead of MP3') parser.add_argument( '-r', '--remove-from-playlist', action='store_true', help='[WARNING: SPOTIFY IS NOT PROPROGATING PLAYLIST CHANGES TO ' 'THEIR SERVERS] Delete tracks from playlist after successful ' 'ripping [Default=no]') parser.add_argument( 'uri', nargs="+", help='One or more Spotify URI(s) (either URI, a file of URIs or a ' 'search query)') args = parser.parse_args(remaining_argv) init_util_globals(args) # kind of a hack to get colorama stripping to work when outputting # to a file instead of stdout. Taken from initialise.py in colorama def wrap_stream(stream, convert, strip, autoreset, wrap): if wrap: wrapper = AnsiToWin32(stream, convert=convert, strip=strip, autoreset=autoreset) if wrapper.should_wrap(): stream = wrapper.stream return stream args.has_log = args.log is not None if args.has_log: if args.log == "-": init(strip=True) else: encoding = "ascii" if args.ascii else "utf-8" log_file = codecs.open(enc_str(args.log), 'a', encoding) sys.stdout = wrap_stream(log_file, None, True, False, True) else: init(strip=True if args.strip_colors else None) if args.ascii_path_only is True: args.ascii = True # unless explicitly told not to, we are going to encode # for utf-8 by default if not args.ascii and sys.version_info < (3, 0): sys.stdout = codecs.getwriter('utf-8')(sys.stdout) # small sanity check on user option if args.user is not None and args.user == "USER": print(Fore.RED + "Please pass your username as --user " + "<YOUR_USER_NAME> instead of --user USER " + "<YOUR_USER_NAME>..." + Fore.RESET) sys.exit(1) # give warning for broken feature if args.remove_from_playlist: print(Fore.RED + "--REMOVE-FROM-PLAYLIST WARNING:") print("SPOTIFY IS NOT PROPROGATING PLAYLIST CHANGES " "TO THEIR SERVERS.") print("YOU WILL NOT SEE ANY CHANGES TO YOUR PLAYLIST ON THE " + "OFFICIAL SPOTIFY DESKTOP OR WEB APP." + Fore.RESET) if args.wav: args.output_type = "wav" elif args.pcm: args.output_type = "pcm" elif args.flac: args.output_type = "flac" if args.comp == "10": args.comp = "8" elif args.vorbis: args.output_type = "ogg" if args.vbr == "0": args.vbr = "9" elif args.opus: args.output_type = "opus" if args.vbr == "0": args.vbr = "320" elif args.aac: args.output_type = "aac" if args.vbr == "0": args.vbr = "500" elif args.mp4: args.output_type = "m4a" if args.vbr == "0": args.vbr = "5" elif args.alac: args.output_type = "alac.m4a" else: args.output_type = "mp3" # check that encoder tool is available encoders = { "flac": ("flac", "flac"), "aiff": ("sox", "sox"), "aac": ("faac", "faac"), "ogg": ("oggenc", "vorbis-tools"), "opus": ("opusenc", "opus-tools"), "mp3": ("lame", "lame"), "m4a": ("fdkaac", "fdk-aac-encoder"), "alac.m4a": ("avconv", "libav-tools"), } if args.output_type in encoders.keys(): encoder = encoders[args.output_type][0] if which(encoder) is None: print(Fore.RED + "Missing dependency '" + encoder + "'. Please install and add to path..." + Fore.RESET) # assumes OS X or Ubuntu/Debian command_help = ("brew install " if sys.platform == "darwin" else "sudo apt-get install ") print("...try " + Fore.YELLOW + command_help + encoders[args.output_type][1] + Fore.RESET) sys.exit(1) # format string if args.flat: args.format = "{artist} - {track_name}.{ext}" elif args.flat_with_index: args.format = "{idx:3} - {artist} - {track_name}.{ext}" elif args.format is None: args.format = "{album_artist}/{album}/{artist} - {track_name}.{ext}" # print some settings print(Fore.GREEN + "Spotify Ripper - v" + prog_version + Fore.RESET) def encoding_output_str(): if args.output_type == "wav": return "WAV, Stereo 16bit 44100Hz" elif args.output_type == "pcm": return "Raw Headerless PCM, Stereo 16bit 44100Hz" else: if args.output_type == "flac": return "FLAC, Compression Level: " + args.comp elif args.output_type == "aiff": return "AIFF" elif args.output_type == "alac.m4a": return "Apple Lossless (ALAC)" elif args.output_type == "ogg": codec = "Ogg Vorbis" elif args.output_type == "opus": codec = "Opus" elif args.output_type == "mp3": codec = "MP3" elif args.output_type == "m4a": codec = "MPEG4 AAC" elif args.output_type == "aac": codec = "AAC" else: codec = "Unknown" if args.cbr: return codec + ", CBR " + args.bitrate + " kbps" else: return codec + ", VBR " + args.vbr print(Fore.YELLOW + " Encoding output:\t" + Fore.RESET + encoding_output_str()) print(Fore.YELLOW + " Spotify bitrate:\t" + Fore.RESET + args.quality + " kbps") def unicode_support_str(): if args.ascii_path_only: return "Unicode tags, ASCII file path" elif args.ascii: return "ASCII only" else: return "Yes" # check that --stop-after and --resume-after options are valid if args.stop_after is not None and \ parse_time_str(args.stop_after) is None: print(Fore.RED + "--stop-after option is not valid" + Fore.RESET) sys.exit(1) if args.resume_after is not None and \ parse_time_str(args.resume_after) is None: print(Fore.RED + "--resume-after option is not valid" + Fore.RESET) sys.exit(1) if args.play_token_resume is not None and \ parse_time_str(args.play_token_resume) is None: print(Fore.RED + "--play_token_resume option is not valid" + Fore.RESET) sys.exit(1) print(Fore.YELLOW + " Unicode support:\t" + Fore.RESET + unicode_support_str()) print(Fore.YELLOW + " Output directory:\t" + Fore.RESET + base_dir()) print(Fore.YELLOW + " Settings directory:\t" + Fore.RESET + settings_dir()) print(Fore.YELLOW + " Format String:\t" + Fore.RESET + args.format) print(Fore.YELLOW + " Overwrite files:\t" + Fore.RESET + ("Yes" if args.overwrite else "No")) # patch a bug when Python 3/MP4 if sys.version_info >= (3, 0) and \ (args.output_type == "m4a" or args.output_type == "alac.m4a"): patch_bug_in_mutagen() ripper = Ripper(args) ripper.start() # try to listen for terminal resize events # (needs to be called on main thread) if not args.has_log: ripper.progress.handle_resize() signal.signal(signal.SIGWINCH, ripper.progress.handle_resize) def hasStdinData(): return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []) def abort(set_logged_in=False): ripper.abort_rip() if set_logged_in: ripper.ripper_continue.set() ripper.join() sys.exit(1) def skip(): if ripper.ripping.is_set(): ripper.skip.set() # check if we were passed a file name or search def check_uri_args(): if len(args.uri) == 1 and path_exists(args.uri[0]): encoding = "ascii" if args.ascii else "utf-8" args.uri = [ line.strip() for line in codecs.open(enc_str(args.uri[0]), 'r', encoding) if not line.strip().startswith("#") and len(line.strip()) > 0 ] elif len(args.uri) == 1 and not args.uri[0].startswith("spotify:"): args.uri = [list(ripper.search_query(args.uri[0]))] # login and uri_parse on main thread to catch any KeyboardInterrupt try: if not ripper.login(): print(Fore.RED + "Encountered issue while logging into " "Spotify, aborting..." + Fore.RESET) abort(set_logged_in=True) else: check_uri_args() ripper.ripper_continue.set() except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort(set_logged_in=True) # wait for ripping thread to finish if not args.has_log: stdin_settings = termios.tcgetattr(sys.stdin) try: if not args.has_log: tty.setcbreak(sys.stdin.fileno()) while ripper.isAlive(): schedule.run_pending() # check if the escape button was pressed if not args.has_log and hasStdinData(): c = sys.stdin.read(1) if c == '\x1b': skip() ripper.join(0.1) except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort() finally: if not args.has_log: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, stdin_settings)
def main(prog_args=sys.argv[1:]): # in case we changed the location of the settings directory where the # config file lives, we need to parse this argument before we parse # the rest of the arguments (which can overwrite the options in the # config file) settings_parser = argparse.ArgumentParser(add_help=False) settings_parser.add_argument( '-S', '--settings', nargs=1, help='Path to settings, config and temp files directory ' '[Default=~/.spotify-ripper]') args, remaining_argv = settings_parser.parse_known_args(prog_args) init_util_globals(args) # load config file, overwriting any defaults defaults = { "bitrate": "320", "quality": "320", "comp": "10", "vbr": "0", } defaults = load_config(defaults) parser = argparse.ArgumentParser( prog='spotify-ripper', description='Rips Spotify URIs to MP3s with ID3 tags and album covers', parents=[settings_parser], formatter_class=argparse.RawTextHelpFormatter, epilog='''Example usage: rip a single file: spotify-ripper -u user -p password spotify:track:52xaypL0Kjzk0ngwv3oBPR rip entire playlist: spotify-ripper -u user -p password spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 rip a list of URIs: spotify-ripper -u user -p password list_of_uris.txt search for tracks to rip: spotify-ripper -l -Q 160 -o "album:Rumours track:'the chain'" ''') # create group to prevent user from using both the -l and -u option is_user_set = defaults.get('user') is not None is_last_set = defaults.get('last') is True if is_user_set or is_last_set: if is_user_set and is_last_set: print("spotify-ripper: error: one of the arguments -u/--user " "-l/--last is required") sys.exit(1) else: group = parser.add_mutually_exclusive_group(required=False) else: group = parser.add_mutually_exclusive_group(required=True) encoding_group = parser.add_mutually_exclusive_group(required=False) # set defaults parser.set_defaults(**defaults) prog_version = pkg_resources.require("spotify-ripper")[0].version parser.add_argument( '-a', '--ascii', action='store_true', help='Convert the file name and the metadata tags to ASCII ' 'encoding [Default=utf-8]') encoding_group.add_argument( '--aac', action='store_true', help='Rip songs to AAC format with FreeAAC instead of MP3') encoding_group.add_argument( '--alac', action='store_true', help='Rip songs to Apple Lossless format instead of MP3') parser.add_argument( '-A', '--ascii-path-only', action='store_true', help='Convert the file name (but not the metadata tags) to ASCII ' 'encoding [Default=utf-8]') parser.add_argument( '-b', '--bitrate', help='CBR bitrate [Default=320]') parser.add_argument( '-c', '--cbr', action='store_true', help='CBR encoding [Default=VBR]') parser.add_argument( '--comp', default="10", help='compression complexity for FLAC and Opus [Default=Max]') parser.add_argument( '--comment', nargs=1, help='Add custom metadata comment to all songs') parser.add_argument( '--cover-file', nargs=1, help='Save album cover image to file name (e.g "cover.jpg") ' '[Default=embed]') parser.add_argument( '-d', '--directory', nargs=1, help='Base directory where ripped MP3s are saved [Default=cwd]') parser.add_argument( '--fail-log', nargs=1, help="Logs the list of track URIs that failed to rip" ) encoding_group.add_argument( '--flac', action='store_true', help='Rip songs to lossless FLAC encoding instead of MP3') parser.add_argument( '-f', '--format', nargs=1, help='Save songs using this path and filename structure (see README)') parser.add_argument( '--flat', action='store_true', help='Save all songs to a single directory ' '(overrides --format option)') parser.add_argument( '--flat-with-index', action='store_true', help='Similar to --flat [-f] but includes the playlist index at ' 'the start of the song file') parser.add_argument( '-g', '--genres', nargs=1, choices=['artist', 'album'], help='Attempt to retrieve genre information from Spotify\'s ' 'Web API [Default=skip]') encoding_group.add_argument( '--id3-v23', action='store_true', help='Store ID3 tags using version v2.3 [Default=v2.4]') parser.add_argument( '-k', '--key', nargs=1, help='Path to Spotify application key file ' '[Default=Settings Directory]') group.add_argument( '-u', '--user', nargs=1, help='Spotify username') parser.add_argument( '-p', '--password', nargs=1, help='Spotify password [Default=ask interactively]') group.add_argument( '-l', '--last', action='store_true', help='Use last login credentials') parser.add_argument( '-L', '--log', nargs=1, help='Log in a log-friendly format to a file (use - to log to stdout)') encoding_group.add_argument( '--pcm', action='store_true', help='Saves a .pcm file with the raw PCM data instead of MP3') encoding_group.add_argument( '--mp4', action='store_true', help='Rip songs to MP4/M4A format with Fraunhofer FDK AAC codec ' 'instead of MP3') parser.add_argument( '--normalize', action='store_true', help='Normalize volume levels of tracks') parser.add_argument( '-o', '--overwrite', action='store_true', help='Overwrite existing MP3 files [Default=skip]') encoding_group.add_argument( '--opus', action='store_true', help='Rip songs to Opus encoding instead of MP3') parser.add_argument( '--playlist-m3u', action='store_true', help='create a m3u file when ripping a playlist') parser.add_argument( '--playlist-wpl', action='store_true', help='create a wpl file when ripping a playlist') parser.add_argument( '--playlist-sync', action='store_true', help='Sync playlist songs (rename and remove old songs)') parser.add_argument( '-q', '--vbr', help='VBR quality setting or target bitrate for Opus [Default=0]') parser.add_argument( '-Q', '--quality', choices=['160', '320', '96'], help='Spotify stream bitrate preference [Default=320]') parser.add_argument( '-s', '--strip-colors', action='store_true', help='Strip coloring from output [Default=colors]') parser.add_argument( '--stereo-mode', choices=['j', 's', 'f', 'd', 'm', 'l', 'r'], help='Advanced stereo settings for Lame MP3 encoder only') parser.add_argument( '-V', '--version', action='version', version=prog_version) encoding_group.add_argument( '--wav', action='store_true', help='Rip songs to uncompressed WAV file instead of MP3') encoding_group.add_argument( '--vorbis', action='store_true', help='Rip songs to Ogg Vorbis encoding instead of MP3') parser.add_argument( '-r', '--remove-from-playlist', action='store_true', help='Delete tracks from playlist after successful ' 'ripping [Default=no]') parser.add_argument( '-x', '--exclude-appears-on', action='store_true', help='Exclude albums that an artist \'appears on\' when passing ' 'a Spotify artist URI') parser.add_argument( 'uri', nargs="+", help='One or more Spotify URI(s) (either URI, a file of URIs or a ' 'search query)') args = parser.parse_args(remaining_argv) init_util_globals(args) # kind of a hack to get colorama stripping to work when outputting # to a file instead of stdout. Taken from initialise.py in colorama def wrap_stream(stream, convert, strip, autoreset, wrap): if wrap: wrapper = AnsiToWin32(stream, convert=convert, strip=strip, autoreset=autoreset) if wrapper.should_wrap(): stream = wrapper.stream return stream args.has_log = args.log is not None if args.has_log: if args.log[0] == "-": init(strip=True) else: log_file = open(args.log[0], 'a') sys.stdout = wrap_stream(log_file, None, True, False, True) else: init(strip=True if args.strip_colors else None) if args.ascii_path_only is True: args.ascii = True # unless explicitly told not to, we are going to encode # for utf-8 by default if not args.ascii and sys.version_info < (3, 0): sys.stdout = codecs.getwriter('utf-8')(sys.stdout) if args.wav: args.output_type = "wav" elif args.pcm: args.output_type = "pcm" elif args.flac: args.output_type = "flac" if args.comp == "10": args.comp = "8" elif args.vorbis: args.output_type = "ogg" if args.vbr == "0": args.vbr = "10" elif args.opus: args.output_type = "opus" if args.vbr == "0": args.vbr = "320" elif args.aac: args.output_type = "aac" if args.vbr == "0": args.vbr = "500" elif args.mp4: args.output_type = "m4a" if args.vbr == "0": args.vbr = "5" elif args.alac: args.output_type = "alac.m4a" else: args.output_type = "mp3" # check that encoder tool is available encoders = { "flac": ("flac", "flac"), "aac": ("faac", "faac"), "ogg": ("oggenc", "vorbis-tools"), "opus": ("opusenc", "opus-tools"), "mp3": ("lame", "lame"), "m4a": ("fdkaac", "fdk-aac-encoder"), "alac.m4a": ("avconv", "libav-tools"), } if args.output_type in encoders.keys(): encoder = encoders[args.output_type][0] if which(encoder) is None: print(Fore.RED + "Missing dependency '" + encoder + "'. Please install and add to path..." + Fore.RESET) # assumes OS X or Ubuntu/Debian command_help = ("brew install " if sys.platform == "darwin" else "sudo apt-get install ") print("...try " + Fore.YELLOW + command_help + encoders[args.output_type][1] + Fore.RESET) sys.exit(1) # format string if args.flat: args.format = ["{artist} - {track_name}.{ext}"] elif args.flat_with_index: args.format = ["{idx:3} - {artist} - {track_name}.{ext}"] elif args.format is None: args.format = ["{album_artist}/{album}/{artist} - {track_name}.{ext}"] # print some settings print(Fore.GREEN + "Spotify Ripper - v" + prog_version + Fore.RESET) def encoding_output_str(): if args.output_type == "wav": return "WAV, Stereo 16bit 44100Hz" elif args.output_type == "pcm": return "Raw Headerless PCM, Stereo 16bit 44100Hz" else: if args.output_type == "flac": return "FLAC, Compression Level: " + args.comp elif args.output_type == "alac.m4a": return "Apple Lossless (ALAC)" elif args.output_type == "ogg": codec = "Ogg Vorbis" elif args.output_type == "opus": codec = "Opus" elif args.output_type == "mp3": codec = "MP3" elif args.output_type == "m4a": codec = "MPEG4 AAC" elif args.output_type == "aac": codec = "AAC" else: codec = "Unknown" if args.cbr: return codec + ", CBR " + args.bitrate + " kbps" else: return codec + ", VBR " + args.vbr print(Fore.YELLOW + " Encoding output:\t" + Fore.RESET + encoding_output_str()) print(Fore.YELLOW + " Spotify bitrate:\t" + Fore.RESET + args.quality + " kbps") def unicode_support_str(): if args.ascii_path_only: return "Unicode tags, ASCII file path" elif args.ascii: return "ASCII only" else: return "Yes" print(Fore.YELLOW + " Unicode support:\t" + Fore.RESET + unicode_support_str()) print(Fore.YELLOW + " Output directory:\t" + Fore.RESET + base_dir()) print(Fore.YELLOW + " Settings directory:\t" + Fore.RESET + settings_dir()) print(Fore.YELLOW + " Format String:\t" + Fore.RESET + args.format[0]) print(Fore.YELLOW + " Overwrite files:\t" + Fore.RESET + ("Yes" if args.overwrite else "No")) # patch a bug when Python 3/MP4 if sys.version_info >= (3, 0) and args.output_type == "m4a": patch_bug_in_mutagen() ripper = Ripper(args) ripper.start() # try to listen for terminal resize events # (needs to be called on main thread) if not args.has_log: ripper.progress.handle_resize() signal.signal(signal.SIGWINCH, ripper.progress.handle_resize) def abort(set_logged_in=False): ripper.abort_rip() if set_logged_in: ripper.logged_in.set() ripper.join() sys.exit(1) # login on main thread to catch any KeyboardInterrupt try: if not ripper.login(): print( Fore.RED + "Encountered issue while logging into " "Spotify, aborting..." + Fore.RESET) abort(set_logged_in=True) except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort(set_logged_in=True) # wait for ripping thread to finish try: while ripper.isAlive(): schedule.run_pending() ripper.join(0.1) except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort()
def main(prog_args=sys.argv[1:]): # in case we changed the location of the settings directory where the config file lives, we need to parse this argument # before we parse the rest of the arguments (which can overwrite the options in the config file) settings_parser = argparse.ArgumentParser(add_help=False) settings_parser.add_argument('-S', '--settings', nargs=1, help='Path to settings, config and temp files directory [Default=~/.spotify-ripper]') args, remaining_argv = settings_parser.parse_known_args(prog_args) # load config file, overwriting any defaults defaults = { "bitrate": "320", "vbr": "0", } defaults = load_config(args, defaults) parser = argparse.ArgumentParser(prog='spotify-ripper', description='Rips Spotify URIs to MP3s with ID3 tags and album covers', parents=[settings_parser], formatter_class=argparse.RawTextHelpFormatter, epilog='''Example usage: rip a single file: spotify-ripper -u user -p password spotify:track:52xaypL0Kjzk0ngwv3oBPR rip entire playlist: spotify-ripper -u user -p password spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 rip a list of URIs: spotify-ripper -u user -p password list_of_uris.txt search for tracks to rip: spotify-ripper -l -b 160 -o "album:Rumours track:'the chain'" ''') # create group to prevent to prevent user from using both the -l and -u option is_user_set = defaults.get('user') is not None is_last_set = defaults.get('last') is True if is_user_set or is_last_set: if is_user_set and is_last_set: print("spotify-ripper: error: one of the arguments -u/--user -l/--last is required") sys.exit(1) else: group = parser.add_mutually_exclusive_group(required=False) else: group = parser.add_mutually_exclusive_group(required=True) # set defaults parser.set_defaults(**defaults) parser.add_argument('-a', '--ascii', action='store_true', help='Convert the file name and the ID3 tag to ASCII encoding [Default=utf-8]') parser.add_argument('-A', '--ascii-path-only', action='store_true', help='Convert the file name (but not the ID3 tag) to ASCII encoding [Default=utf-8]') parser.add_argument('-b', '--bitrate', choices=['160', '320', '96'], help='Bitrate rip quality [Default=320]') parser.add_argument('-c', '--cbr', action='store_true', help='Lame CBR encoding [Default=VBR]') parser.add_argument('-d', '--directory', nargs=1, help='Base directory where ripped MP3s are saved [Default=cwd]') parser.add_argument('-f', '--flat', action='store_true', help='Save all songs to a single directory instead of organizing by album/artist/song') parser.add_argument('-F', '--flat-with-index', action='store_true', help='Similar to --flat [-f] but includes the playlist index at the start of the song file') parser.add_argument('-g', '--genres', nargs=1, choices=['artist', 'album'], help='Attempt to retrieve genre information from Spotify\'s Web API [Default=skip]') parser.add_argument('-k', '--key', nargs=1, help='Path to Spotify application key file [Default=cwd]') group.add_argument('-u', '--user', nargs=1, help='Spotify username') parser.add_argument('-p', '--password', nargs=1, help='Spotify password [Default=ask interactively]') group.add_argument('-l', '--last', action='store_true', help='Use last login credentials') parser.add_argument('-L', '--log', nargs=1, help='Log in a log-friendly format to a file (use - to log to stdout)') parser.add_argument('-m', '--pcm', action='store_true', help='Saves a .pcm file with the raw PCM data') parser.add_argument('-o', '--overwrite', action='store_true', help='Overwrite existing MP3 files [Default=skip]') parser.add_argument('-s', '--strip-colors', action='store_true', help='Strip coloring from output[Default=colors]') parser.add_argument('-v', '--vbr', help='Lame VBR encoding quality setting [Default=0]') parser.add_argument('-V', '--version', action='version', version=pkg_resources.require("spotify-ripper")[0].version) parser.add_argument('-r', '--remove-from-playlist', action='store_true', help='Delete tracks from playlist after successful ripping [Default=no]') parser.add_argument('uri', nargs="+", help='One or more Spotify URI(s) (either URI, a file of URIs or a search query)') args = parser.parse_args(remaining_argv) # kind of a hack to get colorama stripping to work when outputting # to a file instead of stdout. Taken from initialise.py in colorama def wrap_stream(stream, convert, strip, autoreset, wrap): if wrap: wrapper = AnsiToWin32(stream, convert=convert, strip=strip, autoreset=autoreset) if wrapper.should_wrap(): stream = wrapper.stream return stream args.has_log = args.log is not None if args.has_log: if args.log[0] == "-": init(strip=True) else: log_file = open(args.log[0], 'a') sys.stdout = wrap_stream(log_file, None, True, False, True) else: init(strip=True if args.strip_colors else None) if args.ascii_path_only is True: args.ascii = True ripper = Ripper(args) ripper.start() # try to listen for terminal resize events (needs to be called on main thread) if not args.has_log: ripper.progress.handle_resize() signal.signal(signal.SIGWINCH, ripper.progress.handle_resize) # wait for ripping thread to finish try: while not ripper.finished: schedule.run_pending() time.sleep(0.1) except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) ripper.abort() sys.exit(1)
def main(prog_args=sys.argv[1:]): # in case we changed the location of the settings directory where the config file lives, we need to parse this argument # before we parse the rest of the arguments (which can overwrite the options in the config file) settings_parser = argparse.ArgumentParser(add_help=False) settings_parser.add_argument('-S', '--settings', nargs=1, help='Path to settings, config and temp files directory [Default=~/.spotify-ripper]') args, remaining_argv = settings_parser.parse_known_args(prog_args) # load config file, overwriting any defaults defaults = { "bitrate": "320", "quality": "320", "vbr": "0", } defaults = load_config(args, defaults) parser = argparse.ArgumentParser(prog='spotify-ripper', description='Rips Spotify URIs to MP3s with ID3 tags and album covers', parents=[settings_parser], formatter_class=argparse.RawTextHelpFormatter, epilog='''Example usage: rip a single file: spotify-ripper -u user -p password spotify:track:52xaypL0Kjzk0ngwv3oBPR rip entire playlist: spotify-ripper -u user -p password spotify:user:username:playlist:4vkGNcsS8lRXj4q945NIA4 rip a list of URIs: spotify-ripper -u user -p password list_of_uris.txt search for tracks to rip: spotify-ripper -l -b 160 -o "album:Rumours track:'the chain'" ''') # create group to prevent user from using both the -l and -u option is_user_set = defaults.get('user') is not None is_last_set = defaults.get('last') is True if is_user_set or is_last_set: if is_user_set and is_last_set: print("spotify-ripper: error: one of the arguments -u/--user -l/--last is required") sys.exit(1) else: group = parser.add_mutually_exclusive_group(required=False) else: group = parser.add_mutually_exclusive_group(required=True) encoding_group = parser.add_mutually_exclusive_group(required=False) # set defaults parser.set_defaults(**defaults) prog_version = pkg_resources.require("spotify-ripper")[0].version parser.add_argument('-a', '--ascii', action='store_true', help='Convert the file name and the metadata tags to ASCII encoding [Default=utf-8]') encoding_group.add_argument('--aac', action='store_true', help='Rip songs to AAC format with FreeAAC instead of MP3') parser.add_argument('-A', '--ascii-path-only', action='store_true', help='Convert the file name (but not the metadata tags) to ASCII encoding [Default=utf-8]') parser.add_argument('-b', '--bitrate', help='CBR bitrate [Default=320]') parser.add_argument('-c', '--cbr', action='store_true', help='CBR encoding [Default=VBR]') parser.add_argument('-d', '--directory', nargs=1, help='Base directory where ripped MP3s are saved [Default=cwd]') encoding_group.add_argument('--flac', action='store_true', help='Rip songs to lossless FLAC encoding instead of MP3') parser.add_argument('-f', '--flat', action='store_true', help='Save all songs to a single directory instead of organizing by album/artist/song') parser.add_argument('-F', '--flat-with-index', action='store_true', help='Similar to --flat [-f] but includes the playlist index at the start of the song file') parser.add_argument('-g', '--genres', nargs=1, choices=['artist', 'album'], help='Attempt to retrieve genre information from Spotify\'s Web API [Default=skip]') parser.add_argument('-k', '--key', nargs=1, help='Path to Spotify application key file [Default=cwd]') group.add_argument('-u', '--user', nargs=1, help='Spotify username') parser.add_argument('-p', '--password', nargs=1, help='Spotify password [Default=ask interactively]') group.add_argument('-l', '--last', action='store_true', help='Use last login credentials') parser.add_argument('-L', '--log', nargs=1, help='Log in a log-friendly format to a file (use - to log to stdout)') parser.add_argument('-m', '--pcm', action='store_true', help='Saves a .pcm file with the raw PCM data') encoding_group.add_argument('--mp4', action='store_true', help='Rip songs to MP4 format with Fraunhofer FDK AAC codec instead of MP3') parser.add_argument('-o', '--overwrite', action='store_true', help='Overwrite existing MP3 files [Default=skip]') encoding_group.add_argument('--opus', action='store_true', help='Rip songs to Opus encoding instead of MP3') parser.add_argument('-q', '--vbr', help='VBR quality setting or target bitrate for Opus [Default=0]') parser.add_argument('-Q', '--quality', choices=['160', '320', '96'], help='Spotify stream bitrate preference [Default=320]') parser.add_argument('-s', '--strip-colors', action='store_true', help='Strip coloring from output[Default=colors]') parser.add_argument('-V', '--version', action='version', version=prog_version) encoding_group.add_argument('--vorbis', action='store_true', help='Rip songs to Ogg Vorbis encoding instead of MP3') parser.add_argument('-r', '--remove-from-playlist', action='store_true', help='Delete tracks from playlist after successful ripping [Default=no]') parser.add_argument('-x', '--exclude-appears-on', action='store_true', help='Exclude albums that an artist \'appears on\' when passing a Spotify artist URI') parser.add_argument('uri', nargs="+", help='One or more Spotify URI(s) (either URI, a file of URIs or a search query)') args = parser.parse_args(remaining_argv) # kind of a hack to get colorama stripping to work when outputting # to a file instead of stdout. Taken from initialise.py in colorama def wrap_stream(stream, convert, strip, autoreset, wrap): if wrap: wrapper = AnsiToWin32(stream, convert=convert, strip=strip, autoreset=autoreset) if wrapper.should_wrap(): stream = wrapper.stream return stream args.has_log = args.log is not None if args.has_log: if args.log[0] == "-": init(strip=True) else: log_file = open(args.log[0], 'a') sys.stdout = wrap_stream(log_file, None, True, False, True) else: init(strip=True if args.strip_colors else None) if args.ascii_path_only is True: args.ascii = True if args.flac: args.output_type = "flac" elif args.vorbis: args.output_type = "ogg" if args.vbr == "0": args.vbr = "10" elif args.opus: args.output_type = "opus" if args.vbr == "0": args.vbr = "320" elif args.aac: args.output_type = "aac" if args.vbr == "0": args.vbr = "500" elif args.mp4: args.output_type = "m4a" if args.vbr == "0": args.vbr = "5" else: args.output_type = "mp3" # check that encoder tool is available encoders = { "flac": "flac", "aac": "faac", "ogg": "oggenc", "opus": "opusenc", "mp3": "lame", "m4a": "fdkaac" } encoder = encoders[args.output_type] if which(encoder) is None: print(Fore.RED + "Missing dependency '" + encoder + "'. Please install and add to path..." + Fore.RESET) sys.exit(1) # print some settings print(Fore.GREEN + "Spotify Ripper - v" + prog_version + Fore.RESET) def encoding_output_str(): if args.output_type == "flac": return "FLAC" else: if args.output_type == "ogg": codec = "Ogg Vorbis" elif args.output_type == "opus": codec = "Opus" elif args.output_type == "mp3": codec = "MP3" elif args.output_type == "m4a": codec = "MPEG4 AAC" elif args.output_type == "aac": codec = "AAC" if args.cbr: return codec + " CBR " + args.bitrate + " kbps" else: return codec + " VBR " + args.vbr return "" print(Fore.YELLOW + " Encoding output:\t" + Fore.RESET + encoding_output_str()) print(Fore.YELLOW + " Spotify bitrate:\t" + Fore.RESET + args.quality + " kbps") def unicode_support_str(): if args.ascii_path_only: return "Unicode tags, ASCII file path" elif args.ascii: return "ASCII only" else: return "Yes" print(Fore.YELLOW + " Unicode support:\t" + Fore.RESET + unicode_support_str()) print(Fore.YELLOW + " Output directory:\t" + Fore.RESET + (norm_path(args.directory[0]) if args.directory != None else os.getcwd())) print(Fore.YELLOW + " Settings directory:\t" + Fore.RESET + (norm_path(args.settings[0]) if args.settings != None else default_settings_dir())) def export_org_str(): if args.flat: return "Single directory" elif args.flat_with_index: return "Single directory (with playlist index)" else: return "Artist/album/song" print(Fore.YELLOW + " Export Organization:\t" + Fore.RESET + export_org_str()) print(Fore.YELLOW + " Overwrite files:\t" + Fore.RESET + ("Yes" if args.overwrite else "No")) ripper = Ripper(args) ripper.start() # try to listen for terminal resize events (needs to be called on main thread) if not args.has_log: ripper.progress.handle_resize() signal.signal(signal.SIGWINCH, ripper.progress.handle_resize) # wait for ripping thread to finish try: while not ripper.finished: schedule.run_pending() time.sleep(0.1) except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) ripper.abort() sys.exit(1)
def main(): prog_version = pkg_resources.require("spotify-ripper")[0].version # load config file, overwriting any defaults defaults = { "bitrate": "320", "quality": "320", "comp": "10", "vbr": "0", "partial_check": "weak", } defaults = load_config(defaults) spotipy_envs = [ "SPOTIPY_CLIENT_ID", "SPOTIPY_CLIENT_SECRET", "SPOTIPY_REDIRECT_URI" ] for spotipy_env in spotipy_envs: if spotipy_env not in os.environ: value = defaults.get(spotipy_env.lower()) if value: os.environ[spotipy_env] = value parser = argparse.ArgumentParser( prog='spotify-ripper', description= 'Rips Spotify URIs to media files with tags and album covers') # create group to prevent user from using both the -l and -u option is_user_set = defaults.get('user') is not None is_last_set = defaults.get('last') is True if is_user_set or is_last_set: if is_user_set and is_last_set: print("spotify-ripper: error: one of the arguments -u/--user " "-l/--last is required") sys.exit(1) else: group = parser.add_mutually_exclusive_group(required=False) else: group = parser.add_mutually_exclusive_group(required=True) encoding_group = parser.add_mutually_exclusive_group(required=False) # set defaults parser.set_defaults(**defaults) # Positional arguments parser.add_argument( 'uri', nargs="+", help= 'One or more Spotify URI(s) (either URI, a file of URIs or a search query)' ) # Optional arguments parser.add_argument( '-a', '--ascii', action='store_true', help= 'Convert the file name and the metadata tags to ASCII encoding [Default=utf-8]' ) encoding_group.add_argument( '--aac', action='store_true', help='Rip songs to AAC format with FreeAAC instead of MP3') encoding_group.add_argument( '--aiff', action='store_true', help='Rip songs to lossless AIFF encoding instead of MP3') encoding_group.add_argument( '--alac', action='store_true', help='Rip songs to Apple Lossless format instead of MP3') parser.add_argument( '--all-artists', action='store_true', help= 'Store all artists, rather than just the main artist, in the track\'s metadata tag' ) parser.add_argument( '--artist-album-type', help= 'Only load albums of specified types when passing a Spotify artist URI [Default=album,single,ep,compilation,appears_on]' ) parser.add_argument( '--artist-album-market', help= 'Only load albums with the specified ISO2 country code when passing a Spotify artist URI. You may get duplicate albums if not set. [Default=any]' ) parser.add_argument( '-A', '--ascii-path-only', action='store_true', help= 'Convert the file name (but not the metadata tags) to ASCII encoding [Default=utf-8]' ) parser.add_argument('-b', '--bitrate', help='CBR bitrate [Default=320]') parser.add_argument('-c', '--cbr', action='store_true', help='CBR encoding [Default=VBR]') parser.add_argument( '--comp', help='compression complexity for FLAC and Opus [Default=Max]') parser.add_argument( '--comment', help= 'Set comment metadata tag to all songs. Can include same tags as --format.' ) parser.add_argument( '--cover-file', help= 'Save album cover image to file name (e.g "cover.jpg") [Default=embed]' ) parser.add_argument( '--cover-file-and-embed', metavar="COVER_FILE", help='Same as --cover-file but embeds the cover image too') parser.add_argument( '-d', '--directory', help='Base directory where ripped MP3s are saved [Default=cwd]') parser.add_argument('--fail-log', help="Logs the list of track URIs that failed to rip") encoding_group.add_argument( '--flac', action='store_true', help='Rip songs to lossless FLAC encoding instead of MP3') parser.add_argument( '-f', '--format', help='Save songs using this path and filename structure (see README)') parser.add_argument( '--format-case', choices=['upper', 'lower', 'capitalize'], help= 'Convert all words of the file name to upper-case, lower-case, or capitalized' ) parser.add_argument( '--flat', action='store_true', help='Save all songs to a single directory (overrides --format option)' ) parser.add_argument( '--flat-with-index', action='store_true', help= 'Similar to --flat [-f] but includes the playlist index at the start of the song file' ) parser.add_argument( '-g', '--genres', choices=['artist', 'album'], help= 'Attempt to retrieve genre information from Spotify\'s Web API [Default=skip]' ) parser.add_argument( '--grouping', help= 'Set grouping metadata tag to all songs. Can include same tags as --format.' ) encoding_group.add_argument( '--id3-v23', action='store_true', help='Store ID3 tags using version v2.3 [Default=v2.4]') parser.add_argument( '--large-cover-art', action='store_true', help= 'Attempt to retrieve 640x640 cover art from Spotify\'s Web API [Default=300x300]' ) group.add_argument('-l', '--last', action='store_true', help='Use last login credentials') parser.add_argument( '-L', '--log', help='Log in a log-friendly format to a file (use - to log to stdout)') encoding_group.add_argument( '--pcm', action='store_true', help='Saves a .pcm file with the raw PCM data instead of MP3') encoding_group.add_argument( '--mp4', action='store_true', help= 'Rip songs to MP4/M4A format with Fraunhofer FDK AAC codec instead of MP3' ) parser.add_argument('--normalize', action='store_true', help='Normalize volume levels of tracks') parser.add_argument( '-na', '--normalized-ascii', action='store_true', help= 'Convert the file name to normalized ASCII with unicodedata.normalize (NFKD)' ) parser.add_argument('-o', '--overwrite', action='store_true', help='Overwrite existing MP3 files [Default=skip]') encoding_group.add_argument( '--opus', action='store_true', help='Rip songs to Opus encoding instead of MP3') parser.add_argument( '--partial-check', choices=['none', 'weak', 'strict'], help= 'Check for and overwrite partially ripped files. "weak" will err on the side of not re-ripping the file if it is unsure, whereas "strict" will re-rip the file [Default=weak]' ) parser.add_argument('-p', '--password', help='Spotify password [Default=ask interactively]') parser.add_argument( '--play-token-resume', metavar="RESUME_AFTER", help= 'If the \'play token\' is lost to a different device using the same Spotify account, the script will wait a specified amount of time before restarting. This argument takes the same values as --resume-after [Default=abort]' ) parser.add_argument('--playlist-m3u', action='store_true', help='create a m3u file when ripping a playlist') parser.add_argument('--playlist-wpl', action='store_true', help='create a wpl file when ripping a playlist') parser.add_argument( '--playlist-sync', action='store_true', help='Sync playlist songs (rename and remove old songs)') parser.add_argument( '--plus-pcm', action='store_true', help='Saves a .pcm file in addition to the encoded file (e.g. mp3)') parser.add_argument( '--plus-wav', action='store_true', help='Saves a .wav file in addition to the encoded file (e.g. mp3)') parser.add_argument( '-q', '--vbr', help='VBR quality setting or target bitrate for Opus [Default=0]') parser.add_argument('-Q', '--quality', choices=['160', '320', '96'], help='Spotify stream bitrate preference [Default=320]') parser.add_argument( '--remove-offline-cache', action='store_true', help= 'Remove libspotify\'s offline cache directory after the rip is complete to save disk space' ) parser.add_argument( '--resume-after', help= 'Resumes script after a certain amount of time has passed after stopping (e.g. 1h30m). Alternatively, accepts a specific time in 24hr format to start after (e.g 03:30, 16:15). Requires --stop-after option to be set' ) parser.add_argument( '-R', '--replace', nargs="+", required=False, help= 'pattern to replace the output filename separated by "/". The following example replaces all spaces with "_" and all "-" with ".": spotify-ripper --replace " /_" "\-/." uri' ) parser.add_argument('-s', '--strip-colors', action='store_true', help='Strip coloring from output [Default=colors]') parser.add_argument( '--stereo-mode', choices=['j', 's', 'f', 'd', 'm', 'l', 'r'], help='Advanced stereo settings for Lame MP3 encoder only') parser.add_argument( '--stop-after', help= 'Stops script after a certain amount of time has passed (e.g. 1h30m). Alternatively, accepts a specific time in 24hr format to stop after (e.g 03:30, 16:15)' ) parser.add_argument( '--timeout', type=int, help= 'Override the PySpotify timeout value in seconds (Default=10 seconds)') group.add_argument('-u', '--user', help='Spotify username') parser.add_argument('-V', '--version', action='version', version=prog_version) encoding_group.add_argument( '--wav', action='store_true', help='Rip songs to uncompressed WAV file instead of MP3') parser.add_argument( '--windows-safe', action='store_true', help= 'Make filename safe for Windows file system (truncate filename to 255 characters)' ) encoding_group.add_argument( '--vorbis', action='store_true', help='Rip songs to Ogg Vorbis encoding instead of MP3') parser.add_argument( '-r', '--remove-from-playlist', action='store_true', help='Delete tracks from playlist after successful ripping [Default=no]' ) args = parser.parse_args() init_util_globals(args) # kind of a hack to get colorama stripping to work when outputting # to a file instead of stdout. Taken from initialise.py in colorama def wrap_stream(stream, convert, strip, autoreset, wrap): if wrap: wrapper = AnsiToWin32(stream, convert=convert, strip=strip, autoreset=autoreset) if wrapper.should_wrap(): stream = wrapper.stream return stream args.has_log = args.log is not None if args.has_log: if args.log == "-": init(strip=True) else: encoding = "ascii" if args.ascii else "utf-8" log_file = codecs.open(enc_str(args.log), 'a', encoding) sys.stdout = wrap_stream(log_file, None, True, False, True) else: init(strip=True if args.strip_colors else None) if args.ascii_path_only is True: args.ascii = True if args.wav: args.output_type = "wav" elif args.pcm: args.output_type = "pcm" elif args.flac: args.output_type = "flac" if args.comp == "10": args.comp = "8" elif args.vorbis: args.output_type = "ogg" if args.vbr == "0": args.vbr = "9" elif args.opus: args.output_type = "opus" if args.vbr == "0": args.vbr = "320" elif args.aac: args.output_type = "aac" if args.vbr == "0": args.vbr = "500" elif args.mp4: args.output_type = "m4a" if args.vbr == "0": args.vbr = "5" elif args.alac: args.output_type = "alac.m4a" elif args.aiff: args.output_type = "aiff" else: args.output_type = "mp3" # check that encoder tool is available encoders = { "flac": ("flac", "flac"), "aiff": ("sox", "sox"), "aac": ("faac", "faac"), "ogg": ("oggenc", "vorbis-tools"), "opus": ("opusenc", "opus-tools"), "mp3": ("lame", "lame"), "m4a": ("fdkaac", "aac-enc"), "alac.m4a": ("ffmpeg", "ffmpeg"), } if args.output_type in encoders.keys(): encoder = encoders[args.output_type][0] if which(encoder) is None: print(Fore.RED + "Missing dependency '" + encoder + "'. Please install '" + encoders[args.output_type][1] + "'." + Fore.RESET) sys.exit(1) # format string if args.flat: args.format = "{artist} - {track_name}.{ext}" elif args.flat_with_index: args.format = "{idx:3} - {artist} - {track_name}.{ext}" elif args.format is None: args.format = "{album_artist}/{album}/{artist} - {track_name}.{ext}" # print some settings print(Fore.GREEN + "Spotify Ripper - v" + prog_version + Fore.RESET) def encoding_output_str(): if args.output_type == "wav": return "WAV, Stereo 16bit 44100Hz" elif args.output_type == "pcm": return "Raw Headerless PCM, Stereo 16bit 44100Hz" else: if args.output_type == "flac": return "FLAC, Compression Level: " + args.comp elif args.output_type == "aiff": return "AIFF" elif args.output_type == "alac.m4a": return "Apple Lossless (ALAC)" elif args.output_type == "ogg": codec = "Ogg Vorbis" elif args.output_type == "opus": codec = "Opus" elif args.output_type == "mp3": codec = "MP3" elif args.output_type == "m4a": codec = "MPEG4 AAC" elif args.output_type == "aac": codec = "AAC" else: codec = "Unknown" if args.cbr: return codec + ", CBR " + args.bitrate + " kbps" else: return codec + ", VBR " + args.vbr print(Fore.YELLOW + " Encoding output:\t" + Fore.RESET + encoding_output_str()) print(Fore.YELLOW + " Spotify bitrate:\t" + Fore.RESET + args.quality + " kbps") def unicode_support_str(): if args.ascii_path_only: return "Unicode tags, ASCII file path" elif args.ascii: return "ASCII only" else: return "Yes" # check that --stop-after and --resume-after options are valid if args.stop_after is not None and \ parse_time_str(args.stop_after) is None: print(Fore.RED + "--stop-after option is not valid" + Fore.RESET) sys.exit(1) if args.resume_after is not None and \ parse_time_str(args.resume_after) is None: print(Fore.RED + "--resume-after option is not valid" + Fore.RESET) sys.exit(1) if args.play_token_resume is not None and \ parse_time_str(args.play_token_resume) is None: print(Fore.RED + "--play_token_resume option is not valid" + Fore.RESET) sys.exit(1) print(Fore.YELLOW + " Unicode support:\t" + Fore.RESET + unicode_support_str()) print(Fore.YELLOW + " Output directory:\t" + Fore.RESET + base_dir()) print(Fore.YELLOW + " Settings directory:\t" + Fore.RESET + settings_dir()) print(Fore.YELLOW + " Format String:\t" + Fore.RESET + args.format) print(Fore.YELLOW + " Overwrite files:\t" + Fore.RESET + ("Yes" if args.overwrite else "No")) ripper = Ripper(args) ripper.start() # try to listen for terminal resize events # (needs to be called on main thread) if not args.has_log: ripper.progress.handle_resize() signal.signal(signal.SIGWINCH, ripper.progress.handle_resize) def hasStdinData(): return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []) def abort(set_logged_in=False): ripper.abort_rip() if set_logged_in: ripper.ripper_continue.set() ripper.join() sys.exit(1) def skip(): if ripper.ripping.is_set(): ripper.skip.set() # check if we were passed a file name or search def check_uri_args(): if len(args.uri) == 1 and path_exists(args.uri[0]): encoding = "ascii" if args.ascii else "utf-8" args.uri = [ line.strip() for line in codecs.open(enc_str(args.uri[0]), 'r', encoding) if not line.strip().startswith("#") and len(line.strip()) > 0 ] elif len(args.uri) == 1 and not args.uri[0].startswith("spotify:"): args.uri = [list(ripper.search_query(args.uri[0]))] # login and uri_parse on main thread to catch any KeyboardInterrupt try: if not ripper.login(): print(Fore.RED + "Encountered issue while logging into " "Spotify, aborting..." + Fore.RESET) abort(set_logged_in=True) else: check_uri_args() ripper.ripper_continue.set() except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort(set_logged_in=True) # wait for ripping thread to finish if not args.has_log: try: stdin_settings = termios.tcgetattr(sys.stdin) except termios.error: stdin_settings = None try: if not args.has_log and stdin_settings: tty.setcbreak(sys.stdin.fileno()) while ripper.is_alive(): schedule.run_pending() # check if the escape button was pressed if not args.has_log and hasStdinData(): c = sys.stdin.read(1) if c == '\x1b': skip() ripper.join(0.1) except (KeyboardInterrupt, Exception) as e: if not isinstance(e, KeyboardInterrupt): print(str(e)) print("\n" + Fore.RED + "Aborting..." + Fore.RESET) abort() finally: if not args.has_log and stdin_settings: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, stdin_settings)