Merge branch 'main' into FixNekoSama

This commit is contained in:
mobby45 2024-08-09 12:17:41 +02:00 committed by GitHub
commit 30f7f0e6b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 1463 additions and 1571 deletions

View file

@ -8,3 +8,4 @@ Checklist:
- [ ] Have explicitly kept the `id` if a source's name or language were changed
- [ ] Have tested the modifications by compiling and running the extension through Android Studio
- [ ] Have removed `web_hi_res_512.png` when adding a new extension
- [ ] Have made sure all the icons are in png format

View file

@ -40,12 +40,26 @@ jobs:
files_separator: " "
safe_output: false
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6 # v6.1.0
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
git_user_signingkey: true
git_commit_gpgsign: true
# This step is going to commit, but this will not trigger another workflow.
- name: Bump extensions that uses a modified lib
if: steps.modified-libs.outputs.any_changed == 'true'
run: |
./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
# This step is going to commit, but this will not trigger another workflow.
- name: Bump extensions that uses a modified lib
if: steps.modified-libs.outputs.any_changed == 'true'
run: |
chmod +x ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2

View file

@ -1,19 +1,20 @@
# Aniyomi-extensions
## Guide
The source code for the extensions
just paste this into your anime repo
```
https://raw.githubusercontent.com/almightyhak/aniyomi-anime-repo/main/index.min.json
```
If your interested in installing just the apks they can be found [Here](https://github.com/almightyhak/aniyomi-anime-repo)
## Support Server
[Discord](https://discord.gg/vut4mmXQzU)
Join the [Discord](https://discord.gg/vut4mmXQzU) for updates and announcements
and please check the discord BEFORE making an issue
## Guide
## Contributing
just paste this into your anime repo https://raw.githubusercontent.com/almightyhak/aniyomi-anime-repo/main/index.min.json
i am maintaining this but keep in mind that i'm NOT a developer so expect issues to take a while to fix
If your interested in installing just the apks they can be found [Here](https://github.com/almightyhak/aniyomi-anime-repo)
[Template](https://github.com/aniyomiorg/aniyomi-extensions/blob/master/CONTRIBUTING.md)
## Contact

View file

@ -0,0 +1,5 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 2

View file

@ -0,0 +1,166 @@
package eu.kanade.tachiyomi.multisrc.anilist
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.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
abstract class AniListAnimeHttpSource : AnimeHttpSource() {
override val supportsLatest = true
val json by injectLazy<Json>()
/* =============================== Mapping AniList <> Source =============================== */
abstract fun mapAnimeDetailUrl(animeId: Int): String
abstract fun mapAnimeId(animeDetailUrl: String): Int
open fun getPreferredTitleLanguage(): TitleLanguage {
return TitleLanguage.ROMAJI
}
/* ===================================== Popular Anime ===================================== */
override fun popularAnimeRequest(page: Int): Request {
return buildAnimeListRequest(
query = ANIME_LIST_QUERY,
variables = AnimeListVariables(
page = page,
sort = AnimeListVariables.MediaSort.POPULARITY_DESC,
),
)
}
override fun popularAnimeParse(response: Response): AnimesPage {
return parseAnimeListResponse(response)
}
/* ===================================== Latest Anime ===================================== */
override fun latestUpdatesRequest(page: Int): Request {
return buildAnimeListRequest(
query = LATEST_ANIME_LIST_QUERY,
variables = AnimeListVariables(
page = page,
sort = AnimeListVariables.MediaSort.START_DATE_DESC,
),
)
}
override fun latestUpdatesParse(response: Response): AnimesPage {
return parseAnimeListResponse(response)
}
/* ===================================== Search Anime ===================================== */
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return buildAnimeListRequest(
query = ANIME_LIST_QUERY,
variables = AnimeListVariables(
page = page,
sort = AnimeListVariables.MediaSort.SEARCH_MATCH,
search = query.ifBlank { null },
),
)
}
override fun searchAnimeParse(response: Response): AnimesPage {
return parseAnimeListResponse(response)
}
/* ===================================== Anime Details ===================================== */
override fun animeDetailsRequest(anime: SAnime): Request {
return buildRequest(
query = ANIME_DETAILS_QUERY,
variables = json.encodeToString(AnimeDetailsVariables(mapAnimeId(anime.url))),
)
}
override fun animeDetailsParse(response: Response): SAnime {
val media = response.parseAs<AniListAnimeDetailsResponse>().data.media
return media.toSAnime()
}
override fun getAnimeUrl(anime: SAnime): String {
return anime.url
}
/* ==================================== AniList Utility ==================================== */
private fun buildAnimeListRequest(
query: String,
variables: AnimeListVariables,
): Request {
return buildRequest(query, json.encodeToString(variables))
}
private fun buildRequest(query: String, variables: String): Request {
val requestBody = FormBody.Builder()
.add("query", query)
.add("variables", variables)
.build()
return POST(url = "https://graphql.anilist.co", body = requestBody)
}
private fun parseAnimeListResponse(response: Response): AnimesPage {
val page = response.parseAs<AniListAnimeListResponse>().data.page
return AnimesPage(
animes = page.media.map { it.toSAnime() },
hasNextPage = page.pageInfo.hasNextPage,
)
}
private fun AniListMedia.toSAnime(): SAnime {
val otherNames = when (getPreferredTitleLanguage()) {
TitleLanguage.ROMAJI -> listOfNotNull(title.english, title.native)
TitleLanguage.ENGLISH -> listOfNotNull(title.romaji, title.native)
TitleLanguage.NATIVE -> listOfNotNull(title.romaji, title.english)
}
val newDescription = buildString {
append(
description
?.replace("<br>\n<br>", "\n")
?.replace("<.*?>".toRegex(), ""),
)
if (otherNames.isNotEmpty()) {
appendLine()
appendLine()
append("Other name(s): ${otherNames.joinToString(", ")}")
}
}
val media = this
return SAnime.create().apply {
url = mapAnimeDetailUrl(media.id)
title = parseTitle(media.title)
author = media.studios.nodes.joinToString(", ") { it.name }
description = newDescription
genre = media.genres.joinToString(", ")
status = when (media.status) {
AniListMedia.Status.RELEASING -> SAnime.ONGOING
AniListMedia.Status.FINISHED -> SAnime.COMPLETED
}
thumbnail_url = media.coverImage.large
}
}
private fun parseTitle(title: AniListMedia.Title): String {
return when (getPreferredTitleLanguage()) {
TitleLanguage.ROMAJI -> title.romaji
TitleLanguage.ENGLISH -> title.english ?: title.romaji
TitleLanguage.NATIVE -> title.native ?: title.romaji
}
}
enum class TitleLanguage {
ROMAJI,
ENGLISH,
NATIVE,
}
}

View file

@ -0,0 +1,73 @@
package eu.kanade.tachiyomi.multisrc.anilist
import kotlinx.serialization.Serializable
internal const val MEDIA_QUERY = """
id
title {
romaji
english
native
}
coverImage {
large
}
description
status
genres
studios(isMain: true) {
nodes {
name
}
}
"""
internal const val ANIME_LIST_QUERY = """
query (${"$"}page: Int, ${"$"}sort: [MediaSort], ${"$"}search: String) {
Page(page: ${"$"}page, perPage: 30) {
pageInfo {
hasNextPage
}
media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in: [RELEASING, FINISHED], countryOfOrigin: "JP", isAdult: false) {
$MEDIA_QUERY
}
}
}
"""
internal const val LATEST_ANIME_LIST_QUERY = """
query (${"$"}page: Int, ${"$"}sort: [MediaSort], ${"$"}search: String) {
Page(page: ${"$"}page, perPage: 30) {
pageInfo {
hasNextPage
}
media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in: [RELEASING, FINISHED], countryOfOrigin: "JP", isAdult: false, startDate_greater: 1, episodes_greater: 1) {
$MEDIA_QUERY
}
}
}
"""
internal const val ANIME_DETAILS_QUERY = """
query (${"$"}id: Int) {
Media(id: ${"$"}id) {
$MEDIA_QUERY
}
}
"""
@Serializable
internal data class AnimeListVariables(
val page: Int,
val sort: MediaSort,
val search: String? = null,
) {
enum class MediaSort {
POPULARITY_DESC,
SEARCH_MATCH,
START_DATE_DESC,
}
}
@Serializable
internal data class AnimeDetailsVariables(val id: Int)

View file

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.multisrc.anilist
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class AniListAnimeListResponse(val data: Data) {
@Serializable
data class Data(@SerialName("Page") val page: Page) {
@Serializable
data class Page(
val pageInfo: PageInfo,
val media: List<AniListMedia>,
) {
@Serializable
data class PageInfo(val hasNextPage: Boolean)
}
}
}
@Serializable
internal data class AniListAnimeDetailsResponse(val data: Data) {
@Serializable
data class Data(@SerialName("Media") val media: AniListMedia)
}
@Serializable
internal data class AniListMedia(
val id: Int,
val title: Title,
val coverImage: CoverImage,
val description: String?,
val status: Status,
val genres: List<String>,
val studios: Studios,
) {
@Serializable
data class Title(
val romaji: String,
val english: String?,
val native: String?,
)
@Serializable
data class CoverImage(val large: String)
enum class Status {
RELEASING,
FINISHED,
}
@Serializable
data class Studios(val nodes: List<Node>) {
@Serializable
data class Node(val name: String)
}
}

View file

@ -1,18 +1,19 @@
package eu.kanade.tachiyomi.lib.vidsrcextractor
import android.util.Base64
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.lib.vidsrcextractor.MediaResponseBody.Result
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import okhttp3.CacheControl
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.net.URLDecoder
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
@ -21,54 +22,32 @@ import javax.crypto.spec.SecretKeySpec
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val json: Json by injectLazy()
private val cacheControl = CacheControl.Builder().noStore().build()
private val noCacheClient = client.newBuilder()
.cache(null)
.build()
private val keys by lazy {
noCacheClient.newCall(
GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
).execute().parseAs<List<String>>()
}
fun videosFromUrl(embedLink: String, hosterName: String, type: String = "", subtitleList: List<Track> = emptyList()): List<Video> {
fun videosFromUrl(
embedLink: String,
hosterName: String,
type: String = "",
subtitleList: List<Track> = emptyList(),
): List<Video> {
val host = embedLink.toHttpUrl().host
val apiUrl = getApiUrl(embedLink, keys)
val apiUrl = getApiUrl(embedLink)
val apiHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Host", host)
add("Referer", URLDecoder.decode(embedLink, "UTF-8"))
add("X-Requested-With", "XMLHttpRequest")
}.build()
val response = client.newCall(GET(apiUrl)).execute()
val data = response.parseAs<MediaResponseBody>()
val response = client.newCall(
GET(apiUrl, apiHeaders),
).execute()
val data = runCatching {
response.parseAs<MediaResponseBody>()
}.getOrElse { // Keys are out of date
val newKeys = noCacheClient.newCall(
GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
).execute().parseAs<List<String>>()
val newApiUrL = getApiUrl(embedLink, newKeys)
client.newCall(
GET(newApiUrL, apiHeaders),
).execute().parseAs()
}
val decrypted = vrfDecrypt(data.result)
val result = json.decodeFromString<Result>(decrypted)
return playlistUtils.extractFromHls(
data.result.sources.first().file,
playlistUrl = result.sources.first().file,
referer = "https://$host/",
videoNameGen = { q -> hosterName + (if (type.isBlank()) "" else " - $type") + " - $q" },
subtitleList = subtitleList + data.result.tracks.toTracks(),
subtitleList = subtitleList + result.tracks.toTracks(),
)
}
private fun getApiUrl(embedLink: String, keyList: List<String>): String {
private fun getApiUrl(embedLink: String): String {
val host = embedLink.toHttpUrl().host
val params = embedLink.toHttpUrl().let { url ->
url.queryParameterNames.map {
@ -76,13 +55,13 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
}
}
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
val encodedID = encodeID(vidId, keyList)
val apiSlug = callFromFuToken(host, encodedID, embedLink)
val apiSlug = encodeID(vidId, ENCRYPTION_KEY1)
val h = encodeID(vidId, ENCRYPTION_KEY2)
return buildString {
append("https://")
append(host)
append("/")
append("/mediainfo/")
append(apiSlug)
if (params.isNotEmpty()) {
append("?")
@ -91,51 +70,23 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
"${it.first}=${it.second}"
},
)
append("&h=$h")
}
}
}
private fun encodeID(videoID: String, keyList: List<String>): String {
val rc4Key1 = SecretKeySpec(keyList[0].toByteArray(), "RC4")
val rc4Key2 = SecretKeySpec(keyList[1].toByteArray(), "RC4")
val cipher1 = Cipher.getInstance("RC4")
val cipher2 = Cipher.getInstance("RC4")
cipher1.init(Cipher.DECRYPT_MODE, rc4Key1, cipher1.parameters)
cipher2.init(Cipher.DECRYPT_MODE, rc4Key2, cipher2.parameters)
var encoded = videoID.toByteArray()
encoded = cipher1.doFinal(encoded)
encoded = cipher2.doFinal(encoded)
encoded = Base64.encode(encoded, Base64.DEFAULT)
return encoded.toString(Charsets.UTF_8).replace("/", "_").trim()
private fun encodeID(videoID: String, key: String): String {
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
return Base64.encode(cipher.doFinal(videoID.toByteArray()), Base64.DEFAULT)
.toString(Charsets.UTF_8)
.replace("+", "-")
.replace("/", "_")
.trim()
}
private fun callFromFuToken(host: String, data: String, embedLink: String): String {
val refererHeaders = headers.newBuilder().apply {
add("Referer", embedLink)
}.build()
val fuTokenScript = client.newCall(
GET("https://$host/futoken", headers = refererHeaders),
).execute().body.string()
val js = buildString {
append("(function")
append(
fuTokenScript.substringAfter("window")
.substringAfter("function")
.replace("jQuery.ajax(", "")
.substringBefore("+location.search"),
)
append("}(\"$data\"))")
}
return QuickJs.create().use {
it.evaluate(js)?.toString()!!
}
}
private fun List<MediaResponseBody.Result.SubTrack>.toTracks(): List<Track> {
private fun List<Result.SubTrack>.toTracks(): List<Track> {
return filter {
it.kind == "captions"
}.mapNotNull {
@ -147,17 +98,32 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
}.getOrNull()
}
}
private fun vrfDecrypt(input: String): String {
var vrf = Base64.decode(input.toByteArray(), Base64.URL_SAFE)
val rc4Key = SecretKeySpec(DECRYPTION_KEY.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")
}
companion object {
private const val ENCRYPTION_KEY1 = "8Qy3mlM2kod80XIK"
private const val ENCRYPTION_KEY2 = "BgKVSrzpH2Enosgm"
private const val DECRYPTION_KEY = "9jXDYBZUcTcTZveM"
}
}
@Serializable
data class MediaResponseBody(
val status: Int,
val result: Result,
val result: String,
) {
@Serializable
data class Result(
val sources: ArrayList<Source>,
val tracks: ArrayList<SubTrack> = ArrayList(),
val sources: List<Source>,
val tracks: List<SubTrack> = emptyList(),
) {
@Serializable
data class Source(

View file

@ -0,0 +1,73 @@
package eu.kanade.tachiyomi.lib.voeextractor
import android.webkit.CookieManager
import eu.kanade.tachiyomi.network.GET
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
class DdosGuardInterceptor(private val client: OkHttpClient) : Interceptor {
private val cookieManager by lazy { CookieManager.getInstance() }
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val response = chain.proceed(originalRequest)
// Check if DDos-GUARD is on
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
return response
}
response.close()
val cookies = cookieManager.getCookie(originalRequest.url.toString())
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(originalRequest.url, it) }
} else {
emptyList()
}
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
if (!ddg2Cookie?.value.isNullOrEmpty()) {
return chain.proceed(originalRequest)
}
val newCookie = getNewCookie(originalRequest.url) ?: return chain.proceed(originalRequest)
val newCookieHeader = buildString {
(oldCookie + newCookie).forEachIndexed { index, cookie ->
if (index > 0) append("; ")
append(cookie.name).append('=').append(cookie.value)
}
}
return chain.proceed(originalRequest.newBuilder().addHeader("cookie", newCookieHeader).build())
}
fun getNewCookie(url: HttpUrl): Cookie? {
val cookies = cookieManager.getCookie(url.toString())
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
} else {
emptyList()
}
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
if (!ddg2Cookie?.value.isNullOrEmpty()) {
return ddg2Cookie
}
val wellKnown = client.newCall(GET("https://check.ddos-guard.net/check.js"))
.execute().body.string()
.substringAfter("'", "")
.substringBefore("'", "")
val checkUrl = "${url.scheme}://${url.host + wellKnown}"
return client.newCall(GET(checkUrl)).execute().header("set-cookie")?.let {
Cookie.parse(url, it)
}
}
companion object {
private val ERROR_CODES = listOf(403)
private val SERVER_CHECK = listOf("ddos-guard")
}
}

View file

@ -14,7 +14,9 @@ class VoeExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
private val playlistUtils by lazy { PlaylistUtils(client) }
private val clientDdos by lazy { client.newBuilder().addInterceptor(DdosGuardInterceptor(client)).build() }
private val playlistUtils by lazy { PlaylistUtils(clientDdos) }
private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex()
@ -24,7 +26,16 @@ class VoeExtractor(private val client: OkHttpClient) {
data class VideoLinkDTO(val file: String)
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
var document = clientDdos.newCall(GET(url)).execute().asJsoup()
if (document.selectFirst("script")?.data()?.contains("if (typeof localStorage !== 'undefined')") == true) {
val originalUrl = document.selectFirst("script")?.data()
?.substringAfter("window.location.href = '")
?.substringBefore("';") ?: return emptyList()
document = clientDdos.newCall(GET(originalUrl)).execute().asJsoup()
}
val script = document.selectFirst("script:containsData(const sources), script:containsData(var sources), script:containsData(wc0)")
?.data()
?: return emptyList()
@ -43,7 +54,7 @@ class VoeExtractor(private val client: OkHttpClient) {
else -> return emptyList()
}
return playlistUtils.extractFromHls(playlistUrl,
videoNameGen = { quality -> "${prefix}Voe: $quality" }
videoNameGen = { quality -> "${prefix}Voe:$quality" }
)
}
}

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

View file

@ -1,398 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animeflix
import android.app.Application
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MultipartBody
import okhttp3.Request
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 AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeFlix"
override val baseUrl = "https://animeflix.mobi"
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page/")
override fun popularAnimeSelector() = "div#content_box > div.post-cards > article"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
// prevent base64 images
thumbnail_url = element.selectFirst("img")!!.run {
attr("data-pagespeed-high-res-src").ifEmpty { attr("src") }
}
title = element.selectFirst("header")!!.text()
}
override fun popularAnimeNextPageSelector() = "div.nav-links > a.next"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
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 cleanQuery = query.replace(" ", "+").lowercase()
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
val subpageFilter = filterList.find { it is SubPageFilter } as SubPageFilter
return when {
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers)
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers)
subpageFilter.state != 0 -> GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers)
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter(),
SubPageFilter(),
)
private class GenreFilter : UriPartFilter(
"Genres",
arrayOf(
Pair("<select>", ""),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Isekai", "isekai"),
Pair("Drama", "drama"),
Pair("Psychological", "psychological"),
Pair("Ecchi", "ecchi"),
Pair("Sci-Fi", "sci-fi"),
Pair("Magic", "magic"),
Pair("Slice Of Life", "slice-of-life"),
Pair("Sports", "sports"),
Pair("Comedy", "comedy"),
Pair("Fantasy", "fantasy"),
Pair("Horror", "horror"),
Pair("Yaoi", "yaoi"),
),
)
private class SubPageFilter : UriPartFilter(
"Sub-page",
arrayOf(
Pair("<select>", ""),
Pair("Ongoing", "ongoing"),
Pair("Latest Release", "latest-release"),
Pair("Movies", "movies"),
),
)
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
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("div.single_post > header > h1")!!.text()
thumbnail_url = document.selectFirst("img.imdbwp__img")?.attr("src")
val infosDiv = document.selectFirst("div.thecontent h3:contains(Anime Info) ~ ul")!!
status = when (infosDiv.getInfo("Status").toString()) {
"Completed" -> SAnime.COMPLETED
"Currently Airing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
artist = infosDiv.getInfo("Studios")
author = infosDiv.getInfo("Producers")
genre = infosDiv.getInfo("Genres")
val animeInfo = infosDiv.select("li").joinToString("\n") { it.text() }
description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)")
.joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
}
private fun Element.getInfo(info: String) =
selectFirst("li:contains($info)")?.ownText()?.trim()
// ============================== Episodes ==============================
val seasonRegex by lazy { Regex("""season (\d+)""", RegexOption.IGNORE_CASE) }
val qualityRegex by lazy { """(\d+)p""".toRegex() }
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
val document = client.newCall(GET(baseUrl + anime.url)).execute()
.asJsoup()
val seasonList = document.select("div.inline > h3:contains(Season),div.thecontent > h3:contains(Season)")
val episodeList = if (seasonList.distinctBy { seasonRegex.find(it.text())!!.groupValues[1] }.size > 1) {
val seasonsLinks = document.select("div.thecontent p:has(span:contains(Gdrive))").groupBy {
seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
}
seasonsLinks.flatMap { (seasonNumber, season) ->
val serverListSeason = season.map {
val previousText = it.previousElementSibling()!!.text()
val quality = qualityRegex.find(previousText)?.groupValues?.get(1) ?: "Unknown quality"
val url = it.selectFirst("a")!!.attr("href")
val episodesDocument = client.newCall(GET(url)).execute()
.asJsoup()
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(quality, it.attr("href"), "Season $seasonNumber ${it.text()}")
}
}
transposeEpisodes(serverListSeason)
}
} else {
val driveList = document.select("div.thecontent p:has(span:contains(Gdrive))").map {
val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
Pair(it.selectFirst("a")!!.attr("href"), quality)
}
// Load episodes
val serversList = driveList.map { drive ->
val episodesDocument = client.newCall(GET(drive.first)).execute()
.asJsoup()
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(drive.second, it.attr("href"), it.text())
}
}
transposeEpisodes(serversList)
}
return episodeList.reversed()
}
private fun transposeEpisodes(serversList: List<List<EpUrl>>) =
transpose(serversList).mapIndexed { index, serverList ->
SEpisode.create().apply {
name = serverList.first().name
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(json.encodeToString(serverList))
}
}
override fun episodeListSelector(): String = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val urls = json.decodeFromString<List<EpUrl>>(episode.url)
val leechUrls = urls.map {
val firstLeech = client.newCall(GET(it.url)).execute()
.asJsoup()
.selectFirst("script:containsData(downlaod_button)")!!
.data()
.substringAfter("<a href=\"")
.substringBefore("\">")
val path = client.newCall(GET(firstLeech)).execute()
.body.string()
.substringAfter("replace(\"")
.substringBefore("\"")
val link = "https://" + firstLeech.toHttpUrl().host + path
EpUrl(it.quality, link, it.name)
}
val videoList = leechUrls.parallelCatchingFlatMap { url ->
if (url.url.toHttpUrl().encodedPath == "/404") return@parallelCatchingFlatMap emptyList()
val (videos, mediaUrl) = extractVideo(url)
when {
videos.isEmpty() -> {
extractGDriveLink(mediaUrl, url.quality).ifEmpty {
getDirectLink(mediaUrl, "instant", "/mfile/")?.let {
listOf(Video(it, "${url.quality}p - GDrive Instant link", it))
} ?: emptyList()
}
}
else -> videos
}
}
return videoList.sort()
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ==============================
// https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
val matchResult = qualityRegex.find(epUrl.name)
val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
return (1..3).toList().flatMap { type ->
extractWorkerLinks(epUrl.url, quality, type)
}.let { Pair(it, epUrl.url) }
}
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
val resp = client.newCall(GET(reqLink)).execute().asJsoup()
val sizeMatch = SIZE_REGEX.find(resp.select("div.card-header").text().trim())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
val link = linkElement.attr("href")
val decodedLink = if (link.contains("workers.dev")) {
link
} else {
String(Base64.decode(link.substringAfter("download?url="), Base64.DEFAULT))
}
Video(
url = decodedLink,
quality = "${quality}p - CF $type Worker ${index + 1}$size",
videoUrl = decodedLink,
)
}
}
private fun getDirectLink(url: String, action: String = "direct", newPath: String = "/file/"): String? {
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
val script = doc.selectFirst("script:containsData(async function taskaction)")
?.data()
?: return url
val key = script.substringAfter("key\", \"").substringBefore('"')
val form = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("action", action)
.addFormDataPart("key", key)
.addFormDataPart("action_token", "")
.build()
val headers = headersBuilder().set("x-token", url.toHttpUrl().host).build()
val req = client.newCall(POST(url.replace("/file/", newPath), headers, form)).execute()
return runCatching {
json.decodeFromString<DriveLeechDirect>(req.body.string()).url
}.getOrNull()
}
private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> {
val neoUrl = getDirectLink(mediaUrl) ?: mediaUrl
val response = client.newCall(GET(neoUrl)).execute().asJsoup()
val gdBtn = response.selectFirst("div.card-body a.btn")!!
val gdLink = gdBtn.attr("href")
val sizeMatch = SIZE_REGEX.find(gdBtn.text())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup()
val link = gdResponse.select("form#download-form")
return if (link.isNullOrEmpty()) {
emptyList()
} else {
val realLink = link.attr("action")
listOf(Video(realLink, "$quality - Gdrive$size", realLink))
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun <E> transpose(xs: List<List<E>>): List<List<E>> {
// Helpers
fun <E> List<E>.head(): E = this.first()
fun <E> List<E>.tail(): List<E> = this.takeLast(this.size - 1)
fun <E> E.append(xs: List<E>): List<E> = listOf(this).plus(xs)
xs.filter { it.isNotEmpty() }.let { ys ->
return when (ys.isNotEmpty()) {
true -> ys.map { it.head() }.append(transpose(ys.map { it.tail() }))
else -> emptyList()
}
}
}
@Serializable
data class EpUrl(
val quality: String,
val url: String,
val name: String,
)
@Serializable
data class DriveLeechDirect(val url: String? = null)
companion object {
private val SIZE_REGEX = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
private const val PREF_QUALITY_KEY = "pref_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")
}
// ============================== 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)
}
}

