def songobj_from_spotify_url(spotifyURL: str, output_format: str = None):
    # check if URL is a playlist, user, artist or album, if yes raise an Exception,
    # else procede

    # Get the Song Metadata
    print(f"Gathering Spotify Metadata for: {spotifyURL}")
    rawTrackMeta, rawArtistMeta, rawAlbumMeta = metadataProvider.from_url(spotifyURL)

    songName = rawTrackMeta["name"]
    albumName = rawTrackMeta["album"]["name"]
    contributingArtists = []
    for artist in rawTrackMeta["artists"]:
        contributingArtists.append(artist["name"])
    duration = round(rawTrackMeta["duration_ms"] / 1000, ndigits=3)

    convertedFileName = SongObj.create_file_name(
        songName, [artist["name"] for artist in rawTrackMeta["artists"]]
    )

    if output_format is None:
        output_format = "mp3"

    convertedFilePath = Path(".", f"{convertedFileName}.{output_format}")
    displayName = ", ".join(contributingArtists) + " - " + songName

    # if a song is already downloaded skip it
    if convertedFilePath.is_file():
        print(f'Skipping "{convertedFileName}" as it\'s already downloaded')
        return None

    # Get the song's downloadable audio link
    print(f'Searching YouTube for "{displayName}"', end="\r")
    youtubeLink = audioProvider.search_and_get_best_match(
        songName, contributingArtists, albumName, duration
    )
    if youtubeLink is None:
        # raise Exception("Could not match any of the results on YouTube")
        print("Could not match any of the results on YouTube. Skipping")
        return None
    else:
        print(" " * (len(displayName) + 25), end="\r")
        print(f'Found YouTube URL for "{displayName}" : {youtubeLink}')

    # (try to) Get lyrics from Genius
    try:
        lyrics = metadataProvider.get_song_lyrics(songName, contributingArtists)
    except (AttributeError, IndexError):
        lyrics = ""

    return SongObj(rawTrackMeta, rawAlbumMeta, rawArtistMeta, youtubeLink, lyrics)
    def load_tracking_file(self, trackingFilePath: str) -> None:
        """
        `str` `trackingFilePath` : path to a .spotdlTrackingFile

        RETURNS `~`

        reads songsObj's from disk and prepares to track their download
        """

        # Attempt to read .spotdlTrackingFile, raise exception if file can't be read
        trackingFile = Path(trackingFilePath)
        if not trackingFile.is_file():
            raise FileNotFoundError(
                f"no such tracking file found: {trackingFilePath}")

        with trackingFile.open("rb") as file_handle:
            songDataDumps = eval(file_handle.read().decode())

        # Save path to .spotdlTrackingFile
        self.saveFile = trackingFile

        # convert song data dumps to songObj's
        # ! see, songObj.get_data_dump and songObj.from_dump for more details
        for dump in songDataDumps:
            self.songObjList.append(SongObj.from_dump(dump))
Example #3
0
def search_for_song(query: str) -> SongObj:
    '''
    `str` `query` : what you'd type into spotify's search box

    Queries Spotify for a song and returns the best match
    '''

    # get a spotify client
    spotifyClient = get_spotify_client()

    # get possible matches from spotify
    result = spotifyClient.search(query, type='track')

    # return first result link or if no matches are found, raise and Exception
    if len(result['tracks']['items']) == 0:
        raise Exception('No song matches found on Spotify')
    else:
        for songResult in result['tracks']['items']:
            songUrl = 'http://open.spotify.com/track/' + songResult['id']
            song = SongObj.from_url(songUrl)

            if song.get_youtube_link() != None:
                return song

        raise Exception('Could not match any of the results on YouTube')
Example #4
0
def get_album_tracks(albumUrl: str) -> List[SongObj]:
    '''
    `str` `albumUrl` : Spotify Url of the album whose tracks are to be
    retrieved

    returns a `list<songObj>` containing Url's of each track in the given album
    '''

    spotifyClient = get_spotify_client()
    albumTracks = []

    trackResponse = spotifyClient.album_tracks(albumUrl)

    # while loop acts like do-while
    while True:

        for track in trackResponse['items']:
            song = SongObj.from_url('https://open.spotify.com/track/' +
                                    track['id'])

            if song.get_youtube_link() != None:
                albumTracks.append(song)

        # check if more tracks are to be passed
        if trackResponse['next']:
            trackResponse = spotifyClient.album_tracks(albumUrl,
                                                       offset=len(albumTracks))
        else:
            break

    return albumTracks
