diff --git a/multisrc/overrides/animestream/tranimeci/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/animestream/tranimeci/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..d3a350ba9 Binary files /dev/null and b/multisrc/overrides/animestream/tranimeci/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/animestream/tranimeci/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/animestream/tranimeci/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..376e3753c Binary files /dev/null and b/multisrc/overrides/animestream/tranimeci/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/animestream/tranimeci/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/animestream/tranimeci/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..f85dad6f7 Binary files /dev/null and b/multisrc/overrides/animestream/tranimeci/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/animestream/tranimeci/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/animestream/tranimeci/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..488e8a50d Binary files /dev/null and b/multisrc/overrides/animestream/tranimeci/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/animestream/tranimeci/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/animestream/tranimeci/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..1d926a4fa Binary files /dev/null and b/multisrc/overrides/animestream/tranimeci/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/animestream/tranimeci/src/ShittyProtectionInterceptor.kt b/multisrc/overrides/animestream/tranimeci/src/ShittyProtectionInterceptor.kt new file mode 100644 index 000000000..18394917c --- /dev/null +++ b/multisrc/overrides/animestream/tranimeci/src/ShittyProtectionInterceptor.kt @@ -0,0 +1,73 @@ +package eu.kanade.tachiyomi.animeextension.tr.tranimeci + +import app.cash.quickjs.QuickJs +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Cookie +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.IOException + +class ShittyProtectionInterceptor(private val client: OkHttpClient) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + // ignore non-protected requests + if (response.code != 202) return response + return try { + chain.proceed(bypassProtection(request, response)) + } catch (e: Throwable) { + // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that + // we don't crash the entire app + e.printStackTrace() + throw IOException(e) + } + } + + private fun bypassProtection(request: Request, response: Response): Request { + val doc = response.use { it.asJsoup() } + + val script = doc.selectFirst("script:containsData(slowAES)")!!.data() + + val slowAES = doc.selectFirst("script[src*=min.js]")!!.attr("abs:src").let { url -> + client.newCall(GET(url)).execute().use { it.body.string() } + } + + val patchedScript = slowAES + "\n" + ADDITIONAL_FUNCTIONS + script + .replace("document.cookie=", "") + .replace("location.href", "// ") + + val cookieString = QuickJs.create().use { + it.evaluate(patchedScript)?.toString() + }!! + + val cookie = Cookie.parse(request.url, cookieString)!! + + client.cookieJar.saveFromResponse(request.url, listOf(cookie)) + + val headers = request.headers.newBuilder() + .add("Cookie", cookie.toString()) + .build() + + return GET(request.url.toString(), headers) + } + + companion object { + private val ADDITIONAL_FUNCTIONS get() = """ + // QJS doesnt have atob(b64dec) >:( + atob = function(s) { + var e={},i,b=0,c,x,l=0,a,r='',w=String.fromCharCode,L=s.length; + var A="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for(i=0;i<64;i++){e[A.charAt(i)]=i;} + for(x=0;x=8){((a=(b>>>(l-=8))&0xff)||(x<(L-2)))&&(r+=w(a));} + } + return r; + }; + """.trimIndent() + } +} diff --git a/multisrc/overrides/animestream/tranimeci/src/TRAnimeCI.kt b/multisrc/overrides/animestream/tranimeci/src/TRAnimeCI.kt new file mode 100644 index 000000000..48c7503f7 --- /dev/null +++ b/multisrc/overrides/animestream/tranimeci/src/TRAnimeCI.kt @@ -0,0 +1,147 @@ +package eu.kanade.tachiyomi.animeextension.tr.tranimeci + +import eu.kanade.tachiyomi.animeextension.tr.tranimeci.TRAnimeCIFilters.CountryFilter +import eu.kanade.tachiyomi.animeextension.tr.tranimeci.TRAnimeCIFilters.GenresFilter +import eu.kanade.tachiyomi.animeextension.tr.tranimeci.TRAnimeCIFilters.SeasonFilter +import eu.kanade.tachiyomi.animeextension.tr.tranimeci.TRAnimeCIFilters.StudioFilter +import eu.kanade.tachiyomi.animeextension.tr.tranimeci.TRAnimeCIFilters.TypeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SEpisode +import eu.kanade.tachiyomi.animesource.model.Video +import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream +import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.Locale + +class TRAnimeCI : AnimeStream( + "tr", + "TRAnimeCI", + "https://tranimeci.com", +) { + override val client by lazy { + network.client.newBuilder() + .addInterceptor(ShittyProtectionInterceptor(network.client)) + .build() + } + + override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/") + + override val animeListUrl = "$baseUrl/search" + + override val dateFormatter by lazy { + SimpleDateFormat("dd MMMM yyyy", Locale("tr")) + } + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = GET(baseUrl) + + override fun popularAnimeSelector() = "div.releases:contains(Populer) + div.listupd a.tip" + + override fun popularAnimeNextPageSelector() = null + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/index?page=$page") + + override fun latestUpdatesSelector() = "div.releases:contains(Son Güncellenenler) ~ div.listupd a.tip" + + override fun latestUpdatesFromElement(element: Element) = + searchAnimeFromElement(element).apply { + // Convert episode url to anime url + url = "/series$url".replace("/video", "").substringBefore("-bolum").substringBeforeLast("-") + } + + override fun latestUpdatesNextPageSelector() = "div.hpage > a:last-child[href]" + + // =============================== Search =============================== + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val params = TRAnimeCIFilters.getSearchParameters(filters) + val url = "$animeListUrl?${params.genres}".toHttpUrl().newBuilder() + .addIfNotBlank("country[]", params.country) + .addIfNotBlank("season[]", params.season) + .addIfNotBlank("format[]", params.type) + .addIfNotBlank("studio[]", params.studio) + .build() + + return GET(url.toString(), headers) + } + + override fun searchAnimeSelector() = "div.advancedsearch a.tip" + + override fun searchAnimeNextPageSelector() = null + + // ============================== Filters =============================== + override val filtersSelector = "div.filter.dropdown > ul" + + override fun getFilterList(): AnimeFilterList { + return if (AnimeStreamFilters.filterInitialized()) { + AnimeFilterList( + GenresFilter("Tür"), + AnimeFilter.Separator(), + CountryFilter("Ülke"), + SeasonFilter("Mevsim"), + TypeFilter("Tip"), + StudioFilter("Studio"), + ) + } else { + AnimeFilterList(AnimeFilter.Header(filtersMissingWarning)) + } + } + + // =========================== Anime Details ============================ + override val animeDetailsSelector = "div.infox" + override val animeStatusText = "Durum" + + override fun parseStatus(statusString: String?): Int { + return when (statusString?.trim()?.lowercase()) { + "tamamlandı" -> SAnime.COMPLETED + "devam ediyor" -> SAnime.ONGOING + else -> SAnime.UNKNOWN + } + } + + // ============================== Episodes ============================== + override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed() + + override fun episodeFromElement(element: Element) = SEpisode.create().apply { + setUrlWithoutDomain(element.attr("abs:href")) + val epNum = element.selectFirst(".epl-title")!!.text() + .substringBefore(".") + .substringBefore(" ") + .toIntOrNull() ?: 1 // Int because of the episode name, a Float would render with more zeros. + + name = "Bölüm $epNum" + episode_number = epNum.toFloat() + + date_upload = element.selectFirst(".epl-date")?.text().toDate() + } + + // ============================ Video Links ============================= + override fun videoListParse(response: Response): List