示例#1
0
    async def download_song(self, songObj: SongObj) -> None:
        '''
        `songObj` `songObj` : song to be downloaded

        RETURNS `~`

        Downloads, Converts, Normalizes song & embeds metadata as ID3 tags.
        '''

        #! all YouTube downloads are to .\Temp; they are then converted and put into .\ and
        #! finally followed up with ID3 metadata tags

        #! we explicitly use the os.path.join function here to ensure download is
        #! platform agnostic

        # Create a .\Temp folder if not present
        tempFolder = Path('.', 'Temp')

        if not tempFolder.exists():
            tempFolder.mkdir()

        # build file name of converted file
        artistStr = ''

        #! we eliminate contributing artist names that are also in the song name, else we
        #! would end up with things like 'Jetta, Mastubs - I'd love to change the world
        #! (Mastubs REMIX).mp3' which is kinda an odd file name.
        for artist in songObj.get_contributing_artists():
            if artist.lower() not in songObj.get_song_name().lower():
                artistStr += artist + ', '

        #! the ...[:-2] is to avoid the last ', ' appended to artistStr
        convertedFileName = artistStr[:-2] + ' - ' + songObj.get_song_name()

        #! this is windows specific (disallowed chars)
        for disallowedChar in ['/', '?', '\\', '*', '|', '<', '>']:
            if disallowedChar in convertedFileName:
                convertedFileName = convertedFileName.replace(
                    disallowedChar, '')

        #! double quotes (") and semi-colons (:) are also disallowed characters but we would
        #! like to retain their equivalents, so they aren't removed in the prior loop
        convertedFileName = convertedFileName.replace(
            '"', "'").replace(': ', ' - ')

        convertedFilePath = Path(".", f"{convertedFileName}.opus")

        # if a song is already downloaded skip it
        if convertedFilePath.is_file():
            if self.displayManager:
                self.displayManager.notify_download_skip()
            if self.downloadTracker:
                self.downloadTracker.notify_download_completion(songObj)

            #! None is the default return value of all functions, we just explicitly define
            #! it here as a continent way to avoid executing the rest of the function.
            return None

        # download Audio from YouTube
        if self.displayManager:
            youtubeHandler = YouTube(
                url=songObj.get_youtube_link(),
                on_progress_callback=self.displayManager.pytube_progress_hook
            )
        else:
            youtubeHandler = YouTube(songObj.get_youtube_link())

        trackAudioStream = youtubeHandler.streams.filter(
            only_audio=True).order_by('bitrate').last()
        if not trackAudioStream:
            print(f"Unable to get audio stream for \"{songObj.get_song_name()}\" "
                  f"by \"{songObj.get_contributing_artists()[0]}\" "
                  f"from video \"{songObj.get_youtube_link()}\"")
            return None

        downloadedFilePathString = await self._download_from_youtube(convertedFileName, tempFolder,
                                                                     trackAudioStream)

        if downloadedFilePathString is None:
            return None

        downloadedFilePath = Path(downloadedFilePathString)

        # convert downloaded file

        command = 'ffmpeg -v quiet -y -i "%s" -acodec libopus -b:a 80K -vbr on -compression_level 10 "%s"'

        #! bash/ffmpeg on Unix systems need to have excape char (\) for special characters: \$
        #! alternatively the quotes could be reversed (single <-> double) in the command then
        #! the windows special characters needs escaping (^): ^\  ^&  ^|  ^>  ^<  ^^

        if sys.platform == 'win32':
            formattedCommand = command % (
                str(downloadedFilePath),
                str(convertedFilePath)
            )
        else:
            formattedCommand = command % (
                str(downloadedFilePath).replace('$', '\$'),
                str(convertedFilePath).replace('$', '\$')
            )

        process = await asyncio.subprocess.create_subprocess_shell(formattedCommand)
        _ = await process.communicate()

        #! Wait till converted file is actually created
        while True:
            if convertedFilePath.is_file():
                break

        if self.displayManager:
            self.displayManager.notify_conversion_completion()

        # embed song details

        audioFile = OggOpus(convertedFilePath)

        #! Get rid of all existing tags (if any exist)
        audioFile.delete()

        #! song name
        audioFile['title'] = songObj.get_song_name()
        audioFile['titlesort'] = songObj.get_song_name()

        #! track number
        audioFile['tracknumber'] = str(songObj.get_track_number())

        #! genres
        audioFile['genre'] = songObj.get_genres()

        #! all involved artists
        audioFile['artist'] = songObj.get_contributing_artists()

        #! album name
        audioFile['album'] = songObj.get_album_name()

        #! album artist (all of 'em)
        audioFile['albumartist'] = songObj.get_album_artists()

        #! album release date (to what ever precision available)
        audioFile['date'] = songObj.get_album_release()
        audioFile['originaldate'] = songObj.get_album_release()

        audioFile.save()

        #! setting the album art
        audioFile = OggOpus(convertedFilePath)

        rawAlbumArt = urlopen(songObj.get_album_cover_url()).read()

        picture = Picture()
        picture.data = rawAlbumArt
        picture.type = 3
        picture.desc = u"Cover"
        picture.mime = u"image/jpeg"

        picture_data = picture.write()
        encoded_data = base64.b64encode(picture_data)
        vcomment_value = encoded_data.decode("ascii")

        audioFile["metadata_block_picture"] = [vcomment_value]

        audioFile.save()

        # Do the necessary cleanup
        if self.displayManager:
            self.displayManager.notify_download_completion()

        if self.downloadTracker:
            self.downloadTracker.notify_download_completion(songObj)

        # delete the unnecessary YouTube download File
        if downloadedFilePath and downloadedFilePath.is_file():
            downloadedFilePath.unlink()
