From 337a6f4f3cabf3b23f0040842a0029130cf1a192 Mon Sep 17 00:00:00 2001 From: Kitsune Date: Mon, 19 Jun 2023 22:12:32 +0800 Subject: [PATCH] Add Most play downloader & fix 100 limit --- Downloader/res/language/en_us.json | 7 +- Downloader/res/language/zh_cn.json | 7 +- Downloader/src/features/MultiDownload.cpp | 368 ++++++++++++++++------ Downloader/src/features/MultiDownload.h | 25 ++ 4 files changed, 301 insertions(+), 106 deletions(-) diff --git a/Downloader/res/language/en_us.json b/Downloader/res/language/en_us.json index 33e99d5..79130ab 100644 --- a/Downloader/res/language/en_us.json +++ b/Downloader/res/language/en_us.json @@ -91,5 +91,10 @@ "MapperDownloadFailedParseFailed": "Mapper Download failed: Parse response body failed!", "StartDownloadMapper": "Start download mapper(uid=%d): From %d to %d, Beatmap Status: %s", "BeatmapStatus": "Beatmap Status", - "BeatmapStatusDesc": "Ranked: Ranked Beatmaps\nLoved: Loved Beatmaps\nGuest: Guest Participation Beatmaps\nPending: Pending Beatmaps\nGraveyard: Graveyarded Beatmaps\nNominated: Nominated Ranked Beatmaps" + "BeatmapStatusDesc": "Ranked: Ranked Beatmaps\nLoved: Loved Beatmaps\nGuest: Guest Participation Beatmaps\nPending: Pending Beatmaps\nGraveyard: Graveyarded Beatmaps\nNominated: Nominated Ranked Beatmaps", + "StartDownloadMostPlayed": "Start download most played(uid=%d): From %d to %d", + "MostPlayedDownloader": "Most Played Beatmaps Downloader", + "MostPlayedDownloadFailedUserNotFound": "Most Played Download failed: User(%d) not found!", + "MostPlayedDownloadFailedResponseCodeNotOk": "Most Played Download failed: Response code: %d", + "MostPlayedDownloadFailedParseFailed": "Most Played Download failed: Parse response body failed!" } \ No newline at end of file diff --git a/Downloader/res/language/zh_cn.json b/Downloader/res/language/zh_cn.json index 4a1183d..8895561 100644 --- a/Downloader/res/language/zh_cn.json +++ b/Downloader/res/language/zh_cn.json @@ -91,5 +91,10 @@ "MapperDownloadFailedParseFailed": "谱师谱面下载失败:找不到用户(%d)!", "StartDownloadMapper": "开始下载谱师谱面(uid=%d):从 %d 到 %d,谱面状态:%s", "BeatmapStatus": "谱面状态", - "BeatmapStatusDesc": "Ranked:上架 (Ranked) 谱面\nLoved:社区喜爱 (Loved) 谱面\nGuest:客串制作的谱面\nPending:待定 (Pending) 谱面\nGraveyard:已停更的谱面\nNominated:提名并上架 (Ranked) 的谱面" + "BeatmapStatusDesc": "Ranked:上架 (Ranked) 谱面\nLoved:社区喜爱 (Loved) 谱面\nGuest:客串制作的谱面\nPending:待定 (Pending) 谱面\nGraveyard:已停更的谱面\nNominated:提名并上架 (Ranked) 的谱面", + "StartDownloadMostPlayed": "开始下载最多游玩谱面(uid=%d):从 %d 到 %d", + "MostPlayedDownloader": "最多游玩谱面下载器", + "MostPlayedDownloadFailedUserNotFound": "最多游玩谱面下载失败:找不到用户(%d)!", + "MostPlayedDownloadFailedResponseCodeNotOk": "最多游玩谱面下载失败:响应码:%d", + "MostPlayedDownloadFailedParseFailed": "最多游玩谱面下载失败:解析响应体失败!" } \ No newline at end of file diff --git a/Downloader/src/features/MultiDownload.cpp b/Downloader/src/features/MultiDownload.cpp index 7cd04dd..3de47d0 100644 --- a/Downloader/src/features/MultiDownload.cpp +++ b/Downloader/src/features/MultiDownload.cpp @@ -9,6 +9,12 @@ using namespace features; MultiDownload::MultiDownload() : Feature() { + t_SearchThread = std::thread(SearchThread); + t_SearchThread.detach(); +} + +void MultiDownload::doRequest(const Task &task) { + m_Queue.push_back(task); } ui::main::FeatureInfo &MultiDownload::getInfo() { @@ -63,46 +69,54 @@ void DoBPDownload(int id, int beg, int end) { return; } - int offset = beg; - int limit = end - beg; - - const auto link = std::format("https://osu.ppy.sh/users/{}/scores/best?mode={}&limit={}&offset={}", id, - bpDownloadSelectItems[bpDownloadSelect], limit, offset); - std::string response; - int code; - if (net::curl_get(link.c_str(), response, &code) != CURLE_OK) - return; - - switch (code) { - case 200: { - try { - if (nlohmann::json j = nlohmann::json::parse(response); j.is_array() && !j.empty()) { - GuiHelper::ShowInfoToast(lang.getTextCStr("StartDownloadBP"), id, beg + 1, end); - - downloader::BeatmapInfo bi{downloader::BeatmapType::Bid, -1, true}; - for (auto &i : j) { - bi.id = i["beatmap_id"]; - LOGD("Post bp download: bid=%d", bi.id); - Downloader::GetInstance().postSearch(bi); + GuiHelper::ShowInfoToast(lang.getTextCStr("StartDownloadBP"), id, beg + 1, end); + MultiDownload::Task req; + req.url = std::format("https://osu.ppy.sh/users/{}/scores/best?mode={}&", id, + bpDownloadSelectItems[bpDownloadSelect]); + req.id = id; + req.beg = beg; + req.end = end; + req.callback = [&](const int code, std::string &response, MultiDownload::Task &thiz) { + switch (code) { + case 200: { + try { + if (nlohmann::json j = nlohmann::json::parse(response); j.is_array() && !j.empty()) { + if (j.size() < 100) + thiz.canceled = true; + downloader::BeatmapInfo bi{downloader::BeatmapType::Bid, -1, true}; + for (auto &i : j) { + if (!i.contains("beatmap_id")) + continue; + + bi.id = i["beatmap_id"]; + LOGD("Post bp download: bid=%d", bi.id); + Downloader::GetInstance().postSearch(bi); + } } + } catch (...) { + LOGW("Cannot parse bp download response!"); + LOGD("Response: %s", response.c_str()); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("BPDownloadFailedParseFailed")); } - } catch (...) { - LOGW("Cannot parse bp download response!"); - GuiHelper::ShowWarnToast(lang.getTextCStr("BPDownloadFailedParseFailed")); + break; } - break; - } - case 404: { - LOGW("BP Download failed: user(%d) not found!", id); - GuiHelper::ShowWarnToast(lang.getTextCStr("BPDownloadFailedUserNotFound"), id); - break; - } - default: { - LOGW("BP Download failed with http code: %d", code); - GuiHelper::ShowWarnToast(lang.getTextCStr("BPDownloadFailedResponseCodeNotOk"), code); - break; - } - } + case 404: { + LOGW("BP Download failed: user(%d) not found!", thiz.id); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("BPDownloadFailedUserNotFound"), thiz.id); + break; + } + default: { + LOGW("BP Download failed with http code: %d", code); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("BPDownloadFailedResponseCodeNotOk"), code); + break; + } + } + }; + + MultiDownload::GetInstance().doRequest(req); } void DoFavDownload(int id, int beg, int end) { @@ -115,42 +129,54 @@ void DoFavDownload(int id, int beg, int end) { return; } - const auto link = std::format("https://osu.ppy.sh/users/{}/beatmapsets/favourite?limit={}&offset={}", id, end - beg, beg); - std::string response; - int code; - if (net::curl_get(link.c_str(), response, &code) != CURLE_OK) - return; - - switch (code) { - case 200: { - try { - if (nlohmann::json j = nlohmann::json::parse(response); j.is_array() && !j.empty()) { - GuiHelper::ShowInfoToast(lang.getTextCStr("StartDownloadFav"), id, beg + 1, end); - - downloader::BeatmapInfo bi{downloader::BeatmapType::Sid, -1, true}; - for (auto &i : j) { - bi.id = i["id"]; - LOGD("Post bp download: bid=%d", bi.id); - Downloader::GetInstance().postSearch(bi); + GuiHelper::ShowInfoToast(lang.getTextCStr("StartDownloadFav"), id, beg + 1, end); + + MultiDownload::Task req; + req.url = std::format("https://osu.ppy.sh/users/{}/beatmapsets/favourite?", id); + req.beg = beg; + req.end = end; + req.id = id; + req.callback = [&](const int code, std::string &response, MultiDownload::Task &thiz) { + switch (code) { + case 200: { + try { + if (nlohmann::json j = nlohmann::json::parse(response); j.is_array() && !j.empty()) { + if (j.size() < 100) + thiz.canceled = true; + downloader::BeatmapInfo bi{downloader::BeatmapType::Sid, -1, true}; + for (auto &i : j) { + if (!i.contains("id")) + continue; + + bi.id = i["id"]; + LOGD("Post bp download: bid=%d", bi.id); + Downloader::GetInstance().postSearch(bi); + } } + } catch (...) { + LOGW("Cannot parse Fav download response!"); + LOGD("Response: %s", response.c_str()); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("FavDownloadFailedParseFailed")); } - } catch (...) { - LOGW("Cannot parse Fav download response!"); - GuiHelper::ShowWarnToast(lang.getTextCStr("FavDownloadFailedParseFailed")); + break; } - break; - } - case 404: { - LOGW("Fav Download failed: user(%d) not found!", id); - GuiHelper::ShowWarnToast(lang.getTextCStr("FavDownloadFailedUserNotFound"), id); - break; - } - default: { - LOGW("Fav Download failed with http code: %d", code); - GuiHelper::ShowWarnToast(lang.getTextCStr("FavDownloadFailedResponseCodeNotOk"), code); - break; - } - } + case 404: { + LOGW("Fav Download failed: user(%d) not found!", thiz.id); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("FavDownloadFailedUserNotFound"), thiz.id); + break; + } + default: { + LOGW("Fav Download failed with http code: %d", code); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("FavDownloadFailedResponseCodeNotOk"), code); + break; + } + } + }; + + MultiDownload::GetInstance().doRequest(req); } enum class BeatmapStatus : int { @@ -180,44 +206,117 @@ void DoMapperDownload(int id, int beg, int end, const BeatmapStatus &stat) { return; } - const auto link = std::format("https://osu.ppy.sh/users/{}/beatmapsets/{}?limit={}&offset={}", id, - s_StatusStr[(int)stat], end - beg + 1, beg); + GuiHelper::ShowInfoToast(lang.getTextCStr("StartDownloadMapper"), id, beg + 1, end, s_StatusStr[(int)stat]); + + MultiDownload::Task req; + req.url = std::format("https://osu.ppy.sh/users/{}/beatmapsets/{}?", id, + s_StatusStr[(int)stat]); + req.beg = beg; + req.end = end; + req.id = id; + req.callback = [&](const int code, std::string &response, MultiDownload::Task &thiz) { + switch (code) { + case 200: { + try { + if (nlohmann::json j = nlohmann::json::parse(response); j.is_array() && !j.empty()) { + if (j.size() < 100) + thiz.canceled = true; + + downloader::BeatmapInfo bi{downloader::BeatmapType::Sid, -1, true}; + for (auto &i : j) { + if (!i.contains("id")) + continue; + + bi.id = i["id"]; + LOGD("Post Mapper download: bid=%d", bi.id); + Downloader::GetInstance().postSearch(bi); + } + } + } catch (...) { + LOGW("Cannot parse Mapper download response!"); + LOGD("Response: %s", response.c_str()); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("MapperDownloadFailedParseFailed")); + } + break; + } + case 404: { + LOGW("Mapper Download failed: user(%d) not found!", thiz.id); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("MapperDownloadFailedUserNotFound"), thiz.id); + break; + } + default: { + LOGW("Mapper Download failed with http code: %d", code); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("MapperDownloadFailedResponseCodeNotOk"), code); + break; + } + } + }; + + MultiDownload::GetInstance().doRequest(req); +} + +void DoMostPlayedDownload(int id, int beg, int end) { + // https://osu.ppy.sh/users/{}/beatmapsets/most_played? - std::string response; - int code; - if (net::curl_get(link.c_str(), response, &code) != CURLE_OK) + auto &lang = i18n::I18nManager::GetInstance(); + + if (beg > end) { + LOGW("Invalid bp download range: beg=%d, end=%d", beg + 1, end); + GuiHelper::ShowWarnToast(lang.getTextCStr("InvalidDLRange"), beg + 1, end); return; + } - switch (code) { - case 200: { - try { - if (nlohmann::json j = nlohmann::json::parse(response); j.is_array() && !j.empty()) { - GuiHelper::ShowInfoToast(lang.getTextCStr("StartDownloadMapper"), id, beg + 1, end, s_StatusStr[(int)stat]); - - downloader::BeatmapInfo bi{downloader::BeatmapType::Sid, -1, true}; - for (auto &i : j) { - bi.id = i["id"]; - LOGD("Post Mapper download: bid=%d", bi.id); - Downloader::GetInstance().postSearch(bi); + GuiHelper::ShowInfoToast(lang.getTextCStr("StartDownloadMostPlayed"), id, beg + 1, end); + + MultiDownload::Task req; + req.url = std::format("https://osu.ppy.sh/users/{}/beatmapsets/most_played?", id); + req.beg = beg; + req.end = end; + req.id = id; + req.callback = [&](const int code, std::string &response, MultiDownload::Task &thiz) { + switch (code) { + case 200: { + try { + if (nlohmann::json j = nlohmann::json::parse(response); j.is_array() && !j.empty()) { + if (j.size() < 100) + thiz.canceled = true; + downloader::BeatmapInfo bi{downloader::BeatmapType::Bid, -1, true}; + for (auto &i : j) { + if (!i.contains("beatmap_id")) + continue; + + bi.id = i["beatmap_id"]; + LOGD("Post MostPlayed download: bid=%d", bi.id); + Downloader::GetInstance().postSearch(bi); + } } + } catch (...) { + LOGW("Cannot parse most played download response!"); + LOGD("Response: %s", response.c_str()); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("MostPlayedDownloadFailedParseFailed")); } - } catch (...) { - LOGW("Cannot parse Mapper download response!"); - GuiHelper::ShowWarnToast(lang.getTextCStr("MapperDownloadFailedParseFailed")); + break; } - break; - } - case 404: { - LOGW("Mapper Download failed: user(%d) not found!", id); - GuiHelper::ShowWarnToast(lang.getTextCStr("MapperDownloadFailedUserNotFound"), id); - break; - } - default: { - LOGW("Mapper Download failed with http code: %d", code); - GuiHelper::ShowWarnToast(lang.getTextCStr("MapperDownloadFailedResponseCodeNotOk"), code); - break; - } - } + case 404: { + LOGW("Most played Download failed: user(%d) not found!", thiz.id); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("MostPlayedDownloadFailedUserNotFound"), thiz.id); + break; + } + default: { + LOGW("Most played Download failed with http code: %d", code); + thiz.canceled = true; + GuiHelper::ShowWarnToast(lang.getTextCStr("MostPlayedDownloadFailedResponseCodeNotOk"), code); + break; + } + } + }; + + MultiDownload::GetInstance().doRequest(req); } void MultiDownload::drawMain() { @@ -275,7 +374,7 @@ void MultiDownload::drawMain() { static int id = 0; ImGui::InputInt("UID", &id); - static int begend[] = {1, MAX_FAV_DL_COUNT}; + static int begend[] = {1, 100}; ImGui::DragInt2(lang.getTextCStr("Range"), begend, 1, 1, MAX_FAV_DL_COUNT, "%d"); clamp(begend, 1, begend[1]); clamp(begend + 1, begend[0], MAX_FAV_DL_COUNT); @@ -300,7 +399,7 @@ void MultiDownload::drawMain() { static int id = 0; ImGui::InputInt("UID", &id); - static int begend[] = {1, MAX_MAPPER_DL_COUNT}; + static int begend[] = {1, 100}; ImGui::DragInt2(lang.getTextCStr("Range"), begend, 1, 1, MAX_MAPPER_DL_COUNT, "%d"); clamp(begend, 1, begend[1]); clamp(begend + 1, begend[0], MAX_MAPPER_DL_COUNT); @@ -314,4 +413,65 @@ void MultiDownload::drawMain() { #undef MAX_MAPPER_DL_COUNT #pragma endregion +#pragma region Most Play DL +#define MAX_MOST_PLAY_DL_COUNT 99999 + { + ImGui::BeginGroupPanel(lang.getTextCStr("MostPlayedDownloader")); + + static int id = 0; + ImGui::InputInt("UID", &id); + + static int begend[] = {1, 100}; + ImGui::DragInt2(lang.getTextCStr("Range"), begend, 1, 1, MAX_MOST_PLAY_DL_COUNT, "%d"); + clamp(begend, 1, begend[1]); + clamp(begend + 1, begend[0], MAX_MOST_PLAY_DL_COUNT); + ImGui::EndGroupPanel(); + + if (ImGui::Button(lang.getTextCStr("Download"))) { + DoMostPlayedDownload(id, begend[0] - 1, begend[1]); + } + } +#undef MAX_MOST_PLAY_DL_COUNT +#pragma endregion +} + +[[noreturn]] void MultiDownload::SearchThread() { + // auto &lang = i18n::I18nManager::GetInstance(); + auto &inst = GetInstance(); + while (true) { + auto tsk = inst.m_Queue.pop_front(); + + int limit = tsk.end - tsk.beg; + + int code = -1; + std::string response; + + if (limit <= 100) { + auto url = tsk.url; + url.append("limit=").append(std::to_string(limit)).append("&offset=").append(std::to_string(tsk.beg)); + net::curl_get(url.c_str(), response, &code); + + tsk.callback(code, response, tsk); + } else { + const int count = (tsk.end - tsk.beg) / 100 + 1; + int offset = tsk.beg; + for (int i = 0; i < count; ++i) { + response.clear(); + code = -1; + auto url = tsk.url; + const int curLimit = limit < 100 ? limit : 100; + url.append("limit=").append(std::to_string(curLimit)).append("&offset=").append(std::to_string(offset)); + net::curl_get(url.c_str(), response, &code); + tsk.callback(code, response, tsk); + if (tsk.canceled) { + LOGD("MultiDL search canceled"); + break; + } + + offset += 100; + limit -= 100; + } + } + LOGI("MultiDL search finished"); + } } diff --git a/Downloader/src/features/MultiDownload.h b/Downloader/src/features/MultiDownload.h index cc29611..90524d1 100644 --- a/Downloader/src/features/MultiDownload.h +++ b/Downloader/src/features/MultiDownload.h @@ -1,4 +1,9 @@ #pragma once +#include +#include +#include + +#include "BlockingContainer.hpp" #include "ui/Feature.h" @@ -6,6 +11,23 @@ namespace features { class MultiDownload : public ui::main::Feature { MultiDownload(); + std::thread t_SearchThread; + +public: + struct Task { + std::string url; + int beg, end; + // code | response | this + std::function callback; + bool canceled = false; + // for logger + int id; + }; + +private: + container::Blocking> m_Queue; + + [[noreturn]] static void SearchThread(); public: static MultiDownload &GetInstance() { @@ -13,8 +35,11 @@ class MultiDownload : public ui::main::Feature { return instance; } + void doRequest(const Task &task); + ui::main::FeatureInfo &getInfo() override; void drawMain() override; + }; }