From fd120c5081a74393f0f2c174f2b3573b14c6d4e4 Mon Sep 17 00:00:00 2001 From: NGB-Was-Taken <76197326+NGB-Was-Taken@users.noreply.github.com> Date: Mon, 9 Dec 2024 02:10:26 +0545 Subject: [PATCH] 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 --------- Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Co-authored-by: jobobby04 --- .../tachiyomi/data/track/BaseTracker.kt | 5 + .../eu/kanade/tachiyomi/data/track/Tracker.kt | 3 + .../tachiyomi/data/track/anilist/Anilist.kt | 5 + .../data/track/anilist/AnilistApi.kt | 68 ++++++++++ .../data/track/anilist/dto/ALMangaMetadata.kt | 40 ++++++ .../tachiyomi/data/track/bangumi/Bangumi.kt | 5 + .../data/track/bangumi/BangumiApi.kt | 32 +++++ .../data/track/bangumi/dto/BGMSubject.kt | 62 +++++++++ .../tachiyomi/data/track/kitsu/Kitsu.kt | 5 + .../tachiyomi/data/track/kitsu/KitsuApi.kt | 72 ++++++++++ .../track/kitsu/dto/KitsuMangaMetadata.kt | 63 +++++++++ .../data/track/mangaupdates/MangaUpdates.kt | 16 +++ .../track/mangaupdates/MangaUpdatesApi.kt | 8 ++ .../data/track/mangaupdates/dto/MURecord.kt | 7 + .../tachiyomi/data/track/mdlist/MdList.kt | 17 +++ .../data/track/model/TrackMangaMetadata.kt | 10 ++ .../data/track/myanimelist/MyAnimeList.kt | 5 + .../data/track/myanimelist/MyAnimeListApi.kt | 37 +++++ .../data/track/myanimelist/dto/MALManga.kt | 25 ++++ .../data/track/shikimori/Shikimori.kt | 5 + .../data/track/shikimori/ShikimoriApi.kt | 61 +++++++++ .../data/track/shikimori/dto/SMMetadata.kt | 38 ++++++ .../tachiyomi/source/online/all/MangaDex.kt | 4 + .../tachiyomi/ui/manga/EditMangaDialog.kt | 127 +++++++++++++++++- .../main/java/eu/kanade/test/DummyTracker.kt | 6 + .../main/java/exh/md/handlers/MangaHandler.kt | 30 +++++ app/src/main/res/layout/edit_manga_dialog.xml | 36 +++-- .../moko-resources/base/strings.xml | 3 + 28 files changed, 783 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALMangaMetadata.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSubject.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuMangaMetadata.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackMangaMetadata.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMMetadata.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt index 33caf6555453..ce98908ae78e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt @@ -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 @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt index fd3a9f45e431..28ee9395634b 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt @@ -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 @@ -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? } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index db25cc76336c..d401f8f9875d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -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 @@ -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)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 3695b6f25db7..2e0f98ea0bd8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -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 @@ -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() + .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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALMangaMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALMangaMetadata.kt new file mode 100644 index 000000000000..65b6c839d79c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALMangaMetadata.kt @@ -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, +) + +@Serializable +data class ALStaffEdge( + val role: String, + val node: ALStaffNode, +) + +@Serializable +data class ALStaffNode( + val name: ALItemTitle, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index 8eb3ec776993..b8db13fa9fb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -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 @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index bba70d44af1e..ca7b16cb43c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -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 @@ -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, @@ -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() + .let { + TrackMangaMetadata( + remoteId = it.id, + title = it.nameCn, + thumbnailUrl = it.images?.common, + description = it.summary, + authors = it.infobox + .filter { it.key == "作者" } + .filterIsInstance() + .map { it.value } + .joinToString(", "), + artists = it.infobox + .filter { it.key == "插图" } + .filterIsInstance() + .map { it.value } + .joinToString(", "), + ) + } + } + } + } + suspend fun accessToken(code: String): BGMOAuth { return withIOContext { with(json) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSubject.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSubject.kt new file mode 100644 index 000000000000..0da7b0cfa711 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSubject.kt @@ -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, + 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::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + 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, + ) : Infobox +} + +@Serializable +data class InfoboxNestedValue( + @SerialName("k") + val key: String? = null, + @SerialName("v") + val value: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index f0ca4e7204ca..a99ba58a9712 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -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 @@ -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() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 5933c96dd68d..c9cf952e3c00 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -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.KitsuCurrentUserResult 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.KitsuSearchResult +import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.DELETE 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.jsonMime import eu.kanade.tachiyomi.network.parseAs +import eu.kanade.tachiyomi.util.lang.htmlDecode import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject 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() + .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 { private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151" 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 BASE_MANGA_URL = "https://kitsu.app/manga/" private const val ALGOLIA_KEY_URL = "https://kitsu.app/api/edge/algolia-keys/media/" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuMangaMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuMangaMetadata.kt new file mode 100644 index 000000000000..fd864fb3dd0e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuMangaMetadata.kt @@ -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, +) + +@Serializable +data class KitsuMangaStaffNode( + val role: String, + val person: KitsuMangaStaffPerson, +) + +@Serializable +data class KitsuMangaStaffPerson( + val name: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt index c47a799a77d6..b16b3ffe4af0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt @@ -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.copyTo 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.util.lang.htmlDecode import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import tachiyomi.i18n.MR @@ -117,6 +119,20 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker 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? { return trackPreferences.trackPassword(this).get().ifBlank { null } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt index 6f4471df565b..8f85e9a5b54c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt @@ -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() + } + } + companion object { private const val BASE_URL = "https://api.mangaupdates.com" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt index 88a560b9df69..fcca326681a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt @@ -21,6 +21,7 @@ data class MURecord( val ratingVotes: Int? = null, @SerialName("latest_chapter") val latestChapter: Int? = null, + val authors: List? = null, ) fun MURecord.toTrackSearch(id: Long): TrackSearch { @@ -36,3 +37,9 @@ fun MURecord.toTrackSearch(id: Long): TrackSearch { start_date = this@toTrackSearch.year.toString() } } + +@Serializable +data class MUAuthor( + val type: String? = null, + val name: String? = null, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt index 884d16dc4518..524f6e5bb98d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt @@ -2,9 +2,11 @@ package eu.kanade.tachiyomi.data.track.mdlist import android.graphics.Color import dev.icerock.moko.resources.StringResource +import eu.kanade.domain.track.model.toDbTrack 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.model.TrackMangaMetadata import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.SManga @@ -168,6 +170,21 @@ class MdList(id: Long) : BaseTracker(id, "MDList") { 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 get() = trackPreferences.trackToken(this).get().isNotEmpty() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackMangaMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackMangaMetadata.kt new file mode 100644 index 000000000000..0df41b588431 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackMangaMetadata.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index ed744a8807d3..59233fb8752d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth import kotlinx.collections.immutable.ImmutableList @@ -156,6 +157,10 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { interceptor.setAuth(null) } + override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? { + return api.getMangaMetadata(track) + } + fun getIfAuthExpired(): Boolean { return trackPreferences.trackAuthExpired(this).get() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 33435d4c3886..6f7d4ae28bc1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.data.track.myanimelist import android.net.Uri import androidx.core.net.toUri 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.myanimelist.dto.MALListItem import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItemStatus import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALManga +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALMangaMetadata import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser @@ -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() + .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 { return withIOContext { val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt index c4ab92ee92d0..ae0233185cff 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt @@ -23,4 +23,29 @@ data class MALManga( @Serializable data class MALMangaCovers( 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, +) + +@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, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index f04167151338..ff2a6abbb622 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth import kotlinx.collections.immutable.ImmutableList @@ -98,6 +99,10 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { return track } + override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? { + return api.getMangaMetadata(track) + } + override fun getLogo() = R.drawable.ic_tracker_shikimori override fun getLogoColor() = Color.rgb(40, 40, 40) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index b11fc9877b8b..596bf167d3e4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -3,9 +3,11 @@ package eu.kanade.tachiyomi.data.track.shikimori import android.net.Uri import androidx.core.net.toUri 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.shikimori.dto.SMAddMangaResponse 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.SMUser 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() + .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 { return withIOContext { with(json) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMMetadata.kt new file mode 100644 index 000000000000..3a444c9e9643 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMMetadata.kt @@ -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, +) + +@Serializable +data class SMMetadataResult( + val id: String, + val name: String, + val description: String, + val poster: SMMangaPoster, + val personRoles: List, +) + +@Serializable +data class SMMangaPoster( + val originalUrl: String, +) + +@Serializable +data class SMMangaPersonRoles( + val person: SMPerson, + val rolesEn: List, +) + +@Serializable +data class SMPerson( + val name: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt index 3bcefe2fce13..fdfe640cc350 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt @@ -313,6 +313,10 @@ class MangaDex(delegate: HttpSource, val context: Context) : return similarHandler.getRelated(manga) } + suspend fun getMangaMetadata(track: Track): SManga? { + return mangaHandler.getMangaMetadata(track, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc()) + } + companion object { private const val dataSaverPref = "dataSaverV5" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt index 5b6e96f381eb..3c3b346ba96d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -3,20 +3,26 @@ package eu.kanade.tachiyomi.ui.manga import android.content.Context import android.view.LayoutInflater import android.widget.ArrayAdapter +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow 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.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat 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.ChipGroup import com.google.android.material.dialog.MaterialAlertDialogBuilder +import eu.kanade.presentation.track.components.TrackLogoIcon 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.source.model.SManga import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor import exh.util.dropBlank import exh.util.trimOrNull import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import logcat.LogPriority import tachiyomi.core.common.i18n.stringResource +import tachiyomi.core.common.util.system.logcat 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.sy.SYMR import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.source.local.isLocal +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get @Composable fun EditMangaDialog( @@ -61,6 +79,10 @@ fun EditMangaDialog( var binding by remember { mutableStateOf(null) } + val showTrackerSelectionDialogue = remember { mutableStateOf(false) } + val getTracks = remember { Injekt.get() } + val trackerManager = remember { Injekt.get() } + val tracks = remember { mutableStateOf(emptyList>()) } AlertDialog( onDismissRequest = onDismissRequest, confirmButton = { @@ -109,7 +131,7 @@ fun EditMangaDialog( EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext)) .also { binding = it } .apply { - onViewCreated(manga, factoryContext, this, scope) + onViewCreated(manga, factoryContext, this, scope, getTracks, trackerManager, tracks, showTrackerSelectionDialogue) } .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>, + 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>>, showTrackerSelectionDialogue: MutableState) { loadCover(manga, binding) val statusAdapter: ArrayAdapter = ArrayAdapter( @@ -203,6 +277,55 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial binding.resetTags.setOnClickListener { resetTags(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>>, showTrackerSelectionDialogue: MutableState) { + 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) { diff --git a/app/src/main/java/eu/kanade/test/DummyTracker.kt b/app/src/main/java/eu/kanade/test/DummyTracker.kt index 4c77e660d99e..1c1064c99ab2 100644 --- a/app/src/main/java/eu/kanade/test/DummyTracker.kt +++ b/app/src/main/java/eu/kanade/test/DummyTracker.kt @@ -119,4 +119,10 @@ data class DummyTracker( track: eu.kanade.tachiyomi.data.database.models.Track, epochMillis: Long, ) = 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", + ) } diff --git a/app/src/main/java/exh/md/handlers/MangaHandler.kt b/app/src/main/java/exh/md/handlers/MangaHandler.kt index 9bd9d8165023..7666662ace9f 100644 --- a/app/src/main/java/exh/md/handlers/MangaHandler.kt +++ b/app/src/main/java/exh/md/handlers/MangaHandler.kt @@ -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 { return runCatching { service.aggregateChapters(MdUtil.getMangaId(manga.url), lang) } .onFailure { diff --git a/app/src/main/res/layout/edit_manga_dialog.xml b/app/src/main/res/layout/edit_manga_dialog.xml index 1f7d67ebb284..334d86993666 100644 --- a/app/src/main/res/layout/edit_manga_dialog.xml +++ b/app/src/main/res/layout/edit_manga_dialog.xml @@ -130,24 +130,40 @@ android:layout_marginBottom="12dp" />