View file

@ -1,12 +0,0 @@
ext {
extName = 'Animeflix.live'
extClass = '.AnimeflixLive'
extVersionCode = 4
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:gogostream-extractor'))
implementation(project(':lib:playlist-utils'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,503 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
import GenreFilter
import SortFilter
import SubPageFilter
import TypeFilter
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.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.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URLDecoder
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import kotlin.math.min
class AnimeflixLive : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Animeflix.live"
override val baseUrl by lazy { preferences.baseUrl }
private val apiUrl by lazy { preferences.apiUrl }
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 apiHeaders = headersBuilder().apply {
add("Accept", "*/*")
add("Host", apiUrl.toHttpUrl().host)
add("Origin", baseUrl)
add("Referer", "$baseUrl/")
}.build()
private val docHeaders = headersBuilder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
add("Host", apiUrl.toHttpUrl().host)
add("Referer", "$baseUrl/")
}.build()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request =
GET("$apiUrl/popular?page=${page - 1}", apiHeaders)
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<List<AnimeDto>>()
val titlePref = preferences.titleType
val animeList = parsed.map {
it.toSAnime(titlePref)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$apiUrl/trending?page=${page - 1}", apiHeaders)
override fun latestUpdatesParse(response: Response): AnimesPage {
val parsed = response.parseAs<TrendingDto>()
val titlePref = preferences.titleType
val animeList = parsed.trending.map {
it.toSAnime(titlePref)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val sort = filters.filterIsInstance<SortFilter>().first().getValue()
val type = filters.filterIsInstance<TypeFilter>().first().getValues()
val genre = filters.filterIsInstance<GenreFilter>().first().getValues()
val subPage = filters.filterIsInstance<SubPageFilter>().first().getValue()
if (subPage.isNotBlank()) {
return GET("$apiUrl/$subPage?page=${page - 1}", apiHeaders)
}
if (query.isEmpty()) {
throw Exception("Search must not be empty")
}
val filtersObj = buildJsonObject {
put("sort", sort)
if (type.isNotEmpty()) {
put("type", json.encodeToString(type))
}
if (genre.isNotEmpty()) {
put("genre", json.encodeToString(genre))
}
}.toJsonString()
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("info")
addPathSegment("")
addQueryParameter("query", query)
addQueryParameter("limit", "15")
addQueryParameter("filters", filtersObj)
addQueryParameter("k", query.substr(0, 3).sk())
}.build()
return GET(url, apiHeaders)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<List<AnimeDto>>()
val titlePref = preferences.titleType
val animeList = parsed.map {
it.toSAnime(titlePref)
}
val hasNextPage = if (response.request.url.queryParameter("limit") == null) {
animeList.size == 44
} else {
animeList.size == 15
}
return AnimesPage(animeList, hasNextPage)
}
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
SortFilter(),
TypeFilter(),
GenreFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("NOTE: Subpage overrides search and other filters!"),
SubPageFilter(),
)
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request {
return GET("$apiUrl/getslug/${anime.url}", apiHeaders)
}
override fun getAnimeUrl(anime: SAnime): String {
return "$baseUrl/search/${anime.title}?anime=${anime.url}"
}
override fun animeDetailsParse(response: Response): SAnime {
val titlePref = preferences.titleType
return response.parseAs<DetailsDto>().toSAnime(titlePref)
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val lang = preferences.lang
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("episodes")
addQueryParameter("id", anime.url)
addQueryParameter("dub", (lang == "Dub").toString())
addQueryParameter("c", anime.url.sk())
}.build()
return GET(url, apiHeaders)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val slug = response.request.url.queryParameter("id")!!
return response.parseAs<EpisodeResponseDto>().episodes.map {
it.toSEpisode(slug)
}.sortedByDescending { it.episode_number }
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val url = "$apiUrl${episode.url}".toHttpUrl().newBuilder().apply {
addQueryParameter("server", "")
addQueryParameter("c", episode.url.substringAfter("/watch/").sk())
}.build()
return GET(url, apiHeaders)
}
override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>()
val initialPlayerUrl = apiUrl + response.parseAs<ServerDto>().source
val initialServer = initialPlayerUrl.toHttpUrl().queryParameter("server")!!
val initialPlayerDocument = client.newCall(
GET(initialPlayerUrl, docHeaders),
).execute().asJsoup().unescape()
videoList.addAll(
videosFromPlayer(
initialPlayerDocument,
initialServer.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
),
)
// Go through rest of servers
val servers = initialPlayerDocument.selectFirst("script:containsData(server-settings)")!!.data()
val serversHtml = SERVER_REGEX.findAll(servers).map {
Jsoup.parseBodyFragment(it.groupValues[1])
}.toList()
videoList.addAll(
serversHtml.parallelCatchingFlatMapBlocking {
val server = serverMapping[
it.selectFirst("button")!!
.attr("onclick")
.substringAfter("postMessage('")
.substringBefore("'"),
]
if (server == initialServer) {
return@parallelCatchingFlatMapBlocking emptyList()
}
val serverUrl = response.request.url.newBuilder()
.setQueryParameter("server", server)
.build()
val playerUrl = apiUrl + client.newCall(
GET(serverUrl, apiHeaders),
).execute().parseAs<ServerDto>().source
if (server != playerUrl.toHttpUrl().queryParameter("server")!!) {
return@parallelCatchingFlatMapBlocking emptyList()
}
val playerDocument = client.newCall(
GET(playerUrl, docHeaders),
).execute().asJsoup().unescape()
videosFromPlayer(
playerDocument,
server.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
)
},
)
return videoList
}
private val serverMapping = mapOf(
"settings-0" to "moon",
"settings-1" to "sun",
"settings-2" to "zoro",
"settings-3" to "gogo",
)
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private fun getVideoHeaders(baseHeaders: Headers, referer: String, videoUrl: String): Headers {
return baseHeaders.newBuilder().apply {
add("Accept", "*/*")
add("Accept-Language", "en-US,en;q=0.5")
add("Host", videoUrl.toHttpUrl().host)
add("Origin", "https://${apiUrl.toHttpUrl().host}")
add("Referer", "$apiUrl/")
add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "cross-site")
}.build()
}
private fun Document.unescape(): Document {
val unescapeScript = this.selectFirst("script:containsData(unescape)")
return if (unescapeScript == null) {
this
} else {
val data = URLDecoder.decode(unescapeScript.data(), "UTF-8")
Jsoup.parse(data, this.location())
}
}
private fun videosFromPlayer(document: Document, name: String): List<Video> {
val dataScript = document.selectFirst("script:containsData(m3u8)")
?.data() ?: return emptyList()
val subtitleList = document.select("video > track[kind=captions]").map {
Track(it.attr("id"), it.attr("label"))
}
var masterPlaylist = M3U8_REGEX.find(dataScript)?.groupValues?.get(1)
?: return emptyList()
if (name.equals("moon", true)) {
masterPlaylist += dataScript.substringAfter("`${'$'}{url}")
.substringBefore("`")
}
return playlistUtils.extractFromHls(
masterPlaylist,
videoHeadersGen = ::getVideoHeaders,
videoNameGen = { q -> "$name - $q" },
subtitleList = subtitleList,
)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.quality
val server = preferences.server
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(server, true) },
),
).reversed()
}
private fun JsonObject.toJsonString(): String {
return json.encodeToString(this)
}
private fun String.sk(): String {
val t = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val n = 17 + (t.get(Calendar.DAY_OF_MONTH) - t.get(Calendar.MONTH)) / 2
return this.toCharArray().fold("") { acc, c ->
acc + c.code.toString(n).padStart(2, '0')
}
}
private fun String.substr(start: Int, end: Int): String {
val stop = min(end, this.length)
return this.substring(start, stop)
}
companion object {
private val SERVER_REGEX = Regex("""'1' === '1'.*?(<button.*?</button>)""", RegexOption.DOT_MATCHES_ALL)
private val M3U8_REGEX = Regex("""const ?\w*? ?= ?`(.*?)`""")
private const val PAGE_SIZE = 24
private const val PREF_DOMAIN_KEY = "pref_domain_key"
private const val PREF_DOMAIN_DEFAULT = "https://animeflix.live,https://api.animeflix.dev"
private val PREF_DOMAIN_ENTRIES = arrayOf("animeflix.live", "animeflix.ro")
private val PREF_DOMAIN_ENTRY_VALUES = arrayOf(
"https://animeflix.live,https://api.animeflix.dev",
"https://animeflix.ro,https://api.animeflixtv.to",
)
private const val PREF_TITLE_KEY = "pref_title_type_key"
private const val PREF_TITLE_DEFAULT = "English"
private val PREF_TITLE_ENTRIES = arrayOf("English", "Native", "Romaji")
private const val PREF_LANG_KEY = "pref_lang_key"
private const val PREF_LANG_DEFAULT = "Sub"
private val PREF_LANG_ENTRIES = arrayOf("Sub", "Dub")
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360")
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_ENTRY_VALUES.map { "${it}p" }.toTypedArray()
private const val PREF_SERVER_KEY = "pref_server_key"
private const val PREF_SERVER_DEFAULT = "Moon"
private val PREF_SERVER_ENTRIES = arrayOf("Moon", "Sun", "Zoro", "Gogo")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = "Preferred domain (requires app restart)"
entries = PREF_DOMAIN_ENTRIES
entryValues = PREF_DOMAIN_ENTRY_VALUES
setDefaultValue(PREF_DOMAIN_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_TITLE_KEY
title = "Preferred Title Type"
entries = PREF_TITLE_ENTRIES
entryValues = PREF_TITLE_ENTRIES
setDefaultValue(PREF_TITLE_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_LANG_KEY
title = "Preferred Language"
entries = PREF_LANG_ENTRIES
entryValues = PREF_LANG_ENTRIES
setDefaultValue(PREF_LANG_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_QUALITY_KEY
title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRY_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 = "Preferred server"
entries = PREF_SERVER_ENTRIES
entryValues = PREF_SERVER_ENTRIES
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)
}
private val SharedPreferences.baseUrl
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
.split(",").first()
private val SharedPreferences.apiUrl
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
.split(",").last()
private val SharedPreferences.titleType
get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!
private val SharedPreferences.lang
get() = getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
private val SharedPreferences.quality
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.server
get() = getString(PREF_SERVER_KEY, PREF_QUALITY_DEFAULT)!!
}

View file

@ -1,123 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import kotlin.math.ceil
import kotlin.math.floor
@Serializable
class TrendingDto(
val trending: List<AnimeDto>,
)
@Serializable
class AnimeDto(
val slug: String,
@SerialName("title") val titleObj: TitleObject,
val images: ImageObject,
) {
@Serializable
class TitleObject(
val english: String? = null,
val native: String? = null,
val romaji: String? = null,
)
@Serializable
class ImageObject(
val large: String? = null,
val medium: String? = null,
val small: String? = null,
)
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
}
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
url = slug
}
}
@Serializable
class DetailsDto(
val slug: String,
@SerialName("title") val titleObj: TitleObject,
val description: String,
val genres: List<String>,
val status: String? = null,
val images: ImageObject,
) {
@Serializable
class TitleObject(
val english: String? = null,
val native: String? = null,
val romaji: String? = null,
)
@Serializable
class ImageObject(
val large: String? = null,
val medium: String? = null,
val small: String? = null,
)
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
}
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
url = slug
genre = genres.joinToString()
status = this@DetailsDto.status.parseStatus()
description = Jsoup.parseBodyFragment(
this@DetailsDto.description.replace("<br>", "br2n"),
).text().replace("br2n", "\n")
}
private fun String?.parseStatus(): Int = when (this?.lowercase()) {
"releasing" -> SAnime.ONGOING
"finished" -> SAnime.COMPLETED
"cancelled" -> SAnime.CANCELLED
else -> SAnime.UNKNOWN
}
}
@Serializable
class EpisodeResponseDto(
val episodes: List<EpisodeDto>,
) {
@Serializable
class EpisodeDto(
val number: Float,
val title: String? = null,
) {
fun toSEpisode(slug: String): SEpisode = SEpisode.create().apply {
val epNum = if (floor(number) == ceil(number)) {
number.toInt().toString()
} else {
number.toString()
}
url = "/watch/$slug-episode-$epNum"
episode_number = number
name = if (title == null) {
"Episode $epNum"
} else {
"Ep. $epNum - $title"
}
}
}
}
@Serializable
class ServerDto(
val source: String,
)

