from concurrent.futures import ThreadPoolExecutor
from time import sleep
import traceback

from os.path import sep as pathSep
from os import makedirs, system as execute
from pathlib import Path
from shlex import quote
import errno

import logging
from tempfile import gettempdir

import requests
from requests import get

from urllib3.exceptions import SSLError as u3SSLError

from mutagen.flac import FLACNoHeaderError, error as FLACError

from deezer import TrackFormats
from deezer.errors import WrongLicense, WrongGeolocation
from deezer.utils import map_track
from deemix.types.DownloadObjects import Single, Collection
from deemix.types.Track import Track
from deemix.types.Picture import StaticPicture
from deemix.utils import USER_AGENT_HEADER
from deemix.utils.pathtemplates import generatePath, generateAlbumName, generateArtistName, generateDownloadObjectName
from deemix.tagger import tagID3, tagFLAC
from deemix.decryption import generateCryptedStreamURL, streamTrack
from deemix.settings import OverwriteOption
from deemix.errors import DownloadFailed, MD5NotFound, DownloadCanceled, PreferredBitrateNotFound, TrackNot360, AlbumDoesntExists, DownloadError, ErrorMessages

logger = logging.getLogger('deemix')

extensions = {
    TrackFormats.FLAC:    '.flac',
    TrackFormats.LOCAL:   '.mp3',
    TrackFormats.MP3_320: '.mp3',
    TrackFormats.MP3_128: '.mp3',
    TrackFormats.DEFAULT: '.mp3',
    TrackFormats.MP4_RA3: '.mp4',
    TrackFormats.MP4_RA2: '.mp4',
    TrackFormats.MP4_RA1: '.mp4'
}

formatsName = {
    TrackFormats.FLAC:    'FLAC',
    TrackFormats.LOCAL:   'MP3_MISC',
    TrackFormats.MP3_320: 'MP3_320',
    TrackFormats.MP3_128: 'MP3_128',
    TrackFormats.DEFAULT: 'MP3_MISC',
    TrackFormats.MP4_RA3: 'MP4_RA3',
    TrackFormats.MP4_RA2: 'MP4_RA2',
    TrackFormats.MP4_RA1: 'MP4_RA1'
}

TEMPDIR = Path(gettempdir()) / 'deemix-imgs'
if not TEMPDIR.is_dir(): makedirs(TEMPDIR)

def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE):
    if path.is_file() and overwrite not in [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS, OverwriteOption.KEEP_BOTH]: return path

    try:
        image = get(url, headers={'User-Agent': USER_AGENT_HEADER}, timeout=30)
        image.raise_for_status()
        with open(path, 'wb') as f:
            f.write(image.content)
        return path
    except requests.exceptions.HTTPError:
        if path.is_file(): path.unlink()
        if 'cdns-images.dzcdn.net' in url:
            urlBase = url[:url.rfind("/")+1]
            pictureUrl = url[len(urlBase):]
            pictureSize = int(pictureUrl[:pictureUrl.find("x")])
            if pictureSize > 1200:
                return downloadImage(urlBase+pictureUrl.replace(f"{pictureSize}x{pictureSize}", '1200x1200'), path, overwrite)
    except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, u3SSLError):
        if path.is_file(): path.unlink()
        sleep(5)
        return downloadImage(url, path, overwrite)
    except OSError as e:
        if path.is_file(): path.unlink()
        if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") from e
        logger.exception("Error while downloading an image, you should report this to the developers: %s", e)
    return None

