diff --git a/qpm.json b/qpm.json index a12e4ef..61ba0c8 100644 --- a/qpm.json +++ b/qpm.json @@ -36,7 +36,10 @@ "tomb": [ "pwsh ./scripts/pull-tombstone.ps1 -analyze" ] - } + }, + "qmodIncludeDirs": [], + "qmodIncludeFiles": [], + "qmodOutput": null }, "dependencies": [ { @@ -71,8 +74,8 @@ "id": "scotland2", "versionRange": "^0.1.4", "additionalData": { - "private": true, - "includeQmod": false + "includeQmod": false, + "private": true } }, { @@ -94,6 +97,16 @@ "id": "paper", "versionRange": "^3.6.1", "additionalData": {} + }, + { + "id": "web-utils", + "versionRange": "^0.6.5", + "additionalData": {} + }, + { + "id": "beatsaverplusplus", + "versionRange": "^0.1.1", + "additionalData": {} } ] } diff --git a/qpm.shared.json b/qpm.shared.json index 5495cc6..fdbd237 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -98,29 +98,34 @@ "id": "paper", "versionRange": "^3.6.1", "additionalData": {} + }, + { + "id": "web-utils", + "versionRange": "^0.6.5", + "additionalData": {} + }, + { + "id": "beatsaverplusplus", + "versionRange": "^0.1.1", + "additionalData": {} } ] }, "restoredDependencies": [ { "dependency": { - "id": "paper", - "versionRange": "=3.6.3", + "id": "web-utils", + "versionRange": "=0.6.5", "additionalData": { - "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v3.6.3/libpaperlog.so", - "debugSoLink": "https://github.com/Fernthedev/paperlog/releases/download/v3.6.3/debug_libpaperlog.so", - "overrideSoName": "libpaperlog.so", - "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v3.6.3/paperlog.qmod", - "branchName": "version/v3_6_3", - "compileOptions": { - "systemIncludes": [ - "shared/utfcpp/source" - ] - }, + "soLink": "https://github.com/RedBrumbler/WebUtils/releases/download/v0.6.5/libweb-utils.so", + "debugSoLink": "https://github.com/RedBrumbler/WebUtils/releases/download/v0.6.5/debug_libweb-utils.so", + "overrideSoName": "libweb-utils.so", + "modLink": "https://github.com/RedBrumbler/WebUtils/releases/download/v0.6.5/WebUtils.qmod", + "branchName": "version/v0_6_5", "cmake": false } }, - "version": "3.6.3" + "version": "0.6.5" }, { "dependency": { @@ -137,17 +142,78 @@ { "dependency": { "id": "bsml", - "versionRange": "=0.4.34", + "versionRange": "=0.4.40", "additionalData": { - "soLink": "https://github.com/RedBrumbler/Quest-BSML/releases/download/v0.4.34/libbsml.so", - "debugSoLink": "https://github.com/RedBrumbler/Quest-BSML/releases/download/v0.4.34/debug_libbsml.so", + "soLink": "https://github.com/RedBrumbler/Quest-BSML/releases/download/v0.4.40/libbsml.so", + "debugSoLink": "https://github.com/RedBrumbler/Quest-BSML/releases/download/v0.4.40/debug_libbsml.so", "overrideSoName": "libbsml.so", - "modLink": "https://github.com/RedBrumbler/Quest-BSML/releases/download/v0.4.34/BSML.qmod", - "branchName": "version/v0_4_34", + "modLink": "https://github.com/RedBrumbler/Quest-BSML/releases/download/v0.4.40/BSML.qmod", + "branchName": "version/v0_4_40", + "cmake": true + } + }, + "version": "0.4.40" + }, + { + "dependency": { + "id": "libil2cpp", + "versionRange": "=0.3.2", + "additionalData": { + "headersOnly": true, + "cmake": false + } + }, + "version": "0.3.2" + }, + { + "dependency": { + "id": "songcore", + "versionRange": "=1.1.13", + "additionalData": { + "soLink": "https://github.com/raineio/Quest-SongCore/releases/download/v1.1.13/libsongcore.so", + "debugSoLink": "https://github.com/raineio/Quest-SongCore/releases/download/v1.1.13/debug_libsongcore.so", + "overrideSoName": "libsongcore.so", + "modLink": "https://github.com/raineio/Quest-SongCore/releases/download/v1.1.13/SongCore.qmod", + "branchName": "version/v1_1_13", "cmake": true } }, - "version": "0.4.34" + "version": "1.1.13" + }, + { + "dependency": { + "id": "tinyxml2", + "versionRange": "=10.0.0", + "additionalData": { + "soLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/libtinyxml2.so", + "debugSoLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/debug_libtinyxml2.so", + "overrideSoName": "libtinyxml2.so", + "modLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/tinyxml2.qmod", + "branchName": "version/v10_0_0", + "cmake": true + } + }, + "version": "10.0.0" + }, + { + "dependency": { + "id": "paper", + "versionRange": "=3.6.3", + "additionalData": { + "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v3.6.3/libpaperlog.so", + "debugSoLink": "https://github.com/Fernthedev/paperlog/releases/download/v3.6.3/debug_libpaperlog.so", + "overrideSoName": "libpaperlog.so", + "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v3.6.3/paperlog.qmod", + "branchName": "version/v3_6_3", + "compileOptions": { + "systemIncludes": [ + "shared/utfcpp/source" + ] + }, + "cmake": false + } + }, + "version": "3.6.3" }, { "dependency": { @@ -183,17 +249,6 @@ }, "version": "0.17.8" }, - { - "dependency": { - "id": "libil2cpp", - "versionRange": "=0.3.2", - "additionalData": { - "headersOnly": true, - "cmake": false - } - }, - "version": "0.3.2" - }, { "dependency": { "id": "bs-cordl", @@ -217,20 +272,6 @@ }, "version": "3700.0.0" }, - { - "dependency": { - "id": "songcore", - "versionRange": "=1.1.12", - "additionalData": { - "soLink": "https://github.com/raineio/Quest-SongCore/releases/download/v1.1.12/libsongcore.so", - "debugSoLink": "https://github.com/raineio/Quest-SongCore/releases/download/v1.1.12/debug_libsongcore.so", - "overrideSoName": "libsongcore.so", - "modLink": "https://github.com/raineio/Quest-SongCore/releases/download/v1.1.12/SongCore.qmod", - "branchName": "version/v1_1_12" - } - }, - "version": "1.1.12" - }, { "dependency": { "id": "beatsaber-hook", @@ -257,6 +298,21 @@ }, "version": "0.1.4" }, + { + "dependency": { + "id": "beatsaverplusplus", + "versionRange": "=0.1.1", + "additionalData": { + "soLink": "https://github.com/RedBrumbler/BeatSaverPlusPlus/releases/download/v0.1.1/libbeatsaverplusplus.so", + "debugSoLink": "https://github.com/RedBrumbler/BeatSaverPlusPlus/releases/download/v0.1.1/debug_libbeatsaverplusplus.so", + "overrideSoName": "libbeatsaverplusplus.so", + "modLink": "https://github.com/RedBrumbler/BeatSaverPlusPlus/releases/download/v0.1.1/BeatSaverPlusPlus.qmod", + "branchName": "version/v0_1_1", + "cmake": false + } + }, + "version": "0.1.1" + }, { "dependency": { "id": "fmt", @@ -275,21 +331,6 @@ } }, "version": "10.0.0" - }, - { - "dependency": { - "id": "tinyxml2", - "versionRange": "=10.0.0", - "additionalData": { - "soLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/libtinyxml2.so", - "debugSoLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/debug_libtinyxml2.so", - "overrideSoName": "libtinyxml2.so", - "modLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/tinyxml2.qmod", - "branchName": "version/v10_0_0", - "cmake": true - } - }, - "version": "10.0.0" } ] } \ No newline at end of file diff --git a/shared/PlaylistCore.hpp b/shared/PlaylistCore.hpp index 0d3eaca..197b907 100644 --- a/shared/PlaylistCore.hpp +++ b/shared/PlaylistCore.hpp @@ -134,6 +134,12 @@ namespace PlaylistCore { /// @return The number of songs missing from the playlist int PlaylistHasMissingSongs(Playlist* playlist); + /// @brief Attempts to download all missing songs from a playlist + /// @param playlist The playlist to download missing songs for + /// @param onFinished A main thread callback whenever all songs have downloaded + /// @param onProgress A main thread callback called immediately and whenever a song has downloaded - params (total, numFinished) + void DownloadMissingSongsFromPlaylist(Playlist* playlist, std::function onFinished, std::function onProgress); + /// @brief Removes songs that are supposed to be in a playlist but not owned from the playlist - only updates playlist JSON /// @param playlist The playlist to remove missing songs from void RemoveMissingSongsFromPlaylist(Playlist* playlist); diff --git a/src/PlaylistCore.cpp b/src/PlaylistCore.cpp index dafd377..d31a5bc 100644 --- a/src/PlaylistCore.cpp +++ b/src/PlaylistCore.cpp @@ -22,6 +22,7 @@ #include "Utils.hpp" #include "assets.hpp" #include "beatsaber-hook/shared/utils/il2cpp-utils.hpp" +#include "beatsaverplusplus/shared/BeatSaver.hpp" #include "bsml/shared/BSML/MainThreadScheduler.hpp" #include "songcore/shared/SongCore.hpp" @@ -362,9 +363,14 @@ namespace PlaylistCore { foundSongs.emplace_back(search); else if (itr->Hash) { LOG_INFO("level id {} not found, attempting to use hash", itr->LevelID); - if (auto search = Utils::GetLevelByID(*itr->Hash)) + if (auto search = Utils::GetLevelByID(*itr->Hash)) { + // fix levelid and hash + itr->LevelID = *itr->Hash; + itr->Hash = Utils::GetLevelHash(itr->LevelID); + if (itr->Hash->empty()) + itr->Hash = std::nullopt; foundSongs.emplace_back(search); - else + } else LOG_ERROR("level id {} not found", *itr->Hash); } else LOG_ERROR("level id {} not found", itr->LevelID); @@ -616,21 +622,102 @@ namespace PlaylistCore { break; } } - if (hasSong) - continue; - songsMissing += 1; + if (!hasSong) + songsMissing += 1; } return songsMissing; } + struct GetBeatmapForDownloadRequest : WebUtils::GenericRequest { + GetBeatmapForDownloadRequest(std::string hash) : GenericRequest(BeatSaver::API::GetBeatmapByHashURLOptions(hash)), hash(hash) {} + std::string hash; + BeatSaver::API::BeatmapDownloadInfo GetDownload() { + auto& map = *targetResponse.responseData; + for (auto& version : map.Versions) { + if (CaseInsensitiveEquals(version.Hash, hash)) + return {map, version}; + } + logger.warn("failed to find version of beatmap with hash {}", hash); + logger.info("attempting alternative download path"); + return {map.Id, fmt::format(BEATSAVER_CDN_URL "/{}.zip", hash), map.CreateFolderName()}; + } + }; + + void DownloadMissingSongsFromPlaylist(Playlist* playlist, std::function onFinished, std::function onProgress) { + // find all the songs needing downloads + std::vector songsToGet; + for (auto& song : playlist->playlistJSON.Songs) { + bool hasSong = false; + // same as PlaylistHasMissingSongs + ArrayW levelList(playlist->playlistCS->beatmapLevels); + for (int i = 0; i < levelList.size(); i++) { + if (CaseInsensitiveEquals(song.LevelID, levelList[i]->levelID)) { + hasSong = true; + break; + } + } + if (!hasSong) + songsToGet.emplace_back(Utils::GetLevelHash(song.LevelID)); + } + + if (songsToGet.empty()) { + onFinished(); + return; + } + onProgress(songsToGet.size(), 0); + + using Retry = WebUtils::RatelimitedDispatcher::RetryOptions; + auto requester = new WebUtils::RatelimitedDispatcher(); + auto completed = new std::atomic_int(0); + + auto increment = [completed, onProgress, total = songsToGet.size()]() { + int num = ++(*completed); + BSML::MainThreadScheduler::Schedule([onProgress = std::move(onProgress), num, total]() { onProgress(total, num); }); + }; + + requester->downloader = BeatSaver::API::GetBeatsaverDownloader(); + requester->maxConcurrentRequests = 3; + requester->onRequestFinished = [requester, increment](bool success, WebUtils::IRequest* request) -> std::optional { + if (!success) { + auto response = request->TargetResponse; + auto http = response->HttpCode; + auto curl = response->CurlStatus; + logger.error("{} request failed {} {}", request->URL.fullURl(), http, curl); + if (curl == 0 && (http >= 200 && http < 300)) + return Retry{std::chrono::milliseconds(100)}; + // not retrying, mark as done + increment(); + return std::nullopt; + } + // add download request if it was a get request + if (auto cast = dynamic_cast(request)) + requester->AddRequest(BeatSaver::API::CreateDownloadBeatmapRequest(cast->GetDownload())); + else if (auto cast = dynamic_cast(request)) + increment(); + + return std::nullopt; + }; + requester->allFinished = [requester, completed, onFinished = std::move(onFinished)](auto) { + BSML::MainThreadScheduler::Schedule([requester, completed, onFinished = std::move(onFinished)]() { + delete requester; + delete completed; + onFinished(); + }); + }; + + // TODO: use api with batches of 50 + for (auto& hash : songsToGet) + requester->AddRequest(std::make_unique(hash)); + + requester->StartDispatchIfNeeded(); + } + void RemoveMissingSongsFromPlaylist(Playlist* playlist) { // store exisiting songs in a new vector to replace the song list with std::vector existingSongs = {}; for (auto& song : playlist->playlistJSON.Songs) { if (Utils::GetLevelByID(song.LevelID)) existingSongs.push_back(song); - else if (song.Hash && Utils::GetLevelByID(*song.Hash)) - existingSongs.push_back(song); else if (song.SongName.has_value()) LOG_INFO("Removing song {} from playlist {}", song.SongName.value(), playlist->name); else