From fc93c3020149983d2316af1ae4a5fdf1575ab730 Mon Sep 17 00:00:00 2001 From: MajorTanya Date: Sun, 11 Aug 2024 18:51:47 +0200 Subject: [PATCH 1/9] Migrate tracking APIs to DTOs Changes the handling of tracker API responses to be parsed to DTOs instead of doing so "manually" by use of `jsonPrimitive`s and/or `Json.decodeFromString` invocations. This greatly simplifies the API response handling. Renamed constants to SCREAMING_SNAKE_CASE. Largely tried to name the DTOs in a uniform pattern, with the tracker's (short) name at the beginning of file and data class names (ALOAuth instead of OAuth, etc). With these changes, no area of the code base should be using `jsonPrimitive` and/or `Json.decodeFromString` anymore. --- .../tachiyomi/data/track/anilist/Anilist.kt | 13 +- .../data/track/anilist/AnilistApi.kt | 100 ++++---------- .../data/track/anilist/AnilistInterceptor.kt | 10 +- .../data/track/anilist/AnilistModels.kt | 126 ------------------ .../data/track/anilist/AnilistUtils.kt | 45 +++++++ .../data/track/anilist/dto/ALAddManga.kt | 20 +++ .../data/track/anilist/dto/ALFuzzyDate.kt | 21 +++ .../data/track/anilist/dto/ALManga.kt | 75 +++++++++++ .../data/track/anilist/dto/ALOAuth.kt | 17 +++ .../data/track/anilist/dto/ALSearch.kt | 20 +++ .../data/track/anilist/dto/ALSearchItem.kt | 38 ++++++ .../data/track/anilist/dto/ALUser.kt | 26 ++++ .../data/track/anilist/dto/ALUserList.kt | 43 ++++++ .../tachiyomi/data/track/bangumi/Bangumi.kt | 9 +- .../data/track/bangumi/BangumiApi.kt | 100 +++++--------- .../data/track/bangumi/BangumiInterceptor.kt | 26 ++-- .../data/track/bangumi/BangumiModels.kt | 64 --------- .../data/track/bangumi/BangumiUtils.kt | 12 ++ .../bangumi/dto/BangumiCollectionResponse.kt | 28 ++++ .../data/track/bangumi/dto/BangumiOAuth.kt | 25 ++++ .../data/track/bangumi/dto/BangumiSearch.kt | 45 +++++++ .../data/track/bangumi/dto/BangumiUser.kt | 23 ++++ .../tachiyomi/data/track/kitsu/Kitsu.kt | 7 +- .../tachiyomi/data/track/kitsu/KitsuApi.kt | 115 +++++++--------- .../data/track/kitsu/KitsuInterceptor.kt | 10 +- .../tachiyomi/data/track/kitsu/KitsuModels.kt | 121 ----------------- .../tachiyomi/data/track/kitsu/KitsuUtils.kt | 17 +++ .../data/track/kitsu/dto/KitsuAddManga.kt | 13 ++ .../data/track/kitsu/dto/KitsuListSearch.kt | 79 +++++++++++ .../data/track/kitsu/dto/KitsuOAuth.kt | 21 +++ .../data/track/kitsu/dto/KitsuSearch.kt | 56 ++++++++ .../track/kitsu/dto/KitsuSearchItemCover.kt | 8 ++ .../data/track/kitsu/dto/KitsuUser.kt | 13 ++ .../data/track/mangaupdates/MangaUpdates.kt | 6 +- .../track/mangaupdates/MangaUpdatesApi.kt | 90 ++++++------- .../dto/{Context.kt => MUContext.kt} | 2 +- .../mangaupdates/dto/{Image.kt => MUImage.kt} | 4 +- .../dto/{ListItem.kt => MUListItem.kt} | 8 +- .../track/mangaupdates/dto/MULoginResponse.kt | 8 ++ .../dto/{Rating.kt => MURating.kt} | 4 +- .../dto/{Record.kt => MURecord.kt} | 6 +- .../data/track/mangaupdates/dto/MUSearch.kt | 13 ++ .../dto/{Series.kt => MUSeries.kt} | 2 +- .../dto/{Status.kt => MUStatus.kt} | 2 +- .../mangaupdates/dto/{Url.kt => MUUrl.kt} | 2 +- .../data/track/myanimelist/MyAnimeList.kt | 9 +- .../data/track/myanimelist/MyAnimeListApi.kt | 116 +++++++--------- .../myanimelist/MyAnimeListInterceptor.kt | 11 +- ...AnimeListModels.kt => MyAnimeListUtils.kt} | 14 -- .../data/track/myanimelist/dto/MALList.kt | 26 ++++ .../data/track/myanimelist/dto/MALManga.kt | 26 ++++ .../data/track/myanimelist/dto/MALOAuth.kt | 23 ++++ .../data/track/myanimelist/dto/MALSearch.kt | 18 +++ .../data/track/myanimelist/dto/MALUser.kt | 8 ++ .../myanimelist/dto/MALUserListSearch.kt | 25 ++++ .../data/track/shikimori/Shikimori.kt | 9 +- .../data/track/shikimori/ShikimoriApi.kt | 89 ++++--------- .../track/shikimori/ShikimoriInterceptor.kt | 12 +- .../{ShikimoriModels.kt => ShikimoriUtils.kt} | 13 -- .../track/shikimori/dto/SMAddMangaResponse.kt | 8 ++ .../data/track/shikimori/dto/SMManga.kt | 40 ++++++ .../data/track/shikimori/dto/SMOAuth.kt | 22 +++ .../data/track/shikimori/dto/SMUser.kt | 8 ++ .../track/shikimori/dto/SMUserListEntry.kt | 27 ++++ 64 files changed, 1196 insertions(+), 801 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddManga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiCollectionResponse.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiUser.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddManga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchItemCover.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Context.kt => MUContext.kt} (91%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Image.kt => MUImage.kt} (78%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{ListItem.kt => MUListItem.kt} (80%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MULoginResponse.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Rating.kt => MURating.kt} (80%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Record.kt => MURecord.kt} (92%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSearch.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Series.kt => MUSeries.kt} (89%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Status.kt => MUStatus.kt} (89%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Url.kt => MUUrl.kt} (90%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/{MyAnimeListModels.kt => MyAnimeListUtils.kt} (61%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALList.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUser.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/{ShikimoriModels.kt => ShikimoriUtils.kt} (69%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMManga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index abf0d702a1..2cb5623dec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -129,12 +130,14 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { 0.0 -> "0 ★" else -> "${((score + 10) / 20).toInt()} ★" } + POINT_3 -> when { score == 0.0 -> "0" score <= 35 -> "😦" score <= 60 -> "😐" else -> "😊" } + else -> track.toAnilistScore() } } @@ -217,7 +220,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { interceptor.setAuth(oauth) val (username, scoreType) = api.getCurrentUser() scorePreference.set(scoreType) - saveCredentials(username.toString(), oauth.access_token) + saveCredentials(username.toString(), oauth.accessToken) } catch (e: Throwable) { logout() } @@ -229,13 +232,13 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { interceptor.setAuth(null) } - fun saveOAuth(oAuth: OAuth?) { - trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) + fun saveOAuth(alOAuth: ALOAuth?) { + trackPreferences.trackToken(this).set(json.encodeToString(alOAuth)) } - fun loadOAuth(): OAuth? { + fun loadOAuth(): ALOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index e31454e743..6a859c3fb4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -3,6 +3,11 @@ package eu.kanade.tachiyomi.data.track.anilist import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult +import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult +import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth +import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult +import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.awaitSuccess @@ -13,14 +18,6 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.int -import kotlinx.serialization.json.intOrNull -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long -import kotlinx.serialization.json.longOrNull import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.OkHttpClient @@ -28,7 +25,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import tachiyomi.core.common.util.lang.withIOContext import uy.kohesive.injekt.injectLazy import java.time.Instant -import java.time.LocalDate import java.time.ZoneId import java.time.ZonedDateTime import kotlin.time.Duration.Companion.minutes @@ -70,10 +66,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() - .parseAs() + .parseAs() .let { - track.library_id = - it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long + track.library_id = it.data.entry.id track } } @@ -135,6 +130,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { .awaitSuccess() } } + suspend fun search(search: String): List { return withIOContext { val query = """ @@ -177,13 +173,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() - .parseAs() - .let { response -> - val data = response["data"]!!.jsonObject - val page = data["Page"]!!.jsonObject - val media = page["media"]!!.jsonArray - val entries = media.map { jsonToALManga(it.jsonObject) } - entries.map { it.toTrack() } + .parseAs() + .let { + it.data.page.media + .map { item -> item.toALManga().toTrack() } } } } @@ -247,13 +240,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() - .parseAs() - .let { response -> - val data = response["data"]!!.jsonObject - val page = data["Page"]!!.jsonObject - val media = page["mediaList"]!!.jsonArray - val entries = media.map { jsonToALUserManga(it.jsonObject) } - entries.firstOrNull()?.toTrack() + .parseAs() + .let { + it.data.page.mediaList + .map { item -> item.toALUserManga() } + .firstOrNull() + ?.toTrack() } } } @@ -263,8 +255,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { return findLibManga(track, userId) ?: throw Exception("Could not find manga") } - fun createOAuth(token: String): OAuth { - return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) + fun createOAuth(token: String): ALOAuth { + return ALOAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) } suspend fun getCurrentUser(): Pair { @@ -291,61 +283,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() - .parseAs() + .parseAs() .let { - val data = it["data"]!!.jsonObject - val viewer = data["Viewer"]!!.jsonObject - Pair( - viewer["id"]!!.jsonPrimitive.int, - viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content, - ) + val viewer = it.data.viewer + Pair(viewer.id, viewer.mediaListOptions.scoreFormat) } } } } - private fun jsonToALManga(struct: JsonObject): ALManga { - return ALManga( - struct["id"]!!.jsonPrimitive.long, - struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content, - struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content, - struct["description"]!!.jsonPrimitive.contentOrNull, - struct["format"]!!.jsonPrimitive.content.replace("_", "-"), - struct["status"]!!.jsonPrimitive.contentOrNull ?: "", - parseDate(struct, "startDate"), - struct["chapters"]!!.jsonPrimitive.longOrNull ?: 0, - struct["averageScore"]?.jsonPrimitive?.intOrNull ?: -1, - ) - } - - private fun jsonToALUserManga(struct: JsonObject): ALUserManga { - return ALUserManga( - struct["id"]!!.jsonPrimitive.long, - struct["status"]!!.jsonPrimitive.content, - struct["scoreRaw"]!!.jsonPrimitive.int, - struct["progress"]!!.jsonPrimitive.int, - parseDate(struct, "startedAt"), - parseDate(struct, "completedAt"), - jsonToALManga(struct["media"]!!.jsonObject), - ) - } - - private fun parseDate(struct: JsonObject, dateKey: String): Long { - return try { - return LocalDate - .of( - struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int, - struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int, - struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int, - ) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant() - .toEpochMilli() - } catch (_: Exception) { - 0L - } - } - private fun createDate(dateValue: Long): JsonObject { if (dateValue == 0L) { return buildJsonObject { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index 388b3e1b52..b23179f6ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.anilist import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth +import eu.kanade.tachiyomi.data.track.anilist.dto.isExpired import okhttp3.Interceptor import okhttp3.Response import java.io.IOException @@ -13,7 +15,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute * before its original expiration date. */ - private var oauth: OAuth? = null + private var oauth: ALOAuth? = null set(value) { field = value?.copy(expires = value.expires * 1000 - 60 * 1000) } @@ -40,7 +42,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int // Add the authorization header to the original request. val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .addHeader("Authorization", "Bearer ${oauth!!.accessToken}") .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() @@ -51,8 +53,8 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int * Called when the user authenticates with Anilist for the first time. Sets the refresh token * and the oauth object. */ - fun setAuth(oauth: OAuth?) { - token = oauth?.access_token + fun setAuth(oauth: ALOAuth?) { + token = oauth?.accessToken this.oauth = oauth anilist.saveOAuth(oauth) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt deleted file mode 100644 index d7c037afe6..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ /dev/null @@ -1,126 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist - -import eu.kanade.domain.track.service.TrackPreferences -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackerManager -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.util.lang.htmlDecode -import kotlinx.serialization.Serializable -import uy.kohesive.injekt.injectLazy -import java.text.SimpleDateFormat -import java.util.Locale -import tachiyomi.domain.track.model.Track as DomainTrack - -data class ALManga( - val remote_id: Long, - val title_user_pref: String, - val image_url_lge: String, - val description: String?, - val format: String, - val publishing_status: String, - val start_date_fuzzy: Long, - val total_chapters: Long, - val average_score: Int, -) { - - fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply { - remote_id = this@ALManga.remote_id - title = title_user_pref - total_chapters = this@ALManga.total_chapters - cover_url = image_url_lge - summary = description?.htmlDecode() ?: "" - score = average_score.toDouble() - tracking_url = AnilistApi.mangaUrl(remote_id) - publishing_status = this@ALManga.publishing_status - publishing_type = format - if (start_date_fuzzy != 0L) { - start_date = try { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(start_date_fuzzy) - } catch (e: Exception) { - "" - } - } - } -} - -data class ALUserManga( - val library_id: Long, - val list_status: String, - val score_raw: Int, - val chapters_read: Int, - val start_date_fuzzy: Long, - val completed_date_fuzzy: Long, - val manga: ALManga, -) { - - fun toTrack() = Track.create(TrackerManager.ANILIST).apply { - remote_id = manga.remote_id - title = manga.title_user_pref - status = toTrackStatus() - score = score_raw.toDouble() - started_reading_date = start_date_fuzzy - finished_reading_date = completed_date_fuzzy - last_chapter_read = chapters_read.toDouble() - library_id = this@ALUserManga.library_id - total_chapters = manga.total_chapters - } - - private fun toTrackStatus() = when (list_status) { - "CURRENT" -> Anilist.READING - "COMPLETED" -> Anilist.COMPLETED - "PAUSED" -> Anilist.ON_HOLD - "DROPPED" -> Anilist.DROPPED - "PLANNING" -> Anilist.PLAN_TO_READ - "REPEATING" -> Anilist.REREADING - else -> throw NotImplementedError("Unknown status: $list_status") - } -} - -@Serializable -data class OAuth( - val access_token: String, - val token_type: String, - val expires: Long, - val expires_in: Long, -) - -fun OAuth.isExpired() = System.currentTimeMillis() > expires - -fun Track.toAnilistStatus() = when (status) { - Anilist.READING -> "CURRENT" - Anilist.COMPLETED -> "COMPLETED" - Anilist.ON_HOLD -> "PAUSED" - Anilist.DROPPED -> "DROPPED" - Anilist.PLAN_TO_READ -> "PLANNING" - Anilist.REREADING -> "REPEATING" - else -> throw NotImplementedError("Unknown status: $status") -} - -private val preferences: TrackPreferences by injectLazy() - -fun DomainTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) { - // 10 point - "POINT_10" -> (score.toInt() / 10).toString() - // 100 point - "POINT_100" -> score.toInt().toString() - // 5 stars - "POINT_5" -> when { - score == 0.0 -> "0" - score < 30 -> "1" - score < 50 -> "2" - score < 70 -> "3" - score < 90 -> "4" - else -> "5" - } - // Smiley - "POINT_3" -> when { - score == 0.0 -> "0" - score <= 35 -> ":(" - score <= 60 -> ":|" - else -> ":)" - } - // 10 point decimal - "POINT_10_DECIMAL" -> (score / 10).toString() - else -> throw NotImplementedError("Unknown score type") -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt new file mode 100644 index 0000000000..6f69565115 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.data.track.anilist + +import eu.kanade.domain.track.service.TrackPreferences +import eu.kanade.tachiyomi.data.database.models.Track +import uy.kohesive.injekt.injectLazy +import tachiyomi.domain.track.model.Track as DomainTrack + +fun Track.toAnilistStatus() = when (status) { + Anilist.READING -> "CURRENT" + Anilist.COMPLETED -> "COMPLETED" + Anilist.ON_HOLD -> "PAUSED" + Anilist.DROPPED -> "DROPPED" + Anilist.PLAN_TO_READ -> "PLANNING" + Anilist.REREADING -> "REPEATING" + else -> throw NotImplementedError("Unknown status: $status") +} + +private val preferences: TrackPreferences by injectLazy() + +@Suppress("MagicNumber", "CyclomaticComplexMethod") +fun DomainTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) { + // 10 point + "POINT_10" -> (score.toInt() / 10).toString() + // 100 point + "POINT_100" -> score.toInt().toString() + // 5 stars + "POINT_5" -> when { + score == 0.0 -> "0" + score < 30 -> "1" + score < 50 -> "2" + score < 70 -> "3" + score < 90 -> "4" + else -> "5" + } + // Smiley + "POINT_3" -> when { + score == 0.0 -> "0" + score <= 35 -> ":(" + score <= 60 -> ":|" + else -> ":)" + } + // 10 point decimal + "POINT_10_DECIMAL" -> (score / 10).toString() + else -> throw NotImplementedError("Unknown score type") +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddManga.kt new file mode 100644 index 0000000000..a552e7c1df --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddManga.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALAddMangaResult( + val data: ALAddMangaData, +) + +@Serializable +data class ALAddMangaData( + @SerialName("SaveMediaListEntry") + val entry: ALAddMangaEntry, +) + +@Serializable +data class ALAddMangaEntry( + val id: Long, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt new file mode 100644 index 0000000000..7dbd8c2965 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.Serializable +import java.time.LocalDate +import java.time.ZoneId + +@Serializable +data class ALFuzzyDate( + val year: Int?, + val month: Int?, + val day: Int?, +) { + fun toEpochMilli(): Long = try { + LocalDate.of(year!!, month!!, day!!) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() + } catch (_: Exception) { + 0L + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt new file mode 100644 index 0000000000..24eef984e1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt @@ -0,0 +1,75 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackerManager +import eu.kanade.tachiyomi.data.track.anilist.Anilist +import eu.kanade.tachiyomi.data.track.anilist.AnilistApi +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.util.lang.htmlDecode +import java.text.SimpleDateFormat +import java.util.Locale + +data class ALManga( + val remoteId: Long, + val title: String, + val imageUrl: String, + val description: String?, + val format: String, + val publishingStatus: String, + val startDateFuzzy: Long, + val totalChapters: Long, + val averageScore: Int, +) { + @Suppress("SwallowedException") + fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply { + remote_id = remoteId + title = this@ALManga.title + total_chapters = totalChapters + cover_url = imageUrl + summary = description?.htmlDecode() ?: "" + score = averageScore.toDouble() + tracking_url = AnilistApi.mangaUrl(remote_id) + publishing_status = publishingStatus + publishing_type = format + if (startDateFuzzy != 0L) { + start_date = try { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(startDateFuzzy) + } catch (e: IllegalArgumentException) { + "" + } + } + } +} + +data class ALUserManga( + val libraryId: Long, + val listStatus: String, + val scoreRaw: Int, + val chaptersRead: Int, + val startDateFuzzy: Long, + val completedDateFuzzy: Long, + val manga: ALManga, +) { + fun toTrack() = Track.create(TrackerManager.ANILIST).apply { + remote_id = manga.remoteId + title = manga.title + status = toTrackStatus() + score = scoreRaw.toDouble() + started_reading_date = startDateFuzzy + finished_reading_date = completedDateFuzzy + last_chapter_read = chaptersRead.toDouble() + library_id = libraryId + total_chapters = manga.totalChapters + } + + private fun toTrackStatus() = when (listStatus) { + "CURRENT" -> Anilist.READING + "COMPLETED" -> Anilist.COMPLETED + "PAUSED" -> Anilist.ON_HOLD + "DROPPED" -> Anilist.DROPPED + "PLANNING" -> Anilist.PLAN_TO_READ + "REPEATING" -> Anilist.REREADING + else -> throw NotImplementedError("Unknown status: $listStatus") + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALOAuth.kt new file mode 100644 index 0000000000..94fbd64000 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALOAuth.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + val expires: Long, + @SerialName("expires_in") + val expiresIn: Long, +) + +fun ALOAuth.isExpired() = System.currentTimeMillis() > expires diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt new file mode 100644 index 0000000000..f13ebb4007 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALSearchResult( + val data: ALSearchPage, +) + +@Serializable +data class ALSearchPage( + @SerialName("Page") + val page: ALSearchMedia, +) + +@Serializable +data class ALSearchMedia( + val media: List, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt new file mode 100644 index 0000000000..49c8df9381 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ALSearchItem( + val id: Long, + val title: ALItemTitle, + val coverImage: ItemCover, + val description: String?, + val format: String, + val status: String = "", + val startDate: ALFuzzyDate, + val chapters: Long?, + val averageScore: Int?, +) { + fun toALManga(): ALManga = ALManga( + remoteId = id, + title = title.userPreferred, + imageUrl = coverImage.large, + description = description, + format = format.replace("_", "-"), + publishingStatus = status, + startDateFuzzy = startDate.toEpochMilli(), + totalChapters = chapters ?: 0, + averageScore = averageScore ?: -1, + ) +} + +@Serializable +data class ALItemTitle( + val userPreferred: String, +) + +@Serializable +data class ItemCover( + val large: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt new file mode 100644 index 0000000000..39507a0d5f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALCurrentUserResult( + val data: ALUserViewer, +) + +@Serializable +data class ALUserViewer( + @SerialName("Viewer") + val viewer: ALUserViewerData, +) + +@Serializable +data class ALUserViewerData( + val id: Int, + val mediaListOptions: ALUserListOptions, +) + +@Serializable +data class ALUserListOptions( + val scoreFormat: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt new file mode 100644 index 0000000000..4ccec7aa43 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALUserListMangaQueryResult( + val data: ALUserListMangaPage, +) + +@Serializable +data class ALUserListMangaPage( + @SerialName("Page") + val page: ALUserListMediaList, +) + +@Serializable +data class ALUserListMediaList( + val mediaList: List, +) + +@Serializable +data class ALUserListItem( + val id: Long, + val status: String, + val scoreRaw: Int, + val progress: Int, + val startedAt: ALFuzzyDate, + val completedAt: ALFuzzyDate, + val media: ALSearchItem, +) { + fun toALUserManga(): ALUserManga { + return ALUserManga( + libraryId = this@ALUserListItem.id, + listStatus = status, + scoreRaw = scoreRaw, + chaptersRead = progress, + startDateFuzzy = startedAt.toEpochMilli(), + completedDateFuzzy = completedAt.toEpochMilli(), + manga = media.toALManga(), + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index b8e7d2acc0..472b5a6c9a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -5,6 +5,7 @@ import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker +import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiOAuth import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -112,19 +113,19 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { try { val oauth = api.accessToken(code) interceptor.newAuth(oauth) - saveCredentials(oauth.user_id.toString(), oauth.access_token) + saveCredentials(oauth.userId.toString(), oauth.accessToken) } catch (e: Throwable) { logout() } } - fun saveToken(oauth: OAuth?) { + fun saveToken(oauth: BangumiOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { + fun restoreToken(): BangumiOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index c48fa24905..6803f22f37 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -3,20 +3,16 @@ package eu.kanade.tachiyomi.data.track.bangumi import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiCollectionResponse +import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiOAuth +import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiSearchItem +import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiSearchResult import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.doubleOrNull -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import okhttp3.CacheControl import okhttp3.FormBody import okhttp3.OkHttpClient @@ -63,16 +59,14 @@ class BangumiApi( .add("watched_eps", track.last_chapter_read.toInt().toString()) .build() authClient.newCall( - POST( - "$API_URL/subject/${track.remote_id}/update/watched_eps", - body = body, - ), + POST("$API_URL/subject/${track.remote_id}/update/watched_eps", body = body), ).awaitSuccess() track } } + @Suppress("MagicNumber") suspend fun search(search: String): List { return withIOContext { val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}" @@ -80,44 +74,19 @@ class BangumiApi( .buildUpon() .appendQueryParameter("max_results", "20") .build() - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .use { - var responseBody = it.body.string() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - if (responseBody.contains("\"code\":404")) { - responseBody = "{\"results\":0,\"list\":[]}" + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { result -> + if (result.code == 404) emptyList() + + result.list + ?.filter { it.type == 1 } + ?.map { it.toTrackSearch(trackId) } + .orEmpty() } - val response = json.decodeFromString(responseBody)["list"]?.jsonArray - response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 } - ?.map { jsonToSearch(it.jsonObject) }.orEmpty() - } - } - } - - private fun jsonToSearch(obj: JsonObject): TrackSearch { - val coverUrl = if (obj["images"] is JsonObject) { - obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: "" - } else { - // Sometimes JsonNull - "" - } - val totalChapters = if (obj["eps_count"] != null) { - obj["eps_count"]!!.jsonPrimitive.long - } else { - 0 - } - val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.doubleOrNull ?: -1.0 - return TrackSearch.create(trackId).apply { - remote_id = obj["id"]!!.jsonPrimitive.long - title = obj["name_cn"]!!.jsonPrimitive.content - cover_url = coverUrl - summary = obj["name"]!!.jsonPrimitive.content - score = rating - tracking_url = obj["url"]!!.jsonPrimitive.content - total_chapters = totalChapters + } } } @@ -126,12 +95,13 @@ class BangumiApi( with(json) { authClient.newCall(GET("$API_URL/subject/${track.remote_id}")) .awaitSuccess() - .parseAs() - .let { jsonToSearch(it) } + .parseAs() + .toTrackSearch(trackId) } } } + @Suppress("MagicNumber") suspend fun statusLibManga(track: Track): Track? { return withIOContext { val urlUserRead = "$API_URL/collection/${track.remote_id}" @@ -142,25 +112,23 @@ class BangumiApi( .build() // TODO: get user readed chapter here - val response = authClient.newCall(requestUserRead).awaitSuccess() - val responseBody = response.body.string() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - if (responseBody.contains("\"code\":400")) { - null - } else { - json.decodeFromString(responseBody).let { - track.status = it.status?.id!! - track.last_chapter_read = it.ep_status!!.toDouble() - track.score = it.rating!! - track - } + with(json) { + authClient.newCall(requestUserRead) + .awaitSuccess() + .parseAs() + .let { + if (it.code == 400) return@let null + + track.status = it.status?.id!! + track.last_chapter_read = it.epStatus!!.toDouble() + track.score = it.rating!! + track + } } } } - suspend fun accessToken(code: String): OAuth { + suspend fun accessToken(code: String): BangumiOAuth { return withIOContext { with(json) { client.newCall(accessTokenRequest(code)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt index a1822cca06..1c23a8ec83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.bangumi import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiOAuth +import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired import kotlinx.serialization.json.Json import okhttp3.FormBody import okhttp3.Interceptor @@ -14,7 +16,7 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { /** * OAuth object used for authenticated requests. */ - private var oauth: OAuth? = bangumi.restoreToken() + private var oauth: BangumiOAuth? = bangumi.restoreToken() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() @@ -22,9 +24,9 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi") if (currAuth.isExpired()) { - val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!)) + val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!)) if (response.isSuccessful) { - newAuth(json.decodeFromString(response.body.string())) + newAuth(json.decodeFromString(response.body.string())) } else { response.close() } @@ -38,28 +40,28 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { .apply { if (originalRequest.method == "GET") { val newUrl = originalRequest.url.newBuilder() - .addQueryParameter("access_token", currAuth.access_token) + .addQueryParameter("access_token", currAuth.accessToken) .build() url(newUrl) } else { - post(addToken(currAuth.access_token, originalRequest.body as FormBody)) + post(addToken(currAuth.accessToken, originalRequest.body as FormBody)) } } .build() .let(chain::proceed) } - fun newAuth(oauth: OAuth?) { + fun newAuth(oauth: BangumiOAuth?) { this.oauth = if (oauth == null) { null } else { - OAuth( - oauth.access_token, - oauth.token_type, + BangumiOAuth( + oauth.accessToken, + oauth.tokenType, System.currentTimeMillis() / 1000, - oauth.expires_in, - oauth.refresh_token, - this.oauth?.user_id, + oauth.expiresIn, + oauth.refreshToken, + this.oauth?.userId, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt deleted file mode 100644 index c4b1aeed74..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt +++ /dev/null @@ -1,64 +0,0 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -import eu.kanade.tachiyomi.data.database.models.Track -import kotlinx.serialization.Serializable - -@Serializable -data class Avatar( - val large: String? = "", - val medium: String? = "", - val small: String? = "", -) - -@Serializable -data class Collection( - val `private`: Int? = 0, - val comment: String? = "", - val ep_status: Int? = 0, - val lasttouch: Int? = 0, - val rating: Double? = 0.0, - val status: Status? = Status(), - val tag: List? = emptyList(), - val user: User? = User(), - val vol_status: Int? = 0, -) - -@Serializable -data class Status( - val id: Long? = 0, - val name: String? = "", - val type: String? = "", -) - -@Serializable -data class User( - val avatar: Avatar? = Avatar(), - val id: Int? = 0, - val nickname: String? = "", - val sign: String? = "", - val url: String? = "", - val usergroup: Int? = 0, - val username: String? = "", -) - -@Serializable -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long = System.currentTimeMillis() / 1000, - val expires_in: Long, - val refresh_token: String?, - val user_id: Long?, -) - -// Access token refresh before expired -fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) - -fun Track.toBangumiStatus() = when (status) { - Bangumi.READING -> "do" - Bangumi.COMPLETED -> "collect" - Bangumi.ON_HOLD -> "on_hold" - Bangumi.DROPPED -> "dropped" - Bangumi.PLAN_TO_READ -> "wish" - else -> throw NotImplementedError("Unknown status: $status") -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt new file mode 100644 index 0000000000..a10e727538 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt @@ -0,0 +1,12 @@ +package eu.kanade.tachiyomi.data.track.bangumi + +import eu.kanade.tachiyomi.data.database.models.Track + +fun Track.toBangumiStatus() = when (status) { + Bangumi.READING -> "do" + Bangumi.COMPLETED -> "collect" + Bangumi.ON_HOLD -> "on_hold" + Bangumi.DROPPED -> "dropped" + Bangumi.PLAN_TO_READ -> "wish" + else -> throw NotImplementedError("Unknown status: $status") +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiCollectionResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiCollectionResponse.kt new file mode 100644 index 0000000000..8484775e00 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiCollectionResponse.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BangumiCollectionResponse( + val code: Int?, + val `private`: Int? = 0, + val comment: String? = "", + @SerialName("ep_status") + val epStatus: Int? = 0, + @SerialName("lasttouch") + val lastTouch: Int? = 0, + val rating: Double? = 0.0, + val status: Status? = Status(), + val tag: List? = emptyList(), + val user: User? = User(), + @SerialName("vol_status") + val volStatus: Int? = 0, +) + +@Serializable +data class Status( + val id: Long? = 0, + val name: String? = "", + val type: String? = "", +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiOAuth.kt new file mode 100644 index 0000000000..846a16dd5a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiOAuth.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BangumiOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + @Suppress("MagicNumber") + @SerialName("created_at") + val createdAt: Long = System.currentTimeMillis() / 1000, + @SerialName("expires_in") + val expiresIn: Long, + @SerialName("refresh_token") + val refreshToken: String?, + @SerialName("user_id") + val userId: Long?, +) + +// Access token refresh before expired +@Suppress("MagicNumber") +fun BangumiOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiSearch.kt new file mode 100644 index 0000000000..a2d7058c59 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiSearch.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BangumiSearchResult( + val list: List?, + val code: Int?, +) + +@Serializable +data class BangumiSearchItem( + val id: Long, + @SerialName("name_cn") + val nameCn: String, + val name: String, + val type: Int, + val images: BangumiSearchItemCovers?, + @SerialName("eps_count") + val epsCount: Long?, + val rating: BangumiSearchItemRating?, + val url: String, +) { + fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply { + remote_id = this@BangumiSearchItem.id + title = nameCn + cover_url = images?.common ?: "" + summary = this@BangumiSearchItem.name + score = rating?.score ?: -1.0 + tracking_url = url + total_chapters = epsCount ?: 0 + } +} + +@Serializable +data class BangumiSearchItemCovers( + val common: String?, +) + +@Serializable +data class BangumiSearchItemRating( + val score: Double?, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiUser.kt new file mode 100644 index 0000000000..375c39eb64 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiUser.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Avatar( + val large: String? = "", + val medium: String? = "", + val small: String? = "", +) + +@Serializable +data class User( + val avatar: Avatar? = Avatar(), + val id: Int? = 0, + val nickname: String? = "", + val sign: String? = "", + val url: String? = "", + @SerialName("usergroup") + val userGroup: Int? = 0, + val username: String? = "", +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index 4b0db8bce9..f0ca4e7204 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -142,13 +143,13 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { return getPassword() } - fun saveToken(oauth: OAuth?) { + fun saveToken(oauth: KitsuOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { + fun restoreToken(): KitsuOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index c124ddd138..abf205bec4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -2,6 +2,12 @@ package eu.kanade.tachiyomi.data.track.kitsu import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAddMangaResult +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAlgoliaSearchResult +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentUserResult +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuListSearchResult +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET @@ -10,12 +16,7 @@ import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.FormBody @@ -67,18 +68,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) authClient.newCall( POST( "${BASE_URL}library-entries", - headers = headersOf( - "Content-Type", - "application/vnd.api+json", - ), - body = data.toString() - .toRequestBody("application/vnd.api+json".toMediaType()), + headers = headersOf("Content-Type", VND_API_JSON), + body = data.toString().toRequestBody(VND_JSON_MEDIA_TYPE), ), ) .awaitSuccess() - .parseAs() + .parseAs() .let { - track.remote_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long + track.remote_id = it.data.id track } } @@ -101,54 +98,42 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } } - with(json) { - authClient.newCall( - Request.Builder() - .url("${BASE_URL}library-entries/${track.remote_id}") - .headers( - headersOf( - "Content-Type", - "application/vnd.api+json", - ), - ) - .patch( - data.toString().toRequestBody("application/vnd.api+json".toMediaType()), - ) - .build(), - ) - .awaitSuccess() - .parseAs() - .let { - track - } - } + authClient.newCall( + Request.Builder() + .url("${BASE_URL}library-entries/${track.remote_id}") + .headers( + headersOf("Content-Type", VND_API_JSON), + ) + .patch(data.toString().toRequestBody(VND_JSON_MEDIA_TYPE)) + .build(), + ) + .awaitSuccess() + .let { + track + } } } suspend fun removeLibManga(track: DomainTrack) { withIOContext { - authClient - .newCall( - DELETE( - "${BASE_URL}library-entries/${track.remoteId}", - headers = headersOf( - "Content-Type", - "application/vnd.api+json", - ), - ), - ) + authClient.newCall( + DELETE( + "${BASE_URL}library-entries/${track.remoteId}", + headers = headersOf("Content-Type", VND_API_JSON), + ), + ) .awaitSuccess() } } + suspend fun search(query: String): List { return withIOContext { with(json) { authClient.newCall(GET(ALGOLIA_KEY_URL)) .awaitSuccess() - .parseAs() + .parseAs() .let { - val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content - algoliaSearch(key, query) + algoliaSearch(it.media.key, query) } } } @@ -174,11 +159,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) ), ) .awaitSuccess() - .parseAs() - .let { - it["hits"]!!.jsonArray - .map { KitsuSearchManga(it.jsonObject) } - .filter { it.subType != "novel" } + .parseAs() + .let { result -> + result.hits + .filter { it.subtype != "novel" } .map { it.toTrack() } } } @@ -194,12 +178,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - val data = it["data"]!!.jsonArray - if (data.size > 0) { - val manga = it["included"]!!.jsonArray[0].jsonObject - KitsuLibManga(data[0].jsonObject, manga).toTrack() + if (it.data.isNotEmpty()) { + it.firstToTrack() } else { null } @@ -217,12 +199,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - val data = it["data"]!!.jsonArray - if (data.size > 0) { - val manga = it["included"]!!.jsonArray[0].jsonObject - KitsuLibManga(data[0].jsonObject, manga).toTrack() + if (it.data.isNotEmpty()) { + it.firstToTrack() } else { throw Exception("Could not find manga") } @@ -231,7 +211,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } } - suspend fun login(username: String, password: String): OAuth { + suspend fun login(username: String, password: String): KitsuOAuth { return withIOContext { val formBody: RequestBody = FormBody.Builder() .add("username", username) @@ -256,10 +236,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { - it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content - } + .parseAs() + .let { it.data[0].id } } } } @@ -279,6 +257,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) "%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" + "posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" + private const val VND_API_JSON = "application/vnd.api+json" + private val VND_JSON_MEDIA_TYPE = VND_API_JSON.toMediaType() + fun mangaUrl(remoteId: Long): String { return BASE_MANGA_URL + remoteId } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt index da9aff7fc4..0f7c1b7db1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.kitsu import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth +import eu.kanade.tachiyomi.data.track.kitsu.dto.isExpired import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response @@ -13,14 +15,14 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor { /** * OAuth object used for authenticated requests. */ - private var oauth: OAuth? = kitsu.restoreToken() + private var oauth: KitsuOAuth? = kitsu.restoreToken() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu") - val refreshToken = currAuth.refresh_token!! + val refreshToken = currAuth.refreshToken!! // Refresh access token if expired. if (currAuth.isExpired()) { @@ -34,7 +36,7 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor { // Add the authorization header to the original request. val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .addHeader("Authorization", "Bearer ${oauth!!.accessToken}") .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .header("Accept", "application/vnd.api+json") .header("Content-Type", "application/vnd.api+json") @@ -43,7 +45,7 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor { return chain.proceed(authRequest) } - fun newAuth(oauth: OAuth?) { + fun newAuth(oauth: KitsuOAuth?) { this.oauth = oauth kitsu.saveToken(oauth) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt deleted file mode 100644 index 752ef4e125..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ /dev/null @@ -1,121 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu - -import androidx.annotation.CallSuper -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackerManager -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long -import kotlinx.serialization.json.longOrNull -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -class KitsuSearchManga(obj: JsonObject) { - val id = obj["id"]!!.jsonPrimitive.long - private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content - private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.longOrNull - val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull - val original = try { - obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content - } catch (e: IllegalArgumentException) { - // posterImage is sometimes a jsonNull object instead - null - } - private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull - private val rating = obj["averageRating"]?.jsonPrimitive?.contentOrNull?.toDoubleOrNull() - private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(Date(it.toLong() * 1000)) - } - private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull - - @CallSuper - fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply { - remote_id = this@KitsuSearchManga.id - title = canonicalTitle - total_chapters = chapterCount ?: 0 - cover_url = original ?: "" - summary = synopsis ?: "" - tracking_url = KitsuApi.mangaUrl(remote_id) - score = rating ?: -1.0 - publishing_status = if (endDate == null) { - "Publishing" - } else { - "Finished" - } - publishing_type = subType ?: "" - start_date = startDate ?: "" - } -} - -class KitsuLibManga(obj: JsonObject, manga: JsonObject) { - val id = manga["id"]!!.jsonPrimitive.int - private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content - private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.longOrNull - val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty() - val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content - private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content - private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty() - private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull - private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull - private val libraryId = obj["id"]!!.jsonPrimitive.long - val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content - private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull - val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int - - fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply { - remote_id = libraryId - title = canonicalTitle - total_chapters = chapterCount ?: 0 - cover_url = original - summary = synopsis - tracking_url = KitsuApi.mangaUrl(remote_id) - publishing_status = this@KitsuLibManga.status - publishing_type = type - start_date = startDate - started_reading_date = KitsuDateHelper.parse(startedAt) - finished_reading_date = KitsuDateHelper.parse(finishedAt) - status = toTrackStatus() - score = ratingTwenty?.let { it.toInt() / 2.0 } ?: 0.0 - last_chapter_read = progress.toDouble() - } - - private fun toTrackStatus() = when (status) { - "current" -> Kitsu.READING - "completed" -> Kitsu.COMPLETED - "on_hold" -> Kitsu.ON_HOLD - "dropped" -> Kitsu.DROPPED - "planned" -> Kitsu.PLAN_TO_READ - else -> throw Exception("Unknown status") - } -} - -@Serializable -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?, -) - -fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) - -fun Track.toKitsuStatus() = when (status) { - Kitsu.READING -> "current" - Kitsu.COMPLETED -> "completed" - Kitsu.ON_HOLD -> "on_hold" - Kitsu.DROPPED -> "dropped" - Kitsu.PLAN_TO_READ -> "planned" - else -> throw Exception("Unknown status") -} - -fun Track.toKitsuScore(): String? { - return if (score > 0) (score * 2).toInt().toString() else null -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt new file mode 100644 index 0000000000..98e4e70e39 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.track.kitsu + +import eu.kanade.tachiyomi.data.database.models.Track + +@Suppress("TooGenericExceptionThrown") +fun Track.toKitsuStatus() = when (status) { + Kitsu.READING -> "current" + Kitsu.COMPLETED -> "completed" + Kitsu.ON_HOLD -> "on_hold" + Kitsu.DROPPED -> "dropped" + Kitsu.PLAN_TO_READ -> "planned" + else -> throw Exception("Unknown status") +} + +fun Track.toKitsuScore(): String? { + return if (score > 0) (score * 2).toInt().toString() else null +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddManga.kt new file mode 100644 index 0000000000..9ddec35e9f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddManga.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuAddMangaResult( + val data: KitsuAddMangaItem, +) + +@Serializable +data class KitsuAddMangaItem( + val id: Long, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt new file mode 100644 index 0000000000..ab66f8fa6a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt @@ -0,0 +1,79 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import eu.kanade.tachiyomi.data.track.TrackerManager +import eu.kanade.tachiyomi.data.track.kitsu.Kitsu +import eu.kanade.tachiyomi.data.track.kitsu.KitsuApi +import eu.kanade.tachiyomi.data.track.kitsu.KitsuDateHelper +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuListSearchResult( + val data: List, + val included: List, +) { + @Suppress("CyclomaticComplexMethod", "TooGenericExceptionThrown") + fun firstToTrack(): TrackSearch { + require(data.isNotEmpty()) { "Missing data from Kitsu" } + + val userData = data[0] + val userDataAttrs = userData.attributes + val manga = included[0].attributes + + return TrackSearch.create(TrackerManager.KITSU).apply { + remote_id = userData.id + title = manga.canonicalTitle + total_chapters = manga.chapterCount ?: 0 + cover_url = manga.posterImage?.original ?: "" + summary = manga.synopsis + tracking_url = KitsuApi.mangaUrl(remote_id) + publishing_status = manga.status + publishing_type = manga.mangaType ?: "" + start_date = userDataAttrs.startedAt ?: "" + started_reading_date = KitsuDateHelper.parse(userDataAttrs.startedAt) + finished_reading_date = KitsuDateHelper.parse(userDataAttrs.finishedAt) + status = when (userDataAttrs.status) { + "current" -> Kitsu.READING + "completed" -> Kitsu.COMPLETED + "on_hold" -> Kitsu.ON_HOLD + "dropped" -> Kitsu.DROPPED + "planned" -> Kitsu.PLAN_TO_READ + else -> throw Exception("Unknown status") + } + score = userDataAttrs.ratingTwenty?.let { it.toInt() / 2.0 } ?: 0.0 + last_chapter_read = userDataAttrs.progress.toDouble() + } + } +} + +@Serializable +data class KitsuListSearchItemData( + val id: Long, + val attributes: KitsuListSearchItemDataAttributes, +) + +@Serializable +data class KitsuListSearchItemDataAttributes( + val status: String, + val startedAt: String?, + val finishedAt: String?, + val ratingTwenty: String?, + val progress: Int, +) + +@Serializable +data class KitsuListSearchItemIncluded( + val id: Long, + val attributes: KitsuListSearchItemIncludedAttributes, +) + +@Serializable +data class KitsuListSearchItemIncludedAttributes( + val canonicalTitle: String, + val chapterCount: Long?, + val mangaType: String?, + val posterImage: KitsuSearchItemCover?, + val synopsis: String, + val startDate: String?, + val status: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt new file mode 100644 index 0000000000..2aad9939f1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + @SerialName("created_at") + val createdAt: Long, + @SerialName("expires_in") + val expiresIn: Long, + @SerialName("refresh_token") + val refreshToken: String?, +) + +@Suppress("MagicNumber") +fun KitsuOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt new file mode 100644 index 0000000000..0deefed2f8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import eu.kanade.tachiyomi.data.track.TrackerManager +import eu.kanade.tachiyomi.data.track.kitsu.KitsuApi +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Serializable +data class KitsuSearchResult( + val media: KitsuSearchResultData, +) + +@Serializable +data class KitsuSearchResultData( + val key: String, +) + +@Serializable +data class KitsuAlgoliaSearchResult( + val hits: List, +) + +@Serializable +data class KitsuAlgoliaSearchItem( + val id: Long, + val canonicalTitle: String, + val chapterCount: Long?, + val subtype: String?, + val posterImage: KitsuSearchItemCover?, + val synopsis: String?, + val averageRating: String?, + val startDate: String?, + val endDate: String?, +) { + @Suppress("MagicNumber") + fun toTrack(): TrackSearch { + return TrackSearch.create(TrackerManager.KITSU).apply { + remote_id = this@KitsuAlgoliaSearchItem.id + title = canonicalTitle + total_chapters = chapterCount ?: 0 + cover_url = posterImage?.original ?: "" + summary = synopsis ?: "" + tracking_url = KitsuApi.mangaUrl(remote_id) + score = averageRating?.toDoubleOrNull() ?: -1.0 + publishing_status = if (endDate == null) "Publishing" else "Finished" + publishing_type = subtype ?: "" + start_date = startDate?.let { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(Date(it.toLong() * 1000)) + } ?: "" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchItemCover.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchItemCover.kt new file mode 100644 index 0000000000..6c062bf527 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchItemCover.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuSearchItemCover( + val original: String?, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt new file mode 100644 index 0000000000..d023376511 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuCurrentUserResult( + val data: List, +) + +@Serializable +data class KitsuUser( + val id: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt index 6219e728b6..c47a799a77 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt @@ -6,8 +6,8 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableTracker -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch @@ -106,7 +106,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker return track.copyFrom(series, rating) } - private fun Track.copyFrom(item: ListItem, rating: Rating?): Track = apply { + private fun Track.copyFrom(item: MUListItem, rating: MURating?): Track = apply { item.copyTo(this) score = rating?.rating ?: 0.0 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt index 5da3b72224..77b449c6b4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt @@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.data.track.mangaupdates import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.WISH_LIST -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Context -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Record +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUContext +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MULoginResponse +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURecord +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUSearchResult import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -14,21 +16,15 @@ import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.add import kotlinx.serialization.json.addJsonObject import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject -import logcat.LogPriority import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody -import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.injectLazy import tachiyomi.domain.track.model.Track as DomainTrack @@ -38,20 +34,17 @@ class MangaUpdatesApi( ) { private val json: Json by injectLazy() - private val baseUrl = "https://api.mangaupdates.com" - private val contentType = "application/vnd.api+json".toMediaType() - private val authClient by lazy { client.newBuilder() .addInterceptor(interceptor) .build() } - suspend fun getSeriesListItem(track: Track): Pair { + suspend fun getSeriesListItem(track: Track): Pair { val listItem = with(json) { - authClient.newCall(GET("$baseUrl/v1/lists/series/${track.remote_id}")) + authClient.newCall(GET("$BASE_URL/v1/lists/series/${track.remote_id}")) .awaitSuccess() - .parseAs() + .parseAs() } val rating = getSeriesRating(track) @@ -71,8 +64,8 @@ class MangaUpdatesApi( } authClient.newCall( POST( - url = "$baseUrl/v1/lists/series", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/lists/series", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() @@ -98,8 +91,8 @@ class MangaUpdatesApi( } authClient.newCall( POST( - url = "$baseUrl/v1/lists/series/update", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/lists/series/update", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() @@ -113,19 +106,19 @@ class MangaUpdatesApi( } authClient.newCall( POST( - url = "$baseUrl/v1/lists/series/delete", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/lists/series/delete", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() } - private suspend fun getSeriesRating(track: Track): Rating? { + private suspend fun getSeriesRating(track: Track): MURating? { return try { with(json) { - authClient.newCall(GET("$baseUrl/v1/series/${track.remote_id}/rating")) + authClient.newCall(GET("$BASE_URL/v1/series/${track.remote_id}/rating")) .awaitSuccess() - .parseAs() + .parseAs() } } catch (e: Exception) { null @@ -140,22 +133,20 @@ class MangaUpdatesApi( } authClient.newCall( PUT( - url = "$baseUrl/v1/series/${track.remote_id}/rating", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/series/${track.remote_id}/rating", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() } else { authClient.newCall( - DELETE( - url = "$baseUrl/v1/series/${track.remote_id}/rating", - ), + DELETE(url = "$BASE_URL/v1/series/${track.remote_id}/rating"), ) .awaitSuccess() } } - suspend fun search(query: String): List { + suspend fun search(query: String): List { val body = buildJsonObject { put("search", query) put( @@ -166,25 +157,23 @@ class MangaUpdatesApi( }, ) } + return with(json) { client.newCall( POST( - url = "$baseUrl/v1/series/search", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/series/search", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() - .parseAs() - .let { obj -> - obj["results"]?.jsonArray?.map { element -> - json.decodeFromJsonElement(element.jsonObject["record"]!!) - } + .parseAs() + .let { result -> + result.results.map { it.record } } - .orEmpty() } } - suspend fun authenticate(username: String, password: String): Context? { + suspend fun authenticate(username: String, password: String): MUContext? { val body = buildJsonObject { put("username", username) put("password", password) @@ -192,20 +181,19 @@ class MangaUpdatesApi( return with(json) { client.newCall( PUT( - url = "$baseUrl/v1/account/login", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/account/login", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() - .parseAs() - .let { obj -> - try { - json.decodeFromJsonElement(obj["context"]!!) - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - null - } - } + .parseAs() + .context } } + + companion object { + private const val BASE_URL = "https://api.mangaupdates.com" + + private val CONTENT_TYPE = "application/vnd.api+json".toMediaType() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUContext.kt similarity index 91% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUContext.kt index 77019cacd2..688de07007 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUContext.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class Context( +data class MUContext( @SerialName("session_token") val sessionToken: String, val uid: Long, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUImage.kt similarity index 78% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUImage.kt index bed1f2657b..0ef38ca242 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUImage.kt @@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.data.track.mangaupdates.dto import kotlinx.serialization.Serializable @Serializable -data class Image( - val url: Url? = null, +data class MUImage( + val url: MUUrl? = null, val height: Int? = null, val width: Int? = null, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUListItem.kt similarity index 80% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUListItem.kt index 15a551078b..b406fd56e2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUListItem.kt @@ -6,15 +6,15 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ListItem( - val series: Series? = null, +data class MUListItem( + val series: MUSeries? = null, @SerialName("list_id") val listId: Long? = null, - val status: Status? = null, + val status: MUStatus? = null, val priority: Int? = null, ) -fun ListItem.copyTo(track: Track): Track { +fun MUListItem.copyTo(track: Track): Track { return track.apply { this.status = listId ?: READING_LIST this.last_chapter_read = this@copyTo.status?.chapter?.toDouble() ?: 0.0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MULoginResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MULoginResponse.kt new file mode 100644 index 0000000000..6b2a60cc77 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MULoginResponse.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.mangaupdates.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MULoginResponse( + val context: MUContext, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURating.kt similarity index 80% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURating.kt index 89a55b4135..eeca1bbb72 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURating.kt @@ -4,11 +4,11 @@ import eu.kanade.tachiyomi.data.database.models.Track import kotlinx.serialization.Serializable @Serializable -data class Rating( +data class MURating( val rating: Double? = null, ) -fun Rating.copyTo(track: Track): Track { +fun MURating.copyTo(track: Track): Track { return track.apply { this.score = rating ?: 0.0 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt similarity index 92% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt index 4b66273e83..88a560b9df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt @@ -6,13 +6,13 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class Record( +data class MURecord( @SerialName("series_id") val seriesId: Long? = null, val title: String? = null, val url: String? = null, val description: String? = null, - val image: Image? = null, + val image: MUImage? = null, val type: String? = null, val year: String? = null, @SerialName("bayesian_rating") @@ -23,7 +23,7 @@ data class Record( val latestChapter: Int? = null, ) -fun Record.toTrackSearch(id: Long): TrackSearch { +fun MURecord.toTrackSearch(id: Long): TrackSearch { return TrackSearch.create(id).apply { remote_id = this@toTrackSearch.seriesId ?: 0L title = this@toTrackSearch.title?.htmlDecode() ?: "" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSearch.kt new file mode 100644 index 0000000000..3e1771b0b6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSearch.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.data.track.mangaupdates.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MUSearchResult( + val results: List, +) + +@Serializable +data class MUSearchResultItem( + val record: MURecord, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSeries.kt similarity index 89% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSeries.kt index 261c857372..fa8e3feacd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSeries.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.mangaupdates.dto import kotlinx.serialization.Serializable @Serializable -data class Series( +data class MUSeries( val id: Long? = null, val title: String? = null, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUStatus.kt similarity index 89% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUStatus.kt index 7320ac2e3d..99bf5a7af0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUStatus.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.mangaupdates.dto import kotlinx.serialization.Serializable @Serializable -data class Status( +data class MUStatus( val volume: Int? = null, val chapter: Int? = null, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUUrl.kt similarity index 90% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUUrl.kt index f295d3bdc7..3e969e3b35 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUUrl.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.mangaupdates.dto import kotlinx.serialization.Serializable @Serializable -data class Url( +data class MUUrl( val original: String? = null, val thumb: String? = null, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 33daee1a95..ed744a8807 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.encodeToString @@ -143,7 +144,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { val oauth = api.getAccessToken(authCode) interceptor.setAuth(oauth) val username = api.getCurrentUser() - saveCredentials(username, oauth.access_token) + saveCredentials(username, oauth.accessToken) } catch (e: Throwable) { logout() } @@ -163,13 +164,13 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { trackPreferences.trackAuthExpired(this).set(true) } - fun saveOAuth(oAuth: OAuth?) { + fun saveOAuth(oAuth: MALOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) } - fun loadOAuth(): OAuth? { + fun loadOAuth(): MALOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 097e36a5d7..bde30a1a24 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -4,6 +4,13 @@ import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItem +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItemStatus +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALManga +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUserSearchResult import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -13,16 +20,6 @@ import eu.kanade.tachiyomi.util.PkceUtil import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.boolean -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.double -import kotlinx.serialization.json.doubleOrNull -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import okhttp3.FormBody import okhttp3.Headers import okhttp3.OkHttpClient @@ -44,7 +41,7 @@ class MyAnimeListApi( private val authClient = client.newBuilder().addInterceptor(interceptor).build() - suspend fun getAccessToken(authCode: String): OAuth { + suspend fun getAccessToken(authCode: String): MALOAuth { return withIOContext { val formBody: RequestBody = FormBody.Builder() .add("client_id", CLIENT_ID) @@ -69,8 +66,8 @@ class MyAnimeListApi( with(json) { authClient.newCall(request) .awaitSuccess() - .parseAs() - .let { it["name"]!!.jsonPrimitive.content } + .parseAs() + .name } } } @@ -85,13 +82,11 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - it["data"]!!.jsonArray - .map { data -> data.jsonObject["node"]!!.jsonObject } - .map { node -> - val id = node["id"]!!.jsonPrimitive.int - async { getMangaDetails(id) } + it.data + .map { + async { getMangaDetails(it.node.id) } } .awaitAll() .filter { trackSearch -> !trackSearch.publishing_type.contains("novel") } @@ -112,24 +107,19 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - val obj = it.jsonObject TrackSearch.create(trackId).apply { - remote_id = obj["id"]!!.jsonPrimitive.long - title = obj["title"]!!.jsonPrimitive.content - summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" - total_chapters = obj["num_chapters"]!!.jsonPrimitive.long - score = obj["mean"]?.jsonPrimitive?.doubleOrNull ?: -1.0 - cover_url = - obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content - ?: "" + remote_id = it.id + title = it.title + summary = it.synopsis + total_chapters = it.numChapters + score = it.mean + cover_url = it.covers.large tracking_url = "https://myanimelist.net/manga/$remote_id" - publishing_status = - obj["status"]!!.jsonPrimitive.content.replace("_", " ") - publishing_type = - obj["media_type"]!!.jsonPrimitive.content.replace("_", " ") - start_date = obj["start_date"]?.jsonPrimitive?.content ?: "" + publishing_status = it.status.replace("_", " ") + publishing_type = it.mediaType.replace("_", " ") + start_date = it.startDate ?: "" } } } @@ -157,7 +147,7 @@ class MyAnimeListApi( with(json) { authClient.newCall(request) .awaitSuccess() - .parseAs() + .parseAs() .let { parseMangaItem(it, track) } } } @@ -180,12 +170,10 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(uri.toString())) .awaitSuccess() - .parseAs() - .let { obj -> - track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.long - obj.jsonObject["my_list_status"]?.jsonObject?.let { - parseMangaItem(it, track) - } + .parseAs() + .let { item -> + track.total_chapters = item.numChapters + item.myListStatus?.let { parseMangaItem(it, track) } } } } @@ -193,24 +181,15 @@ class MyAnimeListApi( suspend fun findListItems(query: String, offset: Int = 0): List { return withIOContext { - val json = getListPage(offset) - val obj = json.jsonObject + val myListSearchResult = getListPage(offset) - val matches = obj["data"]!!.jsonArray - .filter { - it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains( - query, - ignoreCase = true, - ) - } - .map { - val id = it.jsonObject["node"]!!.jsonObject["id"]!!.jsonPrimitive.int - async { getMangaDetails(id) } - } + val matches = myListSearchResult.data + .filter { it.node.title.contains(query, ignoreCase = true) } + .map { async { getMangaDetails(it.node.id) } } .awaitAll() // Check next page if there's more - if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) { + if (!myListSearchResult.paging.next.isNullOrBlank()) { matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT) } else { matches @@ -218,7 +197,7 @@ class MyAnimeListApi( } } - private suspend fun getListPage(offset: Int): JsonObject { + private suspend fun getListPage(offset: Int): MALUserSearchResult { return withIOContext { val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon() .appendQueryParameter("fields", "list_status{start_date,finish_date}") @@ -239,19 +218,14 @@ class MyAnimeListApi( } } - private fun parseMangaItem(response: JsonObject, track: Track): Track { - val obj = response.jsonObject + private fun parseMangaItem(listStatus: MALListItemStatus, track: Track): Track { return track.apply { - val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean - status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]?.jsonPrimitive?.content) - last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.double - score = obj["score"]!!.jsonPrimitive.int.toDouble() - obj["start_date"]?.let { - started_reading_date = parseDate(it.jsonPrimitive.content) - } - obj["finish_date"]?.let { - finished_reading_date = parseDate(it.jsonPrimitive.content) - } + val isRereading = listStatus.isRereading + status = if (isRereading) MyAnimeList.REREADING else getStatus(listStatus.status) + last_chapter_read = listStatus.numChaptersRead + score = listStatus.score.toDouble() + listStatus.startDate?.let { started_reading_date = parseDate(it) } + listStatus.finishDate?.let { finished_reading_date = parseDate(it) } } } @@ -292,10 +266,10 @@ class MyAnimeListApi( .appendPath("my_list_status") .build() - fun refreshTokenRequest(oauth: OAuth): Request { + fun refreshTokenRequest(oauth: MALOAuth): Request { val formBody: RequestBody = FormBody.Builder() .add("client_id", CLIENT_ID) - .add("refresh_token", oauth.refresh_token) + .add("refresh_token", oauth.refreshToken) .add("grant_type", "refresh_token") .build() @@ -303,7 +277,7 @@ class MyAnimeListApi( // request is called by the interceptor itself so it doesn't reach // the part where the token is added automatically. val headers = Headers.Builder() - .add("Authorization", "Bearer ${oauth.access_token}") + .add("Authorization", "Bearer ${oauth.accessToken}") .build() return POST("$BASE_OAUTH_URL/token", body = formBody, headers = headers) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index d67c2cabec..92eca9ca27 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.data.track.myanimelist +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json import okhttp3.Interceptor @@ -11,7 +12,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor private val json: Json by injectLazy() - private var oauth: OAuth? = myanimelist.loadOAuth() + private var oauth: MALOAuth? = myanimelist.loadOAuth() private val tokenExpired get() = myanimelist.getIfAuthExpired() override fun intercept(chain: Interceptor.Chain): Response { @@ -30,7 +31,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor // Add the authorization header to the original request val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .addHeader("Authorization", "Bearer ${oauth!!.accessToken}") // TODO(antsy): Add back custom user agent when they stop blocking us for no apparent reason // .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() @@ -42,12 +43,12 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor * Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token * and the oauth object. */ - fun setAuth(oauth: OAuth?) { + fun setAuth(oauth: MALOAuth?) { this.oauth = oauth myanimelist.saveOAuth(oauth) } - private fun refreshToken(chain: Interceptor.Chain): OAuth = synchronized(this) { + private fun refreshToken(chain: Interceptor.Chain): MALOAuth = synchronized(this) { if (tokenExpired) throw MALTokenExpired() oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it } @@ -64,7 +65,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor return runCatching { if (response.isSuccessful) { - with(json) { response.parseAs() } + with(json) { response.parseAs() } } else { response.close() null diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListUtils.kt similarity index 61% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListUtils.kt index 1ae02142f2..593111a7d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListUtils.kt @@ -1,20 +1,6 @@ package eu.kanade.tachiyomi.data.track.myanimelist import eu.kanade.tachiyomi.data.database.models.Track -import kotlinx.serialization.Serializable - -@Serializable -data class OAuth( - val token_type: String, - val refresh_token: String, - val access_token: String, - val expires_in: Long, - val created_at: Long = System.currentTimeMillis(), -) { - // Assumes expired a minute earlier - private val adjustedExpiresIn: Long = (expires_in - 60) * 1000 - fun isExpired() = created_at + adjustedExpiresIn < System.currentTimeMillis() -} fun Track.toMyAnimeListStatus() = when (status) { MyAnimeList.READING -> "reading" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALList.kt new file mode 100644 index 0000000000..69cd9bcf1e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALList.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MALListItem( + @SerialName("num_chapters") + val numChapters: Long, + @SerialName("my_list_status") + val myListStatus: MALListItemStatus?, +) + +@Serializable +data class MALListItemStatus( + @SerialName("is_rereading") + val isRereading: Boolean, + val status: String, + @SerialName("num_chapters_read") + val numChaptersRead: Double, + val score: Int, + @SerialName("start_date") + val startDate: String?, + @SerialName("finish_date") + val finishDate: String?, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt new file mode 100644 index 0000000000..c4ab92ee92 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MALManga( + val id: Long, + val title: String, + val synopsis: String = "", + @SerialName("num_chapters") + val numChapters: Long, + val mean: Double = -1.0, + @SerialName("main_picture") + val covers: MALMangaCovers, + val status: String, + @SerialName("media_type") + val mediaType: String, + @SerialName("start_date") + val startDate: String?, +) + +@Serializable +data class MALMangaCovers( + val large: String = "", +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALOAuth.kt new file mode 100644 index 0000000000..2f3a5f8e88 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALOAuth.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MALOAuth( + @SerialName("token_type") + val tokenType: String, + @SerialName("refresh_token") + val refreshToken: String, + @SerialName("access_token") + val accessToken: String, + @SerialName("expires_in") + val expiresIn: Long, + @SerialName("created_at") + val createdAt: Long = System.currentTimeMillis(), +) { + // Assumes expired a minute earlier + private val adjustedExpiresIn: Long = (expiresIn - 60) * 1000 + + fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt new file mode 100644 index 0000000000..51ef2a6a48 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt @@ -0,0 +1,18 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MALSearchResult( + val data: List, +) + +@Serializable +data class MALSearchResultNode( + val node: MALSearchResultItem, +) + +@Serializable +data class MALSearchResultItem( + val id: Int, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUser.kt new file mode 100644 index 0000000000..a59974abd3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUser.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MALUser( + val name: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt new file mode 100644 index 0000000000..fad099a24b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MALUserSearchResult( + val data: List, + val paging: MALUserSearchPaging, +) + +@Serializable +data class MALUserSearchItem( + val node: MALUserSearchItemNode, +) + +@Serializable +data class MALUserSearchPaging( + val next: String?, +) + +@Serializable +data class MALUserSearchItemNode( + val id: Int, + val title: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index 118d005c15..d1ea737bcc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.encodeToString @@ -128,19 +129,19 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { val oauth = api.accessToken(code) interceptor.newAuth(oauth) val user = api.getCurrentUser() - saveCredentials(user.toString(), oauth.access_token) + saveCredentials(user.toString(), oauth.accessToken) } catch (e: Throwable) { logout() } } - fun saveToken(oauth: OAuth?) { + fun saveToken(oauth: SMOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { + fun restoreToken(): SMOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 08a6959305..b513daa7ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -4,6 +4,11 @@ import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMAddMangaResponse +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMManga +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUser +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListEntry import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -11,15 +16,7 @@ import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.double -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.FormBody @@ -58,10 +55,10 @@ class ShikimoriApi( body = payload.toString().toRequestBody(jsonMime), ), ).awaitSuccess() - .parseAs() + .parseAs() .let { // save id of the entry for possible future delete request - track.library_id = it["id"]!!.jsonPrimitive.long + track.library_id = it.id } track } @@ -88,53 +85,22 @@ class ShikimoriApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { response -> - response.map { - jsonToSearch(it.jsonObject) - } - } + .parseAs>() + .map { it.toTrack(trackId) } } } } - private fun jsonToSearch(obj: JsonObject): TrackSearch { - return TrackSearch.create(trackId).apply { - remote_id = obj["id"]!!.jsonPrimitive.long - title = obj["name"]!!.jsonPrimitive.content - total_chapters = obj["chapters"]!!.jsonPrimitive.long - cover_url = BASE_URL + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content - summary = "" - score = obj["score"]!!.jsonPrimitive.double - tracking_url = BASE_URL + obj["url"]!!.jsonPrimitive.content - publishing_status = obj["status"]!!.jsonPrimitive.content - publishing_type = obj["kind"]!!.jsonPrimitive.content - start_date = obj["aired_on"]!!.jsonPrimitive.contentOrNull ?: "" - } - } - - private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track { - return Track.create(trackId).apply { - title = mangas["name"]!!.jsonPrimitive.content - remote_id = obj["id"]!!.jsonPrimitive.long - total_chapters = mangas["chapters"]!!.jsonPrimitive.long - library_id = obj["id"]!!.jsonPrimitive.long - last_chapter_read = obj["chapters"]!!.jsonPrimitive.double - score = obj["score"]!!.jsonPrimitive.int.toDouble() - status = toTrackStatus(obj["status"]!!.jsonPrimitive.content) - tracking_url = BASE_URL + mangas["url"]!!.jsonPrimitive.content - } - } - + @Suppress("TooGenericExceptionThrown") suspend fun findLibManga(track: Track, userId: String): Track? { return withIOContext { val urlMangas = "$API_URL/mangas".toUri().buildUpon() .appendPath(track.remote_id.toString()) .build() - val mangas = with(json) { + val manga = with(json) { authClient.newCall(GET(urlMangas.toString())) .awaitSuccess() - .parseAs() + .parseAs() } val url = "$API_URL/v2/user_rates".toUri().buildUpon() @@ -145,15 +111,14 @@ class ShikimoriApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { response -> - if (response.size > 1) { - throw Exception("Too much mangas in response") + .parseAs>() + .let { entries -> + if (entries.size > 1) { + throw Exception("Too many manga in response") } - val entry = response.map { - jsonToTrack(it.jsonObject, mangas) - } - entry.firstOrNull() + entries + .map { it.toTrack(trackId, manga) } + .firstOrNull() } } } @@ -163,14 +128,12 @@ class ShikimoriApi( return with(json) { authClient.newCall(GET("$API_URL/users/whoami")) .awaitSuccess() - .parseAs() - .let { - it["id"]!!.jsonPrimitive.int - } + .parseAs() + .id } } - suspend fun accessToken(code: String): OAuth { + suspend fun accessToken(code: String): SMOAuth { return withIOContext { with(json) { client.newCall(accessTokenRequest(code)) @@ -192,16 +155,16 @@ class ShikimoriApi( ) companion object { - private const val CLIENT_ID = "PB9dq8DzI405s7wdtwTdirYqHiyVMh--djnP7lBUqSA" - private const val CLIENT_SECRET = "NajpZcOBKB9sJtgNcejf8OB9jBN1OYYoo-k4h2WWZus" - - private const val BASE_URL = "https://shikimori.one" + const val BASE_URL = "https://shikimori.one" private const val API_URL = "$BASE_URL/api" private const val OAUTH_URL = "$BASE_URL/oauth/token" private const val LOGIN_URL = "$BASE_URL/oauth/authorize" private const val REDIRECT_URL = "mihon://shikimori-auth" + private const val CLIENT_ID = "PB9dq8DzI405s7wdtwTdirYqHiyVMh--djnP7lBUqSA" + private const val CLIENT_SECRET = "NajpZcOBKB9sJtgNcejf8OB9jBN1OYYoo-k4h2WWZus" + fun authUrl(): Uri = LOGIN_URL.toUri().buildUpon() .appendQueryParameter("client_id", CLIENT_ID) .appendQueryParameter("redirect_uri", REDIRECT_URL) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt index aa2d4247a2..baa65025df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.shikimori import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth +import eu.kanade.tachiyomi.data.track.shikimori.dto.isExpired import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response @@ -13,34 +15,34 @@ class ShikimoriInterceptor(private val shikimori: Shikimori) : Interceptor { /** * OAuth object used for authenticated requests. */ - private var oauth: OAuth? = shikimori.restoreToken() + private var oauth: SMOAuth? = shikimori.restoreToken() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() val currAuth = oauth ?: throw Exception("Not authenticated with Shikimori") - val refreshToken = currAuth.refresh_token!! + val refreshToken = currAuth.refreshToken!! // Refresh access token if expired. if (currAuth.isExpired()) { val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken)) if (response.isSuccessful) { - newAuth(json.decodeFromString(response.body.string())) + newAuth(json.decodeFromString(response.body.string())) } else { response.close() } } // Add the authorization header to the original request. val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .addHeader("Authorization", "Bearer ${oauth!!.accessToken}") .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() return chain.proceed(authRequest) } - fun newAuth(oauth: OAuth?) { + fun newAuth(oauth: SMOAuth?) { this.oauth = oauth shikimori.saveToken(oauth) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt similarity index 69% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt index 5a28eceb40..fe59f6e80e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt @@ -1,19 +1,6 @@ package eu.kanade.tachiyomi.data.track.shikimori import eu.kanade.tachiyomi.data.database.models.Track -import kotlinx.serialization.Serializable - -@Serializable -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?, -) - -// Access token lives 1 day -fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) fun Track.toShikimoriStatus() = when (status) { Shikimori.READING -> "watching" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt new file mode 100644 index 0000000000..f9c9eb5645 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SMAddMangaResponse( + val id: Long +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMManga.kt new file mode 100644 index 0000000000..00b7754a26 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMManga.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SMManga( + val id: Long, + val name: String, + val chapters: Long, + val image: SUMangaCover, + val score: Double, + val url: String, + val status: String, + val kind: String, + @SerialName("aired_on") + val airedOn: String?, +) { + fun toTrack(trackId: Long): TrackSearch { + return TrackSearch.create(trackId).apply { + remote_id = this@SMManga.id + title = name + total_chapters = chapters + cover_url = ShikimoriApi.BASE_URL + image.preview + summary = "" + score = this@SMManga.score + tracking_url = ShikimoriApi.BASE_URL + url + publishing_status = this@SMManga.status + publishing_type = kind + start_date = airedOn ?: "" + } + } +} + +@Serializable +data class SUMangaCover( + val preview: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt new file mode 100644 index 0000000000..3609c317d8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt @@ -0,0 +1,22 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SMOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + @SerialName("created_at") + val createdAt: Long, + @SerialName("expires_in") + val expiresIn: Long, + @SerialName("refresh_token") + val refreshToken: String?, +) + +// Access token lives 1 day +@Suppress("MagicNumber") +fun SMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt new file mode 100644 index 0000000000..1b9ed6cdb1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SMUser( + val id: Int, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt new file mode 100644 index 0000000000..e5e160560f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi +import eu.kanade.tachiyomi.data.track.shikimori.toTrackStatus +import kotlinx.serialization.Serializable + +@Serializable +data class SMUserListEntry( + val id: Long, + val chapters: Double, + val score: Int, + val status: String, +) { + fun toTrack(trackId: Long, manga: SMManga): Track { + return Track.create(trackId).apply { + title = manga.name + remote_id = this@SMUserListEntry.id + total_chapters = manga.chapters + library_id = this@SMUserListEntry.id + last_chapter_read = this@SMUserListEntry.chapters + score = this@SMUserListEntry.score.toDouble() + status = toTrackStatus(this@SMUserListEntry.status) + tracking_url = ShikimoriApi.BASE_URL + manga.url + } + } +} From cf38769949ca51c6ed69db52b41775a3eb6d87d1 Mon Sep 17 00:00:00 2001 From: MajorTanya Date: Sun, 11 Aug 2024 18:53:11 +0200 Subject: [PATCH 2/9] Fix wrong types in KitsuAlgoliaSearchItem This API returns start and end dates as Long and the score as Double. Kitsu's docs claim they're strings (and they are, when requesting manga details from Kitsu directly) but the Algolia search results return Longs and Double, respectively. --- .../tachiyomi/data/track/kitsu/dto/KitsuSearch.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt index 0deefed2f8..5f05dd0c89 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt @@ -31,9 +31,9 @@ data class KitsuAlgoliaSearchItem( val subtype: String?, val posterImage: KitsuSearchItemCover?, val synopsis: String?, - val averageRating: String?, - val startDate: String?, - val endDate: String?, + val averageRating: Double?, + val startDate: Long?, + val endDate: Long?, ) { @Suppress("MagicNumber") fun toTrack(): TrackSearch { @@ -44,12 +44,12 @@ data class KitsuAlgoliaSearchItem( cover_url = posterImage?.original ?: "" summary = synopsis ?: "" tracking_url = KitsuApi.mangaUrl(remote_id) - score = averageRating?.toDoubleOrNull() ?: -1.0 + score = averageRating ?: -1.0 publishing_status = if (endDate == null) "Publishing" else "Finished" publishing_type = subtype ?: "" start_date = startDate?.let { val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(Date(it.toLong() * 1000)) + outputDf.format(Date(it * 1000)) } ?: "" } } From ee041be4ef1e6b0249a8bbff649bb62fbe7a2e83 Mon Sep 17 00:00:00 2001 From: MajorTanya Date: Sun, 11 Aug 2024 19:09:12 +0200 Subject: [PATCH 3/9] Apply review changes - Renamed `BangumiX` classes to `BGMX` classes. - Renamed `toXStatus` and `toXScore` to `toApiStatus` and `toApiScore` --- .../tachiyomi/data/track/anilist/Anilist.kt | 2 +- .../data/track/anilist/AnilistApi.kt | 4 ++-- .../data/track/anilist/AnilistUtils.kt | 4 ++-- .../tachiyomi/data/track/bangumi/Bangumi.kt | 8 ++++---- .../data/track/bangumi/BangumiApi.kt | 20 +++++++++---------- .../data/track/bangumi/BangumiInterceptor.kt | 10 +++++----- .../data/track/bangumi/BangumiUtils.kt | 2 +- ...onResponse.kt => BGMCollectionResponse.kt} | 2 +- .../dto/{BangumiOAuth.kt => BGMOAuth.kt} | 4 ++-- .../dto/{BangumiSearch.kt => BGMSearch.kt} | 18 ++++++++--------- .../dto/{BangumiUser.kt => BGMUser.kt} | 0 .../tachiyomi/data/track/kitsu/KitsuApi.kt | 6 +++--- .../tachiyomi/data/track/kitsu/KitsuUtils.kt | 4 ++-- 13 files changed, 42 insertions(+), 42 deletions(-) rename app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/{BangumiCollectionResponse.kt => BGMCollectionResponse.kt} (94%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/{BangumiOAuth.kt => BGMOAuth.kt} (83%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/{BangumiSearch.kt => BGMSearch.kt} (68%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/{BangumiUser.kt => BGMUser.kt} (100%) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index 2cb5623dec..db25cc7633 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -138,7 +138,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { else -> "😊" } - else -> track.toAnilistScore() + else -> track.toApiScore() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 6a859c3fb4..0585f62c55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -55,7 +55,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { putJsonObject("variables") { put("mangaId", track.remote_id) put("progress", track.last_chapter_read.toInt()) - put("status", track.toAnilistStatus()) + put("status", track.toApiStatus()) } } with(json) { @@ -98,7 +98,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { putJsonObject("variables") { put("listId", track.library_id) put("progress", track.last_chapter_read.toInt()) - put("status", track.toAnilistStatus()) + put("status", track.toApiStatus()) put("score", track.score.toInt()) put("startedAt", createDate(track.started_reading_date)) put("completedAt", createDate(track.finished_reading_date)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt index 6f69565115..5dab651132 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt @@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Track import uy.kohesive.injekt.injectLazy import tachiyomi.domain.track.model.Track as DomainTrack -fun Track.toAnilistStatus() = when (status) { +fun Track.toApiStatus() = when (status) { Anilist.READING -> "CURRENT" Anilist.COMPLETED -> "COMPLETED" Anilist.ON_HOLD -> "PAUSED" @@ -18,7 +18,7 @@ fun Track.toAnilistStatus() = when (status) { private val preferences: TrackPreferences by injectLazy() @Suppress("MagicNumber", "CyclomaticComplexMethod") -fun DomainTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) { +fun DomainTrack.toApiScore(): String = when (preferences.anilistScoreType().get()) { // 10 point "POINT_10" -> (score.toInt() / 10).toString() // 100 point diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index 472b5a6c9a..91aed7ee23 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -5,7 +5,7 @@ import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker -import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiOAuth +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -119,13 +119,13 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { } } - fun saveToken(oauth: BangumiOAuth?) { + fun saveToken(oauth: BGMOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): BangumiOAuth? { + fun restoreToken(): BGMOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index 6803f22f37..98f513e91b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.data.track.bangumi import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiCollectionResponse -import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiOAuth -import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiSearchItem -import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiSearchResult +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -36,7 +36,7 @@ class BangumiApi( return withIOContext { val body = FormBody.Builder() .add("rating", track.score.toInt().toString()) - .add("status", track.toBangumiStatus()) + .add("status", track.toApiStatus()) .build() authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body)) .awaitSuccess() @@ -49,7 +49,7 @@ class BangumiApi( // read status update val sbody = FormBody.Builder() .add("rating", track.score.toInt().toString()) - .add("status", track.toBangumiStatus()) + .add("status", track.toApiStatus()) .build() authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody)) .awaitSuccess() @@ -77,7 +77,7 @@ class BangumiApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { result -> if (result.code == 404) emptyList() @@ -95,7 +95,7 @@ class BangumiApi( with(json) { authClient.newCall(GET("$API_URL/subject/${track.remote_id}")) .awaitSuccess() - .parseAs() + .parseAs() .toTrackSearch(trackId) } } @@ -115,7 +115,7 @@ class BangumiApi( with(json) { authClient.newCall(requestUserRead) .awaitSuccess() - .parseAs() + .parseAs() .let { if (it.code == 400) return@let null @@ -128,7 +128,7 @@ class BangumiApi( } } - suspend fun accessToken(code: String): BangumiOAuth { + suspend fun accessToken(code: String): BGMOAuth { return withIOContext { with(json) { client.newCall(accessTokenRequest(code)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt index 1c23a8ec83..349ab48867 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.data.track.bangumi import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.data.track.bangumi.dto.BangumiOAuth +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired import kotlinx.serialization.json.Json import okhttp3.FormBody @@ -16,7 +16,7 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { /** * OAuth object used for authenticated requests. */ - private var oauth: BangumiOAuth? = bangumi.restoreToken() + private var oauth: BGMOAuth? = bangumi.restoreToken() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() @@ -26,7 +26,7 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { if (currAuth.isExpired()) { val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!)) if (response.isSuccessful) { - newAuth(json.decodeFromString(response.body.string())) + newAuth(json.decodeFromString(response.body.string())) } else { response.close() } @@ -51,11 +51,11 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { .let(chain::proceed) } - fun newAuth(oauth: BangumiOAuth?) { + fun newAuth(oauth: BGMOAuth?) { this.oauth = if (oauth == null) { null } else { - BangumiOAuth( + BGMOAuth( oauth.accessToken, oauth.tokenType, System.currentTimeMillis() / 1000, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt index a10e727538..5e38960bf0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.track.bangumi import eu.kanade.tachiyomi.data.database.models.Track -fun Track.toBangumiStatus() = when (status) { +fun Track.toApiStatus() = when (status) { Bangumi.READING -> "do" Bangumi.COMPLETED -> "collect" Bangumi.ON_HOLD -> "on_hold" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiCollectionResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMCollectionResponse.kt similarity index 94% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiCollectionResponse.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMCollectionResponse.kt index 8484775e00..85501934f7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiCollectionResponse.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMCollectionResponse.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class BangumiCollectionResponse( +data class BGMCollectionResponse( val code: Int?, val `private`: Int? = 0, val comment: String? = "", diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt similarity index 83% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiOAuth.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt index 846a16dd5a..69b1a94fa7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiOAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class BangumiOAuth( +data class BGMOAuth( @SerialName("access_token") val accessToken: String, @SerialName("token_type") @@ -22,4 +22,4 @@ data class BangumiOAuth( // Access token refresh before expired @Suppress("MagicNumber") -fun BangumiOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) +fun BGMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt similarity index 68% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiSearch.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt index a2d7058c59..241b954306 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt @@ -5,29 +5,29 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class BangumiSearchResult( - val list: List?, +data class BGMSearchResult( + val list: List?, val code: Int?, ) @Serializable -data class BangumiSearchItem( +data class BGMSearchItem( val id: Long, @SerialName("name_cn") val nameCn: String, val name: String, val type: Int, - val images: BangumiSearchItemCovers?, + val images: BGMSearchItemCovers?, @SerialName("eps_count") val epsCount: Long?, - val rating: BangumiSearchItemRating?, + val rating: BGMSearchItemRating?, val url: String, ) { fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply { - remote_id = this@BangumiSearchItem.id + remote_id = this@BGMSearchItem.id title = nameCn cover_url = images?.common ?: "" - summary = this@BangumiSearchItem.name + summary = this@BGMSearchItem.name score = rating?.score ?: -1.0 tracking_url = url total_chapters = epsCount ?: 0 @@ -35,11 +35,11 @@ data class BangumiSearchItem( } @Serializable -data class BangumiSearchItemCovers( +data class BGMSearchItemCovers( val common: String?, ) @Serializable -data class BangumiSearchItemRating( +data class BGMSearchItemRating( val score: Double?, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMUser.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BangumiUser.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMUser.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index abf205bec4..d1c17e5475 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -44,7 +44,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) putJsonObject("data") { put("type", "libraryEntries") putJsonObject("attributes") { - put("status", track.toKitsuStatus()) + put("status", track.toApiStatus()) put("progress", track.last_chapter_read.toInt()) } putJsonObject("relationships") { @@ -89,9 +89,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) put("type", "libraryEntries") put("id", track.remote_id) putJsonObject("attributes") { - put("status", track.toKitsuStatus()) + put("status", track.toApiStatus()) put("progress", track.last_chapter_read.toInt()) - put("ratingTwenty", track.toKitsuScore()) + put("ratingTwenty", track.toApiScore()) put("startedAt", KitsuDateHelper.convert(track.started_reading_date)) put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt index 98e4e70e39..6a52ba1a41 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.kitsu import eu.kanade.tachiyomi.data.database.models.Track @Suppress("TooGenericExceptionThrown") -fun Track.toKitsuStatus() = when (status) { +fun Track.toApiStatus() = when (status) { Kitsu.READING -> "current" Kitsu.COMPLETED -> "completed" Kitsu.ON_HOLD -> "on_hold" @@ -12,6 +12,6 @@ fun Track.toKitsuStatus() = when (status) { else -> throw Exception("Unknown status") } -fun Track.toKitsuScore(): String? { +fun Track.toApiScore(): String? { return if (score > 0) (score * 2).toInt().toString() else null } From f8ece5fd39c0a05573d427c74592bcdb0862d060 Mon Sep 17 00:00:00 2001 From: MajorTanya Date: Mon, 19 Aug 2024 09:52:40 +0200 Subject: [PATCH 4/9] Handle migration from detekt to spotless Removed Suppressions added for detekt. Specifically removed: - `SwallowedException` where an exception ends as a default value - `MagicNumber` - `CyclomaticComplexMethod` - `TooGenericExceptionThrown` Also ran spotlessApply which changed SMAddMangaResponse --- .../java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt | 1 - .../java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt | 1 - .../java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt | 2 -- .../java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt | 2 -- .../java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt | 1 - .../eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt | 1 - .../java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt | 1 - .../eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt | 1 - .../eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt | 1 - .../tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt | 2 +- .../eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt | 1 - 11 files changed, 1 insertion(+), 13 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt index 5dab651132..5e45f8da90 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt @@ -17,7 +17,6 @@ fun Track.toApiStatus() = when (status) { private val preferences: TrackPreferences by injectLazy() -@Suppress("MagicNumber", "CyclomaticComplexMethod") fun DomainTrack.toApiScore(): String = when (preferences.anilistScoreType().get()) { // 10 point "POINT_10" -> (score.toInt() / 10).toString() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt index 24eef984e1..b8f98ce633 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt @@ -20,7 +20,6 @@ data class ALManga( val totalChapters: Long, val averageScore: Int, ) { - @Suppress("SwallowedException") fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply { remote_id = remoteId title = this@ALManga.title diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index 98f513e91b..859f10d3e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -66,7 +66,6 @@ class BangumiApi( } } - @Suppress("MagicNumber") suspend fun search(search: String): List { return withIOContext { val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}" @@ -101,7 +100,6 @@ class BangumiApi( } } - @Suppress("MagicNumber") suspend fun statusLibManga(track: Track): Track? { return withIOContext { val urlUserRead = "$API_URL/collection/${track.remote_id}" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt index 69b1a94fa7..6a4fea3cb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt @@ -9,7 +9,6 @@ data class BGMOAuth( val accessToken: String, @SerialName("token_type") val tokenType: String, - @Suppress("MagicNumber") @SerialName("created_at") val createdAt: Long = System.currentTimeMillis() / 1000, @SerialName("expires_in") @@ -21,5 +20,4 @@ data class BGMOAuth( ) // Access token refresh before expired -@Suppress("MagicNumber") fun BGMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt index 6a52ba1a41..02a88e09c1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.track.kitsu import eu.kanade.tachiyomi.data.database.models.Track -@Suppress("TooGenericExceptionThrown") fun Track.toApiStatus() = when (status) { Kitsu.READING -> "current" Kitsu.COMPLETED -> "completed" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt index ab66f8fa6a..3f82b7ff6b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt @@ -12,7 +12,6 @@ data class KitsuListSearchResult( val data: List, val included: List, ) { - @Suppress("CyclomaticComplexMethod", "TooGenericExceptionThrown") fun firstToTrack(): TrackSearch { require(data.isNotEmpty()) { "Missing data from Kitsu" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt index 2aad9939f1..c5cab234a8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt @@ -17,5 +17,4 @@ data class KitsuOAuth( val refreshToken: String?, ) -@Suppress("MagicNumber") fun KitsuOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt index 5f05dd0c89..c0f9416736 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt @@ -35,7 +35,6 @@ data class KitsuAlgoliaSearchItem( val startDate: Long?, val endDate: Long?, ) { - @Suppress("MagicNumber") fun toTrack(): TrackSearch { return TrackSearch.create(TrackerManager.KITSU).apply { remote_id = this@KitsuAlgoliaSearchItem.id diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index b513daa7ca..b11fc9877b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -91,7 +91,6 @@ class ShikimoriApi( } } - @Suppress("TooGenericExceptionThrown") suspend fun findLibManga(track: Track, userId: String): Track? { return withIOContext { val urlMangas = "$API_URL/mangas".toUri().buildUpon() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt index f9c9eb5645..be5ee9f58f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddMangaResponse.kt @@ -4,5 +4,5 @@ import kotlinx.serialization.Serializable @Serializable data class SMAddMangaResponse( - val id: Long + val id: Long, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt index 3609c317d8..e041048010 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt @@ -18,5 +18,4 @@ data class SMOAuth( ) // Access token lives 1 day -@Suppress("MagicNumber") fun SMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) From b923fe9a22f0d3b6c6d03716dbf2b48610f6ad73 Mon Sep 17 00:00:00 2001 From: MajorTanya Date: Wed, 28 Aug 2024 16:26:58 +0200 Subject: [PATCH 5/9] Fix Kitsu failing to add series The `included` attribute seems to only appear when the user already has the entry in their Kitsu list. Since both `data` and `included` are required for `firstToTrack`, a guard clause has been added before all its calls. --- .../java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt | 4 ++-- .../kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index d1c17e5475..bc12eab323 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -180,7 +180,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .awaitSuccess() .parseAs() .let { - if (it.data.isNotEmpty()) { + if (it.data.isNotEmpty() && it.included.isNotEmpty()) { it.firstToTrack() } else { null @@ -201,7 +201,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .awaitSuccess() .parseAs() .let { - if (it.data.isNotEmpty()) { + if (it.data.isNotEmpty() && it.included.isNotEmpty()) { it.firstToTrack() } else { throw Exception("Could not find manga") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt index 3f82b7ff6b..0a505d6275 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt @@ -10,10 +10,11 @@ import kotlinx.serialization.Serializable @Serializable data class KitsuListSearchResult( val data: List, - val included: List, + val included: List = emptyList(), ) { fun firstToTrack(): TrackSearch { - require(data.isNotEmpty()) { "Missing data from Kitsu" } + require(data.isNotEmpty()) { "Missing User data from Kitsu" } + require(included.isNotEmpty()) { "Missing Manga data from Kitsu" } val userData = data[0] val userDataAttrs = userData.attributes From ebccea38086ef3ca415ba5c12f63a9a732dedfc2 Mon Sep 17 00:00:00 2001 From: MajorTanya Date: Wed, 28 Aug 2024 16:51:58 +0200 Subject: [PATCH 6/9] Fix empty Bangumi error when entry doesn't exist Previously, the non-null assertion (!!) would cause a NullPointerException and a Toast with "Bangumi error: " (no message) when the user had removed their list entry from Bangumi through other means like the website. Now it will show "Bangumi error: Could not find manga". This is analogous to the error shown by Kitsu under these circumstances. --- .../java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index 91aed7ee23..8eb3ec7769 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -76,8 +76,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { } override suspend fun refresh(track: Track): Track { - val remoteStatusTrack = api.statusLibManga(track) - track.copyPersonalFrom(remoteStatusTrack!!) + val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga") + track.copyPersonalFrom(remoteStatusTrack) api.findLibManga(track)?.let { remoteTrack -> track.total_chapters = remoteTrack.total_chapters } From 07e9b0eae455c6e1b4cf9097d01f7b0c9309bd04 Mon Sep 17 00:00:00 2001 From: MajorTanya Date: Wed, 28 Aug 2024 16:59:16 +0200 Subject: [PATCH 7/9] Fix Shikimori ignoring missing remote entry The user would see no indication that Shikimori could not properly refresh the track from the remote. This change causes the error Toast notification to pop up with the following message "Shikimori error: Could not find manga". This is analogous to Kitsu and Bangumi. --- .../java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index d1ea737bcc..f041671513 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -94,7 +94,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { track.library_id = remoteTrack.library_id track.copyPersonalFrom(remoteTrack) track.total_chapters = remoteTrack.total_chapters - } + } ?: throw Exception("Could not find manga") return track } From 418b2996b14235d1ad8d2c7a98f4d90a7eb2de11 Mon Sep 17 00:00:00 2001 From: MajorTanya Date: Mon, 2 Sep 2024 19:18:33 +0200 Subject: [PATCH 8/9] Remove usage of let where not needed These particular occurrences weren't needed because properties are directly accessible to further act upon. This neatly simplifies these clauses. --- .../tachiyomi/data/track/anilist/AnilistApi.kt | 16 ++++++---------- .../tachiyomi/data/track/kitsu/KitsuApi.kt | 11 +++++------ .../data/track/mangaupdates/MangaUpdatesApi.kt | 5 ++--- .../data/track/myanimelist/MyAnimeListApi.kt | 12 ++++-------- 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 0585f62c55..3695b6f25d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -174,10 +174,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ) .awaitSuccess() .parseAs() - .let { - it.data.page.media - .map { item -> item.toALManga().toTrack() } - } + .data.page.media + .map { it.toALManga().toTrack() } } } } @@ -241,12 +239,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ) .awaitSuccess() .parseAs() - .let { - it.data.page.mediaList - .map { item -> item.toALUserManga() } - .firstOrNull() - ?.toTrack() - } + .data.page.mediaList + .map { it.toALUserManga() } + .firstOrNull() + ?.toTrack() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index bc12eab323..3fa3eb9477 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -160,11 +160,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) ) .awaitSuccess() .parseAs() - .let { result -> - result.hits - .filter { it.subtype != "novel" } - .map { it.toTrack() } - } + .hits + .filter { it.subtype != "novel" } + .map { it.toTrack() } } } } @@ -237,7 +235,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) authClient.newCall(GET(url.toString())) .awaitSuccess() .parseAs() - .let { it.data[0].id } + .data[0] + .id } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt index 77b449c6b4..6f4471df56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt @@ -167,9 +167,8 @@ class MangaUpdatesApi( ) .awaitSuccess() .parseAs() - .let { result -> - result.results.map { it.record } - } + .results + .map { it.record } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index bde30a1a24..33435d4c38 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -83,14 +83,10 @@ class MyAnimeListApi( authClient.newCall(GET(url.toString())) .awaitSuccess() .parseAs() - .let { - it.data - .map { - async { getMangaDetails(it.node.id) } - } - .awaitAll() - .filter { trackSearch -> !trackSearch.publishing_type.contains("novel") } - } + .data + .map { async { getMangaDetails(it.node.id) } } + .awaitAll() + .filter { !it.publishing_type.contains("novel") } } } } From 657ced5a0bcf2d553c6d66995cde67d5a2d8cb37 Mon Sep 17 00:00:00 2001 From: MajorTanya Date: Mon, 2 Sep 2024 19:28:28 +0200 Subject: [PATCH 9/9] Remove missed let --- .../java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 3fa3eb9477..5933c96dd6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -108,9 +108,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .build(), ) .awaitSuccess() - .let { - track - } + + track } }