def from_artist(
    artist_url: str,
    output_format: str = None,
    use_youtube: bool = False,
    lyrics_provider: str = None,
    threads: int = 1,
) -> List[SongObject]:
    """
    `str` `artist_url` : 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
    """

    spotify_client = SpotifyClient()

    artist_tracks = []

    artist_response = spotify_client.artist_albums(
        artist_url, album_type="album,single"
    )
    if artist_response is None:
        raise ValueError("Wrong artist id")

    albums_list = artist_response["items"]
    albums_object: Dict[str, str] = {}

    # Fetch all artist albums
    while artist_response and artist_response["next"]:
        response = spotify_client.next(artist_response)
        if response is None:
            break

        artist_response = response
        albums_list.extend(artist_response["items"])

    # Remove duplicate albums
    for album in albums_list:
        # return an iterable containing the string's alphanumeric characters
        alpha_numeric_filter = filter(str.isalnum, album["name"].lower())

        # join all characters into one string
        album_name = "".join(alpha_numeric_filter)

        if albums_object.get(album_name) is None:
            albums_object[album_name] = album["uri"]

    tracks_list = []
    tracks_object: Dict[str, str] = {}

    # Fetch all tracks from all albums
    for album_uri in albums_object.values():
        response = spotify_client.album_tracks(album_uri)
        if response is None:
            continue

        album_response = response
        album_tracks = album_response["items"]

        while album_response["next"]:
            album_response = spotify_client.next(album_response)
            if album_response is None:
                break

            album_tracks.extend(album_response["items"])

        tracks_list.extend(album_tracks)

    # Filter tracks to remove duplicates and songs without our artist
    for track in tracks_list:
        # ignore None tracks or tracks without uri
        if track is not None and track.get("uri") is None:
            continue

        # return an iterable containing the string's alphanumeric characters
        alphaNumericFilter = filter(str.isalnum, track["name"].lower())

        # join all characters into one string
        trackName = "".join(alphaNumericFilter)

        if tracks_object.get(trackName) is None:
            for artist in track["artists"]:
                # get artist id from url
                # https://api.spotify.com/v1/artists/1fZAAHNWdSM5gqbi9o5iEA/albums
                #
                # split string
                #  ['https:', '', 'api.spotify.com', 'v1', 'artists',
                #  '1fZAAHNWdSM5gqbi9o5iEA', 'albums']
                #
                # get second element from the end
                # '1fZAAHNWdSM5gqbi9o5iEA'
                artistId = artist_response["href"].split("/")[-2]

                # ignore tracks that are not from our artist by checking
                # the id
                if artist["id"] == artistId:
                    tracks_object[trackName] = track["uri"]

    # Create song objects from track ids
    def get_song(track_uri):
        try:
            return from_spotify_url(
                f"https://open.spotify.com/track/{track_uri.split(':')[-1]}",
                output_format,
                use_youtube,
                lyrics_provider,
                None,
            )
        except (LookupError, ValueError, OSError):
            return None

    with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
        results = executor.map(get_song, tracks_object.values())

    for result in results:
        if result is not None and result.youtube_link is not None:
            artist_tracks.append(result)

    return artist_tracks
def from_album(
    album_url: str,
    output_format: str = None,
    use_youtube: bool = False,
    lyrics_provider: str = None,
    generate_m3u: bool = False,
    threads: int = 1,
    path_template: str = None,
) -> List[SongObject]:
    """
    Create and return list containing SongObject for every song in the album

    `str` `album_url` : Spotify Url of the album whose tracks are to be retrieved
    `str` `output_format` : output format of the song

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

    spotify_client = SpotifyClient()
    tracks = []

    album_response = spotify_client.album_tracks(album_url)
    if album_response is None:
        raise ValueError("Wrong album id")

    album_tracks = album_response["items"]

    # Get all tracks from album
    while album_response["next"]:
        album_response = spotify_client.next(album_response)

        # Failed to get response, break the loop
        if album_response is None:
            break

        album_tracks.extend(album_response["items"])

    # Remove songs  without id
    album_tracks = [
        track
        for track in album_tracks
        if track is not None and track.get("id") is not None
    ]

    def get_tracks(track):
        try:
            song = from_spotify_url(
                "https://open.spotify.com/track/" + track["id"],
                output_format,
                use_youtube,
                lyrics_provider,
                None,
            )

            if generate_m3u:
                if path_template:
                    file_path = _parse_path_template(path_template, song, output_format)
                else:
                    file_path = _get_converted_file_path(song, output_format)

                return song, f"{file_path}\n"

            return song, None
        except (LookupError, ValueError):
            return None, None
        except OSError:
            if generate_m3u:
                song_obj = SongObject(track, album_response, {}, None, "", None)
                if path_template:
                    file_path = _parse_path_template(
                        path_template, song_obj, output_format
                    )
                else:
                    file_path = _get_converted_file_path(song_obj, output_format)

                return None, f"{file_path}\n"

            return None, None

    with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
        results = executor.map(get_tracks, album_tracks)

    album_text = ""
    for result in results:
        if result[1] is not None:
            album_text += result[1]

        if result[0] is not None and result[0].youtube_link is not None:
            tracks.append(result[0])

    if album_response and generate_m3u is True:
        album_data = spotify_client.album(album_url)

        if album_data is not None:
            album_name = album_data["name"]
        else:
            album_name = album_tracks[0]["name"]

        album_name = format_name(album_name)

        album_file = Path(f"{album_name}.m3u")

        with open(album_file, "w", encoding="utf-8") as file:
            file.write(album_text)

    return tracks