diff --git a/src/all/torrentioanime/build.gradle b/src/all/torrentioanime/build.gradle index 52ec219c..258789ef 100644 --- a/src/all/torrentioanime/build.gradle +++ b/src/all/torrentioanime/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Torrentio Anime (Torrent / Debrid)' extClass = '.Torrentio' - extVersionCode = 14 + extVersionCode = 15 containsNsfw = false } diff --git a/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/AniListQueries.kt b/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/AniListQueries.kt index 9eb474e0..f2925ab2 100644 --- a/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/AniListQueries.kt +++ b/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/AniListQueries.kt @@ -29,7 +29,8 @@ fun anilistQuery() = """ startDate_like: %year, seasonYear: %seasonYear, season: %season, - format_in: %format + format_in: %format, + isAdult: false ) { id title { @@ -103,7 +104,7 @@ fun anilistLatestQuery() = """ fun getDetailsQuery() = """ query media(%id: Int) { - Media(id: %id) { + Media(id: %id, isAdult: false) { id title { romaji @@ -137,23 +138,3 @@ query media(%id: Int) { } } """.toQuery() - -fun getEpisodeQuery() = """ -query media(%id: Int, %type: MediaType) { - Media(id: %id, type: %type) { - episodes - nextAiringEpisode { - episode - } - } -} -""".toQuery() - -fun getMalIdQuery() = """ -query media(%id: Int, %type: MediaType) { - Media(id: %id, type: %type) { - idMal - id - } -} -""".toQuery() diff --git a/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/Torrentio.kt b/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/Torrentio.kt index 007f9cc6..021cf57f 100644 --- a/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/Torrentio.kt +++ b/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/Torrentio.kt @@ -10,10 +10,10 @@ import androidx.preference.ListPreference import androidx.preference.MultiSelectListPreference import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.AniZipResponse import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.AnilistMeta import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.AnilistMetaLatest import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.DetailsById -import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.EpisodeList import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.StreamDataTorrent import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.model.AnimeFilterList @@ -66,6 +66,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() { .add("query", query) .add("variables", variables) .build() + return POST("https://graphql.anilist.co", body = requestBody) } @@ -148,7 +149,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() { override fun popularAnimeParse(response: Response): AnimesPage { val jsonData = response.body.string() - return parseSearchJson(jsonData) } + return parseSearchJson(jsonData) + } // =============================== Latest =============================== override fun latestUpdatesRequest(page: Int): Request { @@ -300,41 +302,55 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() { // ============================== Episodes ============================== override fun episodeListRequest(anime: SAnime): Request { - return GET("https://anime-kitsu.strem.fun/meta/series/anilist%3A${anime.url}.json") + return GET("https://api.ani.zip/mappings?anilist_id=${anime.url}") } override fun episodeListParse(response: Response): List { val responseString = response.body.string() - val episodeList = json.decodeFromString(responseString) + val aniZipResponse = json.decodeFromString(responseString) - return when (episodeList.meta?.type) { - "series" -> { - episodeList.meta.videos - ?.let { videos -> - if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.released?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } } + return when (aniZipResponse.mappings?.type) { + "TV" -> { + aniZipResponse.episodes + ?.let { episodes -> + if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { + episodes + } else { + episodes.filter { (_, episode) -> (episode?.airDate?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } + } } - ?.map { video -> + ?.mapNotNull { (_, episode) -> + val episodeNumber = runCatching { episode?.episode?.toFloat() }.getOrNull() + + if (episodeNumber == null) { + return@mapNotNull null + } + + val title = episode?.title?.get("en") + SEpisode.create().apply { - episode_number = video.episode?.toFloat() ?: 0.0F - url = "/stream/series/${video.videoId}.json" - date_upload = video.released?.let { parseDate(it) } ?: 0L - name = "Episode ${video.episode} : ${ - video.title?.removePrefix("Episode ") - ?.replaceFirst("\\d+\\s*".toRegex(), "") - ?.trim() - }" - scanlator = (video.released?.let { parseDate(it) } ?: 0L).takeIf { it > System.currentTimeMillis() }?.let { "Upcoming" } ?: "" + episode_number = episodeNumber + url = "/stream/series/kitsu:${aniZipResponse.mappings.kitsuId}:${String.format(Locale.ENGLISH, "%.0f", episodeNumber)}.json" + date_upload = episode?.airDate?.let { parseDate(it) } ?: 0L + name = if (title == null) "Episode ${episode?.episode}" else "Episode ${episode.episode}: $title" + scanlator = (episode?.airDate?.let { parseDate(it) } ?: 0L).takeIf { it > System.currentTimeMillis() }?.let { "Upcoming" } ?: "" } }.orEmpty().reversed() } - "movie" -> { - // Handle movie response + "MOVIE" -> { + val dateUpload = if (!aniZipResponse.episodes.isNullOrEmpty()) { + aniZipResponse.episodes["1"]?.airDate?.let { parseDate(it) } ?: 0L + } else { + 0L + } + listOf( SEpisode.create().apply { episode_number = 1.0F - url = "/stream/movie/${episodeList.meta.kitsuId}.json" + url = "/stream/movie/kitsu:${aniZipResponse.mappings.kitsuId}.json" name = "Movie" + date_upload = dateUpload }, ).reversed() } @@ -342,6 +358,12 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() { else -> emptyList() } } + + private fun parseDateTime(dateStr: String): Long { + return runCatching { DATE_TIME_FORMATTER.parse(dateStr)?.time } + .getOrNull() ?: 0L + } + private fun parseDate(dateStr: String): Long { return runCatching { DATE_FORMATTER.parse(dateStr)?.time } .getOrNull() ?: 0L @@ -421,6 +443,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() { udp://www.torrent.eu.org:451/announce, ${fetchTrackers().split("\n").joinToString(",")} """.trimIndent() + return streamList.streams?.map { stream -> val urlOrHash = if (debridProvider == "none") { @@ -875,8 +898,12 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() { private const val IS_EFFICIENT_KEY = "efficient" private const val IS_EFFICIENT_DEFAULT = false - private val DATE_FORMATTER by lazy { + private val DATE_TIME_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH) } + + private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) + } } } diff --git a/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/dto/AniZipDto.kt b/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/dto/AniZipDto.kt new file mode 100644 index 00000000..b555f3f2 --- /dev/null +++ b/src/all/torrentioanime/src/eu/kanade/tachiyomi/animeextension/all/torrentioanime/dto/AniZipDto.kt @@ -0,0 +1,67 @@ +package eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AniZipResponse( + val titles: Map? = null, + val episodes: Map? = null, + val episodeCount: Int? = null, + val specialCount: Int? = null, + val images: List? = null, + val mappings: AniZipMappings? = null, +) + +@Serializable +data class AniZipEpisode( + val episode: String? = null, + val episodeNumber: Int? = null, + val absoluteEpisodeNumber: Int? = null, + val seasonNumber: Int? = null, + val title: Map? = null, + val length: Int? = null, + val runtime: Int? = null, + @SerialName("airdate") + val airDate: String? = null, + val rating: String? = null, + @SerialName("anidbEid") + val aniDbEpisodeId: Long? = null, + val tvdbShowId: Long? = null, + val tvdbId: Long? = null, + val overview: String? = null, + val image: String? = null, +) + +@Serializable +data class AniZipImage( + val coverType: String? = null, + val url: String? = null, +) + +@Serializable +data class AniZipMappings( + @SerialName("animeplanet_id") + val animePlanetId: String? = null, + @SerialName("kitsu_id") + val kitsuId: Long? = null, + @SerialName("mal_id") + val myAnimeListId: Long? = null, + val type: String? = null, + @SerialName("anilist_id") + val aniListId: Long? = null, + @SerialName("anisearch_id") + val aniSearchId: Long? = null, + @SerialName("anidb_id") + val aniDbId: Long? = null, + @SerialName("notifymoe_id") + val notifyMoeId: String? = null, + @SerialName("livechart_id") + val liveChartId: Long? = null, + @SerialName("thetvdb_id") + val theTvDbId: Long? = null, + @SerialName("imdb_id") + val imdbId: String? = null, + @SerialName("themoviedb_id") + val theMovieDbId: String? = null, +)