View file

@ -1,81 +0,0 @@
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
open class UriPartFilter(
name: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : AnimeFilter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
fun getValue(): String {
return vals[state].second
}
}
open class UriMultiSelectOption(name: String, val value: String) : AnimeFilter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val vals: Array<Pair<String, String>>,
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }) {
fun getValues(): List<String> {
return state.filter { it.state }.map { it.value }
}
}
class SortFilter : UriPartFilter(
"Sort",
arrayOf(
Pair("Recently Updated", "recently_updated"),
Pair("Recently Added", "recently_added"),
Pair("Release Date ↓", "release_date_down"),
Pair("Release Date ↑", "release_date_up"),
Pair("Name A-Z", "title_az"),
Pair("Best Rating", "scores"),
Pair("Most Watched", "most_watched"),
Pair("Anime Length", "number_of_episodes"),
),
)
class TypeFilter : UriMultiSelectFilter(
"Type",
arrayOf(
Pair("TV", "TV"),
Pair("Movie", "MOVIE"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Special", "SPECIAL"),
),
)
class GenreFilter : UriMultiSelectFilter(
"Genre",
arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Comedy", "Comedy"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Horror", "Horror"),
Pair("Mecha", "Mecha"),
Pair("Mystery", "Mystery"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Sports", "Sports"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
),
)
class SubPageFilter : UriPartFilter(
"Sub-page",
arrayOf(
Pair("<select>", ""),
Pair("Movies", "movies"),
Pair("Series", "series"),
),
)