Example #5
0
def create_song_obj(name="test song", artist="test artist") -> SongObj:
    artists = [{"name": artist}]
    raw_track_meta = {
        "name": name,
        "album": {
            "name":
            "test album",
            "artists":
            artists,
            "release_date":
            "2021",
            "images": [{
                "url":
                "https://i.ytimg.com/vi_webp/iqKdEhx-dD4/hqdefault.webp"
            }],
        },
        "artists": artists,
        "track_number": "1",
        "genres": ["test genre"],
    }
    raw_album_meta = {"genres": ["test genre"]}
    raw_artist_meta = {"genres": ["test artist genre"]}
    return SongObj(
        raw_track_meta,
        raw_album_meta,
        raw_artist_meta,
        "https://www.youtube.com/watch?v=Th_C95UMegc",
    )
    def load_tracking_file(self, trackingFilePath: str) -> None:
        '''
        `str` `trackingFilePath` : path to a .spotdlTrackingFile

        RETURNS `~`

        reads songsObj's from disk and prepares to track their download
        '''

        # Attempt to read .spotdlTrackingFile, raise exception if file can't be read
        try:
            file = open(trackingFilePath, 'rb')
            songDataDumps = eval(file.read().decode())
            file.close()
        except FileNotFoundError:
            raise Exception('no such tracking file found: %s' %
                            trackingFilePath)

        # Save path to .spotdlTrackingFile
        self.saveFile = trackingFilePath

        # convert song data dumps to songObj's
        #! see, songObj.get_data_dump and songObj.from_dump for more details
        for dump in songDataDumps:
            self.songObjList.append(SongObj.from_dump(dump))
Example #7
0
def get_playlist_tracks(playlistUrl: str) -> List[SongObj]:
    '''
    `str` `playlistUrl` : Spotify Url of the album whose tracks are to be
    retrieved

    returns a `list<songObj>` containing Url's of each track in the given playlist
    '''

    spotifyClient = SpotifyClient()
    playlistTracks = []

    playlistResponse = spotifyClient.playlist_tracks(playlistUrl)

    # while loop to mimic do-while
    while True:

        for songEntry in playlistResponse['items']:
            if songEntry['track'] is None or songEntry['track']['id'] is None:
                continue

            song = SongObj.from_url('https://open.spotify.com/track/' +
                                    songEntry['track']['id'])

            if song.get_youtube_link() is not None:
                playlistTracks.append(song)

        # check if more tracks are to be passed
        if playlistResponse['next']:
            playlistResponse = spotifyClient.playlist_tracks(
                playlistUrl,
                offset=playlistResponse['offset'] + playlistResponse['limit'])
        else:
            break

    return playlistTracks
Example #8
0
def get_artist_tracks(artistUrl: str) -> List[SongObj]:
    '''
    `str` `albumUrl` : Spotify Url of the artist whose tracks are to be
    retrieved

    returns a `list<songObj>` containing Url's of each track in the artist profile
    '''

    spotifyClient = SpotifyClient()
    artistTracks = []
    offset = 0

    artistResponse = spotifyClient.artist_albums(artistUrl)

    # while loop acts like do-while
    while True:
        for album in artistResponse['items']:
            # get albums and singles
            if not (album['album_group'] == 'appears_on'
                    and album['album_type'] in ['album', 'compilation']):
                artistTracks.extend(get_album_tracks(album['id']))
            # get features from other artists albums
            elif album['album_group'] == 'appears_on' and album[
                    'album_type'] == 'album':
                trackResponse = spotifyClient.album_tracks(album['uri'])
                albumTracks = []

                # while loop acts like do-while
                while True:
                    for track in trackResponse['items']:
                        for artist in track['artists']:
                            if artist['id'] == artistResponse['href'].split(
                                    '/')[-2]:
                                song = SongObj.from_url(
                                    'https://open.spotify.com/track/' +
                                    track['id'])

                                if song.get_youtube_link() is not None:
                                    albumTracks.append(song)

                    # check if more tracks are to be passed
                    if trackResponse['next']:
                        trackResponse = spotifyClient.album_tracks(
                            album['uri'], offset=len(albumTracks))
                    else:
                        break

                artistTracks.extend(albumTracks)

        offset += len(artistResponse['items'])

        # check if more albums are to be passed
        if artistResponse['next']:
            artistResponse = spotifyClient.artist_albums(artistUrl,
                                                         offset=offset)
        else:
            break

    return artistTracks
