Merge branch 'Kohi-den:main' into KAA
|
@ -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" },
|
||||||
|
|
|
@ -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" + " \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="(.*?)"""") }
|
||||||
|
|
|
@ -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", "*/*")
|
||||||
|
|
|
@ -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'))
|
|
||||||
}
|
|
Before Width: | Height: | Size: 5 KiB |
Before Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 20 KiB |
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'))
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 9.9 KiB |
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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'))
|
|
||||||
}
|
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 8 KiB |
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|