Merge branch 'Kohi-den:main' into KAA

This commit is contained in:
Arkai1 2025-04-27 17:23:45 +05:30 committed by GitHub
commit b917ee3c6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 35 additions and 1826 deletions

View file

@ -47,7 +47,7 @@ class MegaCloudExtractor(
private var shouldUpdateKey = false private var shouldUpdateKey = false
private const val PREF_KEY_KEY = "megacloud_key_" private const val PREF_KEY_KEY = "megacloud_key_"
private const val PREF_KEY_DEFAULT = "[[0, 0]]" private const val PREF_KEY_DEFAULT = "[[0, 0]]"
private inline fun <reified R> runLocked(crossinline block: () -> R) = runBlocking(Dispatchers.IO) { private inline fun <reified R> runLocked(crossinline block: () -> R) = runBlocking(Dispatchers.IO) {
MUTEX.withLock { block() } MUTEX.withLock { block() }
} }
@ -132,6 +132,7 @@ class MegaCloudExtractor(
?.filter { it.kind == "captions" } ?.filter { it.kind == "captions" }
?.map { Track(it.file, it.label) } ?.map { Track(it.file, it.label) }
.orEmpty() .orEmpty()
.let { playlistUtils.fixSubtitles(it) }
return playlistUtils.extractFromHls( return playlistUtils.extractFromHls(
masterUrl, masterUrl,
videoNameGen = { "$name - $it - $type" }, videoNameGen = { "$name - $it - $type" },

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.lib.playlistutils package eu.kanade.tachiyomi.lib.playlistutils
import android.net.Uri
import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -8,6 +9,7 @@ import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.internal.commonEmptyHeaders import okhttp3.internal.commonEmptyHeaders
import java.io.File
import kotlin.math.abs import kotlin.math.abs
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) { class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
@ -342,7 +344,32 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
return "${result}p" return "${result}p"
} }
private fun cleanSubtitleData(matchResult: MatchResult): String {
val lineCount = matchResult.groupValues[1].count { it == '\n' }
return "\n" + "&nbsp;\n".repeat(lineCount - 1)
}
fun fixSubtitles(subtitleList: List<Track>): List<Track> {
return subtitleList.mapNotNull {
try {
val subData = client.newCall(GET(it.url)).execute().body.string()
val file = File.createTempFile("subs", "vtt")
.also(File::deleteOnExit)
file.writeText(FIX_SUBTITLE_REGEX.replace(subData, ::cleanSubtitleData))
val uri = Uri.fromFile(file)
Track(uri.toString(), it.lang)
} catch (_: Exception) {
null
}
}
}
companion object { companion object {
private val FIX_SUBTITLE_REGEX = Regex("""${'$'}(\n{2,})(?!(?:\d+:)*\d+(?:\.\d+)?\s-+>\s(?:\d+:)*\d+(?:\.\d+)?)""", RegexOption.MULTILINE)
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:" private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"
private val SUBTITLE_REGEX by lazy { Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""") } private val SUBTITLE_REGEX by lazy { Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""") }

View file

@ -291,7 +291,7 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
return playlistUtils.extractFromHls( return playlistUtils.extractFromHls(
playlistUrl = url, playlistUrl = url,
videoNameGen = { quality -> "$serverName - $quality - $typeName" }, videoNameGen = { quality -> "$serverName - $quality - $typeName" },
subtitleList = subtitles, subtitleList = playlistUtils.fixSubtitles(subtitles),
masterHeadersGen = { baseHeaders: Headers, _: String -> masterHeadersGen = { baseHeaders: Headers, _: String ->
baseHeaders.newBuilder().apply { baseHeaders.newBuilder().apply {
set("Accept", "*/*") set("Accept", "*/*")

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,202 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dramacool
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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
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 DramaCool : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "DramaCool"
// TODO: Check frequency of url changes to potentially
// add back overridable baseurl preference
override val baseUrl = "https://asianc.co/"
override val lang = "en"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/most-popular-drama?page=$page") // page/$page
override fun popularAnimeSelector() = "ul.list-episode-item li a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img")?.attr("data-original")?.replace(" ", "%20")
title = element.selectFirst("h3")?.text() ?: "Serie"
}
override fun popularAnimeNextPageSelector() = "li.next a"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/recently-added?page=$page")
override fun latestUpdatesSelector() = "ul.switch-block a"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
GET("$baseUrl/search?keyword=$query&page=$page")
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request {
if (anime.url.contains("-episode-") && anime.url.endsWith(".html")) {
val doc = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
anime.setUrlWithoutDomain(doc.selectFirst("div.category a")!!.attr("href"))
}
return GET(baseUrl + anime.url)
}
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
document.selectFirst("div.img img")!!.run {
title = attr("alt")
thumbnail_url = absUrl("src")
}
with(document.selectFirst("div.info")!!) {
description = select("p:contains(Description) ~ p:not(:has(span))").eachText()
.joinToString("\n")
.takeUnless(String::isBlank)
author = selectFirst("p:contains(Original Network:) > a")?.text()
genre = select("p:contains(Genre:) > a").joinToString { it.text() }.takeUnless(String::isBlank)
status = parseStatus(selectFirst("p:contains(Status) a")?.text())
}
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "ul.all-episode li a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
val epNum = element.selectFirst("h3")!!.text().substringAfterLast("Episode ")
val type = element.selectFirst("span.type")?.text() ?: "RAW"
name = "$type: Episode $epNum".trimEnd()
episode_number = when {
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
else -> 1F
}
date_upload = element.selectFirst("span.time")?.text().orEmpty().toDate()
}
// ============================ Video Links =============================
override fun videoListSelector() = "ul.list-server-items li"
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val iframeUrl = document.selectFirst("iframe")?.absUrl("src") ?: return emptyList()
val iframeDoc = client.newCall(GET(iframeUrl)).execute().asJsoup()
return iframeDoc.select(videoListSelector()).flatMap(::videosFromElement)
}
private val doodExtractor by lazy { DoodExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
// TODO: Create a extractor for the "Standard server" thingie.
// it'll require Synchrony or something similar, but synchrony is too slow >:(
private fun videosFromElement(element: Element): List<Video> {
val url = element.attr("data-video")
return runCatching {
when {
url.contains("dood") -> doodExtractor.videosFromUrl(url)
url.contains("dwish") -> streamwishExtractor.videosFromUrl(url)
url.contains("streamtape") -> streamtapeExtractor.videosFromUrl(url)
else -> emptyList()
}
}.getOrElse { emptyList() }
}
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)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
private fun parseStatus(statusString: String?): Int {
return when (statusString) {
"Ongoing" -> SAnime.ONGOING
"Completed" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
}
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "Doodstream", "StreamTape")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
}
}

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -1,368 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.fmovies
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.vidsrcextractor.VidsrcExtractor
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.parallelCatchingFlatMap
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
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 uy.kohesive.injekt.injectLazy
class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "FMovies"
override val baseUrl = "https://fmovies24.to"
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val utils by lazy { FmoviesUtils(client, headers) }
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending${page.toPageQuery()}", headers)
override fun popularAnimeSelector(): String = "div.items > div.item"
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
element.selectFirst("div.meta a")!!.let { a ->
title = a.text()
setUrlWithoutDomain(a.attr("abs:href"))
}
thumbnail_url = element.select("div.poster img").attr("data-src")
}
override fun popularAnimeNextPageSelector(): String = "ul.pagination > li.active + li"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/filter?keyword=&sort=recently_updated${page.toPageQuery(false)}", headers)
override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = FMoviesFilters.getSearchParameters(filters)
return GET("$baseUrl/filter?keyword=$query${params.filter}${page.toPageQuery(false)}", headers)
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = FMoviesFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val info = document.selectFirst("section#w-info > div.info")!!
val detail = info.selectFirst("div.detail")
val descElement = info.selectFirst("div.description")
val desc = descElement?.selectFirst("div[data-name=full]")?.ownText() ?: descElement?.ownText() ?: ""
val extraInfo = detail?.select("> div")?.joinToString("\n") { it.text() } ?: ""
val mediaTitle = info.selectFirst("h1.name")!!.text()
val mediaDetail = utils.getDetail(mediaTitle)
return SAnime.create().apply {
title = mediaTitle
status = when (mediaDetail?.status) {
"Ended", "Released" -> SAnime.COMPLETED
"In Production" -> SAnime.LICENSED
"Canceled" -> SAnime.CANCELLED
"Returning Series" -> {
mediaDetail.nextEpisode?.let { SAnime.ONGOING } ?: SAnime.ON_HIATUS
}
else -> SAnime.UNKNOWN
}
thumbnail_url = document.selectFirst("section#w-info > div.poster img")!!.attr("src")
description = buildString {
appendLine(desc.ifBlank { mediaDetail?.overview })
appendLine()
mediaDetail?.nextEpisode?.let {
appendLine("Next: Ep ${it.epNumber} - ${it.name}")
appendLine("Air Date: ${it.airDate}")
appendLine()
}
appendLine(extraInfo)
}
genre = detail?.let { dtl ->
dtl.select("> div:has(> div:contains(Genre:)) span").joinToString { it.text() }
}
author = detail?.let { dtl ->
dtl.select("> div:has(> div:contains(Production:)) span").joinToString { it.text() }
}
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
.selectFirst("div[data-id]")!!.attr("data-id")
val vrf = utils.vrfEncrypt(id)
val vrfHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Host", baseUrl.toHttpUrl().host)
add("Referer", baseUrl + anime.url)
add("X-Requested-With", "XMLHttpRequest")
}.build()
return GET("$baseUrl/ajax/episode/list/$id?vrf=$vrf", headers = vrfHeaders)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = Jsoup.parse(
response.parseAs<AjaxResponse>().result,
)
val episodeList = mutableListOf<SEpisode>()
val seasons = document.select("div.body > ul.episodes")
seasons.forEach { season ->
val seasonPrefix = if (seasons.size > 1) {
"Season ${season.attr("data-season")} "
} else {
""
}
season.select("li").forEach { ep ->
episodeList.add(
SEpisode.create().apply {
name = "$seasonPrefix${ep.text().trim()}".replace("Episode ", "Ep. ")
ep.selectFirst("a")!!.let { a ->
episode_number = a.attr("data-num").toFloatOrNull() ?: 0F
url = json.encodeToString(
EpisodeInfo(
id = a.attr("data-id"),
url = "$baseUrl${a.attr("href")}",
),
)
}
},
)
}
}
return episodeList.reversed()
}
override fun episodeListSelector() = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
return client.newCall(videoListRequest(episode))
.awaitSuccess()
.let { response ->
videoListParse(response, episode).sort()
}
}
override fun videoListRequest(episode: SEpisode): Request {
val data = json.decodeFromString<EpisodeInfo>(episode.url)
val vrf = utils.vrfEncrypt(data.id)
val vrfHeaders = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Host", baseUrl.toHttpUrl().host)
.add("Referer", data.url)
.add("X-Requested-With", "XMLHttpRequest")
.build()
return GET("$baseUrl/ajax/server/list/${data.id}?vrf=$vrf", headers = vrfHeaders)
}
private val vidsrcExtractor by lazy { VidsrcExtractor(client, headers) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private suspend fun videoListParse(response: Response, episode: SEpisode): List<Video> {
val data = json.decodeFromString<EpisodeInfo>(episode.url)
val document = Jsoup.parse(
response.parseAs<AjaxResponse>().result,
)
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
return document.select("ul.servers > li.server").parallelCatchingFlatMap { server ->
val name = server.text().trim()
if (!hosterSelection.contains(name)) return@parallelCatchingFlatMap emptyList()
// Get decrypted url
val vrf = utils.vrfEncrypt(server.attr("data-link-id"))
val vrfHeaders = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Host", baseUrl.toHttpUrl().host)
.add("Referer", data.url)
.add("X-Requested-With", "XMLHttpRequest")
.build()
val encrypted = client.newCall(
GET("$baseUrl/ajax/server/${server.attr("data-link-id")}?vrf=$vrf", headers = vrfHeaders),
).await().parseAs<AjaxServerResponse>().result.url
val decrypted = utils.vrfDecrypt(encrypted)
when (name) {
"Vidplay", "MyCloud" -> {
val subs = client.newCall(
GET("$baseUrl/ajax/episode/subtitles/${data.id}"),
).execute().toTracks()
vidsrcExtractor.videosFromUrl(decrypted, name, subtitleList = subs)
}
"Filemoon" -> filemoonExtractor.videosFromUrl(decrypted, headers = headers)
"Streamtape" -> {
val subtitleList = decrypted.toHttpUrl().queryParameter("sub.info")?.let {
client.newCall(GET(it, headers)).await().toTracks()
} ?: emptyList()
streamtapeExtractor.videoFromUrl(decrypted, subtitleList = subtitleList)?.let(::listOf) ?: emptyList()
}
else -> emptyList()
}
}
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(server) },
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
private fun Int.toPageQuery(first: Boolean = true): String {
return if (this == 1) "" else "${if (first) "?" else "&"}page=$this"
}
private fun Response.toTracks(): List<Track> = parseAs<List<FMoviesSubs>>()
.map { t ->
Track(t.file, t.label)
}
companion object {
private val HOSTERS = arrayOf(
"Vidplay",
"MyCloud",
"Filemoon",
"Streamtape",
)
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Vidplay"
private const val PREF_HOSTER_KEY = "hoster_selection"
private val PREF_HOSTER_DEFAULT = setOf("Vidplay", "Filemoon")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
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)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = HOSTERS
entryValues = HOSTERS
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = "Enable/Disable Hosts"
entries = HOSTERS
entryValues = HOSTERS
setDefaultValue(PREF_HOSTER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
}

View file

@ -1,54 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.fmovies
import kotlinx.serialization.Serializable
@Serializable
data class AjaxResponse(
val result: String,
)
@Serializable
data class AjaxServerResponse(
val result: UrlObject,
) {
@Serializable
data class UrlObject(
val url: String,
)
}
@Serializable
data class EpisodeInfo(
val id: String,
val url: String,
)
@Serializable
data class FMoviesSubs(
val file: String,
val label: String,
)
@Serializable
data class MediaResponseBody(
val status: Int,
val result: Result,
) {
@Serializable
data class Result(
val sources: ArrayList<Source>,
val tracks: ArrayList<SubTrack> = ArrayList(),
) {
@Serializable
data class Source(
val file: String,
)
@Serializable
data class SubTrack(
val file: String,
val label: String = "",
val kind: String,
)
}
}

View file

@ -1,304 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.fmovies
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object FMoviesFilters {
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)
open class TriStateFilterList(name: String, values: List<TriFilter>) : AnimeFilter.Group<AnimeFilter.TriState>(name, values)
class TriFilter(name: String, val value: String) : AnimeFilter.TriState(name)
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"
}
}
}
private inline fun <reified R> AnimeFilterList.parseTriFilter(
options: Array<Pair<String, String>>,
name: String,
): String {
return (this.getFirst<R>() as TriStateFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state != AnimeFilter.TriState.STATE_IGNORE) {
(if (checkbox.state == AnimeFilter.TriState.STATE_EXCLUDE) "-" else "") + options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("&$name[]=").let {
if (it.isBlank()) {
""
} else {
"&$name[]=$it"
}
}
}
class TypesFilter : CheckBoxFilterList(
"Type",
FMoviesFiltersData.TYPES.map { CheckBoxVal(it.first, false) },
)
class GenresFilter : TriStateFilterList(
"Genre",
FMoviesFiltersData.GENRES.map { TriFilter(it.first, it.second) },
)
class CountriesFilter : CheckBoxFilterList(
"Country",
FMoviesFiltersData.COUNTRIES.map { CheckBoxVal(it.first, false) },
)
class YearsFilter : CheckBoxFilterList(
"Year",
FMoviesFiltersData.YEARS.map { CheckBoxVal(it.first, false) },
)
class RatingsFilter : CheckBoxFilterList(
"Rating",
FMoviesFiltersData.RATINGS.map { CheckBoxVal(it.first, false) },
)
class QualitiesFilter : CheckBoxFilterList(
"Quality",
FMoviesFiltersData.QUALITIES.map { CheckBoxVal(it.first, false) },
)
class SortFilter : QueryPartFilter("Sort", FMoviesFiltersData.SORT)
val FILTER_LIST get() = AnimeFilterList(
TypesFilter(),
GenresFilter(),
CountriesFilter(),
YearsFilter(),
RatingsFilter(),
QualitiesFilter(),
SortFilter(),
)
data class FilterSearchParams(
val filter: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<TypesFilter>(FMoviesFiltersData.TYPES, "type") +
filters.parseTriFilter<GenresFilter>(FMoviesFiltersData.GENRES, "genre") +
filters.parseCheckbox<CountriesFilter>(FMoviesFiltersData.COUNTRIES, "country") +
filters.parseCheckbox<YearsFilter>(FMoviesFiltersData.YEARS, "year") +
filters.parseCheckbox<RatingsFilter>(FMoviesFiltersData.RATINGS, "rating") +
filters.parseCheckbox<QualitiesFilter>(FMoviesFiltersData.QUALITIES, "quality") +
filters.asQueryPart<SortFilter>("sort"),
)
}
private object FMoviesFiltersData {
val TYPES = arrayOf(
Pair("Movie", "movie"),
Pair("TV-Shows", "tv"),
)
val GENRES = arrayOf(
Pair("Action", "25"),
Pair("Adult", "1068691"),
Pair("Adventure", "17"),
Pair("Animation", "10"),
Pair("Biography", "215"),
Pair("Comedy", "14"),
Pair("Costume", "1693"),
Pair("Crime", "26"),
Pair("Documentary", "131"),
Pair("Drama", "1"),
Pair("Family", "43"),
Pair("Fantasy", "31"),
Pair("Film-Noir", "1068395"),
Pair("Game-Show", "212"),
Pair("History", "47"),
Pair("Horror", "74"),
Pair("Kungfu", "248"),
Pair("Music", "199"),
Pair("Musical", "1066604"),
Pair("Mystery", "64"),
Pair("News", "1066549"),
Pair("Reality", "1123750"),
Pair("Reality-TV", "4"),
Pair("Romance", "23"),
Pair("Sci-Fi", "15"),
Pair("Short", "1066916"),
Pair("Sport", "44"),
Pair("Talk", "1124002"),
Pair("Talk-Show", "1067786"),
Pair("Thriller", "7"),
Pair("TV Movie", "1123752"),
Pair("TV Show", "139"),
Pair("War", "58"),
Pair("Western", "28"),
)
val COUNTRIES = arrayOf(
Pair("Argentina", "181863"),
Pair("Australia", "181851"),
Pair("Austria", "181882"),
Pair("Belgium", "181849"),
Pair("Brazil", "181867"),
Pair("Canada", "181861"),
Pair("China", "108"),
Pair("Czech Republic", "181859"),
Pair("Denmark", "181855"),
Pair("Finland", "181877"),
Pair("France", "11"),
Pair("Germany", "1025332"),
Pair("Hong Kong", "2630"),
Pair("Hungary", "181876"),
Pair("India", "34"),
Pair("Ireland", "181862"),
Pair("Israel", "181887"),
Pair("Italy", "181857"),
Pair("Japan", "36"),
Pair("Luxembourg", "181878"),
Pair("Mexico", "181852"),
Pair("Netherlands", "181848"),
Pair("New Zealand", "181847"),
Pair("Norway", "181901"),
Pair("Philippines", "1025339"),
Pair("Poland", "181880"),
Pair("Romania", "181895"),
Pair("Russia", "181860"),
Pair("South Africa", "181850"),
Pair("South Korea", "1025429"),
Pair("Spain", "181871"),
Pair("Sweden", "181883"),
Pair("Switzerland", "181869"),
Pair("Thailand", "94"),
Pair("Turkey", "1025379"),
Pair("United Kingdom", "8"),
Pair("United States", "2"),
)
val YEARS = arrayOf(
Pair("2024", "2024"),
Pair("2023", "2023"),
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("2017", "2017"),
Pair("2016", "2016"),
Pair("2015", "2015"),
Pair("2014", "2014"),
Pair("2013", "2013"),
Pair("2012", "2012"),
Pair("2011", "2011"),
Pair("2010", "2010"),
Pair("2009", "2009"),
Pair("2008", "2008"),
Pair("2007", "2007"),
Pair("2006", "2006"),
Pair("2005", "2005"),
Pair("2004", "2004"),
Pair("2003", "2003"),
Pair("2000s", "2000s"),
Pair("1990s", "1990s"),
Pair("1980s", "1980s"),
Pair("1970s", "1970s"),
Pair("1960s", "1960s"),
Pair("1950s", "1950s"),
Pair("1940s", "1940s"),
Pair("1930s", "1930s"),
Pair("1920s", "1920s"),
Pair("1910s", "1910s"),
)
val RATINGS = arrayOf(
Pair("12", "12"),
Pair("13+", "13+"),
Pair("16+", "16+"),
Pair("18", "18"),
Pair("18+", "18+"),
Pair("AO", "AO"),
Pair("C", "C"),
Pair("E", "E"),
Pair("G", "G"),
Pair("GP", "GP"),
Pair("M", "M"),
Pair("M/PG", "M/PG"),
Pair("MA-13", "MA-13"),
Pair("MA-17", "MA-17"),
Pair("NC-17", "NC-17"),
Pair("PG", "PG"),
Pair("PG-13", "PG-13"),
Pair("R", "R"),
Pair("TV_MA", "TV_MA"),
Pair("TV-13", "TV-13"),
Pair("TV-14", "TV-14"),
Pair("TV-G", "TV-G"),
Pair("TV-MA", "TV-MA"),
Pair("TV-PG", "TV-PG"),
Pair("TV-Y", "TV-Y"),
Pair("TV-Y7", "TV-Y7"),
Pair("TV-Y7-FV", "TV-Y7-FV"),
Pair("X", "X"),
)
val QUALITIES = arrayOf(
Pair("HD", "HD"),
Pair("HDRip", "HDRip"),
Pair("SD", "SD"),
Pair("TS", "TS"),
Pair("CAM", "CAM"),
)
val SORT = arrayOf(
Pair("Most relevance", "most_relevance"),
Pair("Recently updated", "recently_updated"),
Pair("Recently added", "recently_added"),
Pair("Release date", "release_date"),
Pair("Trending", "trending"),
Pair("Name A-Z", "title_az"),
Pair("Scores", "scores"),
Pair("IMDb", "imdb"),
Pair("Most watched", "most_watched"),
Pair("Most favourited", "most_favourited"),
)
}
}

