Initial commit

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

View file

@ -0,0 +1,17 @@
ext {
extName = 'AnimeIndo'
extClass = '.AnimeIndo'
themePkg = 'animestream'
baseUrl = 'https://animeindo.skin'
overrideVersionCode = 10
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:mp4upload-extractor"))
implementation(project(":lib:gdriveplayer-extractor"))
implementation(project(":lib:streamtape-extractor"))
implementation(project(":lib:yourupload-extractor"))
implementation(project(":lib:okru-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,131 @@
package eu.kanade.tachiyomi.animeextension.id.animeindo
import android.util.Log
import eu.kanade.tachiyomi.animeextension.id.animeindo.AnimeIndoFilters.GenresFilter
import eu.kanade.tachiyomi.animeextension.id.animeindo.AnimeIndoFilters.OrderFilter
import eu.kanade.tachiyomi.animeextension.id.animeindo.AnimeIndoFilters.SeasonFilter
import eu.kanade.tachiyomi.animeextension.id.animeindo.AnimeIndoFilters.StatusFilter
import eu.kanade.tachiyomi.animeextension.id.animeindo.AnimeIndoFilters.StudioFilter
import eu.kanade.tachiyomi.animeextension.id.animeindo.AnimeIndoFilters.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.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters
import eu.kanade.tachiyomi.network.GET
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Element
class AnimeIndo : AnimeStream(
"id",
"AnimeIndo",
"https://animeindo.skin",
) {
override val animeListUrl = "$baseUrl/browse"
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$animeListUrl/browse?sort=view&page=$page")
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$animeListUrl/browse?sort=created_at&page=$page")
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimeIndoFilters.getSearchParameters(filters)
val multiString = buildString {
if (params.genres.isNotEmpty()) append(params.genres + "&")
if (params.seasons.isNotEmpty()) append(params.seasons + "&")
if (params.studios.isNotEmpty()) append(params.studios + "&")
}
return GET("$animeListUrl/browse?page=$page&title=$query&$multiString&status=${params.status}&type=${params.type}&order=${params.order}")
}
override fun searchAnimeSelector() = "div.animepost > div > a"
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("div.title")!!.text()
thumbnail_url = element.selectFirst("img")!!.getImageUrl()
}
override fun searchAnimeNextPageSelector() = "div.pagination a:has(i#nextpagination)"
// ============================== Filters ===============================
override val filtersSelector = "div.filtersearch tbody > tr:not(:has(td.filter_title:contains(Search))) > td.filter_act"
override fun getFilterList(): AnimeFilterList {
return if (AnimeStreamFilters.filterInitialized()) {
AnimeFilterList(
OrderFilter(orderFilterText),
StatusFilter(statusFilterText),
TypeFilter(typeFilterText),
AnimeFilter.Separator(),
GenresFilter(genresFilterText),
SeasonFilter(seasonsFilterText),
StudioFilter(studioFilterText),
)
} else {
AnimeFilterList(AnimeFilter.Header(filtersMissingWarning))
}
}
// =========================== Anime Details ============================
override fun parseStatus(statusString: String?): Int {
return when (statusString?.trim()?.lowercase()) {
"finished airing" -> SAnime.COMPLETED
"currently airing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "div.listeps li:has(.epsleft)"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
val ahref = element.selectFirst("a")!!
setUrlWithoutDomain(ahref.attr("href"))
val num = ahref.text()
name = "Episode $num"
episode_number = num.trim().toFloatOrNull() ?: 0F
date_upload = element.selectFirst("span.date")?.text().toDate()
}
// ============================ Video Links =============================
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val gdrivePlayerExtractor by lazy { GdrivePlayerExtractor(client) }
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
override fun getVideoList(url: String, name: String): List<Video> {
return with(name) {
when {
contains("streamtape") -> streamTapeExtractor.videoFromUrl(url)?.let(::listOf).orEmpty()
contains("mp4") -> mp4uploadExtractor.videosFromUrl(url, headers)
contains("yourupload") -> yourUploadExtractor.videoFromUrl(url, headers)
url.contains("ok.ru") -> okruExtractor.videosFromUrl(url)
contains("gdrive") -> {
val gdriveUrl = when {
baseUrl in url -> "https:" + url.toHttpUrl().queryParameter("data")!!
else -> url
}
gdrivePlayerExtractor.videosFromUrl(gdriveUrl, "Gdrive", headers)
}
else -> {
// just to detect video hosts easily
Log.i("AnimeIndo", "Unrecognized at getVideoList => Name -> $name || URL => $url")
emptyList()
}
}
}
}
}

View file

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.animeextension.id.animeindo
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.filterElements
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.filterInitialized
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.parseCheckbox
object AnimeIndoFilters {
internal class GenresFilter(name: String) : CheckBoxFilterList(name, GENRES_LIST)
internal class SeasonFilter(name: String) : CheckBoxFilterList(name, SEASON_LIST)
internal class StudioFilter(name: String) : CheckBoxFilterList(name, STUDIO_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 seasons: String = "",
val studios: String = "",
val status: String = "",
val type: String = "",
val order: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
if (!filterInitialized()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenresFilter>(GENRES_LIST, "genre"),
filters.parseCheckbox<SeasonFilter>(SEASON_LIST, "season"),
filters.parseCheckbox<StudioFilter>(STUDIO_LIST, "studio"),
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<OrderFilter>(),
)
}
private fun getPairListByIndex(index: Int) = filterElements.get(index)
.select("ul > li, td > label")
.map { element ->
val key = element.text()
val value = element.selectFirst("input")!!.attr("value")
Pair(key, value)
}.toTypedArray()
private val ORDER_LIST by lazy {
getPairListByIndex(0)
.filterNot { it.first.contains("Most favorite", true) }
.toTypedArray()
}
private val STATUS_LIST by lazy { getPairListByIndex(1) }
private val TYPE_LIST by lazy { getPairListByIndex(2) }
private val GENRES_LIST by lazy { getPairListByIndex(3) }
private val SEASON_LIST by lazy { getPairListByIndex(4) }
private val STUDIO_LIST by lazy { getPairListByIndex(5) }
}

View file

@ -0,0 +1,11 @@
ext {
extName = 'Kuramanime'
extClass = '.Kuramanime'
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:streamtape-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -0,0 +1,296 @@
package eu.kanade.tachiyomi.animeextension.id.kuramanime
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
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.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
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 Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Kuramanime"
override val baseUrl = "https://kuramanime.pro"
override val lang = "id"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime?page=$page")
override fun popularAnimeSelector() = "div.product__item"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
thumbnail_url = element.selectFirst("a > div")?.attr("data-setbg")
title = element.selectFirst("div.product__item__text > h5")!!.text()
}
override fun popularAnimeNextPageSelector() = "div.product__pagination > a:last-child"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/anime?order_by=updated&page=$page")
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) = GET("$baseUrl/anime?search=$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 {
thumbnail_url = document.selectFirst("div.anime__details__pic")?.attr("data-setbg")
val details = document.selectFirst("div.anime__details__text")!!
title = details.selectFirst("div > h3")!!.text().replace("Judul: ", "")
val infos = details.selectFirst("div.anime__details__widget")!!
artist = infos.select("li:contains(Studio:) > a").eachText().joinToString().takeUnless(String::isEmpty)
status = parseStatus(infos.selectFirst("li:contains(Status:) > a")?.text())
genre = infos.select("li:contains(Genre:) > a, li:contains(Tema:) > a, li:contains(Demografis:) > a")
.eachText()
.joinToString { it.trimEnd(',', ' ') }
.takeUnless(String::isEmpty)
description = buildString {
details.selectFirst("p#synopsisField")?.text()?.also(::append)
details.selectFirst("div.anime__details__title > span")?.text()
?.also { append("\n\nAlternative names: $it\n") }
infos.select("ul > li").eachText().forEach { append("\n$it") }
}
}
private fun parseStatus(statusString: String?): Int {
return when (statusString) {
"Sedang Tayang" -> SAnime.ONGOING
"Selesai Tayang" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val html = document.selectFirst(episodeListSelector())?.attr("data-content")
?: return emptyList()
val newDoc = response.asJsoup(html)
val limits = newDoc.select("a.btn-secondary")
return when {
limits.isEmpty() -> { // 12 episodes or less
newDoc.select("a")
.filterNot { it.attr("href").contains("batch") }
.map(::episodeFromElement)
.reversed()
}
else -> { // More than 12 episodes
val (start, end) = limits.eachText().take(2).map {
it.filter(Char::isDigit).toInt()
}
val location = document.location()
(end downTo start).map { episodeNumber ->
SEpisode.create().apply {
name = "Ep $episodeNumber"
episode_number = episodeNumber.toFloat()
setUrlWithoutDomain("$location/episode/$episodeNumber")
}
}
}
}
}
override fun episodeListSelector() = "a#episodeLists"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.text()
episode_number = name.filter(Char::isDigit).toFloatOrNull() ?: 1F
}
// ============================ Video Links =============================
override fun videoListSelector() = "video#player > source"
// Shall we add "archive", "archive-v2"? archive.org usually returns a beautiful 403 xD
private val supportedHosters = listOf("kuramadrive", "kuramadrive-v2", "streamtape")
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val scriptData = doc.selectFirst("[data-js]")?.attr("data-js")
?.let(::getScriptData)
?: return emptyList()
val csrfToken = doc.selectFirst("meta[name=csrf-token]")
?.attr("csrf-token")
?: return emptyList()
val servers = doc.select("select#changeServer > option")
.map { it.attr("value") to it.text().substringBefore(" (") }
.filter { supportedHosters.contains(it.first) }
val episodeUrl = response.request.url
val headers = headersBuilder()
.set("Referer", episodeUrl.toString())
.set("X-Requested-With", "XMLHttpRequest")
.build()
return servers.flatMap { (server, serverName) ->
runCatching {
val newHeaders = headers.newBuilder()
.set("X-CSRF-TOKEN", csrfToken)
.set("X-Fuck-ID", scriptData.tokenId)
.set("X-Request-ID", getRandomString())
.set("X-Request-Index", "0")
.build()
val hash = client.newCall(GET("$baseUrl/" + scriptData.authPath, newHeaders)).execute()
.body.string()
.trim('"')
val newUrl = episodeUrl.newBuilder()
.addQueryParameter(scriptData.tokenParam, hash)
.addQueryParameter(scriptData.serverParam, server)
.build()
val playerDoc = client.newCall(GET(newUrl.toString(), headers)).execute()
.asJsoup()
if (server == "streamtape") {
val url = playerDoc.selectFirst("div.video-content iframe")!!.attr("src")
streamtapeExtractor.videosFromUrl(url)
} else {
playerDoc.select("video#player > source").map {
val src = it.attr("src")
Video(src, "${it.attr("size")}p - $serverName", src)
}
}
}.getOrElse { emptyList<Video>() }
}
}
private fun getScriptData(scriptName: String): ScriptDataDto? {
val scriptUrl = "$baseUrl/assets/js/$scriptName.js"
val scriptCode = client.newCall(GET(scriptUrl, headers)).execute()
.body.string()
// Trust me, I hate this too.
val scriptJson = scriptCode.lines()
.filter { it.contains(": '") || it.contains(": \"") }
.map {
val (key, value) = it.split(":", limit = 2).map(String::trim)
val fixedValue = value.replace("'", "\"").substringBeforeLast(',')
"\"$key\": $fixedValue"
}.joinToString(prefix = "{", postfix = "}")
return runCatching {
json.decodeFromString<ScriptDataDto>(scriptJson)
}.onFailure { it.printStackTrace() }.getOrNull()
}
@Serializable
internal data class ScriptDataDto(
@SerialName("MIX_PREFIX_AUTH_ROUTE_PARAM")
private val authPathPrefix: String,
@SerialName("MIX_AUTH_ROUTE_PARAM")
private val authPathSuffix: String,
@SerialName("MIX_AUTH_KEY") private val authKey: String,
@SerialName("MIX_AUTH_TOKEN") private val authToken: String,
@SerialName("MIX_PAGE_TOKEN_KEY") val tokenParam: String,
@SerialName("MIX_STREAM_SERVER_KEY") val serverParam: String,
) {
val authPath = authPathPrefix + authPathSuffix
val tokenId = "$authKey:$authToken"
}
private fun getRandomString(length: Int = 8): String {
val allowedChars = ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
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()
}
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = 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 {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
}
}