View file

@ -0,0 +1,13 @@
ext {
extName = 'AniPlay'
extClass = '.AniPlay'
themePkg = 'anilist'
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib-multisrc:anilist"))
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,375 @@
package eu.kanade.tachiyomi.animeextension.en.aniplay
import android.app.Application
import android.util.Base64
import android.widget.Toast
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
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.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.multisrc.anilist.AniListAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "AniPlay"
override val lang = "en"
override val baseUrl: String
get() = "https://${preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)}"
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
/* ================================= AniList configurations ================================= */
override fun mapAnimeDetailUrl(animeId: Int): String {
return "$baseUrl/anime/info/$animeId"
}
override fun mapAnimeId(animeDetailUrl: String): Int {
val httpUrl = animeDetailUrl.toHttpUrl()
return httpUrl.pathSegments[2].toInt()
}
override fun getPreferredTitleLanguage(): TitleLanguage {
val preferredLanguage = preferences.getString(PREF_TITLE_LANGUAGE_KEY, PREF_TITLE_LANGUAGE_DEFAULT)
return when (preferredLanguage) {
"romaji" -> TitleLanguage.ROMAJI
"english" -> TitleLanguage.ENGLISH
"native" -> TitleLanguage.NATIVE
else -> TitleLanguage.ROMAJI
}
}
/* ====================================== Episode List ====================================== */
override fun episodeListRequest(anime: SAnime): Request {
val httpUrl = anime.url.toHttpUrl()
val animeId = httpUrl.pathSegments[2]
return GET("$baseUrl/api/anime/episode/$animeId")
}
override fun episodeListParse(response: Response): List<SEpisode> {
val isMarkFiller = preferences.getBoolean(PREF_MARK_FILLER_EPISODE_KEY, PREF_MARK_FILLER_EPISODE_DEFAULT)
val episodeListUrl = response.request.url
val animeId = episodeListUrl.pathSegments[3]
val providers = response.parseAs<List<EpisodeListResponse>>()
val episodes = mutableMapOf<Int, EpisodeListResponse.Episode>()
val episodeExtras = mutableMapOf<Int, List<EpisodeExtra>>()
providers.forEach { provider ->
provider.episodes.forEach { episode ->
if (!episodes.containsKey(episode.number)) {
episodes[episode.number] = episode
}
val existingEpisodeExtras = episodeExtras.getOrElse(episode.number) { emptyList() }
val episodeExtra = EpisodeExtra(
source = provider.providerId,
episodeId = episode.id,
hasDub = episode.hasDub,
)
episodeExtras[episode.number] = existingEpisodeExtras + listOf(episodeExtra)
}
}
return episodes.map { episodeMap ->
val episode = episodeMap.value
val episodeNumber = episode.number
val episodeExtra = episodeExtras.getValue(episodeNumber)
val episodeExtraString = json.encodeToString(episodeExtra)
.let { Base64.encode(it.toByteArray(), Base64.DEFAULT) }
.toString(Charsets.UTF_8)
val url = baseUrl.toHttpUrl().newBuilder()
.addPathSegment("anime")
.addPathSegment("watch")
.addQueryParameter("id", animeId)
.addQueryParameter("ep", episodeNumber.toString())
.addQueryParameter("extras", episodeExtraString)
.build()
val name = parseEpisodeName(episodeNumber, episode.title)
val uploadDate = parseDate(episode.createdAt)
val dub = when {
episodeExtra.any { it.hasDub } -> ", Dub"
else -> ""
}
val filler = when {
episode.isFiller && isMarkFiller -> " • Filler Episode"
else -> ""
}
val scanlator = "Sub$dub$filler"
SEpisode.create().apply {
this.url = url.toString()
this.name = name
this.date_upload = uploadDate
this.episode_number = episodeNumber.toFloat()
this.scanlator = scanlator
}
}.reversed()
}
/* ======================================= Video List ======================================= */
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val episodeUrl = episode.url.toHttpUrl()
val animeId = episodeUrl.queryParameter("id") ?: return emptyList()
val episodeNum = episodeUrl.queryParameter("ep") ?: return emptyList()
val extras = episodeUrl.queryParameter("extras")
?.let {
Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8)
}
?.let { json.decodeFromString<List<EpisodeExtra>>(it) }
?: emptyList()
val episodeDataList = extras.parallelFlatMapBlocking { extra ->
val languages = mutableListOf("sub")
if (extra.hasDub) {
languages.add("dub")
}
val url = "$baseUrl/api/anime/source/$animeId"
languages.map { language ->
val requestBody = json
.encodeToString(
VideoSourceRequest(
source = extra.source,
episodeId = extra.episodeId,
episodeNum = episodeNum,
subType = language,
),
)
.toRequestBody("application/json".toMediaType())
val response = client
.newCall(POST(url = url, body = requestBody))
.execute()
.parseAs<VideoSourceResponse>()
EpisodeData(
source = extra.source,
language = language,
response = response,
)
}
}
val videos = episodeDataList.flatMap { episodeData ->
val defaultSource = episodeData.response.sources?.first {
it.quality in listOf("default", "auto")
} ?: return@flatMap emptyList()
val subtitles = episodeData.response.subtitles
?.filter { it.lang != "Thumbnails" }
?.map { Track(it.url, it.lang) }
?: emptyList()
playlistUtils.extractFromHls(
playlistUrl = defaultSource.url,
videoNameGen = { quality ->
val serverName = getServerName(episodeData.source)
val typeName = when {
subtitles.isNotEmpty() -> "SoftSub"
else -> getTypeName(episodeData.language)
}
"$serverName - $quality - $typeName"
},
subtitleList = subtitles,
)
}
return videos.sort()
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val lang = preferences.getString(PREF_TYPE_KEY, PREF_TYPE_DEFAULT)!!.let(::getTypeName)
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!.let(::getServerName)
return sortedWith(
compareByDescending<Video> { it.quality.contains(lang) }
.thenByDescending { it.quality.contains(quality) }
.thenByDescending { it.quality.contains(server, true) },
)
}
/* ====================================== Preferences ====================================== */
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = "Preferred domain"
entries = PREF_DOMAIN_ENTRIES
entryValues = PREF_DOMAIN_ENTRY_VALUES
setDefaultValue(PREF_DOMAIN_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = PREF_SERVER_ENTRIES
entryValues = PREF_SERVER_ENTRY_VALUES
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)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRY_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_ENTRY_VALUES
setDefaultValue(PREF_TYPE_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_TITLE_LANGUAGE_KEY
title = "Preferred title language"
entries = PREF_TITLE_LANGUAGE_ENTRIES
entryValues = PREF_TITLE_LANGUAGE_ENTRY_VALUES
setDefaultValue(PREF_TITLE_LANGUAGE_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
Toast.makeText(screen.context, "Refresh your anime library to apply changes", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = PREF_MARK_FILLER_EPISODE_KEY
title = "Mark filler episodes"
setDefaultValue(PREF_MARK_FILLER_EPISODE_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
Toast.makeText(screen.context, "Refresh your anime library to apply changes", Toast.LENGTH_LONG).show()
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
}
/* =================================== AniPlay Utilities =================================== */
private fun parseEpisodeName(number: Int, name: String): String {
return when {
listOf("EP ", "EPISODE ").any(name::startsWith) -> "Episode $number"
else -> "Episode $number: $name"
}
}
private fun getServerName(value: String): String {
val index = PREF_SERVER_ENTRY_VALUES.indexOf(value)
return PREF_SERVER_ENTRIES[index]
}
private fun getTypeName(value: String): String {
val index = PREF_TYPE_ENTRY_VALUES.indexOf(value)
return PREF_TYPE_ENTRIES[index]
}
@Synchronized
private fun parseDate(dateStr: String?): Long {
return dateStr?.let {
runCatching { DATE_FORMATTER.parse(it)?.time }.getOrNull()
} ?: 0L
}
companion object {
private const val PREF_DOMAIN_KEY = "domain"
private val PREF_DOMAIN_ENTRIES = arrayOf("aniplaynow.live (default)", "aniplay.lol (backup)")
private val PREF_DOMAIN_ENTRY_VALUES = arrayOf("aniplaynow.live", "aniplay.lol")
private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live"
private const val PREF_SERVER_KEY = "server"
private val PREF_SERVER_ENTRIES = arrayOf("Kuro (Gogoanime)", "Yuki (HiAnime)", "Yuno (Yugenanime)")
private val PREF_SERVER_ENTRY_VALUES = arrayOf("kuro", "yuki", "yuno")
private const val PREF_SERVER_DEFAULT = "kuro"
private const val PREF_QUALITY_KEY = "quality"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360")
private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_TYPE_KEY = "type"
private val PREF_TYPE_ENTRIES = arrayOf("Sub", "SoftSub", "Dub")
private val PREF_TYPE_ENTRY_VALUES = arrayOf("sub", "softsub", "dub")
private const val PREF_TYPE_DEFAULT = "sub"
private const val PREF_TITLE_LANGUAGE_KEY = "title_language"
private val PREF_TITLE_LANGUAGE_ENTRIES = arrayOf("Romaji", "English", "Native")
private val PREF_TITLE_LANGUAGE_ENTRY_VALUES = arrayOf("romaji", "english", "native")
private const val PREF_TITLE_LANGUAGE_DEFAULT = "romaji"
private const val PREF_MARK_FILLER_EPISODE_KEY = "mark_filler_episode"
private const val PREF_MARK_FILLER_EPISODE_DEFAULT = true
private val DATE_FORMATTER = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
}

View file

@ -0,0 +1,69 @@
package eu.kanade.tachiyomi.animeextension.en.aniplay
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class EpisodeListResponse(
val episodes: List<Episode>,
val providerId: String,
val default: Boolean?,
) {
@Serializable
data class Episode(
val id: String,
val number: Int,
val title: String,
val hasDub: Boolean,
val isFiller: Boolean,
val img: String?,
val description: String?,
val createdAt: String?,
)
}
@Serializable
data class VideoSourceRequest(
val source: String,
@SerialName("episodeid")
val episodeId: String,
@SerialName("episodenum")
val episodeNum: String,
@SerialName("subtype")
val subType: String,
)
@Serializable
data class VideoSourceResponse(
val sources: List<Source>?,
val subtitles: List<Subtitle>?,
) {
@Serializable
data class Source(
val url: String,
val quality: String,
)
@Serializable
data class Subtitle(
val url: String,
val lang: String,
)
}
@Serializable
data class EpisodeExtra(
val source: String,
val episodeId: String,
val hasDub: Boolean,
)
@Serializable
data class EpisodeData(
val source: String,
val language: String,
val response: VideoSourceResponse,
)

View file

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

View file

@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.animeextension.en.nineanime
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import android.webkit.URLUtil
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
@ -39,7 +41,12 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val id: Long = 98855593379717478
override val baseUrl by lazy {
val customDomain = preferences.getString(PREF_CUSTOM_DOMAIN_KEY, null)
if (customDomain.isNullOrBlank()) {
preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
} else {
customDomain
}
}
override val lang = "en"
@ -90,7 +97,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filters = AniwaveFilters.getSearchParameters(filters)
val vrf = if (query.isNotBlank()) utils.vrfEncrypt(getEncryptionKey(), query) else ""
val vrf = if (query.isNotBlank()) utils.vrfEncrypt(ENCRYPTION_KEY, query) else ""
var url = "$baseUrl/filter?keyword=$query"
if (filters.genre.isNotBlank()) url += filters.genre
@ -117,15 +124,18 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
title = document.select("h1.title").text()
genre = document.select("div:contains(Genre) > span > a").joinToString { it.text() }
description = document.select("div.synopsis > div.shorting > div.content").text()
author = document.select("div:contains(Studio) > span > a").text()
status = parseStatus(document.select("div:contains(Status) > span").text())
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val newDocument = resolveSearchAnime(anime, document)
anime.apply {
title = newDocument.select("h1.title").text()
genre = newDocument.select("div:contains(Genre) > span > a").joinToString { it.text() }
description = newDocument.select("div.synopsis > div.shorting > div.content").text()
author = newDocument.select("div:contains(Studio) > span > a").text()
status = parseStatus(newDocument.select("div:contains(Status) > span").text())
val altName = "Other name(s): "
document.select("h1.title").attr("data-jp").let {
newDocument.select("h1.title").attr("data-jp").let {
if (it.isNotBlank()) {
description = when {
description.isNullOrBlank() -> altName + it
@ -134,13 +144,19 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}
}
return anime
}
// ============================== 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(getEncryptionKey(), id)
Log.i(name, "episodeListRequest")
val response = client.newCall(GET(baseUrl + anime.url)).execute()
var document = response.asJsoup()
document = resolveSearchAnime(anime, document)
val id = document.selectFirst("div[data-id]")?.attr("data-id") ?: throw Exception("ID not found")
val vrf = utils.vrfEncrypt(ENCRYPTION_KEY, id)
val listHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
@ -196,7 +212,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListRequest(episode: SEpisode): Request {
val ids = episode.url.substringBefore("&")
val vrf = utils.vrfEncrypt(getEncryptionKey(), ids)
val vrf = utils.vrfEncrypt(ENCRYPTION_KEY, ids)
val url = "/ajax/server/list/$ids?vrf=$vrf"
val epurl = episode.url.substringAfter("epurl=")
@ -218,7 +234,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> {
val epurl = response.request.url.fragment!!
val document = response.parseAs<ResultResponse>().toDocument()
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
val hosterSelection = getHosters()
val typeSelection = preferences.getStringSet(PREF_TYPE_TOGGLE_KEY, PREF_TYPES_TOGGLE_DEFAULT)!!
return document.select("div.servers > div").parallelFlatMapBlocking { elem ->
@ -249,7 +265,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private fun extractVideo(server: VideoData, epUrl: String): List<Video> {
val vrf = utils.vrfEncrypt(getEncryptionKey(), server.serverId)
val vrf = utils.vrfEncrypt(ENCRYPTION_KEY, server.serverId)
val listHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
@ -264,18 +280,13 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return runCatching {
val parsed = response.parseAs<ServerResponse>()
val embedLink = utils.vrfDecrypt(getDecryptionKey(), parsed.result.url)
val embedLink = utils.vrfDecrypt(DECRYPTION_KEY, parsed.result.url)
when (server.serverName) {
"Vidstream", "Megaf" -> {
val hosterName = when (server.serverName) {
"Vidstream" -> "Vidstream"
else -> "Megaf"
}
vidsrcExtractor.videosFromUrl(embedLink, hosterName, server.type)
}
"filemoon" -> filemoonExtractor.videosFromUrl(embedLink, "Filemoon - ${server.type} - ")
"vidstream" -> vidsrcExtractor.videosFromUrl(embedLink, "Vidstream", server.type)
"megaf" -> vidsrcExtractor.videosFromUrl(embedLink, "MegaF", server.type)
"moonf" -> filemoonExtractor.videosFromUrl(embedLink, "MoonF - ${server.type} - ")
"streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList()
"mp4upload" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}")
"mp4u" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}")
else -> emptyList()
}
}.getOrElse { emptyList() }
@ -313,20 +324,33 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}
private fun getDecryptionKey(): String {
var prefKey = preferences.getString(PREF_VERIFY_KEY_DECRYPT_KEY, null)
if (prefKey.isNullOrBlank()) {
prefKey = PREF_VERIFY_KEY_DECRYPT_VALUE
private fun resolveSearchAnime(anime: SAnime, document: Document): Document {
if (document.location().startsWith("$baseUrl/filter?keyword=")) { // redirected to search
val element = document.selectFirst(searchAnimeSelector())
val foundAnimePath = element?.selectFirst("a[href]")?.attr("href") ?: throw Exception("Search element not found (resolveSearch)")
anime.url = foundAnimePath // probably doesn't work as intended
return client.newCall(GET(baseUrl + foundAnimePath)).execute().asJsoup()
}
return prefKey
return document
}
private fun getEncryptionKey(): String {
var prefKey = preferences.getString(PREF_VERIFY_KEY_ENCRYPT_KEY, null)
if (prefKey.isNullOrBlank()) {
prefKey = PREF_VERIFY_KEY_ENCRYPT_VALUE
private fun getHosters(): Set<String> {
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
var invalidRecord = false
hosterSelection.forEach { str ->
val index = HOSTERS_NAMES.indexOf(str)
if (index == -1) {
invalidRecord = true
}
return prefKey
}
// found invalid record, reset to defaults
if (invalidRecord) {
preferences.edit().putStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT).apply()
return PREF_HOSTER_DEFAULT.toSet()
}
return hosterSelection.toSet()
}
companion object {
@ -340,6 +364,8 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private const val PREF_DOMAIN_KEY = "preferred_domain"
private const val PREF_DOMAIN_DEFAULT = "https://aniwave.to"
private const val PREF_CUSTOM_DOMAIN_KEY = "custom_domain"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
@ -356,16 +382,16 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val HOSTERS = arrayOf(
"Vidstream",
"Megaf",
"Filemoon",
"MoonF",
"StreamTape",
"Mp4Upload",
"MP4u",
)
private val HOSTERS_NAMES = arrayOf(
"Vidstream",
"Megaf",
"filemoon",
"vidstream",
"megaf",
"moonf",
"streamtape",
"mp4upload",
"mp4u",
)
private val PREF_HOSTER_DEFAULT = HOSTERS_NAMES.toSet()
@ -373,22 +399,25 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val TYPES = arrayOf("Sub", "Softsub", "Dub")
private val PREF_TYPES_TOGGLE_DEFAULT = TYPES.toSet()
// https://rowdy-avocado.github.io/multi-keys/
private const val PREF_VERIFY_KEY_DECRYPT_KEY = "verify_key_decrypt"
private const val PREF_VERIFY_KEY_DECRYPT_VALUE = "ctpAbOz5u7S6OMkx"
private const val PREF_VERIFY_KEY_ENCRYPT_KEY = "verify_key_encrypt"
private const val PREF_VERIFY_KEY_ENCRYPT_VALUE = "p01EDKu734HJP1Tm"
private const val DECRYPTION_KEY = "ctpAbOz5u7S6OMkx"
private const val ENCRYPTION_KEY = "T78s2WjTc7hSIZZR"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// validate hosters preferences and if invalid reset
try {
getHosters()
} catch (e: Exception) {
Log.w(name, e.toString())
}
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = "Preferred domain"
entries = arrayOf("aniwave.to", "aniwave.li", "aniwave.ws", "aniwave.vc")
entryValues = arrayOf("https://aniwave.to", "https://aniwave.li", "https://aniwave.ws", "https://aniwave.vc")
entries = arrayOf("aniwave.to", "aniwavetv.to (unofficial)")
entryValues = arrayOf("https://aniwave.to", "https://aniwavetv.to")
setDefaultValue(PREF_DOMAIN_DEFAULT)
summary = "%s"
@ -486,26 +515,27 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = PREF_VERIFY_KEY_DECRYPT_KEY
title = "Custom decryption key"
setDefaultValue("")
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
val newKey = newValue as String
preferences.edit().putString(key, newKey).commit()
key = PREF_CUSTOM_DOMAIN_KEY
title = "Custom domain"
setDefaultValue(null)
val currentValue = preferences.getString(PREF_CUSTOM_DOMAIN_KEY, null)
summary = if (currentValue.isNullOrBlank()) {
"Custom domain of your choosing"
} else {
"Domain: \"$currentValue\". \nLeave blank to disable. Overrides any domain preferences!"
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = PREF_VERIFY_KEY_ENCRYPT_KEY
title = "Custom encryption key"
setDefaultValue("")
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
val newKey = newValue as String
preferences.edit().putString(key, newKey).commit()
val newDomain = newValue as String
if (newDomain.isBlank() || URLUtil.isValidUrl(newDomain)) {
summary = "Restart to apply changes"
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, newDomain).apply()
true
} else {
Toast.makeText(screen.context, "Invalid url. Url example: https://aniwave.to", Toast.LENGTH_LONG).show()
false
}
}
}.also(screen::addPreference)
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".es.cineplus123.Cineplus123UrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="cineplus123.org"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,14 @@
ext {
extName = 'Cineplus123'
extClass = '.Cineplus123'
themePkg = 'dooplay'
baseUrl = 'https://cineplus123.org'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:uqload-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -0,0 +1,196 @@
package eu.kanade.tachiyomi.animeextension.es.cineplus123
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
class Cineplus123 : DooPlay(
"es",
"Cineplus123",
"https://cineplus123.org",
) {
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/tendencias/$page")
override fun popularAnimeSelector() = latestUpdatesSelector()
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/ano/2024/page/$page", headers)
override fun videoListSelector() = "li.dooplay_player_option" // ul#playeroptionsul
override val episodeMovieText = "Película"
override val episodeSeasonPrefix = "Temporada"
override val prefQualityTitle = "Calidad preferida"
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val players = document.select("ul#playeroptionsul li")
return players.parallelFlatMapBlocking { player ->
val name = player.selectFirst("span.title")!!.text()
val url = getPlayerUrl(player)
?: return@parallelFlatMapBlocking emptyList<Video>()
extractVideos(url, name)
}
}
private fun extractVideos(url: String, lang: String): List<Video> {
return when {
"uqload" in url -> uqloadExtractor.videosFromUrl(url, "$lang -")
"strwish" in url -> streamWishExtractor.videosFromUrl(url, lang)
else -> null
} ?: emptyList()
}
private fun getPlayerUrl(player: Element): String? {
val body = FormBody.Builder()
.add("action", "doo_player_ajax")
.add("post", player.attr("data-post"))
.add("nume", player.attr("data-nume"))
.add("type", player.attr("data-type"))
.build()
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
.execute().body.string()
.substringAfter("\"embed_url\":\"")
.substringBefore("\",")
.replace("\\", "")
.takeIf(String::isNotBlank)
}
// ============================== Filters ===============================
override val fetchGenres = false
override fun getFilterList() = Cineplus123Filters.FILTER_LIST
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = Cineplus123Filters.getSearchParameters(filters)
val path = when {
params.genre.isNotBlank() -> {
if (params.genre in listOf("tendencias", "ratings", "series-de-tv", "peliculas")) {
"/${params.genre}"
} else {
"/genero/${params.genre}"
}
}
params.language.isNotBlank() -> "/genero/${params.language}"
params.year.isNotBlank() -> "/ano/${params.year}"
params.movie.isNotBlank() -> {
if (params.movie == "Peliculas") {
"/peliculas"
} else {
"/genero/${params.movie}"
}
}
else -> buildString {
append(
when {
query.isNotBlank() -> "/?s=$query"
else -> "/"
},
)
append(
when (params.type) {
"serie" -> "serie-de-tv"
"pelicula" -> "peliculas"
else -> "tendencias"
},
)
if (params.isInverted) append("&orden=asc")
}
}
return if (path.startsWith("/?s=")) {
GET("$baseUrl/page/$page$path")
} else {
GET("$baseUrl$path/page/$page")
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
super.setupPreferenceScreen(screen) // Quality preference
val langPref = ListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = PREF_LANG_TITLE
entries = PREF_LANG_ENTRIES
entryValues = PREF_LANG_VALUES
setDefaultValue(PREF_LANG_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()
}
}
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = SERVER_LIST
entryValues = SERVER_LIST
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)
screen.addPreference(langPref)
}
// ============================= Utilities ==============================
override fun String.toDate() = 0L
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(lang) },
{ it.quality.contains(server, true) },
{ it.quality.contains(quality) },
),
).reversed()
}
override val prefQualityValues = arrayOf("480p", "720p", "1080p")
override val prefQualityEntries = prefQualityValues
companion object {
private const val PREF_LANG_KEY = "preferred_lang"
private const val PREF_LANG_TITLE = "Preferred language"
private const val PREF_LANG_DEFAULT = "LATINO"
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Uqload"
private val PREF_LANG_ENTRIES = arrayOf("SUBTITULADO", "LATINO", "CASTELLANO")
private val PREF_LANG_VALUES = arrayOf("SUBTITULADO", "LATINO", "CASTELLANO")
private val SERVER_LIST = arrayOf("StreamWish", "Uqload")
}
}

View file

@ -0,0 +1,159 @@
package eu.kanade.tachiyomi.animeextension.es.cineplus123
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object Cineplus123Filters {
open class UriPartFilter(
displayName: String,
private val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toUriPart() = vals[state].second
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.asUriPart(): String {
return getFirst<R>().let {
(it as UriPartFilter).toUriPart()
}
}
class InvertedResultsFilter : AnimeFilter.CheckBox("Invertir resultados", false)
class TypeFilter : UriPartFilter("Tipo", AnimesOnlineNinjaData.TYPES)
class GenreFilter : UriPartFilter("Generos", AnimesOnlineNinjaData.GENRES)
class LanguageFilter : UriPartFilter("Idiomas", AnimesOnlineNinjaData.LANGUAGES)
class YearFilter : UriPartFilter("Año", AnimesOnlineNinjaData.YEARS)
class MovieFilter : UriPartFilter("Peliculas", AnimesOnlineNinjaData.MOVIES)
class OtherOptionsGroup : AnimeFilter.Group<UriPartFilter>(
"Otros filtros",
listOf(
GenreFilter(),
LanguageFilter(),
YearFilter(),
MovieFilter(),
),
)
private inline fun <reified R> AnimeFilter.Group<UriPartFilter>.getItemUri(): String {
return state.first { it is R }.toUriPart()
}
val FILTER_LIST get() = AnimeFilterList(
InvertedResultsFilter(),
TypeFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("Estos filtros no afectan a la busqueda por texto"),
OtherOptionsGroup(),
)
data class FilterSearchParams(
val isInverted: Boolean = false,
val type: String = "",
val genre: String = "",
val language: String = "",
val year: String = "",
val movie: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val others = filters.getFirst<OtherOptionsGroup>()
return FilterSearchParams(
filters.getFirst<InvertedResultsFilter>().state,
filters.asUriPart<TypeFilter>(),
others.getItemUri<GenreFilter>(),
others.getItemUri<LanguageFilter>(),
others.getItemUri<YearFilter>(),
others.getItemUri<MovieFilter>(),
)
}
private object AnimesOnlineNinjaData {
val EVERY = Pair("Seleccionar", "")
val TYPES = arrayOf(
Pair("Todos", "todos"),
Pair("Series", "serie"),
Pair("Peliculas", "pelicula"),
)
val GENRES = arrayOf(
EVERY,
Pair("accion", "accion"),
Pair("action-adventure", "action-adventure"),
Pair("animacion", "animacion"),
Pair("aventura", "aventura"),
Pair("bajalogratis", "bajalogratis"),
Pair("belica", "belica"),
Pair("ciencia-ficcion", "ciencia-ficcion"),
Pair("comedia", "comedia"),
Pair("crimen", "crimen"),
Pair("disney", "disney"),
Pair("documental", "documental"),
Pair("don-torrent", "don-torrent"),
Pair("drama", "drama"),
Pair("familia", "familia"),
Pair("fantasia", "fantasia"),
Pair("gran-torrent", "gran-torrent"),
Pair("hbo", "hbo"),
Pair("historia", "historia"),
Pair("kids", "kids"),
Pair("misterio", "misterio"),
Pair("musica", "musica"),
Pair("romance", "romance"),
Pair("sci-fi-fantasy", "sci-fi-fantasy"),
Pair("series-de-amazon-prime-video", "series-de-amazon-prime-video"),
Pair("soap", "soap"),
Pair("suspense", "suspense"),
Pair("talk", "talk"),
Pair("terror", "terror"),
Pair("war-politics", "war-politics"),
Pair("western", "western"),
)
val LANGUAGES = arrayOf(
EVERY,
Pair("latino", "latino"),
Pair("castellano", "castellano"),
Pair("subtitulado", "subtitulado"),
)
val YEARS = arrayOf(EVERY) + (2024 downTo 1979).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val MOVIES = arrayOf(
EVERY,
Pair("pelicula", "pelicula"),
Pair("series", "series de tv"),
Pair("pelicula-de-tv", "pelicula-de-tv"),
Pair("peliculas-cristianas", "peliculas-cristianas"),
Pair("peliculas-de-halloween", "peliculas-de-halloween"),
Pair("peliculas-de-navidad", "peliculas-de-navidad"),
Pair("peliculas-para-el-dia-de-la-madre", "peliculas-para-el-dia-de-la-madre"),
Pair("pelis-play", "pelis-play"),
Pair("pelishouse", "pelishouse"),
Pair("pelismart-tv", "pelismart-tv"),
Pair("pelisnow", "pelisnow"),
Pair("pelix-tv", "pelix-tv"),
Pair("poseidonhd", "poseidonhd"),
Pair("proximamente", "proximamente"),
Pair("reality", "reality"),
Pair("repelis-go", "repelis-go"),
Pair("repelishd-tv", "repelishd-tv"),
Pair("repelisplus", "repelisplus"),
)
}
}

View file

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

View file

@ -132,8 +132,8 @@ class Hackstore : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val ismovie = response.request.url.toString().contains("/peliculas/")
return if (ismovie) {
val isMovie = response.request.url.toString().contains("/peliculas/")
return if (isMovie) {
listOf(
SEpisode.create().apply {
name = "PELÍCULA"
@ -142,14 +142,14 @@ class Hackstore : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
},
)
} else {
document.select(".movie-thumbnail").map { thumbnail ->
document.select(".movie-thumbnail").mapIndexed { idx, thumbnail ->
val episodeLink = thumbnail.select("a").attr("href")
val seasonMatch = Regex("-(\\d+)x(\\d+)/$").find(episodeLink)
val seasonNumber = seasonMatch?.groups?.get(1)?.value?.toInt() ?: 0
val episodeNumber = seasonMatch?.groups?.get(2)?.value?.toInt() ?: 0
SEpisode.create().apply {
name = "T$seasonNumber - E$episodeNumber"
episode_number = episodeNumber.toFloat()
episode_number = idx + 1f
setUrlWithoutDomain(episodeLink)
}
}
@ -196,7 +196,7 @@ class Hackstore : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
server.contains("streamtape") || server.contains("stp") || server.contains("stape") -> {
listOf(streamTapeExtractor.videoFromUrl(url, quality = "$prefix StreamTape")!!)
}
server.contains("voe") -> voeExtractor.videosFromUrl(url, prefix)
server.contains("voe") -> voeExtractor.videosFromUrl(url, "$prefix ")
server.contains("filemoon") -> filemoonExtractor.videosFromUrl(url, prefix = "$prefix Filemoon:")
server.contains("wishembed") || server.contains("streamwish") || server.contains("strwish") || server.contains("wish") -> {
streamWishExtractor.videosFromUrl(url, videoNameGen = { "$prefix StreamWish:$it" })

View file

@ -1,7 +1,7 @@
ext {
extName = 'Pelisplushd'
extClass = '.PelisplushdFactory'
extVersionCode = 53
extVersionCode = 54
}
apply from: "$rootDir/common.gradle"
@ -22,5 +22,6 @@ dependencies {
implementation(project(':lib:burstcloud-extractor'))
implementation(project(':lib:fastream-extractor'))
implementation(project(':lib:upstream-extractor'))
implementation(project(':lib:streamhidevid-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

View file

@ -5,7 +5,6 @@ import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.pelisplushd.extractors.StreamHideExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
@ -110,10 +110,13 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
val apiUrl = data?.substringAfter("video[1] = '", "")?.substringBefore("';", "")
val alternativeServers = document.select("ul.TbVideoNv.nav.nav-tabs li:not(:first-child)")
if (!apiUrl.isNullOrEmpty()) {
val apiResponse = client.newCall(GET(apiUrl)).execute().asJsoup()
val apiResponse = client.newCall(GET(apiUrl)).execute()
val docResponse = apiResponse.asJsoup()
if (apiResponse.isSuccessful) {
val regIsUrl = "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)".toRegex()
val encryptedList = apiResponse.select("#PlayerDisplay div[class*=\"OptionsLangDisp\"] div[class*=\"ODDIV\"] div[class*=\"OD\"] li")
encryptedList.parallelCatchingFlatMapBlocking {
val encryptedList = docResponse.select("#PlayerDisplay div[class*=\"OptionsLangDisp\"] div[class*=\"ODDIV\"] div[class*=\"OD\"] li")
encryptedList.flatMap {
runCatching {
val url = it.attr("onclick")
.substringAfter("go_to_player('")
.substringAfter("go_to_playerVast('")
@ -135,8 +138,10 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
}
serverVideoResolver(realUrl)
}.getOrNull() ?: emptyList()
}.also(videoList::addAll)
}
}
// verifier for old series
if (!apiUrl.isNullOrEmpty() && !apiUrl.contains("/video/") || alternativeServers.any()) {
@ -210,7 +215,8 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
embedUrl.contains("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "Fastream:")
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url)
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> listOf(StreamTapeExtractor(client).videoFromUrl(url, quality = "StreamTape")!!)
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideExtractor(client).videosFromUrl(url, "StreamHide")
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") ||
embedUrl.contains("streamvid") || embedUrl.contains("vidhide") -> StreamHideVidExtractor(client).videosFromUrl(url)
else -> emptyList()
}
}.getOrNull() ?: emptyList()

View file

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
class PelisplushdFactory : AnimeSourceFactory {
override fun createSources(): List<AnimeSource> = listOf(
Pelisplushd("PelisPlusHD", "https://ww1.pelisplushd.nu"),
Pelisplushd("PelisPlusHD", "https://pelisplushd.bz"),
Pelisplusto("PelisPlusTo", "https://ww3.pelisplus.to"),
Pelisplusph("PelisPlusPh", "https://www.pelisplushd.ph"),
)

View file

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.animeextension.es.pelisplushd
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.pelisplushd.extractors.StreamHideExtractor
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
@ -174,7 +174,8 @@ class Pelisplusph(override val name: String, override val baseUrl: String) : Pel
embedUrl.contains("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "$prefix Fastream:")
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url, prefix = prefix)
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> listOf(StreamTapeExtractor(client).videoFromUrl(url, quality = "$prefix StreamTape")!!)
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideExtractor(client).videosFromUrl(url, "$prefix StreamHide")
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") ||
embedUrl.contains("streamvid") || embedUrl.contains("vidhide") -> StreamHideVidExtractor(client).videosFromUrl(url, "$prefix ")
else -> emptyList()
}
}.getOrNull() ?: emptyList()

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.animeextension.es.pelisplushd
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.pelisplushd.extractors.StreamHideExtractor
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
@ -15,6 +14,7 @@ import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
@ -215,7 +215,8 @@ class Pelisplusto(override val name: String, override val baseUrl: String) : Pel
embedUrl.contains("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "Fastream:")
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url)
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> listOf(StreamTapeExtractor(client).videoFromUrl(url, quality = "StreamTape")!!)
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideExtractor(client).videosFromUrl(url, "StreamHide")
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") ||
embedUrl.contains("streamvid") || embedUrl.contains("vidhide") -> StreamHideVidExtractor(client).videosFromUrl(url)
else -> emptyList()
}
}.getOrNull() ?: emptyList()

View file

@ -1,205 +0,0 @@
package eu.kanade.tachiyomi.animeextension.es.pelisplushd.extractors
import java.util.regex.Pattern
import kotlin.math.pow
// https://github.com/cylonu87/JsUnpacker
class JsUnpacker(packedJS: String?) {
private var packedJS: String? = null
/**
* Detects whether the javascript is P.A.C.K.E.R. coded.
*
* @return true if it's P.A.C.K.E.R. coded.
*/
fun detect(): Boolean {
val js = packedJS!!.replace(" ", "")
val p = Pattern.compile("eval\\(function\\(p,a,c,k,e,[rd]")
val m = p.matcher(js)
return m.find()
}
/**
* Unpack the javascript
*
* @return the javascript unpacked or null.
*/
fun unpack(): String? {
val js = packedJS
runCatching {
var p =
Pattern.compile("""\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", Pattern.DOTALL)
var m = p.matcher(js)
if (m.find() && m.groupCount() == 4) {
val payload = m.group(1).replace("\\'", "'")
val radixStr = m.group(2)
val countStr = m.group(3)
val symtab = m.group(4).split("\\|".toRegex()).toTypedArray()
val radix = radixStr.toIntOrNull() ?: 36
val count = countStr.toIntOrNull() ?: 0
if (symtab.size != count) {
throw Exception("Unknown p.a.c.k.e.r. encoding")
}
val unbase = Unbase(radix)
p = Pattern.compile("\\b\\w+\\b")
m = p.matcher(payload)
val decoded = StringBuilder(payload)
var replaceOffset = 0
while (m.find()) {
val word = m.group(0)
val x = unbase.unbase(word)
var value: String? = null
if (x < symtab.size && x >= 0) {
value = symtab[x]
}
if (value != null && value.isNotEmpty()) {
decoded.replace(m.start() + replaceOffset, m.end() + replaceOffset, value)
replaceOffset += value.length - word.length
}
}
return decoded.toString()
}
}
return null
}
private inner class Unbase(private val radix: Int) {
private val alphabet62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
private val alphabet95 =
" !\"#$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
private var alphabet: String? = null
private var dictionary: HashMap<String, Int>? = null
fun unbase(str: String): Int {
var ret = 0
if (alphabet == null) {
ret = str.toInt(radix)
} else {
val tmp = StringBuilder(str).reverse().toString()
for (i in tmp.indices) {
ret += (radix.toDouble().pow(i.toDouble()) * dictionary!![tmp.substring(i, i + 1)]!!).toInt()
}
}
return ret
}
init {
if (radix > 36) {
when {
radix < 62 -> {
alphabet = alphabet62.substring(0, radix)
}
radix in 63..94 -> {
alphabet = alphabet95.substring(0, radix)
}
radix == 62 -> {
alphabet = alphabet62
}
radix == 95 -> {
alphabet = alphabet95
}
}
dictionary = HashMap(95)
for (i in 0 until alphabet!!.length) {
dictionary!![alphabet!!.substring(i, i + 1)] = i
}
}
}
}
/**
* @param packedJS javascript P.A.C.K.E.R. coded.
*/
init {
this.packedJS = packedJS
}
companion object {
private val C =
listOf(
0x63,
0x6f,
0x6d,
0x2e,
0x67,
0x6f,
0x6f,
0x67,
0x6c,
0x65,
0x2e,
0x61,
0x6e,
0x64,
0x72,
0x6f,
0x69,
0x64,
0x2e,
0x67,
0x6d,
0x73,
0x2e,
0x61,
0x64,
0x73,
0x2e,
0x4d,
0x6f,
0x62,
0x69,
0x6c,
0x65,
0x41,
0x64,
0x73,
)
private val Z =
listOf(
0x63,
0x6f,
0x6d,
0x2e,
0x66,
0x61,
0x63,
0x65,
0x62,
0x6f,
0x6f,
0x6b,
0x2e,
0x61,
0x64,
0x73,
0x2e,
0x41,
0x64,
)
fun String.load(): String? {
return try {
var load = this
for (q in C.indices) {
if (C[q % 4] > 270) {
load += C[q % 3]
} else {
load += C[q].toChar()
}
}
Class.forName(load.substring(load.length - C.size, load.length)).name
} catch (_: Exception) {
try {
var f = C[2].toChar().toString()
for (w in Z.indices) {
f += Z[w].toChar()
}
return Class.forName(f.substring(0b001, f.length)).name
} catch (_: Exception) {
null
}
}
}
}
}

View file

@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.animeextension.es.pelisplushd.extractors
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.OkHttpClient
class StreamHideExtractor(private val client: OkHttpClient) {
// from nineanime / ask4movie FilemoonExtractor
private val subtitleRegex = Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
fun videosFromUrl(url: String, name: String): List<Video> {
val page = client.newCall(GET(url)).execute().body.string()
val unpacked = JsUnpacker(page).unpack() ?: return emptyList()
val playlistUrl = unpacked.substringAfter("sources:")
.substringAfter("file:\"") // StreamHide
.substringAfter("src:\"") // StreamVid
.substringBefore('"')
val playlistData = client.newCall(GET(playlistUrl)).execute().body.string()
val subs = subtitleRegex.findAll(playlistData).map {
val urlPart = it.groupValues[2]
val subUrl = when {
!urlPart.startsWith("https:") ->
playlistUrl.substringBeforeLast("/") + "/$urlPart"
else -> urlPart
}
Track(subUrl, it.groupValues[1])
}.toList()
// The playlist usually only have one video quality.
return listOf(Video(playlistUrl, name, playlistUrl, subtitleTracks = subs))
}
}

View file

@ -72,6 +72,7 @@ data class Episode(
@SerialName("lang") val languages: EpisodeLanguages,
)
@Serializable
data class EpisodeLanguages(
@SerialName("vf") val vf: EpisodeLanguage,

View file

@ -1,7 +1,7 @@
ext {
extName = 'HentaiZM'
extClass = '.HentaiZM'
extVersionCode = 3
extVersionCode = 4
isNsfw = true
}

View file

@ -30,7 +30,7 @@ class HentaiZM : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "HentaiZM"
override val baseUrl = "https://www.hentaizm.life"
override val baseUrl = "https://www.hentaizm.pro"
override val lang = "tr"