[WIP] Refactor spotdl.py; introduced classes (#410)

* Refactor spotdl.py; introduced classes

* introduce CheckExists class

* Move these classes to download.py

* Fix refresh_token

* Add a changelog entry
This commit is contained in:
Ritiek Malhotra
2018-11-25 17:07:56 +05:30
committed by GitHub
parent 8ced90cb39
commit eae9316cee
13 changed files with 315 additions and 217 deletions

261
spotdl/downloader.py Normal file
View File

@@ -0,0 +1,261 @@
from spotdl import const
from spotdl import metadata
from spotdl import convert
from spotdl import internals
from spotdl import spotify_tools
from spotdl import youtube_tools
from logzero import logger as log
import os
class CheckExists:
def __init__(self, music_file, meta_tags=None):
self.music_file = music_file
self.meta_tags = meta_tags
def already_exists(self, raw_song):
""" Check if the input song already exists in the given folder. """
log.debug(
"Cleaning any temp files and checking "
'if "{}" already exists'.format(self.music_file)
)
songs = os.listdir(const.args.folder)
self._remove_temp_files(songs)
for song in songs:
# check if a song with the same name is already present in the given folder
if self._match_filenames(song):
if internals.is_spotify(raw_song) and \
not self._has_metadata(song):
return False
log.warning('"{}" already exists'.format(song))
if const.args.overwrite == "prompt":
return self._prompt_song(song)
elif const.args.overwrite == "force":
return self._force_overwrite_song(song)
elif const.args.overwrite == "skip":
return self._skip_song(song)
return False
def _remove_temp_files(self, songs):
for song in songs:
if song.endswith(".temp"):
os.remove(os.path.join(const.args.folder, song))
def _has_metadata(self, song):
# check if the already downloaded song has correct metadata
# if not, remove it and download again without prompt
already_tagged = metadata.compare(
os.path.join(const.args.folder, song), self.meta_tags
)
log.debug(
"Checking if it is already tagged correctly? {}", already_tagged
)
if not already_tagged:
os.remove(os.path.join(const.args.folder, song))
return False
return True
def _prompt_song(self, song):
log.info(
'"{}" has already been downloaded. '
"Re-download? (y/N): ".format(song)
)
prompt = input("> ")
if prompt.lower() == "y":
return self._force_overwrite_song(song)
else:
return self._skip_song(song)
def _force_overwrite_song(self, song):
os.remove(os.path.join(const.args.folder, song))
log.info('Overwriting "{}"'.format(song))
return False
def _skip_song(self, song):
log.info('Skipping "{}"'.format(song))
return True
def _match_filenames(self, song):
if os.path.splitext(song)[0] == self.music_file:
log.debug('Found an already existing song: "{}"'.format(song))
return True
return False
class Downloader:
def __init__(self, raw_song, number=None):
self.raw_song = raw_song
self.number = number
self.content, self.meta_tags = youtube_tools.match_video_and_metadata(raw_song)
def download_single(self):
""" Logic behind downloading a song. """
if self._to_skip():
return
# "[number]. [artist] - [song]" if downloading from list
# otherwise "[artist] - [song]"
youtube_title = youtube_tools.get_youtube_title(self.content, self.number)
log.info("{} ({})".format(youtube_title, self.content.watchv_url))
# generate file name of the song to download
songname = self.refine_songname(self.content.title)
if const.args.dry_run:
return
song_existence = CheckExists(songname, self.meta_tags)
if not song_existence.already_exists(self.raw_song):
return self._download_single(songname)
def _download_single(self, songname):
# deal with file formats containing slashes to non-existent directories
songpath = os.path.join(const.args.folder, os.path.dirname(songname))
os.makedirs(songpath, exist_ok=True)
input_song = songname + const.args.input_ext
output_song = songname + const.args.output_ext
if youtube_tools.download_song(input_song, self.content):
print("")
try:
convert.song(
input_song,
output_song,
const.args.folder,
avconv=const.args.avconv,
trim_silence=const.args.trim_silence,
)
except FileNotFoundError:
output_song = self.unconverted_filename(songname)
if not const.args.input_ext == const.args.output_ext:
os.remove(os.path.join(const.args.folder, input_song))
if not const.args.no_metadata and self.meta_tags is not None:
metadata.embed(
os.path.join(const.args.folder, output_song), self.meta_tags
)
return True
def _to_skip(self):
if self.content is None:
log.debug("Found no matching video")
return True
if const.args.download_only_metadata and self.meta_tags is None:
log.info("Found no metadata. Skipping the download")
return True
def refine_songname(self, songname):
if self.meta_tags is not None:
refined_songname = internals.format_string(
const.args.file_format, self.meta_tags, slugification=True
)
log.debug(
'Refining songname from "{0}" to "{1}"'.format(
songname, refined_songname
)
)
if not refined_songname == " - ":
songname = refined_songname
else:
if not const.args.no_metadata:
log.warning("Could not find metadata")
songname = internals.sanitize_title(songname)
return songname
@staticmethod
def unconverted_filename(songname):
encoder = "avconv" if const.args.avconv else "ffmpeg"
log.warning("Could not find {0}, skipping conversion".format(encoder))
const.args.output_ext = const.args.input_ext
output_song = songname + const.args.output_ext
return output_song
class ListDownloader:
def __init__(self, tracks_file, skip_file=None, write_successful_file=None):
self.tracks_file = tracks_file
self.skip_file = skip_file
self.write_successful_file = write_successful_file
self.tracks = internals.get_unique_tracks(self.tracks_file)
def download_list(self):
""" Download all songs from the list. """
# override file with unique tracks
log.info("Overriding {} with unique tracks".format(self.tracks_file))
self._override_file()
# Remove tracks to skip from tracks list
if self.skip_file is not None:
self.tracks = self._filter_tracks_against_skip_file()
log.info(u"Preparing to download {} songs".format(len(self.tracks)))
return self._download_list()
def _download_list(self):
downloaded_songs = []
for number, raw_song in enumerate(self.tracks, 1):
print("")
try:
track_dl = Downloader(raw_song, number=number)
track_dl.download_single()
except spotipy.client.SpotifyException:
# token expires after 1 hour
self._regenerate_token()
track_dl.download_single()
# detect network problems
except (urllib.request.URLError, TypeError, IOError) as e:
self._cleanup(raw_song, e)
# TODO: remove this sleep once #397 is fixed
# wait 0.5 sec to avoid infinite looping
time.sleep(0.5)
continue
downloaded_songs.append(raw_song)
# Add track to file of successful downloads
if self.write_successful_file is not None:
self._write_successful(raw_song)
log.debug("Removing downloaded song from tracks file")
internals.trim_song(self.tracks_file)
return downloaded_songs
def _override_file(self):
with open(self.tracks_file, "w") as f:
f.write("\n".join(self.tracks))
def _write_successful(self, raw_song):
log.debug("Adding downloaded song to write successful file")
with open(self.write_successful_file, "a") as f:
f.write("\n" + raw_song)
@staticmethod
def _regenerate_token():
log.debug("Token expired, generating new one and authorizing")
spotify_tools.refresh_token()
def _cleanup(self, raw_song, exception):
self.tracks.append(raw_song)
# remove the downloaded song from file
internals.trim_song(self.tracks_file)
# and append it at the end of file
with open(self.tracks_file, "a") as f:
f.write("\n" + raw_song)
log.exception(exception)
log.warning("Failed to download song. Will retry after other songs\n")
def _filter_tracks_against_skip_file(self):
skip_tracks = internals.get_unique_tracks(self.skip_file)
len_before = len(self.tracks)
tracks = [track for track in self.tracks if track not in skip_tracks]
log.info("Skipping {} tracks".format(len_before - len(tracks)))
return tracks