View file

@ -0,0 +1,14 @@
ext {
extName = 'Kuronime'
extClass = '.Kuronime'
extVersionCode = 9
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:streamlare-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:yourupload-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View file

@ -0,0 +1,221 @@
package eu.kanade.tachiyomi.animeextension.id.kuronime
import android.app.Application
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.id.kuronime.extractors.AnimekuExtractor
import eu.kanade.tachiyomi.animeextension.id.kuronime.extractors.HxFileExtractor
import eu.kanade.tachiyomi.animeextension.id.kuronime.extractors.LinkBoxExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
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.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
class Kuronime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val baseUrl: String = "https://tv1.kuronime.vip"
override val lang: String = "id"
override val name: String = "Kuronime"
override val supportsLatest: Boolean = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val infodetail = document.select("div.infodetail")
val status = parseStatus(infodetail.select("ul > li:nth-child(3)").text().replace("Status: ", ""))
anime.title = infodetail.select("ul > li:nth-child(1)").text().replace("Judul: ", "")
anime.genre = infodetail.select("ul > li:nth-child(2)").joinToString(", ") { it.text() }
anime.status = status
anime.artist = infodetail.select("ul > li:nth-child(4)").text().replace("Studio: ", "")
anime.author = "UNKNOWN"
anime.description = "Synopsis: \n" + document.select("div.main-info > div.con > div.r > div > span > p").text()
return anime
}
private fun parseStatus(statusString: String): Int {
return when (statusString.toLowerCase(Locale.US)) {
"ongoing" -> SAnime.ONGOING
"completed" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val epsNum = getNumberFromEpsString(element.select("span.lchx").text())
episode.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
episode.episode_number = when {
epsNum.isNotEmpty() -> epsNum.toFloatOrNull() ?: 1F
else -> 1F
}
episode.name = element.select("span.lchx").text()
return episode
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
override fun episodeListSelector(): String = "div.bixbox.bxcl > ul > li"
override fun latestUpdatesFromElement(element: Element): SAnime = getAnimeFromAnimeElement(element)
private fun getAnimeFromAnimeElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("div > a")!!.attr("href"))
val thumbnailElement = element.selectFirst("div > a > div.limit > img")!!
val thumbnail = thumbnailElement.attr("src")
anime.thumbnail_url = if (thumbnail.startsWith("https:")) {
thumbnail
} else {
if (thumbnailElement.hasAttr("data-src")) thumbnailElement.attr("data-src") else ""
}
anime.title = element.select("div > a > div.tt > h4").text()
return anime
}
override fun latestUpdatesNextPageSelector(): String = "div.pagination > a.next"
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/anime/?page=$page&status=ongoing&sub=&order=update")
override fun latestUpdatesSelector(): String = "div.listupd > article"
override fun popularAnimeFromElement(element: Element): SAnime = getAnimeFromAnimeElement(element)
override fun popularAnimeNextPageSelector(): String = "div.pagination > a.next"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/anime/page/$page")
override fun popularAnimeSelector(): String = "div.listupd > article"
override fun searchAnimeFromElement(element: Element): SAnime = getAnimeFromAnimeElement(element)
override fun searchAnimeNextPageSelector(): String = "a.next.page-numbers"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
// filter and stuff in v2
return GET("$baseUrl/page/$page/?s=$query")
}
override fun searchAnimeSelector(): String = "div.listupd > article"
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
val hosterSelection = preferences.getStringSet(
"hoster_selection",
setOf("animeku", "mp4upload", "yourupload", "streamlare", "linkbox"),
)!!
document.select("select.mirror > option[value]").forEach { opt ->
val decoded = if (opt.attr("value").isEmpty()) {
document.selectFirst("iframe")!!.attr("data-src")
} else {
Jsoup.parse(
String(Base64.decode(opt.attr("value"), Base64.DEFAULT)),
).select("iframe[data-src~=.]").attr("data-src")
}
when {
hosterSelection.contains("animeku") && decoded.contains("animeku.org") -> {
videoList.addAll(AnimekuExtractor(client).getVideosFromUrl(decoded, opt.text()))
}
hosterSelection.contains("mp4upload") && decoded.contains("mp4upload.com") -> {
val videos = Mp4uploadExtractor(client).videosFromUrl(decoded, headers, suffix = " - ${opt.text()}")
videoList.addAll(videos)
}
hosterSelection.contains("yourupload") && decoded.contains("yourupload.com") -> {
videoList.addAll(YourUploadExtractor(client).videoFromUrl(decoded, headers, opt.text(), "Original - "))
}
hosterSelection.contains("streamlare") && decoded.contains("streamlare.com") -> {
videoList.addAll(StreamlareExtractor(client).videosFromUrl(decoded, suffix = "- " + opt.text()))
}
hosterSelection.contains("hxfile") && decoded.contains("hxfile.co") -> {
videoList.addAll(HxFileExtractor(client).getVideoFromUrl(decoded, opt.text()))
}
hosterSelection.contains("linkbox") && decoded.contains("linkbox.to") -> {
videoList.addAll(LinkBoxExtractor(client).videosFromUrl(decoded, opt.text()))
}
}
}
return videoList.sort()
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val hostSelection = MultiSelectListPreference(screen.context).apply {
key = "hoster_selection"
title = "Enable/Disable Hosts"
entries = arrayOf("Animeku", "Mp4Upload", "YourUpload", "Streamlare", "Hxfile", "Linkbox")
entryValues = arrayOf("animeku", "mp4upload", "yourupload", "streamlare", "hxfile", "linkbox")
setDefaultValue(setOf("animeku", "mp4upload", "yourupload", "streamlare", "linkbox"))
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "HD", "SD")
entryValues = arrayOf("1080", "720", "480", "360", "HD", "SD")
setDefaultValue("1080")
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()
}
}
screen.addPreference(hostSelection)
screen.addPreference(videoQualityPref)
}
}

View file

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.animeextension.id.kuronime.extractors
import app.cash.quickjs.QuickJs
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.OkHttpClient
import uy.kohesive.injekt.injectLazy
@Serializable
data class Source(
val file: String,
val label: String,
)
class AnimekuExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun getVideosFromUrl(url: String, name: String): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
val script = document.selectFirst("script:containsData(decodeURIComponent)") ?: return emptyList()
val quickJs = QuickJs.create()
val decryped = quickJs.evaluate(
script.data().trim().split("\n")[0].replace("eval(function", "function a").replace("decodeURIComponent(escape(r))}(", "r};a(").substringBeforeLast(")"),
).toString()
quickJs.close()
val srcs = json.decodeFromString<List<Source>>(decryped.substringAfter("var srcs = ").substringBefore(";"))
return srcs.map { src ->
Video(
src.file,
"${src.label} - $name",
src.file,
)
}
}
}

View file

@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.animeextension.id.kuronime.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.OkHttpClient
class HxFileExtractor(private val client: OkHttpClient) {
fun getVideoFromUrl(url: String, name: String): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
val packed = document.selectFirst("script:containsData(eval\\(function\\()")!!.data()
val unpacked = JsUnpacker.unpackAndCombine(packed) ?: return emptyList()
val videoUrl = unpacked.substringAfter("\"type\":\"video").substringAfter("\"file\":\"").substringBefore("\"")
return listOf(
Video(videoUrl, "Original - $name", videoUrl, headers = Headers.headersOf("Referer", "https://hxfile.co/")),
)
}
}

View file

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.animeextension.id.kuronime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
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 okhttp3.OkHttpClient
class LinkBoxExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, name: String): List<Video> {
val videoList = mutableListOf<Video>()
val id = if (url.contains("/file/")) {
url.substringAfter("/file/")
} else {
url.substringAfter("?id=")
}
val request = client.newCall(GET("https://www.linkbox.to/api/open/get_url?itemId=$id")).execute().asJsoup()
val responseJson = Json.decodeFromString<JsonObject>(request.select("body").text())
val data = responseJson["data"]?.jsonObject
val resolutions = data!!.jsonObject["rList"]!!.jsonArray
resolutions.map {
videoList.add(
Video(
it.jsonObject["url"].toString().replace("\"", ""),
"${it.jsonObject["resolution"].toString().replace("\"", "")} - $name",
it.jsonObject["url"].toString().replace("\"", ""),
),
)
}
return videoList
}
}

View file

