Initial commit
22
src/tr/animeler/AndroidManifest.xml
Normal 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>
|
20
src/tr/animeler/build.gradle
Normal 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"))
|
||||
}
|
BIN
src/tr/animeler/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
src/tr/animeler/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/tr/animeler/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/tr/animeler/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/tr/animeler/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 19 KiB |
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 & 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 & 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 & 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 & 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 & 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 & 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"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
22
src/tr/anizm/AndroidManifest.xml
Normal 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
|
@ -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"))
|
||||
}
|
BIN
src/tr/anizm/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/tr/anizm/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/tr/anizm/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
src/tr/anizm/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/tr/anizm/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 20 KiB |
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
18
src/tr/asyaanimeleri/build.gradle
Normal 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"))
|
||||
}
|
BIN
src/tr/asyaanimeleri/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/tr/asyaanimeleri/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/tr/asyaanimeleri/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/tr/asyaanimeleri/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
src/tr/asyaanimeleri/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
|
@ -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("Ağ"),
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
22
src/tr/hdfilmcehennemi/AndroidManifest.xml
Normal 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>
|
13
src/tr/hdfilmcehennemi/build.gradle
Normal 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"))
|
||||
}
|
BIN
src/tr/hdfilmcehennemi/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/tr/hdfilmcehennemi/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/tr/hdfilmcehennemi/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
src/tr/hdfilmcehennemi/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/tr/hdfilmcehennemi/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 17 KiB |
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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('"')
|
||||
}
|
|
@ -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" })
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
22
src/tr/hentaizm/AndroidManifest.xml
Normal 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>
|
8
src/tr/hentaizm/build.gradle
Normal file
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'HentaiZM'
|
||||
extClass = '.HentaiZM'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/tr/hentaizm/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/tr/hentaizm/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
src/tr/hentaizm/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/tr/hentaizm/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
src/tr/hentaizm/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
9
src/tr/tranimeci/build.gradle
Normal file
|
@ -0,0 +1,9 @@
|
|||
ext {
|
||||
extName = 'TRAnimeCI'
|
||||
extClass = '.TRAnimeCI'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://tranimaci.com'
|
||||
overrideVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/tr/tranimeci/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/tr/tranimeci/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
src/tr/tranimeci/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/tr/tranimeci/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/tr/tranimeci/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
22
src/tr/tranimeizle/AndroidManifest.xml
Normal 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>
|
20
src/tr/tranimeizle/build.gradle
Normal 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"))
|
||||
}
|
BIN
src/tr/tranimeizle/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/tr/tranimeizle/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/tr/tranimeizle/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/tr/tranimeizle/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
src/tr/tranimeizle/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
|
@ -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
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
24
src/tr/turkanime/build.gradle
Normal 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"))
|
||||
}
|
BIN
src/tr/turkanime/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/tr/turkanime/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/tr/turkanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/tr/turkanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
src/tr/turkanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/tr/turkanime/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 90 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|