Get manga info from tracker (#1271)

* 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>
This commit is contained in:
NGB-Was-Taken 2024-12-09 02:10:26 +05:45 committed by GitHub
parent 34e9d9f146
commit fd120c5081
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 783 additions and 12 deletions

View File

@ -6,6 +6,7 @@ import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.model.toDomainTrack import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track 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.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -120,6 +121,10 @@ abstract class BaseTracker(
updateRemote(track) updateRemote(track)
} }
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
throw NotImplementedError("Not implemented.")
}
private suspend fun updateRemote(track: Track): Unit = withIOContext { private suspend fun updateRemote(track: Track): Unit = withIOContext {
try { try {
update(track) update(track)

View File

@ -5,6 +5,7 @@ import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.data.database.models.Track 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 eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -82,4 +83,6 @@ interface Tracker {
suspend fun setRemoteStartDate(track: Track, epochMillis: Long) suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata?
} }

View File

@ -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.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth 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 eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -232,6 +233,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
interceptor.setAuth(null) interceptor.setAuth(null)
} }
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
return api.getMangaMetadata(track)
}
fun saveOAuth(alOAuth: ALOAuth?) { fun saveOAuth(alOAuth: ALOAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(alOAuth)) trackPreferences.trackToken(this).set(json.encodeToString(alOAuth))
} }

View File

@ -5,15 +5,18 @@ 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.anilist.dto.ALAddMangaResult 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.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.ALOAuth
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult 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.data.track.model.TrackSearch
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.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.htmlDecode
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@ -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 { private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) { if (dateValue == 0L) {
return buildJsonObject { return buildJsonObject {

View File

@ -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,
)

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth 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 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
@ -75,6 +76,10 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
return api.search(query) return api.search(query)
} }
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
return api.getMangaMetadata(track)
}
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) ?: throw Exception("Could not find manga")
track.copyPersonalFrom(remoteStatusTrack) track.copyPersonalFrom(remoteStatusTrack)

View File