@ -0,0 +1,15 @@
ext {
extName = 'MiniOppai'
extClass = '.MiniOppai'
themePkg = 'animestream'
baseUrl = 'https://minioppai.org'
overrideVersionCode = 5
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:gdriveplayer-extractor"))
implementation(project(":lib:unpacker"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View file

@ -0,0 +1,125 @@
package eu.kanade.tachiyomi.animeextension.id.minioppai
import eu.kanade.tachiyomi.animeextension.id.minioppai.extractors.MiniOppaiExtractor
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.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
import eu.kanade.tachiyomi.network.GET
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class MiniOppai : AnimeStream(
"id",
"MiniOppai",
"https://minioppai.org",
) {
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
override val animeListUrl = "$baseUrl/advanced-search"
override val dateFormatter by lazy {
SimpleDateFormat("MMMM d, yyyy", Locale(lang))
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$animeListUrl/page/$page/?order=popular")
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$animeListUrl/page/$page/?order=update")
// ============================== Episodes ==============================
override fun episodeListSelector() = "div.epsdlist > ul > li > a"
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst(".epl-num")!!.text().let {
val num = it.substringAfterLast(" ")
episode_number = num.toFloatOrNull() ?: 0F
name = when {
it.contains("OVA", true) -> "OVA $num"
else -> "Episode $num"
}
}
element.selectFirst(".epl-sub")?.text()?.let { scanlator = it }
date_upload = element.selectFirst(".epl-date")?.text().toDate()
}
}
// ============================ Video Links =============================
override fun getVideoList(url: String, name: String): List<Video> {
return when {
"gdriveplayer" in url -> {
val playerUrl = buildString {
val data = url.toHttpUrl().queryParameter("data")
?: return emptyList()
if (data.startsWith("//")) append("https:")
append(data)
}
GdrivePlayerExtractor(client).videosFromUrl(playerUrl, name, headers)
}
"paistream.my.id" in url ->
MiniOppaiExtractor(client).videosFromUrl(url, headers)
else -> emptyList()
}
}
// =========================== Anime Details ============================
override fun getAnimeDescription(document: Document) =
document.select("div.entry-content > p").eachText().joinToString("\n")
override fun animeDetailsParse(document: Document) = super.animeDetailsParse(document).apply {
title = title.substringBefore("Episode").substringBefore(" OVA ")
}
// =============================== Search ===============================
override fun searchAnimeSelector() = "div.latest article a.tip"
override fun searchAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("h2.entry-title")!!.ownText()
thumbnail_url = element.selectFirst("img")!!.getImageUrl()
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = MiniOppaiFilters.getSearchParameters(filters)
return if (query.isNotEmpty()) {
GET("$baseUrl/page/$page/?s=$query")
} else {
val multiString = listOf(
params.genres,
params.countries,
params.qualities,
params.year,
params.status,
).filter(String::isNotBlank).joinToString("&").let {
when {
it.isBlank() -> it
else -> "&$it"
}
}
GET("$animeListUrl/page/$page/?order=${params.order}$multiString")
}
}
// ============================== Filters ===============================
override val fetchFilters = false
override fun getFilterList() = MiniOppaiFilters.FILTER_LIST
// ============================= Utilities ==============================
override fun Element.getInfo(text: String): String? {
return selectFirst("li:has(b:contains($text))")
?.selectFirst("span.colspan")
?.text()
}
}

View file

@ -0,0 +1,202 @@
package eu.kanade.tachiyomi.animeextension.id.minioppai
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.CheckBoxFilterList
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.QueryPartFilter
object MiniOppaiFilters {
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.filter(String::isNotBlank)
.joinToString("&") { "$name[]=$it" }
}
class OrderFilter : QueryPartFilter("Order", MiniOppaiFiltersData.ORDER_LIST)
class GenresFilter : CheckBoxFilterList("Genres", MiniOppaiFiltersData.GENRES_LIST)
class CountriesFilter : CheckBoxFilterList("Countries", MiniOppaiFiltersData.COUNTRIES_LIST)
class QualitiesFilter : CheckBoxFilterList("Qualities", MiniOppaiFiltersData.QUALITIES_LIST)
class YearsFilter : CheckBoxFilterList("Years", MiniOppaiFiltersData.YEARS_LIST)
class StatusFilter : CheckBoxFilterList("Status", MiniOppaiFiltersData.STATUS_LIST)
val FILTER_LIST get() = AnimeFilterList(
OrderFilter(),
GenresFilter(),
CountriesFilter(),
QualitiesFilter(),
YearsFilter(),
StatusFilter(),
)
data class FilterSearchParams(
val order: String = "",
val genres: String = "",
val countries: String = "",
val qualities: String = "",
val year: String = "",
val status: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<OrderFilter>(),
filters.parseCheckbox<GenresFilter>(MiniOppaiFiltersData.GENRES_LIST, "genre"),
filters.parseCheckbox<CountriesFilter>(MiniOppaiFiltersData.COUNTRIES_LIST, "country"),
filters.parseCheckbox<QualitiesFilter>(MiniOppaiFiltersData.QUALITIES_LIST, "quality"),
filters.parseCheckbox<YearsFilter>(MiniOppaiFiltersData.YEARS_LIST, "years"),
filters.parseCheckbox<StatusFilter>(MiniOppaiFiltersData.STATUS_LIST, "status"),
)
}
private object MiniOppaiFiltersData {
val ORDER_LIST = arrayOf(
Pair("Release date", "added"),
Pair("IMDb", "imdb"),
Pair("Latest", "latest"),
Pair("Most viewed", "popular"),
Pair("Name", "title"),
)
val GENRES_LIST = arrayOf(
Pair("3D Hentai", "3d-hentai"),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Ahegao", "ahegao"),
Pair("Anal", "anal"),
Pair("Armpit", "armpit"),
Pair("Ashikoki", "ashikoki"),
Pair("BDSM", "bdsm"),
Pair("Big Oppai", "big-oppai"),
Pair("Blowjob", "blowjob"),
Pair("Bondage", "bondage"),
Pair("Censored", "censored"),
Pair("Cheating", "cheating"),
Pair("Chikan", "chikan"),
Pair("Comedy", "comedy"),
Pair("Cosplay", "cosplay"),
Pair("Creampie", "creampie"),
Pair("Crime", "crime"),
Pair("Dark Skin", "dark-skin"),
Pair("Demons", "demons"),
Pair("Doctor", "doctor"),
Pair("Domination", "domination"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Elf", "elf"),
Pair("Eroge", "eroge"),
Pair("Exhibitionist", "exhibitionist"),
Pair("Facial", "facial"),
Pair("Fantasy", "fantasy"),
Pair("Fellatio", "fellatio"),
Pair("Female Monster", "female-monster"),
Pair("Femdom", "femdom"),
Pair("Fetish", "fetish"),
Pair("Footjob", "footjob"),
Pair("Forced", "forced"),
Pair("Futanari", "futanari"),
Pair("Gal", "gal"),
Pair("Gangbang", "gangbang"),
Pair("Group", "group"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Housewife", "housewife"),
Pair("Humiliation", "humiliation"),
Pair("Idol", "idol"),
Pair("Incest", "incest"),
Pair("Intercrural", "intercrural"),
Pair("Josei", "josei"),
Pair("Kemonomimi", "kemonomimi"),
Pair("Kimono", "kimono"),
Pair("Lactation", "lactation"),
Pair("Lingerie", "lingerie"),
Pair("MILF", "milf"),
Pair("Magic", "magic"),
Pair("Maid", "maid"),
Pair("Male Monster", "male-monster"),
Pair("Martial Arts", "martial-arts"),
Pair("Masturbation", "masturbation"),
Pair("Mecha", "mecha"),
Pair("Megane", "megane"),
Pair("Mind Control", "mind-control"),
Pair("Mizugi", "mizugi"),
Pair("Monster", "monster"),
Pair("Mystery", "mystery"),
Pair("Neko", "neko"),
Pair("Netorare", "netorare"),
Pair("Nurse", "nurse"),
Pair("Office", "office"),
Pair("Onee-san", "onee-san"),
Pair("Onsen", "onsen"),
Pair("Oppai", "oppai"),
Pair("Oral", "oral"),
Pair("Osananajimi", "osananajimi"),
Pair("Outdoor", "outdoor"),
Pair("Paizuri", "paizuri"),
Pair("Pantyhose", "pantyhose"),
Pair("Pantyhouse", "pantyhouse"),
Pair("Parody", "parody"),
Pair("Pregnant", "pregnant"),
Pair("Prostitution", "prostitution"),
Pair("Rape", "rape"),
Pair("Romance", "romance"),
Pair("School", "school"),
Pair("Schoolgirl", "schoolgirl"),
Pair("Seinen", "seinen"),
Pair("Sex Toys", "sex-toys"),
Pair("Shibari", "shibari"),
Pair("Shota", "shota"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Stocking", "stocking"),
Pair("Succubus", "succubus"),
Pair("Supernatural", "supernatural"),
Pair("Swimsuit", "swimsuit"),
Pair("Teacher", "teacher"),
Pair("Tennis", "tennis"),
Pair("Tentacles", "tentacles"),
Pair("Threesome", "threesome"),
Pair("Tsundere", "tsundere"),
Pair("Uncensored", "uncensored"),
Pair("stock", "stock"),
)
val COUNTRIES_LIST = arrayOf(
Pair("JP", "jp"),
Pair("TH", "th"),
)
val QUALITIES_LIST = arrayOf(
Pair("BD", "bd"),
Pair("Censored", "censored"),
Pair("HD", "hd"),
Pair("TV", "tv"),
Pair("Uncensored", "uncensored"),
Pair("Web", "web"),
)
val YEARS_LIST = (2024 downTo 2001).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val STATUS_LIST = arrayOf(
Pair("Completed", "completed"),
Pair("Dropped", "dropped"),
Pair("Ongoing", "ongoing"),
)
}
}

View file

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.animeextension.id.minioppai.extractors
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.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class MiniOppaiExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, headers: Headers): List<Video> {
val playerDoc = client.newCall(GET(url, headers)).execute().asJsoup()
val scriptData = playerDoc.selectFirst("script:containsData(eval):containsData(p,a,c,k,e,d)")
?.data()
?.let(Unpacker::unpack)
?.takeIf(String::isNotBlank)
?: return emptyList()
val baseUrl = "https://" + url.substringAfter("//").substringBefore("/")
val subs = scriptData.getItems("\"tracks\"", baseUrl) { subUrl, label ->
Track(subUrl, label)
}
return scriptData.getItems("sources", baseUrl) { videoUrl, quality ->
val videoQuality = "MiniOppai - $quality"
Video(videoUrl, videoQuality, videoUrl, headers, subtitleTracks = subs)
}.filterNot { it.url.contains("/uploads/unavailable.mp4") }
}
// time to over-engineer things for no reason at all
private fun <T> String.getItems(key: String, baseUrl: String, transformer: (String, String) -> T): List<T> {
return substringAfter("$key:[").substringBefore("]")
.split("{")
.drop(1)
.map {
val url = "$baseUrl${it.extractKey("file")}"
val label = it.extractKey("label")
transformer(url, label)
}
}
private fun String.extractKey(key: String) =
substringAfter(key)
.substringBefore("}")
.substringBefore(",")
.substringAfter(":")
.trim('"')
.trim('\'')
}