View file

@ -1,140 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.fmovies
import android.util.Base64
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.net.URLDecoder
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
class FmoviesUtils(private val client: OkHttpClient, private val headers: Headers) {
// ===================== Media Detail ================================
private val tmdbURL = "https://api.themoviedb.org/3".toHttpUrl()
private val seez = "https://seez.su"
private val apiKey by lazy {
val jsUrl = client.newCall(GET(seez, headers)).execute().asJsoup()
.select("script[defer][src]")[1].attr("abs:src")
val jsBody = client.newCall(GET(jsUrl, headers)).execute().use { it.body.string() }
Regex("""f="(\w{20,})"""").find(jsBody)!!.groupValues[1]
}
private val apiHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Host", "api.themoviedb.org")
add("Origin", seez)
add("Referer", "$seez/")
}.build()
fun getDetail(mediaTitle: String): TmdbDetailsResponse? =
runCatching {
val searchUrl = tmdbURL.newBuilder().apply {
addPathSegment("search")
addPathSegment("multi")
addQueryParameter("query", mediaTitle)
addQueryParameter("api_key", apiKey)
}.build().toString()
val searchResp = client.newCall(GET(searchUrl, headers = apiHeaders))
.execute()
.parseAs<TmdbResponse>()
val media = searchResp.results.first()
val detailUrl = tmdbURL.newBuilder().apply {
addPathSegment(media.mediaType)
addPathSegment(media.id.toString())
addQueryParameter("api_key", apiKey)
}.build().toString()
client.newCall(GET(detailUrl, headers = apiHeaders))
.execute()
.parseAs<TmdbDetailsResponse>()
}.getOrNull()
// ===================== Encryption ================================
fun vrfEncrypt(input: String): String {
val rc4Key = SecretKeySpec("Ij4aiaQXgluXQRs6".toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.ENCRYPT_MODE, rc4Key, cipher.parameters)
var vrf = cipher.doFinal(input.toByteArray())
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
// vrf = rot13(vrf)
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
vrf.reverse()
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
vrf = vrfShift(vrf)
val stringVrf = vrf.toString(Charsets.UTF_8)
return java.net.URLEncoder.encode(stringVrf, "utf-8")
}
fun vrfDecrypt(input: String): String {
var vrf = input.toByteArray()
vrf = Base64.decode(vrf, Base64.URL_SAFE)
val rc4Key = SecretKeySpec("8z5Ag5wgagfsOuhz".toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
vrf = cipher.doFinal(vrf)
return URLDecoder.decode(vrf.toString(Charsets.UTF_8), "utf-8")
}
private fun rot13(vrf: ByteArray): ByteArray {
for (i in vrf.indices) {
val byte = vrf[i]
if (byte in 'A'.code..'Z'.code) {
vrf[i] = ((byte - 'A'.code + 13) % 26 + 'A'.code).toByte()
} else if (byte in 'a'.code..'z'.code) {
vrf[i] = ((byte - 'a'.code + 13) % 26 + 'a'.code).toByte()
}
}
return vrf
}
private fun vrfShift(vrf: ByteArray): ByteArray {
for (i in vrf.indices) {
val shift = arrayOf(4, 3, -2, 5, 2, -4, -4, 2)[i % 8]
vrf[i] = vrf[i].plus(shift).toByte()
}
return vrf
}
}
@Serializable
data class TmdbResponse(
val results: List<TmdbResult>,
) {
@Serializable
data class TmdbResult(
val id: Int,
@SerialName("media_type")
val mediaType: String = "tv",
)
}
@Serializable
data class TmdbDetailsResponse(
val status: String,
val overview: String? = null,
@SerialName("next_episode_to_air")
val nextEpisode: NextEpisode? = null,
) {
@Serializable
data class NextEpisode(
val name: String? = "",
@SerialName("episode_number")
val epNumber: Int,
@SerialName("air_date")
val airDate: String,
)
}

View file

@ -1,14 +0,0 @@
ext {
extName = 'Gogoanime'
extClass = '.GogoAnime'
extVersionCode = 87
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:gogostream-extractor'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

View file

@ -1,299 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.gogoanime
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.gogostreamextractor.GogoStreamExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Gogoanime"
// TODO: Check frequency of url changes to potentially
// add back overridable baseurl preference
override val baseUrl = "https://anitaku.to"
override val lang = "en"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/popular.html?page=$page", headers)
override fun popularAnimeSelector(): String = "div.img a"
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img")!!.attr("src")
title = element.attr("title")
}
override fun popularAnimeNextPageSelector(): String = "ul.pagination-list li:last-child:not(.selected)"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home.html?page=$page", headers)
override fun latestUpdatesSelector(): String = "div.img a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
thumbnail_url = element.selectFirst("img")?.attr("src")
title = element.attr("title")
val slug = element.attr("href").substringAfter(baseUrl)
.trimStart('/')
.substringBefore("-episode-")
setUrlWithoutDomain("/category/$slug")
}
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = GogoAnimeFilters.getSearchParameters(filters)
return when {
params.genre.isNotEmpty() -> GET("$baseUrl/genre/${params.genre}?page=$page", headers)
params.recent.isNotEmpty() -> GET("$AJAX_URL/page-recent-release.html?page=$page&type=${params.recent}", headers)
params.season.isNotEmpty() -> GET("$baseUrl/${params.season}?page=$page", headers)
else -> GET("$baseUrl/filter.html?keyword=$query&${params.filter}&page=$page", headers)
}
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = GogoAnimeFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val infoDocument = document.selectFirst("div.anime-info a[href]")?.let {
client.newCall(GET(it.absUrl("href"), headers)).execute().asJsoup()
} ?: document
return SAnime.create().apply {
title = infoDocument.selectFirst("div.anime_info_body_bg > h1")!!.text()
genre = infoDocument.getInfo("Genre:")
status = parseStatus(infoDocument.getInfo("Status:").orEmpty())
description = buildString {
val summary = infoDocument.selectFirst("div.anime_info_body_bg > div.description")
append(summary?.text())
// add alternative name to anime description
infoDocument.getInfo("Other name:")?.also {
if (isNotBlank()) append("\n\n")
append("Other name(s): $it")
}
}
}
}
// ============================== Episodes ==============================
private fun episodesRequest(totalEpisodes: String, id: String): List<SEpisode> {
val request = GET("$AJAX_URL/load-list-episode?ep_start=0&ep_end=$totalEpisodes&id=$id", headers)
val epResponse = client.newCall(request).execute()
val document = epResponse.asJsoup()
return document.select("a").map(::episodeFromElement)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val totalEpisodes = document.select(episodeListSelector()).last()!!.attr("ep_end")
val id = document.select("input#movie_id").attr("value")
return episodesRequest(totalEpisodes, id)
}
override fun episodeListSelector() = "ul#episode_page li a"
override fun episodeFromElement(element: Element): SEpisode {
val ep = element.selectFirst("div.name")!!.ownText().substringAfter(" ")
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
episode_number = ep.toFloat()
name = "Episode $ep"
}
}
// ============================ Video Links =============================
private val gogoExtractor by lazy { GogoStreamExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
return document.select("div.anime_muti_link > ul > li").parallelCatchingFlatMapBlocking { server ->
val className = server.className()
if (!hosterSelection.contains(className)) return@parallelCatchingFlatMapBlocking emptyList()
val serverUrl = server.selectFirst("a")
?.attr("abs:data-video")
?: return@parallelCatchingFlatMapBlocking emptyList()
getHosterVideos(className, serverUrl)
}
}
private fun getHosterVideos(className: String, serverUrl: String): List<Video> {
return when (className) {
"anime", "vidcdn" -> gogoExtractor.videosFromUrl(serverUrl)
"streamwish" -> streamwishExtractor.videosFromUrl(serverUrl)
"doodstream" -> doodExtractor.videosFromUrl(serverUrl)
"mp4upload" -> mp4uploadExtractor.videosFromUrl(serverUrl, headers)
"filelions" -> {
streamwishExtractor.videosFromUrl(serverUrl, videoNameGen = { quality -> "FileLions - $quality" })
}
else -> emptyList()
}
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================= Utilities ==============================
private fun Document.getInfo(text: String): String? {
val base = selectFirst("p.type:has(span:containsOwn($text))") ?: return null
return base.select("a").eachText().joinToString("")
.ifBlank { base.ownText() }
.takeUnless(String::isBlank)
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
{ it.quality.contains(server) },
),
).reversed()
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Ongoing" -> SAnime.ONGOING
"Completed" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
companion object {
private const val AJAX_URL = "https://ajax.gogocdn.net/ajax"
private val HOSTERS = arrayOf(
"Gogostream",
"Vidstreaming",
"Doodstream",
"StreamWish",
"Mp4upload",
"FileLions",
)
private val HOSTERS_NAMES = arrayOf(
// Names that appears in the gogo html
"vidcdn",
"anime",
"doodstream",
"streamwish",
"mp4upload",
"filelions",
)
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Preferred server"
private const val PREF_SERVER_DEFAULT = "Gogostream"
private const val PREF_HOSTER_KEY = "hoster_selection"
private const val PREF_HOSTER_TITLE = "Enable/Disable Hosts"
private val PREF_HOSTER_DEFAULT = HOSTERS_NAMES.toSet()
}
// ============================== 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)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = PREF_SERVER_TITLE
entries = HOSTERS
entryValues = HOSTERS
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = PREF_HOSTER_TITLE
entries = HOSTERS
entryValues = HOSTERS_NAMES
setDefaultValue(PREF_HOSTER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
}

