Fix hikari (#963)

* add hikari

* mass bump due for extractor changes
This commit is contained in:
V3u47ZoN 2025-05-01 10:16:57 +00:00 committed by GitHub
parent 45cff438ce
commit 821cbc1d59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 668 additions and 557 deletions

View file

@ -3,7 +3,7 @@ ext {
extClass = '.ChineseAnime'
themePkg = 'animestream'
baseUrl = 'https://www.chineseanime.vip'
overrideVersionCode = 13
overrideVersionCode = 14
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Hikari'
extClass = '.Hikari'
extVersionCode = 16
extVersionCode = 17
}
apply from: "$rootDir/common.gradle"
@ -9,6 +9,7 @@ apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:chillx-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:savefile-extractor'))
implementation(project(':lib:buzzheavier-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:vidhide-extractor'))
}
}

View file

@ -0,0 +1,92 @@
package eu.kanade.tachiyomi.animeextension.all.hikari
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CatalogResponseDto<T>(
val next: String? = null,
val results: List<T>,
)
@Serializable
data class AnimeDto(
val uid: String,
@SerialName("ani_ename")
val aniEName: String? = null,
@SerialName("ani_name")
val aniName: String,
@SerialName("ani_poster")
val aniPoster: String? = null,
@SerialName("ani_synopsis")
val aniSynopsis: String? = null,
@SerialName("ani_synonyms")
val aniSynonyms: String? = null,
@SerialName("ani_genre")
val aniGenre: String? = null,
@SerialName("ani_studio")
val aniStudio: String? = null,
@SerialName("ani_stats")
val aniStats: Int? = null,
) {
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
url = uid
title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName
thumbnail_url = aniPoster
genre = aniGenre?.split(",")?.joinToString(transform = String::trim)
author = aniStudio
description = buildString {
aniSynopsis?.trim()?.let(::append)
append("\n\n")
aniSynonyms?.let { append("Synonyms: $it") }
}.trim()
status = when (aniStats) {
1 -> SAnime.ONGOING
2 -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
}
@Serializable
data class LatestEpisodeDto(
val uid: Int,
val title: String,
@SerialName("title_en")
val titleEn: String? = null,
val imageUrl: String,
) {
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
url = uid.toString()
title = if (preferEnglish) titleEn?.takeUnless(String::isBlank) ?: this@LatestEpisodeDto.title else this@LatestEpisodeDto.title
thumbnail_url = imageUrl
}
}
@Serializable
data class EpisodeDto(
@SerialName("ep_id_name")
val epId: String,
@SerialName("ep_name")
val epName: String? = null,
) {
fun toSEpisode(uid: String): SEpisode = SEpisode.create().apply {
url = "$uid-$epId"
name = epName?.let { "Ep. $epId - $it" } ?: "Episode $epId"
episode_number = epId.toFloatOrNull() ?: 1f
}
}
@Serializable
data class EmbedDto(
@SerialName("embed_type")
val embedType: String,
@SerialName("embed_name")
val embedName: String,
@SerialName("embed_frame")
val embedFrame: String,
)

View file

