From f8d73819eaae86e4de10c8795e44d2347f1ce03e Mon Sep 17 00:00:00 2001 From: KAAAsS Date: Sun, 13 Apr 2025 07:34:04 +0800 Subject: [PATCH] [Feature] Support Bangumi Tracker (#1343) * feat: Support Bangumi Tracker Credits: Andreas, AntsyLich, Caleb Morris, Gauthier, MCAxiaz, MajorTanya, NarwhalHorns, arkon, fei long, jmir1, mutsumi, stevenyomi * Use Suwayomi api keys --------- Co-authored-by: Mitchell Syer --- .../impl/track/tracker/TrackerManager.kt | 6 +- .../impl/track/tracker/bangumi/Bangumi.kt | 163 +++++++++++++++ .../impl/track/tracker/bangumi/BangumiApi.kt | 193 ++++++++++++++++++ .../tracker/bangumi/BangumiInterceptor.kt | 62 ++++++ .../track/tracker/bangumi/BangumiModels.kt | 115 +++++++++++ .../main/resources/static/tracker/bangumi.png | Bin 0 -> 22914 bytes 6 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/Bangumi.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiApi.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiInterceptor.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiModels.kt create mode 100644 server/src/main/resources/static/tracker/bangumi.png diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/TrackerManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/TrackerManager.kt index 6ca4ebca..1091d6f5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/TrackerManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/TrackerManager.kt @@ -1,6 +1,7 @@ package suwayomi.tachidesk.manga.impl.track.tracker import suwayomi.tachidesk.manga.impl.track.tracker.anilist.Anilist +import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.Bangumi import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.Kitsu import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.MyAnimeList @@ -22,13 +23,14 @@ object TrackerManager { val kitsu = Kitsu(KITSU) // val shikimori = Shikimori(SHIKIMORI) -// val bangumi = Bangumi(BANGUMI) + val bangumi = Bangumi(BANGUMI) + // val komga = Komga(KOMGA) val mangaUpdates = MangaUpdates(MANGA_UPDATES) // val kavita = Kavita(context, KAVITA) // val suwayomi = Suwayomi(SUWAYOMI) - val services: List = listOf(myAnimeList, aniList, kitsu, mangaUpdates) + val services: List = listOf(myAnimeList, aniList, kitsu, mangaUpdates, bangumi) fun getTracker(id: Int) = services.find { it.id == id } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/Bangumi.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/Bangumi.kt new file mode 100644 index 00000000..c68f6d28 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/Bangumi.kt @@ -0,0 +1,163 @@ +package suwayomi.tachidesk.manga.impl.track.tracker.bangumi + +import android.annotation.StringRes +import kotlinx.serialization.json.Json +import suwayomi.tachidesk.manga.impl.track.tracker.Tracker +import suwayomi.tachidesk.manga.impl.track.tracker.extractToken +import suwayomi.tachidesk.manga.impl.track.tracker.model.Track +import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch +import uy.kohesive.injekt.injectLazy +import java.io.IOException + +class Bangumi( + id: Int, +) : Tracker(id, "Bangumi") { + companion object { + const val PLAN_TO_READ = 1 + const val COMPLETED = 2 + const val READING = 3 + const val ON_HOLD = 4 + const val DROPPED = 5 + + private val SCORE_LIST = + IntRange(0, 10) + .map(Int::toString) + } + + override val supportsTrackDeletion: Boolean = false + + private val json: Json by injectLazy() + + private val interceptor by lazy { BangumiInterceptor(this) } + + private val api by lazy { BangumiApi(id, client, interceptor) } + + override fun getScoreList(): List = SCORE_LIST + + override fun displayScore(track: Track): String = track.score.toInt().toString() + + private suspend fun add(track: Track): Track = api.addLibManga(track) + + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { + if (track.status != COMPLETED) { + if (didReadChapter) { + if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) { + track.status = COMPLETED + } else { + track.status = READING + } + } + } + + return api.updateLibManga(track) + } + + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track { + val statusTrack = api.statusLibManga(track, getUsername()) + return if (statusTrack != null) { + track.copyPersonalFrom(statusTrack) + track.library_id = statusTrack.library_id + track.score = statusTrack.score + track.last_chapter_read = statusTrack.last_chapter_read + track.total_chapters = statusTrack.total_chapters + if (track.status != COMPLETED) { + track.status = if (hasReadChapters) READING else statusTrack.status + } + + update(track) + } else { + // Set default fields if it's not found in the list + track.status = if (hasReadChapters) READING else PLAN_TO_READ + track.score = 0.0F + add(track) + } + } + + override suspend fun search(query: String): List = api.search(query) + + override suspend fun refresh(track: Track): Track { + val remoteStatusTrack = api.statusLibManga(track, getUsername()) ?: throw Exception("Could not find manga") + track.copyPersonalFrom(remoteStatusTrack) + return track + } + + override fun authUrl(): String = BangumiApi.authUrl().toString() + + override suspend fun authCallback(url: String) { + val code = url.extractToken("code") ?: throw IOException("cannot find token") + login(code) + } + + override fun getLogo() = "/static/tracker/bangumi.png" + + override fun getStatusList(): List = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) + + @StringRes + override fun getStatus(status: Int): String? = + when (status) { + READING -> "Reading" + PLAN_TO_READ -> "Plan to read" + COMPLETED -> "Completed" + ON_HOLD -> "On hold" + DROPPED -> "Dropped" + else -> null + } + + override fun getReadingStatus(): Int = READING + + override fun getRereadingStatus(): Int = -1 + + override fun getCompletionStatus(): Int = COMPLETED + + override suspend fun login( + username: String, + password: String, + ) = login(password) + + suspend fun login(code: String) { + try { + val oauth = api.accessToken(code) + interceptor.newAuth(oauth) + // Users can set a 'username' (not nickname) once which effectively + // replaces the stringified ID in certain queries. + // If no username is set, the API returns the user ID as a strings + val username = api.getUsername() + saveCredentials(username, oauth.accessToken) + } catch (_: Throwable) { + logout() + } + } + + fun saveToken(oauth: BGMOAuth?) { + trackPreferences.setTrackToken(this, json.encodeToString(oauth)) + } + + fun restoreToken(): BGMOAuth? = + try { + json.decodeFromString(trackPreferences.getTrackToken(this)!!) + } catch (_: Exception) { + null + } + + override fun logout() { + super.logout() + trackPreferences.setTrackToken(this, null) + interceptor.newAuth(null) + } +} + +fun Track.toApiStatus() = + when (status) { + Bangumi.PLAN_TO_READ -> 1 + Bangumi.COMPLETED -> 2 + Bangumi.READING -> 3 + Bangumi.ON_HOLD -> 4 + Bangumi.DROPPED -> 5 + else -> throw NotImplementedError("Unknown status: $status") + } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiApi.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiApi.kt new file mode 100644 index 00000000..94a30b26 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiApi.kt @@ -0,0 +1,193 @@ +package suwayomi.tachidesk.manga.impl.track.tracker.bangumi + +import android.net.Uri +import androidx.core.net.toUri +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.HttpException +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.parseAs +import eu.kanade.tachiyomi.util.lang.withIOContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import kotlinx.serialization.json.putJsonObject +import okhttp3.CacheControl +import okhttp3.FormBody +import okhttp3.Headers.Companion.headersOf +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import suwayomi.tachidesk.manga.impl.track.tracker.model.Track +import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch +import uy.kohesive.injekt.injectLazy + +class BangumiApi( + private val trackId: Int, + private val client: OkHttpClient, + interceptor: BangumiInterceptor, +) { + private val json: Json by injectLazy() + + private val authClient = client.newBuilder().addInterceptor(interceptor).build() + + suspend fun addLibManga(track: Track): Track = + withIOContext { + val url = "$API_URL/v0/users/-/collections/${track.media_id}" + val body = + buildJsonObject { + put("type", track.toApiStatus()) + put("rate", track.score.toInt().coerceIn(0, 10)) + put("ep_status", track.last_chapter_read.toInt()) + }.toString().toRequestBody() + // Returns with 202 Accepted on success with no body + authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON))).awaitSuccess() + track + } + + suspend fun updateLibManga(track: Track): Track = + withIOContext { + val url = "$API_URL/v0/users/-/collections/${track.media_id}" + val body = + buildJsonObject { + put("type", track.toApiStatus()) + put("rate", track.score.toInt().coerceIn(0, 10)) + put("ep_status", track.last_chapter_read.toInt()) + }.toString().toRequestBody() + + val request = + Request + .Builder() + .url(url) + .patch(body) + .headers(headersOf("Content-Type", APP_JSON)) + .build() + // Returns with 204 No Content + authClient.newCall(request).awaitSuccess() + + track + } + + suspend fun search(search: String): List { + // This API is marked as experimental in the documentation + // but that has been the case since 2022 with few significant + // changes to the schema for this endpoint since + // "实验性 API, 本 schema 和实际的 API 行为都可能随时发生改动" + return withIOContext { + val url = "$API_URL/v0/search/subjects?limit=20" + val body = + buildJsonObject { + put("keyword", search) + put("sort", "match") + putJsonObject("filter") { + putJsonArray("type") { + add(1) // "Book" (书籍) type + } + } + }.toString().toRequestBody() + with(json) { + authClient + .newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON))) + .awaitSuccess() + .parseAs() + .data + .filter { it.platform == null || it.platform == "漫画" } + .map { it.toTrackSearch(trackId) } + } + } + } + + suspend fun statusLibManga( + track: Track, + username: String, + ): Track? = + withIOContext { + val url = "$API_URL/v0/users/$username/collections/${track.media_id}" + with(json) { + try { + authClient + .newCall(GET(url, cache = CacheControl.FORCE_NETWORK)) + .awaitSuccess() + .parseAs() + .let { + track.status = it.getStatus() + track.last_chapter_read = it.epStatus?.toFloat() ?: 0.0F + track.score = it.rate?.toFloat() ?: 0.0F + track.total_chapters = it.subject?.eps ?: 0 + track + } + } catch (e: HttpException) { + if (e.code == 404) { // "subject is not collected by user" + null + } else { + throw e + } + } + } + } + + suspend fun accessToken(code: String): BGMOAuth = + withIOContext { + val body = + FormBody + .Builder() + .add("grant_type", "authorization_code") + .add("client_id", CLIENT_ID) + .add("client_secret", CLIENT_SECRET) + .add("code", code) + .add("redirect_uri", REDIRECT_URL) + .build() + with(json) { + client.newCall(POST(OAUTH_URL, body = body)).awaitSuccess().parseAs() + } + } + + suspend fun getUsername(): String = + withIOContext { + with(json) { + authClient + .newCall(GET("$API_URL/v0/me")) + .awaitSuccess() + .parseAs() + .username + } + } + + companion object { + private const val CLIENT_ID = "bgm376667faf473119bb" + private const val CLIENT_SECRET = "d74caf0b874ddd18e6c6e7fb86d77a06" + + private const val API_URL = "https://api.bgm.tv" + private const val OAUTH_URL = "https://bgm.tv/oauth/access_token" + private const val LOGIN_URL = "https://bgm.tv/oauth/authorize" + + private const val REDIRECT_URL = "https://suwayomi.org/tracker-oauth" + + private const val APP_JSON = "application/json" + + fun authUrl(): Uri = + LOGIN_URL + .toUri() + .buildUpon() + .appendQueryParameter("client_id", CLIENT_ID) + .appendQueryParameter("response_type", "code") + .appendQueryParameter("redirect_uri", REDIRECT_URL) + .build() + + fun refreshTokenRequest(token: String) = + POST( + OAUTH_URL, + body = + FormBody + .Builder() + .add("grant_type", "refresh_token") + .add("client_id", CLIENT_ID) + .add("client_secret", CLIENT_SECRET) + .add("refresh_token", token) + .add("redirect_uri", REDIRECT_URL) + .build(), + ) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiInterceptor.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiInterceptor.kt new file mode 100644 index 00000000..cb80b252 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiInterceptor.kt @@ -0,0 +1,62 @@ +package suwayomi.tachidesk.manga.impl.track.tracker.bangumi + +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.Response +import suwayomi.tachidesk.server.generated.BuildConfig +import uy.kohesive.injekt.injectLazy + +class BangumiInterceptor( + private val bangumi: Bangumi, +) : Interceptor { + private val json: Json by injectLazy() + + /** + * OAuth object used for authenticated requests. + */ + private var oauth: BGMOAuth? = bangumi.restoreToken() + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + var currAuth: BGMOAuth = oauth ?: throw Exception("Not authenticated with Bangumi") + + if (currAuth.isExpired()) { + val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!)) + if (response.isSuccessful) { + currAuth = json.decodeFromString(response.body.string()) + newAuth(currAuth) + } else { + response.close() + } + } + + return originalRequest + .newBuilder() + .header( + "User-Agent", + "Suwayomi/Suwayomi-Server/v${BuildConfig.VERSION} (${BuildConfig.GITHUB})", + ).apply { + addHeader("Authorization", "Bearer ${currAuth.accessToken}") + }.build() + .let(chain::proceed) + } + + fun newAuth(oauth: BGMOAuth?) { + this.oauth = + if (oauth == null) { + null + } else { + BGMOAuth( + oauth.accessToken, + oauth.tokenType, + System.currentTimeMillis() / 1000, + oauth.expiresIn, + oauth.refreshToken, + this.oauth?.userId, + ) + } + + bangumi.saveToken(oauth) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiModels.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiModels.kt new file mode 100644 index 00000000..6c10d7fc --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/track/tracker/bangumi/BangumiModels.kt @@ -0,0 +1,115 @@ +package suwayomi.tachidesk.manga.impl.track.tracker.bangumi + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager +import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch + +@Serializable +// Incomplete DTO with only our needed attributes +data class BGMUser( + val username: String, +) + +@Serializable +data class BGMSearchResult( + val total: Int, + val limit: Int, + val offset: Int, + val data: List = emptyList(), +) + +@Serializable +// Incomplete DTO with only our needed attributes +data class BGMSubject( + val id: Long, + @SerialName("name_cn") + val nameCn: String, + val name: String, + val summary: String?, + val date: String?, // YYYY-MM-DD + val images: BGMSubjectImages?, + val volumes: Long = 0, + val eps: Long = 0, + val rating: BGMSubjectRating?, + val platform: String?, +) { + fun toTrackSearch(trackId: Int): TrackSearch = + TrackSearch.create(TrackerManager.BANGUMI).apply { + media_id = this@BGMSubject.id + title = nameCn.ifBlank { name } + cover_url = images?.common.orEmpty() + summary = + if (nameCn.isNotBlank()) { + "作品原名:$name" + this@BGMSubject.summary?.let { "\n${it.trim()}" }.orEmpty() + } else { + this@BGMSubject.summary?.trim().orEmpty() + } + tracking_url = "https://bangumi.tv/subject/${this@BGMSubject.id}" + total_chapters = eps.toInt() + start_date = date ?: "" + } +} + +@Serializable +// Incomplete DTO with only our needed attributes +data class BGMSubjectImages( + val common: String?, +) + +@Serializable +// Incomplete DTO with only our needed attributes +data class BGMSubjectRating( + val score: Double?, +) + +@Serializable +data class BGMOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + @SerialName("created_at") + @EncodeDefault + 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) + +@Serializable +// Incomplete DTO with only our needed attributes +data class BGMCollectionResponse( + val rate: Int?, + val type: Int?, + @SerialName("ep_status") + val epStatus: Int? = 0, + @SerialName("vol_status") + val volStatus: Int? = 0, + val private: Boolean = false, + val subject: BGMSlimSubject? = null, +) { + fun getStatus(): Int = + when (type) { + 1 -> Bangumi.PLAN_TO_READ + 2 -> Bangumi.COMPLETED + 3 -> Bangumi.READING + 4 -> Bangumi.ON_HOLD + 5 -> Bangumi.DROPPED + else -> throw NotImplementedError("Unknown status: $type") + } +} + +@Serializable +// Incomplete DTO with only our needed attributes +data class BGMSlimSubject( + val volumes: Int?, + val eps: Int?, +) diff --git a/server/src/main/resources/static/tracker/bangumi.png b/server/src/main/resources/static/tracker/bangumi.png new file mode 100644 index 0000000000000000000000000000000000000000..a91a545a1167de63813b50aa3675985cc84f19a3 GIT binary patch literal 22914 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