def from_url(spotify_url: str):
    if not ("open.spotify.com" in spotify_url and "track" in spotify_url):
        raise Exception(f"passed URL is not that of a track: {spotify_url}")

    # query spotify for song, artist, album details
    spotify_client = SpotifyClient()

    raw_track_meta = spotify_client.track(spotify_url)

    if raw_track_meta is None:
        raise Exception(
            "Couldn't get metadata, check if you have passed correct track id")

    primary_artist_id = raw_track_meta["artists"][0]["id"]
    raw_artist_meta = spotify_client.artist(primary_artist_id)

    albumId = raw_track_meta["album"]["id"]
    raw_album_meta = spotify_client.album(albumId)

    return raw_track_meta, raw_artist_meta, raw_album_meta
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