View file

@ -0,0 +1,15 @@
ext {
extName = 'NeoNime'
extClass = '.NeoNime'
extVersionCode = 14
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:blogger-extractor'))
implementation(project(':lib:gdriveplayer-extractor'))
implementation(project(':lib:yourupload-extractor'))
implementation(project(':lib:okru-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

@ -0,0 +1,336 @@
package eu.kanade.tachiyomi.animeextension.id.neonime
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.id.neonime.extractors.LinkBoxExtractor
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.bloggerextractor.BloggerExtractor
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
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 okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
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 java.text.SimpleDateFormat
import java.util.Locale
class NeoNime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val baseUrl: String = "https://neonime.ink"
override val lang: String = "id"
override val name: String = "NeoNime"
override val supportsLatest: Boolean = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// Private Fun
private fun reconstructDate(Str: String): Long {
val pattern = SimpleDateFormat("dd-MM-yyyy", Locale.US)
return runCatching { pattern.parse(Str)?.time }
.getOrNull() ?: 0L
}
private fun parseStatus(statusString: String): Int {
return when {
statusString.toLowerCase(Locale.US).contains("ongoing") -> SAnime.ONGOING
statusString.toLowerCase(Locale.US).contains("completed") -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
private fun getAnimeFromAnimeElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.thumbnail_url = element.selectFirst("a > div.image > img")!!.attr("data-src")
anime.title = element.select("div.fixyear > div > h2").text()
return anime
}
private fun getAnimeFromEpisodeElement(element: Element): SAnime {
val animepage = client.newCall(GET(element.selectFirst("td.bb > a")!!.attr("href"))).execute().asJsoup()
val anime = SAnime.create()
anime.setUrlWithoutDomain(animepage.selectFirst("#fixar > div.imagen > a")!!.attr("href"))
anime.thumbnail_url = animepage.selectFirst("#fixar > div.imagen > a > img")!!.attr("data-src")
anime.title = animepage.selectFirst("#fixar > div.imagen > a > img")!!.attr("alt")
return anime
}
private fun getAnimeFromSearchElement(element: Element): SAnime {
val url = element.selectFirst("a")!!.attr("href")
val animepage = client.newCall(GET(url)).execute().asJsoup()
val anime = SAnime.create()
anime.setUrlWithoutDomain(url)
anime.title = animepage.select("#info > div:nth-child(2) > span").text()
anime.thumbnail_url = animepage.selectFirst("div.imagen > img")!!.attr("data-src")
anime.status = parseStatus(animepage.select("#info > div:nth-child(13) > span").text())
anime.genre = animepage.select("#info > div:nth-child(3) > span > a").joinToString(", ") { it.text() }
// this site didnt provide artist and author
anime.artist = "Unknown"
anime.author = "Unknown"
anime.description = animepage.select("#info > div.contenidotv").text()
return anime
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
// Popular
override fun popularAnimeFromElement(element: Element): SAnime = getAnimeFromAnimeElement(element)
override fun popularAnimeNextPageSelector(): String = "#contenedor > div > div.box > div.box_item > div.peliculas > div.item_1.items > div.respo_pag > div.pag_b > a"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/tvshows/page/$page")
override fun popularAnimeSelector(): String = "div.items > div.item"
// Latest
override fun latestUpdatesNextPageSelector(): String = "#contenedor > div > div.box > div.box_item > div.peliculas > div.item_1.items > div.respo_pag > div.pag_b > a"
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/episode/page/$page")
override fun latestUpdatesSelector(): String = "table > tbody > tr"
override fun latestUpdatesFromElement(element: Element): SAnime = getAnimeFromEpisodeElement(element)
// Search
override fun searchAnimeFromElement(element: Element): SAnime = getAnimeFromSearchElement(element)
override fun searchAnimeNextPageSelector(): String = "#contenedor > div > div.box > div.box_item > div.peliculas > div.item_1.items > div.respo_pag > div.pag_b > a"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
// filter is not avilable in neonime site
return GET("$baseUrl/list-anime/")
}
override fun searchAnimeSelector() = throw UnsupportedOperationException()
private fun generateSelector(query: String): String = "div.letter-section > ul > li > a:contains($query)"
override fun searchAnimeParse(response: Response) = throw UnsupportedOperationException()
private fun searchQueryParse(response: Response, query: String): AnimesPage {
val document = response.asJsoup()
val animes = filterNoBatch(document.select(generateSelector(query))).map { element ->
searchAnimeFromElement(element)
}
return AnimesPage(animes, false)
}
private fun filterNoBatch(eles: Elements): Elements {
val retElements = Elements()
for (ele in eles) {
if (ele.attr("href").contains("/tvshows")) {
retElements.add(ele)
}
}
return retElements
}
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
val returnedSearch = searchAnimeRequest(page, query, filters)
return client.newCall(returnedSearch)
.awaitSuccess()
.let { response ->
searchQueryParse(response, query)
}
}
// Anime Details
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("#info > div:nth-child(2) > span").text()
anime.thumbnail_url = document.selectFirst("div.imagen > img")!!.attr("data-src")
anime.status = parseStatus(document.select("#info > div:nth-child(13) > span").text())
anime.genre = document.select("#info > div:nth-child(3) > span > a").joinToString(", ") { it.text() }
// this site didnt provide artist and author
anime.artist = "Unknown"
anime.author = "Unknown"
anime.description = document.select("#info > div.contenidotv").text()
return anime
}
// Episode List
override fun episodeListSelector(): String = "ul.episodios > li"
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val epsNum = getNumberFromEpsString(element.select("div.episodiotitle > a").text())
episode.setUrlWithoutDomain(element.select("div.episodiotitle > a").attr("href"))
episode.episode_number = when {
epsNum.isNotEmpty() -> epsNum.toFloat()
else -> 1F
}
episode.name = element.select("div.episodiotitle > a").text()
episode.date_upload = reconstructDate(element.select("div.episodiotitle > span.date").text())
return episode
}
// Video
override fun videoListSelector() = "div > ul >ul > li >a:nth-child(6)"
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
val hosterSelection = preferences.getStringSet(
"hoster_selection",
setOf("blogger", "linkbox", "okru", "yourupload", "gdriveplayer"),
)!!
document.select("div.player2 > div.embed2 > div").forEach {
val iframe = it.selectFirst("iframe") ?: return@forEach
var link = iframe.attr("data-src")
if (!link.startsWith("http")) {
link = "https:$link"
}
when {
hosterSelection.contains("linkbox") && link.contains("linkbox.to") -> {
videoList.addAll(LinkBoxExtractor(client).videosFromUrl(link, it.text()))
}
hosterSelection.contains("okru") && link.contains("ok.ru") -> {
videoList.addAll(OkruExtractor(client).videosFromUrl(link))
}
hosterSelection.contains("yourupload") && link.contains("blogger.com") -> {
videoList.addAll(BloggerExtractor(client).videosFromUrl(link, headers, it.text()))
}
hosterSelection.contains("linkbox") && link.contains("yourupload.com") -> {
videoList.addAll(YourUploadExtractor(client).videoFromUrl(link, headers, it.text(), "Original - "))
}
hosterSelection.contains("gdriveplayer") && link.contains("neonime.fun") -> {
val headers = Headers.headersOf(
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Referer",
response.request.url.toString(),
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0",
)
var iframe = client.newCall(
GET(link, headers = headers),
).execute().asJsoup()
var iframeUrl = iframe.selectFirst("iframe")!!.attr("src")
if (!iframeUrl.startsWith("http")) {
iframeUrl = "https:$iframeUrl"
}
when {
iframeUrl.contains("gdriveplayer.to") -> {
val newHeaders = headersBuilder().add("Referer", baseUrl).build()
videoList.addAll(GdrivePlayerExtractor(client).videosFromUrl(iframeUrl, it.text(), headers = newHeaders))
}
}
}
}
}
return videoList.sort()
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun videoFromElement(element: Element): Video {
val res = client.newCall(GET(element.attr("href"))).execute().asJsoup()
val scr = res.select("script:containsData(dlbutton)").html()
var url = element.attr("href").substringBefore("/v/")
val numbs = scr.substringAfter("\" + (").substringBefore(") + \"")
val firstString = scr.substringAfter(" = \"").substringBefore("\" + (")
val num = numbs.substringBefore(" % ").toInt()
val lastString = scr.substringAfter("913) + \"").substringBefore("\";")
val nums = num % 51245 + num % 913
url += firstString + nums.toString() + lastString
val quality = with(lastString) {
when {
contains("1080p") -> "1080p"
contains("720p") -> "720p"
contains("480p") -> "480p"
contains("360p") -> "360p"
else -> "Default"
}
}
return Video(url, quality, url)
}
// screen
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val hostSelection = MultiSelectListPreference(screen.context).apply {
key = "hoster_selection"
title = "Enable/Disable Hosts"
entries = arrayOf("Blogger", "Linkbox", "Ok.ru", "YourUpload", "GdrivePlayer")
entryValues = arrayOf("blogger", "linkbox", "okru", "yourupload", "gdriveplayer")
setDefaultValue(setOf("blogger", "linkbox", "okru", "yourupload", "gdriveplayer"))
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080")
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()
}
}
screen.addPreference(hostSelection)
screen.addPreference(videoQualityPref)
}
}