Example #9
0
def console_entry_point():
    '''
    This is where all the console processing magic happens.
    Its super simple, rudimentary even but, it's dead simple & it works.
    '''
    arguments = parse_arguments()

    SpotifyClient.init(client_id='4fe3fecfe5334023a1472516cc99d805',
                       client_secret='0f02b7c483c04257984695007a4a8d5c')

    if arguments.path:
        if not os.path.isdir(arguments.path):
            sys.exit("The output directory doesn't exist.")
        print(f"Will download to: {os.path.abspath(arguments.path)}")
        os.chdir(arguments.path)

    with DownloadManager() as downloader:

        for request in arguments.url:
            if 'open.spotify.com' in request and 'track' in request:
                print('Fetching Song...')
                song = SongObj.from_url(request)

                if song.get_youtube_link() is not None:
                    downloader.download_single_song(song)
                else:
                    print(
                        'Skipping %s (%s) as no match could be found on youtube'
                        % (song.get_song_name(), request))

            elif 'open.spotify.com' in request and 'album' in request:
                print('Fetching Album...')
                songObjList = get_album_tracks(request)

                downloader.download_multiple_songs(songObjList)

            elif 'open.spotify.com' in request and 'playlist' in request:
                print('Fetching Playlist...')
                songObjList = get_playlist_tracks(request)

                downloader.download_multiple_songs(songObjList)

            elif 'open.spotify.com' in request and 'artist' in request:
                print('Fetching artist...')
                artistObjList = get_artist_tracks(request)

                downloader.download_multiple_songs(artistObjList)

            elif request.endswith('.spotdlTrackingFile'):
                print('Preparing to resume download...')
                downloader.resume_download_from_tracking_file(request)

            else:
                print('Searching for song "%s"...' % request)
                try:
                    song = search_for_song(request)
                    downloader.download_single_song(song)
                except Exception as e:
                    print(e)
def from_dump(dataDump: dict):
    rawTrackMeta = dataDump["rawTrackMeta"]
    rawAlbumMeta = dataDump["rawAlbumMeta"]
    rawArtistMeta = dataDump["rawAlbumMeta"]
    youtubeLink = dataDump["youtubeLink"]
    lyrics = dataDump["lyrics"]

    return SongObj(rawTrackMeta, rawAlbumMeta, rawArtistMeta, youtubeLink, lyrics)
Example #11
0
def console_entry_point():
    '''
    This is where all the console processing magic happens.
    Its super simple, rudimentary even but, it's dead simple & it works.
    '''

    if '--help' in sys.argv or '-h' in sys.argv or len(sys.argv) == 1:
        print(help_notice)

        #! We use 'return None' as a convenient exit/break from the function
        return None

    spotifyClient.initialize(
        clientId='4fe3fecfe5334023a1472516cc99d805',
        clientSecret='0f02b7c483c04257984695007a4a8d5c'
        )

    downloader = DownloadManager()

    for request in sys.argv[1:]:
        if 'open.spotify.com' in request and 'track' in request:
            print('Fetching Song...')
            song = SongObj.from_url(request)

            if song.get_youtube_link() != None:
                downloader.download_single_song(song)
            else:
                print('Skipping %s (%s) as no match could be found on youtube' % (
                    song.get_song_name(), request
                ))

        elif 'open.spotify.com' in request and 'album' in request:
            print('Fetching Album...')
            songObjList = get_album_tracks(request)

            downloader.download_multiple_songs(songObjList)

        elif 'open.spotify.com' in request and 'playlist' in request:
            print('Fetching Playlist...')
            songObjList = get_playlist_tracks(request)

            downloader.download_multiple_songs(songObjList)

        elif request.endswith('.spotdlTrackingFile'):
            print('Preparing to resume download...')
            downloader.resume_download_from_tracking_file(request)

        else:
            print('Searching for song "%s"...' % request)
            try:
                song = search_for_song(request)
                downloader.download_single_song(song)

            except Exception:
                print('No song named "%s" could be found on spotify' % request)

    downloader.close()
