
(cherry picked from commit 5b2e937d5fdeadc8a9a994b99b3f6b7a277f2513) # Conflicts: # app/src/main/java/eu/kanade/domain/source/model/SourcePagingSourceType.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt
312 lines
11 KiB
Kotlin
312 lines
11 KiB
Kotlin
package exh.recs
|
|
|
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
import eu.kanade.tachiyomi.network.GET
|
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
import eu.kanade.tachiyomi.network.POST
|
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
import eu.kanade.tachiyomi.network.parseAs
|
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
import eu.kanade.tachiyomi.source.model.SManga
|
|
import exh.util.MangaType
|
|
import exh.util.mangaType
|
|
import kotlinx.serialization.json.Json
|
|
import kotlinx.serialization.json.JsonArray
|
|
import kotlinx.serialization.json.JsonElement
|
|
import kotlinx.serialization.json.JsonObject
|
|
import kotlinx.serialization.json.buildJsonObject
|
|
import kotlinx.serialization.json.contentOrNull
|
|
import kotlinx.serialization.json.jsonArray
|
|
import kotlinx.serialization.json.jsonObject
|
|
import kotlinx.serialization.json.jsonPrimitive
|
|
import kotlinx.serialization.json.put
|
|
import logcat.LogPriority
|
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
import okhttp3.MediaType.Companion.toMediaType
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
|
import tachiyomi.core.util.system.logcat
|
|
import tachiyomi.data.source.NoResultsException
|
|
import tachiyomi.data.source.SourcePagingSource
|
|
import tachiyomi.domain.manga.model.Manga
|
|
import tachiyomi.domain.track.interactor.GetTracks
|
|
import uy.kohesive.injekt.Injekt
|
|
import uy.kohesive.injekt.api.get
|
|
import uy.kohesive.injekt.injectLazy
|
|
|
|
abstract class API(val endpoint: String) {
|
|
val client by lazy {
|
|
Injekt.get<NetworkHelper>().client
|
|
}
|
|
val json by injectLazy<Json>()
|
|
|
|
abstract suspend fun getRecsBySearch(search: String): List<SManga>
|
|
|
|
abstract suspend fun getRecsById(id: String): List<SManga>
|
|
}
|
|
|
|
class MyAnimeList : API("https://api.jikan.moe/v4/") {
|
|
override suspend fun getRecsById(id: String): List<SManga> {
|
|
val apiUrl = endpoint.toHttpUrl()
|
|
.newBuilder()
|
|
.addPathSegment("manga")
|
|
.addPathSegment(id)
|
|
.addPathSegment("recommendations")
|
|
.build()
|
|
|
|
val data = with(json) { client.newCall(GET(apiUrl)).awaitSuccess().parseAs<JsonObject>() }
|
|
return data["data"]!!.jsonArray
|
|
.map { it.jsonObject["entry"]!!.jsonObject }
|
|
.map { rec ->
|
|
logcat { "MYANIMELIST > RECOMMENDATION: " + rec["title"]!!.jsonPrimitive.content }
|
|
SManga(
|
|
title = rec["title"]!!.jsonPrimitive.content,
|
|
url = rec["url"]!!.jsonPrimitive.content,
|
|
thumbnail_url = rec["images"]
|
|
?.let(JsonElement::jsonObject)
|
|
?.let(::getImage),
|
|
initialized = true,
|
|
)
|
|
}
|
|
}
|
|
|
|
fun getImage(imageObject: JsonObject): String? {
|
|
return imageObject["webp"]
|
|
?.jsonObject
|
|
?.get("image_url")
|
|
?.jsonPrimitive
|
|
?.contentOrNull
|
|
?: imageObject["jpg"]
|
|
?.jsonObject
|
|
?.get("image_url")
|
|
?.jsonPrimitive
|
|
?.contentOrNull
|
|
}
|
|
|
|
override suspend fun getRecsBySearch(search: String): List<SManga> {
|
|
val url = endpoint.toHttpUrl()
|
|
.newBuilder()
|
|
.addPathSegment("manga")
|
|
.addQueryParameter("q", search)
|
|
.build()
|
|
|
|
val data = with(json) {
|
|
client.newCall(GET(url)).awaitSuccess()
|
|
.parseAs<JsonObject>()
|
|
}
|
|
return getRecsById(data["data"]!!.jsonArray.first().jsonObject["mal_id"]!!.jsonPrimitive.content)
|
|
}
|
|
}
|
|
|
|
class Anilist : API("https://graphql.anilist.co/") {
|
|
private fun countOccurrence(arr: JsonArray, search: String): Int {
|
|
return arr.count {
|
|
val synonym = it.jsonPrimitive.content
|
|
synonym.contains(search, true)
|
|
}
|
|
}
|
|
|
|
private fun languageContains(obj: JsonObject, language: String, search: String): Boolean {
|
|
return obj["title"]?.jsonObject?.get(language)?.jsonPrimitive?.contentOrNull?.contains(search, true) == true
|
|
}
|
|
|
|
private fun getTitle(obj: JsonObject): String {
|
|
val titleObj = obj["title"]!!.jsonObject
|
|
|
|
val english = titleObj["english"]?.jsonPrimitive?.contentOrNull
|
|
val romaji = titleObj["romaji"]?.jsonPrimitive?.contentOrNull
|
|
val native = titleObj["native"]?.jsonPrimitive?.contentOrNull
|
|
val synonym = obj["synonyms"]!!.jsonArray.getOrNull(0)?.jsonPrimitive?.contentOrNull
|
|
|
|
val isJP = obj["countryOfOrigin"]?.jsonPrimitive?.contentOrNull == "JP"
|
|
|
|
return when {
|
|
!english.isNullOrBlank() -> english
|
|
isJP && !romaji.isNullOrBlank() -> romaji
|
|
!synonym.isNullOrBlank() -> synonym
|
|
!isJP && !romaji.isNullOrBlank() -> romaji
|
|
else -> native ?: "NO NAME FOUND"
|
|
}
|
|
}
|
|
|
|
private suspend fun getRecs(
|
|
query: String,
|
|
variables: JsonObject,
|
|
queryParam: String? = null,
|
|
filter: List<JsonElement>.() -> List<JsonElement> = { this },
|
|
): List<SManga> {
|
|
val payload = buildJsonObject {
|
|
put("query", query)
|
|
put("variables", variables)
|
|
}
|
|
val payloadBody = payload.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
|
|
|
|
val data = with(json) {
|
|
client.newCall(POST(endpoint, body = payloadBody)).awaitSuccess()
|
|
.parseAs<JsonObject>()
|
|
}
|
|
|
|
val media = data["data"]!!
|
|
.jsonObject["Page"]!!
|
|
.jsonObject["media"]!!
|
|
.jsonArray
|
|
.ifEmpty { throw Exception("'$queryParam' not found") }
|
|
.filter()
|
|
|
|
return media.flatMap { it.jsonObject["recommendations"]!!.jsonObject["edges"]!!.jsonArray }.map {
|
|
val rec = it.jsonObject["node"]!!.jsonObject["mediaRecommendation"]!!.jsonObject
|
|
val recTitle = getTitle(rec)
|
|
logcat { "ANILIST > RECOMMENDATION: $recTitle" }
|
|
SManga(
|
|
title = recTitle,
|
|
thumbnail_url = rec["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content,
|
|
initialized = true,
|
|
url = rec["siteUrl"]!!.jsonPrimitive.content,
|
|
)
|
|
}
|
|
}
|
|
|
|
override suspend fun getRecsById(id: String): List<SManga> {
|
|
val query =
|
|
"""
|
|
|query Recommendations(${'$'}id: Int!) {
|
|
|Page {
|
|
|media(id: ${'$'}id, type: MANGA) {
|
|
|recommendations {
|
|
|edges {
|
|
|node {
|
|
|mediaRecommendation {
|
|
|countryOfOrigin
|
|
|siteUrl
|
|
|title {
|
|
|romaji
|
|
|english
|
|
|native
|
|
|}
|
|
|synonyms
|
|
|coverImage {
|
|
|large
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|
|
|
""".trimMargin()
|
|
val variables = buildJsonObject {
|
|
put("id", id)
|
|
}
|
|
|
|
return getRecs(
|
|
query = query,
|
|
variables = variables,
|
|
)
|
|
}
|
|
|
|
override suspend fun getRecsBySearch(search: String): List<SManga> {
|
|
val query =
|
|
"""
|
|
|query Recommendations(${'$'}search: String!) {
|
|
|Page {
|
|
|media(search: ${'$'}search, type: MANGA) {
|
|
|title {
|
|
|romaji
|
|
|english
|
|
|native
|
|
|}
|
|
|synonyms
|
|
|recommendations {
|
|
|edges {
|
|
|node {
|
|
|mediaRecommendation {
|
|
|countryOfOrigin
|
|
|siteUrl
|
|
|title {
|
|
|romaji
|
|
|english
|
|
|native
|
|
|}
|
|
|synonyms
|
|
|coverImage {
|
|
|large
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|}
|
|
|
|
|
""".trimMargin()
|
|
val variables = buildJsonObject {
|
|
put("search", search)
|
|
}
|
|
return getRecs(
|
|
queryParam = search,
|
|
query = query,
|
|
variables = variables,
|
|
filter = {
|
|
filter {
|
|
val jsonObject = it.jsonObject
|
|
languageContains(jsonObject, "romaji", search) ||
|
|
languageContains(jsonObject, "english", search) ||
|
|
languageContains(jsonObject, "native", search) ||
|
|
countOccurrence(jsonObject["synonyms"]!!.jsonArray, search) > 0
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
open class RecommendsPagingSource(
|
|
source: CatalogueSource,
|
|
private val manga: Manga,
|
|
private val smart: Boolean = true,
|
|
private var preferredApi: API = API.MYANIMELIST,
|
|
) : SourcePagingSource(source) {
|
|
val getTracks: GetTracks by injectLazy()
|
|
|
|
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
|
if (smart) preferredApi = if (manga.mangaType() != MangaType.TYPE_MANGA) API.ANILIST else preferredApi
|
|
|
|
val apiList = API_MAP.toList().sortedByDescending { it.first == preferredApi }
|
|
|
|
val tracks = getTracks.await(manga.id)
|
|
|
|
val recs = apiList.firstNotNullOfOrNull { (key, api) ->
|
|
try {
|
|
val id = when (key) {
|
|
API.MYANIMELIST -> tracks.find { it.syncId == TrackManager.MYANIMELIST }?.remoteId
|
|
API.ANILIST -> tracks.find { it.syncId == TrackManager.ANILIST }?.remoteId
|
|
}
|
|
|
|
val recs = if (id != null) {
|
|
api.getRecsById(id.toString())
|
|
} else {
|
|
api.getRecsBySearch(manga.ogTitle)
|
|
}
|
|
logcat { key.toString() + " > Results: " + recs.size }
|
|
recs.ifEmpty { null }
|
|
} catch (e: Exception) {
|
|
logcat(LogPriority.ERROR, e) { key.toString() }
|
|
null
|
|
}
|
|
} ?: throw NoResultsException()
|
|
|
|
return MangasPage(recs, false)
|
|
}
|
|
|
|
companion object {
|
|
val API_MAP = mapOf(
|
|
API.MYANIMELIST to MyAnimeList(),
|
|
API.ANILIST to Anilist(),
|
|
)
|
|
|
|
enum class API { MYANIMELIST, ANILIST }
|
|
}
|
|
}
|