diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index d6103df1..0cce2f97 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -15,6 +15,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody import okio.ByteString.Companion.encode import org.jsoup.Jsoup +import java.time.LocalDate val session = Session(Requests().baseClient) @@ -729,6 +730,16 @@ object SoraExtractor : SoraStream() { val servers = tryParseJson>(serversKname) + val sub = app.get("$fmoviesAPI/ajax/episode/subtitles/${servers?.get("28")}").text + tryParseJson>(sub)?.map { + subtitleCallback.invoke( + SubtitleFile( + it.label ?: "", + it.file ?: return@map + ) + ) + } + servers?.apmap { server -> val decryptServer = app.get("$fmoviesAPI/ajax/episode/info?id=${server.value}") .parsedSafe()?.url?.let { decodeVrf(it) } ?: return@apmap @@ -738,16 +749,6 @@ object SoraExtractor : SoraStream() { loadExtractor(decryptServer, fmoviesAPI, subtitleCallback, callback) } } - - val sub = app.get("$fmoviesAPI/ajax/episode/subtitles/${servers?.get("28") ?: return}").text - tryParseJson>(sub)?.map { - subtitleCallback.invoke( - SubtitleFile( - it.label ?: "", - it.file ?: return@map - ) - ) - } } suspend fun invokeKisskh( @@ -829,19 +830,17 @@ object SoraExtractor : SoraStream() { } suspend fun invokeAnimes( - id: Int? = null, title: String? = null, - jpTitle: String? = null, epsTitle: String? = null, - year: Int? = null, + date: String?, + airedDate: String?, season: Int? = null, episode: Int? = null, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val (aniId, malId) = app.get( - if (season == null) "$tmdb2anilist/movie/?id=$id" else "$tmdb2anilist/tv/?id=$id&s=$season" - ).parsedSafe().let { it?.anilist_id to it?.mal_id } + + val (aniId, malId) = convertTmdbToAnimeId(title, date, airedDate, if(season == null) TvType.AnimeMovie else TvType.Anime) argamap( { @@ -860,7 +859,7 @@ object SoraExtractor : SoraStream() { } private suspend fun invokeBiliBili( - aniId: String? = null, + aniId: Int? = null, episode: Int? = null, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit @@ -904,66 +903,68 @@ object SoraExtractor : SoraStream() { } private suspend fun invokeZoro( - aniId: String? = null, + aniId: Int? = null, episode: Int? = null, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { val animeId = - app.get("https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/anilist/anime/$aniId.json") - .parsedSafe()?.pages?.zoro?.keys?.firstOrNull() + app.get("https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/anilist/anime/${aniId ?: return}.json") + .parsedSafe()?.pages?.zoro?.keys?.map { it } - val episodeId = app.get("$zoroAPI/ajax/v2/episode/list/${animeId ?: return}") - .parsedSafe()?.html?.let { - Jsoup.parse(it) - }?.select("div.ss-list a")?.find { it.attr("data-number") == "$episode" } - ?.attr("data-id") + animeId?.apmap { id -> + val episodeId = app.get("$zoroAPI/ajax/v2/episode/list/${id ?: return@apmap}") + .parsedSafe()?.html?.let { + Jsoup.parse(it) + }?.select("div.ss-list a")?.find { it.attr("data-number") == "$episode" } + ?.attr("data-id") + + val servers = app.get("$zoroAPI/ajax/v2/episode/servers?episodeId=${episodeId ?: return@apmap}") + .parsedSafe()?.html?.let { Jsoup.parse(it) } + ?.select("div.item.server-item")?.map { + Triple( + it.text(), + it.attr("data-id"), + it.attr("data-type"), + ) + } + + servers?.apmap servers@{ server -> + val iframe = app.get("$zoroAPI/ajax/v2/episode/sources?id=${server.second ?: return@servers}") + .parsedSafe()?.link ?: return@servers + val audio = if(server.third == "sub") "Raw" else "English Dub" + if(server.first == "Vidstreaming" || server.first == "Vidcloud") { + extractRabbitStream( + "${server.first} [$audio]", + iframe, + "$zoroAPI/", + subtitleCallback, + callback, + false, + decryptKey = RabbitStream.getZoroKey() + ) { it } + } else { + loadExtractor(iframe,"$zoroAPI/", subtitleCallback, callback) + } - val servers = app.get("$zoroAPI/ajax/v2/episode/servers?episodeId=${episodeId ?: return}") - .parsedSafe()?.html?.let { Jsoup.parse(it) } - ?.select("div.item.server-item")?.map { - Triple( - it.text(), - it.attr("data-id"), - it.attr("data-type"), - ) } - - servers?.apmap { server -> - val iframe = app.get("$zoroAPI/ajax/v2/episode/sources?id=${server.second ?: return@apmap}") - .parsedSafe()?.link ?: return@apmap - val audio = if(server.third == "sub") "Raw" else "English Dub" - if(server.first == "Vidstreaming" || server.first == "Vidcloud") { - extractRabbitStream( - "${server.first} [$audio]", - iframe, - "$zoroAPI/", - subtitleCallback, - callback, - false, - decryptKey = RabbitStream.getZoroKey() - ) { it } - } else { - loadExtractor(iframe,"$zoroAPI/", subtitleCallback, callback) - } - } } private suspend fun invokeAnimeKaizoku( - malId: String? = null, + malId: Int? = null, epsTitle: String? = null, season: Int? = null, episode: Int? = null, callback: (ExtractorLink) -> Unit ) { - val search = app.get("$animeKaizokuAPI/?s=$malId").document + val search = app.get("$animeKaizokuAPI/?s=${malId ?: return}").document val detailHref = search.select("ul#posts-container li").map { it.selectFirst("a")?.attr("href") } .find { - it?.contains(malId ?: return) == true + it?.contains("$malId") == true }?.let { fixUrl(it, animeKaizokuAPI) } val detail = app.get(detailHref ?: return).document @@ -1467,14 +1468,14 @@ object SoraExtractor : SoraStream() { } suspend fun invokeCrunchyroll( - aniId: String? = null, + aniId: Int? = null, epsTitle: String? = null, season: Int? = null, episode: Int? = null, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val id = getCrunchyrollId(aniId) + val id = getCrunchyrollId("${aniId ?: return}") val audioLocal = listOf( "ja-JP", "en-US", @@ -2875,6 +2876,28 @@ data class BaymoviesConfig( val workers: List ) +data class AniIds( + var id: Int? = null, + var idMal: Int? = null +) + +data class AniMedia( + @JsonProperty("id") var id: Int? = null, + @JsonProperty("idMal") var idMal: Int? = null +) + +data class AniPage( + @JsonProperty("media") var media: java.util.ArrayList = arrayListOf() +) + +data class AniData( + @JsonProperty("Page") var Page: AniPage? = AniPage() +) + +data class AniSearch( + @JsonProperty("data") var data: AniData? = AniData() +) + data class Tmdb2Anilist( @JsonProperty("tmdb_id") val tmdb_id: String? = null, @JsonProperty("anilist_id") val anilist_id: String? = null, @@ -3264,7 +3287,7 @@ data class CrunchyrollSourcesResponses( ) data class MALSyncPages( - @JsonProperty("Zoro") val zoro: HashMap>? = hashMapOf(), + @JsonProperty("Zoro") val zoro: HashMap>? = hashMapOf(), ) data class MALSyncResponses( diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt index 26b874c4..91b183c0 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt @@ -74,8 +74,8 @@ open class SoraStream : TmdbProvider() { companion object { /** TOOLS */ private const val tmdbAPI = "https://api.themoviedb.org/3" - const val tmdb2anilist = "https://tmdb2anilist.slidemovies.org" const val gdbot = "https://gdtot.pro" + const val anilistAPI = "https://graphql.anilist.co" private val apiKey = base64DecodeAPI("ZTM=NTg=MjM=MjM=ODc=MzI=OGQ=MmE=Nzk=Nzk=ZjI=NTA=NDY=NDA=MzA=YjA=") // PLEASE DON'T STEAL @@ -291,7 +291,9 @@ open class SoraStream : TmdbProvider() { airedYear = year, lastSeason = lastSeason, epsTitle = eps.name, - jpTitle = res.alternative_titles?.results?.find { it.iso_3166_1 == "JP" }?.title + jpTitle = res.alternative_titles?.results?.find { it.iso_3166_1 == "JP" }?.title, + date = season.airDate, + airedDate = res.releaseDate ?: res.firstAirDate ).toJson(), name = eps.name, season = eps.seasonNumber, @@ -334,7 +336,8 @@ open class SoraStream : TmdbProvider() { year = year, orgTitle = orgTitle, isAnime = isAnime, - jpTitle = res.alternative_titles?.results?.find { it.iso_3166_1 == "JP" }?.title + jpTitle = res.alternative_titles?.results?.find { it.iso_3166_1 == "JP" }?.title, + airedDate = res.releaseDate ?: res.firstAirDate ).toJson(), ) { this.posterUrl = poster @@ -399,11 +402,10 @@ open class SoraStream : TmdbProvider() { // }, { if (res.isAnime) invokeAnimes( - res.id, res.title, - res.jpTitle, res.epsTitle, - res.airedYear ?: res.year, + res.date, + res.airedDate, res.season, res.episode, subtitleCallback, @@ -790,6 +792,8 @@ open class SoraStream : TmdbProvider() { val lastSeason: Int? = null, val epsTitle: String? = null, val jpTitle: String? = null, + val date: String? = null, + val airedDate: String? = null, ) data class Data( diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt index a921c45b..f47309c8 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt @@ -107,11 +107,10 @@ class SoraStreamLite : SoraStream() { }, { if (res.isAnime) invokeAnimes( - res.id, res.title, - res.jpTitle, res.epsTitle, - res.airedYear ?: res.year, + res.date, + res.airedDate, res.season, res.episode, subtitleCallback, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index dfcc8ec9..6e68efa0 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -2,6 +2,7 @@ package com.hexated import android.util.Base64 import com.fasterxml.jackson.annotation.JsonProperty +import com.hexated.SoraStream.Companion.anilistAPI import com.hexated.SoraStream.Companion.base64DecodeAPI import com.hexated.SoraStream.Companion.baymoviesAPI import com.hexated.SoraStream.Companion.crunchyrollAPI @@ -10,7 +11,6 @@ import com.hexated.SoraStream.Companion.gdbot import com.hexated.SoraStream.Companion.putlockerAPI import com.hexated.SoraStream.Companion.smashyStreamAPI import com.hexated.SoraStream.Companion.tvMoviesAPI -import com.hexated.SoraStream.Companion.twoEmbedAPI import com.hexated.SoraStream.Companion.watchOnlineAPI import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getCaptchaToken @@ -40,7 +40,8 @@ import javax.crypto.spec.SecretKeySpec import kotlin.collections.ArrayList import kotlin.math.min -val soraAPI = base64DecodeAPI("cA==YXA=cy8=Y20=di8=LnQ=b2s=a2w=bG8=aS4=YXA=ZS0=aWw=b2I=LW0=Z2E=Ly8=czo=dHA=aHQ=") +val soraAPI = + base64DecodeAPI("cA==YXA=cy8=Y20=di8=LnQ=b2s=a2w=bG8=aS4=YXA=ZS0=aWw=b2I=LW0=Z2E=Ly8=czo=dHA=aHQ=") val bflixChipperKey = base64DecodeAPI("Yjc=ejM=TzA=YTk=WHE=WnU=bXU=RFo=") val bflixKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" const val kaguyaBaseUrl = "https://kaguya.app/" @@ -535,7 +536,8 @@ suspend fun invokeSmashyRip( subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit, ) { - val script = app.get(url).document.selectFirst("script:containsData(player =)")?.data() ?: return + val script = + app.get(url).document.selectFirst("script:containsData(player =)")?.data() ?: return val source = Regex("file:\\s*\"([^\"]+)").find(script)?.groupValues?.get(1) val subtitle = Regex("subtitle:\\s*\"([^\"]+)").find(script)?.groupValues?.get(1) @@ -957,10 +959,11 @@ suspend fun getCrunchyrollId(aniId: String?): String? { "variables" to variables ).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) - val externalLinks = app.post("https://graphql.anilist.co", requestBody = data) + val externalLinks = app.post(anilistAPI, requestBody = data) .parsedSafe()?.data?.Media?.externalLinks - return (externalLinks?.find { it.site == "VRV" } ?: externalLinks?.find { it.site == "Crunchyroll" })?.url?.let { + return (externalLinks?.find { it.site == "VRV" } + ?: externalLinks?.find { it.site == "Crunchyroll" })?.url?.let { Regex("series/(\\w+)/?").find(it)?.groupValues?.get(1) } } @@ -1011,6 +1014,89 @@ suspend fun PutlockerResponses?.callback( } } +suspend fun convertTmdbToAnimeId( + title: String?, + date: String?, + airedDate: String?, + type: TvType +): AniIds { + val sDate = date?.split("-") + val sAiredDate = airedDate?.split("-") + + val year = sDate?.firstOrNull()?.toIntOrNull() + val airedYear = sAiredDate?.firstOrNull()?.toIntOrNull() + val season = getSeason(sDate?.get(1)?.toIntOrNull()) + val airedSeason = getSeason(sAiredDate?.get(1)?.toIntOrNull()) + + return if (type == TvType.AnimeMovie) { + tmdbToAnimeId(title, airedYear, "", type) + } else { + val ids = tmdbToAnimeId(title, year, season, type) + if (ids.id == null && ids.idMal == null) tmdbToAnimeId( + title, + airedYear, + airedSeason, + type + ) else ids + } +} + +suspend fun tmdbToAnimeId(title: String?, year: Int?, season: String?, type: TvType): AniIds { + val query = """ + query ( + ${'$'}page: Int = 1 + ${'$'}search: String + ${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC] + ${'$'}type: MediaType + ${'$'}season: MediaSeason + ${'$'}seasonYear: Int + ${'$'}format: [MediaFormat] + ) { + Page(page: ${'$'}page, perPage: 20) { + media( + search: ${'$'}search + sort: ${'$'}sort + type: ${'$'}type + season: ${'$'}season + seasonYear: ${'$'}seasonYear + format_in: ${'$'}format + ) { + id + idMal + } + } + } + """.trimIndent().trim() + + val variables = mapOf( + "search" to title, + "sort" to "SEARCH_MATCH", + "type" to "ANIME", + "season" to season?.uppercase(), + "seasonYear" to year, + "format" to listOf(if (type == TvType.AnimeMovie) "MOVIE" else "TV") + ).filterValues { value -> value != null && value.toString().isNotEmpty() } + + val data = mapOf( + "query" to query, + "variables" to variables + ).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) + + val res = app.post(anilistAPI, requestBody = data) + .parsedSafe()?.data?.Page?.media?.firstOrNull() + return AniIds(res?.id, res?.idMal) + +} + +fun getSeason(month: Int?): String? { + val seasons = arrayOf( + "Winter", "Winter", "Spring", "Spring", "Spring", "Summer", + "Summer", "Summer", "Fall", "Fall", "Fall", "Winter" + ) + if(month == null) return null + return seasons[month - 1] +} + fun getPutlockerQuality(quality: String): Int { return when { quality.contains("NAME=\"1080p\"") || quality.contains("RESOLUTION=1920x1080") -> Qualities.P1080.value