From a253c308a6110fa39bf2461d1f45242ab2fec82e Mon Sep 17 00:00:00 2001 From: Ritiek Malhotra Date: Sun, 12 Apr 2020 14:13:21 +0530 Subject: [PATCH] Accept additional command-line options --- spotdl/command_line/__main__.py | 14 +++-- spotdl/command_line/arguments.py | 19 +++++-- spotdl/command_line/helpers.py | 53 ++++++++++++++----- spotdl/config.py | 7 +-- spotdl/encode/encoders/ffmpeg.py | 2 +- spotdl/encode/encoders/tests/test_avconv.py | 4 +- spotdl/encode/encoders/tests/test_ffmpeg.py | 18 +++---- spotdl/encode/tests/test_encode_base.py | 2 +- spotdl/metadata/embedder_base.py | 6 +-- spotdl/metadata/embedders/default_embedder.py | 6 +-- spotdl/metadata/providers/youtube.py | 24 +++++++-- spotdl/metadata/tests/test_embedder_base.py | 2 +- spotdl/track.py | 6 +-- 13 files changed, 108 insertions(+), 55 deletions(-) diff --git a/spotdl/command_line/__main__.py b/spotdl/command_line/__main__.py index e181237..b6626c1 100644 --- a/spotdl/command_line/__main__.py +++ b/spotdl/command_line/__main__.py @@ -16,12 +16,16 @@ def match_arguments(arguments): track_file=arguments.list ) else: - list_dl = downloader.ListDownloader( - tracks_file=arguments.list, - skip_file=arguments.skip, - write_successful_file=arguments.write_successful, + command_line.helpers.download_tracks_from_file( + arguments.list, + arguments, ) - list_dl.download_list() + # list_dl = downloader.ListDownloader( + # tracks_file=arguments.list, + # skip_file=arguments.skip, + # write_successful_file=arguments.write_successful, + # ) + # list_dl.download_list() elif arguments.playlist: spotify_tools.write_playlist( playlist_url=arguments.playlist, text_file=arguments.write_to diff --git a/spotdl/command_line/arguments.py b/spotdl/command_line/arguments.py index 0bff9eb..92c2aaa 100644 --- a/spotdl/command_line/arguments.py +++ b/spotdl/command_line/arguments.py @@ -134,6 +134,7 @@ def get_arguments(argv=None, to_merge=True): "-e", "--encoder", default=config["encoder"], + choices={"ffmpeg", "avconv", "null"}, help="use this encoder for conversion", ) parser.add_argument( @@ -145,21 +146,29 @@ def get_arguments(argv=None, to_merge=True): parser.add_argument( "--overwrite", default=config["overwrite"], - help="change the overwrite policy", choices={"prompt", "force", "skip"}, + help="change the overwrite policy", + ) + parser.add_argument( + "-q", + "--quality", + default=config["quality"], + choices={"best", "worst"}, + help="preferred audio quality", ) parser.add_argument( "-i", "--input-ext", default=config["input-ext"], - help="preferred input format 'm4a' or 'webm' (Opus)", - choices={"m4a", "webm"}, + choices={"automatic", "m4a", "opus"}, + help="preferred input format", ) parser.add_argument( "-o", "--output-ext", default=config["output-ext"], - help="preferred output format: 'mp3', 'm4a' (AAC), 'flac', etc.", + choices={"mp3", "m4a", "flac"}, + help="preferred output format", ) parser.add_argument( "--write-to", @@ -310,7 +319,7 @@ def run_errands(parser, parsed): if not encoder_exists: # log.warn("Specified encoder () was not found. Will not encode to specified " # "output format".format(parsed.encoder)) - parsed.output_ext = parsed.input_ext + parsed.encoder = "null" song_parameter_passed = parsed.song is not None and parsed.tracks is None if song_parameter_passed: diff --git a/spotdl/command_line/helpers.py b/spotdl/command_line/helpers.py index ce3685b..a90e8a8 100644 --- a/spotdl/command_line/helpers.py +++ b/spotdl/command_line/helpers.py @@ -1,6 +1,7 @@ from spotdl.metadata.providers import ProviderSpotify from spotdl.metadata.providers import ProviderYouTube from spotdl.metadata.embedders import EmbedderDefault +from spotdl.encode.encoders import EncoderFFmpeg, EncoderAvconv from spotdl.lyrics.providers import LyricWikia from spotdl.lyrics.providers import Genius @@ -8,6 +9,7 @@ from spotdl.track import Track import spotdl.util +import os import urllib.request import threading @@ -46,34 +48,57 @@ def download_track_from_metadata(metadata, arguments): track = Track(metadata, cache_albumart=(not arguments.no_metadata)) # log.info(log_fmt) + stream = metadata["streams"].get( + quality=arguments.quality, + preftype=arguments.input_ext, + ) + # log.info(stream) + + Encoder = { + "ffmpeg": EncoderFFmpeg, + "avconv": EncoderAvconv, + }.get(arguments.encoder) + + if Encoder is None: + output_extension = stream["encoding"] + else: + output_extension = arguments.output_ext + filename = spotdl.util.format_string( arguments.file_format, metadata, - output_extension=arguments.output_ext + output_extension=output_extension ) + # log.info(filename) - if arguments.dry_run: - return - - if os.path.isfile(filename): - if arguments.overwrite == "skip": - to_skip = True + to_skip = arguments.dry_run + if not to_skip and os.path.isfile(filename): + if arguments.overwrite == "force": + to_skip = False elif arguments.overwrite == "prompt": to_skip = not input("overwrite? (y/N)").lower() == "y" + else: + to_skip = True if to_skip: return - if arguments.no_encode: - track.download(filename) + if Encoder is None: + track.download(stream, filename) else: track.download_while_re_encoding( + stream, filename, - target_encoding=arguments.output_ext + target_encoding=output_extension, + encoder=Encoder() ) if not arguments.no_metadata: - track.apply_metadata(filename, encoding=arguments.output_ext) + try: + track.apply_metadata(filename, encoding=output_extension) + except TypeError: + # log.warning("Cannot write metadata to given file") + pass def download_tracks_from_file(path, arguments): @@ -142,9 +167,9 @@ def download_tracks_from_file(path, arguments): # log.warning("Failed. Will retry after other songs\n") tracks.append(current_track) else: - # TODO: CONFIG.YML - # Write track to config.write_sucessful - pass + if arguments.write_successful: + with open(arguments.write_successful, "a") as fout: + fout.write(current_track) finally: with open(path, "w") as fout: fout.writelines(tracks) diff --git a/spotdl/config.py b/spotdl/config.py index 605558e..8944deb 100644 --- a/spotdl/config.py +++ b/spotdl/config.py @@ -14,7 +14,8 @@ DEFAULT_CONFIGURATION = { "encoder": "ffmpeg", "directory": spotdl.util.get_music_dir(), "overwrite": "prompt", - "input-ext": "m4a", + "quality": "best", + "input-ext": "automatic", "output-ext": "mp3", "write-to": None, "trim-silence": False, @@ -22,8 +23,8 @@ DEFAULT_CONFIGURATION = { "dry-run": False, "music-videos-only": False, "no-spaces": False, - "file-format": "{artist} - {track_name}.{output_ext}", - "search-format": "{artist} - {track_name} lyrics", + "file-format": "{artist} - {track-name}.{output-ext}", + "search-format": "{artist} - {track-name} lyrics", "youtube-api-key": None, "skip": None, "write-successful": None, diff --git a/spotdl/encode/encoders/ffmpeg.py b/spotdl/encode/encoders/ffmpeg.py index 5725e5c..467f6dd 100644 --- a/spotdl/encode/encoders/ffmpeg.py +++ b/spotdl/encode/encoders/ffmpeg.py @@ -9,7 +9,7 @@ from spotdl.encode.exceptions import FFmpegNotFoundError RULES = { "m4a": { "mp3": "-codec:v copy -codec:a libmp3lame -ar 48000", - "webm": "-codec:a libopus -vbr on", + "opus": "-codec:a libopus -vbr on", "m4a": "-acodec copy", "flac": "-codec:a flac -ar 48000", }, diff --git a/spotdl/encode/encoders/tests/test_avconv.py b/spotdl/encode/encoders/tests/test_avconv.py index ca1f058..2292992 100644 --- a/spotdl/encode/encoders/tests/test_avconv.py +++ b/spotdl/encode/encoders/tests/test_avconv.py @@ -26,7 +26,7 @@ class TestEncodingDefaults: @pytest.mark.parametrize("files, expected_command", [ (("test.m4a", "test.mp3"), encode_command("test.m4a", "test.mp3")), - (("abc.m4a", "cba.webm"), encode_command("abc.m4a", "cba.webm")), + (("abc.m4a", "cba.opus"), encode_command("abc.m4a", "cba.opus")), (("bla bla.m4a", "ble ble.m4a"), encode_command("bla bla.m4a", "ble ble.m4a")), (("😛.m4a", "• tongue.flac"), encode_command("😛.m4a", "• tongue.flac")), ]) @@ -47,7 +47,7 @@ class TestEncodingInDebugMode: @pytest.mark.parametrize("files, expected_command", [ (("test.m4a", "test.mp3"), debug_encode_command("test.m4a", "test.mp3")), - (("abc.m4a", "cba.webm"), debug_encode_command("abc.m4a", "cba.webm")), + (("abc.m4a", "cba.opus"), debug_encode_command("abc.m4a", "cba.opus")), (("bla bla.m4a", "ble ble.m4a"), debug_encode_command("bla bla.m4a", "ble ble.m4a")), (("😛.m4a", "• tongue.flac"), debug_encode_command("😛.m4a", "• tongue.flac")), ]) diff --git a/spotdl/encode/encoders/tests/test_ffmpeg.py b/spotdl/encode/encoders/tests/test_ffmpeg.py index 5a8bddc..d44ce82 100644 --- a/spotdl/encode/encoders/tests/test_ffmpeg.py +++ b/spotdl/encode/encoders/tests/test_ffmpeg.py @@ -28,7 +28,7 @@ class TestEncodingDefaults: ] return command - def m4a_to_webm_encoder(input_path, target_path): + def m4a_to_opus_encoder(input_path, target_path): command = [ 'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic', '-i', input_path, @@ -36,7 +36,7 @@ class TestEncodingDefaults: '-vbr', 'on', '-b:a', '192k', '-vn', - '-f', 'webm', + '-f', 'opus', target_path ] return command @@ -68,7 +68,7 @@ class TestEncodingDefaults: @pytest.mark.parametrize("files, expected_command", [ (("test.m4a", "test.mp3"), m4a_to_mp3_encoder("test.m4a", "test.mp3")), - (("abc.m4a", "cba.webm"), m4a_to_webm_encoder("abc.m4a", "cba.webm")), + (("abc.m4a", "cba.opus"), m4a_to_opus_encoder("abc.m4a", "cba.opus")), (("bla bla.m4a", "ble ble.m4a"), m4a_to_m4a_encoder("bla bla.m4a", "ble ble.m4a")), (("😛.m4a", "• tongue.flac"), m4a_to_flac_encoder("😛.m4a", "• tongue.flac")), ]) @@ -92,7 +92,7 @@ class TestEncodingInDebugMode: ] return command - def m4a_to_webm_encoder_with_debug(input_path, target_path): + def m4a_to_opus_encoder_with_debug(input_path, target_path): command = [ 'ffmpeg', '-y', '-nostdin', '-loglevel', 'debug', '-i', input_path, @@ -100,7 +100,7 @@ class TestEncodingInDebugMode: '-vbr', 'on', '-b:a', '192k', '-vn', - '-f', 'webm', + '-f', 'opus', target_path ] return command @@ -132,7 +132,7 @@ class TestEncodingInDebugMode: @pytest.mark.parametrize("files, expected_command", [ (("test.m4a", "test.mp3"), m4a_to_mp3_encoder_with_debug("test.m4a", "test.mp3")), - (("abc.m4a", "cba.webm"), m4a_to_webm_encoder_with_debug("abc.m4a", "cba.webm")), + (("abc.m4a", "cba.opus"), m4a_to_opus_encoder_with_debug("abc.m4a", "cba.opus")), (("bla bla.m4a", "ble ble.m4a"), m4a_to_m4a_encoder_with_debug("bla bla.m4a", "ble ble.m4a")), (("😛.m4a", "• tongue.flac"), m4a_to_flac_encoder_with_debug("😛.m4a", "• tongue.flac")), ]) @@ -158,7 +158,7 @@ class TestEncodingAndTrimSilence: ] return command - def m4a_to_webm_encoder_and_trim_silence(input_path, target_path): + def m4a_to_opus_encoder_and_trim_silence(input_path, target_path): command = [ 'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic', '-i', input_path, @@ -167,7 +167,7 @@ class TestEncodingAndTrimSilence: '-b:a', '192k', '-vn', '-af', 'silenceremove=start_periods=1', - '-f', 'webm', + '-f', 'opus', target_path ] return command @@ -201,7 +201,7 @@ class TestEncodingAndTrimSilence: @pytest.mark.parametrize("files, expected_command", [ (("test.m4a", "test.mp3"), m4a_to_mp3_encoder_and_trim_silence("test.m4a", "test.mp3")), - (("abc.m4a", "cba.webm"), m4a_to_webm_encoder_and_trim_silence("abc.m4a", "cba.webm")), + (("abc.m4a", "cba.opus"), m4a_to_opus_encoder_and_trim_silence("abc.m4a", "cba.opus")), (("bla bla.m4a", "ble ble.m4a"), m4a_to_m4a_encoder_and_trim_silence("bla bla.m4a", "ble ble.m4a")), (("😛.m4a", "• tongue.flac"), m4a_to_flac_encoder_and_trim_silence("😛.m4a", "• tongue.flac")), ]) diff --git a/spotdl/encode/tests/test_encode_base.py b/spotdl/encode/tests/test_encode_base.py index 21b813a..46d00bc 100644 --- a/spotdl/encode/tests/test_encode_base.py +++ b/spotdl/encode/tests/test_encode_base.py @@ -77,7 +77,7 @@ class TestMethods: @pytest.mark.parametrize("filename, encoding", [ ("example.m4a", "m4a"), ("exampley.mp3", "mp3"), - ("test 123.webm", "webm"), + ("test 123.opus", "opus"), ("flakey.flac", "flac"), ]) def test_get_encoding(self, encoderkid, filename, encoding): diff --git a/spotdl/metadata/embedder_base.py b/spotdl/metadata/embedder_base.py index 77be69e..37b7792 100644 --- a/spotdl/metadata/embedder_base.py +++ b/spotdl/metadata/embedder_base.py @@ -10,7 +10,7 @@ class EmbedderBase(ABC): The subclass must define the supported media file encoding formats here using a static variable - such as: - >>> supported_formats = ("mp3", "opus", "flac") + >>> supported_formats = ("mp3", "m4a", "flac") """ supported_formats = () @@ -72,9 +72,9 @@ class EmbedderBase(ABC): """ raise NotImplementedError - def as_opus(self, path, metadata, cached_albumart=None): + def as_m4a(self, path, metadata, cached_albumart=None): """ - Method for opus support. This method might be defined in + Method for m4a support. This method might be defined in a subclass. Other methods for additional supported formats must also diff --git a/spotdl/metadata/embedders/default_embedder.py b/spotdl/metadata/embedders/default_embedder.py index d024244..cceac53 100644 --- a/spotdl/metadata/embedders/default_embedder.py +++ b/spotdl/metadata/embedders/default_embedder.py @@ -37,7 +37,7 @@ for key in M4A_TAG_PRESET.keys(): class EmbedderDefault(EmbedderBase): - supported_formats = ("mp3", "opus", "flac") + supported_formats = ("mp3", "m4a", "flac") def __init__(self): super().__init__() @@ -102,10 +102,10 @@ class EmbedderDefault(EmbedderBase): audiofile.save(v2_version=3) - def as_opus(self, path, cached_albumart=None): + def as_m4a(self, path, metadata, cached_albumart=None): """ Embed metadata to M4A files. """ audiofile = MP4(path) - self._embed_basic_metadata(audiofile, metadata, "opus", preset=M4A_TAG_PRESET) + self._embed_basic_metadata(audiofile, metadata, "m4a", preset=M4A_TAG_PRESET) if metadata["year"]: audiofile[M4A_TAG_PRESET["year"]] = metadata["year"] provider = metadata["provider"] diff --git a/spotdl/metadata/providers/youtube.py b/spotdl/metadata/providers/youtube.py index 410d35e..3e34cad 100644 --- a/spotdl/metadata/providers/youtube.py +++ b/spotdl/metadata/providers/youtube.py @@ -117,11 +117,27 @@ class YouTubeStreams(StreamsBase): request.add_header(*header) return urllib.request.urlopen(request) - def getbest(self): - return self.all[0] + def get(self, quality="best", preftype="automatic"): + if quality == "best": + return self.getbest(preftype=preftype) + elif quality == "worst": + return self.getworst(preftype=preftype) + else: + return None - def getworst(self): - return self.all[-1] + def getbest(self, preftype="automatic"): + if preftype == "automatic": + return self.all[0] + for stream in self.all: + if stream["encoding"] == preftype: + return stream + + def getworst(self, preftype="automatic"): + if preftype == "automatic": + return self.all[-1] + for stream in self.all[::-1]: + if stream["encoding"] == preftype: + return stream class ProviderYouTube(ProviderBase): diff --git a/spotdl/metadata/tests/test_embedder_base.py b/spotdl/metadata/tests/test_embedder_base.py index 6cb5f18..295df7a 100644 --- a/spotdl/metadata/tests/test_embedder_base.py +++ b/spotdl/metadata/tests/test_embedder_base.py @@ -62,7 +62,7 @@ class TestMethods: @pytest.mark.parametrize("fmt_method_suffix", ( "as_mp3", - "as_opus", + "as_m4a", "as_flac", )) def test_embed_formats(self, fmt_method_suffix, embedderkid): diff --git a/spotdl/track.py b/spotdl/track.py index 8661f0a..e89a378 100644 --- a/spotdl/track.py +++ b/spotdl/track.py @@ -37,9 +37,8 @@ class Track: def _calculate_total_chunks(self, filesize): return (filesize // self._chunksize) + 1 - def download_while_re_encoding(self, target_path, target_encoding=None, + def download_while_re_encoding(self, stream, target_path, target_encoding=None, encoder=EncoderFFmpeg(), show_progress=True): - stream = self.metadata["streams"].getbest() total_chunks = self._calculate_total_chunks(stream["filesize"]) process = encoder.re_encode_from_stdin( stream["encoding"], @@ -54,8 +53,7 @@ class Track: process.stdin.close() process.wait() - def download(self, target_path, show_progress=True): - stream = self.metadata["streams"].getbest() + def download(self, stream, target_path, show_progress=True): total_chunks = self._calculate_total_chunks(stream["filesize"]) response = stream["connection"] with open(target_path, "wb") as fout: