Accept additional command-line options

This commit is contained in:
Ritiek Malhotra
2020-04-12 14:13:21 +05:30
parent 0a8a0db54e
commit a253c308a6
13 changed files with 108 additions and 55 deletions

View File

@@ -16,12 +16,16 @@ def match_arguments(arguments):
track_file=arguments.list track_file=arguments.list
) )
else: else:
list_dl = downloader.ListDownloader( command_line.helpers.download_tracks_from_file(
tracks_file=arguments.list, arguments.list,
skip_file=arguments.skip, arguments,
write_successful_file=arguments.write_successful,
) )
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: elif arguments.playlist:
spotify_tools.write_playlist( spotify_tools.write_playlist(
playlist_url=arguments.playlist, text_file=arguments.write_to playlist_url=arguments.playlist, text_file=arguments.write_to

View File

@@ -134,6 +134,7 @@ def get_arguments(argv=None, to_merge=True):
"-e", "-e",
"--encoder", "--encoder",
default=config["encoder"], default=config["encoder"],
choices={"ffmpeg", "avconv", "null"},
help="use this encoder for conversion", help="use this encoder for conversion",
) )
parser.add_argument( parser.add_argument(
@@ -145,21 +146,29 @@ def get_arguments(argv=None, to_merge=True):
parser.add_argument( parser.add_argument(
"--overwrite", "--overwrite",
default=config["overwrite"], default=config["overwrite"],
help="change the overwrite policy",
choices={"prompt", "force", "skip"}, 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( parser.add_argument(
"-i", "-i",
"--input-ext", "--input-ext",
default=config["input-ext"], default=config["input-ext"],
help="preferred input format 'm4a' or 'webm' (Opus)", choices={"automatic", "m4a", "opus"},
choices={"m4a", "webm"}, help="preferred input format",
) )
parser.add_argument( parser.add_argument(
"-o", "-o",
"--output-ext", "--output-ext",
default=config["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( parser.add_argument(
"--write-to", "--write-to",
@@ -310,7 +319,7 @@ def run_errands(parser, parsed):
if not encoder_exists: if not encoder_exists:
# log.warn("Specified encoder () was not found. Will not encode to specified " # log.warn("Specified encoder () was not found. Will not encode to specified "
# "output format".format(parsed.encoder)) # "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 song_parameter_passed = parsed.song is not None and parsed.tracks is None
if song_parameter_passed: if song_parameter_passed:

View File

@@ -1,6 +1,7 @@
from spotdl.metadata.providers import ProviderSpotify from spotdl.metadata.providers import ProviderSpotify
from spotdl.metadata.providers import ProviderYouTube from spotdl.metadata.providers import ProviderYouTube
from spotdl.metadata.embedders import EmbedderDefault from spotdl.metadata.embedders import EmbedderDefault
from spotdl.encode.encoders import EncoderFFmpeg, EncoderAvconv
from spotdl.lyrics.providers import LyricWikia from spotdl.lyrics.providers import LyricWikia
from spotdl.lyrics.providers import Genius from spotdl.lyrics.providers import Genius
@@ -8,6 +9,7 @@ from spotdl.track import Track
import spotdl.util import spotdl.util
import os
import urllib.request import urllib.request
import threading import threading
@@ -46,34 +48,57 @@ def download_track_from_metadata(metadata, arguments):
track = Track(metadata, cache_albumart=(not arguments.no_metadata)) track = Track(metadata, cache_albumart=(not arguments.no_metadata))
# log.info(log_fmt) # 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( filename = spotdl.util.format_string(
arguments.file_format, arguments.file_format,
metadata, metadata,
output_extension=arguments.output_ext output_extension=output_extension
) )
# log.info(filename)
if arguments.dry_run: to_skip = arguments.dry_run
return if not to_skip and os.path.isfile(filename):
if arguments.overwrite == "force":
if os.path.isfile(filename): to_skip = False
if arguments.overwrite == "skip":
to_skip = True
elif arguments.overwrite == "prompt": elif arguments.overwrite == "prompt":
to_skip = not input("overwrite? (y/N)").lower() == "y" to_skip = not input("overwrite? (y/N)").lower() == "y"
else:
to_skip = True
if to_skip: if to_skip:
return return
if arguments.no_encode: if Encoder is None:
track.download(filename) track.download(stream, filename)
else: else:
track.download_while_re_encoding( track.download_while_re_encoding(
stream,
filename, filename,
target_encoding=arguments.output_ext target_encoding=output_extension,
encoder=Encoder()
) )
if not arguments.no_metadata: 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): 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") # log.warning("Failed. Will retry after other songs\n")
tracks.append(current_track) tracks.append(current_track)
else: else:
# TODO: CONFIG.YML if arguments.write_successful:
# Write track to config.write_sucessful with open(arguments.write_successful, "a") as fout:
pass fout.write(current_track)
finally: finally:
with open(path, "w") as fout: with open(path, "w") as fout:
fout.writelines(tracks) fout.writelines(tracks)

View File

@@ -14,7 +14,8 @@ DEFAULT_CONFIGURATION = {
"encoder": "ffmpeg", "encoder": "ffmpeg",
"directory": spotdl.util.get_music_dir(), "directory": spotdl.util.get_music_dir(),
"overwrite": "prompt", "overwrite": "prompt",
"input-ext": "m4a", "quality": "best",
"input-ext": "automatic",
"output-ext": "mp3", "output-ext": "mp3",
"write-to": None, "write-to": None,
"trim-silence": False, "trim-silence": False,
@@ -22,8 +23,8 @@ DEFAULT_CONFIGURATION = {
"dry-run": False, "dry-run": False,
"music-videos-only": False, "music-videos-only": False,
"no-spaces": False, "no-spaces": False,
"file-format": "{artist} - {track_name}.{output_ext}", "file-format": "{artist} - {track-name}.{output-ext}",
"search-format": "{artist} - {track_name} lyrics", "search-format": "{artist} - {track-name} lyrics",
"youtube-api-key": None, "youtube-api-key": None,
"skip": None, "skip": None,
"write-successful": None, "write-successful": None,

View File

@@ -9,7 +9,7 @@ from spotdl.encode.exceptions import FFmpegNotFoundError
RULES = { RULES = {
"m4a": { "m4a": {
"mp3": "-codec:v copy -codec:a libmp3lame -ar 48000", "mp3": "-codec:v copy -codec:a libmp3lame -ar 48000",
"webm": "-codec:a libopus -vbr on", "opus": "-codec:a libopus -vbr on",
"m4a": "-acodec copy", "m4a": "-acodec copy",
"flac": "-codec:a flac -ar 48000", "flac": "-codec:a flac -ar 48000",
}, },

View File

@@ -26,7 +26,7 @@ class TestEncodingDefaults:
@pytest.mark.parametrize("files, expected_command", [ @pytest.mark.parametrize("files, expected_command", [
(("test.m4a", "test.mp3"), encode_command("test.m4a", "test.mp3")), (("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")), (("bla bla.m4a", "ble ble.m4a"), encode_command("bla bla.m4a", "ble ble.m4a")),
(("😛.m4a", "• tongue.flac"), encode_command("😛.m4a", "• tongue.flac")), (("😛.m4a", "• tongue.flac"), encode_command("😛.m4a", "• tongue.flac")),
]) ])
@@ -47,7 +47,7 @@ class TestEncodingInDebugMode:
@pytest.mark.parametrize("files, expected_command", [ @pytest.mark.parametrize("files, expected_command", [
(("test.m4a", "test.mp3"), debug_encode_command("test.m4a", "test.mp3")), (("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")), (("bla bla.m4a", "ble ble.m4a"), debug_encode_command("bla bla.m4a", "ble ble.m4a")),
(("😛.m4a", "• tongue.flac"), debug_encode_command("😛.m4a", "• tongue.flac")), (("😛.m4a", "• tongue.flac"), debug_encode_command("😛.m4a", "• tongue.flac")),
]) ])

View File

@@ -28,7 +28,7 @@ class TestEncodingDefaults:
] ]
return command return command
def m4a_to_webm_encoder(input_path, target_path): def m4a_to_opus_encoder(input_path, target_path):
command = [ command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic', 'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_path, '-i', input_path,
@@ -36,7 +36,7 @@ class TestEncodingDefaults:
'-vbr', 'on', '-vbr', 'on',
'-b:a', '192k', '-b:a', '192k',
'-vn', '-vn',
'-f', 'webm', '-f', 'opus',
target_path target_path
] ]
return command return command
@@ -68,7 +68,7 @@ class TestEncodingDefaults:
@pytest.mark.parametrize("files, expected_command", [ @pytest.mark.parametrize("files, expected_command", [
(("test.m4a", "test.mp3"), m4a_to_mp3_encoder("test.m4a", "test.mp3")), (("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")), (("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")), (("😛.m4a", "• tongue.flac"), m4a_to_flac_encoder("😛.m4a", "• tongue.flac")),
]) ])
@@ -92,7 +92,7 @@ class TestEncodingInDebugMode:
] ]
return command 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 = [ command = [
'ffmpeg', '-y', '-nostdin', '-loglevel', 'debug', 'ffmpeg', '-y', '-nostdin', '-loglevel', 'debug',
'-i', input_path, '-i', input_path,
@@ -100,7 +100,7 @@ class TestEncodingInDebugMode:
'-vbr', 'on', '-vbr', 'on',
'-b:a', '192k', '-b:a', '192k',
'-vn', '-vn',
'-f', 'webm', '-f', 'opus',
target_path target_path
] ]
return command return command
@@ -132,7 +132,7 @@ class TestEncodingInDebugMode:
@pytest.mark.parametrize("files, expected_command", [ @pytest.mark.parametrize("files, expected_command", [
(("test.m4a", "test.mp3"), m4a_to_mp3_encoder_with_debug("test.m4a", "test.mp3")), (("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")), (("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")), (("😛.m4a", "• tongue.flac"), m4a_to_flac_encoder_with_debug("😛.m4a", "• tongue.flac")),
]) ])
@@ -158,7 +158,7 @@ class TestEncodingAndTrimSilence:
] ]
return command 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 = [ command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic', 'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_path, '-i', input_path,
@@ -167,7 +167,7 @@ class TestEncodingAndTrimSilence:
'-b:a', '192k', '-b:a', '192k',
'-vn', '-vn',
'-af', 'silenceremove=start_periods=1', '-af', 'silenceremove=start_periods=1',
'-f', 'webm', '-f', 'opus',
target_path target_path
] ]
return command return command
@@ -201,7 +201,7 @@ class TestEncodingAndTrimSilence:
@pytest.mark.parametrize("files, expected_command", [ @pytest.mark.parametrize("files, expected_command", [
(("test.m4a", "test.mp3"), m4a_to_mp3_encoder_and_trim_silence("test.m4a", "test.mp3")), (("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")), (("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")), (("😛.m4a", "• tongue.flac"), m4a_to_flac_encoder_and_trim_silence("😛.m4a", "• tongue.flac")),
]) ])