@ -5,7 +5,7 @@ import okhttp3.HttpUrl
import java.util.Calendar
interface UriFilter {
fun addToUri(url: HttpUrl.Builder)
fun addToUri(builder: HttpUrl.Builder)
}
sealed class UriPartFilter(
@ -20,7 +20,10 @@ sealed class UriPartFilter(
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
val value = vals[state].second
if (value.isNotEmpty()) {
builder.addQueryParameter(param, value)
}
}
}
@ -33,13 +36,15 @@ sealed class UriMultiSelectFilter(
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
if (checked.isNotEmpty()) {
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
}
}
}
class TypeFilter : UriPartFilter(
"Type",
"type",
"ani_type",
arrayOf(
Pair("All", ""),
Pair("TV", "1"),
@ -50,165 +55,53 @@ class TypeFilter : UriPartFilter(
),
)
class CountryFilter : UriPartFilter(
"Country",
"country",
arrayOf(
Pair("All", ""),
Pair("Japanese", "1"),
Pair("Chinese", "2"),
),
)
class StatusFilter : UriPartFilter(
"Status",
"stats",
"ani_stats",
arrayOf(
Pair("All", ""),
Pair("Currently Airing", "1"),
Pair("Finished Airing", "2"),
Pair("Not yet Aired", "3"),
),
)
class RatingFilter : UriPartFilter(
"Rating",
"rate",
arrayOf(
Pair("All", ""),
Pair("G", "1"),
Pair("PG", "2"),
Pair("PG-13", "3"),
Pair("R-17+", "4"),
Pair("R+", "5"),
Pair("Rx", "6"),
),
)
class SourceFilter : UriPartFilter(
"Source",
"source",
arrayOf(
Pair("All", ""),
Pair("LightNovel", "1"),
Pair("Manga", "2"),
Pair("Original", "3"),
Pair("Ongoing", "1"),
Pair("Completed", "2"),
Pair("Upcoming", "3"),
),
)
class SeasonFilter : UriPartFilter(
"Season",
"season",
"ani_release_season",
arrayOf(
Pair("All", ""),
Pair("Spring", "1"),
Pair("Summer", "2"),
Pair("Fall", "3"),
Pair("Winter", "4"),
Pair("Winter", "1"),
Pair("Spring", "2"),
Pair("Summer", "3"),
Pair("Fall", "4"),
),
)
class LanguageFilter : UriPartFilter(
"Language",
"language",
arrayOf(
Pair("All", ""),
Pair("Raw", "1"),
Pair("Sub", "2"),
Pair("Dub", "3"),
Pair("Turk", "4"),
),
)
class SortFilter : UriPartFilter(
"Sort",
"sort",
arrayOf(
Pair("Default", "default"),
Pair("Recently Added", "recently_added"),
Pair("Recently Updated", "recently_updated"),
Pair("Score", "score"),
Pair("Name A-Z", "name_az"),
Pair("Released Date", "released_date"),
Pair("Most Watched", "most_watched"),
),
)
class YearFilter(name: String, param: String) : UriPartFilter(
name,
param,
class YearFilter : UriPartFilter(
"Release Year",
"ani_release",
YEARS,
) {
companion object {
private val NEXT_YEAR by lazy {
Calendar.getInstance()[Calendar.YEAR] + 1
private val CURRENT_YEAR by lazy {
Calendar.getInstance()[Calendar.YEAR]
}
private val YEARS = Array(NEXT_YEAR - 1917) { year ->
if (year == 0) {
Pair("Any", "")
} else {
(NEXT_YEAR - year).toString().let { Pair(it, it) }
}
}
}
}
class MonthFilter(name: String, param: String) : UriPartFilter(
name,
param,
MONTHS,
) {
companion object {
private val MONTHS = Array(13) { months ->
if (months == 0) {
Pair("Any", "")
} else {
val monthStr = "%02d".format(months)
Pair(monthStr, monthStr)
}
}
}
}
class DayFilter(name: String, param: String) : UriPartFilter(
name,
param,
DAYS,
) {
companion object {
private val DAYS = Array(32) { day ->
if (day == 0) {
Pair("Any", "")
} else {
val dayStr = "%02d".format(day)
Pair(dayStr, dayStr)
}
}
}
}
class AiringDateFilter(
private val values: List<UriPartFilter> = PARTS,
) : AnimeFilter.Group<UriPartFilter>("Airing Date", values), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
values.forEach {
it.addToUri(builder)
}
}
companion object {
private val PARTS = listOf(
YearFilter("Year", "aired_year"),
MonthFilter("Month", "aired_month"),
DayFilter("Day", "aired_day"),
)
private val YEARS = buildList {
add(Pair("Any", ""))
addAll(
(1990..CURRENT_YEAR).map {
Pair(it.toString(), it.toString())
},
)
}.toTypedArray()
}
}
class GenreFilter : UriMultiSelectFilter(
"Genre",
"genres",
"ani_genre",
arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
@ -233,7 +126,7 @@ class GenreFilter : UriMultiSelectFilter(
Pair("Music", "Music"),
Pair("Mystery", "Mystery"),
Pair("Parody", "Parody"),
Pair("Police", "Police"),
Pair("Policy", "Policy"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Samurai", "Samurai"),
@ -253,3 +146,12 @@ class GenreFilter : UriMultiSelectFilter(
Pair("Vampire", "Vampire"),
),
)
class LanguageFilter : UriPartFilter(
"Language",
"ani_genre",
arrayOf(
Pair("Any", ""),
Pair("Portuguese", "Portuguese"),
),
)

View file

@ -1,42 +1,43 @@
package eu.kanade.tachiyomi.animeextension.all.hikari
import android.app.Application
import android.util.Log
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.buzzheavierextractor.BuzzheavierExtractor
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.savefileextractor.SavefileExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
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
class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Hikari"
override val baseUrl = "https://watch.hikaritv.xyz"
private val proxyUrl = "https://hikari.gg/hiki-proxy/extract/"
private val apiUrl = "https://api.hikari.gg/api"
override val baseUrl = "https://hikari.gg"
override val lang = "all"
override val versionId = 2
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder().apply {
@ -50,75 +51,40 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=score&genres=&page=$page"
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
return GET(url, headers)
}
override fun popularAnimeRequest(page: Int) = searchAnimeRequest(page, "", AnimeFilterList())
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<HtmlResponseDto>()
val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < parsed.page!!.totalPages
val animeList = parsed.toHtml(baseUrl).select(popularAnimeSelector())
.map(::popularAnimeFromElement)
return AnimesPage(animeList, hasNextPage)
}
override fun popularAnimeSelector(): String = ".flw-item"
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a[data-id]")!!.attr("abs:href"))
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
title = element.selectFirst(".film-name")!!.text()
}
override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeParse(response: Response) = searchAnimeParse(response)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=recently_updated&genres=&page=$page"
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
val url = "$apiUrl/episode/new/".toHttpUrl().newBuilder().apply {
addQueryParameter("limit", "100")
addQueryParameter("language", "EN")
}.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): AnimesPage =
popularAnimeParse(response)
override fun latestUpdatesParse(response: Response): AnimesPage {
val data = response.parseAs<CatalogResponseDto<LatestEpisodeDto>>()
val preferEnglish = preferences.getTitleLang
override fun latestUpdatesSelector(): String =
throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SAnime =
throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String =
throw UnsupportedOperationException()
val animeList = data.results.distinctBy { it.uid }.map { it.toSAnime(preferEnglish) }
return AnimesPage(animeList, false)
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (query.isNotEmpty()) {
addPathSegment("search")
addQueryParameter("keyword", query)
addQueryParameter("page", page.toString())
} else {
addPathSegment("ajax")
addPathSegment("getfilter")
filters.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
addQueryParameter("page", page.toString())
val url = "$apiUrl/anime/".toHttpUrl().newBuilder().apply {
addQueryParameter("sort", "created_at")
addQueryParameter("order", "asc")
addQueryParameter("page", page.toString())
filters.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
}.build()
val headers = headersBuilder().apply {
if (query.isNotEmpty()) {
set("Referer", url.toString().substringBeforeLast("&page"))
} else {
set("Referer", "$baseUrl/filter")
addQueryParameter("search", query)
}
}.build()
@ -126,280 +92,148 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
}
override fun searchAnimeParse(response: Response): AnimesPage {
return if (response.request.url.encodedPath.startsWith("/search")) {
super.searchAnimeParse(response)
} else {
popularAnimeParse(response)
}
val data = response.parseAs<CatalogResponseDto<AnimeDto>>()
val preferEnglish = preferences.getTitleLang
val animeList = data.results.map { it.toSAnime(preferEnglish) }
val hasNextPage = data.next != null
return AnimesPage(animeList, hasNextPage)
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.active + li"
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Note: text search ignores filters"),
AnimeFilter.Separator(),
TypeFilter(),
CountryFilter(),
StatusFilter(),
RatingFilter(),
SourceFilter(),
SeasonFilter(),
LanguageFilter(),
SortFilter(),
AiringDateFilter(),
YearFilter(),
GenreFilter(),
LanguageFilter(),
)
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
with(document.selectFirst("#ani_detail")!!) {
title = selectFirst(".film-name")!!.text()
thumbnail_url = selectFirst(".film-poster img")!!.attr("abs:src")
description = selectFirst(".film-description > .text")?.text()
genre = select(".item-list:has(span:contains(Genres)) > a").joinToString { it.text() }
author = select(".item:has(span:contains(Studio)) > a").joinToString { it.text() }
status = selectFirst(".item:has(span:contains(Status)) > .name").parseStatus()
}
override fun getAnimeUrl(anime: SAnime): String {
return "$baseUrl/info/${anime.url}"
}
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
"currently airing" -> SAnime.ONGOING
"finished" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
override fun animeDetailsRequest(anime: SAnime): Request {
return GET("$apiUrl/anime/uid/${anime.url}/", headers)
}
override fun animeDetailsParse(response: Response): SAnime {
return response.parseAs<AnimeDto>().toSAnime(preferences.getTitleLang)
}
// ============================== Episodes ==============================
private val specialCharRegex = Regex("""(?![\-_])\W{1,}""")
override fun episodeListRequest(anime: SAnime): Request {
val animeId = anime.url.split("/")[2]
val sanitized = anime.title.replace(" ", "_")
val refererUrl = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("watch")
addQueryParameter("anime", specialCharRegex.replace(sanitized, ""))
addQueryParameter("uid", animeId)
addQueryParameter("eps", "1")
}.build()
val headers = headersBuilder()
.set("Referer", refererUrl.toString())
.build()
return GET("$baseUrl/ajax/episodelist/$animeId", headers)
return GET("$apiUrl/episode/uid/${anime.url}/", headers)
}
override fun episodeListParse(response: Response): List<SEpisode> {
return response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
.select(episodeListSelector())
.map(::episodeFromElement)
.reversed()
}
val guid = response.request.url.pathSegments[3]
override fun episodeListSelector() = "a[class~=ep-item]"
override fun episodeFromElement(element: Element): SEpisode {
val epText = element.selectFirst(".ssli-order")?.text()?.trim()
?: element.attr("data-number").trim()
val ep = epText.toFloatOrNull() ?: 0F
val nameText = element.selectFirst(".ep-name")?.text()?.trim()
?: element.attr("title").replace("Episode-", "Ep. ") ?: ""
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
episode_number = ep
name = "Ep. $ep - $nameText"
}
return response.parseAs<List<EpisodeDto>>().map { it.toSEpisode(guid) }.reversed()
}
// ============================ Video Links =============================
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
private val filemoonExtractor by lazy { FilemoonExtractor(client, preferences) }
private val savefileExtractor by lazy { SavefileExtractor(client, preferences) }
private val buzzheavierExtractor by lazy { BuzzheavierExtractor(client, headers) }
private val chillxExtractor by lazy { ChillxExtractor(client, headers) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val embedRegex = Regex("""getEmbed\(\s*(\d+)\s*,\s*(\d+)\s*,\s*'(\d+)'""")
private fun getEmbedTypeName(type: String): String {
return when (type) {
"2" -> "[SUB] "
"3" -> "[DUB] "
"4" -> "[MULTI AUDIO] "
"8" -> "[HARD-SUB] "
else -> ""
}
}
override fun videoListRequest(episode: SEpisode): Request {
val url = (baseUrl + episode.url).toHttpUrl()
val animeId = url.queryParameter("uid")!!
val episodeNum = url.queryParameter("eps")!!
val headers = headersBuilder()
.set("Referer", baseUrl + episode.url)
.build()
return GET("$baseUrl/ajax/embedserver/$animeId/$episodeNum", headers)
val (guid, epId) = episode.url.split("-")
return GET("$apiUrl/embed/$guid/$epId/", headers)
}
override fun videoListParse(response: Response): List<Video> {
val html = response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
Log.d("Hikari", html.toString())
val data = response.parseAs<List<EmbedDto>>()
val headers = headersBuilder()
.set("Referer", response.request.url.toString())
.build()
return data.parallelCatchingFlatMapBlocking { embed ->
val prefix = getEmbedTypeName(embed.embedType) + embed.embedName
val embedName = embed.embedName.lowercase()
val subEmbedUrls = html.select(".servers-sub div.item.server-item").flatMap { item ->
val name = item.text().trim() + " (Sub)"
val onClick = item.selectFirst("a")?.attr("onclick")
if (onClick == null) {
Log.e("Hikari", "onClick attribute is null for item: $item")
return@flatMap emptyList<Pair<String, String>>()
}
val match = embedRegex.find(onClick)?.groupValues
if (match == null) {
Log.e("Hikari", "No match found for onClick: $onClick")
return@flatMap emptyList<Pair<String, String>>()
}
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
val iframeList = client.newCall(
GET(url, headers),
).execute().parseAs<List<String>>()
iframeList.map {
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
if (iframeSrc == null) {
Log.e("Hikari", "iframe src is null for URL: $url")
return@map Pair("", "")
}
Pair(iframeSrc, name)
}.filter { it.first.isNotEmpty() }
}
val dubEmbedUrls = html.select(".servers-dub div.item.server-item").flatMap { item ->
val name = item.text().trim() + " (Dub)"
val onClick = item.selectFirst("a")?.attr("onclick")
if (onClick == null) {
Log.e("Hikari", "onClick attribute is null for item: $item")
return@flatMap emptyList<Pair<String, String>>()
}
val match = embedRegex.find(onClick)?.groupValues
if (match == null) {
Log.e("Hikari", "No match found for onClick: $onClick")
return@flatMap emptyList<Pair<String, String>>()
}
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
val iframeList = client.newCall(
GET(url, headers),
).execute().parseAs<List<String>>()
iframeList.map {
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
if (iframeSrc == null) {
Log.e("Hikari", "iframe src is null for URL: $url")
return@map Pair("", "")
}
Pair(iframeSrc, name)
}.filter { it.first.isNotEmpty() }
}
val sdEmbedUrls = html.select(".servers-sub.\\&.dub div.item.server-item").flatMap { item ->
val name = item.text().trim() + " (Sub + Dub)"
val onClick = item.selectFirst("a")?.attr("onclick")
if (onClick == null) {
Log.e("Hikari", "onClick attribute is null for item: $item")
return@flatMap emptyList<Pair<String, String>>()
}
val match = embedRegex.find(onClick)?.groupValues
if (match == null) {
Log.e("Hikari", "No match found for onClick: $onClick")
return@flatMap emptyList<Pair<String, String>>()
}
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
val iframeList = client.newCall(
GET(url, headers),
).execute().parseAs<List<String>>()
iframeList.map {
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
if (iframeSrc == null) {
Log.e("Hikari", "iframe src is null for URL: $url")
return@map Pair("", "")
}
Pair(iframeSrc, name)
}.filter { it.first.isNotEmpty() }
}
return sdEmbedUrls.parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second)
}.ifEmpty {
(subEmbedUrls + dubEmbedUrls).parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second)
when (embedName) {
"streamwish" -> streamwishExtractor.videosFromUrl(embed.embedFrame, videoNameGen = { "$prefix - $it" })
"filemoon" -> filemoonExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
"sv" -> savefileExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
"playerx" -> chillxExtractor.videoFromUrl(embed.embedFrame, "$prefix - ")
"hiki" -> buzzheavierExtractor.videosFromUrl(embed.embedFrame, "$prefix - ", proxyUrl)
else -> emptyList()
}
}
}
private fun getVideosFromEmbed(embedUrl: String, name: String): List<Video> = when {
name.contains("vidhide", true) -> vidHideExtractor.videosFromUrl(embedUrl, videoNameGen = { s -> "$name - $s" })
embedUrl.contains("filemoon", true) -> filemoonExtractor.videosFromUrl(embedUrl, prefix = "$name - ", headers = headers)
name.contains("streamwish", true) -> streamwishExtractor.videosFromUrl(embedUrl, prefix = "$name - ")
else -> chillxExtractor.videoFromUrl(embedUrl, referer = baseUrl, prefix = "$name - ")
}
override fun videoListSelector() = ".server-item:has(a[onclick~=getEmbed])"
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val type = preferences.getString(PREF_TYPE_KEY, PREF_TYPE_DEFAULT)!!
val hoster = preferences.getString(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.startsWith(type) },
{ it.quality.contains(quality) },
{ QUALITY_REGEX.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
{ it.quality.contains(hoster, true) },
),
).reversed()
}
override fun videoFromElement(element: Element): Video =
throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String =
throw UnsupportedOperationException()
// ============================= Utilities ==============================
@Serializable
class HtmlResponseDto(
val html: String,
val page: PageDto? = null,
) {
fun toHtml(baseUrl: String): Document = Jsoup.parseBodyFragment(html, baseUrl)
@Serializable
class PageDto(
val totalPages: Int,
)
}
companion object {
private val QUALITY_REGEX = Regex("""(\d+)p""")
private const val PREF_ENGLISH_TITLE_KEY = "preferred_title_lang"
private const val PREF_ENGLISH_TITLE_DEFAULT = true
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES.map {
"${it}p"
}.toTypedArray()
private val TYPE_LIST = arrayOf("[SUB] ", "[DUB] ", "[MULTI AUDIO] ", "[HARD-SUB] ")
private const val PREF_TYPE_KEY = "pref_type"
private const val PREF_TYPE_DEFAULT = ""
private val PREF_TYPE_VALUES = arrayOf("") + TYPE_LIST
private val PREF_TYPE_ENTRIES = arrayOf("Any") + TYPE_LIST
private val HOSTER_LIST = arrayOf("Streamwish", "Filemoon", "SV", "PlayerX", "Hiki")
private const val PREF_HOSTER_KEY = "pref_hoster"
private const val PREF_HOSTER_DEFAULT = ""
private val PREF_HOSTER_VALUES = arrayOf("") + HOSTER_LIST
private val PREF_HOSTER_ENTRIES = arrayOf("Any") + HOSTER_LIST
}
// ============================== Settings ==============================
private val SharedPreferences.getTitleLang
get() = getBoolean(PREF_ENGLISH_TITLE_KEY, PREF_ENGLISH_TITLE_DEFAULT)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_ENGLISH_TITLE_KEY
title = "Prefer english titles"
setDefaultValue(PREF_ENGLISH_TITLE_DEFAULT)
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
@ -407,13 +241,27 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
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_TYPE_KEY
title = "Preferred type"
entries = PREF_TYPE_ENTRIES
entryValues = PREF_TYPE_VALUES
setDefaultValue(PREF_TYPE_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = "Preferred hoster"
entries = PREF_HOSTER_ENTRIES
entryValues = PREF_HOSTER_VALUES
setDefaultValue(PREF_HOSTER_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
FilemoonExtractor.addSubtitlePref(screen)
SavefileExtractor.addSubtitlePref(screen)
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'JavGG'
extClass = '.Javgg'
extVersionCode = 5
extVersionCode = 6
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Jav Guru'
extClass = '.JavGuru'
extVersionCode = 26
extVersionCode = 27
isNsfw = true
}

View file

@ -3,7 +3,7 @@ ext {
extClass = '.LMAnime'
themePkg = 'animestream'
baseUrl = 'https://lmanime.com'
overrideVersionCode = 9
overrideVersionCode = 10
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'SupJav'
extClass = '.SupJavFactory'
extVersionCode = 14
extVersionCode = 15
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime4up'
extClass = '.Anime4Up'
extVersionCode = 62
extVersionCode = 63
}
apply from: "$rootDir/common.gradle"

View file

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

View file

@ -1,7 +1,7 @@
ext {
extName = 'Arab Seed'
extClass = '.ArabSeed'
extVersionCode = 17
extVersionCode = 18
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'asia2tv'
extClass = '.Asia2TV'
extVersionCode = 22
extVersionCode = 23
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Egy Dead'
extClass = '.EgyDead'
extVersionCode = 17
extVersionCode = 18
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime-Base'
extClass = '.AnimeBase'
extVersionCode = 31
extVersionCode = 32
isNsfw = true
}

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Cinemathek'
themePkg = 'dooplay'
baseUrl = 'https://cinemathek.net'
overrideVersionCode = 24
overrideVersionCode = 25
isNsfw = true
}

