Skip to content

Commit

Permalink
Get manga info from tracker (#1271)
Browse files Browse the repository at this point in the history
* Barebones setup (only AniList works)

* Show tracker selection dialog when entry has more than one tracker

* MangaUpdates implementation

* Add logging and toast on error.

* MyAnimeList implementation

* Kitsu implementation

* Fix MAL authors and artists

* Decode AL description

* Throw NotImplementedError instead of returning null

* Use logcat from LogcatExtensions

* Replace strings with MR strings

* Missed a string

* Delete unused Author class.

* Add Bangumi & Shikimori support for info edit (#2)

This adds the necessary API calls and DTOs to allow for editing an
entry's data to the data from a tracker, specifically adding support
for Bangumi and Shikimori.

* Exclude enhanced trackers from tracker select dialog

* MdList implementation

* Remember getTracks and trackerManager

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>

---------

Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com>
Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 8, 2024
1 parent 34e9d9f commit fd120c5
Show file tree
Hide file tree
Showing 28 changed files with 783 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -120,6 +121,10 @@ abstract class BaseTracker(
updateRemote(track)
}

override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
throw NotImplementedError("Not implemented.")
}

private suspend fun updateRemote(track: Track): Unit = withIOContext {
try {
update(track)
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -82,4 +83,6 @@ interface Tracker {
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)

suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)

suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata?
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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.anilist.dto.ALOAuth
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
Expand Down Expand Up @@ -232,6 +233,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
interceptor.setAuth(null)
}

override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
return api.getMangaMetadata(track)
}

fun saveOAuth(alOAuth: ALOAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(alOAuth))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ 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.ALMangaMetadata
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.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.htmlDecode
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
Expand Down Expand Up @@ -288,6 +291,71 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}

suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
return withIOContext {
val query = """
|query (${'$'}mangaId: Int!) {
|Media (id: ${'$'}mangaId) {
|id
|title {
|userPreferred
|}
|coverImage {
|large
|}
|description
|staff {
|edges {
|role
|node {
|name {
|userPreferred
|}
|}
|}
|}
|}
|}
|
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("mangaId", track.remoteId)
}
}
with(json) {
authClient.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
.parseAs<ALMangaMetadata>()
.let {
val media = it.data.media
TrackMangaMetadata(
remoteId = media.id,
title = media.title.userPreferred,
thumbnailUrl = media.coverImage.large,
description = media.description?.htmlDecode()?.ifEmpty { null },
authors = media.staff.edges
.filter { it.role == "Story" || it.role == "Story & Art" }
.map { it.node.name.userPreferred }
.joinToString(", ")
.ifEmpty { null },
artists = media.staff.edges
.filter { it.role == "Art" || it.role == "Story & Art" }
.map { it.node.name.userPreferred }
.joinToString(", ")
.ifEmpty { null },
)
}
}
}
}

private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) {
return buildJsonObject {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.data.track.anilist.dto

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ALMangaMetadata(
val data: ALMangaMetadataData,
)

@Serializable
data class ALMangaMetadataData(
@SerialName("Media")
val media: ALMangaMetadataMedia,
)

@Serializable
data class ALMangaMetadataMedia(
val id: Long,
val title: ALItemTitle,
val coverImage: ItemCover,
val description: String?,
val staff: ALStaff,
)

@Serializable
data class ALStaff(
val edges: List<ALStaffEdge>,
)

@Serializable
data class ALStaffEdge(
val role: String,
val node: ALStaffNode,
)

@Serializable
data class ALStaffNode(
val name: ALItemTitle,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
Expand Down Expand Up @@ -75,6 +76,10 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
return api.search(query)
}

override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
return api.getMangaMetadata(track)
}

override suspend fun refresh(track: Track): Track {
val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
track.copyPersonalFrom(remoteStatusTrack)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ 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.bangumi.dto.BGMSubject
import eu.kanade.tachiyomi.data.track.bangumi.dto.Infobox
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
Expand All @@ -21,6 +24,7 @@ import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import tachiyomi.domain.track.model.Track as DomainTrack

class BangumiApi(
private val trackId: Long,
Expand Down Expand Up @@ -127,6 +131,34 @@ class BangumiApi(
}
}

suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
return withIOContext {
with(json) {
authClient.newCall(GET("${API_URL}/v0/subjects/${track.remoteId}"))
.awaitSuccess()
.parseAs<BGMSubject>()
.let {
TrackMangaMetadata(
remoteId = it.id,
title = it.nameCn,
thumbnailUrl = it.images?.common,
description = it.summary,
authors = it.infobox
.filter { it.key == "作者" }
.filterIsInstance<Infobox.SingleValue>()
.map { it.value }
.joinToString(", "),
artists = it.infobox
.filter { it.key == "插图" }
.filterIsInstance<Infobox.SingleValue>()
.map { it.value }
.joinToString(", "),
)
}
}
}
}

suspend fun accessToken(code: String): BGMOAuth {
return withIOContext {
with(json) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
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
// 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) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<Infobox> {
if (element !is JsonObject) throw SerializationException("Expected JsonObject go ${element::class}")
val value = element["value"]

return when (value) {
is JsonArray -> Infobox.MultipleValues.serializer()
is JsonPrimitive -> Infobox.SingleValue.serializer()
else -> throw SerializationException("Unexpected element type ${element::class}")
}
}
}

@Serializable(with = InfoBoxSerializer::class)
sealed interface Infobox {
val key: String

@Serializable
class SingleValue(
override val key: String,
val value: String,
) : Infobox

@Serializable
class MultipleValues(
override val key: String,
val value: List<InfoboxNestedValue>,
) : Infobox
}

@Serializable
data class InfoboxNestedValue(
@SerialName("k")
val key: String? = null,
@SerialName("v")
val value: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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.kitsu.dto.KitsuOAuth
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
Expand Down Expand Up @@ -139,6 +140,10 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
interceptor.newAuth(null)
}

override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
return api.getMangaMetadata(track)
}

private fun getUserId(): String {
return getPassword()
}
Expand Down
Loading

0 comments on commit fd120c5

Please sign in to comment.