From 383996dcef32e9372d7ccbbe7cc56f224c0a0a84 Mon Sep 17 00:00:00 2001 From: Nitin Dahale <43776393+ni3x@users.noreply.github.com> Date: Sun, 9 Jun 2024 11:06:30 +0530 Subject: [PATCH] feat(src/all): New source: Torrentio (#3297) --- src/all/torrentio/AndroidManifest.xml | 22 + src/all/torrentio/build.gradle | 8 + .../torrentio/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1784 bytes .../torrentio/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1058 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2272 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4152 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6193 bytes .../animeextension/all/torrentio/Torrentio.kt | 867 ++++++++++++++++++ .../all/torrentio/TorrentioUrlActivity.kt | 41 + .../all/torrentio/dto/TorrentioDto.kt | 115 +++ 10 files changed, 1053 insertions(+) create mode 100644 src/all/torrentio/AndroidManifest.xml create mode 100644 src/all/torrentio/build.gradle create mode 100644 src/all/torrentio/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/all/torrentio/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/all/torrentio/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/all/torrentio/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/all/torrentio/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/all/torrentio/src/eu/kanade/tachiyomi/animeextension/all/torrentio/Torrentio.kt create mode 100644 src/all/torrentio/src/eu/kanade/tachiyomi/animeextension/all/torrentio/TorrentioUrlActivity.kt create mode 100644 src/all/torrentio/src/eu/kanade/tachiyomi/animeextension/all/torrentio/dto/TorrentioDto.kt diff --git a/src/all/torrentio/AndroidManifest.xml b/src/all/torrentio/AndroidManifest.xml new file mode 100644 index 0000000000..360fba20a4 --- /dev/null +++ b/src/all/torrentio/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/all/torrentio/build.gradle b/src/all/torrentio/build.gradle new file mode 100644 index 0000000000..9c35d4543d --- /dev/null +++ b/src/all/torrentio/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Torrentio (Torrent / Debrid)' + extClass = '.Torrentio' + extVersionCode = 1 + containsNsfw = false +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/torrentio/res/mipmap-hdpi/ic_launcher.png b/src/all/torrentio/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..317a43c2d62e60bf2274dc6873e31577a9f68ebb GIT binary patch literal 1784 zcmVUo zKn@TNIRT#m#0NlvuaiUZewFS?Pj%1I)6+eR=1G5z++FqRy?<4`s(Rh+Sga!*=}1R9 z((=+J*xP2nr@BbDd7+IYux|wgxm@miHk-XIzu(I5*Yf*KE5WiE+wtMNmydo$S4^c+ zBck-riHQl-PQWL8bNcE>3iDGG{e{9zCUd`?VEOcS>>)Y6ViG|tCQ6>R$uWDN)gc{! zU!W3+#096%)ryYtLd;hvYe$0c34)Tz3N2*SSwQ%2SW}=&8rZa*Uw6w!g&`loy+zJ5FV_AiX%H4xU?9 zz3*{ufaiC2~1FS>cNPKzpN2JO%u?b5y;Ngm})L`o1bpzpO4x+gKV*1%aSXit+Q z?PEtZF)1>FFwPFhQB12+C`>mia98T2$OvMowF7b`ax#ae)L4FAjqWZKP-;q~7pIHL zjczYg)X#l8aATKw--$>F!XWk|EV-;I=6w^16LYHP!!6bQ*>(ZLXAZl{0rauwvCHlK z=(HNmFSyE?(DlR~>?%iS_yoynaq#St-n6kVsb(o3KY2*)xgkf4FLw(VdSb~{4vaj$ zSX2(OMs_L_gdNy}UD#J@RSTUU&O~hcF+8z4>oUL#Q4j>Mhw|8`&u$)NLMMnh&e@3V zf2m$F5fwoIyRffREDDt%QV$#=IY4qoG8-?2C<($Y?88p4R7t1=k)I(2!701cD&&PI z3BoSy!%plq?}thdsR;5kx_2t31NjM;EGL`e|9 zKJ3I^Gfsv_(4_2~FRkiry;skagFk`}2PO#nu#=s&&T%{Lg-Q@PV|J^_BQq|e%E61E zgU=jsl>+#xBO{0vA#UY+NROIEm4g>S123$(%7MWP#{v_CeL7!sb;Gri zg-g(zCw0S_*~+_TC%g#idHa}t z=Wp}}C8!}IsNONiCW&5Vy6t;QM(3?!a%v^0Au*`ltC%@b3fFI0&&C1}R9MBt-slLb z^FD|q*B%}sItk*8_v&%g^TAdif`%Hh51Od+PWq`OHS*Y^4$9x zN-XMsX~;(72uSL^x+LRna2*9L@~4+;um zTf_1}`dY{t#UN;kjAGb1=~_{glRnqLoHX$LRO9%Z4xFr3Hdw*TazW5MPcH3AWoPN(bhtwt#@609$K>@R&Z z5fOgvlSm})+Wr<(P+meCLCu1CjiB$G%GwF|#0Pcb@Gimo8T?~1L=Y@pS_ zCz;}x8Xg|LX8GG=a8Opd1bYO@R4Vn4Lt$G0U+_r=z`x>S#_|=PeF>Eg#N+W@6p3O; za`i;30Uz*1eELs(JA;ogpVP}JaY0J3B17@U*x1-T!5@O(1i!Z!uo>I$;R`-lzV#J( z>{A$a`vnQXoM1_?+~U9od>I$?$7+~bUP1t4DDs~Q##$WsfG@s+a0Tqgr8gBs;O?)vLL!wV68ZQxFj8!7~8;ptnzy}lk zRYV2#(FZf0Z`dxwY?p4?ZMP(xp~*Y?$#b3LK$?Ur161(4CDogL6oy z)4vmm#JI>+Hhz#)<|$!YOg0N;a+pPy3#@x}mUTUvW4`Al4t&Qk_z)xKHGn1{8jTi2 zuCj>&%wK`QEQ?(jV_i??nQNoM{Le}(bZ44Ht`%9|#c>XN$1(U2gIMGsS2vy>nt(_o zQWUw$*0g}c^hxF?GM?9E7QQ*fltNAyBLgvrMGkT?h8!cIH3Fhn3e5d>iS^u_VX2Wp zoxc%~gItV(v1Gph`vsi1G0EKPODt9y)A^bO#=uw@Q}(H_R{%frRIqodD8-HpTq}QM$sbKeGP|tTcrkVW2<_AjjgJ~8(SKTwiZ`}t;Lo0Z!Oxh zxl0`-(ALdeMNvjG&0K-jLlY2>$0tOtvhhC+NM(wGf2zry^asaQI2_KYT cBkKhF54>#@&&p}lYXATM07*qoM6N<$g0WTOUH||9 literal 0 HcmV?d00001 diff --git a/src/all/torrentio/res/mipmap-xhdpi/ic_launcher.png b/src/all/torrentio/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2e4d185ce12ba393a8ff37a27aac236f8f61ccc3 GIT binary patch literal 2272 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 HW6b{mIWWZ3 literal 0 HcmV?d00001 diff --git a/src/all/torrentio/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/torrentio/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bba11b734f7aff30d1dc4fbda33a625fdfde4468 GIT binary patch literal 6193 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.getSharedPreferences("source_$id", 0x0000) + } + + private val context = Injekt.get() + private val handler by lazy { Handler(Looper.getMainLooper()) } + + // ============================== JustWatch API Request =================== + private fun makeGraphQLRequest(query: String, variables: String): Request { + val requestBody = """ + {"query": "${query.replace("\n", "")}", "variables": $variables} + """.trimIndent().toRequestBody("application/json; charset=utf-8".toMediaType()) + + return POST("https://apis.justwatch.com/graphql", headers = headers, body = requestBody) + } + + // ============================== JustWatch Api Query ====================== + private fun justWatchQuery(): String { + return """ + query GetPopularTitles( + ${"$"}country: Country!, + ${"$"}first: Int!, + ${"$"}language: Language!, + ${"$"}offset: Int, + ${"$"}searchQuery: String, + ${"$"}packages: [String!]!, + ${"$"}objectTypes: [ObjectType!]!, + ${"$"}popularTitlesSortBy: PopularTitlesSorting!, + ${"$"}releaseYear: IntFilter + ) { + popularTitles( + country: ${"$"}country + first: ${"$"}first + offset: ${"$"}offset + sortBy: ${"$"}popularTitlesSortBy + filter: { + objectTypes: ${"$"}objectTypes, + searchQuery: ${"$"}searchQuery, + packages: ${"$"}packages, + genres: [], + excludeGenres: [], + releaseYear: ${"$"}releaseYear + } + ) { + edges { + node { + id + objectType + content(country: ${"$"}country, language: ${"$"}language) { + fullPath + title + shortDescription + externalIds { + imdbId + } + posterUrl + genres { + translation(language: ${"$"}language) + } + credits { + name + role + } + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + } + } + } + """.trimIndent() + } + + private fun parseSearchJson(jsonLine: String?): AnimesPage { + val jsonData = jsonLine ?: return AnimesPage(emptyList(), false) + val popularTitlesResponse = json.decodeFromString(jsonData) + + val edges = popularTitlesResponse.data?.popularTitles?.edges.orEmpty() + val hasNextPage = popularTitlesResponse.data?.popularTitles?.pageInfo?.hasNextPage ?: false + + val metaList = edges + .mapNotNull { edge -> + val node = edge.node ?: return@mapNotNull null + val content = node.content ?: return@mapNotNull null + + SAnime.create().apply { + url = "${content.externalIds?.imdbId ?: ""},${node.objectType ?: ""},${content.fullPath ?: ""}" + title = content.title ?: "" + thumbnail_url = "https://images.justwatch.com${content.posterUrl?.replace("{profile}", "s276")?.replace("{format}", "webp")}" + description = content.shortDescription ?: "" + val genresList = content.genres?.mapNotNull { it.translation }.orEmpty() + genre = genresList.joinToString() + + val directors = content.credits?.filter { it.role == "DIRECTOR" }?.mapNotNull { it.name } + author = directors?.joinToString() + val actors = content.credits?.filter { it.role == "ACTOR" }?.take(4)?.mapNotNull { it.name } + artist = actors?.joinToString() + initialized = true + } + } + + return AnimesPage(metaList, hasNextPage) + } + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int): Request { + return searchAnimeRequest(page, "", AnimeFilterList()) + } + + override fun popularAnimeParse(response: Response): AnimesPage { + val jsonData = response.body.string() + return parseSearchJson(jsonData) } + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() + + override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() + + // =============================== 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", headers)) + .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 country = preferences.getString(PREF_REGION_KEY, PREF_REGION_DEFAULT) + val language = preferences.getString(PREF_JW_LANG_KEY, PREF_JW_LANG_DEFAULT) + val perPage = 40 + val packages = "" + val year = 0 + val objectTypes = "" + val variables = """ + { + "first": $perPage, + "offset": ${(page - 1) * perPage}, + "platform": "WEB", + "country": "$country", + "language": "$language", + "searchQuery": "${query.replace(searchQueryRegex, "").trim()}", + "packages": [$packages], + "objectTypes": [$objectTypes], + "popularTitlesSortBy": "TRENDING", + "releaseYear": { + "min": $year, + "max": $year + } + } + """.trimIndent() + + return makeGraphQLRequest(justWatchQuery(), variables) + } + + private val searchQueryRegex by lazy { + Regex("[^A-Za-z0-9 ]") + } + + override fun searchAnimeParse(response: Response) = popularAnimeParse(response) + + // =========================== Anime Details ============================ + + override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException() + + // override suspend fun getAnimeDetails(anime: SAnime): SAnime = throw UnsupportedOperationException() + + override suspend fun getAnimeDetails(anime: SAnime): SAnime { + val query = """ + query GetUrlTitleDetails(${"$"}fullPath: String!, ${"$"}country: Country!, ${"$"}language: Language!) { + urlV2(fullPath: ${"$"}fullPath) { + node { + ...TitleDetails + } + } + } + + fragment TitleDetails on Node { + ... on MovieOrShowOrSeason { + id + objectType + content(country: ${"$"}country, language: ${"$"}language) { + title + shortDescription + externalIds { + imdbId + } + posterUrl + genres { + translation(language: ${"$"}language) + } + } + } + } + """.trimIndent() + + val country = preferences.getString(PREF_REGION_KEY, PREF_REGION_DEFAULT) + val language = preferences.getString(PREF_JW_LANG_KEY, PREF_JW_LANG_DEFAULT) + val variables = """ + { + "fullPath": "${anime.url.split(',').last()}", + "country": "$country", + "language": "$language" + } + """.trimIndent() + + val content = runCatching { + json.decodeFromString(client.newCall(makeGraphQLRequest(query, variables)).execute().body.string()) + }.getOrNull()?.data?.urlV2?.node?.content + + anime.title = content?.title ?: "" + anime.thumbnail_url = "https://images.justwatch.com${content?.posterUrl?.replace("{profile}", "s718")?.replace("{format}", "webp")}" + anime.description = content?.shortDescription ?: "" + val genresList = content?.genres?.mapNotNull { it.translation }.orEmpty() + anime.genre = genresList.joinToString() + + return anime + } + + // ============================== Episodes ============================== + override fun episodeListRequest(anime: SAnime): Request { + val parts = anime.url.split(",") + val type = parts[1].lowercase() + val imdbId = parts[0] + return GET("https://cinemeta-live.strem.io/meta/$type/$imdbId.json") + } + + override fun episodeListParse(response: Response): List { + val responseString = response.body.string() + val episodeList = json.decodeFromString(responseString) + return when (episodeList.meta?.type) { + "show" -> { + episodeList.meta.videos + ?.let { videos -> + if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.firstAired?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } } + } + ?.map { video -> + SEpisode.create().apply { + episode_number = "${video.season}.${video.number}".toFloat() + url = "/stream/series/${video.id}.json" + date_upload = video.firstAired?.let { parseDate(it) } ?: 0L + name = "S${video.season.toString().trim()}:E${video.number} - ${video.name}" + scanlator = (video.firstAired?.let { parseDate(it) } ?: 0L) + .takeIf { it > System.currentTimeMillis() } + ?.let { "Upcoming" } + ?: "" + } + } + ?.sortedWith( + compareBy { it.name.substringAfter("S").substringBefore(":").toInt() } + .thenBy { it.name.substringAfter("E").substringBefore(" -").toInt() }, + ) + .orEmpty().reversed() + } + + "movie" -> { + // Handle movie response + listOf( + SEpisode.create().apply { + episode_number = 1.0F + url = "/stream/movie/${episodeList.meta.id}.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, "none") + + when { + token.isNullOrBlank() && debridProvider != "none" -> { + handler.post { + context.let { + Toast.makeText( + it, + "Kindly input the debrid 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