Files
spotify-downloader/spotdl/spotify_tools.py
Ritiek Malhotra 29005f24ed Refactor exceptions
* Suffix names for custom exceptions with "Error"
* Introduce exceptions for when the coressponding encoder isn't found
2020-03-17 03:09:56 +05:30

266 lines
8.3 KiB
Python

import spotipy
import spotipy.oauth2 as oauth2
from slugify import slugify
from titlecase import titlecase
from logzero import logger as log
import pprint
import sys
import os
import functools
from spotdl import const
from spotdl import internals
from spotdl.lyrics.providers import LyricClasses
from spotdl.lyrics.exceptions import LyricsNotFoundError
spotify = None
def generate_token():
""" Generate the token. """
credentials = oauth2.SpotifyClientCredentials(
client_id=const.args.spotify_client_id,
client_secret=const.args.spotify_client_secret,
)
token = credentials.get_access_token()
return token
def must_be_authorized(func, spotify=spotify):
def wrapper(*args, **kwargs):
global spotify
try:
assert spotify
return func(*args, **kwargs)
except (AssertionError, spotipy.client.SpotifyException):
token = generate_token()
spotify = spotipy.Spotify(auth=token)
return func(*args, **kwargs)
return wrapper
@must_be_authorized
def generate_metadata(raw_song):
""" Fetch a song's metadata from Spotify. """
if internals.is_spotify(raw_song):
# fetch track information directly if it is spotify link
log.debug("Fetching metadata for given track URL")
meta_tags = spotify.track(raw_song)
else:
# otherwise search on spotify and fetch information from first result
log.debug('Searching for "{}" on Spotify'.format(raw_song))
try:
meta_tags = spotify.search(raw_song, limit=1)["tracks"]["items"][0]
except IndexError:
return None
artist = spotify.artist(meta_tags["artists"][0]["id"])
album = spotify.album(meta_tags["album"]["id"])
try:
meta_tags[u"genre"] = titlecase(artist["genres"][0])
except IndexError:
meta_tags[u"genre"] = None
try:
meta_tags[u"copyright"] = album["copyrights"][0]["text"]
except IndexError:
meta_tags[u"copyright"] = None
try:
meta_tags[u"external_ids"][u"isrc"]
except KeyError:
meta_tags[u"external_ids"][u"isrc"] = None
meta_tags[u"release_date"] = album["release_date"]
meta_tags[u"publisher"] = album["label"]
meta_tags[u"total_tracks"] = album["tracks"]["total"]
log.debug("Fetching lyrics")
meta_tags["lyrics"] = None
for LyricClass in LyricClasses:
track = LyricClass(meta_tags["artists"][0]["name"], meta_tags["name"])
try:
meta_tags["lyrics"] = track.get_lyrics()
except LyricsNotFoundError:
continue
else:
break
# Some sugar
meta_tags["year"], *_ = meta_tags["release_date"].split("-")
meta_tags["duration"] = meta_tags["duration_ms"] / 1000.0
meta_tags["spotify_metadata"] = True
# Remove unwanted parameters
del meta_tags["duration_ms"]
del meta_tags["available_markets"]
del meta_tags["album"]["available_markets"]
log.debug(pprint.pformat(meta_tags))
return meta_tags
@must_be_authorized
def write_user_playlist(username, text_file=None):
""" Write user playlists to text_file """
links = get_playlists(username=username)
playlist = internals.input_link(links)
return write_playlist(playlist, text_file)
@must_be_authorized
def get_playlists(username):
""" Fetch user playlists when using the -u option. """
playlists = spotify.user_playlists(username)
links = []
check = 1
while True:
for playlist in playlists["items"]:
# 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:
log.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)
check += 1
if playlists["next"]:
playlists = spotify.next(playlists)
else:
break
return links
@must_be_authorized
def fetch_playlist(playlist):
try:
playlist_id = internals.extract_spotify_id(playlist)
except IndexError:
# Wrong format, in either case
log.error("The provided playlist URL is not in a recognized format!")
sys.exit(10)
try:
results = spotify.user_playlist(
user=None, playlist_id=playlist_id, fields="tracks,next,name"
)
except spotipy.client.SpotifyException:
log.error("Unable to find playlist")
log.info("Make sure the playlist is set to publicly visible and then try again")
sys.exit(11)
return results
@must_be_authorized
def write_playlist(playlist_url, text_file=None):
playlist = fetch_playlist(playlist_url)
tracks = playlist["tracks"]
if not text_file:
text_file = u"{0}.txt".format(slugify(playlist["name"], ok="-_()[]{}"))
return write_tracks(tracks, text_file)
@must_be_authorized
def fetch_album(album):
album_id = internals.extract_spotify_id(album)
album = spotify.album(album_id)
return album
@must_be_authorized
def fetch_albums_from_artist(artist_url, album_type=None):
"""
This funcction returns all the albums from a give artist_url using the US
market
:param artist_url - spotify artist url
:param album_type - the type of album to fetch (ex: single) the default is
all albums
:param return - the album from the artist
"""
# fetching artist's albums limitting the results to the US to avoid duplicate
# albums from multiple markets
artist_id = internals.extract_spotify_id(artist_url)
results = spotify.artist_albums(artist_id, album_type=album_type, country="US")
albums = results["items"]
# indexing all pages of results
while results["next"]:
results = spotify.next(results)
albums.extend(results["items"])
return albums
@must_be_authorized
def write_all_albums_from_artist(artist_url, 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
of the album
:param artist_url - spotify artist url
:param text_file - file to write albums to
"""
album_base_url = "https://open.spotify.com/album/"
# fetching all default albums
albums = fetch_albums_from_artist(artist_url, album_type=None)
# 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)
@must_be_authorized
def write_album(album_url, text_file=None):
album = fetch_album(album_url)
tracks = 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)
@must_be_authorized
def write_tracks(tracks, text_file):
log.info(u"Writing {0} tracks to {1}".format(tracks["total"], text_file))
track_urls = []
with open(text_file, "a") as file_out:
while True:
for item in tracks["items"]:
if "track" in item:
track = item["track"]
else:
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(
track["name"], track["artists"][0]["name"]
)
)
# 1 page = 50 results
# check if there are more pages
if tracks["next"]:
tracks = spotify.next(tracks)
else:
break
return track_urls