Migrate to Bangumi's newer v0 API (#1748)
This comes with many benefits: - Starting dates are now available and shown to users - Lays groundwork to add private tracking for Bangumi, e.g. in #1736 - Mihon makes approximately 2-4 times fewer calls to Bangumi's API - Simplified interceptor for the access token addition - v0 does not allow access tokens in the query string - There is actively maintained documentation for it Also shrunk the DTOs for Bangumi by removing attributes we have no use for either now or in the foreseeable future. Volume data remains in case Mihon wants to ever support volumes. But attributes such as user avatars, nicknames, data relating to Bangumi's tag & meta-tag systems, etc. have been removed or just not added to the DTOs. (cherry picked from commit a96fbba3dc354e363b85923c52feceb88dc34447) # Conflicts: # CHANGELOG.md # app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt
This commit is contained in:
parent
cb2cfa7e94
commit
ab976d8b07
@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
|||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -49,26 +48,23 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val statusTrack = api.statusLibManga(track)
|
val statusTrack = api.statusLibManga(track, getUsername())
|
||||||
val remoteTrack = api.findLibManga(track)
|
return if (statusTrack != null) {
|
||||||
return if (remoteTrack != null && statusTrack != null) {
|
track.copyPersonalFrom(statusTrack)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.library_id = statusTrack.library_id
|
||||||
track.library_id = remoteTrack.library_id
|
track.score = statusTrack.score
|
||||||
|
track.last_chapter_read = statusTrack.last_chapter_read
|
||||||
|
track.total_chapters = statusTrack.total_chapters
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
track.status = if (hasReadChapters) READING else statusTrack.status
|
track.status = if (hasReadChapters) READING else statusTrack.status
|
||||||
}
|
}
|
||||||
|
|
||||||
track.score = statusTrack.score
|
track
|
||||||
track.last_chapter_read = statusTrack.last_chapter_read
|
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
|
||||||
refresh(track)
|
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0.0
|
track.score = 0.0
|
||||||
add(track)
|
add(track)
|
||||||
update(track)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,11 +77,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
override suspend fun refresh(track: Track): Track {
|
||||||
val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
|
val remoteStatusTrack = api.statusLibManga(track, getUsername()) ?: throw Exception("Could not find manga")
|
||||||
track.copyPersonalFrom(remoteStatusTrack)
|
track.copyPersonalFrom(remoteStatusTrack)
|
||||||
api.findLibManga(track)?.let { remoteTrack ->
|
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
|
||||||
}
|
|
||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,9 +110,13 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
suspend fun login(code: String) {
|
suspend fun login(code: String) {
|
||||||
try {
|
try {
|
||||||
val oauth = api.accessToken(code)
|
val oauth = api.accessToken(code)
|
||||||
|
// 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
|
||||||
|
var username = api.getUsername()
|
||||||
interceptor.newAuth(oauth)
|
interceptor.newAuth(oauth)
|
||||||
saveCredentials(oauth.userId.toString(), oauth.accessToken)
|
saveCredentials(username, oauth.accessToken)
|
||||||
} catch (e: Throwable) {
|
} catch (_: Throwable) {
|
||||||
logout()
|
logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +128,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
fun restoreToken(): BGMOAuth? {
|
fun restoreToken(): BGMOAuth? {
|
||||||
return try {
|
return try {
|
||||||
json.decodeFromString<BGMOAuth>(trackPreferences.trackToken(this).get())
|
json.decodeFromString<BGMOAuth>(trackPreferences.trackToken(this).get())
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,11 +140,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 3L
|
const val PLAN_TO_READ = 1L
|
||||||
const val COMPLETED = 2L
|
const val COMPLETED = 2L
|
||||||
|
const val READING = 3L
|
||||||
const val ON_HOLD = 4L
|
const val ON_HOLD = 4L
|
||||||
const val DROPPED = 5L
|
const val DROPPED = 5L
|
||||||
const val PLAN_TO_READ = 1L
|
|
||||||
|
|
||||||
private val SCORE_LIST = IntRange(0, 10)
|
private val SCORE_LIST = IntRange(0, 10)
|
||||||
.map(Int::toString)
|
.map(Int::toString)
|
||||||
|
@ -5,25 +5,31 @@ import androidx.core.net.toUri
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
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.BGMCollectionResponse
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
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.bangumi.dto.BGMSearchResult
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSubject
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSubject
|
||||||
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMUser
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.Infobox
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.Infobox
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
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.CacheControl
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers.Companion.headersOf
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class BangumiApi(
|
class BangumiApi(
|
||||||
@ -38,11 +44,16 @@ class BangumiApi(
|
|||||||
|
|
||||||
suspend fun addLibManga(track: Track): Track {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val body = FormBody.Builder()
|
val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
|
||||||
.add("rating", track.score.toInt().toString())
|
val body = buildJsonObject {
|
||||||
.add("status", track.toApiStatus())
|
put("type", track.toApiStatus())
|
||||||
.build()
|
put("rate", track.score.toInt().coerceIn(0, 10))
|
||||||
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body))
|
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()
|
.awaitSuccess()
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
@ -50,83 +61,78 @@ class BangumiApi(
|
|||||||
|
|
||||||
suspend fun updateLibManga(track: Track): Track {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
// read status update
|
val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
|
||||||
val sbody = FormBody.Builder()
|
val body = buildJsonObject {
|
||||||
.add("rating", track.score.toInt().toString())
|
put("type", track.toApiStatus())
|
||||||
.add("status", track.toApiStatus())
|
put("rate", track.score.toInt().coerceIn(0, 10))
|
||||||
.build()
|
put("ep_status", track.last_chapter_read.toInt())
|
||||||
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody))
|
}
|
||||||
.awaitSuccess()
|
.toString()
|
||||||
|
.toRequestBody()
|
||||||
|
|
||||||
// chapter update
|
val request = Request.Builder()
|
||||||
val body = FormBody.Builder()
|
.url(url)
|
||||||
.add("watched_eps", track.last_chapter_read.toInt().toString())
|
.patch(body)
|
||||||
|
.headers(headersOf("Content-Type", APP_JSON))
|
||||||
.build()
|
.build()
|
||||||
authClient.newCall(
|
// Returns with 204 No Content
|
||||||
POST("$API_URL/subject/${track.remote_id}/update/watched_eps", body = body),
|
authClient.newCall(request)
|
||||||
).awaitSuccess()
|
.awaitSuccess()
|
||||||
|
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun search(search: String): List<TrackSearch> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
|
// 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 {
|
return withIOContext {
|
||||||
val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
|
val url = "$API_URL/v0/search/subjects?limit=20"
|
||||||
.toUri()
|
val body = buildJsonObject {
|
||||||
.buildUpon()
|
put("keyword", search)
|
||||||
.appendQueryParameter("type", "1")
|
put("sort", "match")
|
||||||
.appendQueryParameter("responseGroup", "large")
|
putJsonObject("filter") {
|
||||||
.appendQueryParameter("max_results", "20")
|
putJsonArray("type") {
|
||||||
.build()
|
add(1) // "Book" (书籍) type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toString()
|
||||||
|
.toRequestBody()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(GET(url.toString()))
|
authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<BGMSearchResult>()
|
.parseAs<BGMSearchResult>()
|
||||||
.let { result ->
|
.data
|
||||||
if (result.code == 404) emptyList<TrackSearch>()
|
.map { it.toTrackSearch(trackId) }
|
||||||
|
|
||||||
result.list
|
|
||||||
?.map { it.toTrackSearch(trackId) }
|
|
||||||
.orEmpty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findLibManga(track: Track): Track? {
|
suspend fun statusLibManga(track: Track, username: String): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
|
val url = "$API_URL/v0/users/$username/collections/${track.remote_id}"
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(GET("$API_URL/subject/${track.remote_id}"))
|
try {
|
||||||
.awaitSuccess()
|
authClient.newCall(GET(url, cache = CacheControl.FORCE_NETWORK))
|
||||||
.parseAs<BGMSearchItem>()
|
.awaitSuccess()
|
||||||
.toTrackSearch(trackId)
|
.parseAs<BGMCollectionResponse>()
|
||||||
}
|
.let {
|
||||||
}
|
track.status = it.getStatus()
|
||||||
}
|
track.last_chapter_read = it.epStatus?.toDouble() ?: 0.0
|
||||||
|
track.score = it.rate?.toDouble() ?: 0.0
|
||||||
suspend fun statusLibManga(track: Track): Track? {
|
track.total_chapters = it.subject?.eps?.toLong() ?: 0L
|
||||||
return withIOContext {
|
track
|
||||||
val urlUserRead = "$API_URL/collection/${track.remote_id}"
|
}
|
||||||
val requestUserRead = Request.Builder()
|
} catch (e: HttpException) {
|
||||||
.url(urlUserRead)
|
if (e.code == 404) { // "subject is not collected by user"
|
||||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
null
|
||||||
.get()
|
} else {
|
||||||
.build()
|
throw e
|
||||||
|
|
||||||
// TODO: get user readed chapter here
|
|
||||||
with(json) {
|
|
||||||
authClient.newCall(requestUserRead)
|
|
||||||
.awaitSuccess()
|
|
||||||
.parseAs<BGMCollectionResponse>()
|
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,24 +167,31 @@ class BangumiApi(
|
|||||||
|
|
||||||
suspend fun accessToken(code: String): BGMOAuth {
|
suspend fun accessToken(code: String): BGMOAuth {
|
||||||
return withIOContext {
|
return 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) {
|
with(json) {
|
||||||
client.newCall(accessTokenRequest(code))
|
client.newCall(POST(OAUTH_URL, body = body))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs()
|
.parseAs<BGMOAuth>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun accessTokenRequest(code: String) = POST(
|
suspend fun getUsername(): String {
|
||||||
OAUTH_URL,
|
return withIOContext {
|
||||||
body = FormBody.Builder()
|
with(json) {
|
||||||
.add("grant_type", "authorization_code")
|
authClient.newCall(GET("$API_URL/v0/me$"))
|
||||||
.add("client_id", CLIENT_ID)
|
.awaitSuccess()
|
||||||
.add("client_secret", CLIENT_SECRET)
|
.parseAs<BGMUser>()
|
||||||
.add("code", code)
|
.username
|
||||||
.add("redirect_uri", REDIRECT_URL)
|
}
|
||||||
.build(),
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CLIENT_ID = "bgm291665acbd06a4c28"
|
private const val CLIENT_ID = "bgm291665acbd06a4c28"
|
||||||
@ -190,6 +203,8 @@ class BangumiApi(
|
|||||||
|
|
||||||
private const val REDIRECT_URL = "mihon://bangumi-auth"
|
private const val REDIRECT_URL = "mihon://bangumi-auth"
|
||||||
|
|
||||||
|
private const val APP_JSON = "application/json"
|
||||||
|
|
||||||
fun authUrl(): Uri =
|
fun authUrl(): Uri =
|
||||||
LOGIN_URL.toUri().buildUpon()
|
LOGIN_URL.toUri().buildUpon()
|
||||||
.appendQueryParameter("client_id", CLIENT_ID)
|
.appendQueryParameter("client_id", CLIENT_ID)
|
||||||
|
@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.BuildConfig
|
|||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -39,14 +38,7 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
|
|||||||
"jobobby04/TachiyomiSY/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/jobobby04/tachiyomisy)",
|
"jobobby04/TachiyomiSY/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/jobobby04/tachiyomisy)",
|
||||||
)
|
)
|
||||||
.apply {
|
.apply {
|
||||||
if (originalRequest.method == "GET") {
|
addHeader("Authorization", "Bearer ${currAuth.accessToken}")
|
||||||
val newUrl = originalRequest.url.newBuilder()
|
|
||||||
.addQueryParameter("access_token", currAuth.accessToken)
|
|
||||||
.build()
|
|
||||||
url(newUrl)
|
|
||||||
} else {
|
|
||||||
post(addToken(currAuth.accessToken, originalRequest.body as FormBody))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
.let(chain::proceed)
|
.let(chain::proceed)
|
||||||
@ -68,13 +60,4 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
|
|||||||
|
|
||||||
bangumi.saveToken(oauth)
|
bangumi.saveToken(oauth)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addToken(token: String, oidFormBody: FormBody): FormBody {
|
|
||||||
val newFormBody = FormBody.Builder()
|
|
||||||
for (i in 0..<oidFormBody.size) {
|
|
||||||
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
|
|
||||||
}
|
|
||||||
newFormBody.add("access_token", token)
|
|
||||||
return newFormBody.build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.data.track.bangumi
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
|
||||||
fun Track.toApiStatus() = when (status) {
|
fun Track.toApiStatus() = when (status) {
|
||||||
Bangumi.READING -> "do"
|
Bangumi.PLAN_TO_READ -> 1
|
||||||
Bangumi.COMPLETED -> "collect"
|
Bangumi.COMPLETED -> 2
|
||||||
Bangumi.ON_HOLD -> "on_hold"
|
Bangumi.READING -> 3
|
||||||
Bangumi.DROPPED -> "dropped"
|
Bangumi.ON_HOLD -> 4
|
||||||
Bangumi.PLAN_TO_READ -> "wish"
|
Bangumi.DROPPED -> 5
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,34 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi.dto
|
package eu.kanade.tachiyomi.data.track.bangumi.dto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
// Incomplete DTO with only our needed attributes
|
||||||
data class BGMCollectionResponse(
|
data class BGMCollectionResponse(
|
||||||
val code: Int?,
|
val rate: Int?,
|
||||||
val `private`: Int? = 0,
|
val type: Int?,
|
||||||
val comment: String? = "",
|
|
||||||
@SerialName("ep_status")
|
@SerialName("ep_status")
|
||||||
val epStatus: Int? = 0,
|
val epStatus: Int? = 0,
|
||||||
@SerialName("lasttouch")
|
|
||||||
val lastTouch: Int? = 0,
|
|
||||||
val rating: Double? = 0.0,
|
|
||||||
val status: Status? = Status(),
|
|
||||||
val tag: List<String?>? = emptyList(),
|
|
||||||
val user: User? = User(),
|
|
||||||
@SerialName("vol_status")
|
@SerialName("vol_status")
|
||||||
val volStatus: Int? = 0,
|
val volStatus: Int? = 0,
|
||||||
)
|
val private: Boolean = false,
|
||||||
|
val subject: BGMSlimSubject? = null,
|
||||||
|
) {
|
||||||
|
fun getStatus(): Long = 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
|
@Serializable
|
||||||
data class Status(
|
// Incomplete DTO with only our needed attributes
|
||||||
val id: Long? = 0,
|
data class BGMSlimSubject(
|
||||||
val name: String? = "",
|
val volumes: Int?,
|
||||||
val type: String? = "",
|
val eps: Int?,
|
||||||
)
|
)
|
||||||
|
@ -6,45 +6,53 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BGMSearchResult(
|
data class BGMSearchResult(
|
||||||
val list: List<BGMSearchItem>?,
|
val total: Int,
|
||||||
val code: Int?,
|
val limit: Int,
|
||||||
|
val offset: Int,
|
||||||
|
val data: List<BGMSubject> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BGMSearchItem(
|
// Incomplete DTO with only our needed attributes
|
||||||
|
data class BGMSubject(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@SerialName("name_cn")
|
@SerialName("name_cn")
|
||||||
val nameCn: String,
|
val nameCn: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val type: Int,
|
|
||||||
val summary: String?,
|
val summary: String?,
|
||||||
val images: BGMSearchItemCovers?,
|
val date: String?, // YYYY-MM-DD
|
||||||
@SerialName("eps_count")
|
val images: BGMSubjectImages?,
|
||||||
val epsCount: Long?,
|
val volumes: Long = 0,
|
||||||
val rating: BGMSearchItemRating?,
|
val eps: Long = 0,
|
||||||
val url: String,
|
val rating: BGMSubjectRating?,
|
||||||
|
// SY -->
|
||||||
|
val infobox: List<Infobox> = emptyList(),
|
||||||
|
// SY <--
|
||||||
) {
|
) {
|
||||||
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
|
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
|
||||||
remote_id = this@BGMSearchItem.id
|
remote_id = this@BGMSubject.id
|
||||||
title = nameCn.ifBlank { name }
|
title = nameCn.ifBlank { name }
|
||||||
cover_url = images?.common.orEmpty()
|
cover_url = images?.common.orEmpty()
|
||||||
summary = if (nameCn.isNotBlank()) {
|
summary = if (nameCn.isNotBlank()) {
|
||||||
"作品原名:$name" + this@BGMSearchItem.summary?.let { "\n$it" }.orEmpty()
|
"作品原名:$name" + this@BGMSubject.summary?.let { "\n${it.trim()}" }.orEmpty()
|
||||||
} else {
|
} else {
|
||||||
this@BGMSearchItem.summary.orEmpty()
|
this@BGMSubject.summary?.trim().orEmpty()
|
||||||
}
|
}
|
||||||
score = rating?.score ?: -1.0
|
score = rating?.score ?: -1.0
|
||||||
tracking_url = url
|
tracking_url = "https://bangumi.tv/subject/${this@BGMSubject.id}"
|
||||||
total_chapters = epsCount ?: 0
|
total_chapters = eps
|
||||||
|
start_date = date ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BGMSearchItemCovers(
|
// Incomplete DTO with only our needed attributes
|
||||||
|
data class BGMSubjectImages(
|
||||||
val common: String?,
|
val common: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BGMSearchItemRating(
|
// Incomplete DTO with only our needed attributes
|
||||||
|
data class BGMSubjectRating(
|
||||||
val score: Double?,
|
val score: Double?,
|
||||||
)
|
)
|
||||||
|
@ -1,23 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi.dto
|
package eu.kanade.tachiyomi.data.track.bangumi.dto
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Avatar(
|
// Incomplete DTO with only our needed attributes
|
||||||
val large: String? = "",
|
data class BGMUser(
|
||||||
val medium: String? = "",
|
val username: 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? = "",
|
|
||||||
)
|
)
|
||||||
|
@ -10,17 +10,6 @@ import kotlinx.serialization.json.JsonElement
|
|||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BGMSubject(
|
|
||||||
val images: BGMSearchItemCovers?,
|
|
||||||
val summary: String,
|
|
||||||
val name: String,
|
|
||||||
@SerialName("name_cn")
|
|
||||||
val nameCn: String,
|
|
||||||
val infobox: List<Infobox>,
|
|
||||||
val id: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
// infobox deserializer and related classes courtesy of
|
// infobox deserializer and related classes courtesy of
|
||||||
// https://github.com/Snd-R/komf/blob/4c260a3dcd326a5e1d74ac9662eec8124ab7e461/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/model/BangumiSubject.kt#L53-L89
|
// https://github.com/Snd-R/komf/blob/4c260a3dcd326a5e1d74ac9662eec8124ab7e461/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/model/BangumiSubject.kt#L53-L89
|
||||||
object InfoBoxSerializer : JsonContentPolymorphicSerializer<Infobox>(Infobox::class) {
|
object InfoBoxSerializer : JsonContentPolymorphicSerializer<Infobox>(Infobox::class) {
|
Loading…
x
Reference in New Issue
Block a user