@ -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.BGMOAuth
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem 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.Infobox
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.POST import eu.kanade.tachiyomi.network.POST
@ -21,6 +24,7 @@ import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import tachiyomi.domain.track.model.Track as DomainTrack
class BangumiApi( class BangumiApi(
private val trackId: Long, private val trackId: Long,
@ -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 { suspend fun accessToken(code: String): BGMOAuth {
return withIOContext { return withIOContext {
with(json) { with(json) {

View File

@ -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,
)

View File

@ -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.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth 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 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
@ -139,6 +140,10 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
interceptor.newAuth(null) interceptor.newAuth(null)
} }
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
return api.getMangaMetadata(track)
}
private fun getUserId(): String { private fun getUserId(): String {
return getPassword() return getPassword()
} }

View File

@ -6,8 +6,10 @@ 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.KitsuAlgoliaSearchResult
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentUserResult 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.KitsuListSearchResult
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuMangaMetadata
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult
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.DELETE import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -15,6 +17,7 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.htmlDecode
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
@ -240,11 +243,80 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
return withIOContext {
val query = """
|query(${'$'}libraryId: ID!, ${'$'}staffCount: Int) {
|findLibraryEntryById(id: ${'$'}libraryId) {
|media {
|id
|titles {
|preferred
|}
|posterImage {
|original {
|url
|}
|}
|description
|staff(first: ${'$'}staffCount) {
|nodes {
|role
|person {
|name
|}
|}
|}
|}
|}
|}
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("libraryId", track.remoteId)
put("staffCount", 25) // 25 based on nothing
}
}
with(json) {
authClient.newCall(
POST(
GRAPHQL_URL,
headers = headersOf("Accept-Language", "en"),
body = payload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
.parseAs<KitsuMangaMetadata>()
.let {
val manga = it.data.findLibraryEntryById.media
TrackMangaMetadata(
remoteId = manga.id.toLong(),
title = manga.titles.preferred,
thumbnailUrl = manga.posterImage.original.url,
description = manga.description.en?.htmlDecode()?.ifEmpty { null },
authors = manga.staff.nodes
.filter { it.role == "Story" || it.role == "Story & Art" }
.map { it.person.name }
.joinToString(", ")
.ifEmpty { null },
artists = manga.staff.nodes
.filter { it.role == "Art" || it.role == "Story & Art" }
.map { it.person.name }
.joinToString(", ")
.ifEmpty { null },
)
}
}
}
}
companion object { companion object {
private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151" private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
private const val BASE_URL = "https://kitsu.app/api/edge/" private const val BASE_URL = "https://kitsu.app/api/edge/"
private const val GRAPHQL_URL = "https://kitsu.app/api/graphql"
private const val LOGIN_URL = "https://kitsu.app/api/oauth/token" private const val LOGIN_URL = "https://kitsu.app/api/oauth/token"
private const val BASE_MANGA_URL = "https://kitsu.app/manga/" private const val BASE_MANGA_URL = "https://kitsu.app/manga/"
private const val ALGOLIA_KEY_URL = "https://kitsu.app/api/edge/algolia-keys/media/" private const val ALGOLIA_KEY_URL = "https://kitsu.app/api/edge/algolia-keys/media/"

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.data.track.kitsu.dto
import kotlinx.serialization.Serializable
@Serializable
data class KitsuMangaMetadata(
val data: KitsuMangaMetadataData,
)
@Serializable
data class KitsuMangaMetadataData(
val findLibraryEntryById: KitsuMangaMetadataById,
)
@Serializable
data class KitsuMangaMetadataById(
val media: KitsuMangaMetadataMedia,
)
@Serializable
data class KitsuMangaMetadataMedia(
val id: String,
val titles: KitsuMangaTitle,
val posterImage: KitsuMangaCover,
val description: KitsuMangaDescription,
val staff: KitsuMangaStaff,
)
@Serializable
data class KitsuMangaTitle(
val preferred: String,
)
@Serializable
data class KitsuMangaCover(
val original: KitsuMangaCoverUrl,
)
@Serializable
data class KitsuMangaCoverUrl(
val url: String,
)
@Serializable
data class KitsuMangaDescription(
val en: String?,
)
@Serializable
data class KitsuMangaStaff(
val nodes: List<KitsuMangaStaffNode>,
)
@Serializable
data class KitsuMangaStaffNode(
val role: String,
val person: KitsuMangaStaffPerson,
)
@Serializable
data class KitsuMangaStaffPerson(
val name: String,
)

View File

@ -10,7 +10,9 @@ 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.MURating
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
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.util.lang.htmlDecode
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@ -117,6 +119,20 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
interceptor.newAuth(authenticated.sessionToken) interceptor.newAuth(authenticated.sessionToken)
} }
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
val series = api.getSeries(track)
return series?.let {
TrackMangaMetadata(
it.seriesId,
it.title?.htmlDecode(),
it.image?.url?.original,
it.description?.htmlDecode(),
it.authors?.filter { it.type == "Author" }?.joinToString(separator = ", ") { it.name ?: "" },
it.authors?.filter { it.type == "Artist" }?.joinToString(separator = ", ") { it.name ?: "" },
)
}
}
fun restoreSession(): String? { fun restoreSession(): String? {
return trackPreferences.trackPassword(this).get().ifBlank { null } return trackPreferences.trackPassword(this).get().ifBlank { null }
} }

View File

@ -190,6 +190,14 @@ class MangaUpdatesApi(
} }
} }
suspend fun getSeries(track: DomainTrack): MURecord {
return with(json) {
client.newCall(GET("$BASE_URL/v1/series/${track.remoteId}"))
.awaitSuccess()
.parseAs<MURecord>()
}
}
companion object { companion object {
private const val BASE_URL = "https://api.mangaupdates.com" private const val BASE_URL = "https://api.mangaupdates.com"

View File

@ -21,6 +21,7 @@ data class MURecord(
val ratingVotes: Int? = null, val ratingVotes: Int? = null,
@SerialName("latest_chapter") @SerialName("latest_chapter")
val latestChapter: Int? = null, val latestChapter: Int? = null,
val authors: List<MUAuthor>? = null,
) )
fun MURecord.toTrackSearch(id: Long): TrackSearch { fun MURecord.toTrackSearch(id: Long): TrackSearch {
@ -36,3 +37,9 @@ fun MURecord.toTrackSearch(id: Long): TrackSearch {
start_date = this@toTrackSearch.year.toString() start_date = this@toTrackSearch.year.toString()
} }
} }
@Serializable
data class MUAuthor(
val type: String? = null,
val name: String? = null,
)

View File

@ -2,9 +2,11 @@ package eu.kanade.tachiyomi.data.track.mdlist
import android.graphics.Color import android.graphics.Color
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.BaseTracker
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.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@ -168,6 +170,21 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
trackPreferences.trackToken(this).delete() trackPreferences.trackToken(this).delete()
} }
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException()
val manga = mdex.getMangaMetadata(track.toDbTrack())
TrackMangaMetadata(
remoteId = 0,
title = manga?.title,
thumbnailUrl = manga?.thumbnail_url, // Doesn't load the actual cover because of Refer header
description = manga?.description,
authors = manga?.author,
artists = manga?.artist,
)
}
}
override val isLoggedIn: Boolean override val isLoggedIn: Boolean
get() = trackPreferences.trackToken(this).get().isNotEmpty() get() = trackPreferences.trackToken(this).get().isNotEmpty()

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.data.track.model
data class TrackMangaMetadata(
val remoteId: Long? = null,
val title: String? = null,
val thumbnailUrl: String? = null,
val description: String? = null,
val authors: String? = null,
val artists: String? = null,
)

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
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.data.track.myanimelist.dto.MALOAuth import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -156,6 +157,10 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
interceptor.setAuth(null) interceptor.setAuth(null)
} }
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
return api.getMangaMetadata(track)
}
fun getIfAuthExpired(): Boolean { fun getIfAuthExpired(): Boolean {
return trackPreferences.trackAuthExpired(this).get() return trackPreferences.trackAuthExpired(this).get()
} }

View File

@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.data.track.myanimelist
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri 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.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch 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.MALListItem
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItemStatus 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.MALManga
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALMangaMetadata
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth 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.MALSearchResult
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
@ -193,6 +195,41 @@ class MyAnimeListApi(
} }
} }
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
return withIOContext {
val url = "$BASE_API_URL/manga".toUri().buildUpon()
.appendPath(track.remoteId.toString())
.appendQueryParameter(
"fields",
"id,title,synopsis,main_picture,authors{first_name,last_name}",
)
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALMangaMetadata>()
.let {
TrackMangaMetadata(
remoteId = it.id,
title = it.title,
thumbnailUrl = it.covers.large.ifEmpty { null } ?: it.covers.medium,
description = it.synopsis,
authors = it.authors
.filter { it.role == "Story" || it.role == "Story & Art" }
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
.joinToString(separator = ", ")
.ifEmpty { null },
artists = it.authors
.filter { it.role == "Art" || it.role == "Story & Art" }
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
.joinToString(separator = ", ")
.ifEmpty { null },
)
}
}
}
}
private suspend fun getListPage(offset: Int): MALUserSearchResult { private suspend fun getListPage(offset: Int): MALUserSearchResult {
return withIOContext { return withIOContext {
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon() val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()

View File

@ -23,4 +23,29 @@ data class MALManga(
@Serializable @Serializable
data class MALMangaCovers( data class MALMangaCovers(
val large: String = "", val large: String = "",
val medium: String,
)
@Serializable
data class MALMangaMetadata(
val id: Long,
val title: String,
val synopsis: String?,
@SerialName("main_picture")
val covers: MALMangaCovers,
val authors: List<MALAuthor>,
)
@Serializable
data class MALAuthor(
val node: MALAuthorNode,
val role: String,
)
@Serializable
data class MALAuthorNode(
@SerialName("first_name")
val firstName: String,
@SerialName("last_name")
val lastName: String,
) )

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
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.data.track.shikimori.dto.SMOAuth import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -98,6 +99,10 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
return track return track
} }
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
return api.getMangaMetadata(track)
}
override fun getLogo() = R.drawable.ic_tracker_shikimori override fun getLogo() = R.drawable.ic_tracker_shikimori
override fun getLogoColor() = Color.rgb(40, 40, 40) override fun getLogoColor() = Color.rgb(40, 40, 40)

View File

@ -3,9 +3,11 @@ package eu.kanade.tachiyomi.data.track.shikimori
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri 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.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch 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.SMAddMangaResponse
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMManga import eu.kanade.tachiyomi.data.track.shikimori.dto.SMManga
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMMetadata
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth 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.SMUser
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListEntry import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListEntry
@ -132,6 +134,65 @@ class ShikimoriApi(
} }
} }
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
return withIOContext {
val query = """
|query(${'$'}ids: String!) {
|mangas(ids: ${'$'}ids) {
|id
|name
|description
|poster {
|originalUrl
|}
|personRoles {
|person {
|name
|}
|rolesEn
|}
|}
|}
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("ids", "${track.remoteId}")
}
}
with(json) {
authClient.newCall(
POST(
"https://shikimori.one/api/graphql",
body = payload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
.parseAs<SMMetadata>()
.let {
if (it.data.mangas.isEmpty()) throw Exception("Could not get metadata from Shikimori")
val manga = it.data.mangas[0]
TrackMangaMetadata(
remoteId = manga.id.toLong(),
title = manga.name,
thumbnailUrl = manga.poster.originalUrl,
description = manga.description,
authors = manga.personRoles
.filter { it.rolesEn.contains("Story") || it.rolesEn.contains("Story & Art") }
.map { it.person.name }
.joinToString(", ")
.ifEmpty { null },
artists = manga.personRoles
.filter { it.rolesEn.contains("Art") || it.rolesEn.contains("Story & Art") }
.map { it.person.name }
.joinToString(", ")
.ifEmpty { null },
)
}
}
}
}
suspend fun accessToken(code: String): SMOAuth { suspend fun accessToken(code: String): SMOAuth {
return withIOContext { return withIOContext {
with(json) { with(json) {

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.data.track.shikimori.dto
import kotlinx.serialization.Serializable
@Serializable
data class SMMetadata(
val data: SMMetadataData,
)
@Serializable
data class SMMetadataData(
val mangas: List<SMMetadataResult>,
)
@Serializable
data class SMMetadataResult(
val id: String,
val name: String,
val description: String,
val poster: SMMangaPoster,
val personRoles: List<SMMangaPersonRoles>,
)
@Serializable
data class SMMangaPoster(
val originalUrl: String,
)
@Serializable
data class SMMangaPersonRoles(
val person: SMPerson,
val rolesEn: List<String>,
)
@Serializable
data class SMPerson(
val name: String,
)

View File

@ -313,6 +313,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return similarHandler.getRelated(manga) return similarHandler.getRelated(manga)
} }
suspend fun getMangaMetadata(track: Track): SManga? {
return mangaHandler.getMangaMetadata(track, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
}
companion object { companion object {
private const val dataSaverPref = "dataSaverV5" private const val dataSaverPref = "dataSaverV5"

View File

@ -3,20 +3,26 @@ package eu.kanade.tachiyomi.ui.manga
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.children import androidx.core.view.children
@ -26,22 +32,34 @@ import coil3.transform.RoundedCornersTransformation
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.databinding.EditMangaDialogBinding import eu.kanade.tachiyomi.databinding.EditMangaDialogBinding
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor
import exh.util.dropBlank import exh.util.dropBlank
import exh.util.trimOrNull import exh.util.trimOrNull
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.model.Track
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun EditMangaDialog( fun EditMangaDialog(
@ -61,6 +79,10 @@ fun EditMangaDialog(
var binding by remember { var binding by remember {
mutableStateOf<EditMangaDialogBinding?>(null) mutableStateOf<EditMangaDialogBinding?>(null)
} }
val showTrackerSelectionDialogue = remember { mutableStateOf(false) }
val getTracks = remember { Injekt.get<GetTracks>() }
val trackerManager = remember { Injekt.get<TrackerManager>() }
val tracks = remember { mutableStateOf(emptyList<Pair<Track, Tracker>>()) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
confirmButton = { confirmButton = {
@ -109,7 +131,7 @@ fun EditMangaDialog(
EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext)) EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext))
.also { binding = it } .also { binding = it }
.apply { .apply {
onViewCreated(manga, factoryContext, this, scope) onViewCreated(manga, factoryContext, this, scope, getTracks, trackerManager, tracks, showTrackerSelectionDialogue)
} }
.root .root
}, },
@ -118,9 +140,61 @@ fun EditMangaDialog(
} }
}, },
) )
if (showTrackerSelectionDialogue.value) {
TrackerSelectDialog(
tracks = tracks.value,
onDismissRequest = { showTrackerSelectionDialogue.value = false },
onTrackerSelect = { tracker, track ->
scope.launch {
autofillFromTracker(binding!!, track, tracker)
}
},
)
}
} }
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope) { @Composable
private fun TrackerSelectDialog(
tracks: List<Pair<Track, Tracker>>,
onDismissRequest: () -> Unit,
onTrackerSelect: (
tracker: Tracker,
track: Track,
) -> Unit,
) {
AlertDialog(
modifier = Modifier.fillMaxWidth(),
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(MR.strings.action_cancel))
}
},
title = {
Text(stringResource(SYMR.strings.select_tracker))
},
text = {
FlowRow(
modifier = Modifier
.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
tracks.forEach { (track, tracker) ->
TrackLogoIcon(
tracker,
onClick = {
onTrackerSelect(tracker, track)
onDismissRequest()
},
)
}
}
},
)
}
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope, getTracks: GetTracks, trackerManager: TrackerManager, tracks: MutableState<List<Pair<Track, Tracker>>>, showTrackerSelectionDialogue: MutableState<Boolean>) {
loadCover(manga, binding) loadCover(manga, binding)
val statusAdapter: ArrayAdapter<String> = ArrayAdapter( val statusAdapter: ArrayAdapter<String> = ArrayAdapter(
@ -203,6 +277,55 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
binding.resetTags.setOnClickListener { resetTags(manga, binding, scope) } binding.resetTags.setOnClickListener { resetTags(manga, binding, scope) }
binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope) } binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope) }
binding.autofillFromTracker.setOnClickListener {
scope.launch {
getTrackers(manga, binding, context, getTracks, trackerManager, tracks, showTrackerSelectionDialogue)
}
}
}
private suspend fun getTrackers(manga: Manga, binding: EditMangaDialogBinding, context: Context, getTracks: GetTracks, trackerManager: TrackerManager, tracks: MutableState<List<Pair<Track, Tracker>>>, showTrackerSelectionDialogue: MutableState<Boolean>) {
tracks.value = getTracks.await(manga.id).map { track ->
track to trackerManager.get(track.trackerId)!!
}
.filterNot { (_, tracker) -> tracker is EnhancedTracker }
if (tracks.value.isEmpty()) {
context.toast(context.stringResource(SYMR.strings.entry_not_tracked))
return
}
if (tracks.value.size > 1) {
showTrackerSelectionDialogue.value = true
return
}
autofillFromTracker(binding, tracks.value.first().first, tracks.value.first().second)
}
private fun setTextIfNotBlank(field: (String) -> Unit, value: String?) {
value?.takeIf { it.isNotBlank() }?.let { field(it) }
}
private suspend fun autofillFromTracker(binding: EditMangaDialogBinding, track: Track, tracker: Tracker) {
try {
val trackerMangaMetadata = tracker.getMangaMetadata(track)
setTextIfNotBlank(binding.title::setText, trackerMangaMetadata?.title)
setTextIfNotBlank(binding.mangaAuthor::setText, trackerMangaMetadata?.authors)
setTextIfNotBlank(binding.mangaArtist::setText, trackerMangaMetadata?.artists)
setTextIfNotBlank(binding.thumbnailUrl::setText, trackerMangaMetadata?.thumbnailUrl)
setTextIfNotBlank(binding.mangaDescription::setText, trackerMangaMetadata?.description)
} catch (e: Throwable) {
tracker.logcat(LogPriority.ERROR, e)
binding.root.context.toast(
binding.root.context.stringResource(
MR.strings.track_error,
tracker.name,
e.message ?: "",
),
)
}
} }
private fun resetTags(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) { private fun resetTags(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) {

View File

@ -119,4 +119,10 @@ data class DummyTracker(
track: eu.kanade.tachiyomi.data.database.models.Track, track: eu.kanade.tachiyomi.data.database.models.Track,
epochMillis: Long, epochMillis: Long,
) = Unit ) = Unit
override suspend fun getMangaMetadata(
track: tachiyomi.domain.track.model.Track,
): eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata = eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata(
0, "test", "test", "test", "test", "test",
)
} }