View file

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

View file

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

View file

@ -1,7 +1,7 @@
ext {
extName = 'Moflix-Stream'
extClass = '.MoflixStream'
extVersionCode = 15
extVersionCode = 16
}
apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AnimeKhor'
themePkg = 'animestream'
baseUrl = 'https://animekhor.org'
overrideVersionCode = 7
overrideVersionCode = 8
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Animenosub'
themePkg = 'animestream'
baseUrl = 'https://animenosub.com'
overrideVersionCode = 8
overrideVersionCode = 9
isNsfw = true
}

View file

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

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.LuciferDonghua'
themePkg = 'animestream'
baseUrl = 'https://luciferdonghua.in'
overrideVersionCode = 6
overrideVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimeFLV'
extClass = '.AnimeFlv'
extVersionCode = 64
extVersionCode = 65
}
apply from: "$rootDir/common.gradle"

View file

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

View file

@ -1,7 +1,7 @@
ext {
extName = 'Animejl'
extClass = '.Animejl'
extVersionCode = 5
extVersionCode = 6
isNsfw = true
}

View file

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

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Animenix'
themePkg = 'dooplay'
baseUrl = 'https://animenix.com'
overrideVersionCode = 9
overrideVersionCode = 10
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AnimeOnlineNinja'
themePkg = 'dooplay'
baseUrl = 'https://ww3.animeonline.ninja'
overrideVersionCode = 44
overrideVersionCode = 45
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AnimeYTES'
themePkg = 'animestream'
baseUrl = 'https://animeyt.pro'
overrideVersionCode = 9
overrideVersionCode = 10
}
apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Cineplus123'
themePkg = 'dooplay'
baseUrl = 'https://cineplus123.org'
overrideVersionCode = 6
overrideVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Cuevana'
extClass = '.CuevanaFactory'
extVersionCode = 46
extVersionCode = 47
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.DeTodoPeliculas'
themePkg = 'dooplay'
baseUrl = 'https://detodopeliculas.nu'
overrideVersionCode = 3
overrideVersionCode = 4
}
apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.FlixLatam'
themePkg = 'dooplay'
baseUrl = 'https://flixlatam.com'
overrideVersionCode = 4
overrideVersionCode = 5
}
apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