View file

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.animeextension.id.neonime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
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 okhttp3.OkHttpClient
class LinkBoxExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, name: String): List<Video> {
val videoList = mutableListOf<Video>()
val id = if (url.contains("/file/")) {
url.substringAfter("/file/")
} else {
url.substringAfter("?id=")
}
val request = client.newCall(GET("https://www.linkbox.to/api/open/get_url?itemId=$id")).execute().asJsoup()
val responseJson = Json.decodeFromString<JsonObject>(request.select("body").text())
val data = responseJson["data"]?.jsonObject
val resolutions = data!!.jsonObject["rList"]!!.jsonArray
resolutions.map {
videoList.add(
Video(
it.jsonObject["url"].toString().replace("\"", ""),
"${it.jsonObject["resolution"].toString().replace("\"", "")} - $name",
it.jsonObject["url"].toString().replace("\"", ""),
),
)
}
return videoList
}
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".id.nimegami.NimeGamiUrlActivity"
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="nimegami.id"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,12 @@
ext {
extName = 'NimeGami'
extClass = '.NimeGami'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
implementation(project(":lib:synchrony"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,263 @@
package eu.kanade.tachiyomi.animeextension.id.nimegami
import android.util.Base64
import dev.datlag.jsunpacker.JsUnpacker
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.await
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator as Synchrony
class NimeGami : ParsedAnimeHttpSource() {
override val name = "NimeGami"
override val baseUrl = "https://nimegami.id"
override val lang = "id"
override val supportsLatest = true
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeSelector() = "div.wrapper-2-a > article > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img")!!.attr("data-lazy-src")
title = element.selectFirst("div.title-post2")!!.text()
}
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page")
override fun latestUpdatesSelector() = "div.post article"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
element.selectFirst("h2 > a")!!.let {
setUrlWithoutDomain(it.attr("href"))
title = it.text()
}
thumbnail_url = element.selectFirst("img")!!.attr("srcset").substringBefore(" ")
}
override fun latestUpdatesNextPageSelector() = "ul.pagination > li > a:contains(Next)"
// =============================== 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/$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)
}
// TODO: Add support for search filters
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
GET("$baseUrl/page/$page/?s=$query&post_type=post")
override fun searchAnimeSelector() = "div.archive > div > article"
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
setUrlWithoutDomain(document.location())
thumbnail_url = document.selectFirst("div.coverthumbnail img")!!.attr("src")
val infosDiv = document.selectFirst("div.info2 > table > tbody")!!
title = infosDiv.getInfo("Judul:")
?: document.selectFirst("h2[itemprop=name]")!!.text()
genre = infosDiv.getInfo("Kategori")
artist = infosDiv.getInfo("Studio")
status = with(document.selectFirst("h1.title")?.text().orEmpty()) {
when {
contains("(On-Going)") -> SAnime.ONGOING
contains("(End)") || contains("(Movie)") -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
description = buildString {
document.select("div#Sinopsis p").eachText().forEach {
append("$it\n")
}
val nonNeeded = listOf("Judul:", "Kategori", "Studio")
infosDiv.select("tr")
.eachText()
.filterNot(nonNeeded::contains)
.forEach { append("\n$it") }
}
}
private fun Element.getInfo(info: String) =
selectFirst("tr:has(td.tablex:contains($info))")?.text()?.substringAfter(": ")
// ============================== Episodes ==============================
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
override fun episodeListSelector() = "div.list_eps_stream > li.select-eps"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
val num = element.attr("id").substringAfterLast("_")
episode_number = num.toFloatOrNull() ?: 1F
name = "Episode $num"
url = element.attr("data")
}
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val qualities = json.decodeFromString<List<VideoQuality>>(episode.url.b64Decode())
val episodeIndex = episode.episode_number.toInt() - 1
var usedBunga = false // to prevent repeating the same request to bunga.nimegami
return qualities.flatMap {
val quality = it.format
it.url.mapNotNull { url ->
if (url.contains("bunga.nimegami")) {
if (usedBunga) {
return@mapNotNull null
} else {
usedBunga = true
}
}
runCatching {
extractVideos(url, quality, episodeIndex)
}.getOrElse { emptyList() }
}.flatten()
}.let { it }
}
private fun extractVideos(url: String, quality: String, episodeIndex: Int): List<Video> {
return with(url) {
when {
contains("video.nimegami.id") -> {
val realUrl = url.substringAfter("url=").substringBefore("&").b64Decode()
extractVideos(realUrl, quality, episodeIndex)
}
contains("berkasdrive") || contains("drive.nimegami") -> {
client.newCall(GET(url, headers)).execute()
.asJsoup()
.selectFirst("source[src]")
?.attr("src")
?.let {
listOf(Video(it, "Berkasdrive - $quality", it, headers))
} ?: emptyList()
}
contains("hxfile.co") -> {
val embedUrl = when {
"embed-" in url -> url
else -> url.replace(".co/", ".co/embed-") + ".html"
}
client.newCall(GET(embedUrl, headers)).execute()
.asJsoup()
.selectFirst("script:containsData(eval):containsData(p,a,c,k,e,d)")
?.data()
?.let(JsUnpacker::unpackAndCombine)
?.substringAfter("sources:[", "")
?.substringAfter("file\":\"", "")
?.substringBefore('"')
?.takeIf(String::isNotBlank)
?.let { listOf(Video(it, "HXFile - $quality", it, headers)) }
?: emptyList()
}
contains("bunga.nimegami") -> {
val episodeUrl = url.replace("select_eps", "eps=$episodeIndex")
client.newCall(GET(episodeUrl, headers)).execute()
.asJsoup()
.select("div.server_list ul > li")
.map { it.attr("url") to it.text() }
.filter { it.first.contains("uservideo") } // naniplay is absurdly slow
.parallelFlatMapBlocking(::extractUserVideo)
}
else -> emptyList()
}
}
}
private val urlPartRegex by lazy {
Regex("\\.(?:title|file) =(?:\n.*?'| ')(.*?)'", RegexOption.MULTILINE)
}
private suspend fun extractUserVideo(pair: Pair<String, String>): List<Video> {
val (url, quality) = pair
val doc = client.newCall(GET(url, headers)).await().asJsoup()
val scriptUrl = doc.selectFirst("script[src*=/s/?data]")?.attr("src")
?: return emptyList()
return client.newCall(GET(scriptUrl, headers)).await()
.body.string()
.let(Synchrony::deobfuscateScript)
?.let(urlPartRegex::findAll)
?.map { it.groupValues.drop(1) }
?.flatten()
?.chunked(2)
?.mapNotNull { videoPair ->
runCatching {
val (part, videoUrl) = videoPair
Video(videoUrl, "$quality - $part", videoUrl, headers)
}.getOrNull()
}
?.toList()
?: emptyList()
}
@Serializable
data class VideoQuality(val format: String, val url: List<String>)
override fun videoListParse(response: Response): List<Video> {
throw UnsupportedOperationException()
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
private fun String.b64Decode() = String(Base64.decode(this, Base64.DEFAULT))
companion object {
const val PREFIX_SEARCH = "id:"
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.id.nimegami
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://nimegami.id/<item> intents
* and redirects them to the main Aniyomi process.
*/
class NimeGamiUrlActivity : 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", "${NimeGami.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,7 @@
ext {
extName = 'Oploverz'
extClass = '.Oploverz'
extVersionCode = 25
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

View file

@ -0,0 +1,234 @@
package eu.kanade.tachiyomi.animeextension.id.oploverz
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
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.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapNotNullBlocking
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.json.JSONObject
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 Oploverz : ConfigurableAnimeSource, AnimeHttpSource() {
override val name: String = "Oploverz"
override val baseUrl: String = "https://oploverz.plus"
override val lang: String = "id"
override val supportsLatest: Boolean = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request =
GET("$baseUrl/anime-list/page/$page/?order=popular")
override fun popularAnimeParse(response: Response): AnimesPage =
getAnimeParse(response, "div.relat > article")
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/anime-list/page/$page/?order=latest")
override fun latestUpdatesParse(response: Response): AnimesPage =
getAnimeParse(response, "div.relat > article")
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = OploverzFilters.getSearchParameters(filters)
return GET("$baseUrl/anime-list/page/$page/?title=$query${params.filter}", headers)
}
override fun searchAnimeParse(response: Response): AnimesPage =
getAnimeParse(response, "div.relat > article")
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = OploverzFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime {
val doc = response.asJsoup()
val detail = doc.selectFirst("div.infox > div.spe")!!
return SAnime.create().apply {
author = detail.getInfo("Studio")
status = parseStatus(doc.selectFirst("div.alternati > span:nth-child(2)")!!.text())
title = doc.selectFirst("div.title > h1.entry-title")!!.text()
thumbnail_url =
doc.selectFirst("div.infoanime.widget_senction > div.thumb > img")!!
.attr("src")
description =
doc.select("div.entry-content.entry-content-single > p")
.joinToString("\n\n") { it.text() }
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.asJsoup()
return doc.select("div.lstepsiode.listeps > ul.scrolling > li").map {
val episode = it.selectFirst("span.eps > a")!!
SEpisode.create().apply {
setUrlWithoutDomain(episode.attr("href"))
episode_number = episode.text().trim().toFloatOrNull() ?: 1F
name = it.selectFirst("span.lchx > a")!!.text()
date_upload = it.selectFirst("span.date")!!.text().toDate()
}
}
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val parseUrl = response.request.url.toUrl()
val url = "${parseUrl.protocol}://${parseUrl.host}"
return doc.select("#server > ul > li > div.east_player_option")
.parallelMapNotNullBlocking {
runCatching { getEmbedLinks(url, it) }.getOrNull()
}
.parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first)
}
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(compareByDescending { it.quality.contains(quality) })
}
private fun String?.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(this?.trim() ?: "")?.time }
.getOrNull() ?: 0L
}
private fun Element.getInfo(info: String, cut: Boolean = true): String {
return selectFirst("span:has(b:contains($info))")!!.text()
.let {
when {
cut -> it.substringAfter(" ")
else -> it
}.trim()
}
}
private fun getAnimeParse(response: Response, query: String): AnimesPage {
val doc = response.asJsoup()
val animes = doc.select(query).map {
SAnime.create().apply {
setUrlWithoutDomain(it.selectFirst("div.animposx > a")!!.attr("href"))
title = it.selectFirst("div.title > h2")!!.text()
thumbnail_url = it.selectFirst("div.content-thumb > img")!!.attr("src")
}
}
val hasNextPage = try {
val pagination = doc.selectFirst("div.pagination")!!
val totalPage = pagination.selectFirst("span:nth-child(1)")!!.text().split(" ").last()
val currentPage = pagination.selectFirst("span.page-numbers.current")!!.text()
currentPage.toInt() < totalPage.toInt()
} catch (_: Exception) {
false
}
return AnimesPage(animes, hasNextPage)
}
private fun parseStatus(status: String?): Int {
return when (status?.trim()?.lowercase()) {
"completed" -> SAnime.COMPLETED
"ongoing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
private fun getEmbedLinks(url: String, element: Element): Pair<String, String> {
val form = FormBody.Builder().apply {
add("action", "player_ajax")
add("post", element.attr("data-post"))
add("nume", element.attr("data-nume"))
add("type", element.attr("data-type"))
}.build()
return client.newCall(POST("$url/wp-admin/admin-ajax.php", body = form))
.execute()
.let { Pair(it.asJsoup().selectFirst(".playeriframe")!!.attr("src"), "") }
}
private fun getVideosFromEmbed(link: String): List<Video> {
return when {
"blogger" in link -> {
client.newCall(GET(link)).execute().body.string().let {
val json = JSONObject(it.substringAfter("= ").substringBefore("<"))
val streams = json.getJSONArray("streams")
val videoList = mutableListOf<Video>()
for (i in 0 until streams.length()) {
val stream = streams.getJSONObject(i)
val url = stream.getString("play_url")
val quality = when (stream.getString("format_id")) {
"18" -> "Google - 360p"
"22" -> "Google - 720p"
else -> "Unknown Resolution"
}
videoList.add(Video(url, quality, url))
}
videoList
}
}
else -> emptyList()
}
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
summary = "%s"
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("dd/MM/yyyy", Locale("id", "ID"))
}
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("720p", "360p")
}
}

View file

@ -0,0 +1,329 @@
package eu.kanade.tachiyomi.animeextension.id.oploverz
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object OploverzFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart(name: String) = "&$name=${vals[state].second}"
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) :
AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(name: String): String {
return (this.getFirst<R>() as QueryPartFilter).toQueryPart(name)
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("&$name[]=").let {
if (it.isBlank()) {
""
} else {
"&$name[]=$it"
}
}
}
class GenreFilter : CheckBoxFilterList(
"Genre",
FiltersData.GENRE.map { CheckBoxVal(it.first, false) },
)
class SeasonFilter : CheckBoxFilterList(
"Season",
FiltersData.SEASON.map { CheckBoxVal(it.first, false) },
)
class StudioFilter : CheckBoxFilterList(
"Studio",
FiltersData.STUDIO.map { CheckBoxVal(it.first, false) },
)
class TypeFilter : QueryPartFilter("Type", FiltersData.TYPE)
class StatusFilter : QueryPartFilter("Status", FiltersData.STATUS)
class OrderFilter : QueryPartFilter("Sort By", FiltersData.ORDER)
val FILTER_LIST
get() = AnimeFilterList(
GenreFilter(),
SeasonFilter(),
StudioFilter(),
TypeFilter(),
StatusFilter(),
OrderFilter(),
)
data class FilterSearchParams(
val filter: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenreFilter>(FiltersData.GENRE, "genre") +
filters.parseCheckbox<SeasonFilter>(FiltersData.SEASON, "season") +
filters.parseCheckbox<StudioFilter>(FiltersData.STUDIO, "studio") +
filters.asQueryPart<TypeFilter>("type") +
filters.asQueryPart<StatusFilter>("status") +
filters.asQueryPart<OrderFilter>("order"),
)
}
private object FiltersData {
val ORDER = arrayOf(
Pair("Latest Update", "update"),
Pair("Latest Added", "latest"),
Pair("Popular", "popular"),
Pair("Rating", "rating"),
)
val STATUS = arrayOf(
Pair("All", ""),
Pair("Currently Airing", "Currently Airing"),
Pair("Finished Airing", "Finished Airing"),
)
val TYPE = arrayOf(
Pair("All", ""),
Pair("TV", "TV"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Special", "Special"),
Pair("Movie", "Movie"),
)
val GENRE = arrayOf(
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Award Winning", "award-winning"),
Pair("Comedy", "comedy"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Fantasy", "fantasy"),
Pair("Game", "game"),
Pair("Gore", "gore"),
Pair("Gourmet", "gourmet"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Isekai", "isekai"),
Pair("Josei", "josei"),
Pair("Live Action", "live-action"),
Pair("Magic", "magic"),
Pair("Martial Arts", "martial-arts"),
Pair("Mecha", "mecha"),
Pair("Military", "military"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Parody", "parody"),
Pair("Psychological", "psychological"),
Pair("Racing", "racing"),
Pair("Reverse Harem", "reverse-harem"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("School", "school"),
Pair("Sci-Fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Siekai", "siekai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Sports", "sports"),
Pair("Super Power", "super-power"),
Pair("Supernatural", "supernatural"),
Pair("Survival", "survival"),
Pair("Suspense", "suspense"),
Pair("Time Travel", "time-travel"),
Pair("Vampire", "vampire"),
)
val SEASON = arrayOf(
Pair("Fall 1999", "fall-1999"),
Pair("Fall 2002", "fall-2002"),
Pair("Fall 2004", "fall-2004"),
Pair("Fall 2006", "fall-2006"),
Pair("Fall 2009", "fall-2009"),
Pair("Fall 2011", "fall-2011"),
Pair("Fall 2012", "fall-2012"),
Pair("Fall 2013", "fall-2013"),
Pair("Fall 2014", "fall-2014"),
Pair("Fall 2015", "fall-2015"),
Pair("Fall 2016", "fall-2016"),
Pair("Fall 2017", "fall-2017"),
Pair("Fall 2018", "fall-2018"),
Pair("Fall 2019", "fall-2019"),
Pair("Fall 2020", "fall-2020"),
Pair("Fall 2021", "fall-2021"),
Pair("Fall 2022", "fall-2022"),
Pair("Fall 2023", "fall-2023"),
Pair("Spring 1998", "spring-1998"),
Pair("Spring 2004", "spring-2004"),
Pair("Spring 2009", "spring-2009"),
Pair("Spring 2011", "spring-2011"),
Pair("Spring 2012", "spring-2012"),
Pair("Spring 2013", "spring-2013"),
Pair("Spring 2014", "spring-2014"),
Pair("Spring 2015", "spring-2015"),
Pair("Spring 2016", "spring-2016"),
Pair("Spring 2017", "spring-2017"),
Pair("Spring 2018", "spring-2018"),
Pair("Spring 2019", "spring-2019"),
Pair("Spring 2020", "spring-2020"),
Pair("Spring 2021", "spring-2021"),
Pair("Spring 2022", "spring-2022"),
Pair("Spring 2023", "spring-2023"),
Pair("Spring 2024", "spring-2024"),
Pair("Summer 1996", "summer-1996"),
Pair("Summer 2012", "summer-2012"),
Pair("Summer 2013", "summer-2013"),
Pair("Summer 2014", "summer-2014"),
Pair("Summer 2015", "summer-2015"),
Pair("Summer 2016", "summer-2016"),
Pair("Summer 2017", "summer-2017"),
Pair("Summer 2018", "summer-2018"),
Pair("Summer 2019", "summer-2019"),
Pair("Summer 2020", "summer-2020"),
Pair("Summer 2021", "summer-2021"),
Pair("Summer 2022", "summer-2022"),
Pair("Summer 2023", "summer-2023"),
Pair("Winter 2000", "winter-2000"),
Pair("Winter 2001", "winter-2001"),
Pair("Winter 2007", "winter-2007"),
Pair("Winter 2012", "winter-2012"),
Pair("Winter 2013", "winter-2013"),
Pair("Winter 2014", "winter-2014"),
Pair("Winter 2015", "winter-2015"),
Pair("Winter 2016", "winter-2016"),
Pair("Winter 2017", "winter-2017"),
Pair("Winter 2018", "winter-2018"),
Pair("Winter 2019", "winter-2019"),
Pair("Winter 2020", "winter-2020"),
Pair("Winter 2021", "winter-2021"),
Pair("Winter 2022", "winter-2022"),
Pair("Winter 2023", "winter-2023"),
Pair("Winter 2024", "winter-2024"),
)
val STUDIO = arrayOf(
Pair("8b", "8b"),
Pair("8bit", "8bit"),
Pair("A-1 Pictures", "a-1-pictures"),
Pair("A.C.G.T.", "a-c-g-t"),
Pair("AIC PLUS+", "aic-plus"),
Pair("Ajia-Do", "ajia-do"),
Pair("Animation Do", "animation-do"),
Pair("Aniplex", "aniplex"),
Pair("Aniplex of America", "aniplex-of-america"),
Pair("Arms", "arms"),
Pair("Asread", "asread"),
Pair("AtelierPontdarc", "atelierpontdarc"),
Pair("Bandai Namco Pictures", "bandai-namco-pictures"),
Pair("Bones", "bones"),
Pair("Brain's Base", "brains-base"),
Pair("Bridge", "bridge"),
Pair("BUG FILMS", "bug-films"),
Pair("C2C", "c2c"),
Pair("Children's Playground Entertainment", "childrens-playground-entertainment"),
Pair("CloverWorks", "cloverworks"),
Pair("Connect", "connect"),
Pair("David Production", "david-production"),
Pair("Diomedea", "diomedea"),
Pair("Doga Kobo", "doga-kobo"),
Pair("DR Movie", "dr-movie"),
Pair("Drive", "drive"),
Pair("E&H Production", "eh-production"),
Pair("Encourage Films", "encourage-films"),
Pair("Feel", "feel"),
Pair("Gallop", "gallop"),
Pair("GEEK TOYS", "geek-toys"),
Pair("Geno Studio", "geno-studio"),
Pair("GoHands", "gohands"),
Pair("Gonzo", "gonzo"),
Pair("Graphinica", "graphinica"),
Pair("Hoods Drifters Studio", "hoods-drifters-studio"),
Pair("Hoods Entertainment", "hoods-entertainment"),
Pair("HOTLINE", "hotline"),
Pair("J.C.Staff", "j-c-staff"),
Pair("Kinema Citrus", "kinema-citrus"),
Pair("Kyoto Animation", "kyoto-animation"),
Pair("Lerche", "lerche"),
Pair("LIDENFILMS", "lidenfilms"),
Pair("M.S.C", "m-s-c"),
Pair("Madhouse", "madhouse"),
Pair("Manglobe", "manglobe"),
Pair("MAPPA", "mappa"),
Pair("Media Factory", "media-factory"),
Pair("Millepensee", "millepensee"),
Pair("NAZ", "naz"),
Pair("Nexus", "nexus"),
Pair("Nitroplus", "nitroplus"),
Pair("Okuruto Noboru", "okuruto-noboru"),
Pair("Orange", "orange"),
Pair("P.A. Works", "p-a-works"),
Pair("Pastel", "pastel"),
Pair("Pierrot Plus", "pierrot-plus"),
Pair("Polygon Pictures", "polygon-pictures"),
Pair("Pony Canyon", "pony-canyon"),
Pair("Production I.G", "production-i-g"),
Pair("Production IMS", "production-ims"),
Pair("Production Reed", "production-reed"),
Pair("SANZIGEN", "sanzigen"),
Pair("Satelight", "satelight"),
Pair("Sentai Filmworks", "sentai-filmworks"),
Pair("Seven Arcs", "seven-arcs"),
Pair("Shaft", "shaft"),
Pair("Shin-Ei Animation", "shin-ei-animation"),
Pair("Shuka", "shuka"),
Pair("Signal.MD", "signal-md"),
Pair("SILVER LINK.", "silver-link"),
Pair("Studio 3Hz", "studio-3hz"),
Pair("Studio Bind", "studio-bind"),
Pair("Studio Comet", "studio-comet"),
Pair("Studio Deen", "studio-deen"),
Pair("Studio Kai", "studio-kai"),
Pair("studio MOTHER", "studio-mother"),
Pair("Studio Pierrot", "studio-pierrot"),
Pair("Studio VOLN", "studio-voln"),
Pair("Sunrise", "sunrise"),
Pair("SynergySP", "synergysp"),
Pair("Tatsunoko Production", "tatsunoko-production"),
Pair("Telecom Animation Film", "telecom-animation-film"),
Pair("Tezuka Productions", "tezuka-productions"),
Pair("TMS Entertainment", "tms-entertainment"),
Pair("Toei Animation", "toei-animation"),
Pair("Trigger", "trigger"),
Pair("TROYCA", "troyca"),
Pair("TYO Animations", "tyo-animations"),
Pair("ufotable", "ufotable"),
Pair("White Fox", "white-fox"),
Pair("Wit Studio", "wit-studio"),
Pair("Yumeta Company", "yumeta-company"),
)
}
}

View file

@ -0,0 +1,12 @@
ext {
extName = 'OtakuDesu'
extClass = '.OtakuDesu'
extVersionCode = 25
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:yourupload-extractor"))
implementation(project(":lib:streamwish-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -0,0 +1,380 @@
package eu.kanade.tachiyomi.animeextension.id.otakudesu
import android.app.Application
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
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.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapNotNullBlocking
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
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 OtakuDesu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "OtakuDesu"
override val baseUrl = "https://otakudesu.cloud"
override val lang = "id"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
return SAnime.create().apply {
val info = document.selectFirst("div.infozingle")!!
title = info.getInfo("Judul") ?: ""
genre = info.getInfo("Genre")
status = parseStatus(info.getInfo("Status"))
artist = info.getInfo("Studio")
author = info.getInfo("Produser")
description = buildString {
info.getInfo("Japanese", false)?.also { append("$it\n") }
info.getInfo("Skor", false)?.also { append("$it\n") }
info.getInfo("Total Episode", false)?.also { append("$it\n") }
append("\n\nSynopsis:\n")
document.select("div.sinopc > p").eachText().forEach { append("$it\n\n") }
}
}
}
private fun parseStatus(statusString: String?): Int {
return when (statusString) {
"Ongoing" -> SAnime.ONGOING
"Completed" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
// ============================== Episodes ==============================
private val nameRegex by lazy { ".+?(?=Episode)|\\sSubtitle.+".toRegex() }
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
val link = element.selectFirst("span > a")!!
val text = link.text()
episode_number = text.substringAfter("Episode ")
.substringBefore(" ")
.toFloatOrNull() ?: 1F
setUrlWithoutDomain(link.attr("href"))
name = text.replace(nameRegex, "")
date_upload = element.selectFirst("span.zeebr")?.text().toDate()
}
}
override fun episodeListSelector() = "#venkonten > div.venser > div:nth-child(8) > ul > li"
// =============================== Latest ===============================
override fun latestUpdatesFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img")!!.attr("src")
title = element.selectFirst("h2")!!.text()
}
}
override fun latestUpdatesNextPageSelector() = "a.next.page-numbers"
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/ongoing-anime/page/$page")
override fun latestUpdatesSelector() = "div.detpost div.thumb > a"
// ============================== Popular ===============================
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/complete-anime/page/$page")
override fun popularAnimeSelector() = latestUpdatesSelector()
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element): SAnime = throw UnsupportedOperationException()
private fun searchAnimeFromElement(element: Element, ui: String): SAnime {
return SAnime.create().apply {
when (ui) {
"search" -> {
val link = element.selectFirst("h2 > a")!!
setUrlWithoutDomain(link.attr("href"))
title = link.text().replace(" Subtitle Indonesia", "")
thumbnail_url = element.selectFirst("img")!!.attr("src")
}
else -> {
val link = element.selectFirst(".col-anime-title > a")!!
setUrlWithoutDomain(link.attr("href"))
title = link.text()
thumbnail_url = element.selectFirst(".col-anime-cover > img")!!.attr("src")
}
}
}
}
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
return when {
query.isNotBlank() -> GET("$baseUrl/?s=$query&post_type=anime")
genreFilter.state != 0 -> GET("$baseUrl/genres/${genreFilter.toUriPart()}/page/$page")
else -> GET("$baseUrl/complete-anime/page/$page")
}
}
override fun searchAnimeSelector() = "#venkonten > div > div.venser > div > div > ul > li"
private val genreSelector = ".col-anime"
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val ui = when {
document.selectFirst(genreSelector) == null -> "search"
document.selectFirst(searchAnimeSelector()) == null -> "genres"
else -> "unknown"
}
val animes = when (ui) {
"genres" -> document.select(genreSelector).map { searchAnimeFromElement(it, ui) }
"search" -> document.select(searchAnimeSelector()).map { searchAnimeFromElement(it, ui) }
else -> document.select(latestUpdatesSelector()).map(::latestUpdatesFromElement)
}
val hasNextPage = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNextPage)
}
// ============================ Video Links =============================
override fun videoListSelector() = "div.mirrorstream ul li > a"
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val script = doc.selectFirst("script:containsData({action:)")!!
.data()
val nonceAction = script.substringAfter("{action:\"").substringBefore('"')
val action = script.substringAfter("action:\"").substringBefore('"')
val nonce = getNonce(nonceAction)
return doc.select(videoListSelector())
.parallelMapNotNullBlocking {
runCatching { getEmbedLinks(it, action, nonce) }.getOrNull()
}
.parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second)
}
}
private fun getEmbedLinks(element: Element, action: String, nonce: String): Pair<String, String> {
val decodedData = element.attr("data-content").b64Decode()
.drop(1)
.dropLast(1)
val (id, mirror, quality) = decodedData.split(",").map {
it.substringAfter(":").replace("\"", "")
}
val form = FormBody.Builder().apply {
add("id", id)
add("i", mirror)
add("q", quality)
add("nonce", nonce)
add("action", action)
}.build()
val doc = client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", body = form))
.execute()
.body.string()
.substringAfter(":\"")
.substringBefore('"')
.b64Decode()
.let(Jsoup::parse)
val url = doc.selectFirst("iframe")!!.attr("src")
return Pair(quality, url)
}
private val filelionsExtractor by lazy { StreamWishExtractor(client, headers) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private fun getVideosFromEmbed(quality: String, link: String): List<Video> {
return when {
"filelions" in link -> {
filelionsExtractor.videosFromUrl(link, videoNameGen = { "FileLions - $it" })
}
"yourupload" in link -> {
val id = link.substringAfter("id=").substringBefore("&")
val url = "https://yourupload.com/embed/$id"
yourUploadExtractor.videoFromUrl(url, headers, "YourUpload - $quality")
}
"desustream" in link -> {
client.newCall(GET(link, headers)).execute().let {
val doc = it.asJsoup()
val script = doc.selectFirst("script:containsData(sources)")!!.data()
val videoUrl = script.substringAfter("sources:[{")
.substringAfter("file':'")
.substringBefore("'")
listOf(Video(videoUrl, "DesuStream - $quality", videoUrl, headers))
}
}
"mp4upload" in link -> {
client.newCall(GET(link, headers)).execute().let {
val doc = it.asJsoup()
val script = doc.selectFirst("script:containsData(player.src)")!!.data()
val videoUrl = script.substringAfter("src: \"").substringBefore('"')
listOf(Video(videoUrl, "Mp4upload - $quality", videoUrl, headers))
}
}
else -> emptyList()
}
}
private fun getNonce(action: String): String {
val form = FormBody.Builder().add("action", action).build()
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", body = form))
.execute()
.body.string()
.substringAfter(":\"")
.substringBefore('"')
}
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter(),
)
private class GenreFilter : UriPartFilter(
"Genres",
arrayOf(
Pair("<select>", ""),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Comedy", "comedy"),
Pair("Demons", "demons"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Fantasy", "fantasy"),
Pair("Game", "game"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Josei", "josei"),
Pair("Magic", "magic"),
Pair("Martial Arts", "martial-arts"),
Pair("Mecha", "mecha"),
Pair("Military", "military"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Psychological", "psychological"),
Pair("Parody", "parody"),
Pair("Police", "police"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("School", "school"),
Pair("Sci-Fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounen", "shounen"),
Pair("Slice of Life", "slice-of-life"),
Pair("Sports", "sports"),
Pair("Space", "space"),
Pair("Super Power", "super-power"),
Pair("Supernatural", "supernatural"),
Pair("Thriller", "thriller"),
Pair("Vampire", "vampire"),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
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()
}
}
screen.addPreference(videoQualityPref)
}
// ============================= Utilities ==============================
private fun Element.getInfo(info: String, cut: Boolean = true): String? {
return selectFirst("p > span:has(b:contains($info))")?.text()
?.let {
when {
cut -> it.substringAfter(":")
else -> it
}.trim()
}
}
private fun String?.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(this?.trim() ?: "")?.time }
.getOrNull() ?: 0L
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareByDescending { it.quality.contains(quality) },
)
}
private fun String.b64Decode(): String {
return String(Base64.decode(this, Base64.DEFAULT))
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("d MMM,yyyy", Locale("id", "ID"))
}
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
}
}