View File

@ -128,6 +128,36 @@ class MangaHandler(
} }
} }
suspend fun getMangaMetadata(
track: Track,
sourceId: Long,
coverQuality: String,
tryUsingFirstVolumeCover: Boolean,
altTitlesInDesc: Boolean,
): SManga? {
return withIOContext {
val mangaId = MdUtil.getMangaId(track.tracking_url)
val response = service.viewManga(mangaId)
val coverFileName = if (tryUsingFirstVolumeCover) {
service.fetchFirstVolumeCover(response)
} else {
null
}
apiMangaParser.parseToManga(
SManga.create().apply {
url = track.tracking_url
},
sourceId,
response,
emptyList(),
null,
coverFileName,
coverQuality,
altTitlesInDesc,
)
}
}
private suspend fun getSimpleChapters(manga: SManga): List<String> { private suspend fun getSimpleChapters(manga: SManga): List<String> {
return runCatching { service.aggregateChapters(MdUtil.getMangaId(manga.url), lang) } return runCatching { service.aggregateChapters(MdUtil.getMangaId(manga.url), lang) }
.onFailure { .onFailure {

View File

@ -129,6 +129,21 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp" /> android:layout_marginBottom="12dp" />
<Button
android:id="@+id/autofill_from_tracker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@string/fill_from_tracker"
android:textAllCaps="false" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button <Button
android:id="@+id/reset_tags" android:id="@+id/reset_tags"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -148,6 +163,7 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:text="@string/reset_info" android:text="@string/reset_info"
android:textAllCaps="false" /> android:textAllCaps="false" />
</LinearLayout>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -403,6 +403,9 @@
<string name="artist_hint">Artist: %1$s</string> <string name="artist_hint">Artist: %1$s</string>
<string name="thumbnail_url_hint">Thumbnail Url: %1$s</string> <string name="thumbnail_url_hint">Thumbnail Url: %1$s</string>
<string name="multi_tags_comma_separated">Enter tag(s), seperated by commas.</string> <string name="multi_tags_comma_separated">Enter tag(s), seperated by commas.</string>
<string name="select_tracker">Select a tracker</string>
<string name="entry_not_tracked">Entry is not tracked.</string>
<string name="fill_from_tracker">Fill from tracker</string>
<!-- Browse --> <!-- Browse -->
<!-- Sources Tab --> <!-- Sources Tab -->