Basic downloading

This commit is contained in:
Ritiek Malhotra
2020-04-08 08:00:43 +05:30
parent 121fcdcdf6
commit 51da0b7a29
22 changed files with 868 additions and 619 deletions

View File

View File

@@ -0,0 +1,57 @@
def match_args():
if const.args.song:
for track in const.args.song:
track_dl = downloader.Downloader(raw_song=track)
track_dl.download_single()
elif const.args.list:
if const.args.write_m3u:
youtube_tools.generate_m3u(
track_file=const.args.list
)
else:
list_dl = downloader.ListDownloader(
tracks_file=const.args.list,
skip_file=const.args.skip,
write_successful_file=const.args.write_successful,
)
list_dl.download_list()
elif const.args.playlist:
spotify_tools.write_playlist(
playlist_url=const.args.playlist, text_file=const.args.write_to
)
elif const.args.album:
spotify_tools.write_album(
album_url=const.args.album, text_file=const.args.write_to
)
elif const.args.all_albums:
spotify_tools.write_all_albums_from_artist(
artist_url=const.args.all_albums, text_file=const.args.write_to
)
elif const.args.username:
spotify_tools.write_user_playlist(
username=const.args.username, text_file=const.args.write_to
)
def main():
const.args = handle.get_arguments()
internals.filter_path(const.args.folder)
youtube_tools.set_api_key()
logzero.setup_default_logger(formatter=const._formatter, level=const.args.log_level)
try:
match_args()
# actually we don't necessarily need this, but yeah...
# explicit is better than implicit!
sys.exit(0)
except KeyboardInterrupt as e:
# log.exception(e)
sys.exit(3)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,301 @@
from logzero import logger as log
import appdirs
import logging
import argparse
import mimetypes
import os
import sys
import spotdl.util
import spotdl.config
_LOG_LEVELS_STR = ("INFO", "WARNING", "ERROR", "DEBUG")
def log_leveller(log_level_str):
logging_levels = [logging.INFO, logging.WARNING, logging.ERROR, logging.DEBUG]
log_level_str_index = _LOG_LEVELS_STR.index(log_level_str)
logging_level = logging_levels[log_level_str_index]
return logging_level
def override_config(config_file, parser, argv=None):
""" Override default dict with config dict passed as comamnd line argument. """
config_file = os.path.realpath(config_file)
config = spotdl.util.merge(DEFAULT_CONFIGURATION["spotify-downloader"], spotdl.config.get_config(config_file))
parser.set_defaults(**config)
return parser.parse_args(argv)
def get_arguments(argv=None, to_group=True, to_merge=True):
parser = argparse.ArgumentParser(
description="Download and convert tracks from Spotify, Youtube etc.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
if to_merge:
config_file = spotdl.config.default_config_file
config_dir = os.path.dirname(spotdl.config.default_config_file)
os.makedirs(os.path.dirname(spotdl.config.default_config_file), exist_ok=True)
config = spotdl.util.merge(
spotdl.config.DEFAULT_CONFIGURATION["spotify-downloader"],
spotdl.config.get_config(config_file)
)
else:
config = spotdl.config.DEFAULT_CONFIGURATION["spotify-downloader"]
if to_group:
group = parser.add_mutually_exclusive_group(required=True)
# TODO: --song is deprecated. Remove in future versions.
# Use --track instead.
group.add_argument(
"-s",
"--song",
nargs="+",
help=argparse.SUPPRESS
)
group.add_argument(
"-t",
"--track",
nargs="+",
help="download track by spotify link or name"
)
group.add_argument(
"-l",
"--list",
help="download tracks from a file"
)
group.add_argument(
"-p",
"--playlist",
help="load tracks from playlist URL into <playlist_name>.txt",
)
group.add_argument(
"-b", "--album", help="load tracks from album URL into <album_name>.txt"
)
group.add_argument(
"-ab",
"--all-albums",
help="load all tracks from artist URL into <artist_name>.txt",
)
group.add_argument(
"-u",
"--username",
help="load tracks from user's playlist into <playlist_name>.txt",
)
parser.add_argument(
"--write-m3u",
help="generate an .m3u playlist file with youtube links given "
"a text file containing tracks",
action="store_true",
)
parser.add_argument(
"-m",
"--manual",
default=config["manual"],
help="choose the track to download manually from a list of matching tracks",
action="store_true",
)
parser.add_argument(
"-nr",
"--no-remove-original",
default=config["no-remove-original"],
help="do not remove the original file after conversion",
action="store_true",
)
parser.add_argument(
"-nm",
"--no-metadata",
default=config["no-metadata"],
help="do not embed metadata in tracks",
action="store_true",
)
parser.add_argument(
"-nf",
"--no-fallback-metadata",
default=config["no-fallback-metadata"],
help="do not use YouTube as fallback for metadata if track not found on Spotify",
action="store_true",
)
parser.add_argument(
"-a",
"--avconv",
default=config["avconv"],
help="use avconv for conversion (otherwise defaults to ffmpeg)",
action="store_true",
)
parser.add_argument(
"-f",
"--directory",
default=os.path.abspath(config["directory"]),
help="path to directory where downloaded tracks will be stored in",
)
parser.add_argument(
"--overwrite",
default=config["overwrite"],
help="change the overwrite policy",
choices={"prompt", "force", "skip"},
)
parser.add_argument(
"-i",
"--input-ext",
default=config["input-ext"],
help="preferred input format .m4a or .webm (Opus)",
choices={".m4a", ".webm"},
)
parser.add_argument(
"-o",
"--output-ext",
default=config["output-ext"],
help="preferred output format .mp3, .m4a (AAC), .flac, etc.",
)
parser.add_argument(
"--write-to",
default=config["write-to"],
help="write tracks from Spotify playlist, album, etc. to this file",
)
parser.add_argument(
"-ff",
"--file-format",
default=config["file-format"],
help="file format to save the downloaded track with, each tag "
"is surrounded by curly braces. Possible formats: "
"{}".format([spotdl.util.formats[x] for x in spotdl.util.formats]),
)
parser.add_argument(
"--trim-silence",
default=config["trim-silence"],
help="remove silence from the start of the audio",
action="store_true",
)
parser.add_argument(
"-sf",
"--search-format",
default=config["search-format"],
help="search format to search for on YouTube, each tag "
"is surrounded by curly braces. Possible formats: "
"{}".format([spotdl.util.formats[x] for x in spotdl.util.formats]),
)
parser.add_argument(
"-dm",
"--download-only-metadata",
default=config["download-only-metadata"],
help="download tracks only whose metadata is found",
action="store_true",
)
parser.add_argument(
"-d",
"--dry-run",
default=config["dry-run"],
help="show only track title and YouTube URL, and then skip "
"to the next track (if any)",
action="store_true",
)
parser.add_argument(
"-mo",
"--music-videos-only",
default=config["music-videos-only"],
help="search only for music videos on Youtube (works only "
"when YouTube API key is set",
action="store_true",
)
parser.add_argument(
"-ns",
"--no-spaces",
default=config["no-spaces"],
help="replace spaces with underscores in file names",
action="store_true",
)
parser.add_argument(
"-ll",
"--log-level",
default=config["log-level"],
choices=_LOG_LEVELS_STR,
type=str.upper,
help="set log verbosity",
)
parser.add_argument(
"-yk",
"--youtube-api-key",
default=config["youtube-api-key"],
help=argparse.SUPPRESS,
)
parser.add_argument(
"-sk",
"--skip",
default=config["skip"],
help="path to file containing tracks to skip",
)
parser.add_argument(
"-w",
"--write-successful",
default=config["write-successful"],
help="path to file to write successful tracks to",
)
parser.add_argument(
"-sci",
"--spotify-client-id",
default=config["spotify_client_id"],
help=argparse.SUPPRESS,
)
parser.add_argument(
"-scs",
"--spotify-client-secret",
default=config["spotify_client_secret"],
help=argparse.SUPPRESS,
)
parser.add_argument(
"-c",
"--config",
default=None,
help="path to custom config.yml file"
)
parser.add_argument(
"-V",
"--version",
action="version",
version="%(prog)s {}".format(spotdl.__version__),
)
parsed = parser.parse_args(argv)
if parsed.config is not None and to_merge:
parsed = override_config(parsed.config, parser)
if (
to_group
and parsed.list
and not mimetypes.MimeTypes().guess_type(parsed.list)[0] == "text/plain"
):
parser.error(
"{0} is not of a valid argument to --list, argument must be plain text file".format(
parsed.list
)
)
if parsed.write_m3u and not parsed.list:
parser.error("--write-m3u can only be used with --list")
if parsed.avconv and parsed.trim_silence:
parser.error("--trim-silence can only be used with FFmpeg")
if parsed.write_to and not (
parsed.playlist or parsed.album or parsed.all_albums or parsed.username
):
parser.error(
"--write-to can only be used with --playlist, --album, --all-albums, or --username"
)
song_parameter_passed = parsed.song is not None and parsed.track is None
if song_parameter_passed:
# log.warn("-s / --song is deprecated and will be removed in future versions. "
# "Use -t / --track instead.")
setattr(parsed, "track", parsed.song)
del parsed.song
parsed.log_level = log_leveller(parsed.log_level)
return parsed

View File

@@ -0,0 +1,117 @@
from spotdl.metadata.providers import ProviderSpotify
from spotdl.metadata.providers import ProviderYouTube
from spotdl.metadata.embedders import EmbedderDefault
from spotdl.track import Track
import spotdl.util
import urllib.request
import threading
def search_metadata(track):
youtube = ProviderYouTube()
if spotdl.util.is_spotify(track):
spotify = ProviderSpotify()
spotify_metadata = spotify.from_url(track)
# TODO: CONFIG.YML
# Generate string in config.search_format
search_query = "{} - {}".format(
spotify_metadata["artists"][0]["name"],
spotify_metadata["name"]
)
youtube_metadata = youtube.from_query(search_query)
metadata = spotdl.util.merge(
youtube_metadata,
spotify_metadata
)
elif spotdl.util.is_youtube(track):
metadata = youtube.from_url(track)
else:
metadata = youtube.from_query(track)
return metadata
def download_track(metadata,
dry_run=False, overwrite="prompt", output_ext="mp3", file_format="{artist} - {track-name}", log_fmt="{artist} - {track_name}"):
# TODO: CONFIG.YML
# Exit here if config.dry_run
# TODO: CONFIG.YML
# Check if test.mp3 already exists here
# log.info(log_fmt)
# TODO: CONFIG.YML
# Download tracks with name config.file_format
# TODO: CONFIG.YML
# Append config.output_ext to config.file_format
track = Track(metadata, cache_albumart=True)
track.download_while_re_encoding("test.mp3")
track.apply_metadata("test.mp3")
def download_tracks_from_file(path):
# log.info(
# "Checking and removing any duplicate tracks "
# "in reading {}".format(path)
# )
with open(path, "r") as fin:
# Read tracks into a list and remove any duplicates
tracks = fin.read().splitlines()
# Remove duplicates and empty elements
# Also strip whitespaces from elements (if any)
spotdl.util.remove_duplicates(tracks, condition=lambda x: x, operation=str.strip)
# Overwrite file
with open(path, "w") as fout:
fout.writelines(tracks)
next_track_metadata = threading.Thread(target=lambda: None)
next_track_metadata.start()
tracks_count = len(tracks)
current_iteration = 1
def mutable_assignment(mutable_resource, track):
mutable_resource["next_track"] = search_metadata(track)
metadata = {
"current_track": None,
"next_track": None,
}
while tracks_count > 0:
current_track = tracks.pop(0)
tracks_count -= 1
metadata["current_track"] = metadata["next_track"]
metadata["next_track"] = None
try:
if metadata["current_track"] is None:
metadata["current_track"] = search_metadata(current_track)
if tracks_count > 0:
next_track = tracks[0]
next_track_metadata = threading.Thread(
target=mutable_assignment,
args=(metadata, next_track)
)
next_track_metadata.start()
download_track(metadata["current_track"], log_fmt=(str(current_iteration) + ". {artist} - {track_name}"))
current_iteration += 1
next_track_metadata.join()
except (urllib.request.URLError, TypeError, IOError) as e:
# log.exception(e.args[0])
# log.warning("Failed. Will retry after other songs\n")
tracks.append(current_track)
else:
# TODO: CONFIG.YML
# Write track to config.write_sucessful
pass
finally:
with open(path, "w") as fout:
fout.writelines(tracks)

View File

@@ -0,0 +1,78 @@
import spotdl.command_line.arguments
import sys
import pytest
def test_log_str_to_int():
expect_levels = [20, 30, 40, 10]
levels = [spotdl.command_line.arguments.log_leveller(level)
for level in spotdl.command_line.arguments._LOG_LEVELS_STR]
assert levels == expect_levels
class TestBadArguments:
def test_error_m3u_without_list(self):
with pytest.raises(SystemExit):
spotdl.command_line.arguments.get_arguments(argv=("-t cool song", "--write-m3u"), to_group=True)
def test_m3u_with_list(self):
spotdl.command_line.arguments.get_arguments(argv=("-l cool_list.txt", "--write-m3u"), to_group=True)
def test_write_to_error(self):
with pytest.raises(SystemExit):
spotdl.command_line.arguments.get_arguments(argv=("-t", "sekai all i had", "--write-to", "output.txt"))
class TestArguments:
def test_general_arguments(self):
arguments = spotdl.command_line.arguments.get_arguments(argv=("-t", "elena coats - one last song"))
arguments = arguments.__dict__
assert isinstance(arguments["spotify_client_id"], str)
assert isinstance(arguments["spotify_client_secret"], str)
arguments["spotify_client_id"] = None
arguments["spotify_client_secret"] = None
expect_arguments = {
"track": ["elena coats - one last song"],
"song": None,
"list": None,
"playlist": None,
"album": None,
"all_albums": None,
"username": None,
"write_m3u": False,
"manual": False,
"no_remove_original": False,
"no_metadata": False,
"no_fallback_metadata": False,
"avconv": False,
"directory": "/home/ritiek/Music",
"overwrite": "prompt",
"input_ext": ".m4a",
"output_ext": ".mp3",
"write_to": None,
"file_format": "{artist} - {track_name}",
"trim_silence": False,
"search_format": "{artist} - {track_name} lyrics",
"download_only_metadata": False,
"dry_run": False,
"music_videos_only": False,
"no_spaces": False,
"log_level": 20,
"youtube_api_key": None,
"skip": None,
"write_successful": None,
"spotify_client_id": None,
"spotify_client_secret": None,
"config": None
}
assert arguments == expect_arguments
def test_grouped_arguments(self):
with pytest.raises(SystemExit):
spotdl.command_line.arguments.get_arguments(to_group=True, to_merge=True)