View file

@ -0,0 +1,7 @@
ext {
extName = 'Samehadaku'
extClass = '.Samehadaku'
extVersionCode = 6
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View file

@ -0,0 +1,250 @@
package eu.kanade.tachiyomi.animeextension.id.samehadaku
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
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.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapNotNullBlocking
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
import java.text.SimpleDateFormat
import java.util.Locale
class Samehadaku : ConfigurableAnimeSource, AnimeHttpSource() {
override val name: String = "Samehadaku"
override val baseUrl: String = "https://samehadaku.show"
override val lang: String = "id"
override val supportsLatest: Boolean = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request =
GET("$baseUrl/daftar-anime-2/page/$page/?order=popular")
override fun popularAnimeParse(response: Response): AnimesPage =
getAnimeParse(response.asJsoup(), "div.relat > article")
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/daftar-anime-2/page/$page/?order=latest")
override fun latestUpdatesParse(response: Response): AnimesPage =
getAnimeParse(response.asJsoup(), "div.relat > article")
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = SamehadakuFilters.getSearchParameters(filters)
return GET("$baseUrl/daftar-anime-2/page/$page/?s=$query${params.filter}", headers)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val doc = response.asJsoup()
val searchSelector = "main.site-main.relat > article"
return if (doc.selectFirst(searchSelector) != null) {
getAnimeParse(doc, searchSelector)
} else {
getAnimeParse(doc, "div.relat > article")
}
}
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = SamehadakuFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime {
val doc = response.asJsoup()
val detail = doc.selectFirst("div.infox > div.spe")!!
return SAnime.create().apply {
author = detail.getInfo("Studio")
status = parseStatus(detail.getInfo("Status"))
title = doc.selectFirst("h3.anim-detail")!!.text().split("Detail Anime ")[1]
thumbnail_url =
doc.selectFirst("div.infoanime.widget_senction > div.thumb > img")!!
.attr("src")
description =
doc.selectFirst("div.entry-content.entry-content-single > p")!!.text()
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.asJsoup()
return doc.select("div.lstepsiode > ul > li")
.map {
val episode = it.selectFirst("span.eps > a")!!
SEpisode.create().apply {
setUrlWithoutDomain(episode.attr("href"))
episode_number = episode.text().trim().toFloatOrNull() ?: 1F
name = it.selectFirst("span.lchx > a")!!.text()
date_upload = it.selectFirst("span.date")!!.text().toDate()
}
}
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val parseUrl = response.request.url.toUrl()
val url = "${parseUrl.protocol}://${parseUrl.host}"
return doc.select("#server > ul > li > div")
.parallelMapNotNullBlocking {
runCatching { getEmbedLinks(url, it) }.getOrNull()
}
.parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second)
}
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(compareByDescending { it.quality.contains(quality) })
}
private fun String?.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(this?.trim() ?: "")?.time }
.getOrNull() ?: 0L
}
private fun Element.getInfo(info: String, cut: Boolean = true): String {
return selectFirst("span:has(b:contains($info))")!!.text()
.let {
when {
cut -> it.substringAfter(" ")
else -> it
}.trim()
}
}
private fun getAnimeParse(document: Document, query: String): AnimesPage {
val animes = document.select(query).map {
SAnime.create().apply {
setUrlWithoutDomain(it.selectFirst("div > a")!!.attr("href"))
title = it.selectFirst("div.title > h2")!!.text()
thumbnail_url = it.selectFirst("div.content-thumb > img")!!.attr("src")
}
}
val hasNextPage = try {
val pagination = document.selectFirst("div.pagination")!!
val totalPage = pagination.selectFirst("span:nth-child(1)")!!.text().split(" ").last()
val currentPage = pagination.selectFirst("span.page-numbers.current")!!.text()
currentPage.toInt() < totalPage.toInt()
} catch (_: Exception) {
false
}
return AnimesPage(animes, hasNextPage)
}
private fun parseStatus(status: String?): Int {
return when (status?.trim()?.lowercase()) {
"completed" -> SAnime.COMPLETED
"ongoing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
private fun getEmbedLinks(url: String, element: Element): Pair<String, String> {
val form = FormBody.Builder().apply {
add("action", "player_ajax")
add("post", element.attr("data-post"))
add("nume", element.attr("data-nume"))
add("type", element.attr("data-type"))
}.build()
return client.newCall(POST("$url/wp-admin/admin-ajax.php", body = form))
.execute()
.let {
val link = it.body.string().substringAfter("src=\"").substringBefore("\"")
val server = element.selectFirst("span")!!.text()
Pair(server, link)
}
}
private fun getVideosFromEmbed(server: String, link: String): List<Video> {
return when {
"wibufile" in link -> {
client.newCall(GET(link)).execute().use {
if (!it.isSuccessful) return emptyList()
val videoUrl = it.body.string().substringAfter("cast(\"").substringBefore("\"")
listOf(Video(videoUrl, server, videoUrl, headers))
}
}
"krakenfiles" in link -> {
client.newCall(GET(link)).execute().let {
val doc = it.asJsoup()
val getUrl = doc.selectFirst("source")!!.attr("src")
val videoUrl = "https:${getUrl.replace("&amp;", "&")}"
listOf(Video(videoUrl, server, videoUrl, headers))
}
}
"blogger" in link -> {
client.newCall(GET(link)).execute().let {
val videoUrl =
it.body.string().substringAfter("play_url\":\"").substringBefore("\"")
listOf(Video(videoUrl, server, videoUrl, headers))
}
}
else -> emptyList()
}
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
summary = "%s"
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("d MMMM yyyy", Locale("id", "ID"))
}
private const val PREF_QUALITY_KEY = "preferred_quality"
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")
}
}