def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, feelingLucky, uuid=None, listener=None):
    preferredBitrate = int(preferredBitrate)

    falledBack = False
    hasAlternative = track.fallbackID != "0"
    isGeolocked = False
    wrongLicense = False

    def testURL(track, url, formatName):
        if not url: return False
        request = requests.head(
            url,
            headers={'User-Agent': USER_AGENT_HEADER},
            timeout=30
        )
        try:
            request.raise_for_status()
            track.filesizes[f"{formatName.lower()}"] = int(request.headers["Content-Length"])
            track.filesizes[f"{formatName.lower()}_TESTED"] = True
            return track.filesizes[f"{formatName.lower()}"] != 0
        except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error
            return False

    def getCorrectURL(track, formatName, formatNumber, feelingLucky):
        nonlocal wrongLicense, isGeolocked
        url = None
        # Check the track with the legit method
        wrongLicense = (
            (formatName == "FLAC" or formatName.startswith("MP4_RA")) and not dz.current_user.get('can_stream_lossless') or \
            formatName == "MP3_320" and not dz.current_user.get('can_stream_hq')
        )
        if track.filesizes.get(formatName.lower()) and track.filesizes[formatName.lower()] != "0":
            try:
                url = dz.get_track_url(track.trackToken, formatName)
            except (WrongLicense, WrongGeolocation) as e:
                wrongLicense = isinstance(e, WrongLicense)
                isGeolocked = isinstance(e, WrongGeolocation)
        # Fallback to old method
        if not url and feelingLucky:
            url = generateCryptedStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber)
            if testURL(track, url, formatName): return url
            url = None
        return url

    if track.local:
        url = getCorrectURL(track, "MP3_MISC", TrackFormats.LOCAL, feelingLucky)
        track.urls["MP3_MISC"] = url
        return TrackFormats.LOCAL

    formats_non_360 = {
        TrackFormats.FLAC: "FLAC",
        TrackFormats.MP3_320: "MP3_320",
        TrackFormats.MP3_128: "MP3_128",
    }
    formats_360 = {
        TrackFormats.MP4_RA3: "MP4_RA3",
        TrackFormats.MP4_RA2: "MP4_RA2",
        TrackFormats.MP4_RA1: "MP4_RA1",
    }

    is360format = preferredBitrate in formats_360.keys()
    if not shouldFallback:
        formats = formats_360
        formats.update(formats_non_360)
    elif is360format:
        formats = formats_360
    else:
        formats = formats_non_360

    # check and renew trackToken before starting the check
    track.checkAndRenewTrackToken(dz)
    for formatNumber, formatName in formats.items():
        # Current bitrate is higher than preferred bitrate; skip
        if formatNumber > preferredBitrate: continue

        currentTrack = track
        url = getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky)
        newTrack = None
        while True:
            if not url and hasAlternative:
                newTrack = dz.gw.get_track_with_fallback(currentTrack.fallbackID)
                newTrack = map_track(newTrack)
                currentTrack = Track()
                currentTrack.parseEssentialData(newTrack)
                hasAlternative = currentTrack.fallbackID != "0"
            if not url: getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky)
            if (url or not hasAlternative): break

        if url:
            if newTrack: track.parseEssentialData(newTrack)
            track.urls[formatName] = url
            return formatNumber

        if not shouldFallback:
            if wrongLicense: raise WrongLicense(formatName)
            if isGeolocked: raise WrongGeolocation(dz.current_user['country'])
            raise PreferredBitrateNotFound
        if not falledBack:
            falledBack = True
            logger.info("%s Fallback to lower bitrate", f"[{track.mainArtist.name} - {track.title}]")
            if listener and uuid:
                listener.send('downloadInfo', {
                    'uuid': uuid,
                    'state': 'bitrateFallback',
                    'data': {
                        'id': track.id,
                        'title': track.title,
                        'artist': track.mainArtist.name
                    },
                })
    if is360format: raise TrackNot360
    url = getCorrectURL(track, "MP3_MISC", TrackFormats.DEFAULT, feelingLucky)
    track.urls["MP3_MISC"] = url
    return TrackFormats.DEFAULT

