diff --git a/project/vs2017/Taiga.vcxproj b/project/vs2017/Taiga.vcxproj index 740d12cd2..bd6796103 100644 --- a/project/vs2017/Taiga.vcxproj +++ b/project/vs2017/Taiga.vcxproj @@ -217,6 +217,8 @@ + + @@ -382,6 +384,9 @@ + + + diff --git a/project/vs2017/Taiga.vcxproj.filters b/project/vs2017/Taiga.vcxproj.filters index da9569ffd..f19647398 100644 --- a/project/vs2017/Taiga.vcxproj.filters +++ b/project/vs2017/Taiga.vcxproj.filters @@ -73,6 +73,9 @@ {d6043b6b-33fb-470e-bab4-28380104489e} + + {b390951c-cbaf-4d02-871d-563957f7080e} + {4eb462d7-7759-463c-99cf-e34daf381970} @@ -424,6 +427,12 @@ sync + + sync\anilist + + + sync\anilist + sync\kitsu @@ -915,6 +924,15 @@ sync + + sync\anilist + + + sync\anilist + + + sync\anilist + sync\kitsu diff --git a/res/menu.xml b/res/menu.xml index f673c4015..a82514e47 100644 --- a/res/menu.xml +++ b/res/menu.xml @@ -24,9 +24,15 @@ + + + + + + diff --git a/src/library/anime_util.cpp b/src/library/anime_util.cpp index 80cc4517f..6e43eb4f2 100644 --- a/src/library/anime_util.cpp +++ b/src/library/anime_util.cpp @@ -29,6 +29,7 @@ #include "library/anime_episode.h" #include "library/anime_util.h" #include "library/history.h" +#include "sync/anilist_util.h" #include "sync/kitsu_util.h" #include "sync/myanimelist_util.h" #include "sync/sync.h" @@ -965,7 +966,10 @@ std::wstring TranslateMyScore(int value, const std::wstring& default_char) { return sync::myanimelist::TranslateMyRating(value, false); case sync::kKitsu: return sync::kitsu::TranslateMyRating( - value, sync::kitsu::GetCurrentRatingSystem()); + value, sync::kitsu::GetRatingSystem()); + case sync::kAniList: + return sync::anilist::TranslateMyRating( + value, sync::anilist::GetRatingSystem()); } } @@ -977,7 +981,10 @@ std::wstring TranslateMyScoreFull(int value) { return sync::myanimelist::TranslateMyRating(value, true); case sync::kKitsu: return sync::kitsu::TranslateMyRating( - value, sync::kitsu::GetCurrentRatingSystem()); + value, sync::kitsu::GetRatingSystem()); + case sync::kAniList: + return sync::anilist::TranslateMyRating( + value, sync::anilist::GetRatingSystem()); } } @@ -986,9 +993,10 @@ std::wstring TranslateScore(double value) { default: case sync::kMyAnimeList: return ToWstr(value, 2); - case sync::kKitsu: return ToWstr(sync::kitsu::TranslateSeriesRatingTo(value), 2) + L"%"; + case sync::kAniList: + return ToWstr(sync::anilist::TranslateSeriesRatingTo(value), 0) + L"%"; } } diff --git a/src/sync/anilist.cpp b/src/sync/anilist.cpp new file mode 100644 index 000000000..a6d4b2f1b --- /dev/null +++ b/src/sync/anilist.cpp @@ -0,0 +1,632 @@ +/* +** Taiga +** Copyright (C) 2010-2017, Eren Okka +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "base/format.h" +#include "base/http.h" +#include "base/log.h" +#include "base/string.h" +#include "library/anime_db.h" +#include "library/anime_item.h" +#include "library/anime_util.h" +#include "sync/anilist.h" +#include "sync/anilist_util.h" +#include "taiga/settings.h" + +namespace sync { +namespace anilist { + +constexpr auto kRepeatingMediaListStatus = "REPEATING"; + +Service::Service() { + host_ = L"graphql.anilist.co"; + + id_ = kAniList; + canonical_name_ = L"anilist"; + name_ = L"AniList"; +} + +//////////////////////////////////////////////////////////////////////////////// + +void Service::BuildRequest(Request& request, HttpRequest& http_request) { + http_request.url.host = host_; + http_request.method = L"POST"; + + if (Settings.GetBool(taiga::kSync_Service_AniList_UseHttps)) + http_request.url.protocol = base::http::Protocol::Https; + + http_request.header[L"Accept"] = L"application/json"; + http_request.header[L"Accept-Charset"] = L"utf-8"; + http_request.header[L"Accept-Encoding"] = L"gzip"; + http_request.header[L"Content-Type"] = L"application/json"; + + if (RequestNeedsAuthentication(request.type)) + http_request.header[L"Authorization"] = L"Bearer " + user().access_token; + + switch (request.type) { + BUILD_HTTP_REQUEST(kAddLibraryEntry, AddLibraryEntry); + BUILD_HTTP_REQUEST(kAuthenticateUser, AuthenticateUser); + BUILD_HTTP_REQUEST(kDeleteLibraryEntry, DeleteLibraryEntry); + BUILD_HTTP_REQUEST(kGetLibraryEntries, GetLibraryEntries); + BUILD_HTTP_REQUEST(kGetMetadataById, GetMetadataById); + BUILD_HTTP_REQUEST(kGetSeason, GetSeason); + BUILD_HTTP_REQUEST(kSearchTitle, SearchTitle); + BUILD_HTTP_REQUEST(kUpdateLibraryEntry, UpdateLibraryEntry); + } +} + +void Service::HandleResponse(Response& response, HttpResponse& http_response) { + if (RequestSucceeded(response, http_response)) { + switch (response.type) { + HANDLE_HTTP_RESPONSE(kAddLibraryEntry, AddLibraryEntry); + HANDLE_HTTP_RESPONSE(kAuthenticateUser, AuthenticateUser); + HANDLE_HTTP_RESPONSE(kDeleteLibraryEntry, DeleteLibraryEntry); + HANDLE_HTTP_RESPONSE(kGetLibraryEntries, GetLibraryEntries); + HANDLE_HTTP_RESPONSE(kGetMetadataById, GetMetadataById); + HANDLE_HTTP_RESPONSE(kGetSeason, GetSeason); + HANDLE_HTTP_RESPONSE(kSearchTitle, SearchTitle); + HANDLE_HTTP_RESPONSE(kUpdateLibraryEntry, UpdateLibraryEntry); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Request builders + +void Service::AuthenticateUser(Request& request, HttpRequest& http_request) { + static const auto query{R"( +{ + Viewer { + id + name + mediaListOptions { + scoreFormat + } + } +})" + }; + + http_request.body = BuildRequestBody(ExpandQuery(query), nullptr); +} + +void Service::GetLibraryEntries(Request& request, HttpRequest& http_request) { + static const auto query{R"( +query ($userName: String!) { + MediaListCollection (userName: $userName, type: ANIME) { + statusLists { + ...mediaListFragment + } + } +} + +fragment mediaListFragment on MediaList { + {mediaListFields} + media { + ...mediaFragment + } +} + +fragment mediaFragment on Media { + {mediaFields} +})" + }; + + const Json variables{ + {"userName", WstrToStr(request.data[canonical_name_ + L"-username"])}, + }; + + http_request.body = BuildRequestBody(ExpandQuery(query), variables); +} + +void Service::GetMetadataById(Request& request, HttpRequest& http_request) { + static const auto query{R"( +query ($id: Int!) { + Media (id: $id, type: ANIME) { + {mediaFields} + } +})" + }; + + const Json variables{ + {"id", ToInt(request.data[canonical_name_ + L"-id"])}, + }; + + http_request.body = BuildRequestBody(ExpandQuery(query), variables); +} + +void Service::GetSeason(Request& request, HttpRequest& http_request) { + static const auto query{R"( +query ($season: MediaSeason!, $seasonYear: Int!, $page: Int) { + Page(page: $page) { + media(season: $season, seasonYear: $seasonYear, type: ANIME, sort: START_DATE) { + {mediaFields} + } + pageInfo { + total + perPage + currentPage + lastPage + hasNextPage + } + } +})" + }; + + const int page = request.data.count(L"page_offset") ? + ToInt(request.data[L"page_offset"]) : 1; + + const Json variables{ + {"season", TranslateSeasonTo(request.data[L"season"])}, + {"seasonYear", ToInt(request.data[L"year"])}, + {"page", page}, + }; + + http_request.body = BuildRequestBody(ExpandQuery(query), variables); +} + +void Service::SearchTitle(Request& request, HttpRequest& http_request) { + static const auto query{R"( +query ($query: String!) { + Page { + media(search: $query, type: ANIME) { + {mediaFields} + } + } +})" + }; + + const Json variables{ + {"query", WstrToStr(request.data[L"title"])}, + }; + + http_request.body = BuildRequestBody(ExpandQuery(query), variables); +} + +void Service::AddLibraryEntry(Request& request, HttpRequest& http_request) { + http_request.body = BuildLibraryObject(request); +} + +void Service::DeleteLibraryEntry(Request& request, HttpRequest& http_request) { + static const auto query{R"( +mutation ($id: Int!) { + DeleteMediaListEntry (id: $id) { + deleted + } +})" + }; + + const Json variables{ + {"id", ToInt(request.data[canonical_name_ + L"-library-id"])}, + }; + + http_request.body = BuildRequestBody(query, variables); +} + +void Service::UpdateLibraryEntry(Request& request, HttpRequest& http_request) { + http_request.body = BuildLibraryObject(request); +} + +//////////////////////////////////////////////////////////////////////////////// +// Response handlers + +void Service::AuthenticateUser(Response& response, HttpResponse& http_response) { + Json root; + + if (!ParseResponseBody(http_response.body, response, root)) + return; + + ParseUserObject(root["data"]["Viewer"]); +} + +void Service::GetLibraryEntries(Response& response, HttpResponse& http_response) { + Json root; + + if (!ParseResponseBody(http_response.body, response, root)) + return; + + const auto& status_lists = root["data"]["MediaListCollection"]["statusLists"]; + for (const auto& status_list : status_lists) { + for (const auto& value : status_list) { + ParseMediaListObject(value); + } + } +} + +void Service::GetMetadataById(Response& response, HttpResponse& http_response) { + Json root; + + if (!ParseResponseBody(http_response.body, response, root)) + return; + + ParseMediaObject(root["data"]["Media"]); +} + +void Service::GetSeason(Response& response, HttpResponse& http_response) { + Json root; + + if (!ParseResponseBody(http_response.body, response, root)) + return; + + const auto& page = root["data"]["Page"]; + + for (const auto& media : page["media"]) { + const auto anime_id = ParseMediaObject(media); + AppendString(response.data[L"ids"], ToWstr(anime_id), L","); + } + + const auto& page_info = page["pageInfo"]; + if (JsonReadBool(page_info, "hasNextPage")) { + const int current_page = JsonReadInt(page_info, "currentPage"); + response.data[L"next_page_offset"] = ToWstr(current_page + 1); + } +} + +void Service::SearchTitle(Response& response, HttpResponse& http_response) { + Json root; + + if (!ParseResponseBody(http_response.body, response, root)) + return; + + for (const auto& media : root["data"]["Page"]["media"]) { + const auto anime_id = ParseMediaObject(media); + AppendString(response.data[L"ids"], ToWstr(anime_id), L","); + } +} + +void Service::AddLibraryEntry(Response& response, HttpResponse& http_response) { + UpdateLibraryEntry(response, http_response); +} + +void Service::DeleteLibraryEntry(Response& response, HttpResponse& http_response) { + // Returns: {"data":{"DeleteMediaListEntry":{"deleted":true}}} +} + +void Service::UpdateLibraryEntry(Response& response, HttpResponse& http_response) { + Json root; + + if (!ParseResponseBody(http_response.body, response, root)) + return; + + ParseMediaListObject(root["data"]["SaveMediaListEntry"]); +} + +//////////////////////////////////////////////////////////////////////////////// + +bool Service::RequestNeedsAuthentication(RequestType request_type) const { + switch (request_type) { + case kAuthenticateUser: + case kAddLibraryEntry: + case kDeleteLibraryEntry: + case kUpdateLibraryEntry: + return true; + case kGetLibraryEntries: + case kGetMetadataById: + case kGetSeason: + case kSearchTitle: + return !user().access_token.empty(); + } + + return false; +} + +bool Service::RequestSucceeded(Response& response, + const HttpResponse& http_response) { + if (http_response.GetStatusCategory() == 200) + return true; + + // Error + Json root; + std::wstring error_description; + + if (JsonParseString(http_response.body, root)) { + if (root.count("errors")) { + const auto& errors = root["errors"]; + if (errors.is_array() && !errors.empty()) { + const auto& error = errors.front(); + error_description = StrToWstr(JsonReadStr(error, "message")); + if (error.count("validation")) { + const auto& validation = error["validation"]; + error_description = L"Validation error: " + StrToWstr( + validation.front().front().get()); + } + } + } + } + + response.data[L"error"] = error_description; + HandleError(http_response, response); + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// + +std::wstring Service::BuildLibraryObject(Request& request) const { + static const auto query{R"( +mutation ( + $id: Int, + $mediaId: Int, + $status: MediaListStatus, + $scoreRaw: Int, + $progress: Int, + $repeat: Int, + $notes: String, + $startedAt: FuzzyDateInput, + $completedAt: FuzzyDateInput) { + SaveMediaListEntry ( + id: $id, + mediaId: $mediaId, + status: $status, + scoreRaw: $scoreRaw, + progress: $progress, + repeat: $repeat, + notes: $notes, + startedAt: $startedAt, + completedAt: $completedAt) { + {mediaListFields} + media { + {mediaFields} + } + } +})" + }; + + Json variables{ + {"mediaId", ToInt(request.data[canonical_name_ + L"-id"])}, + }; + + const auto library_id = ToInt(request.data[canonical_name_ + L"-library-id"]); + if (library_id) + variables["id"] = library_id; + + std::string status; + if (request.data.count(L"status")) + status = TranslateMyStatusTo(ToInt(request.data[L"status"])); + if (request.data.count(L"enable_rewatching") && + ToBool(request.data[L"enable_rewatching"])) { + status = kRepeatingMediaListStatus; + } + if (!status.empty()) + variables["status"] = status; + + if (request.data.count(L"score")) + variables["scoreRaw"] = ToInt(request.data[L"score"]); + if (request.data.count(L"episode")) + variables["progress"] = ToInt(request.data[L"episode"]); + if (request.data.count(L"rewatched_times")) + variables["repeat"] = ToInt(request.data[L"rewatched_times"]); + if (request.data.count(L"notes")) + variables["notes"] = WstrToStr(request.data[L"notes"]); + if (request.data.count(L"date_start")) + variables["startedAt"] = TranslateFuzzyDateTo(Date(request.data[L"date_start"])); + if (request.data.count(L"date_finish")) + variables["completedAt"] = TranslateFuzzyDateTo(Date(request.data[L"date_finish"])); + + return BuildRequestBody(ExpandQuery(query), variables); +} + +std::wstring Service::BuildRequestBody(const std::string& query, + const Json& variables) const { + const Json json{ + {"query", query}, + {"variables", variables} + }; + + return StrToWstr(json.dump()); +} + +int Service::ParseMediaObject(const Json& json) const { + const auto anime_id = JsonReadInt(json, "id"); + + if (!anime_id) { + LOGW(L"Could not parse anime object:\n{}", StrToWstr(json.dump())); + return anime::ID_UNKNOWN; + } + + anime::Item anime_item; + anime_item.SetSource(this->id()); + anime_item.SetId(ToWstr(anime_id), this->id()); + anime_item.SetLastModified(time(nullptr)); // current time + + anime_item.SetTitle(StrToWstr(JsonReadStr(json["title"], "userPreferred"))); + anime_item.SetType(TranslateSeriesTypeFrom(JsonReadStr(json, "format"))); + anime_item.SetSynopsis(DecodeDescription(JsonReadStr(json, "description"))); + anime_item.SetDateStart(TranslateFuzzyDateFrom(json["startDate"])); + anime_item.SetDateEnd(TranslateFuzzyDateFrom(json["endDate"])); + anime_item.SetEpisodeCount(JsonReadInt(json, "episodes")); + anime_item.SetEpisodeLength(JsonReadInt(json, "duration")); + anime_item.SetImageUrl(StrToWstr(JsonReadStr(json["coverImage"], "large"))); + anime_item.SetScore(TranslateSeriesRatingFrom(JsonReadInt(json, "averageScore"))); + anime_item.SetPopularity(JsonReadInt(json, "popularity")); + + ParseMediaTitleObject(json, anime_item); + + std::vector genres; + for (const auto& genre : json["genres"]) { + if (genre.is_string()) + genres.push_back(StrToWstr(genre)); + } + anime_item.SetGenres(genres); + + for (const auto& synonym : json["synonyms"]) { + if (synonym.is_string()) + anime_item.InsertSynonym(StrToWstr(synonym)); + } + + return AnimeDatabase.UpdateItem(anime_item); +} + +int Service::ParseMediaListObject(const Json& json) const { + const auto anime_id = JsonReadInt(json["media"], "id"); + const auto library_id = JsonReadInt(json, "id"); + + if (!anime_id) { + LOGW(L"Could not parse library entry #{}", library_id); + return anime::ID_UNKNOWN; + } + + ParseMediaObject(json["media"]); + + anime::Item anime_item; + anime_item.SetSource(this->id()); + anime_item.SetId(ToWstr(anime_id), this->id()); + anime_item.AddtoUserList(); + + anime_item.SetMyId(ToWstr(library_id)); + + const auto status = JsonReadStr(json, "status"); + if (status == kRepeatingMediaListStatus) { + anime_item.SetMyStatus(anime::kWatching); + anime_item.SetMyRewatching(true); + } else { + anime_item.SetMyStatus(TranslateMyStatusFrom(status)); + } + + anime_item.SetMyScore(JsonReadInt(json, "score")); + anime_item.SetMyLastWatchedEpisode(JsonReadInt(json, "progress")); + anime_item.SetMyRewatchedTimes(JsonReadInt(json, "repeat")); + anime_item.SetMyNotes(StrToWstr(JsonReadStr(json, "notes"))); + anime_item.SetMyDateStart(TranslateFuzzyDateFrom(json["startedAt"])); + anime_item.SetMyDateEnd(TranslateFuzzyDateFrom(json["completedAt"])); + anime_item.SetMyLastUpdated(ToWstr(JsonReadInt(json, "updatedAt"))); + + return AnimeDatabase.UpdateItem(anime_item); +} + +void Service::ParseMediaTitleObject(const Json& json, + anime::Item& anime_item) const { + enum class TitleLanguage { + Romaji, + English, + Native, + }; + + static const std::map title_languages{ + {"romaji", TitleLanguage::Romaji}, + {"english", TitleLanguage::English}, + {"native", TitleLanguage::Native}, + }; + + const auto& titles = json["title"]; + const auto origin = StrToWstr(JsonReadStr(json, "countryOfOrigin")); + + for (auto it = titles.begin(); it != titles.end(); ++it) { + auto language = title_languages.find(it.key()); + if (language == title_languages.end() || !it->is_string()) + continue; + + const auto title = StrToWstr(it.value()); + + switch (language->second) { + case TitleLanguage::Romaji: + anime_item.SetTitle(title); + break; + case TitleLanguage::English: + anime_item.SetEnglishTitle(title); + break; + case TitleLanguage::Native: + if (IsEqual(origin, L"JP")) { + anime_item.SetJapaneseTitle(title); + } else if (!origin.empty()) { + anime_item.InsertSynonym(title); + } + break; + } + } +} + +void Service::ParseUserObject(const Json& json) { + user_.id = ToWstr(JsonReadInt(json, "id")); + user_.username = StrToWstr(JsonReadStr(json, "name")); + user_.rating_system = + StrToWstr(JsonReadStr(json["mediaListOptions"], "scoreFormat")); + + Settings.Set(taiga::kSync_Service_AniList_RatingSystem, user_.rating_system); +} + +bool Service::ParseResponseBody(const std::wstring& body, + Response& response, Json& json) { + if (JsonParseString(body, json)) + return true; + + switch (response.type) { + case kGetLibraryEntries: + response.data[L"error"] = L"Could not parse library entries"; + break; + case kGetMetadataById: + response.data[L"error"] = L"Could not parse anime object"; + break; + case kGetSeason: + response.data[L"error"] = L"Could not parse season data"; + break; + case kSearchTitle: + response.data[L"error"] = L"Could not parse search results"; + break; + case kUpdateLibraryEntry: + response.data[L"error"] = L"Could not parse library entry"; + break; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// + +std::string Service::ExpandQuery(const std::string& query) const { + auto str = StrToWstr(query); + + ReplaceString(str, L"{mediaFields}", GetMediaFields()); + ReplaceString(str, L"{mediaListFields}", GetMediaListFields()); + + ReplaceChar(str, '\n', ' '); + while (ReplaceString(str, L" ", L" ")); + + return WstrToStr(str); +} + +std::wstring Service::GetMediaFields() const { + return LR"(id +title { romaji english native userPreferred } +format +description +startDate { year month day } +endDate { year month day } +episodes +duration +countryOfOrigin +updatedAt +coverImage { large } +genres +synonyms +averageScore +popularity)"; +} + +std::wstring Service::GetMediaListFields() const { + return LR"(id +status +score(format: POINT_100) +progress +repeat +notes +startedAt { year month day } +completedAt { year month day } +updatedAt)"; +} + +} // namespace anilist +} // namespace sync diff --git a/src/sync/anilist.h b/src/sync/anilist.h new file mode 100644 index 000000000..93aa2634b --- /dev/null +++ b/src/sync/anilist.h @@ -0,0 +1,73 @@ +/* +** Taiga +** Copyright (C) 2010-2017, Eren Okka +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include "base/json.h" +#include "base/types.h" +#include "sync/service.h" + +namespace anime { +class Item; +} + +namespace sync { +namespace anilist { + +// API documentation: +// https://anilist.gitbooks.io/anilist-apiv2-docs/ +// https://anilist.github.io/ApiV2-GraphQL-Docs/ + +class Service : public sync::Service { +public: + Service(); + ~Service() {} + + void BuildRequest(Request& request, HttpRequest& http_request); + void HandleResponse(Response& response, HttpResponse& http_response); + bool RequestNeedsAuthentication(RequestType request_type) const; + +private: + REQUEST_AND_RESPONSE(AddLibraryEntry); + REQUEST_AND_RESPONSE(AuthenticateUser); + REQUEST_AND_RESPONSE(DeleteLibraryEntry); + REQUEST_AND_RESPONSE(GetLibraryEntries); + REQUEST_AND_RESPONSE(GetMetadataById); + REQUEST_AND_RESPONSE(GetSeason); + REQUEST_AND_RESPONSE(SearchTitle); + REQUEST_AND_RESPONSE(UpdateLibraryEntry); + + bool RequestSucceeded(Response& response, const HttpResponse& http_response); + + std::wstring BuildLibraryObject(Request& request) const; + std::wstring BuildRequestBody(const std::string& query, const Json& variables) const; + + int ParseMediaObject(const Json& json) const; + int ParseMediaListObject(const Json& json) const; + void ParseMediaTitleObject(const Json& json, anime::Item& anime_item) const; + void ParseUserObject(const Json& json); + + bool ParseResponseBody(const std::wstring& body, Response& response, Json& json); + + std::string ExpandQuery(const std::string& query) const; + std::wstring GetMediaFields() const; + std::wstring GetMediaListFields() const; +}; + +} // namespace anilist +} // namespace sync diff --git a/src/sync/anilist_types.h b/src/sync/anilist_types.h new file mode 100644 index 000000000..f03ba9cfa --- /dev/null +++ b/src/sync/anilist_types.h @@ -0,0 +1,35 @@ +/* +** Taiga +** Copyright (C) 2010-2017, Eren Okka +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +namespace sync { +namespace anilist { + +enum class RatingSystem { + Point_100, + Point_10_Decimal, + Point_10, + Point_5, + Point_3, +}; + +constexpr auto kDefaultRatingSystem = RatingSystem::Point_10; + +} // namespace anilist +} // namespace sync diff --git a/src/sync/anilist_util.cpp b/src/sync/anilist_util.cpp new file mode 100644 index 000000000..89685f96f --- /dev/null +++ b/src/sync/anilist_util.cpp @@ -0,0 +1,314 @@ +/* +** Taiga +** Copyright (C) 2010-2017, Eren Okka +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include + +#include "base/file.h" +#include "base/format.h" +#include "base/html.h" +#include "base/json.h" +#include "base/log.h" +#include "base/string.h" +#include "library/anime.h" +#include "library/anime_db.h" +#include "sync/anilist.h" +#include "sync/anilist_types.h" +#include "sync/anilist_util.h" +#include "sync/manager.h" +#include "taiga/settings.h" + +namespace sync { +namespace anilist { + +std::wstring DecodeDescription(std::string text) { + auto str = StrToWstr(text); + + ReplaceString(str, L"\n", L"\r\n"); + ReplaceString(str, L"
", L"\r\n"); + ReplaceString(str, L"\r\n\r\n\r\n", L"\r\n\r\n"); + + StripHtmlTags(str); + + return str; +} + +//////////////////////////////////////////////////////////////////////////////// + +RatingSystem GetRatingSystem() { + const auto& service = *ServiceManager.service(sync::kAniList); + return TranslateRatingSystemFrom(WstrToStr(service.user().rating_system)); +} + +std::vector GetMyRatings(RatingSystem rating_system) { + constexpr int k = anime::kUserScoreMax / 100; + + switch (rating_system) { + case RatingSystem::Point_100: + return { + { 0, L"0"}, + { 10 * k, L"10"}, + { 15 * k, L"15"}, + { 20 * k, L"20"}, + { 25 * k, L"25"}, + { 30 * k, L"30"}, + { 35 * k, L"35"}, + { 40 * k, L"40"}, + { 45 * k, L"45"}, + { 50 * k, L"50"}, + { 55 * k, L"55"}, + { 60 * k, L"60"}, + { 65 * k, L"65"}, + { 70 * k, L"70"}, + { 75 * k, L"75"}, + { 80 * k, L"80"}, + { 85 * k, L"85"}, + { 90 * k, L"90"}, + { 95 * k, L"95"}, + {100 * k, L"100"}, + }; + break; + case RatingSystem::Point_10_Decimal: + return { + { 0, L"0"}, + { 10 * k, L"1.0"}, + { 15 * k, L"1.5"}, + { 20 * k, L"2.0"}, + { 25 * k, L"2.5"}, + { 30 * k, L"3.0"}, + { 35 * k, L"3.5"}, + { 40 * k, L"4.0"}, + { 45 * k, L"4.5"}, + { 50 * k, L"5.0"}, + { 55 * k, L"5.5"}, + { 60 * k, L"6.0"}, + { 65 * k, L"6.5"}, + { 70 * k, L"7.0"}, + { 75 * k, L"7.5"}, + { 80 * k, L"8.0"}, + { 85 * k, L"8.5"}, + { 90 * k, L"9.0"}, + { 95 * k, L"9.5"}, + {100 * k, L"10"}, + }; + break; + case RatingSystem::Point_10: + return { + { 0, L"0"}, + { 10 * k, L"1"}, + { 20 * k, L"2"}, + { 30 * k, L"3"}, + { 40 * k, L"4"}, + { 50 * k, L"5"}, + { 60 * k, L"6"}, + { 70 * k, L"7"}, + { 80 * k, L"8"}, + { 90 * k, L"9"}, + {100 * k, L"10"}, + }; + case RatingSystem::Point_5: + return { + { 0, L"\u2606\u2606\u2606\u2606\u2606"}, + {10 * k, L"\u2605\u2606\u2606\u2606\u2606"}, + {30 * k, L"\u2605\u2605\u2606\u2606\u2606"}, + {50 * k, L"\u2605\u2605\u2605\u2606\u2606"}, + {70 * k, L"\u2605\u2605\u2605\u2605\u2606"}, + {90 * k, L"\u2605\u2605\u2605\u2605\u2605"}, + }; + case RatingSystem::Point_3: + return { + { 0, L"No Score"}, + {35 * k, L":("}, + {60 * k, L":|"}, + {85 * k, L":)"}, + }; + } + + return {}; +} + +//////////////////////////////////////////////////////////////////////////////// + +Date TranslateFuzzyDateFrom(const Json& json) { + return Date{ + static_cast(JsonReadInt(json, "year")), + static_cast(JsonReadInt(json, "month")), + static_cast(JsonReadInt(json, "day")) + }; +} + +Json TranslateFuzzyDateTo(const Date& date) { + Json json{{"year", nullptr}, {"month", nullptr}, {"day", nullptr}}; + + if (date.year()) json["year"] = date.year(); + if (date.month()) json["month"] = date.month(); + if (date.day()) json["day"] = date.day(); + + return json; +} + +std::string TranslateSeasonTo(const std::wstring& value) { + return WstrToStr(ToUpper_Copy(value)); +} + +double TranslateSeriesRatingFrom(int value) { + return static_cast(value) / 10.0; +} + +double TranslateSeriesRatingTo(double value) { + return value * 10.0; +} + +int TranslateSeriesTypeFrom(const std::string& value) { + static const std::map table{ + {"TV", anime::kTv}, + {"TV_SHORT", anime::kTv}, + {"MOVIE", anime::kMovie}, + {"SPECIAL", anime::kSpecial}, + {"OVA", anime::kOva}, + {"ONA", anime::kOna}, + {"MUSIC", anime::kMusic}, + }; + + const auto it = table.find(value); + if (it != table.end()) + return it->second; + + LOGW(L"Invalid value: {}", StrToWstr(value)); + return anime::kUnknownType; +} + +std::wstring TranslateMyRating(int value, RatingSystem rating_system) { + value = (value * 100) / anime::kUserScoreMax; + + switch (rating_system) { + case RatingSystem::Point_100: + return ToWstr(value); + + case RatingSystem::Point_10_Decimal: + return ToWstr(static_cast(value) / 10, 1); + + case RatingSystem::Point_10: + return ToWstr(value / 10); + + case RatingSystem::Point_5: + if (!value) {} + else if (value < 30) value = 1; + else if (value < 50) value = 2; + else if (value < 70) value = 3; + else if (value < 90) value = 4; + else value = 5; + return std::wstring(static_cast( value), L'\u2605') + + std::wstring(static_cast(5 - value), L'\u2606'); + + case RatingSystem::Point_3: + if (!value) {} + else if (value < 36) value = 1; + else if (value < 61) value = 2; + else value = 3; + switch (value) { + default: return L"No Score"; + case 1: return L":("; + case 2: return L":|"; + case 3: return L":)"; + } + } + + LOGW(L"Invalid value: {}", value); + return ToWstr(value); +} + +int TranslateMyStatusFrom(const std::string& value) { + static const std::map table{ + {"CURRENT", anime::kWatching}, + {"PLANNING", anime::kPlanToWatch}, + {"COMPLETED", anime::kCompleted}, + {"DROPPED", anime::kDropped}, + {"PAUSED", anime::kOnHold}, + }; + + const auto it = table.find(value); + if (it != table.end()) + return it->second; + + LOGW(L"Invalid value: {}", StrToWstr(value)); + return anime::kNotInList; +} + +std::string TranslateMyStatusTo(int value) { + switch (value) { + case anime::kWatching: return "CURRENT"; + case anime::kCompleted: return "COMPLETED"; + case anime::kOnHold: return "PAUSED"; + case anime::kDropped: return "DROPPED"; + case anime::kPlanToWatch: return "PLANNING"; + } + + LOGW(L"Invalid value: {}", value); + return ""; +} + +RatingSystem TranslateRatingSystemFrom(const std::string& value) { + static const std::map table{ + {"POINT_100", RatingSystem::Point_100}, + {"POINT_10_DECIMAL", RatingSystem::Point_10_Decimal}, + {"POINT_10", RatingSystem::Point_10}, + {"POINT_5", RatingSystem::Point_5}, + {"POINT_3", RatingSystem::Point_3}, + }; + + const auto it = table.find(value); + if (it != table.end()) + return it->second; + + LOGW(L"Invalid value: {}", StrToWstr(value)); + return kDefaultRatingSystem; +} + +//////////////////////////////////////////////////////////////////////////////// + +static const std::wstring kBaseUrl = L"https://anilist.co"; + +std::wstring GetAnimePage(const anime::Item& anime_item) { + return L"{}/anime/{}"_format(kBaseUrl, anime_item.GetId()); +} + +void RequestToken() { + constexpr auto kTaigaClientId = 161; + ExecuteLink(L"https://anilist.co/api/v2/oauth/authorize" + L"?client_id={}&response_type=token"_format(kTaigaClientId)); +} + +void ViewAnimePage(int anime_id) { + auto anime_item = AnimeDatabase.FindItem(anime_id); + + if (anime_item) + ExecuteLink(GetAnimePage(*anime_item)); +} + +void ViewProfile() { + ExecuteLink(L"{}/user/{}"_format(kBaseUrl, + Settings[taiga::kSync_Service_AniList_Username])); +} + +void ViewStats() { + ExecuteLink(L"{}/user/{}/stats"_format(kBaseUrl, + Settings[taiga::kSync_Service_AniList_Username])); +} + +} // namespace anilist +} // namespace sync diff --git a/src/sync/anilist_util.h b/src/sync/anilist_util.h new file mode 100644 index 000000000..f839eb2de --- /dev/null +++ b/src/sync/anilist_util.h @@ -0,0 +1,58 @@ +/* +** Taiga +** Copyright (C) 2010-2017, Eren Okka +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include + +#include "base/json.h" +#include "base/time.h" +#include "sync/anilist_types.h" +#include "sync/service.h" + +namespace anime { +class Item; +} + +namespace sync { +namespace anilist { + +std::wstring DecodeDescription(std::string text); + +RatingSystem GetRatingSystem(); +std::vector GetMyRatings(RatingSystem rating_system); + +Date TranslateFuzzyDateFrom(const Json& json); +Json TranslateFuzzyDateTo(const Date& date); +std::string TranslateSeasonTo(const std::wstring& value); +double TranslateSeriesRatingFrom(int value); +double TranslateSeriesRatingTo(double value); +int TranslateSeriesTypeFrom(const std::string& value); +std::wstring TranslateMyRating(int value, RatingSystem rating_system); +int TranslateMyStatusFrom(const std::string& value); +std::string TranslateMyStatusTo(int value); +RatingSystem TranslateRatingSystemFrom(const std::string& value); + +std::wstring GetAnimePage(const anime::Item& anime_item); +void RequestToken(); +void ViewAnimePage(int anime_id); +void ViewProfile(); +void ViewStats(); + +} // namespace anilist +} // namespace sync diff --git a/src/sync/kitsu.cpp b/src/sync/kitsu.cpp index 06ae0af2a..720a4af2c 100644 --- a/src/sync/kitsu.cpp +++ b/src/sync/kitsu.cpp @@ -52,14 +52,6 @@ Service::Service() { name_ = L"Kitsu"; } -RatingSystem Service::rating_system() const { - return rating_system_; -} - -void Service::set_rating_system(RatingSystem rating_system) { - rating_system_ = rating_system; -} - //////////////////////////////////////////////////////////////////////////////// void Service::BuildRequest(Request& request, HttpRequest& http_request) { @@ -77,7 +69,7 @@ void Service::BuildRequest(Request& request, HttpRequest& http_request) { // token, but behave differently (e.g. private library entries are included) // when it's provided. if (RequestNeedsAuthentication(request.type)) - http_request.header[L"Authorization"] = L"Bearer " + access_token_; + http_request.header[L"Authorization"] = L"Bearer " + user().access_token; switch (request.type) { BUILD_HTTP_REQUEST(kAddLibraryEntry, AddLibraryEntry); @@ -156,9 +148,9 @@ void Service::GetLibraryEntries(Request& request, HttpRequest& http_request) { // 4. Client doesn't know entry was deleted // // Our "solution" to this is to allow Taiga to download the entire library - // after each restart (i.e. last_synchronized_ is not saved on exit). + // after each restart (i.e. last_synchronized is not saved on exit). if (IsPartialLibraryRequest()) { - const auto date = GetDate(last_synchronized_ - (60 * 60 * 24)); // 1 day before, to be safe + const auto date = GetDate(user_.last_synchronized - (60 * 60 * 24)); // 1 day before, to be safe http_request.url.query[L"filter[since]"] = date.to_string(); } @@ -266,7 +258,7 @@ void Service::AuthenticateUser(Response& response, HttpResponse& http_response) if (!ParseResponseBody(http_response.body, response, root)) return; - access_token_ = StrToWstr(JsonReadStr(root, "access_token")); + user().access_token = StrToWstr(JsonReadStr(root, "access_token")); } void Service::GetUser(Response& response, HttpResponse& http_response) { @@ -279,11 +271,9 @@ void Service::GetUser(Response& response, HttpResponse& http_response) { user_.id = StrToWstr(JsonReadStr(user, "id")); user_.username = StrToWstr(JsonReadStr(user["attributes"], "name")); + user_.rating_system = StrToWstr(JsonReadStr(user["attributes"], "ratingSystem")); - const auto rating_system = JsonReadStr(user["attributes"], "ratingSystem"); - rating_system_ = TranslateRatingSystemFrom(rating_system); - Settings.Set(taiga::kSync_Service_Kitsu_RatingSystem, - StrToWstr(rating_system)); + Settings.Set(taiga::kSync_Service_Kitsu_RatingSystem, user_.rating_system); } void Service::GetLibraryEntries(Response& response, HttpResponse& http_response) { @@ -309,7 +299,7 @@ void Service::GetLibraryEntries(Response& response, HttpResponse& http_response) } if (!next_page) { - last_synchronized_ = time(nullptr); // current time + user_.last_synchronized = time(nullptr); // current time } } @@ -390,7 +380,7 @@ bool Service::RequestNeedsAuthentication(RequestType request_type) const { case kGetMetadataById: case kGetSeason: case kSearchTitle: - return !access_token_.empty(); + return !user().access_token.empty(); } return false; @@ -436,15 +426,9 @@ bool Service::RequestSucceeded(Response& response, } } - if (error_description.empty()) { - error_description = L"Unknown error (" + - canonical_name() + L"|" + - ToWstr(response.type) + L"|" + - ToWstr(http_response.code) + L")"; - } + response.data[L"error"] = error_description; + HandleError(http_response, response); - response.data[L"error"] = name() + L" returned an error: " + - error_description; return false; } @@ -787,7 +771,7 @@ bool Service::ParseResponseBody(const std::wstring& body, //////////////////////////////////////////////////////////////////////////////// bool Service::IsPartialLibraryRequest() const { - return last_synchronized_ && + return user_.last_synchronized && Settings.GetBool(taiga::kSync_Service_Kitsu_PartialLibrary); } diff --git a/src/sync/kitsu.h b/src/sync/kitsu.h index 34c1f07b5..615b83ba2 100644 --- a/src/sync/kitsu.h +++ b/src/sync/kitsu.h @@ -20,7 +20,6 @@ #include "base/json.h" #include "base/types.h" -#include "sync/kitsu_types.h" #include "sync/service.h" namespace sync { @@ -38,9 +37,6 @@ class Service : public sync::Service { void HandleResponse(Response& response, HttpResponse& http_response); bool RequestNeedsAuthentication(RequestType request_type) const; - RatingSystem rating_system() const; - void set_rating_system(RatingSystem rating_system); - private: REQUEST_AND_RESPONSE(AddLibraryEntry); REQUEST_AND_RESPONSE(AuthenticateUser); @@ -69,9 +65,6 @@ class Service : public sync::Service { bool ParseResponseBody(const std::wstring& body, Response& response, Json& json); bool IsPartialLibraryRequest() const; - - string_t access_token_; - RatingSystem rating_system_ = RatingSystem::Regular; }; } // namespace kitsu diff --git a/src/sync/kitsu_types.h b/src/sync/kitsu_types.h index c72ff9ba8..eadad638c 100644 --- a/src/sync/kitsu_types.h +++ b/src/sync/kitsu_types.h @@ -40,5 +40,7 @@ enum class RatingSystem { Advanced, }; +constexpr auto kDefaultRatingSystem = RatingSystem::Regular; + } // namespace kitsu } // namespace sync diff --git a/src/sync/kitsu_util.cpp b/src/sync/kitsu_util.cpp index ef208af51..30ade40fd 100644 --- a/src/sync/kitsu_util.cpp +++ b/src/sync/kitsu_util.cpp @@ -41,16 +41,9 @@ std::wstring DecodeSynopsis(std::string text) { //////////////////////////////////////////////////////////////////////////////// -RatingSystem GetCurrentRatingSystem() { - const auto& service = *reinterpret_cast( - ServiceManager.service(sync::kKitsu)); - return service.rating_system(); -} - -void SetCurrentRatingSystem(RatingSystem rating_system) { - auto& service = *reinterpret_cast( - ServiceManager.service(sync::kKitsu)); - service.set_rating_system(rating_system); +RatingSystem GetRatingSystem() { + const auto& service = *ServiceManager.service(sync::kKitsu); + return TranslateRatingSystemFrom(WstrToStr(service.user().rating_system)); } std::vector GetMyRatings(RatingSystem rating_system) { @@ -247,7 +240,7 @@ RatingSystem TranslateRatingSystemFrom(const std::string& value) { return it->second; LOGW(L"Invalid value: {}", StrToWstr(value)); - return RatingSystem::Regular; + return kDefaultRatingSystem; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/sync/kitsu_util.h b/src/sync/kitsu_util.h index f10bc19c3..acfd76726 100644 --- a/src/sync/kitsu_util.h +++ b/src/sync/kitsu_util.h @@ -33,8 +33,7 @@ namespace kitsu { std::wstring DecodeSynopsis(std::string text); -RatingSystem GetCurrentRatingSystem(); -void SetCurrentRatingSystem(RatingSystem rating_system); +RatingSystem GetRatingSystem(); std::vector GetMyRatings(RatingSystem rating_system); int TranslateAgeRatingFrom(const std::string& value); diff --git a/src/sync/manager.cpp b/src/sync/manager.cpp index 4036b7784..3b43071f7 100644 --- a/src/sync/manager.cpp +++ b/src/sync/manager.cpp @@ -22,6 +22,7 @@ #include "library/discover.h" #include "library/history.h" #include "library/resource.h" +#include "sync/anilist.h" #include "sync/kitsu.h" #include "sync/manager.h" #include "sync/myanimelist.h" @@ -39,6 +40,7 @@ Manager::Manager() { // Create services services_[kMyAnimeList].reset(new myanimelist::Service()); services_[kKitsu].reset(new kitsu::Service()); + services_[kAniList].reset(new anilist::Service()); } Manager::~Manager() { @@ -151,7 +153,7 @@ void Manager::HandleError(Response& response, HttpResponse& http_response) { switch (response.type) { case kAuthenticateUser: case kGetUser: - service.set_authenticated(false); + service.user().authenticated = false; ui::OnLogout(); ui::ChangeStatusText(response.data[L"error"]); break; @@ -220,7 +222,7 @@ void Manager::HandleResponse(Response& response, HttpResponse& http_response) { break; } } - service.set_authenticated(true); + service.user().authenticated = true; ui::OnLogin(); if (response.service_id == kKitsu) { // We need to make an additional request to get the user ID @@ -233,7 +235,7 @@ void Manager::HandleResponse(Response& response, HttpResponse& http_response) { case kGetUser: { ui::OnLogin(); - if (service.authenticated()) { + if (service.user().authenticated) { Synchronize(); } else { GetLibraryEntries(); diff --git a/src/sync/myanimelist.cpp b/src/sync/myanimelist.cpp index caab313a2..70244b1be 100644 --- a/src/sync/myanimelist.cpp +++ b/src/sync/myanimelist.cpp @@ -516,16 +516,12 @@ bool Service::RequestSucceeded(Response& response, response.data[L"error"] = error_message; break; } - default: - response.data[L"error"] += L"Unknown error (" + - canonical_name() + L"|" + - ToWstr(response.type) + L"|" + - ToWstr(http_response.code) + L")"; - break; } + HandleError(http_response, response); + return false; } } // namespace myanimelist -} // namespace sync \ No newline at end of file +} // namespace sync diff --git a/src/sync/service.cpp b/src/sync/service.cpp index 5c9f84f9c..bbc190ab3 100644 --- a/src/sync/service.cpp +++ b/src/sync/service.cpp @@ -16,7 +16,9 @@ ** along with this program. If not, see . */ -#include "service.h" +#include "base/format.h" +#include "base/http.h" +#include "sync/service.h" namespace sync { @@ -35,17 +37,13 @@ Response::Response() //////////////////////////////////////////////////////////////////////////////// Service::Service() - : authenticated_(false), id_(0), last_synchronized_(0) { + : id_(0) { } bool Service::RequestNeedsAuthentication(RequestType request_type) const { return false; } -bool Service::authenticated() const { - return authenticated_; -} - const string_t& Service::host() const { return host_; } @@ -62,12 +60,27 @@ const string_t& Service::name() const { return name_; } +User& Service::user() { + return user_; +} + const User& Service::user() const { return user_; } -void Service::set_authenticated(bool authenticated) { - authenticated_ = authenticated; +//////////////////////////////////////////////////////////////////////////////// + +void Service::HandleError(const HttpResponse& http_response, + Response& response) const { + auto& error = response.data[L"error"]; + + if (!error.empty()) { + error = L"{} returned an error: {}"_format(name(), error); + } else { + error = L"{} returned an unknown error. " + L"Request type: {} - HTTP status code: {}"_format( + name(), response.type, http_response.code); + } } -} // namespace sync \ No newline at end of file +} // namespace sync diff --git a/src/sync/service.h b/src/sync/service.h index a6c4c3c67..bb7012f47 100644 --- a/src/sync/service.h +++ b/src/sync/service.h @@ -33,7 +33,8 @@ enum ServiceId { kFirstService = 1, kMyAnimeList = 1, kKitsu = 2, - kLastService = 2 + kAniList = 3, + kLastService = 3 }; enum RequestType { @@ -70,10 +71,14 @@ class Response { dictionary_t data; }; -class User { -public: +struct User { string_t id; string_t username; + string_t rating_system; + + string_t access_token; + bool authenticated = false; + time_t last_synchronized = 0; }; struct Rating { @@ -90,16 +95,17 @@ class Service { virtual void HandleResponse(Response& response, HttpResponse& http_response) = 0; virtual bool RequestNeedsAuthentication(RequestType request_type) const; - bool authenticated() const; const string_t& host() const; enum_t id() const; const string_t& canonical_name() const; const string_t& name() const; - const User& user() const; - void set_authenticated(bool authenticated); + User& user(); + const User& user() const; protected: + void HandleError(const HttpResponse& http_response, Response& response) const; + // API end-point string_t host_; // Service identifiers @@ -107,9 +113,7 @@ class Service { string_t canonical_name_; string_t name_; // User information - bool authenticated_; User user_; - time_t last_synchronized_; }; // Creates two overloaded functions: First one is to build a request, second diff --git a/src/sync/sync.cpp b/src/sync/sync.cpp index ed4d85692..a7e73e7be 100644 --- a/src/sync/sync.cpp +++ b/src/sync/sync.cpp @@ -221,6 +221,7 @@ bool AddAuthenticationToRequest(Request& request) { void AddPageOffsetToRequest(const int offset, Request& request) { switch (taiga::GetCurrentServiceId()) { case sync::kKitsu: + case sync::kAniList: request.data[L"page_offset"] = ToWstr(offset); break; } @@ -242,6 +243,7 @@ bool AddServiceDataToRequest(Request& request, int id) { add_data(kMyAnimeList); add_data(kKitsu); + add_data(kAniList); return true; } @@ -257,12 +259,12 @@ void SetActiveServiceForRequest(Request& request) { bool UserAuthenticated() { auto service = taiga::GetCurrentService(); - return service->authenticated(); + return service->user().authenticated; } void InvalidateUserAuthentication() { auto service = taiga::GetCurrentService(); - service->set_authenticated(false); + service->user().authenticated = false; } //////////////////////////////////////////////////////////////////////////////// @@ -272,9 +274,16 @@ bool ServiceSupportsRequestType(RequestType request_type) { switch (request_type) { case kGetUser: + switch (service_id) { + case sync::kKitsu: + return true; + default: + return false; + } case kGetSeason: switch (service_id) { case sync::kKitsu: + case sync::kAniList: return true; default: return false; diff --git a/src/taiga/action.cpp b/src/taiga/action.cpp index 86f680122..fd09acf69 100644 --- a/src/taiga/action.cpp +++ b/src/taiga/action.cpp @@ -25,6 +25,7 @@ #include "library/anime_util.h" #include "library/discover.h" #include "library/history.h" +#include "sync/anilist_util.h" #include "sync/kitsu_util.h" #include "sync/myanimelist_util.h" #include "sync/sync.h" @@ -111,6 +112,9 @@ void ExecuteAction(std::wstring action, WPARAM wParam, LPARAM lParam) { case sync::kKitsu: sync::kitsu::ViewAnimePage(anime_id); break; + case sync::kAniList: + sync::anilist::ViewAnimePage(anime_id); + break; } // ViewUpcomingAnime @@ -144,6 +148,14 @@ void ExecuteAction(std::wstring action, WPARAM wParam, LPARAM lParam) { } else if (action == L"KitsuViewProfile") { sync::kitsu::ViewProfile(); + // AniListViewProfile() + // AniListViewStats() + // Opens up AniList user pages. + } else if (action == L"AniListViewProfile") { + sync::anilist::ViewProfile(); + } else if (action == L"AniListViewStats") { + sync::anilist::ViewStats(); + ////////////////////////////////////////////////////////////////////////////// // Execute(path) @@ -550,6 +562,7 @@ void ExecuteAction(std::wstring action, WPARAM wParam, LPARAM lParam) { } break; case sync::kKitsu: + case sync::kAniList: if (SeasonDatabase.LoadSeasonFromMemory(body)) { Settings.Set(taiga::kApp_Seasons_LastSeason, SeasonDatabase.current_season.GetString()); diff --git a/src/taiga/dummy.cpp b/src/taiga/dummy.cpp index 1cb121f3c..822351c67 100644 --- a/src/taiga/dummy.cpp +++ b/src/taiga/dummy.cpp @@ -30,6 +30,7 @@ void DummyAnime::Initialize() { SetId(L"4224", sync::kTaiga); SetId(L"4224", sync::kMyAnimeList); SetId(L"3532", sync::kKitsu); + SetId(L"4224", sync::kAniList); SetSlug(L"toradora"); SetTitle(L"Toradora!"); SetSynonyms(L"Tiger X Dragon"); diff --git a/src/taiga/resource.h b/src/taiga/resource.h index 0f7fc5c0f..fff3be8c6 100644 --- a/src/taiga/resource.h +++ b/src/taiga/resource.h @@ -28,183 +28,187 @@ #define IDD_SETTINGS_RECOGNITION_GENERAL 132 #define IDD_SETTINGS_RECOGNITION_MEDIA 133 #define IDD_SETTINGS_RECOGNITION_STREAM 134 -#define IDD_SETTINGS_SERVICES_KITSU 135 -#define IDD_SETTINGS_SERVICES_MAIN 136 -#define IDD_SETTINGS_SERVICES_MAL 137 -#define IDD_SETTINGS_SHARING_HTTP 138 -#define IDD_SETTINGS_SHARING_MIRC 139 -#define IDD_SETTINGS_SHARING_SKYPE 140 -#define IDD_SETTINGS_SHARING_TWITTER 141 -#define IDD_SETTINGS_TORRENTS_DISCOVERY 142 -#define IDD_SETTINGS_TORRENTS_DOWNLOADS 143 -#define IDD_SETTINGS_TORRENTS_FILTERS 144 -#define IDD_STATS 145 -#define IDD_TORRENT 146 -#define IDD_UPDATE 147 -#define IDD_UPDATE_NEW 148 +#define IDD_SETTINGS_SERVICES_ANILIST 135 +#define IDD_SETTINGS_SERVICES_KITSU 136 +#define IDD_SETTINGS_SERVICES_MAIN 137 +#define IDD_SETTINGS_SERVICES_MAL 138 +#define IDD_SETTINGS_SHARING_HTTP 139 +#define IDD_SETTINGS_SHARING_MIRC 140 +#define IDD_SETTINGS_SHARING_SKYPE 141 +#define IDD_SETTINGS_SHARING_TWITTER 142 +#define IDD_SETTINGS_TORRENTS_DISCOVERY 143 +#define IDD_SETTINGS_TORRENTS_DOWNLOADS 144 +#define IDD_SETTINGS_TORRENTS_FILTERS 145 +#define IDD_STATS 146 +#define IDD_TORRENT 147 +#define IDD_UPDATE 148 +#define IDD_UPDATE_NEW 149 #define IDC_BUTTON_ADDFOLDER 1000 -#define IDC_BUTTON_BROWSE 1001 -#define IDC_BUTTON_CACHE_CLEAR 1002 -#define IDC_BUTTON_CANCELSEARCH 1003 -#define IDC_BUTTON_FORMAT_HTTP 1004 -#define IDC_BUTTON_FORMAT_MIRC 1005 -#define IDC_BUTTON_FORMAT_SKYPE 1006 -#define IDC_BUTTON_FORMAT_TWITTER 1007 -#define IDC_BUTTON_MIRC_TEST 1008 -#define IDC_BUTTON_REMOVEFOLDER 1009 -#define IDC_BUTTON_TORRENT_BROWSE_APP 1010 -#define IDC_BUTTON_TORRENT_BROWSE_FOLDER 1011 -#define IDC_BUTTON_TWITTER_AUTH 1012 -#define IDC_CHECK_ANIME_ALT 1013 -#define IDC_CHECK_ANIME_REWATCH 1014 -#define IDC_CHECK_AUTOSTART 1015 -#define IDC_CHECK_CACHE1 1016 -#define IDC_CHECK_CACHE2 1017 -#define IDC_CHECK_CACHE3 1018 -#define IDC_CHECK_DETECT_MEDIA_PLAYER 1019 -#define IDC_CHECK_DETECT_STREAMING_MEDIA 1020 -#define IDC_CHECK_FOLDERS_WATCH 1021 -#define IDC_CHECK_GENERAL_CLOSE 1022 -#define IDC_CHECK_GENERAL_MINIMIZE 1023 -#define IDC_CHECK_GOTO_NOTRECOGNIZED 1024 -#define IDC_CHECK_GOTO_RECOGNIZED 1025 -#define IDC_CHECK_HIGHLIGHT 1026 -#define IDC_CHECK_HIGHLIGHT_ONTOP 1027 -#define IDC_CHECK_HTTP 1028 -#define IDC_CHECK_LIST_ENGLISH 1029 -#define IDC_CHECK_LIST_PROGRESS_AIRED 1030 -#define IDC_CHECK_LIST_PROGRESS_AVAILABLE 1031 -#define IDC_CHECK_MIRC 1032 -#define IDC_CHECK_MIRC_ACTION 1033 -#define IDC_CHECK_MIRC_MULTISERVER 1034 -#define IDC_CHECK_NOTIFY_NOTRECOGNIZED 1035 -#define IDC_CHECK_NOTIFY_RECOGNIZED 1036 -#define IDC_CHECK_SKYPE 1037 -#define IDC_CHECK_START_CHECKEPS 1038 -#define IDC_CHECK_START_LOGIN 1039 -#define IDC_CHECK_START_MINIMIZE 1040 -#define IDC_CHECK_START_VERSION 1041 -#define IDC_CHECK_TORRENT_APP_OPEN 1042 -#define IDC_CHECK_TORRENT_AUTOCHECK 1043 -#define IDC_CHECK_TORRENT_AUTOCREATEFOLDER 1044 -#define IDC_CHECK_TORRENT_AUTOSETFOLDER 1045 -#define IDC_CHECK_TORRENT_AUTOUSEFOLDER 1046 -#define IDC_CHECK_TORRENT_FILTER 1047 -#define IDC_CHECK_TWITTER 1048 -#define IDC_CHECK_UPDATE_CHECKMP 1049 -#define IDC_CHECK_UPDATE_CONFIRM 1050 -#define IDC_CHECK_UPDATE_RANGE 1051 -#define IDC_CHECK_UPDATE_ROOT 1052 -#define IDC_CHECK_UPDATE_WAITMP 1053 -#define IDC_COMBO_ANIME_SCORE 1054 -#define IDC_COMBO_ANIME_STATUS 1055 -#define IDC_COMBO_DBLCLICK 1056 -#define IDC_COMBO_FEED_ELEMENT 1057 -#define IDC_COMBO_FEED_FILTER_ACTION 1058 -#define IDC_COMBO_FEED_FILTER_MATCH 1059 -#define IDC_COMBO_FEED_FILTER_OPTION 1060 -#define IDC_COMBO_FEED_OPERATOR 1061 -#define IDC_COMBO_FEED_VALUE 1062 -#define IDC_COMBO_MDLCLICK 1063 -#define IDC_COMBO_SERVICE 1064 -#define IDC_COMBO_TORRENT_FOLDER 1065 -#define IDC_COMBO_TORRENT_SEARCH 1066 -#define IDC_COMBO_TORRENT_SOURCE 1067 -#define IDC_COMBO_TORRENTS_QUEUE_SORTBY 1068 -#define IDC_COMBO_TORRENTS_QUEUE_SORTORDER 1069 -#define IDC_DATETIME_FINISH 1070 -#define IDC_DATETIME_START 1071 -#define IDC_EDIT_ANIME_ALT 1072 -#define IDC_EDIT_ANIME_FOLDER 1073 -#define IDC_EDIT_ANIME_PROGRESS 1074 -#define IDC_EDIT_ANIME_SYNOPSIS 1075 -#define IDC_EDIT_ANIME_TAGS 1076 -#define IDC_EDIT_ANIME_TITLE 1077 -#define IDC_EDIT_DELAY 1078 -#define IDC_EDIT_EXTERNALLINKS 1079 -#define IDC_EDIT_FEED_NAME 1080 -#define IDC_EDIT_HTTP_URL 1081 -#define IDC_EDIT_INPUT 1082 -#define IDC_EDIT_MIRC_CHANNELS 1083 -#define IDC_EDIT_MIRC_SERVICE 1084 -#define IDC_EDIT_PASS_KITSU 1085 -#define IDC_EDIT_PASS_MAL 1086 -#define IDC_EDIT_PREVIEW 1087 -#define IDC_EDIT_SEARCH 1088 -#define IDC_EDIT_TORRENT_APP 1089 -#define IDC_EDIT_TORRENT_INTERVAL 1090 -#define IDC_EDIT_USER_KITSU 1091 -#define IDC_EDIT_USER_MAL 1092 -#define IDC_LINK_ACCOUNT_KITSU 1093 -#define IDC_LINK_ACCOUNT_MAL 1094 -#define IDC_LINK_ANIME_FANSUB 1095 -#define IDC_LINK_DEFAULTS 1096 -#define IDC_LINK_NOWPLAYING 1097 -#define IDC_LINK_TWITTER 1098 -#define IDC_LIST_ADVANCED_SETTINGS 1099 -#define IDC_LIST_EVENT 1100 -#define IDC_LIST_FEED_FILTER_ANIME 1101 -#define IDC_LIST_FEED_FILTER_CONDITIONS 1102 -#define IDC_LIST_FEED_FILTER_PRESETS 1103 -#define IDC_LIST_FOLDERS_ROOT 1104 -#define IDC_LIST_MAIN 1105 -#define IDC_LIST_MEDIA 1106 -#define IDC_LIST_SEARCH 1107 -#define IDC_LIST_SEASON 1108 -#define IDC_LIST_STREAM_PROVIDER 1109 -#define IDC_LIST_TORRENT 1110 -#define IDC_LIST_TORRENT_FILTER 1111 -#define IDC_PROGRESS_UPDATE 1112 -#define IDC_RADIO_MIRC_CHANNEL1 1113 -#define IDC_RADIO_MIRC_CHANNEL2 1114 -#define IDC_RADIO_MIRC_CHANNEL3 1115 -#define IDC_RADIO_TORRENT_APP1 1116 -#define IDC_RADIO_TORRENT_APP2 1117 -#define IDC_RADIO_TORRENT_NEW1 1118 -#define IDC_RADIO_TORRENT_NEW2 1119 -#define IDC_REBAR_MAIN 1120 -#define IDC_REBAR_SEASON 1121 -#define IDC_REBAR_TORRENT 1122 -#define IDC_RICHEDIT_ABOUT 1123 -#define IDC_RICHEDIT_FORMAT 1124 -#define IDC_RICHEDIT_UPDATE 1125 -#define IDC_SPIN_DELAY 1126 -#define IDC_SPIN_INPUT 1127 -#define IDC_SPIN_PROGRESS 1128 -#define IDC_SPIN_TORRENT_INTERVAL 1129 -#define IDC_STATIC_ANIME_DETAILS 1130 -#define IDC_STATIC_ANIME_IMG 1131 -#define IDC_STATIC_ANIME_STAT1 1132 -#define IDC_STATIC_ANIME_STAT2 1133 -#define IDC_STATIC_ANIME_STAT2_LABEL 1134 -#define IDC_STATIC_ANIME_STAT3 1135 -#define IDC_STATIC_ANIME_STAT4 1136 -#define IDC_STATIC_APP_ICON 1137 -#define IDC_STATIC_CACHE1 1138 -#define IDC_STATIC_CACHE2 1139 -#define IDC_STATIC_CACHE3 1140 -#define IDC_STATIC_FEED_FILTER_DISCARDTYPE 1141 -#define IDC_STATIC_FEED_FILTER_LIMIT 1142 -#define IDC_STATIC_HEADER 1143 -#define IDC_STATIC_HEADER1 1144 -#define IDC_STATIC_HEADER2 1145 -#define IDC_STATIC_HEADER3 1146 -#define IDC_STATIC_HEADER4 1147 -#define IDC_STATIC_INPUTINFO 1148 -#define IDC_STATIC_TAGSNOTES 1149 -#define IDC_STATIC_TITLE 1150 -#define IDC_STATIC_UPDATE_DETAILS 1151 -#define IDC_STATIC_UPDATE_PROGRESS 1152 -#define IDC_STATIC_UPDATE_TITLE 1153 -#define IDC_STATUSBAR_MAIN 1154 -#define IDC_TAB_ANIME 1155 -#define IDC_TAB_MAIN 1156 -#define IDC_TAB_PAGES 1157 -#define IDC_TOOLBAR_FEED_FILTER 1158 -#define IDC_TOOLBAR_MAIN 1159 -#define IDC_TOOLBAR_MENU 1160 -#define IDC_TOOLBAR_SEARCH 1161 -#define IDC_TOOLBAR_SEASON 1162 -#define IDC_TOOLBAR_TORRENT 1163 -#define IDC_TREE_MAIN 1164 -#define IDC_TREE_SECTIONS 1165 +#define IDC_BUTTON_ANILIST_AUTH 1001 +#define IDC_BUTTON_BROWSE 1002 +#define IDC_BUTTON_CACHE_CLEAR 1003 +#define IDC_BUTTON_CANCELSEARCH 1004 +#define IDC_BUTTON_FORMAT_HTTP 1005 +#define IDC_BUTTON_FORMAT_MIRC 1006 +#define IDC_BUTTON_FORMAT_SKYPE 1007 +#define IDC_BUTTON_FORMAT_TWITTER 1008 +#define IDC_BUTTON_MIRC_TEST 1009 +#define IDC_BUTTON_REMOVEFOLDER 1010 +#define IDC_BUTTON_TORRENT_BROWSE_APP 1011 +#define IDC_BUTTON_TORRENT_BROWSE_FOLDER 1012 +#define IDC_BUTTON_TWITTER_AUTH 1013 +#define IDC_CHECK_ANIME_ALT 1014 +#define IDC_CHECK_ANIME_REWATCH 1015 +#define IDC_CHECK_AUTOSTART 1016 +#define IDC_CHECK_CACHE1 1017 +#define IDC_CHECK_CACHE2 1018 +#define IDC_CHECK_CACHE3 1019 +#define IDC_CHECK_DETECT_MEDIA_PLAYER 1020 +#define IDC_CHECK_DETECT_STREAMING_MEDIA 1021 +#define IDC_CHECK_FOLDERS_WATCH 1022 +#define IDC_CHECK_GENERAL_CLOSE 1023 +#define IDC_CHECK_GENERAL_MINIMIZE 1024 +#define IDC_CHECK_GOTO_NOTRECOGNIZED 1025 +#define IDC_CHECK_GOTO_RECOGNIZED 1026 +#define IDC_CHECK_HIGHLIGHT 1027 +#define IDC_CHECK_HIGHLIGHT_ONTOP 1028 +#define IDC_CHECK_HTTP 1029 +#define IDC_CHECK_LIST_ENGLISH 1030 +#define IDC_CHECK_LIST_PROGRESS_AIRED 1031 +#define IDC_CHECK_LIST_PROGRESS_AVAILABLE 1032 +#define IDC_CHECK_MIRC 1033 +#define IDC_CHECK_MIRC_ACTION 1034 +#define IDC_CHECK_MIRC_MULTISERVER 1035 +#define IDC_CHECK_NOTIFY_NOTRECOGNIZED 1036 +#define IDC_CHECK_NOTIFY_RECOGNIZED 1037 +#define IDC_CHECK_SKYPE 1038 +#define IDC_CHECK_START_CHECKEPS 1039 +#define IDC_CHECK_START_LOGIN 1040 +#define IDC_CHECK_START_MINIMIZE 1041 +#define IDC_CHECK_START_VERSION 1042 +#define IDC_CHECK_TORRENT_APP_OPEN 1043 +#define IDC_CHECK_TORRENT_AUTOCHECK 1044 +#define IDC_CHECK_TORRENT_AUTOCREATEFOLDER 1045 +#define IDC_CHECK_TORRENT_AUTOSETFOLDER 1046 +#define IDC_CHECK_TORRENT_AUTOUSEFOLDER 1047 +#define IDC_CHECK_TORRENT_FILTER 1048 +#define IDC_CHECK_TWITTER 1049 +#define IDC_CHECK_UPDATE_CHECKMP 1050 +#define IDC_CHECK_UPDATE_CONFIRM 1051 +#define IDC_CHECK_UPDATE_RANGE 1052 +#define IDC_CHECK_UPDATE_ROOT 1053 +#define IDC_CHECK_UPDATE_WAITMP 1054 +#define IDC_COMBO_ANIME_SCORE 1055 +#define IDC_COMBO_ANIME_STATUS 1056 +#define IDC_COMBO_DBLCLICK 1057 +#define IDC_COMBO_FEED_ELEMENT 1058 +#define IDC_COMBO_FEED_FILTER_ACTION 1059 +#define IDC_COMBO_FEED_FILTER_MATCH 1060 +#define IDC_COMBO_FEED_FILTER_OPTION 1061 +#define IDC_COMBO_FEED_OPERATOR 1062 +#define IDC_COMBO_FEED_VALUE 1063 +#define IDC_COMBO_MDLCLICK 1064 +#define IDC_COMBO_SERVICE 1065 +#define IDC_COMBO_TORRENT_FOLDER 1066 +#define IDC_COMBO_TORRENT_SEARCH 1067 +#define IDC_COMBO_TORRENT_SOURCE 1068 +#define IDC_COMBO_TORRENTS_QUEUE_SORTBY 1069 +#define IDC_COMBO_TORRENTS_QUEUE_SORTORDER 1070 +#define IDC_DATETIME_FINISH 1071 +#define IDC_DATETIME_START 1072 +#define IDC_EDIT_ANIME_ALT 1073 +#define IDC_EDIT_ANIME_FOLDER 1074 +#define IDC_EDIT_ANIME_PROGRESS 1075 +#define IDC_EDIT_ANIME_SYNOPSIS 1076 +#define IDC_EDIT_ANIME_TAGS 1077 +#define IDC_EDIT_ANIME_TITLE 1078 +#define IDC_EDIT_DELAY 1079 +#define IDC_EDIT_EXTERNALLINKS 1080 +#define IDC_EDIT_FEED_NAME 1081 +#define IDC_EDIT_HTTP_URL 1082 +#define IDC_EDIT_INPUT 1083 +#define IDC_EDIT_MIRC_CHANNELS 1084 +#define IDC_EDIT_MIRC_SERVICE 1085 +#define IDC_EDIT_PASS_KITSU 1086 +#define IDC_EDIT_PASS_MAL 1087 +#define IDC_EDIT_PREVIEW 1088 +#define IDC_EDIT_SEARCH 1089 +#define IDC_EDIT_TORRENT_APP 1090 +#define IDC_EDIT_TORRENT_INTERVAL 1091 +#define IDC_EDIT_USER_ANILIST 1092 +#define IDC_EDIT_USER_KITSU 1093 +#define IDC_EDIT_USER_MAL 1094 +#define IDC_LINK_ACCOUNT_ANILIST 1095 +#define IDC_LINK_ACCOUNT_KITSU 1096 +#define IDC_LINK_ACCOUNT_MAL 1097 +#define IDC_LINK_ANIME_FANSUB 1098 +#define IDC_LINK_DEFAULTS 1099 +#define IDC_LINK_NOWPLAYING 1100 +#define IDC_LINK_TWITTER 1101 +#define IDC_LIST_ADVANCED_SETTINGS 1102 +#define IDC_LIST_EVENT 1103 +#define IDC_LIST_FEED_FILTER_ANIME 1104 +#define IDC_LIST_FEED_FILTER_CONDITIONS 1105 +#define IDC_LIST_FEED_FILTER_PRESETS 1106 +#define IDC_LIST_FOLDERS_ROOT 1107 +#define IDC_LIST_MAIN 1108 +#define IDC_LIST_MEDIA 1109 +#define IDC_LIST_SEARCH 1110 +#define IDC_LIST_SEASON 1111 +#define IDC_LIST_STREAM_PROVIDER 1112 +#define IDC_LIST_TORRENT 1113 +#define IDC_LIST_TORRENT_FILTER 1114 +#define IDC_PROGRESS_UPDATE 1115 +#define IDC_RADIO_MIRC_CHANNEL1 1116 +#define IDC_RADIO_MIRC_CHANNEL2 1117 +#define IDC_RADIO_MIRC_CHANNEL3 1118 +#define IDC_RADIO_TORRENT_APP1 1119 +#define IDC_RADIO_TORRENT_APP2 1120 +#define IDC_RADIO_TORRENT_NEW1 1121 +#define IDC_RADIO_TORRENT_NEW2 1122 +#define IDC_REBAR_MAIN 1123 +#define IDC_REBAR_SEASON 1124 +#define IDC_REBAR_TORRENT 1125 +#define IDC_RICHEDIT_ABOUT 1126 +#define IDC_RICHEDIT_FORMAT 1127 +#define IDC_RICHEDIT_UPDATE 1128 +#define IDC_SPIN_DELAY 1129 +#define IDC_SPIN_INPUT 1130 +#define IDC_SPIN_PROGRESS 1131 +#define IDC_SPIN_TORRENT_INTERVAL 1132 +#define IDC_STATIC_ANIME_DETAILS 1133 +#define IDC_STATIC_ANIME_IMG 1134 +#define IDC_STATIC_ANIME_STAT1 1135 +#define IDC_STATIC_ANIME_STAT2 1136 +#define IDC_STATIC_ANIME_STAT2_LABEL 1137 +#define IDC_STATIC_ANIME_STAT3 1138 +#define IDC_STATIC_ANIME_STAT4 1139 +#define IDC_STATIC_APP_ICON 1140 +#define IDC_STATIC_CACHE1 1141 +#define IDC_STATIC_CACHE2 1142 +#define IDC_STATIC_CACHE3 1143 +#define IDC_STATIC_FEED_FILTER_DISCARDTYPE 1144 +#define IDC_STATIC_FEED_FILTER_LIMIT 1145 +#define IDC_STATIC_HEADER 1146 +#define IDC_STATIC_HEADER1 1147 +#define IDC_STATIC_HEADER2 1148 +#define IDC_STATIC_HEADER3 1149 +#define IDC_STATIC_HEADER4 1150 +#define IDC_STATIC_INPUTINFO 1151 +#define IDC_STATIC_TAGSNOTES 1152 +#define IDC_STATIC_TITLE 1153 +#define IDC_STATIC_UPDATE_DETAILS 1154 +#define IDC_STATIC_UPDATE_PROGRESS 1155 +#define IDC_STATIC_UPDATE_TITLE 1156 +#define IDC_STATUSBAR_MAIN 1157 +#define IDC_TAB_ANIME 1158 +#define IDC_TAB_MAIN 1159 +#define IDC_TAB_PAGES 1160 +#define IDC_TOOLBAR_FEED_FILTER 1161 +#define IDC_TOOLBAR_MAIN 1162 +#define IDC_TOOLBAR_MENU 1163 +#define IDC_TOOLBAR_SEARCH 1164 +#define IDC_TOOLBAR_SEASON 1165 +#define IDC_TOOLBAR_TORRENT 1166 +#define IDC_TREE_MAIN 1167 +#define IDC_TREE_SECTIONS 1168 diff --git a/src/taiga/resource.rc b/src/taiga/resource.rc index 06336f2f1..4da8ec419 100644 --- a/src/taiga/resource.rc +++ b/src/taiga/resource.rc @@ -450,6 +450,21 @@ FONT 9, "Segoe UI", 400, 0, 0 +LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL +IDD_SETTINGS_SERVICES_ANILIST DIALOGEX 0, 0, 315, 215 +STYLE DS_3DLOOK | DS_CONTROL | DS_SHELLFONT | WS_CHILDWINDOW +EXSTYLE WS_EX_CONTROLPARENT +FONT 9, "Segoe UI", 400, 0, 0 +{ + GROUPBOX "Account", IDC_STATIC, 7, 7, 301, 55, 0, WS_EX_LEFT + LTEXT "Username: (not your email address)", IDC_STATIC, 12, 18, 141, 8, SS_LEFT, WS_EX_LEFT + EDITTEXT IDC_EDIT_USER_ANILIST, 12, 28, 141, 12, ES_AUTOHSCROLL, WS_EX_LEFT + PUSHBUTTON "Authorize...", IDC_BUTTON_ANILIST_AUTH, 160, 27, 85, 14, 0, WS_EX_LEFT + CONTROL "Create a new AniList account", IDC_LINK_ACCOUNT_ANILIST, "SysLink", 0x50010004, 12, 47, 141, 8, 0x00000000 +} + + + LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL IDD_SETTINGS_SERVICES_KITSU DIALOGEX 0, 0, 315, 215 STYLE DS_3DLOOK | DS_CONTROL | DS_SHELLFONT | WS_CHILDWINDOW diff --git a/src/taiga/script.cpp b/src/taiga/script.cpp index 7b1a0dbca..ecdc2d328 100644 --- a/src/taiga/script.cpp +++ b/src/taiga/script.cpp @@ -23,6 +23,7 @@ #include "library/anime_db.h" #include "library/anime_episode.h" #include "library/anime_util.h" +#include "sync/anilist_util.h" #include "sync/kitsu_util.h" #include "sync/myanimelist_util.h" #include "sync/sync.h" @@ -402,6 +403,9 @@ std::wstring ReplaceVariables(std::wstring str, const anime::Episode& episode, case sync::kKitsu: REPLACE(L"animeurl", ENCODE(sync::kitsu::GetAnimePage(*anime_item))); break; + case sync::kAniList: + REPLACE(L"animeurl", ENCODE(sync::anilist::GetAnimePage(*anime_item))); + break; } } else { pos_var = pos_end + 1; diff --git a/src/taiga/settings.cpp b/src/taiga/settings.cpp index b40fa50c9..396544641 100644 --- a/src/taiga/settings.cpp +++ b/src/taiga/settings.cpp @@ -118,6 +118,10 @@ void AppSettings::InitializeMap() { INITKEY(kSync_Service_Kitsu_PartialLibrary, L"true", L"account/kitsu/partiallibrary"); INITKEY(kSync_Service_Kitsu_RatingSystem, L"regular", L"account/kitsu/ratingsystem"); INITKEY(kSync_Service_Kitsu_UseHttps, L"true", L"account/kitsu/https"); + INITKEY(kSync_Service_AniList_Username, nullptr, L"account/anilist/username"); + INITKEY(kSync_Service_AniList_RatingSystem, L"POINT_10", L"account/anilist/ratingsystem"); + INITKEY(kSync_Service_AniList_Token, nullptr, L"account/anilist/token"); + INITKEY(kSync_Service_AniList_UseHttps, L"true", L"account/anilist/https"); // Library INITKEY(kLibrary_FileSizeThreshold, ToWstr(kDefaultFileSizeThreshold).c_str(), L"anime/folders/scan/minfilesize"); @@ -252,14 +256,12 @@ bool AppSettings::Load() { ReadLegacyValues(settings); // Services - switch (taiga::GetCurrentServiceId()) { - case sync::kKitsu: { - const auto rating_system = sync::kitsu::TranslateRatingSystemFrom( - WstrToStr(GetWstr(kSync_Service_Kitsu_RatingSystem))); - sync::kitsu::SetCurrentRatingSystem(rating_system); - break; - } - } + ServiceManager.service(sync::kKitsu)->user().rating_system = + GetWstr(kSync_Service_Kitsu_RatingSystem); + ServiceManager.service(sync::kAniList)->user().rating_system = + GetWstr(kSync_Service_AniList_RatingSystem); + ServiceManager.service(sync::kAniList)->user().access_token = + GetWstr(kSync_Service_AniList_Token); // Folders library_folders.clear(); @@ -509,6 +511,8 @@ const std::wstring GetCurrentUsername() { username = Settings[kSync_Service_Mal_Username]; } else if (service->id() == sync::kKitsu) { username = Settings[kSync_Service_Kitsu_Username]; + } else if (service->id() == sync::kAniList) { + username = Settings[kSync_Service_AniList_Username]; } return username; @@ -522,6 +526,8 @@ const std::wstring GetCurrentPassword() { password = Base64Decode(Settings[kSync_Service_Mal_Password]); } else if (service->id() == sync::kKitsu) { password = Base64Decode(Settings[kSync_Service_Kitsu_Password]); + } else if (service->id() == sync::kAniList) { + password = Base64Decode(Settings[kSync_Service_AniList_Token]); } return password; diff --git a/src/taiga/settings.h b/src/taiga/settings.h index 40ed85e5a..7062ede9d 100644 --- a/src/taiga/settings.h +++ b/src/taiga/settings.h @@ -52,6 +52,10 @@ enum AppSettingName { kSync_Service_Kitsu_PartialLibrary, kSync_Service_Kitsu_RatingSystem, kSync_Service_Kitsu_UseHttps, + kSync_Service_AniList_Username, + kSync_Service_AniList_RatingSystem, + kSync_Service_AniList_Token, + kSync_Service_AniList_UseHttps, // Library kLibrary_FileSizeThreshold, diff --git a/src/ui/dlg/dlg_anime_info_page.cpp b/src/ui/dlg/dlg_anime_info_page.cpp index 86d1f5be4..9984c88bb 100644 --- a/src/ui/dlg/dlg_anime_info_page.cpp +++ b/src/ui/dlg/dlg_anime_info_page.cpp @@ -23,6 +23,7 @@ #include "library/anime_db.h" #include "library/anime_util.h" #include "library/history.h" +#include "sync/anilist_util.h" #include "sync/kitsu_util.h" #include "sync/myanimelist_util.h" #include "sync/sync.h" @@ -238,14 +239,22 @@ BOOL PageMyInfo::OnCommand(WPARAM wParam, LPARAM lParam) { int episode_value = 0; spin.GetPos32(episode_value); if (IsDlgButtonChecked(IDC_CHECK_ANIME_REWATCH)) { - if (taiga::GetCurrentServiceId() == sync::kKitsu) - combobox.SetCurSel(anime::kWatching - 1); + switch (taiga::GetCurrentServiceId()) { + case sync::kKitsu: + case sync::kAniList: + combobox.SetCurSel(anime::kWatching - 1); + break; + } if (anime_item->GetMyStatus() == anime::kCompleted && episode_value == anime_item->GetEpisodeCount()) spin.SetPos32(0); } else { - if (taiga::GetCurrentServiceId() == sync::kKitsu) - combobox.SetCurSel(anime_item->GetMyStatus() - 1); + switch (taiga::GetCurrentServiceId()) { + case sync::kKitsu: + case sync::kAniList: + combobox.SetCurSel(anime_item->GetMyStatus() - 1); + break; + } if (episode_value == 0) spin.SetPos32(anime_item->GetMyLastWatchedEpisode()); } @@ -357,11 +366,17 @@ void PageMyInfo::Refresh(int anime_id) { current_rating = sync::myanimelist::TranslateMyRating(anime_item->GetMyScore(), true); break; case sync::kKitsu: { - const auto rating_system = sync::kitsu::GetCurrentRatingSystem(); + const auto rating_system = sync::kitsu::GetRatingSystem(); ratings = sync::kitsu::GetMyRatings(rating_system); current_rating = sync::kitsu::TranslateMyRating(anime_item->GetMyScore(), rating_system); break; } + case sync::kAniList: { + const auto rating_system = sync::anilist::GetRatingSystem(); + ratings = sync::anilist::GetMyRatings(rating_system); + current_rating = sync::anilist::TranslateMyRating(anime_item->GetMyScore(), rating_system); + break; + } } for (auto it = ratings.rbegin(); it != ratings.rend(); ++it) { const auto& rating = *it; @@ -383,6 +398,7 @@ void PageMyInfo::Refresh(int anime_id) { edit.SetText(anime_item->GetMyTags()); break; case sync::kKitsu: + case sync::kAniList: SetDlgItemText(IDC_STATIC_TAGSNOTES, L"Notes:"); edit.SetCueBannerText(L"Enter your notes about this anime"); edit.SetText(anime_item->GetMyNotes()); @@ -501,6 +517,7 @@ bool PageMyInfo::Save() { history_item.tags = GetDlgItemText(IDC_EDIT_ANIME_TAGS); break; case sync::kKitsu: + case sync::kAniList: history_item.notes = GetDlgItemText(IDC_EDIT_ANIME_TAGS); break; } diff --git a/src/ui/dlg/dlg_season.cpp b/src/ui/dlg/dlg_season.cpp index c65fd0045..a766c00f0 100644 --- a/src/ui/dlg/dlg_season.cpp +++ b/src/ui/dlg/dlg_season.cpp @@ -94,6 +94,7 @@ BOOL SeasonDialog::OnInitDialog() { } break; case sync::kKitsu: + case sync::kAniList: SeasonDatabase.LoadSeasonFromMemory(last_season); break; } @@ -130,6 +131,7 @@ BOOL SeasonDialog::OnCommand(WPARAM wParam, LPARAM lParam) { RefreshData(); break; case sync::kKitsu: + case sync::kAniList: GetData(); break; } @@ -427,8 +429,11 @@ LRESULT SeasonDialog::OnListCustomDraw(LPARAM lParam) { DRAWLINE(L"Aired:"); DRAWLINE(L"Episodes:"); DRAWLINE(L"Genres:"); - if (current_service == sync::kMyAnimeList) { - DRAWLINE(L"Producers:"); + switch (current_service) { + case sync::kMyAnimeList: + case sync::kAniList: + DRAWLINE(L"Producers:"); + break; } DRAWLINE(L"Score:"); DRAWLINE(L"Popularity:"); @@ -444,8 +449,11 @@ LRESULT SeasonDialog::OnListCustomDraw(LPARAM lParam) { DRAWLINE(text); DRAWLINE(anime::TranslateNumber(anime_item->GetEpisodeCount(), L"Unknown")); DRAWLINE(anime_item->GetGenres().empty() ? L"?" : Join(anime_item->GetGenres(), L", ")); - if (current_service == sync::kMyAnimeList) { - DRAWLINE(anime_item->GetProducers().empty() ? L"?" : Join(anime_item->GetProducers(), L", ")); + switch (current_service) { + case sync::kMyAnimeList: + case sync::kAniList: + DRAWLINE(anime_item->GetProducers().empty() ? L"?" : Join(anime_item->GetProducers(), L", ")); + break; } DRAWLINE(anime::TranslateScore(anime_item->GetScore())); DRAWLINE(L"#" + ToWstr(anime_item->GetPopularity())); @@ -764,6 +772,7 @@ int SeasonDialog::GetLineCount() const { switch (taiga::GetCurrentServiceId()) { default: case sync::kMyAnimeList: + case sync::kAniList: return 6; case sync::kKitsu: return 5; // missing producers diff --git a/src/ui/dlg/dlg_settings.cpp b/src/ui/dlg/dlg_settings.cpp index 2f4b30424..8ad6bf8ed 100644 --- a/src/ui/dlg/dlg_settings.cpp +++ b/src/ui/dlg/dlg_settings.cpp @@ -73,6 +73,7 @@ void SettingsDialog::SetCurrentSection(SettingsSections section) { tab_.InsertItem(0, L"Main", kSettingsPageServicesMain); tab_.InsertItem(1, L"MyAnimeList", kSettingsPageServicesMal); tab_.InsertItem(2, L"Kitsu", kSettingsPageServicesKitsu); + tab_.InsertItem(3, L"AniList", kSettingsPageServicesAniList); break; case kSettingsSectionLibrary: tab_.InsertItem(0, L"Folders", kSettingsPageLibraryFolders); @@ -197,6 +198,11 @@ void SettingsDialog::OnOK() { Settings.Set(taiga::kSync_Service_Kitsu_Username, page->GetDlgItemText(IDC_EDIT_USER_KITSU)); Settings.Set(taiga::kSync_Service_Kitsu_Password, Base64Encode(page->GetDlgItemText(IDC_EDIT_PASS_KITSU))); } + // Services > Kitsu + page = &pages[kSettingsPageServicesAniList]; + if (page->IsWindow()) { + Settings.Set(taiga::kSync_Service_AniList_Username, page->GetDlgItemText(IDC_EDIT_USER_ANILIST)); + } // Library > Folders page = &pages[kSettingsPageLibraryFolders]; diff --git a/src/ui/dlg/dlg_settings_page.cpp b/src/ui/dlg/dlg_settings_page.cpp index 6e84ffa13..906d6953b 100644 --- a/src/ui/dlg/dlg_settings_page.cpp +++ b/src/ui/dlg/dlg_settings_page.cpp @@ -29,6 +29,7 @@ #include "library/anime_db.h" #include "library/history.h" #include "library/resource.h" +#include "sync/anilist_util.h" #include "sync/manager.h" #include "taiga/announce.h" #include "taiga/path.h" @@ -72,6 +73,7 @@ void SettingsPage::Create() { SETRESOURCEID(kSettingsPageServicesMain, IDD_SETTINGS_SERVICES_MAIN); SETRESOURCEID(kSettingsPageServicesMal, IDD_SETTINGS_SERVICES_MAL); SETRESOURCEID(kSettingsPageServicesKitsu, IDD_SETTINGS_SERVICES_KITSU); + SETRESOURCEID(kSettingsPageServicesAniList, IDD_SETTINGS_SERVICES_ANILIST); SETRESOURCEID(kSettingsPageSharingHttp, IDD_SETTINGS_SHARING_HTTP); SETRESOURCEID(kSettingsPageSharingMirc, IDD_SETTINGS_SHARING_MIRC); SETRESOURCEID(kSettingsPageSharingSkype, IDD_SETTINGS_SHARING_SKYPE); @@ -117,6 +119,11 @@ BOOL SettingsPage::OnInitDialog() { SetDlgItemText(IDC_EDIT_PASS_KITSU, Base64Decode(Settings[taiga::kSync_Service_Kitsu_Password]).c_str()); break; } + // Services > AniList + case kSettingsPageServicesAniList: { + SetDlgItemText(IDC_EDIT_USER_ANILIST, Settings[taiga::kSync_Service_AniList_Username].c_str()); + break; + } //////////////////////////////////////////////////////////////////////////// @@ -560,6 +567,16 @@ BOOL SettingsPage::OnCommand(WPARAM wParam, LPARAM lParam) { //////////////////////////////////////////////////////////////////////// + // Authorize AniList + case IDC_BUTTON_ANILIST_AUTH: { + sync::anilist::RequestToken(); + std::wstring auth_pin; + if (ui::EnterAuthorizationPin(L"AniList", auth_pin)) { + Settings.Set(taiga::kSync_Service_AniList_Token, auth_pin); + } + return TRUE; + } + // Authorize Twitter case IDC_BUTTON_TWITTER_AUTH: { Twitter.RequestToken(); @@ -817,6 +834,7 @@ LRESULT SettingsPage::OnNotify(int idCtrl, LPNMHDR pnmh) { case NM_RETURN: { switch (pnmh->idFrom) { // Execute link + case IDC_LINK_ACCOUNT_ANILIST: case IDC_LINK_ACCOUNT_KITSU: case IDC_LINK_ACCOUNT_MAL: case IDC_LINK_TWITTER: { diff --git a/src/ui/dlg/dlg_settings_page.h b/src/ui/dlg/dlg_settings_page.h index aeafea236..9a80a6dff 100644 --- a/src/ui/dlg/dlg_settings_page.h +++ b/src/ui/dlg/dlg_settings_page.h @@ -31,6 +31,7 @@ enum SettingsPages { kSettingsPageRecognitionGeneral, kSettingsPageRecognitionMedia, kSettingsPageRecognitionStream, + kSettingsPageServicesAniList, kSettingsPageServicesKitsu, kSettingsPageServicesMain, kSettingsPageServicesMal, diff --git a/src/ui/dlg/dlg_stats.cpp b/src/ui/dlg/dlg_stats.cpp index 460fa9223..83669eb0d 100644 --- a/src/ui/dlg/dlg_stats.cpp +++ b/src/ui/dlg/dlg_stats.cpp @@ -169,7 +169,7 @@ void StatsDialog::Refresh() { } rating_type = RatingType::Ten; switch (taiga::GetCurrentServiceId()) { case sync::kKitsu: - switch (sync::kitsu::GetCurrentRatingSystem()) { + switch (sync::kitsu::GetRatingSystem()) { case sync::kitsu::RatingSystem::Regular: rating_type = RatingType::Five; break; diff --git a/src/ui/menu.cpp b/src/ui/menu.cpp index 6b332699c..247dd6a63 100644 --- a/src/ui/menu.cpp +++ b/src/ui/menu.cpp @@ -25,9 +25,10 @@ #include "library/anime_episode.h" #include "library/anime_util.h" #include "library/discover.h" -#include "sync/sync.h" +#include "sync/anilist_util.h" #include "sync/kitsu_util.h" #include "sync/myanimelist_util.h" +#include "sync/sync.h" #include "taiga/settings.h" #include "track/feed.h" #include "ui/menu.h" @@ -260,15 +261,21 @@ void MenuList::UpdateScore(const anime::Item* anime_item) { anime_item->GetMyScore(), true); break; case sync::kKitsu: { - const auto rating_system = sync::kitsu::GetCurrentRatingSystem(); + const auto rating_system = sync::kitsu::GetRatingSystem(); ratings = sync::kitsu::GetMyRatings(rating_system); current_rating = sync::kitsu::TranslateMyRating( anime_item->GetMyScore(), rating_system); break; } + case sync::kAniList: { + const auto rating_system = sync::anilist::GetRatingSystem(); + ratings = sync::anilist::GetMyRatings(rating_system); + current_rating = sync::anilist::TranslateMyRating( + anime_item->GetMyScore(), rating_system); + break; + } } - for (const auto& rating : ratings) { const bool current = rating.text == current_rating; menu->CreateItem(L"EditScore({})"_format(rating.value), rating.text, @@ -331,7 +338,8 @@ void MenuList::UpdateSeason() { auto season_min = SeasonDatabase.available_seasons.first; auto season_max = SeasonDatabase.available_seasons.second; switch (taiga::GetCurrentServiceId()) { - case sync::kKitsu: { + case sync::kKitsu: + case sync::kAniList: { const auto next_season = ++anime::Season(GetDate()); season_max = std::max(season_max, next_season); season_min = anime::Season(anime::Season::Name::kWinter, 2010); @@ -385,7 +393,8 @@ void MenuList::UpdateSeason() { // Add available seasons create_available_seasons(*menu, season_min, season_max); switch (taiga::GetCurrentServiceId()) { - case sync::kKitsu: { + case sync::kKitsu: + case sync::kAniList: { menu->CreateItem(); // separator for (int decade = 2000; decade >= 1960; decade -= 10) { std::wstring submenu_name = L"SeasonDecade" + ToWstr(decade); diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 6db30efbd..d70c994b8 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -95,6 +95,21 @@ void DisplayErrorMessage(const std::wstring& text, MessageBox(nullptr, text.c_str(), caption.c_str(), MB_OK | MB_ICONERROR); } +bool EnterAuthorizationPin(const string_t& service, string_t& auth_pin) { + InputDialog dlg; + dlg.title = L"{} Authorization"_format(service); + dlg.info = L"Please enter the PIN shown on the page after " + L"logging into {}:"_format(service); + dlg.Show(); + + if (dlg.result == IDOK && !dlg.text.empty()) { + auth_pin = dlg.text; + return true; + } + + return false; +} + //////////////////////////////////////////////////////////////////////////////// void OnHttpError(const taiga::HttpClient& http_client, const string_t& error) { @@ -996,19 +1011,7 @@ void OnTwitterTokenRequest(bool success) { bool OnTwitterTokenEntry(string_t& auth_pin) { ClearStatusText(); - - InputDialog dlg; - dlg.title = L"Twitter Authorization"; - dlg.info = L"Please enter the PIN shown on the page after logging into " - L"Twitter:"; - dlg.Show(); - - if (dlg.result == IDOK && !dlg.text.empty()) { - auth_pin = dlg.text; - return true; - } - - return false; + return EnterAuthorizationPin(L"Twitter", auth_pin); } void OnTwitterAuth(bool success) { diff --git a/src/ui/ui.h b/src/ui/ui.h index cbe8009bb..1ceb829ab 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -60,6 +60,7 @@ void SetSharedCursor(LPCWSTR name); int StatusToIcon(int status); void DisplayErrorMessage(const std::wstring& text, const std::wstring& caption); +bool EnterAuthorizationPin(const string_t& service, string_t& auth_pin); void OnHttpError(const taiga::HttpClient& http_client, const string_t& error); void OnHttpHeadersAvailable(const taiga::HttpClient& http_client);