Example #12
0
def dlmusic():
    Dir=Dirfield.get()
    url=musfield.get()
    temp=SongObj.from_url(url)
    currdir=os.getcwd()
    os.chdir(Dir)
    download=DownloadManager()
    download.download_single_song(songObj=temp)
    os.remove('./Temp')
    os.chdir(currdir)
    return
Example #13
0
def get_song_obj_from_url(songEntry, playlistResponse, spotifyClient,
                          playlistUrl):
    playlistTracks = []
    song = SongObj.from_url('https://open.spotify.com/track/' +
                            songEntry['track']['id'])
    print('Got song info: %s' % (song.get_song_name()))
    if song.get_youtube_link() != None:
        playlistTracks.append(song)

    # check if more tracks are to be passed
    if playlistResponse['next']:
        playlistResponse = spotifyClient.playlist_tracks(
            playlistUrl, offset=len(playlistTracks))

    return playlistTracks
Example #14
0
def process_request(downloader, request):
    if 'open.spotify.com' in request and 'track' in request:
        print('Fetching Song...')
        song = SongObj.from_url(request)

        if song.get_youtube_link() is not None:
            downloader.download_single_song(song)
        else:
            print('Skipping %s (%s) as no match could be found on youtube' %
                  (song.get_song_name(), request))

    elif 'open.spotify.com' in request and 'album' in request:
        print('Fetching Album...')
        songObjList = get_album_tracks(request)

        downloader.download_multiple_songs(songObjList)

    elif 'open.spotify.com' in request and 'playlist' in request:
        print('Fetching Playlist...')
        songObjList = get_playlist_tracks(request)

        downloader.download_multiple_songs(songObjList)

    elif 'open.spotify.com' in request and 'artist' in request:
        print('Fetching artist...')
        artistObjList = get_artist_tracks(request)

        downloader.download_multiple_songs(artistObjList)

    elif request.endswith('.spotdlTrackingFile'):
        print('Preparing to resume download...')
        downloader.resume_download_from_tracking_file(request)

    else:
        print('Searching for song "%s"...' % request)
        try:
            song = search_for_song(request)
            downloader.download_single_song(song)
        except Exception as e:
            print(e)
Example #15
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)

        # Encapsulate downloaded file to OGG container without re-processing

        command = 'ffmpeg -v quiet -y -i "%s" -acodec copy "%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()

        self.set_vorbis_data(convertedFilePath, songObj)

        # 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()