View file

@ -0,0 +1,185 @@
package eu.kanade.tachiyomi.animeextension.id.samehadaku
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object SamehadakuFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart(name: String) = "&$name=${vals[state].second}"
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) :
AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(name: String): String {
return (this.getFirst<R>() as QueryPartFilter).toQueryPart(name)
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("&$name[]=").let {
if (it.isBlank()) {
""
} else {
"&$name[]=$it"
}
}
}
class GenreFilter : CheckBoxFilterList(
"Genre",
FiltersData.GENRE.map { CheckBoxVal(it.first, false) },
)
class TypeFilter : QueryPartFilter("Type", FiltersData.TYPE)
class StatusFilter : QueryPartFilter("Status", FiltersData.STATUS)
class OrderFilter : QueryPartFilter("Sort By", FiltersData.ORDER)
val FILTER_LIST
get() = AnimeFilterList(
AnimeFilter.Header("Ignored With Text Search!!"),
GenreFilter(),
TypeFilter(),
StatusFilter(),
OrderFilter(),
)
data class FilterSearchParams(
val filter: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenreFilter>(FiltersData.GENRE, "genre") +
filters.asQueryPart<TypeFilter>("type") +
filters.asQueryPart<StatusFilter>("status") +
filters.asQueryPart<OrderFilter>("order"),
)
}
private object FiltersData {
val ORDER = arrayOf(
Pair("A-Z", "title"),
Pair("Z-A", "titlereverse"),
Pair("Latest Update", "update"),
Pair("Latest Added", "latest"),
Pair("Popular", "popular"),
)
val STATUS = arrayOf(
Pair("All", ""),
Pair("Currently Airing", "Currently Airing"),
Pair("Finished Airing", "Finished Airing"),
)
val TYPE = arrayOf(
Pair("All", ""),
Pair("TV", "TV"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Special", "Special"),
Pair("Movie", "Movie"),
)
val GENRE = arrayOf(
Pair("Action", "action"),
Pair("Adult Cast", "adult-cast"),
Pair("Adventure", "adventure"),
Pair("Boys Love", "boys-love"),
Pair("Childcare", "childcare"),
Pair("Comedy", "comedy"),
Pair("Delinquents", "delinquents"),
Pair("Dementia", "dementia"),
Pair("Demons", "demons"),
Pair("Detective", "detective"),
Pair("Drama", "drama"),
Pair("ecch", "ecch"),
Pair("Ecchi", "ecchi"),
Pair("Fantasy", "fantasy"),
Pair("Gag Humor", "gag-humor"),
Pair("Game", "game"),
Pair("Gore", "gore"),
Pair("Gourmet", "gourmet"),
Pair("Harem", "harem"),
Pair("High Stakes Game", "high-stakes-game"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Isekai", "isekai"),
Pair("Iyashikei", "iyashikei"),
Pair("Kids", "kids"),
Pair("Magic", "magic"),
Pair("Martial Arts", "martial-arts"),
Pair("Mecha", "mecha"),
Pair("Medical", "medical"),
Pair("Military", "military"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Mythology", "mythology"),
Pair("Organized Crime", "organized-crime"),
Pair("Otaku Culture", "otaku-culture"),
Pair("Parody", "parody"),
Pair("Performing Arts", "performing-arts"),
Pair("pilihan", "pilihan"),
Pair("Police", "police"),
Pair("poling", "poling"),
Pair("Psychological", "psychological"),
Pair("Rame", "recomendation"),
Pair("Reincarnation", "reincarnation"),
Pair("Romance", "romance"),
Pair("Romantic Subtext", "romantic-subtext"),
Pair("Romantic1", "romantic1"),
Pair("Samurai", "samurai"),
Pair("School", "school"),
Pair("Sci-Fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounen", "shounen"),
Pair("Showbiz", "showbiz"),
Pair("Slice of Life", "slice-of-life"),
Pair("Space", "space"),
Pair("Sports", "sports"),
Pair("Spring 2023", "spring-2023"),
Pair("Strategy Game", "strategy-game"),
Pair("Subtext", "subtext"),
Pair("Super Power", "super-power"),
Pair("Supernatural", "supernatural"),
Pair("Survival", "survival"),
Pair("Suspense", "suspense"),
Pair("Team Sports", "team-sports"),
Pair("Thriller", "thriller"),
Pair("Time Travel", "time-travel"),
Pair("Vampire", "vampire"),
Pair("Video Game", "video-game"),
Pair("vidiogame", "vidiogame"),
Pair("Visual Arts", "visual-arts"),
Pair("Work Life", "work-life"),
Pair("Workplace", "workplace"),
)
}
}