Link arguments to spotipy helpers

This commit is contained in:
Ritiek Malhotra
2020-05-05 02:25:05 +05:30
parent 5a75687722
commit c3e8a0f0db
4 changed files with 116 additions and 85 deletions

View File

@@ -70,7 +70,7 @@ def get_arguments(argv=None, base_config_file=spotdl.config.default_config_file)
"if `--write-to=<path/to/file.txt>` has been passed",
)
group.add_argument(
"-b"
"-b",
"--album",
help="load tracks from album URL into <album_name>.txt or if "
"`--write-to=<path/to/file.txt>` has been passed"

View File

@@ -21,6 +21,8 @@ import spotdl.config
from spotdl.command_line.exceptions import NoYouTubeVideoFoundError
from spotdl.command_line.exceptions import NoYouTubeVideoMatchError
from spotdl.helpers.spotify import SpotifyHelpers
import sys
import os
import urllib.request
@@ -47,15 +49,20 @@ def search_metadata_on_spotify(query):
def prompt_for_youtube_search_result(videos):
print("0. Skip downloading this track", file=sys.stderr)
max_index_length = len(str(len(videos)))
max_title_length = max(len(v["title"]) for v in videos)
print(" 0. Skip downloading this track", file=sys.stderr)
for index, video in enumerate(videos, 1):
video_repr = "{index}. {title} ({url}) [{duration}]".format(
vid_details = "{index:>{max_index}}. {title:<{max_title}} {url} [{duration}]".format(
index=index,
max_index=max_index_length,
title=video["title"],
max_title=max_title_length,
url=video["url"],
duration=video["duration"],
)
print(video_repr, file=sys.stderr)
print(vid_details, file=sys.stderr)
print("", file=sys.stderr)
selection = spotdl.util.prompt_user_for_selection(range(1, len(videos)+1))
@@ -121,7 +128,7 @@ def search_metadata(track, search_format="{artist} - {track-name} lyrics", manua
youtube_videos = youtube_searcher.search(search_query)
if not youtube_videos:
raise NoYouTubeVideoFoundError(
'YouTube returned no videos for the search query "{}"'.format(search_query)
'YouTube returned no videos for the search query "{}".'.format(search_query)
)
if manual:
youtube_video = prompt_for_youtube_search_result(youtube_videos)
@@ -130,7 +137,7 @@ def search_metadata(track, search_format="{artist} - {track-name} lyrics", manua
if youtube_video is None:
raise NoYouTubeVideoMatchError(
'No matching videos found on YouTube for the search query "{}"'.format(
'No apparent matching videos found on YouTube for the search query "{}"'.format(
search_query
)
)
@@ -177,9 +184,10 @@ class Spotdl:
client_id=self.arguments["spotify_client_id"],
client_secret=self.arguments["spotify_client_secret"]
)
spotify_tools = SpotifyHelpers()
# youtube_tools.set_api_key()
logger.debug("Received arguments:\n{}".format(self.arguments))
# youtube_tools.set_api_key()
if self.arguments["song"]:
for track in self.arguments["song"]:
if track == "-":
@@ -205,27 +213,21 @@ class Spotdl:
self.arguments["list"],
)
elif self.arguments["playlist"]:
spotify_tools.write_playlist(
playlist_url=self.arguments["playlist"], text_file=self.arguments["write_to"]
)
playlist = spotify_tools.fetch_playlist(self.arguments["playlist"])
spotify_tools.write_playlist_tracks(playlist, self.arguments["write_to"])
elif self.arguments["album"]:
spotify_tools.write_album(
album_url=self.arguments["album"], text_file=self.arguments["write_to"]
)
album = spotify_tools.fetch_album(self.arguments["album"])
spotify_tools.write_album_tracks(album, self.arguments["write_to"])
elif self.arguments["all_albums"]:
spotify_tools.write_all_albums_from_artist(
artist_url=self.arguments["all_albums"], text_file=self.arguments["write_to"]
)
albums = spotify_tools.fetch_albums_from_artist(self.arguments["all_albums"])
spotify_tools.write_all_albums(albums, self.arguments["write_to"])
elif self.arguments["username"]:
spotify_tools.write_user_playlist(
username=self.arguments["username"], text_file=self.arguments["write_to"]
)
playlist_url = spotify_tools.prompt_for_user_playlist(self.arguments["username"])
playlist = spotify_tools.fetch_playlist(playlist_url)
spotify_tools.write_playlist_tracks(playlist, self.arguments["write_to"])
def download_track(self, track):
logger.info('Downloading "{}"'.format(track))
track_splits = track.split(":")
if len(track_splits) == 2:
youtube_track, spotify_track = track_splits
try:
metadata = search_metadata(
track,
@@ -237,8 +239,19 @@ class Spotdl:
else:
self.download_track_from_metadata(metadata)
def should_we_overwrite_existing_file(self):
if self.arguments["overwrite"] == "force":
logger.info("Forcing overwrite on existing file.")
to_overwrite = True
elif self.arguments["overwrite"] == "prompt":
to_overwrite = input("Overwrite? (y/N): ").lower() == "y"
else:
logger.info("Not overwriting existing file.")
to_overwrite = False
return to_overwrite
def download_track_from_metadata(self, metadata):
# TODO: Add `-m` flag
track = Track(metadata, cache_albumart=(not self.arguments["no_metadata"]))
stream = metadata["streams"].get(
quality=self.arguments["quality"],
@@ -264,28 +277,19 @@ class Spotdl:
)
)
to_skip = self.arguments["dry_run"]
if not to_skip and os.path.isfile(filename):
msg_already_exists = 'A file with name "{filename}" already exists.'.format(
to_skip_download = self.arguments["dry_run"]
if os.path.isfile(filename):
logger.info('A file with name "{filename}" already exists.'.format(
filename=filename
)
if self.arguments["overwrite"] == "force":
force_overwrite_msg = msg_already_exists + " Forcing overwrite."
to_skip = False
logger.info(force_overwrite_msg)
elif self.arguments["overwrite"] == "prompt":
prompt_overwrite_msg = msg_already_exists + " Overwrite? (y/N): "
to_skip = not input(prompt_overwrite_msg).lower() == "y"
else:
skip_overwrite_msg = msg_already_exists + " Skipping download."
to_skip = True
logger.info(skip_overwrite_msg)
))
to_skip_download = to_skip_download \
or not self.should_we_overwrite_existing_file()
if to_skip:
if to_skip_download:
logger.debug("Skip track download.")
return
logger.info('Downloading to "{filename}".'.format(filename=filename))
logger.info('Downloading to "{filename}"'.format(filename=filename))
if Encoder is None:
track.download(stream, filename)
else:
@@ -297,12 +301,15 @@ class Spotdl:
)
if not self.arguments["no_metadata"]:
track.metadata["lyrics"] = track.metadata["lyrics"].join()
try:
logger.info("Applying metadata")
track.apply_metadata(filename, encoding=output_extension)
except TypeError:
logger.warning("Cannot apply metadata on provided output format.")
self.apply_metadata(track, filename, output_extension)
def apply_metadata(self, track, filename, encoding):
track.metadata["lyrics"] = track.metadata["lyrics"].join()
logger.info("Applying metadata")
try:
track.apply_metadata(filename, encoding=encoding)
except TypeError:
logger.warning("Cannot apply metadata on provided output format.")
def download_tracks_from_file(self, path):
logger.info(
@@ -324,19 +331,26 @@ class Spotdl:
with open(path, "w") as fout:
fout.writelines(tracks)
print("", file=sys.stderr)
for number, track in enumerate(tracks, 1):
try:
metadata = search_metadata(track, self.arguments["search_format"])
log_track_query = str(number) + ". {artist} - {track-name}"
log_track_query = '{position}. Downloading "{track}"'.format(
position=number,
track=track
)
logger.info(log_track_query)
metadata = search_metadata(track, self.arguments["search_format"])
self.download_track_from_metadata(metadata)
except (urllib.request.URLError, TypeError, IOError) as e:
logger.exception(e.args[0])
logger.warning("Failed. Will retry after other songs\n")
logger.warning(
"Failed to download current track due to possible network issue. "
"Will retry after other songs."
)
tracks.append(track)
except NoYouTubeVideoFoundError:
logger.warning("Failed. No YouTube video found.\n")
pass
except (NoYouTubeVideoFoundError, NoYouTubeVideoMatchError) as e:
logger.error(e.args[0])
else:
if self.arguments["write_successful"]:
with open(self.arguments["write_successful"], "a") as fout:
@@ -344,12 +358,13 @@ class Spotdl:
finally:
with open(path, "w") as fout:
fout.writelines(tracks[number-1:])
print("", file=sys.stderr)
def download_tracks_from_file_threaded(self, path):
# FIXME: Can we make this function cleaner?
logger.info(
"Checking and removing any duplicate tracks in {}.".format(path)
"Checking and removing any duplicate tracks in {}.\n".format(path)
)
with open(path, "r") as fin:
# Read tracks into a list and remove any duplicates
@@ -402,6 +417,7 @@ class Spotdl:
print(metadata["current_track"]["name"], file=sys.stderr)
# self.download_track_from_metadata(metadata["current_track"])
except (urllib.request.URLError, TypeError, IOError) as e:
print("", file=sys.stderr)
logger.exception(e.args[0])
logger.warning("Failed. Will retry after other songs\n")
tracks.append(current_track)

View File

@@ -4,26 +4,47 @@
# to `spotify._get_id` in below methods.
from spotdl.authorize.services import AuthorizeSpotify
import spotdl.util
import sys
import logging
logger = logging.getLogger(__name__)
try:
from slugify import SLUG_OK, slugify
except ImportError:
logger.error("Oops! `unicode-slugify` was not found.")
logger.info("Please remove any other slugify library and install `unicode-slugify`.")
sys.exit(5)
ALBUM_BASE_URL = "https://open.spotify.com/album/"
class SpotifyHelpers:
def __init__(self, spotify=None):
self._ALBUM_BASE_URL = ALBUM_BASE_URL
if spotify is None:
spotify = AuthorizeSpotify()
self.spotify = spotify
def prompt_for_user_playlist(self, username):
""" Write user playlists to text_file """
links = fetch_user_playlist_urls(username)
playlist = internals.input_link(links)
return playlist
playlists = self.fetch_user_playlist_urls(username)
for i, playlist in enumerate(playlists, 1):
playlist_details = "{0}. {1:<30} ({2} tracks)".format(
i, playlist["name"], playlist["tracks"]["total"]
)
print(playlist_details, file=sys.stderr)
print("", file=sys.stderr)
playlist = spotdl.util.prompt_user_for_selection(playlists)
return playlist["external_urls"]["spotify"]
def fetch_user_playlist_urls(self, username):
""" Fetch user playlists when using the -u option. """
logger.debug('Fetching playlists for "{username}".'.format(username=username))
playlists = self.spotify.user_playlists(username)
links = []
collected_playlists = []
check = 1
while True:
@@ -31,56 +52,51 @@ class SpotifyHelpers:
# in rare cases, playlists may not be found, so playlists['next']
# is None. Skip these. Also see Issue #91.
if playlist["name"] is not None:
logger.info(
# u"{0:>5}. {1:<30} ({2} tracks)".format(
# check, playlist["name"], playlist["tracks"]["total"]
# )
# )
playlist_url = playlist["external_urls"]["spotify"]
# log.debug(playlist_url)
links.append(playlist_url)
collected_playlists.append(playlist)
check += 1
if playlists["next"]:
playlists = self.spotify.next(playlists)
else:
break
return links
return collected_playlists
def fetch_playlist(self, playlist_url):
logger.debug('Fetching playlist "{playlist}".'.format(playlist=playlist_url))
try:
playlist_id = self.spotify._get_id("playlist", playlist_url)
except IndexError:
# Wrong format, in either case
# log.error("The provided playlist URL is not in a recognized format!")
logger.error("The provided playlist URL is not in a recognized format!")
sys.exit(10)
try:
results = self.spotify.user_playlist(
user=None, playlist_id=playlist_id, fields="tracks,next,name"
)
except spotipy.client.SpotifyException:
# log.error("Unable to find playlist")
logger.info("Make sure the playlist is set to publicly visible and then try again")
logger.error("Unable to find playlist")
logger.info("Make sure the playlist is set to publicly visible and then try again.")
sys.exit(11)
return results
def write_playlist(self, playlist, text_file=None):
def write_playlist_tracks(self, playlist, text_file=None):
tracks = playlist["tracks"]
if not text_file:
text_file = u"{0}.txt".format(slugify(playlist["name"], ok="-_()[]{}"))
return write_tracks(tracks, text_file)
return self.write_tracks(tracks, text_file)
def fetch_album(self, album_url):
logger.debug('Fetching album "{album}".'.format(album=album_url))
album_id = self.spotify._get_id("album", album_url)
album = self.spotify.album(album_id)
return album
def write_album(self, album, text_file=None):
def write_album_tracks(self, album, text_file=None):
tracks = self.spotify.album_tracks(album["id"])
if not text_file:
text_file = u"{0}.txt".format(slugify(album["name"], ok="-_()[]{}"))
return write_tracks(tracks, text_file)
return self.write_tracks(tracks, text_file)
def fetch_albums_from_artist(self, artist_url, album_type=None):
"""
@@ -92,9 +108,10 @@ class SpotifyHelpers:
:param return - the album from the artist
"""
logger.debug('Fetching all albums for "{artist}".'.format(artist=artist_url))
artist_id = self.spotify._get_id("artist", artist_url)
# fetching artist's albums limitting the results to the US to avoid duplicate
# albums from multiple markets
artist_id = self.spotify._get_id("artist", artist_url)
results = self.spotify.artist_albums(artist_id, album_type=album_type, country="US")
albums = results["items"]
@@ -106,7 +123,7 @@ class SpotifyHelpers:
return albums
def write_all_albums_from_artist(self, albums, text_file=None):
def write_all_albums(self, albums, text_file=None):
"""
This function gets all albums from an artist and writes it to a file in the
current working directory called [ARTIST].txt, where [ARTIST] is the artist
@@ -115,20 +132,17 @@ class SpotifyHelpers:
:param text_file - file to write albums to
"""
album_base_url = "https://open.spotify.com/album/"
# if no file if given, the default save file is in the current working
# directory with the name of the artist
if text_file is None:
text_file = albums[0]["artists"][0]["name"] + ".txt"
for album in albums:
logging album name
log.info("Fetching album: " + album["name"])
write_album(album_base_url + album["id"], text_file=text_file)
logger.info('Fetching album "{album}".'.format(album=album["name"]))
self.write_album_tracks(album, text_file=text_file)
def write_tracks(self, tracks, text_file):
logger.info(u"Writing {0} tracks to {1}".format(tracks["total"], text_file))
logger.info(u"Writing {0} tracks to {1}.".format(tracks["total"], text_file))
track_urls = []
with open(text_file, "a") as file_out:
while True:
@@ -139,12 +153,13 @@ class SpotifyHelpers:
track = item
try:
track_url = track["external_urls"]["spotify"]
# log.debug(track_url)
file_out.write(track_url + "\n")
track_urls.append(track_url)
except KeyError:
# log.warning(
u"Skipping track {0} by {1} (local only?)".format(
# FIXME: Write "{artist} - {name}" instead of Spotify URI for
# "local only" tracks.
logger.warning(
'Skipping track "{0}" by "{1}" (local only?)'.format(
track["name"], track["artists"][0]["name"]
)
)

View File

@@ -52,7 +52,7 @@ def merge(base, overrider):
def prompt_user_for_selection(items):
""" Let the user input a choice. """
logger.info("Choose your number:")
logger.info("Enter a number:")
while True:
try:
the_chosen_one = int(input("> "))