class Downloader:
    def __init__(self, dz, downloadObject, settings, listener=None):
        self.dz = dz
        self.downloadObject = downloadObject
        self.settings = settings
        self.bitrate = downloadObject.bitrate
        self.listener = listener

        self.playlistCoverName = None
        self.playlistURLs = []

    def start(self):
        if not self.downloadObject.isCanceled:
            if isinstance(self.downloadObject, Single):
                track = self.downloadWrapper({
                    'trackAPI': self.downloadObject.single.get('trackAPI'),
                    'albumAPI': self.downloadObject.single.get('albumAPI')
                })
                if track: self.afterDownloadSingle(track)
            elif isinstance(self.downloadObject, Collection):
                tracks = [None] * len(self.downloadObject.collection['tracks'])
                with ThreadPoolExecutor(self.settings['queueConcurrency']) as executor:
                    for pos, track in enumerate(self.downloadObject.collection['tracks'], start=0):
                        tracks[pos] = executor.submit(self.downloadWrapper, {
                            'trackAPI': track,
                            'albumAPI': self.downloadObject.collection.get('albumAPI'),
                            'playlistAPI': self.downloadObject.collection.get('playlistAPI')
                        })
                self.afterDownloadCollection(tracks)

        if self.listener:
            if self.downloadObject.isCanceled:
                self.listener.send('currentItemCancelled', self.downloadObject.uuid)
                self.listener.send("removedFromQueue", self.downloadObject.uuid)
            else:
                self.listener.send("finishDownload", self.downloadObject.uuid)

    def log(self, data, state):
        if self.listener:
            self.listener.send('downloadInfo', {'uuid': self.downloadObject.uuid, 'data': data, 'state': state})

    def warn(self, data, state, solution):
        if self.listener:
            self.listener.send('downloadWarn', {'uuid': self.downloadObject.uuid, 'data': data, 'state': state, 'solution': solution})

    def download(self, extraData, track=None):
        returnData = {}
        trackAPI = extraData.get('trackAPI')
        albumAPI = extraData.get('albumAPI')
        playlistAPI = extraData.get('playlistAPI')
        trackAPI['size'] = self.downloadObject.size
        if self.downloadObject.isCanceled: raise DownloadCanceled
        if int(trackAPI['id']) == 0: raise DownloadFailed("notOnDeezer")

        itemData = {
            'id': trackAPI['id'],
            'title': trackAPI['title'],
            'artist': trackAPI['artist']['name']
        }

        # Create Track object
        if not track:
            self.log(itemData, "getTags")
            try:
                track = Track().parseData(
                    dz=self.dz,
                    track_id=trackAPI['id'],
                    trackAPI=trackAPI,
                    albumAPI=albumAPI,
                    playlistAPI=playlistAPI
                )
            except AlbumDoesntExists as e:
                raise DownloadError('albumDoesntExists') from e
            except MD5NotFound as e:
                raise DownloadError('notLoggedIn') from e
            self.log(itemData, "gotTags")

        itemData = {
            'id': track.id,
            'title': track.title,
            'artist': track.mainArtist.name
        }

        # Check if track not yet encoded
        if track.MD5 == '': raise DownloadFailed("notEncoded", track)

        # Choose the target bitrate
        self.log(itemData, "getBitrate")
        try:
            selectedFormat = getPreferredBitrate(
                self.dz,
                track,
                self.bitrate,
                self.settings['fallbackBitrate'], self.settings['feelingLucky'],
                self.downloadObject.uuid, self.listener
            )
        except WrongLicense as e:
            raise DownloadFailed("wrongLicense") from e
        except WrongGeolocation as e:
            raise DownloadFailed("wrongGeolocation", track) from e
        except PreferredBitrateNotFound as e:
            raise DownloadFailed("wrongBitrate", track) from e
        except TrackNot360 as e:
            raise DownloadFailed("no360RA") from e
        track.bitrate = selectedFormat
        track.album.bitrate = selectedFormat
        self.log(itemData, "gotBitrate")

        # Apply settings
        track.applySettings(self.settings)

        # Generate filename and filepath from metadata
        (filename, filepath, artistPath, coverPath, extrasPath) = generatePath(track, self.downloadObject, self.settings)

        # Make sure the filepath exists
        makedirs(filepath, exist_ok=True)
        extension = extensions[track.bitrate]
        writepath = filepath / f"{filename}{extension}"

        # Save extrasPath
        if extrasPath and not self.downloadObject.extrasPath: self.downloadObject.extrasPath = extrasPath

        # Generate covers URLs
        embeddedImageFormat = f'jpg-{self.settings["jpegImageQuality"]}'
        if self.settings['embeddedArtworkPNG']: embeddedImageFormat = 'png'

        track.album.embeddedCoverURL = track.album.pic.getURL(self.settings['embeddedArtworkSize'], embeddedImageFormat)
        ext = track.album.embeddedCoverURL[-4:]
        if ext[0] != ".": ext = ".jpg" # Check for Spotify images
        track.album.embeddedCoverPath = TEMPDIR / ((f"pl{track.playlist.id}" if track.album.isPlaylist else f"alb{track.album.id}") + f"_{self.settings['embeddedArtworkSize']}{ext}")

        # Download and cache coverart
        self.log(itemData, "getAlbumArt")
        track.album.embeddedCoverPath = downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath)
        self.log(itemData, "gotAlbumArt")

        # Save local album art
        if coverPath:
            returnData['albumURLs'] = []
            for pic_format in self.settings['localArtworkFormat'].split(","):
                if pic_format in ["png","jpg"]:
                    extendedFormat = pic_format
                    if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}"
                    url = track.album.pic.getURL(self.settings['localArtworkSize'], extendedFormat)
                    # Skip non deezer pictures at the wrong format
                    if isinstance(track.album.pic, StaticPicture) and pic_format != "jpg":
                        continue
                    returnData['albumURLs'].append({'url': url, 'ext': pic_format})
            returnData['albumPath'] = coverPath
            returnData['albumFilename'] = generateAlbumName(self.settings['coverImageTemplate'], track.album, self.settings, track.playlist)

        # Save artist art
        if artistPath:
            returnData['artistURLs'] = []
            for pic_format in self.settings['localArtworkFormat'].split(","):
                # Deezer doesn't support png artist images
                if pic_format == "jpg":
                    extendedFormat = f"{pic_format}-{self.settings['jpegImageQuality']}"
                    url = track.album.mainArtist.pic.getURL(self.settings['localArtworkSize'], extendedFormat)
                    if track.album.mainArtist.pic.md5 == "": continue
                    returnData['artistURLs'].append({'url': url, 'ext': pic_format})
            returnData['artistPath'] = artistPath
            returnData['artistFilename'] = generateArtistName(self.settings['artistImageTemplate'], track.album.mainArtist, self.settings, rootArtist=track.album.rootArtist)

        # Save playlist art
        if track.playlist:
            if len(self.playlistURLs) == 0:
                for pic_format in self.settings['localArtworkFormat'].split(","):
                    if pic_format in ["png","jpg"]:
                        extendedFormat = pic_format
                        if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}"
                        url = track.playlist.pic.getURL(self.settings['localArtworkSize'], extendedFormat)
                        if isinstance(track.playlist.pic, StaticPicture) and pic_format != "jpg": continue
                        self.playlistURLs.append({'url': url, 'ext': pic_format})
            if not self.playlistCoverName:
                track.playlist.bitrate = selectedFormat
                track.playlist.dateString = track.playlist.date.format(self.settings['dateFormat'])
                self.playlistCoverName = generateAlbumName(self.settings['coverImageTemplate'], track.playlist, self.settings, track.playlist)

        # Save lyrics in lrc file
        if self.settings['syncedLyrics'] and track.lyrics.sync:
            if not (filepath / f"{filename}.lrc").is_file() or self.settings['overwriteFile'] in [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS]:
                with open(filepath / f"{filename}.lrc", 'w', encoding="utf-8") as f:
                    f.write(track.lyrics.sync)

        # Check for overwrite settings
        trackAlreadyDownloaded = writepath.is_file()

        # Don't overwrite and don't mind extension
        if not trackAlreadyDownloaded and self.settings['overwriteFile'] == OverwriteOption.DONT_CHECK_EXT:
            exts = ['.mp3', '.flac', '.opus', '.m4a']
            baseFilename = str(filepath / filename)
            for ext in exts:
                trackAlreadyDownloaded = Path(baseFilename+ext).is_file()
                if trackAlreadyDownloaded: break
        # Don't overwrite and keep both files
        if trackAlreadyDownloaded and self.settings['overwriteFile'] == OverwriteOption.KEEP_BOTH:
            baseFilename = str(filepath / filename)
            c = 1
            currentFilename = baseFilename+' ('+str(c)+')'+ extension
            while Path(currentFilename).is_file():
                c += 1
                currentFilename = baseFilename+' ('+str(c)+')'+ extension
            trackAlreadyDownloaded = False
            writepath = Path(currentFilename)

        if not trackAlreadyDownloaded or self.settings['overwriteFile'] == OverwriteOption.OVERWRITE:
            track.downloadURL = track.urls[formatsName[track.bitrate]]
            if not track.downloadURL: raise DownloadFailed('notAvailable', track)
            try:
                with open(writepath, 'wb') as stream:
                    streamTrack(stream, track, downloadObject=self.downloadObject, listener=self.listener)
            except requests.exceptions.HTTPError as e:
                if writepath.is_file(): writepath.unlink()
                raise DownloadFailed('notAvailable', track) from e
            except OSError as e:
                if writepath.is_file(): writepath.unlink()
                if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") from e
                raise e
            self.log(itemData, "downloaded")
        else:
            self.log(itemData, "alreadyDownloaded")
            self.downloadObject.completeTrackProgress(self.listener)

        # Adding tags
        if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE]) and not track.local:
            self.log(itemData, "tagging")
            if extension == '.mp3':
                tagID3(writepath, track, self.settings['tags'])
            elif extension == '.flac':
                try:
                    tagFLAC(writepath, track, self.settings['tags'])
                except (FLACNoHeaderError, FLACError):
                    writepath.unlink()
                    logger.warning("%s Track not available in FLAC, falling back if necessary", f"{itemData['artist']} - {itemData['title']}")
                    self.downloadObject.removeTrackProgress(self.listener)
                    track.filesizes['FILESIZE_FLAC'] = "0"
                    track.filesizes['FILESIZE_FLAC_TESTED'] = True
                    return self.download(extraData, track=track)
            self.log(itemData, "tagged")

        if track.searched: returnData['searched'] = True
        self.downloadObject.downloaded += 1
        if self.listener: self.listener.send("updateQueue", {
            'uuid': self.downloadObject.uuid,
            'downloaded': True,
            'downloadPath': str(writepath),
            'extrasPath': str(self.downloadObject.extrasPath)
        })
        returnData['filename'] = str(writepath)[len(str(extrasPath))+ len(pathSep):]
        returnData['data'] = itemData
        returnData['path'] = str(writepath)
        self.downloadObject.files.append(returnData)
        return returnData

    def downloadWrapper(self, extraData, track=None):
        trackAPI = extraData['trackAPI']
        # Temp metadata to generate logs
        itemData = {
            'id': trackAPI['id'],
            'title': trackAPI['title'],
            'artist': trackAPI['artist']['name']
        }

        try:
            result = self.download(extraData, track)
        except DownloadFailed as error:
            if error.track:
                track = error.track
                if track.fallbackID != "0":
                    self.warn(itemData, error.errid, 'fallback')
                    newTrack = self.dz.gw.get_track_with_fallback(track.fallbackID)
                    newTrack = map_track(newTrack)
                    track.parseEssentialData(newTrack)
                    return self.downloadWrapper(extraData, track)
                if len(track.albumsFallback) != 0 and self.settings['fallbackISRC']:
                    newAlbumID = track.albumsFallback.pop()
                    newAlbum = self.dz.gw.get_album_page(newAlbumID)
                    fallbackID = 0
                    for newTrack in newAlbum['SONGS']['data']:
                        if newTrack['ISRC'] == track.ISRC:
                            fallbackID = newTrack['SNG_ID']
                            break
                    if fallbackID != 0:
                        self.warn(itemData, error.errid, 'fallback')
                        newTrack = self.dz.gw.get_track_with_fallback(fallbackID)
                        newTrack = map_track(newTrack)
                        track.parseEssentialData(newTrack)
                        return self.downloadWrapper(extraData, track)
                if not track.searched and self.settings['fallbackSearch']:
                    self.warn(itemData, error.errid, 'search')
                    searchedId = self.dz.api.get_track_id_from_metadata(track.mainArtist.name, track.title, track.album.title)
                    if searchedId != "0":
                        newTrack = self.dz.gw.get_track_with_fallback(searchedId)
                        newTrack = map_track(newTrack)
                        track.parseEssentialData(newTrack)
                        track.searched = True
                        self.log(itemData, "searchFallback")
                        return self.downloadWrapper(extraData, track)
                error.errid += "NoAlternative"
                error.message = ErrorMessages[error.errid]
            result = {'error': {
                'message': error.message,
                'errid': error.errid,
                'data': itemData,
                'type': "track"
            }}
        except Exception as e:
            logger.exception("%s %s", f"{itemData['artist']} - {itemData['title']}", e)
            result = {'error': {
                'message': str(e),
                'data': itemData,
                'stack': traceback.format_exc(),
                'type': "track"
            }}

        if 'error' in result:
            self.downloadObject.completeTrackProgress(self.listener)
            self.downloadObject.failed += 1
            self.downloadObject.errors.append(result['error'])
            if self.listener:
                error = result['error']
                self.listener.send("updateQueue", {
                    'uuid': self.downloadObject.uuid,
                    'failed': True,
                    'data': error['data'],
                    'error': error['message'],
                    'errid': error.get('errid'),
                    'stack': error.get('stack'),
                    'type': error['type']
                })
        return result

    def afterDownloadErrorReport(self, position, error, itemData=None):
        if not itemData: itemData = {}
        data = {'position': position }
        data.update(itemData)
        logger.exception("%s %s", position, error)
        self.downloadObject.errors.append({
            'message': str(error),
            'stack': traceback.format_exc(),
            'data': data,
            'type': "post"
        })
        if self.listener:
            self.listener.send("updateQueue", {
                'uuid': self.downloadObject.uuid,
                'postFailed': True,
                'data': data,
                'error': str(error),
                'stack': traceback.format_exc(),
                'type': "post"
            })

    def afterDownloadSingle(self, track):
        if not self.downloadObject.extrasPath: self.downloadObject.extrasPath = Path(self.settings['downloadLocation'])

        # Save Album Cover
        try:
            if self.settings['saveArtwork'] and 'albumPath' in track:
                for image in track['albumURLs']:
                    downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
        except Exception as e:
            self.afterDownloadErrorReport("SaveLocalAlbumArt", e)

        # Save Artist Artwork
        try:
            if self.settings['saveArtworkArtist'] and 'artistPath' in track:
                for image in track['artistURLs']:
                    downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
        except Exception as e:
            self.afterDownloadErrorReport("SaveLocalArtistArt", e)

        # Create searched logfile
        try:
            if self.settings['logSearched'] and 'searched' in track:
                filename = f"{track.data.artist} - {track.data.title}"
                with open(self.downloadObject.extrasPath / 'searched.txt', 'w+', encoding="utf-8") as f:
                    searchedFile = f.read()
                    if not filename in searchedFile:
                        if searchedFile != "": searchedFile += "\r\n"
                        searchedFile += filename + "\r\n"
                    f.write(searchedFile)
        except Exception as e:
            self.afterDownloadErrorReport("CreateSearchedLog", e)

        # Execute command after download
        try:
            if self.settings['executeCommand'] != "":
                execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.downloadObject.extrasPath))).replace("%filename%", quote(track['filename'])))
        except Exception as e:
            self.afterDownloadErrorReport("ExecuteCommand", e)

    def afterDownloadCollection(self, tracks):
        if not self.downloadObject.extrasPath: self.downloadObject.extrasPath = Path(self.settings['downloadLocation'])
        playlist = [None] * len(tracks)
        errors = ""
        searched = ""

        for i, track in enumerate(tracks):
            track = track.result()
            if not track: return # Check if item is cancelled

            # Log errors to file
            if track.get('error'):
                if not track['error'].get('data'): track['error']['data'] = {'id': "0", 'title': 'Unknown', 'artist': 'Unknown'}
                errors += f"{track['error']['data']['id']} | {track['error']['data']['artist']} - {track['error']['data']['title']} | {track['error']['message']}\r\n"

            # Log searched to file
            if 'searched' in track: searched += track['searched'] + "\r\n"

            # Save Album Cover
            try:
                if self.settings['saveArtwork'] and 'albumPath' in track:
                    for image in track['albumURLs']:
                        downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
            except Exception as e:
                self.afterDownloadErrorReport("SaveLocalAlbumArt", e, track['data'])

            # Save Artist Artwork
            try:
                if self.settings['saveArtworkArtist'] and 'artistPath' in track:
                    for image in track['artistURLs']:
                        downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
            except Exception as e:
                self.afterDownloadErrorReport("SaveLocalArtistArt", e, track['data'])

            # Save filename for playlist file
            playlist[i] = track.get('filename', "")

        # Create errors logfile
        try:
            if self.settings['logErrors'] and errors != "":
                with open(self.downloadObject.extrasPath / 'errors.txt', 'w', encoding="utf-8") as f:
                    f.write(errors)
        except Exception as e:
            self.afterDownloadErrorReport("CreateErrorLog", e)

        # Create searched logfile
        try:
            if self.settings['logSearched'] and searched != "":
                with open(self.downloadObject.extrasPath / 'searched.txt', 'w', encoding="utf-8") as f:
                    f.write(searched)
        except Exception as e:
            self.afterDownloadErrorReport("CreateSearchedLog", e)

        # Save Playlist Artwork
        try:
            if self.settings['saveArtwork'] and self.playlistCoverName and not self.settings['tags']['savePlaylistAsCompilation']:
                for image in self.playlistURLs:
                    downloadImage(image['url'], self.downloadObject.extrasPath / f"{self.playlistCoverName}.{image['ext']}", self.settings['overwriteFile'])
        except Exception as e:
            self.afterDownloadErrorReport("SavePlaylistArt", e)

        # Create M3U8 File
        try:
            if self.settings['createM3U8File']:
                filename = generateDownloadObjectName(self.settings['playlistFilenameTemplate'], self.downloadObject, self.settings) or "playlist"
                with open(self.downloadObject.extrasPath / f'{filename}.m3u8', 'w', encoding="utf-8") as f:
                    for line in playlist:
                        f.write(line + "\n")
        except Exception as e:
            self.afterDownloadErrorReport("CreatePlaylistFile", e)

        # Execute command after download
        try:
            if self.settings['executeCommand'] != "":
                execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.downloadObject.extrasPath))))
        except Exception as e:
            self.afterDownloadErrorReport("ExecuteCommand", e)