Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".tr.animeler.AnimelerUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="animeler.me"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,20 @@
ext {
extName = 'Animeler'
extClass = '.Animeler'
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:dood-extractor"))
implementation(project(":lib:filemoon-extractor"))
implementation(project(":lib:gdriveplayer-extractor"))
implementation(project(":lib:sibnet-extractor"))
implementation(project(":lib:streamlare-extractor"))
implementation(project(":lib:okru-extractor"))
implementation(project(":lib:streamtape-extractor"))
implementation(project(":lib:uqload-extractor"))
implementation(project(":lib:voe-extractor"))
implementation(project(":lib:vudeo-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,374 @@
package eu.kanade.tachiyomi.animeextension.tr.animeler
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.AnimeEpisodes
import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.FullAnimeDto
import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.SearchRequestDto
import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.SearchResponseDto
import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.SingleDto
import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.SourcesDto
import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.VideoDto
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
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.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.vudeoextractor.VudeoExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.Jsoup
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class Animeler : AnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Animeler"
override val baseUrl = "https://animeler.me"
override val lang = "tr"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = searchOrderBy("total_kiranime_views", page)
override fun popularAnimeParse(response: Response): AnimesPage {
val results = response.parseAs<SearchResponseDto>()
val doc = Jsoup.parseBodyFragment(results.data)
val animes = doc.select("div.w-full:has(div.kira-anime)").map {
SAnime.create().apply {
thumbnail_url = it.selectFirst("img")?.attr("src")
with(it.selectFirst("h3 > a")!!) {
title = text()
setUrlWithoutDomain(attr("href"))
}
}
}
val page = response.request.url.queryParameter("page")?.toIntOrNull() ?: 1
val hasNextPage = page < results.pages
return AnimesPage(animes, hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = searchOrderBy("kiranime_anime_updated", page)
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
// =============================== 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 getFilterList() = AnimelerFilters.FILTER_LIST
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimelerFilters.getSearchParameters(filters)
val (meta, orderBy) = when (params.orderBy) {
"date", "title" -> Pair(null, params.orderBy)
else -> Pair(params.orderBy, "meta_value_num")
}
val single = SingleDto(
paged = page,
key = meta,
order = params.order,
orderBy = orderBy,
season = params.season.ifEmpty { null },
year = params.year.ifEmpty { null },
)
val taxonomies = with(params) {
listOf(genres, status, producers, studios, types).filter {
it.terms.isNotEmpty()
}
}
val requestDto = SearchRequestDto(single, query, query, taxonomies)
val requestData = json.encodeToString(requestDto)
return searchRequest(requestData, page)
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
private fun searchOrderBy(order: String, page: Int): Request {
val body = """
{
"keyword": "",
"query": "",
"single": {
"paged": $page,
"orderby": "meta_value_num",
"meta_key": "$order",
"order": "desc"
},
"tax": []
}
""".trimIndent()
return searchRequest(body, page)
}
private fun searchRequest(data: String, page: Int): Request {
val body = data.toRequestBody("application/json".toMediaType())
return POST("$baseUrl/wp-json/kiranime/v1/anime/advancedsearch?_locale=user&page=$page", headers, body)
}
// =========================== Anime Details ============================
private inline fun <reified T> Response.parseBody(): T {
val body = use { it.body.string() }
.substringAfter("const anime = ")
.substringBefore("};") + "}"
return json.decodeFromString<T>(body)
}
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val animeDto = response.parseBody<FullAnimeDto>()
setUrlWithoutDomain(animeDto.url)
thumbnail_url = animeDto.thumbnail
title = animeDto.title
artist = animeDto.studios
author = animeDto.producers
genre = animeDto.genres
status = when {
animeDto.meta.aired.orEmpty().contains(" to ") -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
description = buildString {
animeDto.post.post_content?.also { append(it + "\n") }
with(animeDto.meta) {
score?.takeIf(String::isNotBlank)?.also { append("\nScore: $it") }
native?.takeIf(String::isNotBlank)?.also { append("\nNative: $it") }
synonyms?.takeIf(String::isNotBlank)?.also { append("\nDiğer İsimleri: $it") }
rate?.takeIf(String::isNotBlank)?.also { append("\nRate: $it") }
premiered?.takeIf(String::isNotBlank)?.also { append("\nPremiered: $it") }
aired?.takeIf(String::isNotBlank)?.also { append("\nYayınlandı: $it") }
duration?.takeIf(String::isNotBlank)?.also { append("\nSüre: $it") }
}
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val episodes = response.parseBody<AnimeEpisodes>().episodes
return episodes.map {
SEpisode.create().apply {
setUrlWithoutDomain(it.url)
name = "Bölüm " + it.meta.number
episode_number = it.meta.number.toFloat()
date_upload = it.date.toDate()
}
}
}
// ============================ Video Links =============================
private val doodExtractor by lazy { DoodExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val gdrivePlayerExtractor by lazy { GdrivePlayerExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val sibnetExtractor by lazy { SibnetExtractor(client) }
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val voeExtractor by lazy { VoeExtractor(client) }
private val vudeoExtractor by lazy { VudeoExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val iframeUrl = doc.selectFirst("div.episode-player-box > iframe")
?.run { attr("data-src").ifBlank { attr("src") } }
?: doc.selectFirst("script:containsData(embedUrl)")
?.data()
?.substringAfter("\"embedUrl\": \"")
?.substringBefore('"')
?: throw Exception("No video available.")
val playerBody = { it: String ->
FormBody.Builder()
.add("hash", iframeUrl.substringAfter("/video/"))
.add("r", "$baseUrl/")
.add("s", it)
.build()
}
val headers = headersBuilder()
.add("Origin", "https://" + iframeUrl.toHttpUrl().host) // just to be sure
.add("X-Requested-With", "XMLHttpRequest")
.build()
val actionUrl = "$iframeUrl?do=getVideo"
val players = client.newCall(POST(actionUrl, headers, playerBody(""))).execute()
.parseAs<SourcesDto>()
val chosenHosts = preferences.getStringSet(PREF_HOSTS_SELECTION_KEY, SUPPORTED_PLAYERS)!!
val filteredSources = players.sourceList.entries.filter { source ->
chosenHosts.any { it.contains(source.value, true) }
}
return filteredSources.parallelCatchingFlatMapBlocking {
val body = playerBody(it.key)
val res = client.newCall(POST(actionUrl, headers, body)).await()
.parseAs<VideoDto>()
videosFromUrl(res.videoSrc)
}
}
private fun videosFromUrl(url: String): List<Video> {
return when {
"dood" in url -> doodExtractor.videosFromUrl(url)
"drive.google" in url -> {
val newUrl = "https://gdriveplayer.to/embed2.php?link=$url"
gdrivePlayerExtractor.videosFromUrl(newUrl, "GdrivePlayer", headers)
}
"filemoon." in url -> filemoonExtractor.videosFromUrl(url)
"ok.ru" in url || "odnoklassniki.ru" in url -> okruExtractor.videosFromUrl(url)
"streamtape" in url -> streamtapeExtractor.videoFromUrl(url)?.let(::listOf)
"sibnet" in url -> sibnetExtractor.videosFromUrl(url)
"streamlare" in url -> streamlareExtractor.videosFromUrl(url)
"uqload" in url -> uqloadExtractor.videosFromUrl(url)
"voe." in url -> voeExtractor.videosFromUrl(url)
"vudeo." in url -> vudeoExtractor.videosFromUrl(url)
else -> null
} ?: emptyList()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTS_SELECTION_KEY
title = PREF_HOSTS_SELECTION_TITLE
entries = PREF_HOSTS_SELECTION_ENTRIES
entryValues = PREF_HOSTS_SELECTION_ENTRIES
setDefaultValue(PREF_HOSTS_SELECTION_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
private val qualityRegex by lazy { Regex("""(\d+)p""") }
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ qualityRegex.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
}
const val PREFIX_SEARCH = "id:"
private val SUPPORTED_PLAYERS = setOf(
"doodstream.com",
"G.Drive",
"Moon",
"ok.ru",
"S.Tape",
"Sibnet",
"Streamlare",
"UQload",
"Voe",
"vudeo",
)
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
private const val PREF_HOSTS_SELECTION_KEY = "pref_hosts_selection"
private const val PREF_HOSTS_SELECTION_TITLE = "Disable/enable video hosts"
private val PREF_HOSTS_SELECTION_ENTRIES = SUPPORTED_PLAYERS.toTypedArray()
private val PREF_HOSTS_SELECTION_DEFAULT = SUPPORTED_PLAYERS
}
}

View file

@ -0,0 +1,683 @@
package eu.kanade.tachiyomi.animeextension.tr.animeler
import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.TaxonomyDto
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimelerFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, Int>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.getFirst(): R = first { it is R } as R
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, Int>>,
name: String,
): TaxonomyDto {
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.let { TaxonomyDto(name, it) }
}
class GenresFilter : CheckBoxFilterList("Genres", AnimelerFiltersData.GENRES)
class StatusFilter : CheckBoxFilterList("Durumu", AnimelerFiltersData.STATUS)
class ProducersFilter : CheckBoxFilterList("Yapımcı", AnimelerFiltersData.PRODUCERS)
class StudiosFilter : CheckBoxFilterList("Stüdyo", AnimelerFiltersData.GENRES)
class TypesFilter : CheckBoxFilterList("Tür", AnimelerFiltersData.TYPES)
class OrderFilter : AnimeFilter.Sort(
"Order by",
AnimelerFiltersData.ORDERS.map { it.first }.toTypedArray(),
Selection(0, false),
)
class YearFilter : QueryPartFilter("Yil", AnimelerFiltersData.YEARS)
class SeasonFilter : QueryPartFilter("Sezon", AnimelerFiltersData.SEASONS)
val FILTER_LIST get() = AnimeFilterList(
OrderFilter(),
YearFilter(),
SeasonFilter(),
AnimeFilter.Separator(),
GenresFilter(),
StatusFilter(),
ProducersFilter(),
StudiosFilter(),
TypesFilter(),
)
data class FilterSearchParams(
val genres: TaxonomyDto = TaxonomyDto(),
val status: TaxonomyDto = TaxonomyDto(),
val producers: TaxonomyDto = TaxonomyDto(),
val studios: TaxonomyDto = TaxonomyDto(),
val types: TaxonomyDto = TaxonomyDto(),
val order: String = "desc",
val orderBy: String = "total_kiranime_views",
val year: String = "",
val season: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val (order, orderBy) = filters.getFirst<OrderFilter>().state?.let {
val order = if (it.ascending) "asc" else "desc"
val orderBy = AnimelerFiltersData.ORDERS[it.index].second
Pair(order, orderBy)
} ?: Pair("desc", "total_kiranime_views")
return FilterSearchParams(
filters.parseCheckbox<GenresFilter>(AnimelerFiltersData.GENRES, "genre"),
filters.parseCheckbox<StatusFilter>(AnimelerFiltersData.STATUS, "status"),
filters.parseCheckbox<ProducersFilter>(AnimelerFiltersData.PRODUCERS, "producer"),
filters.parseCheckbox<StudiosFilter>(AnimelerFiltersData.STUDIOS, "studio"),
filters.parseCheckbox<TypesFilter>(AnimelerFiltersData.TYPES, "type"),
order,
orderBy,
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<SeasonFilter>(),
)
}
private object AnimelerFiltersData {
val EVERY = Pair("Seçiniz", "")
val GENRES = arrayOf(
Pair("Action", 10),
Pair("Adult Cast", 459),
Pair("Adventure", 34),
Pair("Aksiyon", 158),
Pair("Antropomorfik", 220),
Pair("Arabalar", 192),
Pair("Aşk Üçgeni", 219),
Pair("Askeri", 184),
Pair("Avangart", 211),
Pair("Bilim Kurgu", 171),
Pair("Büyü", 159),
Pair("CGDCT", 215),
Pair("Childcare", 364),
Pair("Çocuk Bakımı", 216),
Pair("Çocuklar", 206),
Pair("Comedy", 95),
Pair("Comic", 228),
Pair("Dedektif", 221),
Pair("Delinquents", 405),
Pair("Doğaüstü Güçler", 176),
Pair("Dövüş Sanatları", 187),
Pair("Dram", 180),
Pair("Drama", 51),
Pair("Ecchi", 22),
Pair("Fantastik", 160),
Pair("Fantasy", 13),
Pair("Gag Humor", 397),
Pair("Gerilim", 172),
Pair("Girls Love", 65),
Pair("Gizem", 173),
Pair("Gore", 358),
Pair("Gourmet", 473),
Pair("Harem", 170),
Pair("Historical", 359),
Pair("Horror", 119),
Pair("İdol", 225),
Pair("Idols (Female)", 292),
Pair("Isekai", 196),
Pair("Iyashikei", 223),
Pair("Josei", 178),
Pair("Komedi", 168),
Pair("Korku", 174),
Pair("Kumar Oyunu", 222),
Pair("Macera", 161),
Pair("Mahou Shoujo", 214),
Pair("Martial Arts", 425),
Pair("Mecha", 193),
Pair("Medikal", 254),
Pair("Military", 394),
Pair("Mitoloji", 213),
Pair("Music", 522),
Pair("Müzik", 203),
Pair("Mystery", 76),
Pair("Mythology", 316),
Pair("Okul", 179),
Pair("OP M.C.", 541),
Pair("Oyun", 191),
Pair("Parodi", 197),
Pair("Polisiye", 186),
Pair("Psikolojik", 175),
Pair("Psychological", 303),
Pair("Rebirth", 517),
Pair("Reenkarnasyon", 217),
Pair("Reincarnation", 381),
Pair("Revenge", 518),
Pair("Romance", 29),
Pair("Romantic Subtext", 270),
Pair("Romantizm", 181),
Pair("Sahne Sanatçıları", 227),
Pair("Samuray", 188),
Pair("School", 289),
Pair("Sci-Fi", 45),
Pair("Seinen", 183),
Pair("Şeytan", 189),
Pair("Shoujo", 194),
Pair("Shoujo Ai", 212),
Pair("Shounen", 162),
Pair("Shounen Ai", 210),
Pair("Slice of Life", 128),
Pair("Spor", 207),
Pair("Sports", 144),
Pair("Strategy Game", 434),
Pair("Strateji Oyunu", 218),
Pair("Süper Güçler", 177),
Pair("Super Power", 362),
Pair("Supernatural", 49),
Pair("Survival", 415),
Pair("Suspense", 78),
Pair("Tarihi", 185),
Pair("Team Sports", 369),
Pair("Time Travel", 407),
Pair("Uzay", 190),
Pair("Vampir", 182),
Pair("Video Game", 402),
Pair("Visual Arts", 503),
Pair("Workplace", 462),
Pair("Yaşamdan Kesitler", 169),
Pair("Yemek", 204),
Pair("Yetişkin Karakterler", 226),
Pair("Zaman Yolculuğu", 224),
)
val STATUS = arrayOf(
Pair("Airing", 3),
Pair("Completed", 4),
Pair("Not Yet Aired", 244),
Pair("Upcoming", 2),
Pair("Upcomming", 205),
)
val PRODUCERS = arrayOf(
Pair("A-Sketch", 137),
Pair("ABC Animation", 60),
Pair("ADK Emotions", 299),
Pair("ADK Marketing Solutions", 106),
Pair("Ai Addiction", 79),
Pair("Aiming", 384),
Pair("Akita Shoten", 373),
Pair("Amusement Media Academy", 130),
Pair("Animation Do", 340),
Pair("Animax", 491),
Pair("Aniplex", 30),
Pair("APDREAM", 109),
Pair("AQUAPLUS", 236),
Pair("arma bianca", 80),
Pair("ASCII Media Works", 529),
Pair("Ashi Productions", 338),
Pair("Asmik Ace", 347),
Pair("AT-X", 81),
Pair("Atelier Musa", 314),
Pair("Avex Entertainment", 327),
Pair("Avex Pictures", 266),
Pair("B.CMAY PICTURES", 267),
Pair("Bandai", 334),
Pair("Bandai Namco Arts", 74),
Pair("Bandai Namco Entertainment", 336),
Pair("Bandai Namco Filmworks", 231),
Pair("Bandai Namco Music Live", 232),
Pair("Bandai Spirits", 11),
Pair("Bandai Visual", 337),
Pair("BeDream", 284),
Pair("Being", 305),
Pair("Bergamo", 110),
Pair("Beyond C.", 392),
Pair("Bibury Animation CG", 489),
Pair("bilibili", 151),
Pair("Bit grooove promotion", 58),
Pair("BS Asahi", 263),
Pair("BS Fuji", 12),
Pair("BS NTV", 67),
Pair("BS11", 107),
Pair("Bushiroad", 241),
Pair("Bushiroad Creative", 276),
Pair("Bushiroad Move", 277),
Pair("C-one", 131),
Pair("CG Year", 282),
Pair("China Literature Limited", 199),
Pair("Chiptune", 127),
Pair("CHOCOLATE", 482),
Pair("Chosen", 429),
Pair("Chugai Mining", 450),
Pair("Cloud Art", 239),
Pair("Cloud22", 248),
Pair("Contents Seed", 68),
Pair("Crest", 510),
Pair("Crunchyroll", 141),
Pair("CTW", 348),
Pair("Culture Entertainment", 300),
Pair("CyberAgent", 295),
Pair("Cygames", 514),
Pair("DAX Production", 163),
Pair("Days", 269),
Pair("Delfi sound", 557),
Pair("DeNA", 291),
Pair("Dentsu", 91),
Pair("Disney Platform Distribution", 485),
Pair("DMM Music", 96),
Pair("DMM pictures", 97),
Pair("DMM.com", 438),
Pair("Docomo Anime Store", 21),
Pair("Dream Shift", 111),
Pair("dugout", 124),
Pair("Egg Firm", 35),
Pair("Energy Studio", 273),
Pair("Enterbrain", 343),
Pair("Epicross", 297),
Pair("Exa International", 344),
Pair("F.M.F", 513),
Pair("flying DOG", 83),
Pair("Foch Films", 436),
Pair("Frontier Works", 82),
Pair("Fuji Creative", 401),
Pair("Fuji TV", 14),
Pair("Fujimi Shobo", 315),
Pair("FuRyu", 36),
Pair("Futabasha", 349),
Pair("FUTURE LEAP", 350),
Pair("Geek Pictures", 472),
Pair("Genco", 84),
Pair("Geneon Universal Entertainment", 117),
Pair("Gentosha Comics", 376),
Pair("Glovision", 325),
Pair("Good Smile Company", 233),
Pair("Good Smile Film", 458),
Pair("GREE", 37),
Pair("GREE Entertainment", 377),
Pair("Grooove", 342),
Pair("Hakuhodo", 367),
Pair("Hakuhodo DY Media Partners", 15),
Pair("Hakuhodo DY Music &amp; Pictures", 38),
Pair("Hakusensha", 320),
Pair("Half H.P Studio", 25),
Pair("Half HP Studio", 508),
Pair("Happinet Phantom Studios", 246),
Pair("Heart Company", 474),
Pair("High Energy Studio", 447),
Pair("HM Heros", 283),
Pair("Hobby Japan", 464),
Pair("HoriPro International", 307),
Pair("Ichijinsha", 132),
Pair("INCS toenter", 365),
Pair("Infinite", 257),
Pair("INSPION Edge", 408),
Pair("iQIYI", 274),
Pair("IRMA LA DOUCE", 148),
Pair("Jinnan Studio", 102),
Pair("JR East Marketing &amp; Communications", 378),
Pair("Jumondo", 260),
Pair("K contents", 383),
Pair("Kadokawa", 26),
Pair("Kadokawa Media House", 27),
Pair("Kadokawa Shoten", 332),
Pair("Kanetsu Investment", 133),
Pair("Kansai Telecasting", 253),
Pair("KDDI", 393),
Pair("King Records", 237),
Pair("Kizuna AI", 520),
Pair("KLab", 234),
Pair("KlockWorx", 40),
Pair("Kodansha", 98),
Pair("Konami", 339),
Pair("Konami Cross Media NY", 433),
Pair("Konami Digital Entertainment", 301),
Pair("Kuaikan Manhua", 479),
Pair("Kyoraku Industrial Holdings", 157),
Pair("Lantis", 52),
Pair("Lawson", 333),
Pair("Lawson HMV Entertainment", 138),
Pair("Legs", 521),
Pair("LHL Culture", 456),
Pair("MAGES.", 238),
Pair("Magic Bus", 487),
Pair("Magic Capsule", 88),
Pair("MAGNET", 309),
Pair("Mainichi Broadcasting System", 75),
Pair("Marui Group", 360),
Pair("Marvelous", 69),
Pair("Marvelous AQL", 311),
Pair("MediaNet", 85),
Pair("MediBang", 398),
Pair("Medicos Entertainment", 108),
Pair("Medo", 574),
Pair("Micro House", 379),
Pair("Micro Magazine Publishing", 380),
Pair("Mixer", 507),
Pair("Movic", 16),
Pair("Muse Communication", 142),
Pair("My Theater D.D.", 261),
Pair("Nagoya Broadcasting Network", 61),
Pair("NBCUniversal Entertainment Japan", 70),
Pair("NetEase", 388),
Pair("Netflix", 403),
Pair("NewGin", 385),
Pair("NHK", 356),
Pair("NHK Enterprises", 357),
Pair("NichiNare", 439),
Pair("Nichion", 440),
Pair("Nihon Ad Systems", 103),
Pair("Nikkatsu", 368),
Pair("Nippon Animation", 494),
Pair("Nippon Columbia", 28),
Pair("Nippon Television Music", 488),
Pair("Nippon Television Network", 255),
Pair("Nitroplus", 154),
Pair("NTT Plala", 310),
Pair("Overlap", 278),
Pair("Paper Plane Animation Studio", 443),
Pair("Pia", 419),
Pair("Pierrot", 399),
Pair("Pony Canyon", 59),
Pair("Pony Canyon Enterprise", 413),
Pair("PRA", 317),
Pair("Precious tone", 553),
Pair("Production Ace", 293),
Pair("Production I.G", 414),
Pair("Pure Arts", 426),
Pair("Q-Tec", 104),
Pair("Quaras", 477),
Pair("Rakuonsha", 140),
Pair("Ranzai Studio", 428),
Pair("Rialto Entertainment", 149),
Pair("Saber Links", 461),
Pair("Sammy", 72),
Pair("SB Creative", 41),
Pair("Seikaisha", 155),
Pair("Shochiku", 328),
Pair("Shogakukan", 54),
Pair("Shogakukan Music &amp; Digital Entertainment", 478),
Pair("Shogakukan-Shueisha Productions", 153),
Pair("Shounen Gahousha", 123),
Pair("Showgate", 318),
Pair("Shueisha", 32),
Pair("Shufunotomo", 453),
Pair("Sonilude", 135),
Pair("Sony Music Entertainment", 92),
Pair("Sony Music Solutions", 471),
Pair("Sony Pictures Entertainment", 476),
Pair("Sound Team Don Juan", 500),
Pair("Square Enix", 46),
Pair("Starchild Records", 323),
Pair("Starry Cube", 352),
Pair("Straight Edge", 47),
Pair("Stray Cats", 302),
Pair("Studio Easter", 290),
Pair("Studio Hibari", 331),
Pair("Studio Mausu", 48),
Pair("Sumzap", 126),
Pair("Sun TV", 346),
Pair("TBS", 272),
Pair("TC Entertainment", 312),
Pair("Tencent", 445),
Pair("Tencent Animation &amp; Comics", 209),
Pair("Tencent Games", 326),
Pair("Tencent Penguin Pictures", 201),
Pair("TMS Entertainment", 389),
Pair("TO Books", 468),
Pair("Toei animation", 166),
Pair("Toei Video", 113),
Pair("Tohan Corporation", 361),
Pair("Tohjak", 490),
Pair("TOHO animation", 17),
Pair("Tohokushinsha Film Corporation", 143),
Pair("Tokyo MX", 63),
Pair("Toy's Factory", 370),
Pair("Trinity Sound", 329),
Pair("TV Aichi", 294),
Pair("TV Asahi", 371),
Pair("TV Tokyo", 164),
Pair("TV Tokyo Music", 324),
Pair("TVA advance", 470),
Pair("Twin Engine", 281),
Pair("Ultra Super Pictures", 20),
Pair("Universal Music Japan", 353),
Pair("VAP", 256),
Pair("Visual Arts", 64),
Pair("Vobile Japan", 391),
Pair("Wanda Media", 288),
Pair("Warner Bros. Japan", 43),
Pair("WOWMAX", 504),
Pair("WOWOW", 87),
Pair("Xuanshi Tangmen", 448),
Pair("Yahoo! Japan", 396),
Pair("Yokohama Animation Lab", 441),
Pair("Yomiko Advertising", 330),
Pair("Yomiuri Advertising", 73),
Pair("Yomiuri Shimbun", 114),
Pair("Yomiuri Telecasting", 363),
Pair("Yomiuri TV Enterprise", 57),
Pair("Yostar", 558),
Pair("Youku", 268),
Pair("Yuewen Animation &amp; Comics", 467),
)
val STUDIOS = arrayOf(
Pair("2:10 AM Animation", 430),
Pair("8bit", 44),
Pair("A-1 Pictures", 116),
Pair("A.C.G.T.", 265),
Pair("Actas", 568),
Pair("Ajia-do", 526),
Pair("Arvo animation", 145),
Pair("Asahi Production", 509),
Pair("Ashi Productions", 465),
Pair("AtelierPontdarc", 247),
Pair("Axsiz", 567),
Pair("B.CMAY PICTURES", 480),
Pair("Bakken Record", 481),
Pair("Bandai Namco Pictures", 355),
Pair("Bibury Animation Studios", 387),
Pair("Big firebird culture", 93),
Pair("bilibili", 374),
Pair("Blade", 279),
Pair("Bones", 90),
Pair("Brain's Base", 484),
Pair("Bug films", 569),
Pair("BYMENT", 435),
Pair("C-Station", 502),
Pair("C2C", 375),
Pair("CG Year", 411),
Pair("China south angel", 571),
Pair("Chongzhuo Animation", 412),
Pair("Chosen", 285),
Pair("Clap", 540),
Pair("Cloud Hearts", 437),
Pair("CloverWorks", 264),
Pair("Connect", 451),
Pair("CygamesPictures", 519),
Pair("Da huoniao donghua", 94),
Pair("Dancing CG Studio", 304),
Pair("David Production", 152),
Pair("DC Impression Vision", 466),
Pair("Diomedéa", 252),
Pair("Djinn Power", 527),
Pair("DLE", 382),
Pair("Doga Kobo", 115),
Pair("Drive", 296),
Pair("EKACHI EPILKA", 195),
Pair("Elite Animation", 444),
Pair("EMT Squared", 306),
Pair("Encourage Films", 147),
Pair("ENGI", 156),
Pair("feel.", 250),
Pair("Foch", 536),
Pair("Foch Films", 200),
Pair("Gaina", 251),
Pair("Gallop", 432),
Pair("GARDEN", 416),
Pair("Garden Culture", 275),
Pair("Geek Toys", 24),
Pair("Gekkou", 505),
Pair("Geno Studio", 280),
Pair("GIFTanimation", 242),
Pair("Gohands", 561),
Pair("Good smile company", 417),
Pair("Graphinica", 118),
Pair("Haoliners Animation League", 442),
Pair("Heart &amp; soul animation", 455),
Pair("Ilca", 556),
Pair("J.C.Staff", 39),
Pair("Kinema Citrus", 243),
Pair("Kung Fu Frog Animation", 524),
Pair("Kyoto Animation", 341),
Pair("Lapin track", 562),
Pair("Larx entertainment", 563),
Pair("Lay-duce", 409),
Pair("Lerche", 229),
Pair("Liber", 460),
Pair("LIDENFILMS", 139),
Pair("Liyu culture", 532),
Pair("Lx animation studio", 531),
Pair("Madhouse", 120),
Pair("Magic Bus", 308),
Pair("Maho Film", 53),
Pair("MAPPA", 125),
Pair("Millepensee", 495),
Pair("Motion Magic", 287),
Pair("Movic", 418),
Pair("NAZ", 262),
Pair("New deer", 544),
Pair("Nexus", 386),
Pair("Nhk", 533),
Pair("Nhk enterprises", 534),
Pair("Nice Boat Animation", 496),
Pair("Nippon animation", 535),
Pair("Nomad", 134),
Pair("OLM", 410),
Pair("Olm team yoshioka", 564),
Pair("Orange", 452),
Pair("Oriental Creative Color", 528),
Pair("Original force", 530),
Pair("P.A. Works", 62),
Pair("Passion paint animation", 576),
Pair("Passione", 249),
Pair("Pencil Lead Animate", 525),
Pair("Pia", 420),
Pair("Pie in the sky", 554),
Pair("Pierrot", 71),
Pair("Pierrot Plus", 313),
Pair("Pine jam", 538),
Pair("Polygon Pictures", 475),
Pair("Pony canyon", 421),
Pair("Production I.G", 77),
Pair("Project No.9", 112),
Pair("Qingxiang Culture", 427),
Pair("Qiyuan Yinghua", 457),
Pair("Quad", 483),
Pair("Quyue Technology", 493),
Pair("Revoroot", 230),
Pair("Rocen", 498),
Pair("Ruo Hong Culture", 259),
Pair("Satelight", 122),
Pair("Sb creative", 422),
Pair("Seven", 351),
Pair("Seven Arcs", 286),
Pair("Shaft", 235),
Pair("Shenman entertainment", 572),
Pair("Shin-Ei Animation", 486),
Pair("Shirogumi", 552),
Pair("Signal.MD", 523),
Pair("Silver", 146),
Pair("SILVER LINK.", 271),
Pair("Soyep", 202),
Pair("Sparkly Key Animation Studio", 208),
Pair("Staple Entertainment", 240),
Pair("Studio 3Hz", 258),
Pair("Studio 4°C", 395),
Pair("Studio A-CAT", 366),
Pair("Studio bind", 570),
Pair("Studio Blanc.", 449),
Pair("Studio Deen", 86),
Pair("Studio Elle", 511),
Pair("Studio Flad", 400),
Pair("Studio ghibli", 560),
Pair("Studio gokumi", 566),
Pair("Studio Jemi", 245),
Pair("Studio Kafka", 501),
Pair("Studio Kai", 129),
Pair("Studio LAN", 298),
Pair("Studio Lings", 499),
Pair("Studio Mir", 404),
Pair("studio MOTHER", 345),
Pair("Studio Palette", 497),
Pair("Studio Signpost", 492),
Pair("Sunrise", 55),
Pair("Sunrise beyond", 555),
Pair("SynergySP", 506),
Pair("Telecom animation film", 150),
Pair("Tencent Penguin Pictures", 454),
Pair("Tezuka Productions", 66),
Pair("TMS Entertainment", 99),
Pair("TNK", 319),
Pair("Toei Animation", 101),
Pair("Toho", 423),
Pair("Tokyo mx", 424),
Pair("Trigger", 18),
Pair("TROYCA", 390),
Pair("Typhoon Graphics", 512),
Pair("ufotable", 33),
Pair("Wawayu Animation", 515),
Pair("White Fox", 105),
Pair("Wit Studio", 136),
Pair("Wolfsbane", 354),
Pair("Wonder Cat Animation", 446),
Pair("Xuni Ying Ye", 516),
Pair("Yokohama Animation Lab", 372),
Pair("Yostar pictures", 559),
Pair("Youku", 573),
Pair("Yumeta Company", 167),
Pair("Zero-G", 463),
Pair("Zexcs", 537),
)
val TYPES = arrayOf(
Pair("Movie", 165),
Pair("ONA", 89),
Pair("OVA", 121),
Pair("Special", 198),
Pair("TV", 19),
)
val ORDERS = arrayOf(
Pair("Popüler", "total_kiranime_views"),
Pair("Favori", "bookmark_count"),
Pair("Başlık", "title"),
Pair("Yayımlandı", "date"),
Pair("Güncellendi", "kiranime_anime_updated"),
)
val YEARS = arrayOf(EVERY) + (2024 downTo 1990).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val SEASONS = arrayOf(
EVERY,
Pair("Kış", "winter"),
Pair("Spring", "spring"),
Pair("Summer", "summer"),
Pair("Sonbahar", "fall"),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.tr.animeler
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://animeler.me/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimelerUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${Animeler.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,105 @@
package eu.kanade.tachiyomi.animeextension.tr.animeler.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
@Serializable
data class SearchResponseDto(val data: String, val pages: Int)
@Serializable
data class PostDto(
val post_title: String,
val post_content: String? = null,
)
@Serializable
data class ThumbnailDto(private val featured_url: JsonPrimitive) {
val url = if (featured_url.isString) featured_url.content else null
}
@Serializable
data class TaxonomyDto(val taxonomy: String = "", val terms: List<Int> = emptyList())
@Serializable
data class SearchRequestDto(
val single: SingleDto,
val keyword: String,
val query: String,
val tax: List<TaxonomyDto>,
)
@Serializable
data class SingleDto(
val paged: Int,
@SerialName("meta_key")
val key: String?,
val order: String,
val orderBy: String,
val season: String?,
val year: String?,
)
@Serializable
data class FullAnimeDto(
val url: String,
val post: PostDto,
val meta: MetaDto,
private val taxonomies: TaxonomiesDto,
private val image: String = "",
private val images: ThumbnailDto? = null,
) {
val thumbnail = image.ifEmpty { images?.url }
val title = post.post_title
@Serializable
data class MetaDto(
val native: String? = null,
val synonyms: String? = null,
val score: String? = null,
val premiered: String? = null,
val aired: String? = null,
val duration: String? = null,
val rate: String? = null,
)
@Serializable
data class TaxonomiesDto(
val producer: List<ItemDto> = emptyList(),
val studio: List<ItemDto> = emptyList(),
val genre: List<ItemDto> = emptyList(),
)
val genres = taxonomies.genre.parseItems()
val studios = taxonomies.studio.parseItems()
val producers = taxonomies.producer.parseItems()
}
@Serializable
data class ItemDto(val name: String)
private fun List<ItemDto>.parseItems() = joinToString { it.name }.takeIf(String::isNotBlank)
@Serializable
data class AnimeEpisodes(val episodes: List<EpisodeDto>)
@Serializable
data class EpisodeDto(
val url: String,
val post: EpisodePostDto,
val meta: EpisodeMetaDto,
) {
@Serializable
data class EpisodeMetaDto(val number: String)
@Serializable
data class EpisodePostDto(val post_modified_gmt: String? = null)
val date = post.post_modified_gmt ?: ""
}
@Serializable
data class SourcesDto(val sourceList: Map<String, String>)
@Serializable
data class VideoDto(val videoSrc: String)

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".tr.anizm.AnizmUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="anizm.net"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

22
src/tr/anizm/build.gradle Normal file
View file

@ -0,0 +1,22 @@
ext {
extName = 'Anizm'
extClass = '.Anizm'
extVersionCode = 20
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:playlist-utils"))
implementation(project(":lib:dood-extractor"))
implementation(project(":lib:filemoon-extractor"))
implementation(project(":lib:gdriveplayer-extractor"))
implementation(project(":lib:mp4upload-extractor"))
implementation(project(":lib:okru-extractor"))
implementation(project(":lib:sendvid-extractor"))
implementation(project(":lib:sibnet-extractor"))
implementation(project(":lib:streamtape-extractor"))
implementation(project(':lib:uqload-extractor'))
implementation(project(":lib:voe-extractor"))
implementation(project(":lib:yourupload-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,454 @@
package eu.kanade.tachiyomi.animeextension.tr.anizm
import android.app.Application
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.tr.anizm.AnizmFilters.applyFilterParams
import eu.kanade.tachiyomi.animeextension.tr.anizm.extractors.AincradExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
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.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class Anizm : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Anizm"
override val baseUrl = "https://anizm.net"
override val lang = "tr"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
override fun popularAnimeSelector() = "div.popularAnimeCarousel a.slideAnimeLink"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
title = element.selectFirst(".title")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("src")
element.attr("href")
.substringBefore("-bolum-izle")
.substringBeforeLast("-")
.also { setUrlWithoutDomain(it) }
}
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/anime-izle?sayfa=$page", headers)
override fun latestUpdatesSelector() = "div#episodesMiddle div.posterBlock > a"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = "div.nextBeforeButtons > div.ui > a.right:not(.disabled)"
// =============================== Search ===============================
private val animeList by lazy {
client.newCall(GET("$baseUrl/getAnimeListForSearch", headers)).execute()
.parseAs<List<SearchItemDto>>()
.asSequence()
}
override fun getFilterList(): AnimeFilterList = AnizmFilters.FILTER_LIST
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/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
val params = AnizmFilters.getSearchParameters(filters).apply {
animeName = query
}
val filtered = animeList.applyFilterParams(params)
val results = filtered.chunked(30).toList()
val hasNextPage = results.size > page
val currentPage = if (results.size == 0) {
emptyList<SAnime>()
} else {
results.get(page - 1).map {
SAnime.create().apply {
title = it.title
url = "/" + it.slug
thumbnail_url = baseUrl + "/storage/pcovers/" + it.thumbnail
}
}
}
AnimesPage(currentPage, hasNextPage)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
throw UnsupportedOperationException()
}
override fun searchAnimeSelector(): String {
throw UnsupportedOperationException()
}
override fun searchAnimeFromElement(element: Element): SAnime {
throw UnsupportedOperationException()
}
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
setUrlWithoutDomain(document.location())
title = document.selectFirst("h2.anizm_pageTitle")!!.text()
thumbnail_url = document.selectFirst("div.infoPosterImg > img")!!.attr("abs:src")
val infosDiv = document.selectFirst("div.anizm_boxContent")!!
genre = infosDiv.select("span.dataValue > span.tag > span.label").eachText().joinToString()
artist = infosDiv.selectFirst("span.dataTitle:contains(Stüdyo) + span")?.text()
description = buildString {
infosDiv.selectFirst("div.infoDesc")?.text()?.also(::append)
infosDiv.select("li.dataRow:not(:has(span.ui.tag)):not(:has(div.star)) > span")
.forEach {
when {
it.hasClass("dataTitle") -> append("\n${it.text()}: ")
else -> append(it.text())
}
}
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
override fun episodeListSelector() = "div.episodeListTabContent div > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
episode_number = element.text().filter(Char::isDigit).toFloatOrNull() ?: 1F
name = element.text()
}
// ============================ Video Links =============================
@Serializable
data class ResponseDto(val data: String)
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val fansubUrls = doc.select("div#fansec > a")
.filterSubs()
.map { it.text().fixedFansubName() to it.attr("translator") }
.ifEmpty {
throw Exception("No fansubs available! Have you filtered them out?")
}
val chosenHosts = preferences.getStringSet(PREF_HOSTS_SELECTION_KEY, PREF_HOSTS_SELECTION_DEFAULT)!!
val playerUrls = fansubUrls.flatMap { pair ->
val (fansub, url) = pair
runCatching {
client.newCall(GET(url, headers)).execute()
.parseAs<ResponseDto>()
.data
.let(Jsoup::parse)
.select("a.videoPlayerButtons")
.toList()
.filter { host ->
val hostName = host.text().trim()
chosenHosts.any { hostName.contains(it, true) }
}
.map { fansub to it.attr("video").replace("/video/", "/player/") }
}.getOrElse { emptyList() }
}
return playerUrls.parallelCatchingFlatMapBlocking { pair ->
val (fansub, url) = pair
getVideosFromUrl(url).map {
Video(
it.url,
"[$fansub] ${it.quality}",
it.videoUrl,
it.headers,
it.subtitleTracks,
it.audioTracks,
)
}
}
}
private val noRedirectClient by lazy {
client.newBuilder().followRedirects(false).build()
}
private val aincradExtractor by lazy { AincradExtractor(client, headers, json) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val gdrivePlayerExtractor by lazy { GdrivePlayerExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val sendvidExtractor by lazy { SendvidExtractor(client, headers) }
private val sibnetExtractor by lazy { SibnetExtractor(client) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val voeExtractor by lazy { VoeExtractor(client) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private fun getVideosFromUrl(firstUrl: String): List<Video> {
val url = noRedirectClient.newCall(GET(firstUrl, headers)).execute()
.use { it.headers["location"] }
?: return emptyList()
return when {
"filemoon.sx" in url -> filemoonExtractor.videosFromUrl(url, headers = headers)
"sendvid.com" in url -> sendvidExtractor.videosFromUrl(url)
"video.sibnet" in url -> sibnetExtractor.videosFromUrl(url)
"mp4upload" in url -> mp4uploadExtractor.videosFromUrl(url, headers)
"ok.ru" in url || "odnoklassniki.ru" in url -> okruExtractor.videosFromUrl(url)
"yourupload" in url -> yourUploadExtractor.videoFromUrl(url, headers)
"streamtape" in url -> streamtapeExtractor.videoFromUrl(url)?.let(::listOf)
"dood" in url -> doodExtractor.videoFromUrl(url)?.let(::listOf)
"drive.google" in url -> {
val newUrl = "https://gdriveplayer.to/embed2.php?link=$url"
gdrivePlayerExtractor.videosFromUrl(newUrl, "GdrivePlayer", headers)
}
"uqload" in url -> uqloadExtractor.videosFromUrl(url)
"voe.sx" in url -> voeExtractor.videosFromUrl(url)
"anizmplayer.com" in url -> aincradExtractor.videosFromUrl(url)
else -> null
} ?: emptyList()
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_FANSUB_SELECTION_KEY
title = PREF_FANSUB_SELECTION_TITLE
PREF_FANSUB_SELECTION_ENTRIES.let {
entries = it
entryValues = it
setDefaultValue(it.toSet())
}
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = PREF_ADDITIONAL_FANSUBS_KEY
title = PREF_ADDITIONAL_FANSUBS_TITLE
dialogTitle = PREF_ADDITIONAL_FANSUBS_DIALOG_TITLE
dialogMessage = PREF_ADDITIONAL_FANSUBS_DIALOG_MESSAGE
setDefaultValue(PREF_ADDITIONAL_FANSUBS_DEFAULT)
summary = PREF_ADDITIONAL_FANSUBS_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
runCatching {
val value = newValue as String
Toast.makeText(screen.context, PREF_ADDITIONAL_FANSUBS_TOAST, Toast.LENGTH_LONG).show()
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTS_SELECTION_KEY
title = PREF_HOSTS_SELECTION_TITLE
entries = PREF_HOSTS_SELECTION_ENTRIES
entryValues = PREF_HOSTS_SELECTION_ENTRIES
setDefaultValue(PREF_HOSTS_SELECTION_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) }, // preferred quality first
{ it.quality.substringBefore("]") }, // then group by fansub
// then group by quality
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
private fun String.fixedFansubName(): String =
substringBefore("- BD")
.substringBefore("Fansub")
.substringBefore("Bağımsız")
.trim()
private fun Elements.filterSubs(): List<Element> {
val allFansubs = PREF_FANSUB_SELECTION_ENTRIES
val chosenFansubs = preferences.getStringSet(PREF_FANSUB_SELECTION_KEY, allFansubs.toSet())!!
return toList().filter {
val text = it.text().fixedFansubName()
text in chosenFansubs || text !in allFansubs
}
}
private val PREF_FANSUB_SELECTION_ENTRIES: Array<String> get() {
val additional = preferences.getString(PREF_ADDITIONAL_FANSUBS_KEY, "")!!
.split(",")
.map { it.fixedFansubName() }
.filter(String::isNotBlank)
.toSet()
return (DEFAULT_FANSUBS + additional).sorted().toTypedArray()
}
companion object {
const val PREFIX_SEARCH = "id:"
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
private const val PREF_FANSUB_SELECTION_KEY = "pref_fansub_selection"
private const val PREF_FANSUB_SELECTION_TITLE = "Enable/Disable Fansubs"
private val DEFAULT_FANSUBS by lazy {
setOf(
"Adonis",
"Akatsuki",
"AnimeSeverler",
"AniSekai",
"Aoi",
"ARE-YOU-SURE",
"ÇeviriBükücüler",
"DeiraSubs",
"Güncellenecek",
"hitokirireaper",
"Holy",
"Lawsonia",
"LoliSubs",
"LowSubs",
"Magnum357",
"NaoSubs",
"Origami",
"PijamalıKoi",
"Tempest",
"UragiriSubs",
"whosgoodbadass",
"Yuki",
"YuushaSubs",
)
}
private const val PREF_ADDITIONAL_FANSUBS_KEY = "pref_additional_fansubs_key"
private const val PREF_ADDITIONAL_FANSUBS_TITLE = "Add custom fansubs to the selection preference"
private const val PREF_ADDITIONAL_FANSUBS_DEFAULT = ""
private const val PREF_ADDITIONAL_FANSUBS_DIALOG_TITLE = "Enter a list of additional fansubs, separated by a comma."
private const val PREF_ADDITIONAL_FANSUBS_DIALOG_MESSAGE = "Example: AntichristHaters Fansub, 2cm erect subs"
private const val PREF_ADDITIONAL_FANSUBS_SUMMARY = "You can add more fansubs to the previous preference from here."
private const val PREF_ADDITIONAL_FANSUBS_TOAST = "Reopen the extension's preferences for it to take effect."
private const val PREF_HOSTS_SELECTION_KEY = "pref_hosts_selection"
private const val PREF_HOSTS_SELECTION_TITLE = "Disable/enable video hosts"
private val PREF_HOSTS_SELECTION_ENTRIES = arrayOf(
"Aincrad",
"DoodStream",
"FileMoon",
"GDrive",
"MP4Upload",
"Odnoklassniki",
"SendVid",
"Sibnet",
"StreamTape",
"UQload",
"Voe",
"YourUpload",
)
private val PREF_HOSTS_SELECTION_DEFAULT by lazy { PREF_HOSTS_SELECTION_ENTRIES.toSet() }
}
}

View file

@ -0,0 +1,551 @@
package eu.kanade.tachiyomi.animeextension.tr.anizm
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnizmFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class TriStateFilterList(name: String, val vals: Array<String>) :
AnimeFilter.Group<TriState>(name, vals.map(::TriStateVal))
private class TriStateVal(name: String) : TriState(name)
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.parseTriFilter(): List<List<String>> {
return (getFirst<R>() as TriStateFilterList).state
.filterNot { it.isIgnored() }
.map { filter -> filter.state to filter.name }
.groupBy { it.first } // group by state
.let {
val included = it.get(TriState.STATE_INCLUDE)?.map { it.second } ?: emptyList<String>()
val excluded = it.get(TriState.STATE_EXCLUDE)?.map { it.second } ?: emptyList<String>()
listOf(included, excluded)
}
}
class InitialLetterFilter : QueryPartFilter("İlk harf", AnizmFiltersData.INITIAL_LETTER)
class SortFilter : AnimeFilter.Sort(
"Sıra",
AnizmFiltersData.ORDERS.map { it.first }.toTypedArray(),
Selection(0, true),
)
class StudiosFilter : TriStateFilterList("Stüdyos", AnizmFiltersData.STUDIOS)
val FILTER_LIST get() = AnimeFilterList(
InitialLetterFilter(),
SortFilter(),
AnimeFilter.Separator(),
StudiosFilter(),
)
data class FilterSearchParams(
val initialLetter: String = "",
val sortBy: String = "A-Z",
val orderAscending: Boolean = true,
val blackListedStudios: List<String> = emptyList(),
val includedStudios: List<String> = emptyList(),
var animeName: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val (order, isAscending) = filters.getFirst<SortFilter>().state?.let {
Pair(AnizmFiltersData.ORDERS[it.index].second, it.ascending)
} ?: Pair("A-Z", true)
val (includedStudios, excludedStudios) = filters.parseTriFilter<StudiosFilter>()
return FilterSearchParams(
initialLetter = filters.asQueryPart<InitialLetterFilter>(),
sortBy = order,
orderAscending = isAscending,
blackListedStudios = excludedStudios,
includedStudios = includedStudios,
)
}
private fun mustRemove(anime: SearchItemDto, params: FilterSearchParams): Boolean {
return when {
params.animeName != "" && !anime.names.any { it.contains(params.animeName, true) } -> true
params.initialLetter != "" && !anime.title.lowercase().startsWith(params.initialLetter) -> true
params.blackListedStudios.size > 0 && params.blackListedStudios.any {
anime.studios?.contains(it, true) == true
} -> true
params.includedStudios.size > 0 && params.includedStudios.any {
anime.studios?.contains(it, true)?.not() == true
} -> true
else -> false
}
}
private inline fun <T, R : Comparable<R>> Sequence<T>.sortedByIf(
isAscending: Boolean,
crossinline selector: (T) -> R,
): Sequence<T> {
return when {
isAscending -> sortedBy(selector)
else -> sortedByDescending(selector)
}
}
fun Sequence<SearchItemDto>.applyFilterParams(params: FilterSearchParams): Sequence<SearchItemDto> {
return filterNot { mustRemove(it, params) }.let { results ->
when (params.sortBy) {
"A-Z" -> results.sortedByIf(params.orderAscending) { it.title.lowercase() }
"year" -> results.sortedByIf(params.orderAscending) { it.year?.toIntOrNull() ?: 0 }
"mal" -> results.sortedByIf(params.orderAscending) { it.malpoint ?: 0.0 }
else -> results
}
}
}
private object AnizmFiltersData {
val INITIAL_LETTER = arrayOf(Pair("Select", "")) + ('A'..'Z').map {
Pair(it.toString(), it.toString().lowercase())
}.toTypedArray()
val ORDERS = arrayOf(
Pair("Alfabetik sıra", "A-Z"),
Pair("Yapım Yılı", "year"),
Pair("MAL Score", "mal"),
)
val STUDIOS = arrayOf(
"2:10 AM Animation",
"3xCube",
"5 Inc.",
"8bit",
"A-1 Pictures",
"A-Real",
"A.C.G.T.",
"AHA Entertainment",
"AIC",
"APPP",
"AQUA ARIS",
"ARECT",
"ASK Animation Studio",
"AXsiZ",
"Acca effe",
"Actas",
"Adonero",
"Agent 21",
"Ajia-Do",
"Akatsuki",
"Albacrow",
"Alfred Imageworks",
"Anima",
"Anima&Co.",
"Animate Film",
"Animation Do",
"Anime Beans",
"Anpro",
"Ark",
"Arms",
"Artland",
"Artmic",
"Arvo Animation",
"Asahi Production",
"Ascension",
"Ashi Production",
"Asread",
"Asread.",
"AtelierPontdarc",
"B.CMAY PICTURES",
"BUG FILMS",
"Bakken Record",
"Bandai Namco Pictures",
"Barnum Studio",
"BeSTACK",
"Bee Media",
"Bee Train",
"Bibury Animation CG",
"Bibury Animation Studios",
"BigFireBird Animation",
"Blade",
"Bones",
"Brain's Base",
"Bridge",
"C-Station",
"C2C",
"CANDY BOX",
"CG Year",
"CGCG Studio",
"CLAP",
"Chaos Project",
"Charaction",
"Children's Playground Entertainment",
"CloverWorks",
"CoMix Wave Films",
"Code",
"Colored Pencil Animation",
"Colored Pencil Animation Japan",
"Connect",
"Craftar Studios",
"Creators in Pack",
"Cyclone Graphics",
"CygamesPictures",
"DLE",
"DMM.futureworks",
"DR Movie",
"DRAWIZ",
"Da Huoniao Donghua",
"Dai-Ichi Douga",
"DandeLion Animation Studio",
"Daume",
"David Production",
"Digital Frontier",
"Digital Network Animation",
"Diomedea",
"Diomedéa",
"Disney Plus",
"Doga Kobo",
"Domerica",
"Dongwoo A&E",
"Drive",
"Drop",
"Dwango",
"Dynamo Pictures",
"E&G Films",
"EKACHI EPILKA",
"EMT Squared",
"ENGI",
"East Fish Studio",
"Egg Firm",
"Emon",
"Encourage Films",
"EzÏ<EFBFBD>la",
"FILMONY",
"Fanworks",
"Feel.",
"Felix Film",
"Fenz",
"Fifth Avenue",
"Filmlink International",
"Flat Studio",
"Front Line",
"Fuji TV",
"Fukushima Gaina",
"G&G Entertainment",
"G-angle",
"GANSIS",
"GEEK TOYS",
"GEMBA",
"GIFTanimation",
"GRIZZLY",
"Gaina",
"Gainax",
"Gallop",
"Gathering",
"Geek Toys",
"Gekkou",
"Geno Studio",
"Giga Production",
"Ginga Ya",
"GoHands",
"Gonzo",
"Gosay Studio",
"Graphinica",
"Gravity Well",
"Group TAC",
"Grouper Productions",
"HORNETS",
"Hal Film Maker",
"Haoliners Animation League",
"Helo.inc",
"Hoods Drifters Studio",
"Hoods Entertainment",
"Hotline",
"I.Gzwei",
"IDRAGONS Creative Studio",
"ILCA",
"IMAGICA Lab.",
"Imagin",
"Imagineer",
"Indivision",
"Irawias",
"Ishikawa Pro",
"Issen",
"Ixtl",
"J.C.Staff",
"JCF",
"Japan Vistec",
"Jinnis Animation Studios",
"Jumondo",
"KOO-KI",
"Kachidoki Studio",
"Kamikaze Douga",
"Kanaban Graphics",
"Kaname Productions",
"Kazami Gakuen Koushiki Douga-bu",
"KeyEast",
"Khara",
"Kinema Citrus",
"Kitty Film Mitaka Studio",
"Kitty Films",
"Kyoto Animation",
"Kyotoma",
"L-a-unchã<68>»BOX",
"LAN Studio",
"LEVELS",
"LICO",
"LIDENFILMS",
"LIDENFILMS Kyoto Studio",
"LIDENFILMS Osaka Studio",
"LMD",
"LandQ studios",
"Lapin Track",
"Larx Entertainment",
"Lay-duce",
"Lerche",
"Lesprit",
"Liber",
"Life Work",
"Light Chaser Animation Studios",
"Lilix",
"L²Studio",
"M.S.C",
"MAPPA",
"MASTER LIGHTS",
"MMT Technology",
"Madhouse",
"Magia Doraglier",
"Magic Bus",
"Maho Film",
"Manglobe",
"Marine Entertainment",
"Marvy Jack",
"Marza Animation Planet",
"Milky Cartoon",
"Millepensee",
"Mimoid",
"Minami Machi Bugyousho",
"Monofilmo",
"MooGoo",
"Mook Animation",
"Mook DLE",
"Motion Magic",
"Mushi Production",
"NAZ",
"NHK",
"Namu Animation",
"Netflix",
"Next Media Animation",
"Nexus",
"Nice Boat Animation",
"Nihon Ad Systems",
"Nippon Animation",
"Nomad",
"Nut",
"OLM",
"OLM Digital",
"OLM Team Yoshioka",
"OZ",
"Office DCI",
"Office No. 8",
"Oh! Production",
"Okuruto Noboru",
"Opera House",
"Orange",
"Ordet",
"Oxybot",
"P.A. Works",
"P.I.C.S.",
"PRA",
"Pancake",
"Passione",
"Pastel",
"Pb Animation Co. Ltd.",
"Pencil Lead Animate",
"Phoenix Entertainment",
"Picture Magic",
"Pierrot",
"Pierrot Plus",
"Pine Jam",
"Planet",
"Platinum Vision",
"Plum",
"Polygon Pictures",
"Primastea",
"PrimeTime",
"Production +h.",
"Production GoodBook",
"Production I.G",
"Production IMS",
"Production Reed",
"Production doA",
"Project No.9",
"Purple Cow Studio Japan",
"Quad",
"Qualia Animation",
"Qubic Pictures",
"REALTHING",
"Radix",
"Red Dog Culture House",
"Remic",
"Revoroot",
"Rikuentai",
"Rising Force",
"Robot Communications",
"Rockwell Eyes",
"Ruo Hong Culture",
"SANZIGEN",
"SILVER LINK.",
"Saetta",
"Saigo no Shudan",
"Sakura Create",
"Samsara Animation Studio",
"Sanctuary",
"Sanrio",
"Satelight",
"Science SARU",
"Scooter Films",
"Seven",
"Seven Arcs",
"Seven Stone Entertainment",
"Shaft",
"Shanghai Animation Film Studio",
"Shanghai Foch Film",
"Shenying Animation",
"Shimogumi",
"Shin-Ei Animation",
"Shirogumi",
"Shuka",
"Signal.MD",
"Silver",
"Silver Link.",
"Sola Digital Arts",
"Soyep",
"Space Neko Company",
"Sparkly Key Animation Studio",
"Square Enix Visual Works",
"Staple Entertainment",
"Steve N' Steven",
"Stingray",
"Studio 3Hz",
"Studio 4°C",
"Studio A-CAT",
"Studio Animal",
"Studio Bind",
"Studio Blanc",
"Studio Blanc.",
"Studio Chizu",
"Studio Colorido",
"Studio Comet",
"Studio Dadashow",
"Studio Daisy",
"Studio Deen",
"Studio Fantasia",
"Studio Flad",
"Studio Flag",
"Studio GOONEYS",
"Studio Ghibli",
"Studio Gokumi",
"Studio Hibari",
"Studio Hokiboshi",
"Studio Jemi",
"Studio Junio",
"Studio Kafka",
"Studio Kai",
"Studio Kikan",
"Studio LAN",
"Studio Lings",
"Studio Live",
"Studio M2",
"Studio MOTHER",
"Studio March",
"Studio Matrix",
"Studio Moriken",
"Studio Palette",
"Studio Pierrot",
"Studio Ponoc",
"Studio PuYUKAI",
"Studio Rikka",
"Studio Signal",
"Studio Signpost",
"Studio VOLN",
"Studio Z5",
"Studio elle",
"Studio! Cucuri",
"Sublimation",
"Success Corp.",
"Sunrise",
"Sunrise Beyond",
"Super Normal Studio",
"SynergySP",
"TMS Entertainment",
"TNK",
"TROYCA",
"TYO Animations",
"Tama Production",
"Tamura Shigeru Studio",
"Tatsunoko Production",
"Team Yamahitsuji",
"Team YokkyuFuman",
"TeamKG",
"Tear Studio",
"Telecom Animation Film",
"Tencent Penguin Pictures",
"Tengu Kobo",
"Tezuka Productions",
"The Answer Studio",
"Thundray",
"Toei Animation",
"Toho Interactive Animation",
"Tokyo Kids",
"Tokyo Movie Shinsha",
"Tomason",
"Tomovies",
"Topcraft",
"Trans Arts",
"Tri-Slash",
"TriF Studio",
"Triangle Staff",
"Trigger",
"Trinet Entertainment",
"Tsuchida Productions",
"Twilight Studio",
"Typhoon Graphics",
"UWAN Pictures",
"Ufotable",
"Vega Entertainment",
"View Works",
"W-Toon Studio",
"WAO World",
"Wawayu Animation",
"White Fox",
"Wit Studio",
"Wolf Smoke Studio",
"Wolfsbane",
"XFLAG",
"Xebec",
"YHKT Entertainment",
"Yaoyorozu",
"Yokohama Animation Lab",
"Yostar Pictures",
"Yumeta Company",
"Zero-G",
"Zexcs",
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.tr.anizm
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://anizm.net/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnizmUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 0) {
val item = pathSegments.first()
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${Anizm.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,18 @@
package eu.kanade.tachiyomi.animeextension.tr.anizm
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SearchItemDto(
@SerialName("info_title") val title: String,
@SerialName("info_othernames") val othernames: String?,
@SerialName("info_japanese") val japanese: String?,
@SerialName("info_slug") val slug: String,
@SerialName("info_studios") val studios: String?,
@SerialName("info_poster") val thumbnail: String,
@SerialName("info_year") val year: String?,
@SerialName("info_malpoint") val malpoint: Double?,
) {
val names by lazy { listOfNotNull(othernames, japanese, title) }
}

View file

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.animeextension.tr.anizm.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
class AincradExtractor(
private val client: OkHttpClient,
private val headers: Headers,
private val json: Json,
) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String): List<Video> {
val hash = url.substringAfterLast("video/").substringBefore("/")
val body = FormBody.Builder()
.add("hash", hash)
.add("r", "https://anizm.net/")
.build()
val headers = headers.newBuilder()
.set("Origin", DOMAIN)
.set("Referer", url)
.set("X-Requested-With", "XMLHttpRequest")
.build()
val req = POST("$DOMAIN/player/index.php?data=$hash&do=getVideo", headers, body)
val res = client.newCall(req).execute().body.string()
return runCatching {
val data = json.decodeFromString<ResponseDto>(res)
playlistUtils.extractFromHls(
data.securedLink!!,
referer = url,
videoNameGen = { "Aincrad - $it" },
)
}.getOrElse { emptyList() }
}
@Serializable
data class ResponseDto(val securedLink: String?)
companion object {
private const val DOMAIN = "https://anizmplayer.com"
}
}

View file

@ -0,0 +1,18 @@
ext {
extName = 'AsyaAnimeleri'
extClass = '.AsyaAnimeleri'
themePkg = 'animestream'
baseUrl = 'https://asyaanimeleri.com'
overrideVersionCode = 3
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:vk-extractor"))
implementation(project(":lib:okru-extractor"))
implementation(project(":lib:sibnet-extractor"))
implementation(project(":lib:gdriveplayer-extractor"))
implementation(project(":lib:dood-extractor"))
// implementation(project(":lib:dailymotion-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,145 @@
package eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri
import eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri.AsyaAnimeleriFilters.CountryFilter
import eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri.AsyaAnimeleriFilters.GenresFilter
import eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri.AsyaAnimeleriFilters.NetworkFilter
import eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri.AsyaAnimeleriFilters.OrderFilter
import eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri.AsyaAnimeleriFilters.StatusFilter
import eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri.AsyaAnimeleriFilters.StudioFilter
import eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri.AsyaAnimeleriFilters.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.Video
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters
import eu.kanade.tachiyomi.network.GET
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class AsyaAnimeleri : AnimeStream(
"tr",
"AsyaAnimeleri",
"https://asyaanimeleri.com",
) {
override val animeListUrl = "$baseUrl/series"
override val dateFormatter by lazy {
SimpleDateFormat("MMMM dd, yyyy", Locale("tr"))
}
override val client by lazy {
network.client.newBuilder()
.addInterceptor(ShittyProtectionInterceptor(network.client))
.build()
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AsyaAnimeleriFilters.getSearchParameters(filters)
return if (query.isNotEmpty()) {
GET("$baseUrl/page/$page/?s=$query")
} else {
val additional = params.run { listOf(genres, studios, countries, networks) }
.filter(String::isNotBlank)
.joinToString("&")
val url = "$animeListUrl/?$additional".toHttpUrl().newBuilder()
.addQueryParameter("page", "$page")
.addIfNotBlank("status", params.status)
.addIfNotBlank("type", params.type)
.addIfNotBlank("order", params.order)
.build()
GET(url.toString(), headers)
}
}
// ============================== Filters ===============================
override val filtersSelector = "div.filter.dropdown > ul"
override fun getFilterList(): AnimeFilterList {
return if (AnimeStreamFilters.filterInitialized()) {
AnimeFilterList(
GenresFilter("Tür"),
StudioFilter("Stüdyo"),
CountryFilter("Ülke"),
NetworkFilter(""),
AnimeFilter.Separator(),
StatusFilter("Durum"),
TypeFilter("Tip"),
OrderFilter("Sirala"),
)
} else {
AnimeFilterList(AnimeFilter.Header(filtersMissingWarning))
}
}
// =========================== Anime Details ============================
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 val episodePrefix = "Bölüm"
// ============================ Video Links =============================
override val prefQualityValues = arrayOf("1080p", "720p", "480p", "360p", "240p", "144p")
override val prefQualityEntries = prefQualityValues
private val vkExtractor by lazy { VkExtractor(client, headers) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val sibnetExtractor by lazy { SibnetExtractor(client) }
private val gdrivePlayerExtractor by lazy { GdrivePlayerExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
// private val dailyExtractor by lazy { DailymotionExtractor(client, headers) }
override fun getVideoList(url: String, name: String): List<Video> {
return when (name.lowercase().trim()) {
"vk" -> vkExtractor.videosFromUrl(url)
"ok.ru" -> okruExtractor.videosFromUrl(url)
"sibnet" -> sibnetExtractor.videosFromUrl(url)
// "daily" -> dailyExtractor.videosFromUrl(url)
"dood", "doodstream" -> doodExtractor.videoFromUrl(url)?.let(::listOf) ?: emptyList()
"gdrive" -> {
val newUrl = "https://gdriveplayer.to/embed2.php?link=$url"
gdrivePlayerExtractor.videosFromUrl(newUrl, "Gdrive", headers)
}
else -> emptyList()
}
}
// ============================= Utilities ==============================
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String) = apply {
if (value.isNotBlank()) {
addQueryParameter(query, value)
}
}
// Overriding to prevent removing the ?resize part.
// Without it, some images simply don't load (????)
// Turkish source moment. That's why i prefer greeks.
override fun Element.getImageUrl(): String? {
return when {
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
else -> attr("abs:src")
}
}
}

View file

@ -0,0 +1,54 @@
package eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.CheckBoxFilterList
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.QueryPartFilter
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.asQueryPart
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.filterInitialized
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.getPairListByIndex
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.parseCheckbox
object AsyaAnimeleriFilters {
internal class GenresFilter(name: String) : CheckBoxFilterList(name, GENRES_LIST)
internal class StudioFilter(name: String) : CheckBoxFilterList(name, STUDIO_LIST)
internal class CountryFilter(name: String) : CheckBoxFilterList(name, COUNTRY_LIST)
internal class NetworkFilter(name: String) : CheckBoxFilterList(name, NETWORK_LIST)
internal class StatusFilter(name: String) : QueryPartFilter(name, STATUS_LIST)
internal class TypeFilter(name: String) : QueryPartFilter(name, TYPE_LIST)
internal class OrderFilter(name: String) : QueryPartFilter(name, ORDER_LIST)
internal data class FilterSearchParams(
val genres: String = "",
val studios: String = "",
val countries: String = "",
val networks: String = "",
val status: String = "",
val type: String = "",
val order: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty() || !filterInitialized()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenresFilter>(GENRES_LIST, "genre"),
filters.parseCheckbox<StudioFilter>(STUDIO_LIST, "studio"),
filters.parseCheckbox<CountryFilter>(COUNTRY_LIST, "country"),
filters.parseCheckbox<NetworkFilter>(NETWORK_LIST, "network"),
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<OrderFilter>(),
)
}
private val GENRES_LIST by lazy { getPairListByIndex(0) }
private val STUDIO_LIST by lazy { getPairListByIndex(2) }
private val COUNTRY_LIST by lazy { getPairListByIndex(3) }
private val NETWORK_LIST by lazy { getPairListByIndex(4) }
private val STATUS_LIST by lazy { getPairListByIndex(5) }
private val TYPE_LIST by lazy { getPairListByIndex(6) }
private val ORDER_LIST by lazy { getPairListByIndex(7) }
}

View file

@ -0,0 +1,73 @@
package eu.kanade.tachiyomi.animeextension.tr.asyaanimeleri
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.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().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<L;x++){
c=e[s.charAt(x)];b=(b<<6)+c;l+=6;
while(l>=8){((a=(b>>>(l-=8))&0xff)||(x<(L-2)))&&(r+=w(a));}
}
return r;
};
""".trimIndent()
}
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".tr.hdfilmcehennemi.HDFilmCehennemiUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="www.hdfilmcehennemi.us"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,13 @@
ext {
extName = 'HDFilmCehennemi'
extClass = '.HDFilmCehennemi'
extVersionCode = 15
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:unpacker"))
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,305 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors.CloseloadExtractor
import eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors.VidmolyExtractor
import eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors.XBetExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
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.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import okhttp3.FormBody
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class HDFilmCehennemi : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "HDFilmCehennemi"
override val baseUrl = "https://www.hdfilmcehennemi.us"
override val lang = "tr"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Origin", baseUrl)
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/en-cok-begenilen-filmleri-izle/page/$page/")
override fun popularAnimeSelector() = "div.row div.poster > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("h2.title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("data-src")
}
override fun popularAnimeNextPageSelector() = "ul.pagination > li > a[rel=next]"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page/")
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun getFilterList() = HDFilmCehennemiFilters.FILTER_LIST
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/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val headers = headersBuilder()
.add("X-Requested-With", "XMLHttpRequest")
.build()
return when {
query.isNotBlank() -> {
val body = FormBody.Builder().add("query", query).build()
POST("$baseUrl/search/", headers, body)
}
else -> {
val params = HDFilmCehennemiFilters.getSearchParameters(filters)
val form = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("kesfet[type]", params.type)
.addFormDataPart("kesfet[genres]", params.genres)
.addFormDataPart("kesfet[years]", params.years)
.addFormDataPart("kesfet[imdb]", params.imdbScore)
.addFormDataPart("kesfet[orderBy]", params.order)
.addFormDataPart("page", page.toString())
.build()
POST("$baseUrl/movies/load/", headers, form)
}
}
}
@Serializable
data class SearchResponse(val result: List<ItemDto>)
@Serializable
data class ItemDto(val title: String, val poster: String, val slug: String, val slug_prefix: String)
@Serializable
data class FilterSearchResponse(val html: String, val showMore: Boolean, val status: Int)
override fun searchAnimeParse(response: Response): AnimesPage {
return when {
response.request.url.toString().contains("/search/") -> { // Text search
val data = response.parseAs<SearchResponse>()
val items = data.result.map {
SAnime.create().apply {
title = it.title
thumbnail_url = "$baseUrl/uploads/poster/" + it.poster
url = "/" + it.slug_prefix + it.slug
}
}
AnimesPage(items, false)
}
else -> { // Filter search
val data = response.parseAs<FilterSearchResponse>()
if (data.status != 1) return AnimesPage(emptyList(), false)
val doc = response.asJsoup(data.html)
val items = doc.select(searchAnimeSelector()).map(::searchAnimeFromElement)
AnimesPage(items, data.showMore)
}
}
}
override fun searchAnimeSelector() = "div.poster > a"
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
status = when {
document.location().contains("/dizi/") -> SAnime.UNKNOWN // serie
else -> SAnime.COMPLETED // movie
}
val div = document.selectFirst("div.card-body > div.row")!!
div.selectFirst("img")!!.run {
thumbnail_url = absUrl("src")
title = attr("alt")
}
genre = div.select("div > a[href*=tur/]").eachText().joinToString().takeIf(String::isNotEmpty)
artist = div.select("a.chip[href*=oyuncu/]").eachText().joinToString().takeIf(String::isNotEmpty)
description = div.selectFirst("article > p")?.text()
}
// ============================== Episodes ==============================
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
// Series
if (anime.url.contains("/dizi/")) return super.getEpisodeList(anime)
// Movies
return listOf(
SEpisode.create().apply {
url = anime.url
name = "Movie"
episode_number = 1F
},
)
}
override fun episodeListParse(response: Response) =
super.episodeListParse(response).sortedByDescending { it.episode_number }
override fun episodeListSelector() = "div#seasonsTabs-tabContent div.card-list-item > a"
private val numberRegex by lazy { Regex("(\\d+)\\.") }
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.selectFirst("h3")!!.text()
date_upload = element.selectFirst("date")?.attr("datetime")?.toDate() ?: 0L
val (seasonNum, epNum) = numberRegex.findAll(name).map { it.groupValues.last() }.toList()
// good luck trying to track this xD
episode_number = "$seasonNum.${epNum.padStart(3, '0')}".toFloatOrNull() ?: 1F
}
// ============================ Video Links =============================
private val vidmolyExtractor by lazy { VidmolyExtractor(client, headers) }
private val closeloadExtractor by lazy { CloseloadExtractor(client, headers) }
private val xbetExtractor by lazy { XBetExtractor(client, headers) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
return doc.select("div.card-body > nav > a:not([href^=#])")
.drop(1)
.parallelMapBlocking { client.newCall(GET(it.absUrl("href") + "/")).await().asJsoup() }
.let { listOf(doc) + it }
.mapNotNull { it.selectFirst("div.card-video > iframe") }
.map { it.attr("data-src").ifBlank { it.attr("src") } }
.filter(String::isNotBlank)
.parallelCatchingFlatMapBlocking { url ->
when {
url.contains("https://closeload") -> closeloadExtractor.videosFromUrl(url)
url.contains("vidmoly") -> vidmolyExtractor.videosFromUrl(url)
url.contains("trstx.org") -> xbetExtractor.videosFromUrl(url)
else -> emptyList()
}
}
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
companion object {
const val PREFIX_SEARCH = "id:"
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("360p", "480p", "720p", "1080p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
}
}

View file

@ -0,0 +1,155 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object HDFilmCehennemiFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (first { it is R } as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): String {
return (first { it is R } as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.joinToString(",")
}
class TypeFilter : QueryPartFilter("Türü", HDFilmCehennemiFiltersData.TYPES)
class GenresFilter : CheckBoxFilterList("Türler", HDFilmCehennemiFiltersData.GENRES)
class YearsFilter : CheckBoxFilterList("Yıllar", HDFilmCehennemiFiltersData.YEARS)
class IMDBScoreFilter : CheckBoxFilterList("IMDb Puanı", HDFilmCehennemiFiltersData.SCORES)
class SortFilter : AnimeFilter.Sort(
"Sıralama Türü",
HDFilmCehennemiFiltersData.ORDERS.map { it.first }.toTypedArray(),
Selection(0, false),
)
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("NOTE: Ignored if using text search!"),
AnimeFilter.Separator(),
TypeFilter(),
SortFilter(),
AnimeFilter.Separator(),
IMDBScoreFilter(),
GenresFilter(),
YearsFilter(),
)
data class FilterSearchParams(
val type: String = "1",
val order: String = "posts.imdb desc",
val imdbScore: String = "",
val genres: String = "",
val years: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val sortFilter = filters.firstOrNull { it is SortFilter } as? SortFilter
val orderBy = sortFilter?.state?.run {
val order = HDFilmCehennemiFiltersData.ORDERS[index].second
val orderWay = if (ascending) "asc" else "desc"
"$order $orderWay"
} ?: "posts.imdb desc"
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
orderBy,
filters.parseCheckbox<IMDBScoreFilter>(HDFilmCehennemiFiltersData.SCORES),
filters.parseCheckbox<GenresFilter>(HDFilmCehennemiFiltersData.GENRES),
filters.parseCheckbox<YearsFilter>(HDFilmCehennemiFiltersData.YEARS),
)
}
private object HDFilmCehennemiFiltersData {
val TYPES = arrayOf(
Pair("Filmler", "1"),
Pair("Diziler", "2"),
)
val GENRES = arrayOf(
Pair("Adult", "40"),
Pair("Aile", "8"),
Pair("Aksiyon", "1"),
Pair("Animasyon", "3"),
Pair("Belgesel", "6"),
Pair("Bilim Kurgu", "24"),
Pair("Biyografi", "26"),
Pair("Dram", "7"),
Pair("Fantastik", "9"),
Pair("Film-Noir", "39"),
Pair("Game-Show", "34"),
Pair("Gerilim", "16"),
Pair("Gizem", "13"),
Pair("Komedi", "4"),
Pair("Korku", "11"),
Pair("Macera", "2"),
Pair("Müzik", "12"),
Pair("Müzik", "27"),
Pair("Polisiye", "32"),
Pair("Reality", "37"),
Pair("Reality-TV", "33"),
Pair("Romantik", "14"),
Pair("Savaş", "17"),
Pair("Short", "35"),
Pair("Spor", "28"),
Pair("Suç", "5"),
Pair("Tarih", "10"),
Pair("Western", "18"),
)
val YEARS = arrayOf(
Pair("2024", "2024"),
Pair("2023", "2023"),
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("2017", "2017"),
Pair("2016", "2016"),
Pair("2015-2010 arası", "2010-2015"),
Pair("2010-2000 arası", "2000-2010"),
Pair("2000 öncesi", "1901-2000"),
)
val SCORES = arrayOf(
Pair("9", "9-10"),
Pair("8", "8-9"),
Pair("7", "7-8"),
Pair("6", "6-7"),
Pair("5 ve altı", "0-6"),
)
val ORDERS = arrayOf(
Pair("IMDb Puanına", "posts.imdb"),
Pair("Site Puanı", "avg"),
Pair("Yıla", "posts.year"),
Pair("İzlenme", "views"),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://www.hdfilmcehennemi.us/<item> intents
* and redirects them to the main Aniyomi process.
*/
class HDFilmCehennemiUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size == 1) {
val item = pathSegments.first()
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${HDFilmCehennemi.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class CloseloadExtractor(private val client: OkHttpClient, private val headers: Headers) {
suspend fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url, headers)).await().asJsoup()
val script = doc.selectFirst("script:containsData(eval):containsData(PlayerInit)")?.data()
?: return emptyList()
val unpackedScript = Unpacker.unpack(script).takeIf(String::isNotEmpty)
?: return emptyList()
val varName = unpackedScript.substringAfter("atob(").substringBefore(")")
val playlistUrl = unpackedScript.getProperty("$varName=")
.let { String(Base64.decode(it, Base64.DEFAULT)) }
val hostUrl = "https://" + url.toHttpUrl().host
val videoHeaders = headers.newBuilder()
.set("Referer", url)
.set("origin", hostUrl)
.build()
runCatching { tryAjaxPost(unpackedScript, hostUrl) }
val subtitles = doc.select("track[src]").map {
Track(it.absUrl("src"), it.attr("label").ifEmpty { it.attr("srclang") })
}
return listOf(Video(playlistUrl, "Closeload", playlistUrl, videoHeaders, subtitleTracks = subtitles))
}
private suspend fun tryAjaxPost(script: String, hostUrl: String) {
val hash = script.getProperty("hash:")
val url = script.getProperty("url:").let {
when {
it.startsWith("//") -> "https:$it"
it.startsWith("/") -> "https://" + hostUrl + it
!it.startsWith("https://") -> "https://$it"
else -> it
}
}
val body = FormBody.Builder().add("hash", hash).build()
client.newCall(POST(url, headers, body)).await().close()
}
private fun String.getProperty(before: String) =
substringAfter("$before\"").substringBefore('"')
}

View file

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import okhttp3.Headers
import okhttp3.OkHttpClient
class VidmolyExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
suspend fun videosFromUrl(url: String): List<Video> {
val body = client.newCall(GET(url, headers)).await()
.body.string()
val playlistUrl = body.substringAfter("file:\"", "").substringBefore('"', "")
.takeIf(String::isNotBlank)
?: return emptyList()
return playlistUtils.extractFromHls(playlistUrl, url, videoNameGen = { "Vidmoly - $it" })
}
}

View file

@ -0,0 +1,58 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class XBetExtractor(
private val client: OkHttpClient,
private val headers: Headers,
) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
suspend fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url, headers)).await().asJsoup()
val script = doc.selectFirst("script:containsData(playerConfigs =)")?.data()
?: return emptyList()
val host = "https://${url.toHttpUrl().host}"
val postPath = script.substringAfter("file\":\"").substringBefore('"')
.replace("\\", "")
val postHeaders = headers.newBuilder()
.set("Referer", url)
.set("Origin", host)
.build()
val postRes = client.newCall(POST(host + postPath, postHeaders)).await()
.parseAs<List<VideoItemDto>> { it.replace("[],", "") }
return postRes.flatMap { video ->
runCatching {
val playlistUrl = client.newCall(POST(host + video.path, postHeaders)).await()
.body.string()
playlistUtils.extractFromHls(
playlistUrl,
url,
videoNameGen = { "[${video.title}] XBet - $it" },
)
}.getOrElse { emptyList() }
}
}
@Serializable
data class VideoItemDto(val file: String, val title: String) {
val path = "/playlist/${file.removeSuffix("~")}.txt"
}
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".tr.hentaizm.HentaiZMUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="www.hentaizm.fun"
android:pathPattern="/hentai-detay/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,8 @@
ext {
extName = 'HentaiZM'
extClass = '.HentaiZM'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,227 @@
package eu.kanade.tachiyomi.animeextension.tr.hentaizm
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.tr.hentaizm.extractors.VideaExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
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.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class HentaiZM : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "HentaiZM"
override val baseUrl = "https://www.hentaizm.fun"
override val lang = "tr"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
init {
runBlocking {
withContext(Dispatchers.IO) {
val body = FormBody.Builder()
.add("user", "demo")
.add("pass", "demo") // peak security
.add("redirect_to", baseUrl)
.build()
val headers = headersBuilder()
.add("X-Requested-With", "XMLHttpRequest")
.build()
client.newCall(POST("$baseUrl/giris", headers, body)).execute()
.close()
}
}
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/en-cok-izlenenler/page/$page", headers)
override fun popularAnimeParse(response: Response) =
super.popularAnimeParse(response).let { page ->
val animes = page.animes.distinctBy { it.url }
AnimesPage(animes, page.hasNextPage)
}
override fun popularAnimeSelector() = "div.moviefilm"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
title = element.selectFirst("div.movief > a")!!.text()
.substringBefore(". Bölüm")
.substringBeforeLast(" ")
element.selectFirst("img")!!.attr("abs:src").also {
thumbnail_url = it
val slug = it.substringAfterLast("/").substringBefore(".")
setUrlWithoutDomain("/hentai-detay/$slug")
}
}
override fun popularAnimeNextPageSelector() = "span.current + a"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/yeni-eklenenler?c=${page - 1}", headers)
override fun latestUpdatesParse(response: Response) =
super.latestUpdatesParse(response).let { page ->
val animes = page.animes.distinctBy { it.url }
AnimesPage(animes, page.hasNextPage)
}
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = "a[rel=next]:contains(Sonraki Sayfa)"
// =============================== 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/hentai-detay/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return GET("$baseUrl/page/$page/?s=$query", headers)
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun searchAnimeSelector() = throw UnsupportedOperationException()
override fun searchAnimeFromElement(element: Element) = throw UnsupportedOperationException()
override fun searchAnimeNextPageSelector() = null
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
setUrlWithoutDomain(document.location())
val content = document.selectFirst("div.filmcontent")!!
title = content.selectFirst("h1")!!.text()
thumbnail_url = content.selectFirst("img")!!.attr("abs:src")
genre = content.select("tr:contains(Hentai Türü) > td > a").eachText().joinToString()
description = content.selectFirst("tr:contains(Özet) + tr > td")
?.text()
?.takeIf(String::isNotBlank)
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "div#Bolumler li > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.text().also {
val num = it.substringBeforeLast(". Bölüm", "")
.substringAfterLast(" ")
.ifBlank { "1" }
episode_number = num.toFloatOrNull() ?: 1F
name = "$num. Bölüm"
}
}
// ============================ Video Links =============================
private val videaExtractor by lazy { VideaExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val videaItem = doc.selectFirst("div.alternatif a:contains(Videa)")!!
val path = videaItem.attr("onclick").substringAfter("../../").substringBefore("'")
val req = client.newCall(GET("$baseUrl/$path", headers)).execute()
.asJsoup()
val videaUrl = req.selectFirst("iframe")!!.attr("abs:src")
return videaExtractor.videosFromUrl(videaUrl)
}
private val qualityRegex by lazy { Regex("""(\d+)p""") }
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ qualityRegex.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
companion object {
const val PREFIX_SEARCH = "id:"
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "240p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.tr.hentaizm
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://www.hentaizm.fun/hentai-detay/<item> intents
* and redirects them to the main Aniyomi process.
*/
class HentaiZMUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${HentaiZM.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.animeextension.tr.hentaizm.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import org.jsoup.Jsoup
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
class VideaExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val body = client.newCall(GET(url)).execute().body.string()
val nonce = NONCE_REGEX.find(body)?.groupValues?.elementAt(1) ?: return emptyList()
val paramL = nonce.substring(0, 32)
val paramS = nonce.substring(32)
val result = (0..31).joinToString("") {
val index = it - (STUPID_KEY.indexOf(paramL.elementAt(it)) - 31)
paramS.elementAt(index).toString()
}
val seed = getRandomString(8)
val requestUrl = REQUEST_URL.toHttpUrl().newBuilder()
.addQueryParameter("_s", seed)
.addQueryParameter("_t", result.substring(0, 16))
.addQueryParameter("v", url.toHttpUrl().queryParameter("v") ?: "")
.build()
val headers = Headers.headersOf("referer", url, "origin", "https://videa.hu")
val response = client.newCall(GET(requestUrl.toString(), headers)).execute()
val doc = response.body.string().let {
when {
it.startsWith("<?xml") -> Jsoup.parse(it)
else -> {
val key = result.substring(16) + seed + response.headers["x-videa-xs"]
val b64dec = Base64.decode(it, Base64.DEFAULT)
Jsoup.parse(decryptXml(b64dec, key))
}
}
}
return doc.select("video_source").mapNotNull {
val name = it.attr("name")
val quality = "Videa - $name"
val hash = doc.selectFirst("hash_value_$name")?.text()
?: return@mapNotNull null
val videoUrl = "https:" + it.text() + "?md5=$hash&expires=${it.attr("exp")}"
Video(videoUrl, quality, videoUrl, headers)
}
}
private fun decryptXml(xml: ByteArray, key: String): String {
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.getParameters())
return cipher.doFinal(xml).toString(Charsets.UTF_8)
}
private fun getRandomString(length: Int = 8): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
companion object {
private val NONCE_REGEX by lazy { Regex("_xt\\s*=\\s*\"([^\"]+)\"") }
private const val REQUEST_URL = "https://videa.hu/player/xml?platform=desktop"
private const val STUPID_KEY = "xHb0ZvME5q8CBcoQi6AngerDu3FGO9fkUlwPmLVY_RTzj2hJIS4NasXWKy1td7p"
}
}

View file

@ -0,0 +1,9 @@
ext {
extName = 'TRAnimeCI'
extClass = '.TRAnimeCI'
themePkg = 'animestream'
baseUrl = 'https://tranimaci.com'
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -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.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().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<L;x++){
c=e[s.charAt(x)];b=(b<<6)+c;l+=6;
while(l>=8){((a=(b>>>(l-=8))&0xff)||(x<(L-2)))&&(r+=w(a));}
}
return r;
};
""".trimIndent()
}
}

View file

@ -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://tranimaci.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<Video> {
val doc = response.asJsoup()
val script = doc.selectFirst("script:containsData(let video_source)")!!.data()
return script.substringAfter("[").substringBefore("]")
.split("{")
.drop(1)
.map {
val quality = it.substringAfter("name\":\"").substringBefore('"')
val url = it.substringAfter("url\":\"").substringBefore('"')
Video(url, quality, url, headers)
}
}
// ============================= Utilities ==============================
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String) = apply {
if (value.isNotBlank()) {
addQueryParameter(query, value)
}
}
}

View file

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.animeextension.tr.tranimeci
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.CheckBoxFilterList
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.QueryPartFilter
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.asQueryPart
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.filterInitialized
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.getPairListByIndex
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.parseCheckbox
object TRAnimeCIFilters {
internal class GenresFilter(name: String) : CheckBoxFilterList(name, GENRES_LIST)
internal class CountryFilter(name: String) : QueryPartFilter(name, COUNTRY_LIST)
internal class SeasonFilter(name: String) : QueryPartFilter(name, SEASON_LIST)
internal class TypeFilter(name: String) : QueryPartFilter(name, TYPE_LIST)
internal class StudioFilter(name: String) : QueryPartFilter(name, STUDIO_LIST)
internal data class FilterSearchParams(
val genres: String = "",
val country: String = "",
val season: String = "",
val type: String = "",
val studio: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty() || !filterInitialized()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenresFilter>(GENRES_LIST, "category"),
filters.asQueryPart<CountryFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<StudioFilter>(),
)
}
private fun getPairListByIndexSorted(index: Int) =
getPairListByIndex(index)
.sortedBy { it.first.lowercase() }
.toTypedArray()
private val EVERY get() = arrayOf(Pair("Tüm", ""))
private val GENRES_LIST by lazy { getPairListByIndexSorted(0) }
private val COUNTRY_LIST by lazy { EVERY + getPairListByIndexSorted(1) }
private val SEASON_LIST by lazy { EVERY + getPairListByIndexSorted(2) }
private val TYPE_LIST by lazy { EVERY + getPairListByIndexSorted(4) }
private val STUDIO_LIST by lazy { EVERY + getPairListByIndexSorted(5) }
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".tr.tranimeizle.TRAnimeIzleUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="www.tranimeizle.co"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,20 @@
ext {
extName = 'TR Anime Izle'
extClass = '.TRAnimeIzle'
extVersionCode = 16
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:filemoon-extractor"))
implementation(project(":lib:mixdrop-extractor"))
implementation(project(":lib:mp4upload-extractor"))
implementation(project(":lib:okru-extractor"))
implementation(project(":lib:sendvid-extractor"))
implementation(project(":lib:sibnet-extractor"))
implementation(project(":lib:streamlare-extractor"))
implementation(project(":lib:voe-extractor"))
implementation(project(":lib:vudeo-extractor"))
implementation(project(":lib:yourupload-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,70 @@
package eu.kanade.tachiyomi.animeextension.tr.tranimeizle
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import java.security.MessageDigest
class ShittyCaptchaInterceptor(private val baseUrl: String, private val headers: Headers) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val originalResponse = chain.proceed(request)
val currentUrl = originalResponse.request.url.toString()
if (!currentUrl.contains("/api/CaptchaChallenge")) {
return originalResponse
}
originalResponse.close()
val body = FormBody.Builder()
.add("cID", "0")
.add("rT", "1")
.add("tM", "light")
.build()
val newHeaders = headers.newBuilder()
.set("Referer", currentUrl)
.add("X-Requested-With", "XMLHttpRequest")
.build()
val imagesIDs = chain.proceed(POST("$baseUrl/api/Captcha/", newHeaders, body))
.body.string()
.removeSurrounding("[", "]")
.split(',')
.map { it.removeSurrounding("\"") }
val hashes = imagesIDs.map { id ->
chain.proceed(GET("$baseUrl/api/Captcha/?cid=0&hash=$id")).use { req ->
// TODO: Use OKIO built-in md5 function
// for some reason it refused to work well
val hash = req.body.use { md5Hash(it.bytes()) }
Pair(id, hash)
}
}
val correctHash = hashes.groupingBy { it.second }.eachCount()
.minByOrNull { it.value }
?.let { entry -> hashes.firstOrNull { it.second == entry.key }?.first }
?: throw IOException("Error while bypassing captcha!")
val finalBody = FormBody.Builder()
.add("cID", "0")
.add("rT", "2")
.add("pC", correctHash)
.build()
chain.proceed(POST("$baseUrl/api/Captcha/", newHeaders, finalBody))
.close()
return chain.proceed(GET(currentUrl, headers))
}
private fun md5Hash(byteArray: ByteArray) =
MessageDigest.getInstance("MD5")
.digest(byteArray)
.joinToString("") { "%02x".format(it) } // create hex
}

View file

@ -0,0 +1,442 @@
package eu.kanade.tachiyomi.animeextension.tr.tranimeizle
import android.app.Application
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
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.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.vudeoextractor.VudeoExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class TRAnimeIzle : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "TR Anime Izle"
override val baseUrl = "https://www.tranimeizle.co"
override val lang = "tr"
override val supportsLatest = true
override val client by lazy {
network.client.newBuilder()
.addInterceptor(ShittyCaptchaInterceptor(baseUrl, headers))
.build()
}
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Origin", baseUrl)
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/listeler/populer/sayfa-$page")
override fun popularAnimeSelector() = "div.post-body div.flx-block"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("data-href"))
thumbnail_url = element.selectFirst("img")!!.attr("src")
title = element.selectFirst("div.bar > h4")!!.text().clearName()
}
override fun popularAnimeNextPageSelector() = "ul.pagination > li:has(.ti-angle-right):not(.disabled)"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/listeler/yenibolum/sayfa-$page")
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) =
popularAnimeFromElement(element).apply {
// Convert episode url to anime url
url = "/anime$url".substringBefore("-bolum").substringBeforeLast("-") + "-izle"
}
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== 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.asJsoup())
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
GET("$baseUrl/arama/$query?page=$page")
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
setUrlWithoutDomain(document.location())
title = document.selectFirst("div.playlist-title h1")!!.text().clearName()
thumbnail_url = document.selectFirst("div.poster .social-icon img")!!.attr("src")
val infosDiv = document.selectFirst("div.col-md-6 > div.row")!!
genre = infosDiv.select("div > a.genre").eachText().joinToString()
author = infosDiv.select("dd:contains(Fansublar) + dt a").eachText().joinToString()
description = buildString {
document.selectFirst("div.p-10 > p")?.text()?.also(::append)
var dtCount = 0 // AAAAAAAA I HATE MUTABLE VALUES
infosDiv.select("dd, dt").forEach {
// Ignore non-wanted info
it.selectFirst("dd:contains(Puanlama), dd:contains(Anime Türü), dt:has(i.fa-star), dt:has(a.genre)")
?.let { return@forEach }
val text = it.text()
// yes
when (it.tagName()) {
"dd" -> {
append("\n$text: ")
dtCount = 0
}
"dt" -> {
if (dtCount == 0) {
append(text)
} else {
append(", $text")
}
dtCount++
}
}
}
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
override fun episodeListSelector() = "div.animeDetail-items > ol a:has(div.episode-li)"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
val epNum = element.selectFirst(".etitle > span")!!.text()
.substringBefore(". Bölüm", "")
.substringAfterLast(" ", "")
.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(".etitle > small.author")?.text()?.toDate() ?: 0L
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val episodeId = doc.selectFirst("input#EpisodeId")!!.attr("value")
val allFansubs = PREF_FANSUB_SELECTION_ENTRIES
val chosenFansubs = preferences.getStringSet(PREF_FANSUB_SELECTION_KEY, allFansubs.toSet())!!
val chosenHosts = preferences.getStringSet(PREF_HOSTS_SELECTION_KEY, PREF_HOSTS_SELECTION_DEFAULT)!!
return doc.select("div.fansubSelector").toList()
// Filter-out non-chosen fansubs that were included in the fansub selection preference.
// This way we prevent excluding unknown/non-added fansubs.
.filter { it.text() in chosenFansubs || it.text() !in allFansubs }
.flatMap { fansub ->
val fansubId = fansub.attr("data-fid")
val fansubName = fansub.text()
val body = """{"EpisodeId":$episodeId,"FansubId":$fansubId}"""
.toRequestBody("application/json".toMediaType())
client.newCall(POST("$baseUrl/api/fansubSources", headers, body))
.execute()
.asJsoup()
.select("li.sourceBtn")
.toList()
.filter { it.selectFirst("p")?.ownText().orEmpty() in chosenHosts }
.parallelCatchingFlatMapBlocking {
getVideosFromId(it.attr("data-id"))
}
.map {
Video(
it.url,
"[$fansubName] ${it.quality}",
it.videoUrl,
it.headers,
it.subtitleTracks,
it.audioTracks,
)
}
}
}
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val mixDropExtractor by lazy { MixDropExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val sendvidExtractor by lazy { SendvidExtractor(client, headers) }
private val sibnetExtractor by lazy { SibnetExtractor(client) }
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
private val voeExtractor by lazy { VoeExtractor(client) }
private val vudeoExtractor by lazy { VudeoExtractor(client) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private fun getVideosFromId(id: String): List<Video> {
val url = client.newCall(POST("$baseUrl/api/sourcePlayer/$id")).execute()
.body.string()
.substringAfter("src=")
.substringAfter('"')
.substringAfter("/embed2/?id=")
.substringBefore('"')
.replace("\\", "")
.trim()
.let {
when {
it.startsWith("https") -> it
else -> "https:$it"
}
}
// That's going to take an entire year to load, and I really don't care.
return when {
"filemoon.sx" in url -> filemoonExtractor.videosFromUrl(url, headers = headers)
"mixdrop" in url -> mixDropExtractor.videoFromUrl(url)
"mp4upload" in url -> mp4uploadExtractor.videosFromUrl(url, headers)
"ok.ru" in url || "odnoklassniki.ru" in url -> okruExtractor.videosFromUrl(url)
"sendvid.com" in url -> sendvidExtractor.videosFromUrl(url)
"video.sibnet" in url -> sibnetExtractor.videosFromUrl(url)
"streamlare.com" in url -> streamlareExtractor.videosFromUrl(url)
"voe.sx" in url -> voeExtractor.videosFromUrl(url)
"//vudeo." in url -> vudeoExtractor.videosFromUrl(url)
"yourupload.com" in url -> {
yourUploadExtractor.videoFromUrl(url, headers)
// ignore error links
.filterNot { it.url.contains("/novideo.mp4") }
}
else -> emptyList()
}
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_FANSUB_SELECTION_KEY
title = PREF_FANSUB_SELECTION_TITLE
PREF_FANSUB_SELECTION_ENTRIES.let {
entries = it
entryValues = it
setDefaultValue(it.toSet())
}
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = PREF_ADDITIONAL_FANSUBS_KEY
title = PREF_ADDITIONAL_FANSUBS_TITLE
dialogTitle = PREF_ADDITIONAL_FANSUBS_DIALOG_TITLE
dialogMessage = PREF_ADDITIONAL_FANSUBS_DIALOG_MESSAGE
setDefaultValue(PREF_ADDITIONAL_FANSUBS_DEFAULT)
summary = PREF_ADDITIONAL_FANSUBS_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
runCatching {
val value = newValue as String
Toast.makeText(screen.context, PREF_ADDITIONAL_FANSUBS_TOAST, Toast.LENGTH_LONG).show()
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTS_SELECTION_KEY
title = PREF_HOSTS_SELECTION_TITLE
entries = PREF_HOSTS_SELECTION_ENTRIES
entryValues = PREF_HOSTS_SELECTION_ENTRIES
setDefaultValue(PREF_HOSTS_SELECTION_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun String.clearName() = removeSuffix(" İzle").removeSuffix(" Bölüm")
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private val defaultSubs by lazy {
setOf(
"Adonis Fansub",
"Aitr",
"Akatsuki Fansub",
"AniKeyf",
"ANS Fansub",
"AnimeMangaTR",
"AnimeOu Fansub",
"AniSekai Fansub",
"AniTürk",
"AoiSubs",
"ARE-YOU-SURE (AYS)",
"AnimeWho",
"Chevirman",
"Fatality",
"HikiGayaFansub",
"HolySubs",
"Lawsonia Sub",
"LowSubs",
"Momo & Berhann",
"NoaSubs",
"OrigamiSubs",
"Puzzle Fansub",
"ShimazuSubs",
"SoutenSubs",
"TAÇE",
"TRanimeizle",
"TR Altyazılı",
"Uragiri Fansub",
"Varsayılan",
)
}
private val PREF_FANSUB_SELECTION_ENTRIES: Array<String> get() {
val additional = preferences.getString(PREF_ADDITIONAL_FANSUBS_KEY, "")!!
.split(",")
.map(String::trim)
.filter(String::isNotBlank)
.toSet()
return (defaultSubs + additional.sorted()).toTypedArray()
}
companion object {
const val PREFIX_SEARCH = "id:"
private val DATE_FORMATTER by lazy {
SimpleDateFormat("dd MMM yyyy", Locale("tr"))
}
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
private const val PREF_FANSUB_SELECTION_KEY = "pref_fansub_selection"
private const val PREF_FANSUB_SELECTION_TITLE = "Enable/Disable Fansubs"
private const val PREF_ADDITIONAL_FANSUBS_KEY = "pref_additional_fansubs_key"
private const val PREF_ADDITIONAL_FANSUBS_TITLE = "Add custom fansubs to the selection preference"
private const val PREF_ADDITIONAL_FANSUBS_DEFAULT = ""
private const val PREF_ADDITIONAL_FANSUBS_DIALOG_TITLE = "Enter a list of additional fansubs, separated by a comma."
private const val PREF_ADDITIONAL_FANSUBS_DIALOG_MESSAGE = "Example: AntichristHaters Fansub, 2cm erect subs"
private const val PREF_ADDITIONAL_FANSUBS_SUMMARY = "You can add more fansubs to the previous preference from here."
private const val PREF_ADDITIONAL_FANSUBS_TOAST = "Reopen the extension's preferences for it to take effect."
private const val PREF_HOSTS_SELECTION_KEY = "pref_hosts_selection"
private const val PREF_HOSTS_SELECTION_TITLE = "Enable/disable video hosts"
private val PREF_HOSTS_SELECTION_ENTRIES = arrayOf(
"Filemoon",
"MixDrop",
"Mp4upload",
"Ok.RU",
"SendVid",
"Sibnet",
"Streamlare",
"Voe",
"Vudeo",
"Yourupload",
)
// XDDDDDDDDD
private val PREF_HOSTS_SELECTION_DEFAULT by lazy { PREF_HOSTS_SELECTION_ENTRIES.toSet() }
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.tr.tranimeizle
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://www.tranimeizle.co/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class TRAnimeIzleUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${TRAnimeIzle.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,24 @@
ext {
extName = 'Türk Anime TV'
extClass = '.TurkAnime'
extVersionCode = 24
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:vudeo-extractor'))
implementation(project(':lib:uqload-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
implementation(project(":lib:cryptoaes"))
implementation(project(":lib:dood-extractor"))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:googledrive-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(":lib:okru-extractor"))
implementation(project(":lib:sendvid-extractor"))
implementation(project(":lib:sibnet-extractor"))
implementation(project(":lib:synchrony"))
implementation(project(":lib:vk-extractor"))
implementation(project(":lib:voe-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View file

@ -0,0 +1,572 @@
package eu.kanade.tachiyomi.animeextension.tr.turkanime
import android.app.Application
import android.util.Base64
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors.AlucardExtractor
import eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors.EmbedgramExtractor
import eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors.MVidooExtractor
import eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors.MailRuExtractor
import eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors.StreamVidExtractor
import eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors.VTubeExtractor
import eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors.WolfstreamExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
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.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.googledriveextractor.GoogleDriveExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.vudeoextractor.VudeoExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapBlocking
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class TurkAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Türk Anime TV"
override val baseUrl = "https://www.turkanime.co"
override val lang = "tr"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/ajax/rankagore?sayfa=$page", xmlHeader)
override fun popularAnimeSelector() = "div.panel-visible"
override fun popularAnimeNextPageSelector() = "button.btn-default[data-loading-text*=Sonraki]"
override fun popularAnimeFromElement(element: Element): SAnime {
val animeTitle = element.selectFirst("div.panel-title > a")!!
val name = animeTitle.attr("title")
.substringBefore(" izle")
val img = element.selectFirst("img.media-object")
val animeId = element.selectFirst("a.reactions")!!.attr("data-unique-id")
val animeUrl = animeTitle.attr("abs:href").toHttpUrl()
.newBuilder()
.addQueryParameter("animeId", animeId)
.build().toString()
return SAnime.create().apply {
setUrlWithoutDomain(animeUrl)
title = name
thumbnail_url = img?.attr("abs:data-src")
}
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/ajax/yenieklenenseriler?sayfa=$page", xmlHeader)
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
POST(
"$baseUrl/arama?sayfa=$page",
headers,
FormBody.Builder().add("arama", query).build(),
)
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val scriptElement = document.selectFirst("div.panel-body > script:containsData(window.location)")
return if (scriptElement == null) {
val animeList = document.select(searchAnimeSelector()).map(::searchAnimeFromElement)
AnimesPage(animeList, document.selectFirst(searchAnimeSelector()) != null)
} else {
val location = scriptElement.data()
.substringAfter("window.location")
.substringAfter("\"")
.substringBefore("\"")
val slug = if (location.startsWith("/")) location else "/$location"
val animeList = listOf(
SAnime.create().apply {
setUrlWithoutDomain(slug)
thumbnail_url = ""
title = slug.substringAfter("anime/")
},
)
AnimesPage(animeList, false)
}
}
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val img = document.selectFirst("div.imaj > img.media-object")
val studio = document.selectFirst("div#animedetay > table tr:contains(Stüdyo) > td:last-child a")
val desc = document.selectFirst("div#animedetay p.ozet")
val genres = document.select("div#animedetay > table tr:contains(Anime Türü) > td:last-child a")
.ifEmpty { null }
return SAnime.create().apply {
title = document.select("div#detayPaylas div.panel-title").text()
thumbnail_url = img?.let { "https:" + it.attr("data-src") }
author = studio?.text()
description = desc?.text()
genre = genres?.joinToString { it.text() }
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val animeId = (baseUrl + anime.url).toHttpUrl().queryParameter("animeId")
?: client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
.selectFirst("a[data-unique-id]")!!.attr("data-unique-id")
return GET("$baseUrl/ajax/bolumler?animeId=$animeId", xmlHeader)
}
override fun episodeListSelector() = "ul.menum li"
override fun episodeFromElement(element: Element): SEpisode {
val a = element.selectFirst("a:has(span.bolumAdi)")!!
val title = a.attr("title")
val substring = title.substringBefore(". Bölüm")
val numIdx = substring.indexOfLast { !it.isDigit() } + 1
val numbers = substring.slice(numIdx..substring.lastIndex)
return SEpisode.create().apply {
setUrlWithoutDomain(a.attr("abs:href"))
name = title
episode_number = numbers.toFloatOrNull() ?: 1F
}
}
override fun episodeListParse(response: Response): List<SEpisode> =
super.episodeListParse(response).reversed()
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val fansubbers = document.select("div#videodetay div.pull-right button")
val videoList = if (fansubbers.size == 1) {
getVideosFromHosters(document, fansubbers.first()!!.text().trim())
} else {
val allFansubs = PREF_FANSUB_SELECTION_ENTRIES
val chosenFansubs = preferences.getStringSet(PREF_FANSUB_SELECTION_KEY, allFansubs.toSet())!!
val filteredSubs = fansubbers.toList().filter {
val subName = it.text().substringBeforeLast("BD").trim()
chosenFansubs.any(subName::contains) || allFansubs.none(subName::contains)
}
filteredSubs.parallelCatchingFlatMapBlocking {
val url = it.attr("onclick").trimOnClick()
val subDoc = client.newCall(GET(url, xmlHeader)).await().asJsoup()
getVideosFromHosters(subDoc, it.text().trim())
}
}
require(videoList.isNotEmpty()) { "Failed to extract videos" }
return videoList
}
private fun getVideosFromHosters(document: Document, subber: String): List<Video> {
val selectedHoster = document.select("div#videodetay div.btn-group:not(.pull-right) > button.btn-danger")
val hosters = document.select("div#videodetay div.btn-group:not(.pull-right) > button.btn-default[onclick*=videosec]")
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
val videoList = buildList {
val selectedHosterName = selectedHoster.text().trim()
if (selectedHosterName in SUPPORTED_HOSTERS && selectedHosterName in hosterSelection) {
document.selectFirst("iframe")?.attr("src")?.also { src ->
addAll(getVideosFromSource(src, selectedHosterName, subber))
}
}
hosters.parallelMapBlocking {
val hosterName = it.text().trim()
if (hosterName !in SUPPORTED_HOSTERS) return@parallelMapBlocking
if (hosterName !in hosterSelection) return@parallelMapBlocking
val url = it.attr("onclick").trimOnClick()
val videoDoc = client.newCall(GET(url, xmlHeader)).await().asJsoup()
val src = videoDoc.selectFirst("iframe")?.attr("src")
?.replace("^//".toRegex(), "https://")
?: return@parallelMapBlocking
addAll(getVideosFromSource(src, hosterName, subber))
}
}
return videoList
}
private fun getVideosFromSource(src: String, hosterName: String, subber: String): List<Video> {
val cipherParamsEncoded = src
.substringAfter("/embed/#/url/")
.substringBefore("?status")
val cipherParams = json.decodeFromString<CipherParams>(
String(
Base64.decode(cipherParamsEncoded, Base64.DEFAULT),
),
)
val hosterLink = "https:" + decryptParams(cipherParams)
val videoList = runCatching {
when (hosterName) {
"ALUCARD(BETA)" -> {
AlucardExtractor(client, json, baseUrl).extractVideos(hosterLink, subber)
}
"DOODSTREAM" -> {
DoodExtractor(client).videosFromUrl(hosterLink, "$subber: DOODSTREAM", redirect = false)
}
"EMBEDGRAM" -> {
EmbedgramExtractor(client, headers).videosFromUrl(hosterLink, prefix = "$subber: ")
}
"FILEMOON" -> {
FilemoonExtractor(client).videosFromUrl(hosterLink, prefix = "$subber: ", headers = headers)
}
"GDRIVE" -> {
Regex("""[\w-]{28,}""").find(hosterLink)?.groupValues?.get(0)?.let {
GoogleDriveExtractor(client, headers).videosFromUrl("https://drive.google.com/uc?id=$it", "$subber: Gdrive")
}
}
"MAIL" -> {
MailRuExtractor(client, headers).videosFromUrl(hosterLink, prefix = "$subber: ")
}
"MP4UPLOAD" -> {
Mp4uploadExtractor(client).videosFromUrl(hosterLink, headers, prefix = "$subber: ")
}
"MVIDOO" -> {
MVidooExtractor(client).videosFromUrl(hosterLink, prefix = "$subber: ")
}
"ODNOKLASSNIKI" -> {
OkruExtractor(client).videosFromUrl(hosterLink, prefix = "$subber: ")
}
"SENDVID" -> {
SendvidExtractor(client, headers).videosFromUrl(hosterLink, prefix = "$subber: ")
}
"SIBNET" -> {
SibnetExtractor(client).videosFromUrl(hosterLink, prefix = "$subber: ")
}
"STREAMVID" -> {
StreamVidExtractor(client).videosFromUrl(hosterLink, headers, prefix = "$subber: ")
}
"UQLOAD" -> {
UqloadExtractor(client).videosFromUrl(hosterLink, "$subber:")
}
"VK" -> {
val vkUrl = "https://vk.com" + hosterLink.substringAfter("vk.com")
VkExtractor(client, headers).videosFromUrl(vkUrl, prefix = "$subber: ")
}
"VOE" -> {
VoeExtractor(client).videosFromUrl(hosterLink, "($subber) ")
}
"VTUBE" -> {
VTubeExtractor(client, headers).videosFromUrl(hosterLink, baseUrl, prefix = "$subber: ")
}
"VUDEA" -> {
VudeoExtractor(client).videosFromUrl(hosterLink, prefix = "$subber: ")
}
"WOLFSTREAM" -> {
WolfstreamExtractor(client).videosFromUrl(hosterLink, prefix = "$subber: ")
}
else -> null
}
}.getOrNull() ?: emptyList()
return videoList
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) }, // preferred quality first
{ it.quality.substringBefore(":") }, // then group by fansub
// then group by quality
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
@Serializable
private data class CipherParams(
val ct: String,
val s: String,
)
private fun String.trimOnClick() = baseUrl + "/" + this.substringAfter("IndexIcerik('").substringBefore("'")
private val xmlHeader = Headers.headersOf("X-Requested-With", "XMLHttpRequest")
private val refererHeader = Headers.headersOf("Referer", baseUrl)
private val mutex = Mutex()
private var shouldUpdateKey = false
private val key: String
get() {
return runBlocking(Dispatchers.IO) {
mutex.withLock {
if (shouldUpdateKey) {
updateKey()
shouldUpdateKey = false
}
preferences.getString(PREF_KEY_KEY, DEFAULT_KEY)!!
}
}
}
private fun decryptParams(params: CipherParams, tried: Boolean = false): String {
val decrypted = CryptoAES.decryptWithSalt(
params.ct,
params.s,
key,
).ifEmpty {
if (tried) {
""
} else {
shouldUpdateKey = true
decryptParams(params, true)
}
}
return json.decodeFromString<String>(decrypted)
}
private fun updateKey() {
val script4 = client.newCall(GET("$baseUrl/embed/#/")).execute().asJsoup()
.select("script[defer]").getOrNull(1)
?.attr("src") ?: return
val embeds4 = client.newCall(GET(baseUrl + script4)).execute().body.string()
val name = JS_NAME_REGEX.findAll(embeds4).toList().firstOrNull()?.value
val file5 = client.newCall(GET("$baseUrl/embed/js/embeds.$name.js")).execute().body.string()
val embeds5 = Deobfuscator.deobfuscateScript(file5) ?: return
val key = KEY_REGEX.find(embeds5)?.value ?: return
preferences.edit().putString(PREF_KEY_KEY, key).apply()
}
companion object {
private val JS_NAME_REGEX by lazy { "(?<=')[0-9a-f]{16}(?=')".toRegex() }
private val KEY_REGEX by lazy { "(?<=')\\S{100}(?=')".toRegex() }
private val SUPPORTED_HOSTERS = listOf(
// TODO: Fix Alucard
// "ALUCARD(BETA)",
"DOODSTREAM",
"EMBEDGRAM",
"FILEMOON",
"GDRIVE",
"MAIL",
"MP4UPLOAD",
"MVIDOO",
"ODNOKLASSNIKI",
"SENDVID",
"SIBNET",
"STREAMVID",
"UQLOAD",
"VK",
"VOE",
"VTUBE",
"VUDEA",
"WOLFSTREAM",
)
private val DEFAULT_SUBS by lazy {
setOf(
"Adonis",
"Aitr",
"Akatsuki",
"AkiraSubs",
"AniKeyf",
"ANS",
"AnimeMangaTR",
"AnimeOU",
"AniSekai",
"AniTürk",
"AoiSubs",
"ARE-YOU-SURE",
"AnimeWho",
"Benihime",
"Chevirman",
"Fatality",
"Hikigaya",
"HolySubs",
"Kirigana Fairies",
"Lawsonia Sub",
"LowSubs",
"Magnus357",
"Momo & Berhann",
"NoaSubs",
"OrigamiSubs",
"Pijamalı Koi",
"Puzzlesubs",
"RaionSubs",
"ShimazuSubs",
"SoutenSubs",
"TAÇE",
"TRanimeizle",
"TR Altyazılı",
"Uragiri",
"Varsayılan",
"YukiSubs",
)
}
private const val PREF_KEY_KEY = "key"
private const val DEFAULT_KEY = "710^8A@3@>T2}#zN5xK?kR7KNKb@-A!LzYL5~M1qU0UfdWsZoBm4UUat%}ueUv6E--*hDPPbH7K2bp9^3o41hw,khL:}Kx8080@M"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
private const val PREF_HOSTER_KEY = "hoster_selection"
private const val PREF_HOSTER_TITLE = "Enable/Disable Hosts"
private val PREF_HOSTER_DEFAULT = setOf("GDRIVE", "VOE")
// Copypasted from tr/tranimeizle.
private const val PREF_FANSUB_SELECTION_KEY = "pref_fansub_selection"
private const val PREF_FANSUB_SELECTION_TITLE = "Enable/Disable Fansubs"
private const val PREF_ADDITIONAL_FANSUBS_KEY = "pref_additional_fansubs_key"
private const val PREF_ADDITIONAL_FANSUBS_TITLE = "Add custom fansubs to the selection preference"
private const val PREF_ADDITIONAL_FANSUBS_DEFAULT = ""
private const val PREF_ADDITIONAL_FANSUBS_DIALOG_TITLE = "Enter a list of additional fansubs, separated by a comma."
private const val PREF_ADDITIONAL_FANSUBS_DIALOG_MESSAGE = "Example: AntichristHaters Fansub, 2cm erect subs"
private const val PREF_ADDITIONAL_FANSUBS_SUMMARY = "You can add more fansubs to the previous preference from here."
private const val PREF_ADDITIONAL_FANSUBS_TOAST = "Reopen the extension's preferences for it to take effect."
}
private val PREF_FANSUB_SELECTION_ENTRIES: Array<String> get() {
val additional = preferences.getString(PREF_ADDITIONAL_FANSUBS_KEY, "")!!
.split(",")
.map(String::trim)
.filter(String::isNotBlank)
.toSet()
return (DEFAULT_SUBS + additional).sorted().toTypedArray()
}
// =============================== Preferences ==============================
@Suppress("UNCHECKED_CAST")
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = PREF_HOSTER_TITLE
entries = SUPPORTED_HOSTERS.toTypedArray()
entryValues = SUPPORTED_HOSTERS.toTypedArray()
setDefaultValue(PREF_HOSTER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_FANSUB_SELECTION_KEY
title = PREF_FANSUB_SELECTION_TITLE
PREF_FANSUB_SELECTION_ENTRIES.let {
entries = it
entryValues = it
setDefaultValue(it.toSet())
}
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = PREF_ADDITIONAL_FANSUBS_KEY
title = PREF_ADDITIONAL_FANSUBS_TITLE
dialogTitle = PREF_ADDITIONAL_FANSUBS_DIALOG_TITLE
dialogMessage = PREF_ADDITIONAL_FANSUBS_DIALOG_MESSAGE
setDefaultValue(PREF_ADDITIONAL_FANSUBS_DEFAULT)
summary = PREF_ADDITIONAL_FANSUBS_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
runCatching {
val value = newValue as String
Toast.makeText(screen.context, PREF_ADDITIONAL_FANSUBS_TOAST, Toast.LENGTH_LONG).show()
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
}
}

View file

@ -0,0 +1,65 @@
package eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.OkHttpClient
class AlucardExtractor(private val client: OkHttpClient, private val json: Json, private val baseUrl: String) {
private val refererHeader = Headers.headersOf("referer", baseUrl)
fun extractVideos(hosterLink: String, subber: String): List<Video> {
return try {
val sourcesId = hosterLink.substringBeforeLast("/true").substringAfterLast("/")
val playerJs = client.newCall(GET("$baseUrl/js/player.js"))
.execute().body.string()
val csrf = "(?<=')[a-zA-Z]{64}(?=')".toRegex().find(playerJs)!!.value
val sourcesResponse = client.newCall(
GET(
"$baseUrl/sources/$sourcesId/true",
Headers.headersOf(
"Referer",
hosterLink,
"X-Requested-With",
"XMLHttpRequest",
"Cookie",
"__",
"csrf-token",
csrf,
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
),
),
)
.execute().body.string()
val sources = json.decodeFromString<JsonObject>(sourcesResponse)["response"]!!
.jsonObject["sources"]!!
.jsonArray.first()
.jsonObject["file"]!!
.jsonPrimitive.content
val masterPlaylist = client.newCall(GET(sources, refererHeader))
.execute().body.string()
val separator = "#EXT-X-STREAM-INF"
masterPlaylist.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore("\n") + "p"
val videoUrl = it.substringAfter("\n")
.substringBefore("\n")
// TODO: This gives 403 in MPV
Video(videoUrl, "$subber: Alucard: $quality", videoUrl, refererHeader)
}
} catch (e: Throwable) {
emptyList()
}
}
}

View file

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class EmbedgramExtractor(private val client: OkHttpClient, private val headers: Headers) {
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val response = client.newCall(GET(url)).execute()
val xsrfToken = response.headers.firstOrNull {
it.first == "set-cookie" && it.second.startsWith("XSRF-TOKEN", true)
}?.second?.substringBefore(";") ?: ""
val sourceElement = response.asJsoup().selectFirst("video#my-video > source[src~=.]") ?: return emptyList()
val videoUrl = sourceElement.attr("src").replace("^//".toRegex(), "https://")
val videoHeaders = headers.newBuilder()
.add("Cookie", xsrfToken)
.add("Host", videoUrl.toHttpUrl().host)
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
return listOf(
Video(videoUrl, "${prefix}Embedgram", videoUrl, headers = videoHeaders),
)
}
}

View file

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
class MVidooExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val body = client.newCall(GET(url)).execute().body.string()
val url = Regex("""\{var\s?.*?\s?=\s?(\[.*?\])""").find(body)?.groupValues?.get(1)?.let {
Json.decodeFromString<List<String>>(it.replace("\\x", ""))
.joinToString("") { t -> t.decodeHex() }.reversed()
.substringAfter("src=\"").substringBefore("\"")
} ?: return emptyList()
return listOf(
Video(url, "${prefix}MVidoo", url),
)
}
// Stolen from BestDubbedAnime
private fun String.decodeHex(): String {
require(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
.toString(Charsets.UTF_8)
}
}

View file

@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class MailRuExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val json = Json {
ignoreUnknownKeys = true
}
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
val metaUrl = document.selectFirst("script:containsData(metadataUrl)")?.let {
it.data().substringAfter("metadataUrl\":\"").substringBefore("\"").replace("^//".toRegex(), "https://")
} ?: return emptyList()
val metaHeaders = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Host", url.toHttpUrl().host)
.add("Referer", url)
.build()
val metaResponse = client.newCall(GET(metaUrl, headers = metaHeaders)).execute()
val metaJson = json.decodeFromString<MetaResponse>(
metaResponse.body.string(),
)
val videoKey = metaResponse.headers.firstOrNull {
it.first.equals("set-cookie", true) && it.second.startsWith("video_key", true)
}?.second?.substringBefore(";") ?: ""
return metaJson.videos.map {
val videoUrl = it.url
.replace("^//".toRegex(), "https://")
.replace(".mp4", ".mp4/stream.mpd")
val videoHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Cookie", videoKey)
.add("Host", videoUrl.toHttpUrl().host)
.add("Origin", "https://${url.toHttpUrl().host}")
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
Video(videoUrl, "${prefix}Mail.ru ${it.key}", videoUrl, headers = videoHeaders)
}
}
@Serializable
data class MetaResponse(
val videos: List<VideoObject>,
) {
@Serializable
data class VideoObject(
val url: String,
val key: String,
)
}
}

View file

@ -0,0 +1,46 @@
package eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class StreamVidExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, headers: Headers, prefix: String = ""): List<Video> {
val videoList = mutableListOf<Video>()
val packed = client.newCall(GET(url)).execute()
.asJsoup().selectFirst("script:containsData(m3u8)")?.data() ?: return emptyList()
val unpacked = JsUnpacker.unpackAndCombine(packed) ?: return emptyList()
val masterUrl = Regex("""src: ?"(.*?)"""").find(unpacked)?.groupValues?.get(1) ?: return emptyList()
val masterHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", masterUrl.toHttpUrl().host)
.add("Origin", "https://${url.toHttpUrl().host}")
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
val masterPlaylist = client.newCall(
GET(masterUrl, headers = masterHeaders),
).execute().body.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
.forEach {
val quality = "StreamVid:" + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p "
val videoUrl = it.substringAfter("\n").substringBefore("\n")
val videoHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", videoUrl.toHttpUrl().host)
.add("Origin", "https://${url.toHttpUrl().host}")
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
videoList.add(Video(videoUrl, prefix + quality, videoUrl, headers = videoHeaders))
}
return videoList
}
}

View file

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class VTubeExtractor(private val client: OkHttpClient, private val headers: Headers) {
fun videosFromUrl(url: String, baseUrl: String, prefix: String = ""): List<Video> {
val documentHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", url.toHttpUrl().host)
.add("Referer", "$baseUrl/")
.build()
val document = client.newCall(
GET(url, headers = documentHeaders),
).execute().asJsoup()
val masterUrl = document.selectFirst("script:containsData(sources)")?.let {
it.data().substringAfter("{file:\"").substringBefore("\"")
} ?: return emptyList()
val masterHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", masterUrl.toHttpUrl().host)
.add("Origin", "https://${url.toHttpUrl().host}")
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
val masterPlaylist = client.newCall(
GET(masterUrl, headers = masterHeaders),
).execute().body.string()
val videoList = mutableListOf<Video>()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
.forEach {
val quality = "VTube:" + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p "
val videoUrl = it.substringAfter("\n").substringBefore("\n")
val videoHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", videoUrl.toHttpUrl().host)
.add("Origin", "https://${url.toHttpUrl().host}")
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
videoList.add(Video(videoUrl, prefix + quality, videoUrl, headers = videoHeaders))
}
return videoList
}
}

View file

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.animeextension.tr.turkanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
class WolfstreamExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val url = client.newCall(
GET(url),
).execute().asJsoup().selectFirst("script:containsData(sources)")?.let {
it.data().substringAfter("{file:\"").substringBefore("\"")
} ?: return emptyList()
return listOf(
Video(url, "${prefix}WolfStream", url),
)
}
}