tsg&1c6E2dpFMy6TyzPT0?YJ%!r#WGbhD5KqAX6)mr$D6*qnHolWk7}u;
zoSNoETC*L;XHwshM-XN_Qim`oVXi4}EdM;W)GJ3{*jwp--+P_SY#-L8Pe}jCHKc=0
z=wh8lHS!${#h?`)ln|RShX46a(Ur@XS!>CB*_j3^tH{mCX*2bzsYPtd*sb-uXx(4U
z@Z8xA3vMIosPs_D=H}**ggTw%a^!`*NybTEos09aS%m=gpPaV-tE;PZU2h3Ljd$f|
zB928j85=9T!^baOw2bncvWtu$RYnm^58QNKYE4
z%r_#~^j7c}ivT`1BJzZWJ%d_35ri)5#=Eg{_AaCGs@|jiXb`FGXhd`Az`N|#FQ7w?
zvdnmSa;vAWU&!~>|6UCtI}M>4u#z#8s#Qx0O(YY0TL!tBJwHExGMp#AJM6gQ9Tb!`
z%$Ap%+tF!Txc+ERIYg>NfV!!P7lbuk0}QCVgoK1tJKmfVvRCoCX@UKGtO$ySQ&ae=
zowyMEiT6F|?$E;isy0deAaRr{npXZ;3o_htHvnj9X$`2os%fBn5Gr}wt?lq;S9Q;-9
zT8BAe$M=eJ(NCmkvw4-rZ~b7Cv43vZS{Kw9Kso=7sO%PaL20Sd4M{nKuwqvF|=h}54?
zK_>0+3E@I(lC?hVb|TDH;eP?I9ybt%afxkLC#@3;B3e~9d<(=okN~aL)Tq(pcjA
z+TD+L5YGLnxtQ^@;Pki5X#rf;qe{q>WjcfZOoL-3Z{(sF^pw?D(NZpak}dp4_XF(F
z@4P4jv$EreKZ!a;*MrNa8`m^1rPCOY^C7qC{z3P64`C{43KtLkPv^iZ2Leux1w
zs;z+_<`c=g_lsh=
z?1HY_j}PFR)A~@AmnzHS_+b!(XG$Vl`0yx$lN4C$n_^G`K1(-O`?3`7}0ha3}@nYQ2g)a}t
zMv)dnx;>|393f{9-Bqk!u>}8Ag~y~0en`Q-VXk4P!P!OiywWsmo^D>BytIRLl?^yB
z!ebjwtol>8Y*xhe*HQ%$FX+l77{9KlK9W=(>9Mb-P$HwnQxZMkU30AkRT1FJI?*nN
zI-g1hG+3|<^^&NpRVCB%Dp*#4st8N-YZHTrI-X7%6)kug1fH-fkxM7%vI{90_|4lA
zP!VRf<@9ao`xeWm+R;8Vv^Z28j>k&Qs0-eXU`QSpo8ZN!XA@eXhGj1QX{nJ?I^k0a
zx=ud6hmX0GDF*pDQFav|59mls^B#`TD`Ta-hjS#g92KKT9XZde-5~Kc$CU$am9p)N
zLQM!?{1oSV_!A{l3yO5Y+1c!-F`
z(INO~R(Nvy`LRusoSMZQc*->TxWaqcl-?MxfA#B!pOLb1%U=ARakiYYIBMPqz;rG7
z$4UA(q~Os5`_@Z$SADQZsq}e7GU)~ct1u(P>#EA!@?NAElp|92wv7|>;9UXb0q7!a
ze+9QN%GfouPqbu7?W&gz?>A5y|HSSuo2bg$lN$NM&}CC~f(}+i0v%_Ne7hjesiF#7lrIl0`tMODJgUH}!ZUVE)SO>hnNh5pSoWohk6>
zLr{{jo5TgNs|CPz?ugmJ$XnVsYb#$rBB0XWobU+fuP!upyGVPTw$GDhhv&DPXoGS8
zwWxovPz@rE|H$N~Y9DD`HYp&FSx#^<1NC&_jLX>Juf;D&r-SH^q++NF1#T?>^gn-8<8Z%LqHTr8tKH%&5wD|Nh?yMvB_GJsCrGm;^S|)HG?O1kD
z{v$6RZ=266h#1Ma+Un@XPnHa7@QU^(>94;!dyopb82tXWkgEyD_T)0tL6z#LEN94~
zjMhuAp{4p4&N_-WJ^q=@6bk(~l_3@@TT@&Lgq
z0bNkd!rAAvonp}T>*
zyI!5mJP3PhswXgi?u#GT;{i*Rjbk&d7+{cLm^=cLA)4Sp7eWbd6HR2hIur`#MP0+B
z&UINgwP?LSNG^nx9nBx{S)<)7_0u6tu-@jS$J`Fv@w|Y-pGr;#sc+Lq$HuklEa2j#
zt?VM~I{TpAmXs!@E0&HncB-KJ2gOSk
zvY59f!I?ndlUgS5Op*3`9>){sh-4-L;F&z)Tgb;?HDlTLn!KNc5s6)_QAb0y>=}!XV>vQH
zWZ$;1Ezu$D70b}<2Tg+Avh3v}631slq^?%K0Pd6NQ`
zntPO76iqvBv|irdR0yajpRf%zJdj6e=e9HwqWHQ%8l_r>+gs69z~DXG
z2R@W?ckWFHkMQ_33ju_Nmb7fd^%ku*YhfK$IIH9==<~l{rpHQNXE{&Q`eSygvsgI!^cw!Kbi$XImK0D&W8dk!AG7hHKmdiPN*#
z$FNp|Nb`tKx8~+}2zDlzY`dM@$_dUDK7GF`$cB^j&D4RpO+{@DAS|RFaIQPR@&4x6
z!FSY47C~A}-aE4MEaF~16D;DY3Q%CsuR}dJum6>~%TN#&IK==j{imHuAq(|v+9DcV
z_=d#wn>{D3AVRS$!b5f@s8iiaLk@R8WB+aNNfn-yiWV`*Ia9bsEd^iN6Exllo8tI!
zLR|T1(zu(Ub=_nS;fwzM`pv*5qwe8{%g`#o_J@o?PE=bo(b%k%MBm!mmzEU+l~MLZ
zcdW_+7bfizTu0nROaZ6H`7esP0A-)4Ck)1=i^7+;U+!C~`+l9TT{mZr7dv3O(zGLk
zpX8SFmUT$Qn*de3;bjw5{RIm@s9`K4r#WpREoOn)RLLFUv3MTb%{IjQ&5Nbv>`bwT
zvk#f`PvP{C0N>@^#8GdFN8|G!Ij|li6IOI)m;Q3Smtj$-K?A2irHuiSjeKqN19yDO
zex&t00~N&`>P*>k!Vm4&-AsX?^u_xW9~=1@2*#-ktl2-@Q5v*x8c6l^iHtmpFvXJw
z}8;NO^isp=&7cEN)TJbnoi@ophosc8+CPlo=U4bcpUVZOiK87R1PEy07;V
zdp>^AXE|(aVKX#_u`f~VFm~t*Sh)(wiyAEJA6x?m?#xyy_HpdLDz7JW*vN!hxPKo{
z6_dZ3F~pOmSb&7N#`k=QGI}iUQxag%^2J2hz?YbHvt>Z6ci*vPG?kQ#CMz-ENatfX
z0o+|Nvvg!Tx7z{+-H?K3M{f|(LtV<8G}{W;`TYn#BOZ#)!7?ok(ip~s!YFHwzwy9b
zyB^NXIpGA6{;oGgQ<#aCB4bY9G;R75YxML5>2}H`9OcZAgeBzU6Yr?QossALGl~K6
zgLh}b3^`lL4!@xnikr3U$4pu}dl|2=cL#n_;C3N_jPM1Mc(X~h>Ib!ceWC-e@j_Yj
zsuI`Xc2swJm+Zl#EKOXhVys|T1xjGG{}mtEL-){rW8kI1e3pBC!O@s~nGvMUn?++8
z{kY~TEd-u6D43c=TFfq9kVj!V^p)v2W+}D7;p49-4_ofTc+zB$TGll^XoWGUuv8Ds
zkKj^VGWW@~+3zyv!R6$iv$sO`LGe)~y?;|t1=DA;UjonD@7_T|t+i-9$XF+rzr~z=
z<=f6us4PorrZxq9H23-rJftpFs^K%L6c9@Zg?6X?FSYUIeedQteegdP{C?UYyFL)|
zUlQpjOH2&i&@CKW5uVFg$^AEv)FO{<3`cTcOk^A3;>FVEBd#!=hqM!<*#@7GrGtv?
zjF{~;xMs^hjrbUTSX1d5ni0z(h#0=da7a1&4*6`Fzv`C;YLw{v9u5BEP*
zd_51RN{l~k{K%CzN~VCF
zcGGH->I{-h6_DbO_aK^|vx0f0um54a9ldYc#kh-}vsl+R1#~Km@C0%-2#Q_uj;Q;Q
zNMz-Wnvf28nGG=5D4sXHg3^)Trcad~2&DooK&L%e$azn8^ol
z7R8lYXr7B(JbN=qtecsci3xQzlPora@Cn+Z*R{T3V8d?>tNx$U8CvOdA^tNLrJ~#F
zZjxjd$L!=-7kwkoN0SG}ztcN!i)c-qqHcSSP&WW$7a{>;^_|G|J
z5=JJj62fb7)KESf{bHP)8$}~}fx$Siv$Kcmn?@K!^xjp@BIkC!y-erPOo-Y^AaRai
zI^8O#ksqdNQq!ZhsfJ&|ex#6`hLEtj|&)#dqvz`cSMkM)ZaBO)5<UPi>Woxz6y}bL?
z**TLa*Wv|dWG*fKFi7H&Hx%Rw%W&5ZU#%fEjPfB(`eQ(2Z%|52&{V&aW|Kj#tmws55pM(
().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ private val context = Injekt.get()
+ private val handler by lazy { Handler(Looper.getMainLooper()) }
+
+ // ============================== Anilist API Request ===================
+ private fun makeGraphQLRequest(query: String, variables: String): Request {
+ val requestBody = FormBody.Builder()
+ .add("query", query)
+ .add("variables", variables)
+ .build()
+
+ return POST("https://graphql.anilist.co", body = requestBody)
+ }
+
+ // ============================== Anilist Meta List ======================
+ private fun anilistQuery(): String {
+ return """
+ query (${"$"}page: Int, ${"$"}perPage: Int, ${"$"}sort: [MediaSort], ${"$"}search: String) {
+ Page(page: ${"$"}page, perPage: ${"$"}perPage) {
+ pageInfo{
+ currentPage
+ hasNextPage
+ }
+ media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in:[RELEASING,FINISHED]) {
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ coverImage {
+ extraLarge
+ large
+ }
+ description
+ status
+ tags{
+ name
+ }
+ genres
+ studios {
+ nodes {
+ name
+ }
+ }
+ countryOfOrigin
+ isAdult
+ }
+ }
+ }
+ """.trimIndent()
+ }
+
+ private fun anilistLatestQuery(): String {
+ return """
+ query (${"$"}page: Int, ${"$"}perPage: Int, ${"$"}sort: [AiringSort]) {
+ Page(page: ${"$"}page, perPage: ${"$"}perPage) {
+ pageInfo {
+ currentPage
+ hasNextPage
+ }
+ airingSchedules(
+ airingAt_greater: 0
+ airingAt_lesser: ${System.currentTimeMillis() / 1000 - 10000}
+ sort: ${"$"}sort
+ ) {
+ media{
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ coverImage {
+ extraLarge
+ large
+ }
+ description
+ status
+ tags{
+ name
+ }
+ genres
+ studios {
+ nodes {
+ name
+ }
+ }
+ countryOfOrigin
+ isAdult
+ }
+ }
+ }
+ }
+ """.trimIndent()
+ }
+
+ private fun parseSearchJson(jsonLine: String?, isLatestQuery: Boolean = false): AnimesPage {
+ val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
+ val metaData: Any = if (!isLatestQuery) {
+ json.decodeFromString(jsonData)
+ } else {
+ json.decodeFromString(jsonData)
+ }
+
+ val mediaList = when (metaData) {
+ is AnilistMeta -> metaData.data?.page?.media.orEmpty()
+ is AnilistMetaLatest -> metaData.data?.page?.airingSchedules.orEmpty().map { it.media }
+ else -> emptyList()
+ }
+
+ val hasNextPage: Boolean = when (metaData) {
+ is AnilistMeta -> metaData.data?.page?.pageInfo?.hasNextPage ?: false
+ is AnilistMetaLatest -> metaData.data?.page?.pageInfo?.hasNextPage ?: false
+ else -> false
+ }
+
+ 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("
"), "\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
+ }
+
+ return AnimesPage(animeList, hasNextPage)
+ }
+
+ // ============================== Popular ===============================
+ override fun popularAnimeRequest(page: Int): Request {
+ val variables = """
+ {
+ "page": $page,
+ "perPage": 30,
+ "sort": "TRENDING_DESC"
+ }
+ """.trimIndent()
+
+ return makeGraphQLRequest(anilistQuery(), variables)
+ }
+
+ override fun popularAnimeParse(response: Response): AnimesPage {
+ val jsonData = response.body.string()
+ return parseSearchJson(jsonData) }
+
+ // =============================== Latest ===============================
+ override fun latestUpdatesRequest(page: Int): Request {
+ val variables = """
+ {
+ "page": $page,
+ "perPage": 30,
+ "sort": "TIME_DESC"
+ }
+ """.trimIndent()
+
+ return makeGraphQLRequest(anilistLatestQuery(), variables)
+ }
+
+ override fun latestUpdatesParse(response: Response): AnimesPage {
+ val jsonData = response.body.string()
+ return parseSearchJson(jsonData, true)
+ }
+
+ // =============================== Search ===============================
+ override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
+ return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
+ val id = query.removePrefix(PREFIX_SEARCH)
+ client.newCall(GET("$baseUrl/anime/$id"))
+ .awaitSuccess()
+ .use(::searchAnimeByIdParse)
+ } else {
+ super.getSearchAnime(page, query, filters)
+ }
+ }
+
+ private fun searchAnimeByIdParse(response: Response): AnimesPage {
+ val details = animeDetailsParse(response)
+ return AnimesPage(listOf(details), false)
+ }
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
+ val variables = """
+ {
+ "page": $page,
+ "perPage": 30,
+ "sort": "POPULARITY_DESC",
+ "search": "$query"
+ }
+ """.trimIndent()
+
+ return makeGraphQLRequest(anilistQuery(), variables)
+ }
+
+ override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
+ // =========================== Anime Details ============================
+
+ override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
+
+ override suspend fun getAnimeDetails(anime: SAnime): SAnime {
+ val query = """
+ query(${"$"}id: Int){
+ Media(id: ${"$"}id){
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ coverImage {
+ extraLarge
+ large
+ }
+ description
+ status
+ tags{
+ name
+ }
+ genres
+ studios {
+ nodes {
+ name
+ }
+ }
+ countryOfOrigin
+ isAdult
+ }
+ }
+ """.trimIndent()
+
+ val variables = """{"id": ${anime.url}}"""
+
+ val metaData = runCatching {
+ json.decodeFromString(client.newCall(makeGraphQLRequest(query, variables)).execute().body.string())
+ }.getOrNull()?.data?.media
+
+ anime.title = metaData?.title?.let { title ->
+ when (preferences.getString(PREF_TITLE_KEY, "romaji")) {
+ "romaji" -> title.romaji
+ "english" -> (metaData.title.english?.takeIf { it.isNotBlank() } ?: metaData.title.romaji).toString()
+ "native" -> title.native
+ else -> ""
+ }
+ } ?: ""
+
+ anime.thumbnail_url = metaData?.coverImage?.extraLarge
+ anime.description = metaData?.description
+ ?.replace(Regex("
"), "\n")
+ ?.replace(Regex("<.*?>"), "")
+ ?: "No Description"
+
+ anime.status = when (metaData?.status) {
+ "RELEASING" -> SAnime.ONGOING
+ "FINISHED" -> SAnime.COMPLETED
+ "HIATUS" -> SAnime.ON_HIATUS
+ "NOT_YET_RELEASED" -> SAnime.LICENSED
+ else -> SAnime.UNKNOWN
+ }
+
+ // Extracting tags, genres, and studios
+ val tagsList = metaData?.tags?.mapNotNull { it.name } ?: emptyList()
+ val genresList = metaData?.genres ?: emptyList()
+ val studiosList = metaData?.studios?.nodes?.mapNotNull { it.name } ?: emptyList()
+
+ anime.genre = (tagsList + genresList).toSet().sorted().joinToString()
+ anime.author = studiosList.sorted().joinToString()
+
+ return anime
+ }
+
+ // ============================== Episodes ==============================
+ override fun episodeListRequest(anime: SAnime): Request {
+ return GET("https://anime-kitsu.strem.fun/meta/series/anilist%3A${anime.url}.json")
+ }
+
+ override fun episodeListParse(response: Response): List {
+ val responseString = response.body.string()
+ val episodeList = json.decodeFromString(responseString)
+
+ return when (episodeList.meta?.type) {
+ "series" -> {
+ episodeList.meta.videos?.filter { video ->
+ (video.released?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis()
+ }?.map { video ->
+ 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()
+ }"
+ }
+ }.orEmpty().reversed()
+ }
+
+ "movie" -> {
+ // Handle movie response
+ val movieId = episodeList.meta.kitsuId?.substringAfterLast(":")?.toIntOrNull() ?: 0
+ listOf(
+ SEpisode.create().apply {
+ episode_number = 1.0F
+ url = "/stream/movie/$movieId.json"
+ name = "Movie"
+ },
+ ).reversed()
+ }
+
+ else -> emptyList()
+ }
+ }
+ private fun parseDate(dateStr: String): Long {
+ return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
+ .getOrNull() ?: 0L
+ }
+
+ // ============================ Video Links =============================
+
+ override fun videoListRequest(episode: SEpisode): Request {
+ val mainURL = buildString {
+ append("$baseUrl/")
+
+ val appendQueryParam: (String, Set?) -> Unit = { key, values ->
+ values?.takeIf { it.isNotEmpty() }?.let {
+ append("$key=${it.filter(String::isNotBlank).joinToString(",")}|")
+ }
+ }
+
+ appendQueryParam("providers", preferences.getStringSet(PREF_PROVIDER_KEY, PREF_PROVIDERS_DEFAULT))
+ appendQueryParam("language", preferences.getStringSet(PREF_LANG_KEY, PREF_LANG_DEFAULT))
+ appendQueryParam("qualityfilter", preferences.getStringSet(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT))
+
+ val sortKey = preferences.getString(PREF_SORT_KEY, "quality")
+ appendQueryParam("sort", sortKey?.let { setOf(it) })
+
+ val token = preferences.getString(PREF_TOKEN_KEY, null)
+ val debridProvider = preferences.getString(PREF_DEBRID_KEY, null)
+
+ when {
+ token.isNullOrBlank() && debridProvider != "none" -> {
+ handler.post {
+ context.let {
+ Toast.makeText(
+ it,
+ "Kindly input the token in the extension settings.",
+ Toast.LENGTH_LONG,
+ ).show()
+ }
+ }
+ throw UnsupportedOperationException()
+ }
+ !token.isNullOrBlank() && debridProvider != "none" -> append("$debridProvider=$token|")
+ }
+ append(episode.url)
+ }.removeSuffix("|")
+ return GET(mainURL)
+ }
+
+ override fun videoListParse(response: Response): List