Add relations and recommendations

This commit is contained in:
Cuong-Tran
2025-06-11 03:11:49 +07:00
parent d01a0c402a
commit bd42ee1d63
3 changed files with 216 additions and 36 deletions

View File

@@ -102,6 +102,85 @@ fun anilistLatestQuery() = """
}
""".toQuery()
// relations{edges{id relationType(version:2)node{id title{userPreferred}format type status(version:2)bannerImage coverImage{large}}}}
fun getRelationsById() = """
query Relations(%id: Int!) {
Page {
media(id: %id, type: ANIME) {
relations {
edges {
node {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
}
}
}
""".toQuery()
fun getRecommendationsById() = """
query Recommendations(%id: Int!, %page: Int) {
Page {
media(id: %id, type: ANIME) {
recommendations(page:%page, sort:[RATING_DESC,ID]) {
edges {
node {
mediaRecommendation {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
}
}
}
}
""".toQuery()
fun getDetailsQuery() = """
query media(%id: Int) {
Media(id: %id, isAdult: false) {

View File

@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.animeextension.all.torrentioanime
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
@@ -25,6 +26,8 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import extensions.utils.getPreferencesLazy
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
@@ -90,42 +93,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
val animeList = mediaList
.filterNot { (it?.countryOfOrigin == "CN" || it?.isAdult == true) && isLatestQuery }
.map { media ->
val anime = SAnime.create().apply {
url = media?.id.toString()
title = when (preferences.getString(PREF_TITLE_KEY, "romaji")) {
"romaji" -> media?.title?.romaji.toString()
"english" -> (media?.title?.english?.takeIf { it.isNotBlank() } ?: media?.title?.romaji).toString()
"native" -> media?.title?.native.toString()
else -> ""
}
thumbnail_url = media?.coverImage?.extraLarge
description = media?.description
?.replace(Regex("<br><br>"), "\n")
?.replace(Regex("<.*?>"), "")
?: "No Description"
status = when (media?.status) {
"RELEASING" -> SAnime.ONGOING
"FINISHED" -> SAnime.COMPLETED
"HIATUS" -> SAnime.ON_HIATUS
"NOT_YET_RELEASED" -> SAnime.LICENSED
else -> SAnime.UNKNOWN
}
// Extracting tags
val tagsList = media?.tags?.mapNotNull { it.name }.orEmpty()
// Extracting genres
val genresList = media?.genres.orEmpty()
genre = (tagsList + genresList).toSet().sorted().joinToString()
// Extracting studios
val studiosList = media?.studios?.nodes?.mapNotNull { it.name }.orEmpty()
author = studiosList.sorted().joinToString()
initialized = true
}
anime
.mapNotNull { media ->
media?.toSAnime(preferences.getString(PREF_TITLE_KEY, "romaji"))
}
return AnimesPage(animeList, hasNextPage)
@@ -235,6 +204,75 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// ============================== Suggestions ===============================
override fun relatedAnimeListRequest(anime: SAnime): Request {
val variables = buildJsonObject {
put("id", anime.url)
}.toString()
return makeGraphQLRequest(getRelationsById(), variables)
}
override fun relatedAnimeListParse(response: Response): List<SAnime> {
val jsonData = response.body.string()
val metaData = json.decodeFromString<AnilistMeta>(jsonData)
val mediaList = metaData.data?.page?.media
?.mapNotNull { it.relations?.edges }?.flatten()
?.mapNotNull { it.node }
?: emptyList()
return mediaList
.filterNot { it.countryOfOrigin == "CN" || it.isAdult }
.map { media ->
media.toSAnime(preferences.getString(PREF_TITLE_KEY, "romaji"))
}
}
override suspend fun getRelatedAnimeListBySearch(
anime: SAnime,
pushResults: suspend (relatedAnime: Pair<String, List<SAnime>>, completed: Boolean) -> Unit,
) {
coroutineScope {
(1..3).map { page ->
launch {
runCatching {
client.newCall(recommendationsRequest(anime, page))
.awaitSuccess()
.use(::recommendationsParse)
}
.onSuccess { if (it.isNotEmpty()) pushResults(Pair("${anime.title}-$page", it), false) }
.onFailure { e ->
Log.e(this::class.simpleName, "## getRelatedAnimeListBySearch: $e")
}
}
}
}
}
private fun recommendationsRequest(anime: SAnime, page: Int): Request {
val variables = buildJsonObject {
put("id", anime.url)
put("page", page)
}.toString()
return makeGraphQLRequest(getRecommendationsById(), variables)
}
private fun recommendationsParse(response: Response): List<SAnime> {
val jsonData = response.body.string()
val metaData = json.decodeFromString<AnilistMeta>(jsonData)
val mediaList = metaData.data?.page?.media
?.mapNotNull { it.recommendations?.edges }?.flatten()
?.mapNotNull { it.node.mediaRecommendation }
?: emptyList()
return mediaList
.filterNot { it.countryOfOrigin == "CN" || it.isAdult }
.map { media ->
media.toSAnime(preferences.getString(PREF_TITLE_KEY, "romaji"))
}
}
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AniListFilters.FILTER_LIST

View File

@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto
import eu.kanade.tachiyomi.animesource.model.SAnime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -80,6 +81,68 @@ data class AnilistMedia(
val seasonYear: Int? = null,
val countryOfOrigin: String? = null,
val isAdult: Boolean = false,
val recommendations: AnilistRecommendations? = null,
val relations: AnilistRelations? = null,
) {
fun toSAnime(userPreferredTitle: String?) = SAnime.create().apply {
url = id.toString()
title = when (userPreferredTitle) {
"romaji" -> this@AnilistMedia.title?.romaji.toString()
"english" -> (this@AnilistMedia.title?.english?.takeIf { it.isNotBlank() } ?: this@AnilistMedia.title?.romaji).toString()
"native" -> this@AnilistMedia.title?.native.toString()
else -> ""
}
thumbnail_url = coverImage?.extraLarge
description = description
?.replace(Regex("<br><br>"), "\n")
?.replace(Regex("<.*?>"), "")
?: "No Description"
status = when (this@AnilistMedia.status) {
"RELEASING" -> SAnime.ONGOING
"FINISHED" -> SAnime.COMPLETED
"HIATUS" -> SAnime.ON_HIATUS
"NOT_YET_RELEASED" -> SAnime.LICENSED
else -> SAnime.UNKNOWN
}
// Extracting tags
val tagsList = tags?.mapNotNull { it.name }.orEmpty()
// Extracting genres
val genresList = genres.orEmpty()
genre = (tagsList + genresList).toSet().sorted().joinToString()
// Extracting studios
val studiosList = studios?.nodes?.mapNotNull { it.name }.orEmpty()
author = studiosList.sorted().joinToString()
initialized = true
}
}
@Serializable
data class AnilistRelations(
val edges: List<AnilistRelationsEdge>,
)
@Serializable
data class AnilistRelationsEdge(
val node: AnilistMedia?,
)
@Serializable
data class AnilistRecommendations(
val edges: List<AnilistRecommendationEdge>,
)
@Serializable
data class AnilistRecommendationEdge(
val node: MediaRecommendation,
)
@Serializable
data class MediaRecommendation(
val mediaRecommendation: AnilistMedia?,
)
@Serializable