def download_song(songObj: SongObj,
                  displayManager: DisplayManager = None,
                  downloadTracker: DownloadTracker = None) -> None:
    '''
    `songObj` `songObj` : song to be downloaded

    `AutoProxy` `displayManager` : autoproxy reference to a `DisplayManager`

    `AutoProxy` `downloadTracker`: autoproxy reference to a `DownloadTracker`

    RETURNS `~`

    Downloads, Converts, Normalizes song & embeds metadata as ID3 tags.
    '''

    #! all YouTube downloads are to .\Temp; they are then converted and put into .\ and
    #! finally followed up with ID3 metadata tags

    #! we explicitly use the os.path.join function here to ensure download is
    #! platform agnostic

    # Create a .\Temp folder if not present
    tempFolder = join('.', 'Temp')

    if not exists(tempFolder):
        mkdir(tempFolder)

    # build file name of converted file
    artistStr = ''

    #! we eliminate contributing artist names that are also in the song name, else we
    #! would end up with things like 'Jetta, Mastubs - I'd love to change the world
    #! (Mastubs REMIX).mp3' which is kinda an odd file name.
    for artist in songObj.get_contributing_artists():
        if artist.lower() not in songObj.get_song_name().lower():
            artistStr += artist + ', '

    #! the ...[:-2] is to avoid the last ', ' appended to artistStr
    convertedFileName = artistStr[:-2] + ' - ' + songObj.get_song_name()

    #! this is windows specific (disallowed chars)
    for disallowedChar in ['/', '?', '\\', '*', '|', '<', '>']:
        if disallowedChar in convertedFileName:
            convertedFileName = convertedFileName.replace(disallowedChar, '')

    #! double quotes (") and semi-colons (:) are also disallowed characters but we would
    #! like to retain their equivalents, so they aren't removed in the prior loop
    convertedFileName = convertedFileName.replace('"',
                                                  "'").replace(': ', ' - ')
    #! if a songObj's playlistIndex is not None then we prepend it to keep the playlist/album order
    playlistIndex = songObj.get_playlist_index()
    if playlistIndex is not None:
        convertedFileName = '{:04d}'.format(
            playlistIndex) + ' - ' + convertedFileName

    convertedFilePath = join('.', convertedFileName) + '.mp3'

    # if a song is already downloaded skip it
    if exists(convertedFilePath):
        if displayManager:
            displayManager.notify_download_skip()
        if downloadTracker:
            downloadTracker.notify_download_completion(songObj)

        #! None is the default return value of all functions, we just explicitly define
        #! it here as a continent way to avoid executing the rest of the function.
        return None

    # download Audio from YouTube
    if displayManager:
        youtubeHandler = YouTube(
            url=songObj.get_youtube_link(),
            on_progress_callback=displayManager.pytube_progress_hook)
    else:
        youtubeHandler = YouTube(songObj.get_youtube_link())

    trackAudioStream = youtubeHandler.streams.get_audio_only()

    #! The actual download, if there is any error, it'll be here,
    try:
        #! pyTube will save the song in .\Temp\$songName.mp4, it doesn't save as '.mp3'
        downloadedFilePath = trackAudioStream.download(
            output_path=tempFolder,
            filename=convertedFileName,
            skip_existing=False)
    except:
        #! This is equivalent to a failed download, we do nothing, the song remains on
        #! downloadTrackers download queue and all is well...
        #!
        #! None is again used as a convenient exit
        remove(join(tempFolder, convertedFileName) + '.mp4')
        return None

    # convert downloaded file to MP3 with normalization

    #! -af loudnorm=I=-7:LRA applies EBR 128 loudness normalization algorithm with
    #! intergrated loudness target (I) set to -17, using values lower than -15
    #! causes 'pumping' i.e. rhythmic variation in loudness that should not
    #! exist -loud parts exaggerate, soft parts left alone.
    #!
    #! apad=pad_dur=2 adds 2 seconds of silence toward the end of the track, this is
    #! done because the loudnorm filter clips/cuts/deletes the last 1-2 seconds on
    #! occasion especially if the song is EDM-like, so we add a few extra seconds to
    #! combat that.
    #!
    #! -acodec libmp3lame sets the encoded to 'libmp3lame' which is far better
    #! than the default 'mp3_mf', '-abr true' automatically determines and passes the
    #! audio encoding bitrate to the filters and encoder. This ensures that the
    #! sampled length of songs matches the actual length (i.e. a 5 min song won't display
    #! as 47 seconds long in your music player, yeah that was an issue earlier.)

    command = 'ffmpeg -v quiet -y -i "%s" -acodec libmp3lame -abr true -af loudnorm=I=-17 "%s"'
    formattedCommand = command % (downloadedFilePath, convertedFilePath)

    run_in_shell(formattedCommand)

    #! Wait till converted file is actually created
    while True:
        if exists(convertedFilePath):
            break

    if displayManager:
        displayManager.notify_conversion_completion()

    # embed song details
    #! we save tags as both ID3 v2.3 and v2.4

    #! The simple ID3 tags
    audioFile = EasyID3(convertedFilePath)

    #! Get rid of all existing ID3 tags (if any exist)
    audioFile.delete()

    #! song name
    audioFile['title'] = songObj.get_song_name()
    audioFile['titlesort'] = songObj.get_song_name()

    #! track number
    audioFile['tracknumber'] = str(songObj.get_track_number())

    #! genres (pretty pointless if you ask me)
    #! we only apply the first available genre as ID3 v2.3 doesn't support multiple
    #! genres and ~80% of the world PC's run Windows - an OS with no ID3 v2.4 support
    genres = songObj.get_genres()

    if len(genres) > 0:
        audioFile['genre'] = genres[0]

    #! all involved artists
    audioFile['artist'] = songObj.get_contributing_artists()

    #! album name
    audioFile['album'] = songObj.get_album_name()

    #! album artist (all of 'em)
    audioFile['albumartist'] = songObj.get_album_artists()

    #! album release date (to what ever precision available)
    audioFile['date'] = songObj.get_album_release()
    audioFile['originaldate'] = songObj.get_album_release()

    #! spotify link: in case you wanna re-download your whole offline library,
    #! you can just read the links from the tags and redownload the songs.
    audioFile['website'] = songObj.get_spotify_link()

    #! save as both ID3 v2.3 & v2.4 as v2.3 isn't fully features and
    #! windows doesn't support v2.4 until later versions of Win10
    audioFile.save(v2_version=3)

    #! setting the album art
    audioFile = ID3(convertedFilePath)

    rawAlbumArt = urlopen(songObj.get_album_cover_url()).read()

    audioFile['APIC'] = AlbumCover(encoding=3,
                                   mime='image/jpeg',
                                   type=3,
                                   desc='Cover',
                                   data=rawAlbumArt)

    #! adding lyrics
    try:
        lyrics = songObj.get_song_lyrics()
        USLTOutput = USLT(encoding=3, lang=u'eng', desc=u'desc', text=lyrics)
        audioFile["USLT::'eng'"] = USLTOutput
    except:
        pass

    audioFile.save(v2_version=3)

    # Do the necessary cleanup
    if displayManager:
        displayManager.notify_download_completion()

    if downloadTracker:
        downloadTracker.notify_download_completion(songObj)

    # delete the unnecessary YouTube download File
    remove(downloadedFilePath)
