feat(en/gogoanime): Add advanced search, new video hoster & more (#2017)

This commit is contained in:
Secozzi
2023-08-06 00:17:14 +02:00
committed by GitHub
parent cfc7ff07bd
commit c7c6f4403f
4 changed files with 492 additions and 211 deletions

View File

@ -1,18 +1,20 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
ext {
extName = 'Gogoanime'
pkgNameSuffix = 'en.gogoanime'
extClass = '.GogoAnime'
extVersionCode = 68
extVersionCode = 69
libVersion = '13'
}
dependencies {
implementation(project(':lib-mp4upload-extractor'))
implementation(project(':lib-streamsb-extractor'))
implementation(project(':lib-dood-extractor'))
implementation(project(':lib-playlist-utils'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

View File

@ -2,13 +2,15 @@ package eu.kanade.tachiyomi.animeextension.en.gogoanime
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors.GogoCdnExtractor
import eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors.StreamWishExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
@ -16,7 +18,6 @@ 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.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
@ -56,7 +57,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/popular.html?page=$page")
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/popular.html?page=$page", headers)
override fun popularAnimeSelector(): String = "div.img a"
@ -87,17 +88,13 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
val recentFilter = filterList.find { it is RecentFilter } as RecentFilter
val seasonFilter = filterList.find { it is SeasonFilter } as SeasonFilter
val params = GogoAnimeFilters.getSearchParameters(filters)
return when {
query.isNotBlank() -> GET("$baseUrl/search.html?keyword=$query&page=$page", headers)
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}?page=$page")
recentFilter.state != 0 -> GET("https://ajax.gogo-load.com/ajax/page-recent-release.html?page=$page&type=${recentFilter.toUriPart()}")
seasonFilter.state != 0 -> GET("$baseUrl/${seasonFilter.toUriPart()}?page=$page", headers)
else -> GET("$baseUrl/popular.html?page=$page")
params.genre.isNotEmpty() -> GET("$baseUrl/genre/${params.genre}?page=$page", headers)
params.recent.isNotEmpty() -> GET("https://ajax.gogo-load.com/ajax/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)
}
}
@ -107,6 +104,10 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = GogoAnimeFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
@ -138,8 +139,6 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return document.select("a").map { episodeFromElement(it) }
}
override fun episodeListSelector() = "ul#episode_page li a"
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val totalEpisodes = document.select(episodeListSelector()).last()!!.attr("ep_end")
@ -147,6 +146,8 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
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 {
@ -158,45 +159,40 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================ Video Links =============================
private val gogoExtractor by lazy { GogoCdnExtractor(client, json) }
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 gogoExtractor = GogoCdnExtractor(client, json)
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
val videoList = mutableListOf<Video>()
return document.select("div.anime_muti_link > ul > li").parallelMap { server ->
runCatching {
val className = server.className()
if (!hosterSelection.contains(className)) return@runCatching emptyList()
val serverUrl = server.selectFirst("a")
?.attr("abs:data-video")
?: return@runCatching emptyList()
videoList.addAll(
document.select("div.anime_muti_link > ul > li").parallelMap { server ->
runCatching {
val className = server.className()
if (!hosterSelection.contains(className)) return@runCatching null
val serverUrl = server.selectFirst("a")
?.attr("data-video")
?.replace(Regex("^//"), "https://")
?: return@runCatching null
when (className) {
"anime" -> {
gogoExtractor.videosFromUrl(serverUrl)
}
"vidcdn" -> {
gogoExtractor.videosFromUrl(serverUrl)
}
"streamsb" -> {
StreamSBExtractor(client).videosFromUrl(serverUrl, headers)
}
"doodstream" -> {
DoodExtractor(client).videosFromUrl(serverUrl)
}
"mp4upload" -> {
Mp4uploadExtractor(client).videosFromUrl(serverUrl, headers)
}
else -> null
}
}.getOrNull()
}.filterNotNull().flatten(),
)
getHosterVideos(className, serverUrl)
}.getOrElse { emptyList() }
}.flatten().sort().ifEmpty { throw Exception("Failed to extract videos") }
}
return videoList.sort()
private fun getHosterVideos(className: String, serverUrl: String): List<Video> {
return when (className) {
"anime" -> gogoExtractor.videosFromUrl(serverUrl)
"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 Exception("not used")
@ -239,20 +235,23 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
"Gogostream",
"Vidstreaming",
"Doodstream",
"StreamSB",
"StreamWish",
"Mp4upload",
"FileLions",
)
private val HOSTERS_NAMES = arrayOf( // Names that appears in the gogo html
"vidcdn",
"anime",
"doodstream",
"streamsb",
"streamwish",
"mp4upload",
"filelions",
)
private const val PREF_DOMAIN_KEY = "preferred_domain_name"
private val PREF_DOMAIN_KEY = "preferred_domain_name_v${AppInfo.getVersionName()}"
private const val PREF_DOMAIN_TITLE = "Override BaseUrl"
private const val PREF_DOMAIN_DEFAULT = "https://gogoanime.hu"
private const val PREF_DOMAIN_DEFAULT = "https://gogoanime3.net"
private const val PREF_DOMAIN_SUMMARY = "For temporary uses. Updating the extension will erase this setting."
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
@ -270,13 +269,14 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
EditTextPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
summary = "Override default domain (requires app restart)"
summary = PREF_DOMAIN_SUMMARY
dialogTitle = PREF_DOMAIN_TITLE
dialogMessage = "Default: $PREF_DOMAIN_DEFAULT"
setDefaultValue(PREF_DOMAIN_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val newValueString = newValue as String
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, newValueString.trim()).commit()
}
}.also(screen::addPreference)
@ -326,159 +326,4 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}.also(screen::addPreference)
}
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter(),
RecentFilter(),
SeasonFilter(),
)
private class GenreFilter : UriPartFilter(
"Genres",
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("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("Shounen Ai", "shounen-ai"),
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("Suspense", "suspense"),
Pair("Team Sports", "team-sports"),
Pair("Thriller", "thriller"),
Pair("Time Travel", "time-travel"),
Pair("Vampire", "vampire"),
Pair("Work Life", "work-life"),
Pair("Workplace", "workplace"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
),
)
private class RecentFilter : UriPartFilter(
"Recent Episodes",
arrayOf(
Pair("<select>", ""),
Pair("Recent Release", "1"),
Pair("Recent Dub", "2"),
Pair("Recent Chinese", "3"),
),
)
private class SeasonFilter : UriPartFilter(
"Season",
arrayOf(
Pair("<select>", ""),
Pair("Latest season", "new-season.html"),
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"),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
}

View File

@ -0,0 +1,410 @@
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("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("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

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) {
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "StreamWish - $quality" }): List<Video> {
val doc = client.newCall(GET(url, headers = headers)).execute().asJsoup()
val jsEval = doc.selectFirst("script:containsData(m3u8)")?.data() ?: return emptyList()
val masterUrl = JsUnpacker.unpackAndCombine(jsEval)
?.substringAfter("source")
?.substringAfter("file:\"")
?.substringBefore("\"")
?: return emptyList()
return PlaylistUtils(client, headers).extractFromHls(masterUrl, videoNameGen = videoNameGen)
}
}