Peter-Schorn / SpotifyAPI

A Swift library for the Spotify web API. Supports all endpoints.
https://peter-schorn.github.io/SpotifyAPI/documentation/spotifywebapi
MIT License
265 stars 32 forks source link

How to append [SpotifyWebAPI.Playlist<PlaylistItemsReference>] #32

Closed kimfucious closed 2 years ago

kimfucious commented 2 years ago

HI @Peter-Schorn,

I've been putting your wonderful library to great use, thanks for making this!

Question:

I'm fetching a user's playlists and successfully storing them in "state" as a published variable:

@Published var currentUsersSpotifyPlaylists = [SpotifyWebAPI.Playlist<PlaylistItemsReference>]()

I can populate this initially with:

@MainActor
func getSpotifyUsersPlaylists() async {
    do {
        Logger.log(.info, "Fetching user's Spotify playlists")
        if spotify.isAuthorized {
            Logger.log(.success, "Spotify is Authorized")
            Logger.log(.fetch, "Fetching user's Spotify playlists...")
            let playlistsPage = try await spotify.api.currentUserPlaylists(limit: 50)
                .extendPages(spotify.api)
                .awaitSingleValue()
            networkManager.currentUsersSpotifyPlaylists = playlistsPage?.items ?? []
        } else {
            Logger.log(.warning, "Spotify is not Authorized")
            networkManager.viewToShow = .tracklistTabView
        }
    } catch {
        print(error)
    }
}

In some cases the user will create a playlist using this:

func createSpotifyPlaylist(playlistDetails: PlaylistDetails, spotifyUserProfile: SpotifyUser) async throws -> PlaylistItems? {
        var playlistToReturn: SpotifyWebAPI.Playlist<PlaylistItems>?
        do {
            Logger.log(.info, "Creating Spotify playlist: \(String(describing: playlistDetails.name))...")
            let result = try await spotify.api.createPlaylist(for: spotifyUserProfile.uri, playlistDetails)
                .awaitSingleValue()
            if result != nil {
                Logger.log(.success, "Playlist created!")
                print(result as Any)
                playlistToReturn = result!
            }
        } catch {
            print("Error Saving Tracks to Playlist", error)
            throw error
        }
        guard playlistToReturn != nil else {
            Logger.log(.warning, "No playlist to return!")
            return nil
        }
        await self.addImageToSpotifyPlaylist(playlistId: playlistToReturn!.uri)
        return playlistToReturn!.items
    }

If the above returns the items, I'd like update "state" by appending currentSpotifyUsersPlaylists with said items; however, after quite a while of futzing around, I cannot figure out how to do that, as .append() and + operator are obviously the wrong tools for the job here when dealing with the type at hand.

Any tips?

DispatchQueue.main.async {
  self.currentSpotifyUsersPlaylists = ??? // <= what goes here?
}

Thanks!

Peter-Schorn commented 2 years ago
print("Error Saving Tracks to Playlist", error)

The above code is not saving any tracks to a playlist.

If the above returns the items, I'd like update "state" by appending currentSpotifyUsersPlaylists with said items

The playlist that you create using SpotifyAPI.createPlaylist(for:_:) will not contain any items (tracks/episodes). The documentation mentions this. Why would you expect a just-created playlist to contain items?

The return type of the aforementioned method is AnyPublisher<Playlist<PlaylistItems>, Error>, meaning that the type of playlistToReturn is Playlist<PlaylistItems>. But the type of currentUsersSpotifyPlaylists is [Playlist<PlaylistItemsReference>]. You can only append values of type Playlist<PlaylistItemsReference>—not Playlist<PlaylistItems>— to this array. Swift is a statically typed language.

After you create a new playlist and, optionally, add items to it (which the posted code doesn't do yet), you should make another call to getSpotifyUsersPlaylists, which will then update currentUsersSpotifyPlaylists with the newly-created playlist. Spotify's database may not update immediately after you create the playlist, so you can call this function repeatedly —with an exponentially increasing delay—until currentUsersSpotifyPlaylists contains a playlist with the URI of the newly-created playlist.

let playlistsPage = try await spotify.api.currentUserPlaylists(limit: 50)
.extendPages(spotify.api)
.awaitSingleValue()

This doesn't do what you think it does. Publisher.extendPages(_:maxExtraPages:) returns a publisher that publishes multiple values: one for each page of results. Publisher.awaitSingleValue only waits for a single value from the publisher. Therefore, you're only retrieving a single page of playlists.

What you need is a way of asynchronously collecting all values produced by the publisher into an array:

let playlists: [Playlist<PlaylistItemsReference>] = try await spotify.api
    .currentUserPlaylists(limit: 50)
    .extendPages(spotify.api)
    .values
    .reduce(into: [], { result, page in
        result.append(contentsOf: page.items)
    })

networkManager.currentUsersSpotifyPlaylists = playlists

Publisher.values has the same behavior as Publisher.awaitValues in #28, but I trust Apple's implementation more than my own. The reduce method collects the items array from each PagingObject into a single array.

Peter-Schorn commented 2 years ago
if result != nil {
// ...
playlistToReturn = result!
}

Stop using this pattern. Don't use a boolean test for whether an optional is nil and then force-unwrap it and assign it to a non-optional variable. Instead use the if-let pattern:

if let result = result {
// ...
playlistToReturn = result
}
kimfucious commented 2 years ago

Hi @Peter-Schorn, thanks for your insightful feedback.

First, you're spot on that I was misunderstanding the return of the createSpotifyPlaylist function. I was thinking that items would be multiple playlists (actually one playlist in an array), not a single playlist, and not tracks, which, as you said, wouldn't be there anyhow. So, thanks for setting me straight there.

So my original question was how to append [SpotifyWebAPI.Playlist] is flawed as I was indeed trying to stick a square peg into a round hole.

And thus, your solution to re-fetch the user's playlists after the creation of a new one, is ironically what I put in as a work-around while waiting for your reply. I was hoping to avoid that extra call, but it seems the path of least resistance for now.

The new playlist seems to be there pretty quick. I've not seen it not be there, even after an immediate get of the playlists after a new one is created. In my use case, the new playlist is not immediately needed until another view appears later on, at which point another call to get the playlists is done, so I believe it's kinda safe.

Refetching also obviates the need to return the playlist after creation, as well, which simplifies the create function.

Regarding awaitSingleValue(), I've implemented that heavily, so I guess, I'll begin replacing that with values/reduce(into... pattern where it makes sense to do so, though awaitSingleValue() has been working as expected thus far.

Lastly, I realize that I'm bothering and that my code is mostly Scheiße. I've only really been working with Swift for a couple/few months now, so my apologies for any anguish that I cause.

I do appreciate your taking the time to respond to my queries and the valueable feedback you provide.

Peter-Schorn commented 2 years ago

Regarding awaitSingleValue(), I've implemented that heavily, so I guess, I'll begin replacing that with values/reduce pattern where it makes sense to do so, though awaitSingleValue() has been working as expected thus far.

If only one page of results exists, then the behavior will be the same. But you should never assume only one page of results exists.