示例#3
0
    async def download_song(self, songObj: SongObj) -> None:
        '''
        `songObj` `songObj` : song to be downloaded

        RETURNS `~`

        Downloads, Converts, Normalizes song & embeds metadata as ID3 tags.
        '''

        #! all YouTube downloads are to .\Temp; they are then converted and put into .\ and
        #! finally followed up with ID3 metadata tags

        #! we explicitly use the os.path.join function here to ensure download is
        #! platform agnostic

        # Create a .\Temp folder if not present
        tempFolder = Path('.', 'Temp')

        if not tempFolder.exists():
            tempFolder.mkdir()

        # build file name of converted file
        artistStr = ''

        #! we eliminate contributing artist names that are also in the song name, else we
        #! would end up with things like 'Jetta, Mastubs - I'd love to change the world
        #! (Mastubs REMIX).mp3' which is kinda an odd file name.
        for artist in songObj.get_contributing_artists():
            if artist.lower() not in songObj.get_song_name().lower():
                artistStr += artist + ', '

        #! the ...[:-2] is to avoid the last ', ' appended to artistStr
        convertedFileName = artistStr[:-2] + ' - ' + songObj.get_song_name()

        #! this is windows specific (disallowed chars)
        for disallowedChar in ['/', '?', '\\', '*', '|', '<', '>']:
            if disallowedChar in convertedFileName:
                convertedFileName = convertedFileName.replace(
                    disallowedChar, '')

        #! double quotes (") and semi-colons (:) are also disallowed characters but we would
        #! like to retain their equivalents, so they aren't removed in the prior loop
        convertedFileName = convertedFileName.replace('"', "'").replace(
            ': ', ' - ')

        convertedFilePath = Path(".", f"{convertedFileName}.mp3")

        # if a song is already downloaded skip it
        if convertedFilePath.is_file():
            if self.displayManager:
                self.displayManager.notify_download_skip()
            if self.downloadTracker:
                self.downloadTracker.notify_download_completion(songObj)

            #! None is the default return value of all functions, we just explicitly define
            #! it here as a continent way to avoid executing the rest of the function.
            return None

        # download Audio from YouTube
        if self.displayManager:
            youtubeHandler = YouTube(
                url=songObj.get_youtube_link(),
                on_progress_callback=self.displayManager.pytube_progress_hook)
        else:
            youtubeHandler = YouTube(songObj.get_youtube_link())

        trackAudioStream = youtubeHandler.streams.filter(
            only_audio=True).order_by('bitrate').last()
        if not trackAudioStream:
            print(
                f"Unable to get audio stream for \"{songObj.get_song_name()}\" "
                f"by \"{songObj.get_contributing_artists()[0]}\" "
                f"from video \"{songObj.get_youtube_link()}\"")
            return None

        downloadedFilePathString = await self._download_from_youtube(
            convertedFileName, tempFolder, trackAudioStream)

        if downloadedFilePathString is None:
            return None

        downloadedFilePath = Path(downloadedFilePathString)

        # convert downloaded file to MP3 with normalization

        #! -af loudnorm=I=-7:LRA applies EBR 128 loudness normalization algorithm with
        #! intergrated loudness target (I) set to -17, using values lower than -15
        #! causes 'pumping' i.e. rhythmic variation in loudness that should not
        #! exist -loud parts exaggerate, soft parts left alone.
        #!
        #! dynaudnorm applies dynamic non-linear RMS based normalization, this is what
        #! actually normalized the audio. The loudnorm filter just makes the apparent
        #! loudness constant
        #!
        #! apad=pad_dur=2 adds 2 seconds of silence toward the end of the track, this is
        #! done because the loudnorm filter clips/cuts/deletes the last 1-2 seconds on
        #! occasion especially if the song is EDM-like, so we add a few extra seconds to
        #! combat that.
        #!
        #! -acodec libmp3lame sets the encoded to 'libmp3lame' which is far better
        #! than the default 'mp3_mf', '-abr true' automatically determines and passes the
        #! audio encoding bitrate to the filters and encoder. This ensures that the
        #! sampled length of songs matches the actual length (i.e. a 5 min song won't display
        #! as 47 seconds long in your music player, yeah that was an issue earlier.)

        command = 'ffmpeg -v quiet -y -i "%s" -acodec libmp3lame -abr true ' \
            f'-b:a {trackAudioStream.bitrate} ' \
                  '-af "apad=pad_dur=2, dynaudnorm, loudnorm=I=-17" "%s"'

        #! bash/ffmpeg on Unix systems need to have excape char (\) for special characters: \$
        #! alternatively the quotes could be reversed (single <-> double) in the command then
        #! the windows special characters needs escaping (^): ^\  ^&  ^|  ^>  ^<  ^^

        if sys.platform == 'win32':
            formattedCommand = command % (str(downloadedFilePath),
                                          str(convertedFilePath))
        else:
            formattedCommand = command % (str(downloadedFilePath).replace(
                '$', '\$'), str(convertedFilePath).replace('$', '\$'))

        process = await asyncio.subprocess.create_subprocess_shell(
            formattedCommand)
        _ = await process.communicate()

        #! Wait till converted file is actually created
        while True:
            if convertedFilePath.is_file():
                break

        if self.displayManager:
            self.displayManager.notify_conversion_completion()

        # embed song details
        #! we save tags as both ID3 v2.3 and v2.4

        #! The simple ID3 tags
        audioFile = EasyID3(convertedFilePath)

        #! Get rid of all existing ID3 tags (if any exist)
        audioFile.delete()

        #! song name
        audioFile['title'] = songObj.get_song_name()
        audioFile['titlesort'] = songObj.get_song_name()

        #! track number
        audioFile['tracknumber'] = str(songObj.get_track_number())

        #! genres (pretty pointless if you ask me)
        #! we only apply the first available genre as ID3 v2.3 doesn't support multiple
        #! genres and ~80% of the world PC's run Windows - an OS with no ID3 v2.4 support
        genres = songObj.get_genres()

        if len(genres) > 0:
            audioFile['genre'] = genres[0]

        #! all involved artists
        audioFile['artist'] = songObj.get_contributing_artists()

        #! album name
        audioFile['album'] = songObj.get_album_name()

        #! album artist (all of 'em)
        audioFile['albumartist'] = songObj.get_album_artists()

        #! album release date (to what ever precision available)
        audioFile['date'] = songObj.get_album_release()
        audioFile['originaldate'] = songObj.get_album_release()

        #! save as both ID3 v2.3 & v2.4 as v2.3 isn't fully features and
        #! windows doesn't support v2.4 until later versions of Win10
        audioFile.save(v2_version=3)

        #! setting the album art
        audioFile = ID3(convertedFilePath)

        rawAlbumArt = urlopen(songObj.get_album_cover_url()).read()

        audioFile['APIC'] = AlbumCover(encoding=3,
                                       mime='image/jpeg',
                                       type=3,
                                       desc='Cover',
                                       data=rawAlbumArt)

        audioFile.save(v2_version=3)

        # Do the necessary cleanup
        if self.displayManager:
            self.displayManager.notify_download_completion()

        if self.downloadTracker:
            self.downloadTracker.notify_download_completion(songObj)

        # delete the unnecessary YouTube download File
        if downloadedFilePath and downloadedFilePath.is_file():
            downloadedFilePath.unlink()
