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", "comp": "10", "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 -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') 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]') 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-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) # 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) sys.stdout = codecs.getwriter('utf-8')(sys.stdout) 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 = "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", "flac"), "aac": ("faac", "faac"), "ogg": ("oggenc", "vorbis-tools"), "opus": ("opusenc", "opus-tools"), "mp3": ("lame", "lame"), "m4a": ("fdkaac", "fdk-aac-encoder"), } 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 == "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(args)) print(Fore.YELLOW + " Settings directory:\t" + Fore.RESET + settings_dir(args)) 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) # 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", "comp": "10", "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( '--comp', default="10", help='compression complexity for FLAC and Opus [Default=Max]') 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', '--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]') 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)') 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( '-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( '--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) # 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.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" 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", "fdkaac"), } 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 == "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 + (norm_path(args.directory[0]) if args.directory is not None else os.getcwd())) print(Fore.YELLOW + " Settings directory:\t" + Fore.RESET + (norm_path(args.settings[0]) if args.settings is not None else default_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) # 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", "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", "comp": "10", "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 -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-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) # 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(args)) print(Fore.YELLOW + " Settings directory:\t" + Fore.RESET + settings_dir(args)) 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) # 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)