text: 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.safe_name:
              songname = self.raw_song.replace(':', '_')
  
          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
  