diff --git a/lib/include/lib/spotify/api.hpp b/lib/include/lib/spotify/api.hpp index 97c85e85..856a4b27 100644 --- a/lib/include/lib/spotify/api.hpp +++ b/lib/include/lib/spotify/api.hpp @@ -21,7 +21,7 @@ #include "lib/spotify/callback.hpp" #include "lib/spotify/request.hpp" #include "lib/spotify/queue.hpp" -#include "lib/spotify/pagination.hpp" +#include "lib/spotify/page.hpp" #include "lib/httpclient.hpp" #include "lib/datetime.hpp" @@ -278,7 +278,11 @@ namespace lib void playlist_tracks(const lib::spt::playlist &playlist, lib::callback> &callback); - auto playlist_tracks(const lib::spt::playlist &playlist) -> lib::spt::pagination *; + /** + * @note Experimental + */ + void playlist_tracks(const lib::spt::playlist &playlist, + const std::function> &)> &callback); void add_to_playlist(const std::string &playlist_id, const std::vector &track_uris, diff --git a/lib/include/lib/spotify/page.hpp b/lib/include/lib/spotify/page.hpp new file mode 100644 index 00000000..5d25e27f --- /dev/null +++ b/lib/include/lib/spotify/page.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "thirdparty/json.hpp" + +namespace lib +{ + namespace spt + { + /** + * Page of items + * @tparam T Item type + */ + template + class page + { + public: + page() = default; + + /** + * Next page exists + */ + auto has_next() const -> bool + { + return !next.empty(); + }; + + /** + * Items on this page + */ + std::vector items; + + /** + * Maximum number of items per page + */ + int limit = 0; + + /** + * Number of offset items + */ + int offset = 0; + + /** + * Total number of items + */ + int total = 0; + + /** + * URL for next page + */ + std::string next; + }; + + template + void from_json(const nlohmann::json &json, lib::spt::page &page) + { + json.at("items").get_to(page.items); + json.at("limit").get_to(page.limit); + json.at("offset").get_to(page.offset); + json.at("total").get_to(page.total); + + const auto &next = json.at("next"); + if (next.is_string()) + { + next.get_to(page.next); + } + } + } +} diff --git a/lib/include/lib/spotify/request.hpp b/lib/include/lib/spotify/request.hpp index a36c2470..a227aed5 100644 --- a/lib/include/lib/spotify/request.hpp +++ b/lib/include/lib/spotify/request.hpp @@ -5,6 +5,7 @@ #include "lib/spotify/error.hpp" #include "lib/spotify/util.hpp" #include "lib/spotify/deviceselect.hpp" +#include "lib/spotify/page.hpp" namespace lib { @@ -47,6 +48,62 @@ namespace lib }); } + /** + * Get a paged list of items contained in specified key + * @tparam T Type of item to fetch + * @param url Initial URL to fetch from + * @param key Key to fetch items from + * @param callback Callback with page index, returning true to fetch the next page + */ + template + void get_page(const std::string &url, const std::string &key, + const std::function> &)> &callback) + { + const auto api_url = lib::strings::starts_with(url, "https://") + ? lib::spt::to_relative_url(url) + : url; + + lib::log::debug("GET: {}", api_url); + + get(api_url, [this, key, callback](const lib::result &result) + { + if (!result.success()) + { + const auto message = parse_error_message(result.message()); + callback(lib::result>::fail(message)); + return; + } + + const auto &json = result.value(); + if (!key.empty() && !json.contains(key)) + { + const auto message = lib::fmt::format("No such key: {}", key); + callback(lib::result>::fail(message)); + return; + } + + lib::spt::page page; + try + { + page = key.empty() ? json : json.at(key); + } + catch (const std::exception &exception) + { + const std::string message = exception.what(); + callback(lib::result>::fail(message)); + return; + } + + if (!callback(lib::result>::ok(page)) + || !page.has_next()) + { + return; + } + + get_page(page.next, key, callback); + }); + } + /** * POST request without body */ diff --git a/lib/src/spotifyapi/playlists.cpp b/lib/src/spotifyapi/playlists.cpp index 714a35b6..f0a9ec65 100644 --- a/lib/src/spotifyapi/playlists.cpp +++ b/lib/src/spotifyapi/playlists.cpp @@ -49,11 +49,11 @@ void lib::spt::api::playlist_tracks(const lib::spt::playlist &playlist, get_items(url, callback); } -auto lib::spt::api::playlist_tracks(const lib::spt::playlist &playlist) -> lib::spt::pagination * +void lib::spt::api::playlist_tracks(const lib::spt::playlist &playlist, + const std::function> &)> &callback) { - // TODO: pagination is just a temporary object, and thus need heap allocation, find better solution const auto url = lib::fmt::format("playlists/{}/tracks?market=from_token&limit=50", playlist.id); - return new lib::spt::pagination(url, request); + request.get_page(url, std::string(), callback); } void lib::spt::api::add_to_playlist(const std::string &playlist_id, diff --git a/src/list/tracks.cpp b/src/list/tracks.cpp index ab1d0e6d..365d0023 100644 --- a/src/list/tracks.cpp +++ b/src/list/tracks.cpp @@ -493,11 +493,15 @@ auto List::Tracks::getSelectedTrackIds() const -> std::vector } void List::Tracks::load(const std::vector &tracks, - const std::string &selectedId, const std::string &addedAt) + const std::string &selectedId, const std::string &addedAt, bool clearItems) { - clear(); - trackItems.clear(); - playingTrackItem = nullptr; + if (clearItems) + { + clear(); + trackItems.clear(); + playingTrackItem = nullptr; + } + auto fieldWidth = static_cast(std::to_string(tracks.size()).size()); auto current = getCurrent(); auto anyHasDate = false; @@ -570,14 +574,15 @@ void List::Tracks::load(const std::vector &tracks, }); } -void List::Tracks::load(const std::vector &tracks) +void List::Tracks::load(const std::vector &tracks, bool clearItems) { - load(tracks, std::string()); + load(tracks, std::string(), clearItems); } -void List::Tracks::load(const std::vector &tracks, const std::string &selectedId) +void List::Tracks::load(const std::vector &tracks, + const std::string &selectedId, bool clearItems) { - load(tracks, selectedId, std::string()); + load(tracks, selectedId, std::string(), clearItems); } void List::Tracks::load(const lib::spt::playlist &playlist) @@ -640,27 +645,26 @@ void List::Tracks::refreshPlaylist(const lib::spt::playlist &playlist) if (lib::developer_mode::is_experiment_enabled(lib::experiment::new_paging)) { - auto *pagination = spotify.playlist_tracks(playlist); - pagination->success([this, mainWindow, playlistUri, &pagination](const std::vector &tracks) - { - if (playlistUri != mainWindow->history()->currentUri()) + spotify.playlist_tracks(playlist, + [this, mainWindow, playlistUri](const lib::result> &result) -> bool { - delete pagination; - return; - } - - load(tracks); // TODO: Clears - setEnabled(true); - delete pagination; - }); - pagination->error([&pagination](const std::string &message) - { - StatusMessage::error(QString("Failed to fetch page: %1") - .arg(QString::fromStdString(message))); - - delete pagination; - }); - pagination->next(); + if (playlistUri != mainWindow->history()->currentUri()) + { + return false; + } + + if (!result.success()) + { + StatusMessage::error(QString("Failed to load playlist: %1") + .arg(QString::fromStdString(result.message()))); + return false; + } + + const auto &page = result.value(); + load(page.items, page.offset == 0); + setEnabled(true); + return page.has_next(); + }); return; } diff --git a/src/list/tracks.hpp b/src/list/tracks.hpp index 113b0478..bff661f3 100644 --- a/src/list/tracks.hpp +++ b/src/list/tracks.hpp @@ -27,19 +27,20 @@ namespace List /** * Load tracks directly, without cache, but select an item */ - void load(const std::vector &tracks, const std::string &selectedId); + void load(const std::vector &tracks, const std::string &selectedId, + bool clearItems = true); /** * Load tracks directly, without cache, * but select an item and provide a fallback added date */ void load(const std::vector &tracks, const std::string &selectedId, - const std::string &addedAt); + const std::string &addedAt, bool clearItems = true); /** * Load tracks directly, without cache */ - void load(const std::vector &tracks); + void load(const std::vector &tracks, bool clearItems = true); /** * Load playlist first from cache, then refresh it