View file

@ -1,414 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.gogoanime
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object GogoAnimeFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (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 GenreSearchFilter : CheckBoxFilterList("Genre", GogoAnimeFiltersData.GENRE_SEARCH_LIST)
class CountrySearchFilter : CheckBoxFilterList("Country", GogoAnimeFiltersData.COUNTRY_SEARCH_LIST)
class SeasonSearchFilter : CheckBoxFilterList("Season", GogoAnimeFiltersData.SEASON_SEARCH_LIST)
class YearSearchFilter : CheckBoxFilterList("Year", GogoAnimeFiltersData.YEAR_SEARCH_LIST)
class LanguageSearchFilter : CheckBoxFilterList("Language", GogoAnimeFiltersData.LANGUAGE_SEARCH_LIST)
class TypeSearchFilter : CheckBoxFilterList("Type", GogoAnimeFiltersData.TYPE_SEARCH_LIST)
class StatusSearchFilter : CheckBoxFilterList("Status", GogoAnimeFiltersData.STATUS_SEARCH_LIST)
class SortSearchFilter : QueryPartFilter("Sort by", GogoAnimeFiltersData.SORT_SEARCH_LIST)
class GenreFilter : QueryPartFilter("Genre", GogoAnimeFiltersData.GENRE_LIST)
class RecentFilter : QueryPartFilter("Recent episodes", GogoAnimeFiltersData.RECENT_LIST)
class SeasonFilter : QueryPartFilter("Season", GogoAnimeFiltersData.SEASON_LIST)
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("Advanced search"),
GenreSearchFilter(),
CountrySearchFilter(),
SeasonSearchFilter(),
YearSearchFilter(),
LanguageSearchFilter(),
TypeSearchFilter(),
StatusSearchFilter(),
SortSearchFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("Select sub-page"),
AnimeFilter.Header("Note: Ignores search & other filters"),
GenreFilter(),
RecentFilter(),
SeasonFilter(),
)
data class FilterSearchParams(
val filter: String = "",
val genre: String = "",
val recent: String = "",
val season: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val filter = buildList {
add(filters.parseCheckbox<GenreSearchFilter>(GogoAnimeFiltersData.GENRE_SEARCH_LIST, "genre"))
add(filters.parseCheckbox<CountrySearchFilter>(GogoAnimeFiltersData.GENRE_SEARCH_LIST, "country"))
add(filters.parseCheckbox<SeasonSearchFilter>(GogoAnimeFiltersData.SEASON_SEARCH_LIST, "season"))
add(filters.parseCheckbox<YearSearchFilter>(GogoAnimeFiltersData.YEAR_SEARCH_LIST, "year"))
add(filters.parseCheckbox<LanguageSearchFilter>(GogoAnimeFiltersData.LANGUAGE_SEARCH_LIST, "language"))
add(filters.parseCheckbox<TypeSearchFilter>(GogoAnimeFiltersData.TYPE_SEARCH_LIST, "type"))
add(filters.parseCheckbox<StatusSearchFilter>(GogoAnimeFiltersData.STATUS_SEARCH_LIST, "status"))
add("sort=${filters.asQueryPart<SortSearchFilter>()}")
}.filter(String::isNotBlank).joinToString("&")
return FilterSearchParams(
filter,
filters.asQueryPart<GenreFilter>(),
filters.asQueryPart<RecentFilter>(),
filters.asQueryPart<SeasonFilter>(),
)
}
private object GogoAnimeFiltersData {
// copy($("div.cls_genre ul.dropdown-menu li").map((i,el) => `Pair("${$(el).text().trim()}", "${$(el).find("input").first().attr('value')}")`).get().join(',\n'))
// on /filter.html
val GENRE_SEARCH_LIST = arrayOf(
Pair("Action", "action"),
Pair("Adult Cast", "adult-cast"),
Pair("Adventure", "adventure"),
Pair("Anthropomorphic", "anthropomorphic"),
Pair("Avant Garde", "avant-garde"),
Pair("Boys Love", "shounen-ai"),
Pair("Cars", "cars"),
Pair("CGDCT", "cgdct"),
Pair("Childcare", "childcare"),
Pair("Comedy", "comedy"),
Pair("Comic", "comic"),
Pair("Crime", "crime"),
Pair("Crossdressing", "crossdressing"),
Pair("Delinquents", "delinquents"),
Pair("Dementia", "dementia"),
Pair("Demons", "demons"),
Pair("Detective", "detective"),
Pair("Drama", "drama"),
Pair("Dub", "dub"),
Pair("Ecchi", "ecchi"),
Pair("Erotica", "erotica"),
Pair("Family", "family"),
Pair("Fantasy", "fantasy"),
Pair("Gag Humor", "gag-humor"),
Pair("Game", "game"),
Pair("Gender Bender", "gender-bender"),
Pair("Gore", "gore"),
Pair("Gourmet", "gourmet"),
Pair("Harem", "harem"),
Pair("Hentai", "hentai"),
Pair("High Stakes Game", "high-stakes-game"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Isekai", "isekai"),
Pair("Iyashikei", "iyashikei"),
Pair("Josei", "josei"),
Pair("Kids", "kids"),
Pair("Magic", "magic"),
Pair("Magical Sex Shift", "magical-sex-shift"),
Pair("Mahou Shoujo", "mahou-shoujo"),
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("Parody", "parody"),
Pair("Performing Arts", "performing-arts"),
Pair("Pets", "pets"),
Pair("Police", "police"),
Pair("Psychological", "psychological"),
Pair("Racing", "racing"),
Pair("Reincarnation", "reincarnation"),
Pair("Romance", "romance"),
Pair("Romantic Subtext", "romantic-subtext"),
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("Strategy Game", "strategy-game"),
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("Visual Arts", "visual-arts"),
Pair("Work Life", "work-life"),
Pair("Workplace", "workplace"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
)
// copy($("div.cls_genre ul.dropdown-menu li").map((i,el) => `Pair("${$(el).text().trim()}", "${$(el).find("input").first().attr('value')}")`).get().join(',\n'))
// on /filter.html
val COUNTRY_SEARCH_LIST = arrayOf(
Pair("China", "5"),
Pair("Japan", "2"),
)
// copy($("div.cls_season ul.dropdown-menu li").map((i,el) => `Pair("${$(el).text().trim()}", "${$(el).find("input").first().attr('value')}")`).get().join(',\n'))
// on /filter.html
val SEASON_SEARCH_LIST = arrayOf(
Pair("Fall", "fall"),
Pair("Summer", "summer"),
Pair("Spring", "spring"),
Pair("Winter", "winter"),
)
// copy($("div.cls_year ul.dropdown-menu li").map((i,el) => `Pair("${$(el).text().trim()}", "${$(el).find("input").first().attr('value')}")`).get().join(',\n'))
// on /filter.html
val YEAR_SEARCH_LIST = arrayOf(
Pair("2024", "2024"),
Pair("2023", "2023"),
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("2017", "2017"),
Pair("2016", "2016"),
Pair("2015", "2015"),
Pair("2014", "2014"),
Pair("2013", "2013"),
Pair("2012", "2012"),
Pair("2011", "2011"),
Pair("2010", "2010"),
Pair("2009", "2009"),
Pair("2008", "2008"),
Pair("2007", "2007"),
Pair("2006", "2006"),
Pair("2005", "2005"),
Pair("2004", "2004"),
Pair("2003", "2003"),
Pair("2002", "2002"),
Pair("2001", "2001"),
Pair("2000", "2000"),
Pair("1999", "1999"),
)
// copy($("div.cls_lang ul.dropdown-menu li").map((i,el) => `Pair("${$(el).text().trim()}", "${$(el).find("input").first().attr('value')}")`).get().join(',\n'))
// on /filter.html
val LANGUAGE_SEARCH_LIST = arrayOf(
Pair("Sub & Dub", "subdub"),
Pair("Sub", "sub"),
Pair("Dub", "dub"),
)
// copy($("div.cls_type ul.dropdown-menu li").map((i,el) => `Pair("${$(el).text().trim()}", "${$(el).find("input").first().attr('value')}")`).get().join(',\n'))
// on /filter.html
val TYPE_SEARCH_LIST = arrayOf(
Pair("Movie", "3"),
Pair("TV", "1"),
Pair("OVA", "26"),
Pair("ONA", "30"),
Pair("Special", "2"),
Pair("Music", "32"),
)
// copy($("div.cls_status ul.dropdown-menu li").map((i,el) => `Pair("${$(el).text().trim()}", "${$(el).find("input").first().attr('value')}")`).get().join(',\n'))
// on /filter.html
val STATUS_SEARCH_LIST = arrayOf(
Pair("Not Yet Aired", "Upcoming"),
Pair("Ongoing", "Ongoing"),
Pair("Completed", "Completed"),
)
// copy($("div.cls_sort ul.dropdown-menu li").map((i,el) => `Pair("${$(el).text().trim()}", "${$(el).find("input").first().attr('value')}")`).get().join(',\n'))
// on /filter.html
val SORT_SEARCH_LIST = arrayOf(
Pair("Name A-Z", "title_az"),
Pair("Recently updated", "recently_updated"),
Pair("Recently added", "recently_added"),
Pair("Release date", "release_date"),
)
// copy($("div.dropdown-menu > a.dropdown-item").map((i,el) => `Pair("${$(el).text().trim()}", "${$(el).attr('href').trim().slice(18)}")`).get().join(',\n'))
// on /
val GENRE_LIST = arrayOf(
Pair("<select>", ""),
Pair("Action", "action"),
Pair("Adult Cast", "adult-cast"),
Pair("Adventure", "adventure"),
Pair("Anthropomorphic", "anthropomorphic"),
Pair("Avant Garde", "avant-garde"),
Pair("Boys Love", "shounen-ai"),
Pair("Cars", "cars"),
Pair("CGDCT", "cgdct"),
Pair("Childcare", "childcare"),
Pair("Comedy", "comedy"),
Pair("Comic", "comic"),
Pair("Crime", "crime"),
Pair("Crossdressing", "crossdressing"),
Pair("Delinquents", "delinquents"),
Pair("Dementia", "dementia"),
Pair("Demons", "demons"),
Pair("Detective", "detective"),
Pair("Drama", "drama"),
Pair("Dub", "dub"),
Pair("Ecchi", "ecchi"),
Pair("Erotica", "erotica"),
Pair("Family", "family"),
Pair("Fantasy", "fantasy"),
Pair("Gag Humor", "gag-humor"),
Pair("Game", "game"),
Pair("Gender Bender", "gender-bender"),
Pair("Gore", "gore"),
Pair("Gourmet", "gourmet"),
Pair("Harem", "harem"),
Pair("Hentai", "hentai"),
Pair("High Stakes Game", "high-stakes-game"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Isekai", "isekai"),
Pair("Iyashikei", "iyashikei"),
Pair("Josei", "josei"),
Pair("Kids", "kids"),
Pair("Magic", "magic"),
Pair("Magical Sex Shift", "magical-sex-shift"),
Pair("Mahou Shoujo", "mahou-shoujo"),
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("Parody", "parody"),
Pair("Performing Arts", "performing-arts"),
Pair("Pets", "pets"),
Pair("Police", "police"),
Pair("Psychological", "psychological"),
Pair("Racing", "racing"),
Pair("Reincarnation", "reincarnation"),
Pair("Romance", "romance"),
Pair("Romantic Subtext", "romantic-subtext"),
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("Strategy Game", "strategy-game"),
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("Visual Arts", "visual-arts"),
Pair("Work Life", "work-life"),
Pair("Workplace", "workplace"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
)
val RECENT_LIST = arrayOf(
Pair("<select>", ""),
Pair("Recent Release", "1"),
Pair("Recent Dub", "2"),
Pair("Recent Chinese", "3"),
)
val SEASON_LIST = arrayOf(
Pair("<select>", ""),
Pair("Latest season", "new-season.html"),
Pair("Spring 2024", "sub-category/spring-2024-anime"),
Pair("Winter 2024", "sub-category/winter-2024-anime"),
Pair("Fall 2023", "sub-category/fall-2023-anime"),
Pair("Summer 2023", "sub-category/summer-2023-anime"),
Pair("Spring 2023", "sub-category/spring-2023-anime"),
Pair("Winter 2023", "sub-category/winter-2023-anime"),
Pair("Fall 2022", "sub-category/fall-2022-anime"),
Pair("Summer 2022", "sub-category/summer-2022-anime"),
Pair("Spring 2022", "sub-category/spring-2022-anime"),
Pair("Winter 2022", "sub-category/winter-2022-anime"),
Pair("Fall 2021", "sub-category/fall-2021-anime"),
Pair("Summer 2021", "sub-category/summer-2021-anime"),
Pair("Spring 2021", "sub-category/spring-2021-anime"),
Pair("Winter 2021", "sub-category/winter-2021-anime"),
Pair("Fall 2020", "sub-category/fall-2020-anime"),
Pair("Summer 2020", "sub-category/summer-2020-anime"),
Pair("Spring 2020", "sub-category/spring-2020-anime"),
Pair("Winter 2020", "sub-category/winter-2020-anime"),
Pair("Fall 2019", "sub-category/fall-2019-anime"),
Pair("Summer 2019", "sub-category/summer-2019-anime"),
Pair("Spring 2019", "sub-category/spring-2019-anime"),
Pair("Winter 2019", "sub-category/winter-2019-anime"),
Pair("Fall 2018", "sub-category/fall-2018-anime"),
Pair("Summer 2018", "sub-category/summer-2018-anime"),
Pair("Spring 2018", "sub-category/spring-2018-anime"),
Pair("Winter 2018", "sub-category/winter-2018-anime"),
Pair("Fall 2017", "sub-category/fall-2017-anime"),
Pair("Summer 2017", "sub-category/summer-2017-anime"),
Pair("Spring 2017", "sub-category/spring-2017-anime"),
Pair("Winter 2017", "sub-category/winter-2017-anime"),
Pair("Fall 2016", "sub-category/fall-2016-anime"),
Pair("Summer 2016", "sub-category/summer-2016-anime"),
Pair("Spring 2016", "sub-category/spring-2016-anime"),
Pair("Winter 2016", "sub-category/winter-2016-anime"),
Pair("Fall 2015", "sub-category/fall-2015-anime"),
Pair("Summer 2015", "sub-category/summer-2015-anime"),
Pair("Spring 2015", "sub-category/spring-2015-anime"),
Pair("Winter 2015", "sub-category/winter-2015-anime"),
Pair("Fall 2014", "sub-category/fall-2014-anime"),
Pair("Summer 2014", "sub-category/summer-2014-anime"),
Pair("Spring 2014", "sub-category/spring-2014-anime"),
Pair("Winter 2014", "sub-category/winter-2014-anime"),
)
}
}

View file

@ -18,6 +18,8 @@ class KickAssAnimeExtractor(
private val json: Json, private val json: Json,
private val headers: Headers, private val headers: Headers,
) { ) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, name: String): List<Video> { fun videosFromUrl(url: String, name: String): List<Video> {
val host = url.toHttpUrl().host val host = url.toHttpUrl().host
val mid = if (name == "DuckStream") "mid" else "id" val mid = if (name == "DuckStream") "mid" else "id"
@ -77,7 +79,7 @@ class KickAssAnimeExtractor(
val language = "${it.name} (${it.language})" val language = "${it.name} (${it.language})"
Track(subUrl, language) Track(subUrl, language)
} }.let { playlistUtils.fixSubtitles(it) }
fun getVideoHeaders(baseHeaders: Headers, referer: String, videoUrl: String): Headers { fun getVideoHeaders(baseHeaders: Headers, referer: String, videoUrl: String): Headers {
return baseHeaders.newBuilder().apply { return baseHeaders.newBuilder().apply {
@ -92,8 +94,8 @@ class KickAssAnimeExtractor(
return when { return when {
videoObject.hls.isBlank() -> videoObject.hls.isBlank() ->
PlaylistUtils(client, headers).extractFromDash(videoObject.playlistUrl, videoNameGen = { res -> "$name - $res" }, subtitleList = subtitles) playlistUtils.extractFromDash(videoObject.playlistUrl, videoNameGen = { res -> "$name - $res" }, subtitleList = subtitles)
else -> PlaylistUtils(client, headers).extractFromHls( else -> playlistUtils.extractFromHls(
videoObject.playlistUrl, videoObject.playlistUrl,
videoNameGen = { "$name - $it" }, videoNameGen = { "$name - $it" },
videoHeadersGen = ::getVideoHeaders, videoHeadersGen = ::getVideoHeaders,