komikku/app/src/main/java/exh/recs/sources/MangaUpdatesPagingSource.kt
Tim Schneeberger 9a31ffcc41
feat(recommendation): batch processing for recommendations & sort by relevancy (jobobby04/TachiyomiSY#1383)
* refactor: use NoResultsException

* refactor: cleanup RecommendationPagingSources

* refactor: turn wake/wifi lock functions into reusable extensions

* feat: implement batch recommendation (initial version)

* fix: serialization issues

* fix: wrong source id

* refactor: increase performance using virtual paging

* refactor: update string

* refactor: handle 404 of MD source correctly

* style: add newline

* refactor: create universal throttle manager

* refactor: throttle requests

* chore: remove unused strings

* feat: rank recommendations by match count

* feat: add badges indicating match count to batch recommendations

* fix: handle rec search with no results

* fix: validate flags in pre-search bottom sheet

* feat: implement 'hide library entries' for recommendation search using custom SmartSearchEngine for library items

* style: run spotless

* fix: cancel button

* fix: racing condition causing loss of state

* Avoid runBlocking in a property getter

* Remove unnecessary `synchronized`

* Fixes screen staying on in library tab. (jobobby04/TachiyomiSY#1451)

(cherry picked from commit 254980695bc39fe064e7aeb181ecdbf70c1eff95)

---------

Co-authored-by: Cuong-Tran <cuongtran.tm@gmail.com>
Co-authored-by: NGB-Was-Taken <76197326+NGB-Was-Taken@users.noreply.github.com>
2025-06-12 15:19:34 +07:00

114 lines
4 KiB
Kotlin

package exh.recs.sources
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.SManga
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.source.NoResultsException
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.sy.SYMR
abstract class MangaUpdatesPagingSource(manga: Manga) : TrackerRecommendationPagingSource(
"https://api.mangaupdates.com/v1/",
manga,
) {
override val name: String
get() = "MangaUpdates"
override val associatedTrackerId: Long
get() = trackerManager.mangaUpdates.id
protected abstract val recommendationJsonObjectName: String
override suspend fun getRecsById(id: String): List<SManga> {
val apiUrl = endpoint.toHttpUrl()
.newBuilder()
.addPathSegment("series")
.addPathSegment(id)
.build()
val data = with(json) { client.newCall(GET(apiUrl)).awaitSuccess().parseAs<JsonObject>() }
return getRecommendations(data[recommendationJsonObjectName]!!.jsonArray)
}
private fun getRecommendations(recommendations: JsonArray): List<SManga> {
return recommendations
.map(JsonElement::jsonObject)
.map { rec ->
logcat { "MANGAUPDATES > RECOMMENDATION: " + rec["series_name"]!!.jsonPrimitive.content }
SManga(
title = rec["series_name"]!!.jsonPrimitive.content,
url = rec["series_url"]!!.jsonPrimitive.content,
thumbnail_url = rec["series_image"]
?.jsonObject
?.get("url")
?.jsonObject
?.get("original")
?.jsonPrimitive
?.contentOrNull,
initialized = true,
)
}
}
override suspend fun getRecsBySearch(search: String): List<SManga> {
val url = endpoint.toHttpUrl()
.newBuilder()
.addPathSegments("series/search")
.build()
.toString()
val payload = buildJsonObject {
put("search", search)
put("stype", "title")
}
val body = payload
.toString()
.toRequestBody("application/json; charset=utf-8".toMediaType())
val data = with(json) {
client.newCall(POST(url, body = body))
.awaitSuccess()
.parseAs<JsonObject>()
}
return getRecsById(
data["results"]!!
.jsonArray
.ifEmpty { throw NoResultsException() }
.first()
.jsonObject["record"]!!
.jsonObject["series_id"]!!
.jsonPrimitive.content,
)
}
}
class MangaUpdatesCommunityPagingSource(manga: Manga) : MangaUpdatesPagingSource(manga) {
override val category: StringResource
get() = SYMR.strings.community_recommendations
override val recommendationJsonObjectName: String
get() = "recommendations"
}
class MangaUpdatesSimilarPagingSource(manga: Manga) : MangaUpdatesPagingSource(manga) {
override val category: StringResource
get() = SYMR.strings.similar_titles
override val recommendationJsonObjectName: String
get() = "category_recommendations"
}