Example #16
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()
Example #17
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.
        """

        dispayProgressTracker = self.displayManager.new_progress_tracker(
            songObj)

        # ! since most errors are expected to happen within this function, we wrap in
        # ! exception catcher to prevent blocking on multiple downloads
        try:

            # ! 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()

            convertedFileName = songObj.get_file_name()

            convertedFilePath = Path(
                ".", f"{convertedFileName}.{self.arguments['format']}")

            # if a song is already downloaded skip it
            if convertedFilePath.is_file():
                if self.displayManager:
                    dispayProgressTracker.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 dispayProgressTracker:
                youtubeHandler = YouTube(
                    url=songObj.get_youtube_link(),
                    on_progress_callback=dispayProgressTracker.
                    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._perform_audio_download_async(
                convertedFileName, tempFolder, trackAudioStream)

            if downloadedFilePathString is None:
                return None

            if dispayProgressTracker:
                dispayProgressTracker.notify_youtube_download_completion()

            downloadedFilePath = Path(downloadedFilePathString)

            ffmpeg_success = await ffmpeg.convert(
                downloaded_file_path=downloadedFilePath,
                converted_file_path=convertedFilePath,
                output_format=self.arguments["format"],
                ffmpeg_path=self.arguments["ffmpeg_path"],
            )

            if dispayProgressTracker:
                dispayProgressTracker.notify_conversion_completion()

            if ffmpeg_success is False:
                # delete the file that wasn't successfully converted
                convertedFilePath.unlink()
            else:
                # if a file was successfully downloaded, tag it
                set_id3_data(convertedFilePath, songObj,
                             self.arguments["format"])

            # Do the necessary cleanup
            if dispayProgressTracker:
                dispayProgressTracker.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()

        except Exception as e:
            tb = traceback.format_exc()
            if dispayProgressTracker:
                dispayProgressTracker.notify_error(e, tb)
            else:
                raise e
Example #18
0
def console_entry_point():
    """
    This is where all the console processing magic happens.
    Its super simple, rudimentary even but, it's dead simple & it works.
    """

    if "--help" in cliArgs or "-h" in cliArgs:
        print(help_notice)

        #! We use 'return None' as a convenient exit/break from the function
        return None

    initialize(
        clientId="4fe3fecfe5334023a1472516cc99d805",
        clientSecret="0f02b7c483c04257984695007a4a8d5c",
    )

    downloader = DownloadManager()

    for request in cliArgs[1:]:
        if "?" in request:
            # strip unnecessary data for both url and uri
            # e.g https://open.spotify.com/track/4Q34FP1AT7GEl9oLgNtiWj?context=spotify%3Aplaylist%3A37i9dQZF1DXcBWIGoYBM5M&si=DlMAsJ5pSD6tdUSn2XqB0g
            # becomes https://open.spotify.com/track/4Q34FP1AT7GEl9oLgNtiWj
            # e.g spotify:track:4Q34FP1AT7GEl9oLgNtiWj?context=spotify%3Aplaylist%3A37i9dQZF1DXcBWIGoYBM5M
            # becomes spotify:track:4Q34FP1AT7GEl9oLgNtiWj

            request = request[:request.find("?")]
        if "open.spotify.com" in request:
            # it's a url
            if "track" in request:
                print("Fetching Song...")
                song = SongObj.from_url(request)

                if song.get_youtube_link() != None:
                    downloader.download_single_song(song)
                else:
                    print(
                        "Skipping %s (%s) as no match could be found on youtube"
                        % (song.get_song_name(), request))

            elif "album" in request:
                print("Fetching Album...")
                songObjList = get_album_tracks(request)

                downloader.download_multiple_songs(songObjList)

            elif "playlist" in request:
                print("Fetching Playlist...")
                songObjList = get_playlist_tracks(request)

                downloader.download_multiple_songs(songObjList)
        elif "spotify:" in request:
            # it's a URI with format Spotify:...:ID
            if "track:" in request:
                print("Fetching Song...")
                # yes, passing a URI to this function still works coz it relies on another
                # spotipy function that simply extracts the ID, ideally u can just pass the ID
                # and the track downloads
                song = SongObj.from_url(request)

                if song.get_youtube_link() != None:
                    downloader.download_single_song(song)
                else:
                    print(
                        f"Skipping {song.get_song_name()} ({request}) as no match could be found on youtube"
                    )

            elif "album:" in request:
                print("Fetching Album...")
                songObjList = get_album_tracks(request)

                downloader.download_multiple_songs(songObjList)

            elif "playlist:" in request:
                print("Fetching Playlist...")
                songObjList = get_playlist_tracks(request)

                downloader.download_multiple_songs(songObjList)

        elif request.endswith(".spotdlTrackingFile"):
            print("Preparing to resume download...")
            downloader.resume_download_from_tracking_file(request)

        else:
            print('Searching for song "%s"...' % request)
            try:
                song = search_for_song(request)
                downloader.download_single_song(song)

            except Exception:
                print('No song named "%s" could be found on spotify' % request)

    downloader.close()
Example #19
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()
def console_entry_point():
    '''
    This is where all the console processing magic happens.
    Its super simple, rudimentary even but, it's dead simple & it works.
    '''
    arguments = parse_arguments()

    if ffmpeg.has_correct_version(arguments.ignore_ffmpeg_version) is False:
        sys.exit(1)

    SpotifyClient.init(client_id='5f573c9620494bae87890c0f08a60293',
                       client_secret='212476d9b0f3472eaa762d90b19b0ba8')

    if arguments.path:
        if not os.path.isdir(arguments.path):
            sys.exit("The output directory doesn't exist.")
        print(f"Will download to: {os.path.abspath(arguments.path)}")
        os.chdir(arguments.path)

    with DownloadManager() as downloader:

        for request in arguments.url:
            if 'open.spotify.com' in request and 'track' in request:
                print('Fetching Song...')
                song = SongObj.from_url(request)

                if song.get_youtube_link() is not None:
                    downloader.download_single_song(song)
                else:
                    print(
                        'Skipping %s (%s) as no match could be found on youtube'
                        % (song.get_song_name(), request))

            elif 'open.spotify.com' in request and 'album' in request:
                print('Fetching Album...')
                songObjList = get_album_tracks(request)

                downloader.download_multiple_songs(songObjList)

            elif 'open.spotify.com' in request and 'playlist' in request:
                print('Fetching Playlist...')
                songObjList = get_playlist_tracks(request)

                downloader.download_multiple_songs(songObjList)

            elif 'open.spotify.com' in request and 'artist' in request:
                print('Fetching artist...')
                artistObjList = get_artist_tracks(request)

                downloader.download_multiple_songs(artistObjList)

            elif request.endswith('.spotdlTrackingFile'):
                print('Preparing to resume download...')
                downloader.resume_download_from_tracking_file(request)

            else:
                print('Searching for song "%s"...' % request)
                try:
                    song = search_for_song(request)
                    downloader.download_single_song(song)
                except Exception as e:
                    print(e)
Example #21
0
def console_entry_point():
    '''
    This is where all the console processing magic happens.
    Its super simple, rudimentary even but, it's dead simple & it works.
    '''
    arguments = parse_arguments()

    spotifyClient.initialize(clientId='03eb56e5ab2843e98507b3a6a0359a56',
                             clientSecret='4e6600fae80845ef8dab67ccaaecee4d')

    if arguments.path:
        if not os.path.isdir(arguments.path):
            sys.exit("The output directory doesn't exist.")
        print(f"Will download to: {os.path.abspath(arguments.path)}")
        os.chdir(arguments.path)

    downloader = DownloadManager()

    for request in arguments.url:
        if 'open.spotify.com' in request and 'track' in request:
            print('Fetching Song...')
            song = SongObj.from_url(request)

            if song.get_youtube_link() is not None:
                downloader.download_single_song(song)
            else:
                print(
                    'Skipping %s (%s) as no match could be found on youtube' %
                    (song.get_song_name(), request))

        elif 'open.spotify.com' in request and 'album' in request:
            print('Fetching Album...')
            songObjList = get_album_tracks(request)

            downloader.download_multiple_songs(songObjList)

        elif 'open.spotify.com' in request and 'playlist' in request:
            print('Fetching Playlist...')
            songObjList = get_playlist_tracks(request)

            downloader.download_multiple_songs(songObjList)

        elif 'open.spotify.com' in request and 'artist' in request:
            print('Fetching artist...')
            artistObjList = get_artist_tracks(request)

            downloader.download_multiple_songs(artistObjList)

        elif request.endswith('.spotdlTrackingFile'):
            print('Preparing to resume download...')
            downloader.resume_download_from_tracking_file(request)

        else:
            print('Searching for song "%s"...' % request)
            try:
                song = search_for_song(request)
                downloader.download_single_song(song)

            except Exception:
                print('No song named "%s" could be found on spotify' % request)

    downloader.close()
Example #22
0
def console_entry_point():
    '''
    This is where all the console processing magic happens.
    Its super simple, rudimentary even but, it's dead simple & it works.
    '''

    if '--help' in cliArgs or '-h' in cliArgs:
        print(help_notice)

        #! We use 'return None' as a convenient exit/break from the function
        return None

    if '--quiet' in cliArgs:
        #! removing --quiet so it doesnt mess up with the download
        cliArgs.remove('--quiet')
        #! make stdout & stderr silent
        sys.stdout = quiet()
        sys.stderr = quiet()

    initialize(clientId='4fe3fecfe5334023a1472516cc99d805',
               clientSecret='0f02b7c483c04257984695007a4a8d5c')

    downloader = DownloadManager()

    for request in cliArgs[1:]:
        if ('open.spotify.com' in request
                and 'track' in request) or 'spotify:track:' in request:
            print('Fetching Song...')
            song = SongObj.from_url(request)

            if song.get_youtube_link() != None:
                downloader.download_single_song(song)
            else:
                print(
                    'Skipping %s (%s) as no match could be found on youtube' %
                    (song.get_song_name(), request))

        elif ('open.spotify.com' in request
              and 'album' in request) or 'spotify:album:' in request:
            print('Fetching Album...')
            songObjList = get_album_tracks(request)

            downloader.download_multiple_songs(songObjList)

        elif ('open.spotify.com' in request
              and 'playlist' in request) or 'spotify:playlist:' in request:
            print('Fetching Playlist...')
            songObjList = get_playlist_tracks(request)

            downloader.download_multiple_songs(songObjList)

        elif request.endswith('.txt'):
            print('Fetching songs from %s...' % request)
            songObjList = []

            with open(request, 'r') as songFile:
                for songLink in songFile.readlines():
                    song = SongObj.from_url(songLink)
                    songObjList.append(song)

            downloader.download_multiple_songs(songObjList)

        elif request.endswith('.spotdlTrackingFile'):
            print('Preparing to resume download...')
            downloader.resume_download_from_tracking_file(request)

        else:
            print('Searching for song "%s"...' % request)
            try:
                song = search_for_song(request)
                downloader.download_single_song(song)

            except Exception:
                print('No song named "%s" could be found on spotify' % request)

    downloader.close()
Example #23
0
from spotdl.search.songObj import SongObj
from spotdl.search.spotifyClient import initialize
from spotdl.lyrics.genius import Genius

initialize(
    clientId="4fe3fecfe5334023a1472516cc99d805",
    clientSecret="0f02b7c483c04257984695007a4a8d5c",
)

# g = Genius()

# print(g.from_query("A$ap Rocky", "j",lyric_fail=True))

s = SongObj.from_url(
    'spotify:track:02kDW379Yfd5PzW5A6vuGt?context=spotify%3Aplaylist%3A37i9dQZF1DXcBWIGoYBM5M'
)
print(s.get_lyrics())

# print(Genius.from_query("Pop Smoke", "For The Night"))
Example #24
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.
        '''

        dispayProgressTracker = self.displayManager.new_progress_tracker(
            songObj)

        # ! since most errors are expected to happen within this function, we wrap in
        # ! exception catcher to prevent blocking on multiple downloads
        try:

            # ! 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 + ', '

            # make sure that main artist is included in artistStr even if they
            # are in the song name, for example
            # Lil Baby - Never Recover (Lil Baby & Gunna, Drake).mp3
            if songObj.get_contributing_artists()[0].lower(
            ) not in artistStr.lower():
                artistStr = songObj.get_contributing_artists(
                )[0] + ', ' + artistStr

            # ! 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:
                    dispayProgressTracker.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 dispayProgressTracker:
                youtubeHandler = YouTube(
                    url=songObj.get_youtube_link(),
                    on_progress_callback=dispayProgressTracker.
                    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

            if dispayProgressTracker:
                dispayProgressTracker.notify_youtube_download_completion()

            downloadedFilePath = Path(downloadedFilePathString)

            ffmpeg_success = await ffmpeg.convert(
                trackAudioStream=trackAudioStream,
                downloadedFilePath=downloadedFilePath,
                convertedFilePath=convertedFilePath,
                ffmpegPath=self.ffmpeg_path)

            if dispayProgressTracker:
                dispayProgressTracker.notify_conversion_completion()

            if ffmpeg_success is False:
                # delete the file that wasn't successfully converted
                convertedFilePath.unlink()
            else:
                # if a file was successfully downloaded, tag it
                self.set_id3_data(convertedFilePath, songObj)

            # Do the necessary cleanup
            if dispayProgressTracker:
                dispayProgressTracker.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()

        except Exception as e:
            tb = traceback.format_exc()
            if dispayProgressTracker:
                dispayProgressTracker.notify_error(e, tb)
            else:
                raise e
Example #25
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)
Example #26
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 + ', '

        # make sure that main artist is included in artistStr even if they
        # are in the song name, for example
        # Lil Baby - Never Recover (Lil Baby & Gunna, Drake).mp3
        if songObj.get_contributing_artists()[0].lower() not in artistStr.lower():
            artistStr = songObj.get_contributing_artists()[0] + ', ' + artistStr

        # ! 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(':', '-')

        # Shorten the filename if it's too long
        # 250 + .mp3 = 254 (max length == 254)
        convertedFileName = convertedFileName[:250]

        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('$', r'\$'),
                str(convertedFilePath).replace('$', r'\$')
            )

        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()

        self.set_id3_data(convertedFilePath, songObj)

        # 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)