View File

@@ -77,7 +77,7 @@ class TestMethods:
@pytest.mark.parametrize("filename, encoding", [ @pytest.mark.parametrize("filename, encoding", [
("example.m4a", "m4a"), ("example.m4a", "m4a"),
("exampley.mp3", "mp3"), ("exampley.mp3", "mp3"),
("test 123.webm", "webm"), ("test 123.opus", "opus"),
("flakey.flac", "flac"), ("flakey.flac", "flac"),
]) ])
def test_get_encoding(self, encoderkid, filename, encoding): def test_get_encoding(self, encoderkid, filename, encoding):

View File

@@ -10,7 +10,7 @@ class EmbedderBase(ABC):
The subclass must define the supported media file encoding The subclass must define the supported media file encoding
formats here using a static variable - such as: formats here using a static variable - such as:
>>> supported_formats = ("mp3", "opus", "flac") >>> supported_formats = ("mp3", "m4a", "flac")
""" """
supported_formats = () supported_formats = ()
@@ -72,9 +72,9 @@ class EmbedderBase(ABC):
""" """
raise NotImplementedError 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. a subclass.
Other methods for additional supported formats must also Other methods for additional supported formats must also

View File

@@ -37,7 +37,7 @@ for key in M4A_TAG_PRESET.keys():
class EmbedderDefault(EmbedderBase): class EmbedderDefault(EmbedderBase):
supported_formats = ("mp3", "opus", "flac") supported_formats = ("mp3", "m4a", "flac")
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -102,10 +102,10 @@ class EmbedderDefault(EmbedderBase):
audiofile.save(v2_version=3) 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. """ """ Embed metadata to M4A files. """
audiofile = MP4(path) 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"]: if metadata["year"]:
audiofile[M4A_TAG_PRESET["year"]] = metadata["year"] audiofile[M4A_TAG_PRESET["year"]] = metadata["year"]
provider = metadata["provider"] provider = metadata["provider"]

View File

@@ -117,11 +117,27 @@ class YouTubeStreams(StreamsBase):
request.add_header(*header) request.add_header(*header)
return urllib.request.urlopen(request) return urllib.request.urlopen(request)
def getbest(self): def get(self, quality="best", preftype="automatic"):
return self.all[0] if quality == "best":
return self.getbest(preftype=preftype)
elif quality == "worst":
return self.getworst(preftype=preftype)
else:
return None
def getworst(self): def getbest(self, preftype="automatic"):
return self.all[-1] 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): class ProviderYouTube(ProviderBase):

View File

@@ -62,7 +62,7 @@ class TestMethods:
@pytest.mark.parametrize("fmt_method_suffix", ( @pytest.mark.parametrize("fmt_method_suffix", (
"as_mp3", "as_mp3",
"as_opus", "as_m4a",
"as_flac", "as_flac",
)) ))
def test_embed_formats(self, fmt_method_suffix, embedderkid): def test_embed_formats(self, fmt_method_suffix, embedderkid):

View File

@@ -37,9 +37,8 @@ class Track:
def _calculate_total_chunks(self, filesize): def _calculate_total_chunks(self, filesize):
return (filesize // self._chunksize) + 1 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): encoder=EncoderFFmpeg(), show_progress=True):
stream = self.metadata["streams"].getbest()
total_chunks = self._calculate_total_chunks(stream["filesize"]) total_chunks = self._calculate_total_chunks(stream["filesize"])
process = encoder.re_encode_from_stdin( process = encoder.re_encode_from_stdin(
stream["encoding"], stream["encoding"],
@@ -54,8 +53,7 @@ class Track:
process.stdin.close() process.stdin.close()
process.wait() process.wait()
def download(self, target_path, show_progress=True): def download(self, stream, target_path, show_progress=True):
stream = self.metadata["streams"].getbest()
total_chunks = self._calculate_total_chunks(stream["filesize"]) total_chunks = self._calculate_total_chunks(stream["filesize"])
response = stream["connection"] response = stream["connection"]
with open(target_path, "wb") as fout: with open(target_path, "wb") as fout: