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 abf0d702a..db25cc763 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,13 +130,15 @@ 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() + + else -> track.toApiScore() } } @@ -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 e31454e74..3695b6f25 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 @@ -59,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) { @@ -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 } } @@ -103,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)) @@ -135,6 +130,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { .awaitSuccess() } } + suspend fun search(search: String): List { return withIOContext { val query = """ @@ -177,14 +173,9 @@ 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() + .data.page.media + .map { it.toALManga().toTrack() } } } } @@ -247,14 +238,11 @@ 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() + .data.page.mediaList + .map { it.toALUserManga() } + .firstOrNull() + ?.toTrack() } } } @@ -263,8 +251,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 +279,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 388b3e1b5..b23179f6e 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 d7c037afe..000000000 --- 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 000000000..5e45f8da9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt @@ -0,0 +1,44 @@ +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.toApiStatus() = 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.toApiScore(): 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 000000000..a552e7c1d --- /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 000000000..7dbd8c296 --- /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 000000000..b8f98ce63 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt @@ -0,0 +1,74 @@ +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, +) { + 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 000000000..94fbd6400 --- /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 000000000..f13ebb400 --- /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 000000000..49c8df938 --- /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 000000000..39507a0d5 --- /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 000000000..4ccec7aa4 --- /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 b8e7d2acc..8eb3ec776 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.BGMOAuth import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -75,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 } @@ -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: BGMOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { + 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 c48fa2490..859f10d3e 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.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 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 @@ -40,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() @@ -53,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() @@ -63,10 +59,7 @@ 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 @@ -80,44 +73,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\":[]}" - } - val response = json.decodeFromString(responseBody)["list"]?.jsonArray - response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 } - ?.map { jsonToSearch(it.jsonObject) }.orEmpty() - } - } - } + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { result -> + if (result.code == 404) emptyList() - 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 + result.list + ?.filter { it.type == 1 } + ?.map { it.toTrackSearch(trackId) } + .orEmpty() + } + } } } @@ -126,8 +94,8 @@ class BangumiApi( with(json) { authClient.newCall(GET("$API_URL/subject/${track.remote_id}")) .awaitSuccess() - .parseAs() - .let { jsonToSearch(it) } + .parseAs() + .toTrackSearch(trackId) } } } @@ -142,25 +110,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): 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 a1822cca0..349ab4886 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.BGMOAuth +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: BGMOAuth? = 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: BGMOAuth?) { this.oauth = if (oauth == null) { null } else { - OAuth( - oauth.access_token, - oauth.token_type, + BGMOAuth( + 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 c4b1aeed7..000000000 --- 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 000000000..5e38960bf --- /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.toApiStatus() = 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/BGMCollectionResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMCollectionResponse.kt new file mode 100644 index 000000000..85501934f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMCollectionResponse.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BGMCollectionResponse( + 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/BGMOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt new file mode 100644 index 000000000..6a4fea3cb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BGMOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + @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 +fun BGMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt new file mode 100644 index 000000000..241b95430 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.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 BGMSearchResult( + val list: List?, + val code: Int?, +) + +@Serializable +data class BGMSearchItem( + val id: Long, + @SerialName("name_cn") + val nameCn: String, + val name: String, + val type: Int, + val images: BGMSearchItemCovers?, + @SerialName("eps_count") + val epsCount: Long?, + val rating: BGMSearchItemRating?, + val url: String, +) { + fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply { + remote_id = this@BGMSearchItem.id + title = nameCn + cover_url = images?.common ?: "" + summary = this@BGMSearchItem.name + score = rating?.score ?: -1.0 + tracking_url = url + total_chapters = epsCount ?: 0 + } +} + +@Serializable +data class BGMSearchItemCovers( + val common: String?, +) + +@Serializable +data class BGMSearchItemRating( + val score: Double?, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMUser.kt new file mode 100644 index 000000000..375c39eb6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMUser.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 4b0db8bce..f0ca4e720 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 c124ddd13..5933c96dd 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 @@ -43,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") { @@ -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 } } @@ -92,63 +89,50 @@ 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)) } } } - 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() + + 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,13 +158,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) ), ) .awaitSuccess() - .parseAs() - .let { - it["hits"]!!.jsonArray - .map { KitsuSearchManga(it.jsonObject) } - .filter { it.subType != "novel" } - .map { it.toTrack() } - } + .parseAs() + .hits + .filter { it.subtype != "novel" } + .map { it.toTrack() } } } } @@ -194,12 +175,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.included.isNotEmpty()) { + it.firstToTrack() } else { null } @@ -217,12 +196,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.included.isNotEmpty()) { + it.firstToTrack() } else { throw Exception("Could not find manga") } @@ -231,7 +208,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 +233,9 @@ 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() + .data[0] + .id } } } @@ -279,6 +255,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 da9aff7fc..0f7c1b7db 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 752ef4e12..000000000 --- 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 000000000..02a88e09c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt @@ -0,0 +1,16 @@ +package eu.kanade.tachiyomi.data.track.kitsu + +import eu.kanade.tachiyomi.data.database.models.Track + +fun Track.toApiStatus() = 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.toApiScore(): 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 000000000..9ddec35e9 --- /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 000000000..0a505d627 --- /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 = emptyList(), +) { + fun firstToTrack(): TrackSearch { + require(data.isNotEmpty()) { "Missing User data from Kitsu" } + require(included.isNotEmpty()) { "Missing Manga 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 000000000..c5cab234a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt @@ -0,0 +1,20 @@ +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?, +) + +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 000000000..c0f941673 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt @@ -0,0 +1,55 @@ +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: Double?, + val startDate: Long?, + val endDate: Long?, +) { + 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 ?: -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 * 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 000000000..6c062bf52 --- /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 000000000..d02337651 --- /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 6219e728b..c47a799a7 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 5da3b7222..6f4471df5 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,22 @@ 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"]!!) - } - } - .orEmpty() + .parseAs() + .results + .map { it.record } } } - 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 +180,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 77019cacd..688de0700 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 bed1f2657..0ef38ca24 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 15a551078..b406fd56e 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 000000000..6b2a60cc7 --- /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 89a55b413..eeca1bbb7 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 4b66273e8..88a560b9d 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 000000000..3e1771b0b --- /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 261c85737..fa8e3feac 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 7320ac2e3..99bf5a7af 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 f295d3bdc..3e969e3b3 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 33daee1a9..ed744a880 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 097e36a5d..33435d4c3 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,17 +82,11 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { - it["data"]!!.jsonArray - .map { data -> data.jsonObject["node"]!!.jsonObject } - .map { node -> - val id = node["id"]!!.jsonPrimitive.int - async { getMangaDetails(id) } - } - .awaitAll() - .filter { trackSearch -> !trackSearch.publishing_type.contains("novel") } - } + .parseAs() + .data + .map { async { getMangaDetails(it.node.id) } } + .awaitAll() + .filter { !it.publishing_type.contains("novel") } } } } @@ -112,24 +103,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 +143,7 @@ class MyAnimeListApi( with(json) { authClient.newCall(request) .awaitSuccess() - .parseAs() + .parseAs() .let { parseMangaItem(it, track) } } } @@ -180,12 +166,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 +177,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 +193,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 +214,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 +262,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 +273,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 d67c2cabe..92eca9ca2 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 1ae02142f..593111a7d 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 000000000..69cd9bcf1 --- /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 000000000..c4ab92ee9 --- /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 000000000..2f3a5f8e8 --- /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 000000000..51ef2a6a4 --- /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 000000000..a59974abd --- /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 000000000..fad099a24 --- /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 118d005c1..f04167151 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 @@ -93,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 } @@ -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 08a695930..b11fc9877 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,21 @@ 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 - } - } - 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 +110,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 +127,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 +154,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 aa2d4247a..baa65025d 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 5a28eceb4..fe59f6e80 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 000000000..be5ee9f58 --- /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 000000000..00b7754a2 --- /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 000000000..e04104801 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt @@ -0,0 +1,21 @@ +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 +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 000000000..1b9ed6cdb --- /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 000000000..e5e160560 --- /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 + } + } +}