@ -1,7 +1,7 @@
ext {
extName = 'HentaiLA'
extClass = '.Hentaila'
extVersionCode = 34
extVersionCode = 35
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'HentaiTk'
extClass = '.Hentaitk'
extVersionCode = 13
extVersionCode = 14
isNsfw = true
}

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
ext {
extName = 'LACartoons'
extClass = '.Lacartoons'
extVersionCode = 11
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
ext {
extName = 'Pelisplushd'
extClass = '.PelisplushdFactory'
extVersionCode = 73
extVersionCode = 74
}
apply from: "$rootDir/common.gradle"

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.SoloLatino'
themePkg = 'dooplay'
baseUrl = 'https://sololatino.net'
overrideVersionCode = 6
overrideVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

@ -1,7 +1,7 @@
ext {
extName = 'zeroanime'
extClass = '.Zeroanime'
extVersionCode = 3
extVersionCode = 4
}
apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Hds'
themePkg = 'dooplay'
baseUrl = 'https://www.hds.quest'
overrideVersionCode = 4
overrideVersionCode = 5
}
apply from: "$rootDir/common.gradle"

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AniSAGA'
themePkg = 'dooplay'
baseUrl = 'https://www.anisaga.org'
overrideVersionCode = 17
overrideVersionCode = 18
isNsfw = false
}

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Q1N'
themePkg = 'dooplay'
baseUrl = 'https://q1n.net'
overrideVersionCode = 18
overrideVersionCode = 19
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime Srbija'
extClass = '.AnimeSrbija'
extVersionCode = 11
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

@ -1,7 +1,7 @@
ext {
extName = 'TR Anime Izle'
extClass = '.TRAnimeIzle'
extVersionCode = 21
extVersionCode = 22
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Türk Anime TV'
extClass = '.TurkAnime'
extVersionCode = 34
extVersionCode = 35
}
apply from: "$rootDir/common.gradle"