示例#4
0
def download_song(
    songObj: SongObj,
    displayManager: DisplayManager = None,
    downloadTracker: DownloadTracker = None,
) -> None:
    """
    `songObj` `songObj` : song to be downloaded

    `AutoProxy` `displayManager` : autoproxy reference to a `DisplayManager`

    `AutoProxy` `downloadTracker`: autoproxy reference to a `DownloadTracker`

    RETURNS `~`

    Downloads, Converts, Normalizes song & embeds metadata as ID3 tags.
    """

    #! all YouTube downloads are to .\Temp; they are then converted and put into .\ and
    #! finally followed up with ID3 metadata tags

    #! we explicitly use the os.path.join function here to ensure download is
    #! platform agnostic

    # Create a .\Temp folder if not present
    tempFolder = join(".", "Temp")

    if not exists(tempFolder):
        mkdir(tempFolder)

    # build file name of converted file
    artistStr = ""

    #! we eliminate contributing artist names that are also in the song name, else we
    #! would end up with things like 'Jetta, Mastubs - I'd love to change the world
    #! (Mastubs REMIX).mp3' which is kinda an odd file name.
    for artist in songObj.get_contributing_artists():
        if artist.lower() not in songObj.get_song_name().lower():
            artistStr += artist + ", "

    #! the ...[:-2] is to avoid the last ', ' appended to artistStr
    convertedFileName = artistStr[:-2] + " - " + songObj.get_song_name()

    #! this is windows specific (disallowed chars)
    for disallowedChar in ["/", "?", "\\", "*", "|", "<", ">"]:
        if disallowedChar in convertedFileName:
            convertedFileName = convertedFileName.replace(disallowedChar, "")

    #! double quotes (") and semi-colons (:) are also disallowed characters but we would
    #! like to retain their equivalents, so they aren't removed in the prior loop
    convertedFileName = convertedFileName.replace('"',
                                                  "'").replace(": ", " - ")

    convertedFilePath = join(".", convertedFileName) + ".mp3"

    # if a song is already downloaded skip it
    if exists(convertedFilePath):
        if displayManager:
            displayManager.notify_download_skip()
        if downloadTracker:
            downloadTracker.notify_download_completion(songObj)

        #! None is the default return value of all functions, we just explicitly define
        #! it here as a continent way to avoid executing the rest of the function.
        return None

    Lyrics = songObj.get_lyrics()

    # download Audio from YouTube
    if displayManager:
        youtubeHandler = YouTube(
            url=songObj.get_youtube_link(),
            on_progress_callback=displayManager.pytube_progress_hook,
        )
    else:
        youtubeHandler = YouTube(songObj.get_youtube_link())

    trackAudioStream = youtubeHandler.streams.get_audio_only()

    #! The actual download, if there is any error, it'll be here,
    try:
        #! pyTube will save the song in .\Temp\$songName.mp4, it doesn't save as '.mp3'
        downloadedFilePath = trackAudioStream.download(
            output_path=tempFolder,
            filename=convertedFileName,
            skip_existing=False)
    except:
        #! This is equivalent to a failed download, we do nothing, the song remains on
        #! downloadTrackers download queue and all is well...
        #!
        #! None is again used as a convenient exit
        remove(join(tempFolder, convertedFileName) + ".mp4")
        return None

    # convert downloaded file to MP3 with normalization

    #! -af loudnorm=I=-7:LRA applies EBR 128 loudness normalization algorithm with
    #! intergrated loudness target (I) set to -17, using values lower than -15
    #! causes 'pumping' i.e. rhythmic variation in loudness that should not
    #! exist -loud parts exaggerate, soft parts left alone.
    #!
    #! dynaudnorm applies dynamic non-linear RMS based normalization, this is what
    #! actually normalized the audio. The loudnorm filter just makes the apparent
    #! loudness constant
    #!
    #! apad=pad_dur=2 adds 2 seconds of silence toward the end of the track, this is
    #! done because the loudnorm filter clips/cuts/deletes the last 1-2 seconds on
    #! occasion especially if the song is EDM-like, so we add a few extra seconds to
    #! combat that.
    #!
    #! -acodec libmp3lame sets the encoded to 'libmp3lame' which is far better
    #! than the default 'mp3_mf', '-abr true' automatically determines and passes the
    #! audio encoding bitrate to the filters and encoder. This ensures that the
    #! sampled length of songs matches the actual length (i.e. a 5 min song won't display
    #! as 47 seconds long in your music player, yeah that was an issue earlier.)

    command = 'ffmpeg  -v quiet -hwaccel_output_format cuda -y -i "%s" -acodec libmp3lame -abr true  "%s"'
    formattedCommand = command % (downloadedFilePath, convertedFilePath)

    # run_in_shell(formattedCommand)
    return_code = None
    return_code = call(formattedCommand)

    if return_code != 0:
        raise ("Error occurred during conversion, ffmpeg issue probably")

    if displayManager:
        displayManager.notify_conversion_completion()

    # embed song details
    #! we save tags as both ID3 v2.3 and v2.4

    #! The simple ID3 tags
    audioFile = EasyID3(convertedFilePath)

    #! Get rid of all existing ID3 tags (if any exist)
    audioFile.delete()

    #! song name
    audioFile["title"] = songObj.get_song_name()
    audioFile["titlesort"] = songObj.get_song_name()

    #! track number
    audioFile["tracknumber"] = str(songObj.get_track_number())

    #! genres (pretty pointless if you ask me)
    #! we only apply the first available genre as ID3 v2.3 doesn't support multiple
    #! genres and ~80% of the world PC's run Windows - an OS with no ID3 v2.4 support
    genres = songObj.get_genres()

    if len(genres) > 0:
        audioFile["genre"] = genres[0]

    #! all involved artists
    audioFile["artist"] = songObj.get_contributing_artists()

    #! album name
    audioFile["album"] = songObj.get_album_name()

    #! album artist (all of 'em)
    audioFile["albumartist"] = songObj.get_album_artists()

    #! album release date (to what ever precision available)
    audioFile["date"] = songObj.get_album_release()
    audioFile["originaldate"] = songObj.get_album_release()
    #! save as both ID3 v2.3 & v2.4 as v2.3 isn't fully features and
    #! windows doesn't support v2.4 until later versions of Win10
    audioFile.save(v2_version=3)

    #! setting the album art
    audioFile = ID3(convertedFilePath)

    rawAlbumArt = get(songObj.get_album_cover_url()).content

    audioFile["APIC"] = AlbumCover(encoding=3,
                                   mime="image/jpeg",
                                   type=3,
                                   desc="Cover",
                                   data=rawAlbumArt)

    audioFile["USLT"] = USLT(encoding=3, desc=u"Lyrics", text=Lyrics)

    audioFile.save(v2_version=3)

    # Do the necessary cleanup
    if displayManager:
        displayManager.notify_download_completion()

    if downloadTracker:
        downloadTracker.notify_download_completion(songObj)

    # delete the unnecessary YouTube download File
    remove(downloadedFilePath)