import spotipy import urllib import os import time from logzero import logger as log 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 class CheckExists: def __init__(self, music_file, meta_tags=None): self.meta_tags = meta_tags basepath, filename = os.path.split(music_file) filepath = os.path.join(const.args.folder, basepath) os.makedirs(filepath, exist_ok=True) self.filepath = filepath self.filename = filename 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.filename) ) songs = os.listdir(self.filepath) 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(self.filepath, 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(self.filepath, song), self.meta_tags ) log.debug("Checking if it is already tagged correctly? {}", already_tagged) if not already_tagged: os.remove(os.path.join(self.filepath, 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.filename: 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) self.total_songs = int(self.meta_tags["total_tracks"]) 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, delete_original=not const.args.no_remove_original, ) except FileNotFoundError: encoder = "avconv" if const.args.avconv else "ffmpeg" log.warning("Could not find {0}, skip encoding".format(encoder)) output_song = self.unconverted_filename(songname) 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, total_songs=self.total_songs, ) log.debug( 'Refining songname from "{0}" to "{1}"'.format( songname, refined_songname ) ) if not refined_songname == " - ": songname = refined_songname else: songname = internals.sanitize_title(songname) return songname @staticmethod def unconverted_filename(songname): 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 (urllib.request.URLError, TypeError, IOError) as e: # detect network problems 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) 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