Merge branch 'main' into FixNekoSama
1
.github/pull_request_template.md
vendored
|
@ -8,3 +8,4 @@ Checklist:
|
||||||
- [ ] Have explicitly kept the `id` if a source's name or language were changed
|
- [ ] 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 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 removed `web_hi_res_512.png` when adding a new extension
|
||||||
|
- [ ] Have made sure all the icons are in png format
|
||||||
|
|
14
.github/workflows/build_push.yml
vendored
|
@ -40,12 +40,26 @@ jobs:
|
||||||
files_separator: " "
|
files_separator: " "
|
||||||
safe_output: false
|
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.
|
# This step is going to commit, but this will not trigger another workflow.
|
||||||
- name: Bump extensions that uses a modified lib
|
- name: Bump extensions that uses a modified lib
|
||||||
if: steps.modified-libs.outputs.any_changed == 'true'
|
if: steps.modified-libs.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
|
./.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
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2
|
uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2
|
||||||
|
|
||||||
|
|
19
README.md
|
@ -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
|
## 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
|
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
|
[Template](https://github.com/aniyomiorg/aniyomi-extensions/blob/master/CONTRIBUTING.md)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
|
|
5
lib-multisrc/anilist/build.gradle.kts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-multisrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseVersionCode = 2
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,19 @@
|
||||||
package eu.kanade.tachiyomi.lib.vidsrcextractor
|
package eu.kanade.tachiyomi.lib.vidsrcextractor
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import app.cash.quickjs.QuickJs
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
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.network.GET
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import okhttp3.CacheControl
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
@ -21,54 +22,32 @@ import javax.crypto.spec.SecretKeySpec
|
||||||
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val cacheControl = CacheControl.Builder().noStore().build()
|
fun videosFromUrl(
|
||||||
private val noCacheClient = client.newBuilder()
|
embedLink: String,
|
||||||
.cache(null)
|
hosterName: String,
|
||||||
.build()
|
type: String = "",
|
||||||
|
subtitleList: List<Track> = emptyList(),
|
||||||
private val keys by lazy {
|
): List<Video> {
|
||||||
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> {
|
|
||||||
val host = embedLink.toHttpUrl().host
|
val host = embedLink.toHttpUrl().host
|
||||||
val apiUrl = getApiUrl(embedLink, keys)
|
val apiUrl = getApiUrl(embedLink)
|
||||||
|
|
||||||
val apiHeaders = headers.newBuilder().apply {
|
val response = client.newCall(GET(apiUrl)).execute()
|
||||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
val data = response.parseAs<MediaResponseBody>()
|
||||||
add("Host", host)
|
|
||||||
add("Referer", URLDecoder.decode(embedLink, "UTF-8"))
|
|
||||||
add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val response = client.newCall(
|
val decrypted = vrfDecrypt(data.result)
|
||||||
GET(apiUrl, apiHeaders),
|
val result = json.decodeFromString<Result>(decrypted)
|
||||||
).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()
|
|
||||||
}
|
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
return playlistUtils.extractFromHls(
|
||||||
data.result.sources.first().file,
|
playlistUrl = result.sources.first().file,
|
||||||
referer = "https://$host/",
|
referer = "https://$host/",
|
||||||
videoNameGen = { q -> hosterName + (if (type.isBlank()) "" else " - $type") + " - $q" },
|
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 host = embedLink.toHttpUrl().host
|
||||||
val params = embedLink.toHttpUrl().let { url ->
|
val params = embedLink.toHttpUrl().let { url ->
|
||||||
url.queryParameterNames.map {
|
url.queryParameterNames.map {
|
||||||
|
@ -76,13 +55,13 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
||||||
val encodedID = encodeID(vidId, keyList)
|
val apiSlug = encodeID(vidId, ENCRYPTION_KEY1)
|
||||||
val apiSlug = callFromFuToken(host, encodedID, embedLink)
|
val h = encodeID(vidId, ENCRYPTION_KEY2)
|
||||||
|
|
||||||
return buildString {
|
return buildString {
|
||||||
append("https://")
|
append("https://")
|
||||||
append(host)
|
append(host)
|
||||||
append("/")
|
append("/mediainfo/")
|
||||||
append(apiSlug)
|
append(apiSlug)
|
||||||
if (params.isNotEmpty()) {
|
if (params.isNotEmpty()) {
|
||||||
append("?")
|
append("?")
|
||||||
|
@ -91,51 +70,23 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
"${it.first}=${it.second}"
|
"${it.first}=${it.second}"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
append("&h=$h")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun encodeID(videoID: String, keyList: List<String>): String {
|
private fun encodeID(videoID: String, key: String): String {
|
||||||
val rc4Key1 = SecretKeySpec(keyList[0].toByteArray(), "RC4")
|
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
|
||||||
val rc4Key2 = SecretKeySpec(keyList[1].toByteArray(), "RC4")
|
val cipher = Cipher.getInstance("RC4")
|
||||||
val cipher1 = Cipher.getInstance("RC4")
|
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||||
val cipher2 = Cipher.getInstance("RC4")
|
return Base64.encode(cipher.doFinal(videoID.toByteArray()), Base64.DEFAULT)
|
||||||
cipher1.init(Cipher.DECRYPT_MODE, rc4Key1, cipher1.parameters)
|
.toString(Charsets.UTF_8)
|
||||||
cipher2.init(Cipher.DECRYPT_MODE, rc4Key2, cipher2.parameters)
|
.replace("+", "-")
|
||||||
var encoded = videoID.toByteArray()
|
.replace("/", "_")
|
||||||
|
.trim()
|
||||||
encoded = cipher1.doFinal(encoded)
|
|
||||||
encoded = cipher2.doFinal(encoded)
|
|
||||||
encoded = Base64.encode(encoded, Base64.DEFAULT)
|
|
||||||
return encoded.toString(Charsets.UTF_8).replace("/", "_").trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun callFromFuToken(host: String, data: String, embedLink: String): String {
|
private fun List<Result.SubTrack>.toTracks(): List<Track> {
|
||||||
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> {
|
|
||||||
return filter {
|
return filter {
|
||||||
it.kind == "captions"
|
it.kind == "captions"
|
||||||
}.mapNotNull {
|
}.mapNotNull {
|
||||||
|
@ -147,17 +98,32 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
}.getOrNull()
|
}.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
|
@Serializable
|
||||||
data class MediaResponseBody(
|
data class MediaResponseBody(
|
||||||
val status: Int,
|
val status: Int,
|
||||||
val result: Result,
|
val result: String,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Result(
|
data class Result(
|
||||||
val sources: ArrayList<Source>,
|
val sources: List<Source>,
|
||||||
val tracks: ArrayList<SubTrack> = ArrayList(),
|
val tracks: List<SubTrack> = emptyList(),
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Source(
|
data class Source(
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,9 @@ class VoeExtractor(private val client: OkHttpClient) {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
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()
|
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)
|
data class VideoLinkDTO(val file: String)
|
||||||
|
|
||||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
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)")
|
val script = document.selectFirst("script:containsData(const sources), script:containsData(var sources), script:containsData(wc0)")
|
||||||
?.data()
|
?.data()
|
||||||
?: return emptyList()
|
?: return emptyList()
|
||||||
|
@ -43,7 +54,7 @@ class VoeExtractor(private val client: OkHttpClient) {
|
||||||
else -> return emptyList()
|
else -> return emptyList()
|
||||||
}
|
}
|
||||||
return playlistUtils.extractFromHls(playlistUrl,
|
return playlistUtils.extractFromHls(playlistUrl,
|
||||||
videoNameGen = { quality -> "${prefix}Voe: $quality" }
|
videoNameGen = { quality -> "${prefix}Voe:$quality" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'AnimeFlix'
|
|
||||||
extClass = '.AnimeFlix'
|
|
||||||
extVersionCode = 7
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 92 KiB |
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'))
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 10 KiB |
|
@ -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)!!
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
)
|
|
|
@ -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"),
|
|
||||||
),
|
|
||||||
)
|
|
13
src/en/aniplay/build.gradle
Normal 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"))
|
||||||
|
}
|
BIN
src/en/aniplay/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/en/aniplay/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/en/aniplay/res/mipmap-ldpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 843 B |
BIN
src/en/aniplay/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/en/aniplay/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/en/aniplay/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
src/en/aniplay/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 17 KiB |
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Aniwave'
|
extName = 'Aniwave'
|
||||||
extClass = '.Aniwave'
|
extClass = '.Aniwave'
|
||||||
extVersionCode = 71
|
extVersionCode = 74
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.animeextension.en.nineanime
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.URLUtil
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
|
@ -39,7 +41,12 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
override val id: Long = 98855593379717478
|
override val id: Long = 98855593379717478
|
||||||
|
|
||||||
override val baseUrl by lazy {
|
override val baseUrl by lazy {
|
||||||
preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
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"
|
override val lang = "en"
|
||||||
|
@ -90,7 +97,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
val filters = AniwaveFilters.getSearchParameters(filters)
|
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"
|
var url = "$baseUrl/filter?keyword=$query"
|
||||||
|
|
||||||
if (filters.genre.isNotBlank()) url += filters.genre
|
if (filters.genre.isNotBlank()) url += filters.genre
|
||||||
|
@ -117,30 +124,39 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
// =========================== Anime Details ============================
|
||||||
|
|
||||||
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
title = document.select("h1.title").text()
|
val anime = SAnime.create()
|
||||||
genre = document.select("div:contains(Genre) > span > a").joinToString { it.text() }
|
val newDocument = resolveSearchAnime(anime, document)
|
||||||
description = document.select("div.synopsis > div.shorting > div.content").text()
|
anime.apply {
|
||||||
author = document.select("div:contains(Studio) > span > a").text()
|
title = newDocument.select("h1.title").text()
|
||||||
status = parseStatus(document.select("div:contains(Status) > span").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): "
|
val altName = "Other name(s): "
|
||||||
document.select("h1.title").attr("data-jp").let {
|
newDocument.select("h1.title").attr("data-jp").let {
|
||||||
if (it.isNotBlank()) {
|
if (it.isNotBlank()) {
|
||||||
description = when {
|
description = when {
|
||||||
description.isNullOrBlank() -> altName + it
|
description.isNullOrBlank() -> altName + it
|
||||||
else -> description + "\n\n$altName" + it
|
else -> description + "\n\n$altName" + it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return anime
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
// ============================== Episodes ==============================
|
||||||
|
|
||||||
override fun episodeListRequest(anime: SAnime): Request {
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
Log.i(name, "episodeListRequest")
|
||||||
.selectFirst("div[data-id]")!!.attr("data-id")
|
val response = client.newCall(GET(baseUrl + anime.url)).execute()
|
||||||
val vrf = utils.vrfEncrypt(getEncryptionKey(), id)
|
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 {
|
val listHeaders = headers.newBuilder().apply {
|
||||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
|
@ -196,7 +212,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request {
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
val ids = episode.url.substringBefore("&")
|
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 url = "/ajax/server/list/$ids?vrf=$vrf"
|
||||||
val epurl = episode.url.substringAfter("epurl=")
|
val epurl = episode.url.substringAfter("epurl=")
|
||||||
|
|
||||||
|
@ -218,7 +234,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
val epurl = response.request.url.fragment!!
|
val epurl = response.request.url.fragment!!
|
||||||
val document = response.parseAs<ResultResponse>().toDocument()
|
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)!!
|
val typeSelection = preferences.getStringSet(PREF_TYPE_TOGGLE_KEY, PREF_TYPES_TOGGLE_DEFAULT)!!
|
||||||
|
|
||||||
return document.select("div.servers > div").parallelFlatMapBlocking { elem ->
|
return document.select("div.servers > div").parallelFlatMapBlocking { elem ->
|
||||||
|
@ -249,7 +265,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||||
|
|
||||||
private fun extractVideo(server: VideoData, epUrl: String): List<Video> {
|
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 {
|
val listHeaders = headers.newBuilder().apply {
|
||||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
|
@ -264,18 +280,13 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val parsed = response.parseAs<ServerResponse>()
|
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) {
|
when (server.serverName) {
|
||||||
"Vidstream", "Megaf" -> {
|
"vidstream" -> vidsrcExtractor.videosFromUrl(embedLink, "Vidstream", server.type)
|
||||||
val hosterName = when (server.serverName) {
|
"megaf" -> vidsrcExtractor.videosFromUrl(embedLink, "MegaF", server.type)
|
||||||
"Vidstream" -> "Vidstream"
|
"moonf" -> filemoonExtractor.videosFromUrl(embedLink, "MoonF - ${server.type} - ")
|
||||||
else -> "Megaf"
|
|
||||||
}
|
|
||||||
vidsrcExtractor.videosFromUrl(embedLink, hosterName, server.type)
|
|
||||||
}
|
|
||||||
"filemoon" -> filemoonExtractor.videosFromUrl(embedLink, "Filemoon - ${server.type} - ")
|
|
||||||
"streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList()
|
"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()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}.getOrElse { emptyList() }
|
}.getOrElse { emptyList() }
|
||||||
|
@ -313,20 +324,33 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDecryptionKey(): String {
|
private fun resolveSearchAnime(anime: SAnime, document: Document): Document {
|
||||||
var prefKey = preferences.getString(PREF_VERIFY_KEY_DECRYPT_KEY, null)
|
if (document.location().startsWith("$baseUrl/filter?keyword=")) { // redirected to search
|
||||||
if (prefKey.isNullOrBlank()) {
|
val element = document.selectFirst(searchAnimeSelector())
|
||||||
prefKey = PREF_VERIFY_KEY_DECRYPT_VALUE
|
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 {
|
private fun getHosters(): Set<String> {
|
||||||
var prefKey = preferences.getString(PREF_VERIFY_KEY_ENCRYPT_KEY, null)
|
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
|
||||||
if (prefKey.isNullOrBlank()) {
|
var invalidRecord = false
|
||||||
prefKey = PREF_VERIFY_KEY_ENCRYPT_VALUE
|
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 {
|
companion object {
|
||||||
|
@ -340,6 +364,8 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
private const val PREF_DOMAIN_KEY = "preferred_domain"
|
private const val PREF_DOMAIN_KEY = "preferred_domain"
|
||||||
private const val PREF_DOMAIN_DEFAULT = "https://aniwave.to"
|
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_KEY = "preferred_quality"
|
||||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||||
|
|
||||||
|
@ -356,16 +382,16 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
private val HOSTERS = arrayOf(
|
private val HOSTERS = arrayOf(
|
||||||
"Vidstream",
|
"Vidstream",
|
||||||
"Megaf",
|
"Megaf",
|
||||||
"Filemoon",
|
"MoonF",
|
||||||
"StreamTape",
|
"StreamTape",
|
||||||
"Mp4Upload",
|
"MP4u",
|
||||||
)
|
)
|
||||||
private val HOSTERS_NAMES = arrayOf(
|
private val HOSTERS_NAMES = arrayOf(
|
||||||
"Vidstream",
|
"vidstream",
|
||||||
"Megaf",
|
"megaf",
|
||||||
"filemoon",
|
"moonf",
|
||||||
"streamtape",
|
"streamtape",
|
||||||
"mp4upload",
|
"mp4u",
|
||||||
)
|
)
|
||||||
private val PREF_HOSTER_DEFAULT = HOSTERS_NAMES.toSet()
|
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 TYPES = arrayOf("Sub", "Softsub", "Dub")
|
||||||
private val PREF_TYPES_TOGGLE_DEFAULT = TYPES.toSet()
|
private val PREF_TYPES_TOGGLE_DEFAULT = TYPES.toSet()
|
||||||
|
|
||||||
// https://rowdy-avocado.github.io/multi-keys/
|
private const val DECRYPTION_KEY = "ctpAbOz5u7S6OMkx"
|
||||||
private const val PREF_VERIFY_KEY_DECRYPT_KEY = "verify_key_decrypt"
|
private const val ENCRYPTION_KEY = "T78s2WjTc7hSIZZR"
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Settings ==============================
|
// ============================== Settings ==============================
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
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 {
|
ListPreference(screen.context).apply {
|
||||||
key = PREF_DOMAIN_KEY
|
key = PREF_DOMAIN_KEY
|
||||||
title = "Preferred domain"
|
title = "Preferred domain"
|
||||||
entries = arrayOf("aniwave.to", "aniwave.li", "aniwave.ws", "aniwave.vc")
|
entries = arrayOf("aniwave.to", "aniwavetv.to (unofficial)")
|
||||||
entryValues = arrayOf("https://aniwave.to", "https://aniwave.li", "https://aniwave.ws", "https://aniwave.vc")
|
entryValues = arrayOf("https://aniwave.to", "https://aniwavetv.to")
|
||||||
setDefaultValue(PREF_DOMAIN_DEFAULT)
|
setDefaultValue(PREF_DOMAIN_DEFAULT)
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
|
|
||||||
|
@ -486,26 +515,27 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
EditTextPreference(screen.context).apply {
|
EditTextPreference(screen.context).apply {
|
||||||
key = PREF_VERIFY_KEY_DECRYPT_KEY
|
key = PREF_CUSTOM_DOMAIN_KEY
|
||||||
title = "Custom decryption key"
|
title = "Custom domain"
|
||||||
setDefaultValue("")
|
setDefaultValue(null)
|
||||||
|
val currentValue = preferences.getString(PREF_CUSTOM_DOMAIN_KEY, null)
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
summary = if (currentValue.isNullOrBlank()) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
"Custom domain of your choosing"
|
||||||
val newKey = newValue as String
|
} else {
|
||||||
preferences.edit().putString(key, newKey).commit()
|
"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 ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
@Suppress("UNCHECKED_CAST")
|
val newDomain = newValue as String
|
||||||
val newKey = newValue as String
|
if (newDomain.isBlank() || URLUtil.isValidUrl(newDomain)) {
|
||||||
preferences.edit().putString(key, newKey).commit()
|
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)
|
}.also(screen::addPreference)
|
||||||
}
|
}
|
||||||
|
|
22
src/es/cineplus123/AndroidManifest.xml
Normal 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>
|
14
src/es/cineplus123/build.gradle
Normal 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"))
|
||||||
|
}
|
BIN
src/es/cineplus123/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/es/cineplus123/res/mipmap-hdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 6 KiB |
BIN
src/es/cineplus123/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/es/cineplus123/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
src/es/cineplus123/res/mipmap-mdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/es/cineplus123/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
src/es/cineplus123/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 17 KiB |
BIN
src/es/cineplus123/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 32 KiB |
BIN
src/es/cineplus123/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 52 KiB |
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hackstore'
|
extName = 'Hackstore'
|
||||||
extClass = '.Hackstore'
|
extClass = '.Hackstore'
|
||||||
extVersionCode = 10
|
extVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -132,8 +132,8 @@ class Hackstore : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val ismovie = response.request.url.toString().contains("/peliculas/")
|
val isMovie = response.request.url.toString().contains("/peliculas/")
|
||||||
return if (ismovie) {
|
return if (isMovie) {
|
||||||
listOf(
|
listOf(
|
||||||
SEpisode.create().apply {
|
SEpisode.create().apply {
|
||||||
name = "PELÍCULA"
|
name = "PELÍCULA"
|
||||||
|
@ -142,14 +142,14 @@ class Hackstore : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
document.select(".movie-thumbnail").map { thumbnail ->
|
document.select(".movie-thumbnail").mapIndexed { idx, thumbnail ->
|
||||||
val episodeLink = thumbnail.select("a").attr("href")
|
val episodeLink = thumbnail.select("a").attr("href")
|
||||||
val seasonMatch = Regex("-(\\d+)x(\\d+)/$").find(episodeLink)
|
val seasonMatch = Regex("-(\\d+)x(\\d+)/$").find(episodeLink)
|
||||||
val seasonNumber = seasonMatch?.groups?.get(1)?.value?.toInt() ?: 0
|
val seasonNumber = seasonMatch?.groups?.get(1)?.value?.toInt() ?: 0
|
||||||
val episodeNumber = seasonMatch?.groups?.get(2)?.value?.toInt() ?: 0
|
val episodeNumber = seasonMatch?.groups?.get(2)?.value?.toInt() ?: 0
|
||||||
SEpisode.create().apply {
|
SEpisode.create().apply {
|
||||||
name = "T$seasonNumber - E$episodeNumber"
|
name = "T$seasonNumber - E$episodeNumber"
|
||||||
episode_number = episodeNumber.toFloat()
|
episode_number = idx + 1f
|
||||||
setUrlWithoutDomain(episodeLink)
|
setUrlWithoutDomain(episodeLink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@ class Hackstore : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
server.contains("streamtape") || server.contains("stp") || server.contains("stape") -> {
|
server.contains("streamtape") || server.contains("stp") || server.contains("stape") -> {
|
||||||
listOf(streamTapeExtractor.videoFromUrl(url, quality = "$prefix StreamTape")!!)
|
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("filemoon") -> filemoonExtractor.videosFromUrl(url, prefix = "$prefix Filemoon:")
|
||||||
server.contains("wishembed") || server.contains("streamwish") || server.contains("strwish") || server.contains("wish") -> {
|
server.contains("wishembed") || server.contains("streamwish") || server.contains("strwish") || server.contains("wish") -> {
|
||||||
streamWishExtractor.videosFromUrl(url, videoNameGen = { "$prefix StreamWish:$it" })
|
streamWishExtractor.videosFromUrl(url, videoNameGen = { "$prefix StreamWish:$it" })
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Pelisplushd'
|
extName = 'Pelisplushd'
|
||||||
extClass = '.PelisplushdFactory'
|
extClass = '.PelisplushdFactory'
|
||||||
extVersionCode = 53
|
extVersionCode = 54
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -22,5 +22,6 @@ dependencies {
|
||||||
implementation(project(':lib:burstcloud-extractor'))
|
implementation(project(':lib:burstcloud-extractor'))
|
||||||
implementation(project(':lib:fastream-extractor'))
|
implementation(project(':lib:fastream-extractor'))
|
||||||
implementation(project(':lib:upstream-extractor'))
|
implementation(project(':lib:upstream-extractor'))
|
||||||
|
implementation(project(':lib:streamhidevid-extractor'))
|
||||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||||
}
|
}
|
|
@ -5,7 +5,6 @@ import android.content.SharedPreferences
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.animeextension.es.pelisplushd.extractors.StreamHideExtractor
|
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
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.filemoonextractor.FilemoonExtractor
|
||||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
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.streamlareextractor.StreamlareExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||||
|
@ -110,32 +110,37 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
|
||||||
val apiUrl = data?.substringAfter("video[1] = '", "")?.substringBefore("';", "")
|
val apiUrl = data?.substringAfter("video[1] = '", "")?.substringBefore("';", "")
|
||||||
val alternativeServers = document.select("ul.TbVideoNv.nav.nav-tabs li:not(:first-child)")
|
val alternativeServers = document.select("ul.TbVideoNv.nav.nav-tabs li:not(:first-child)")
|
||||||
if (!apiUrl.isNullOrEmpty()) {
|
if (!apiUrl.isNullOrEmpty()) {
|
||||||
val apiResponse = client.newCall(GET(apiUrl)).execute().asJsoup()
|
val apiResponse = client.newCall(GET(apiUrl)).execute()
|
||||||
val regIsUrl = "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)".toRegex()
|
val docResponse = apiResponse.asJsoup()
|
||||||
val encryptedList = apiResponse.select("#PlayerDisplay div[class*=\"OptionsLangDisp\"] div[class*=\"ODDIV\"] div[class*=\"OD\"] li")
|
if (apiResponse.isSuccessful) {
|
||||||
encryptedList.parallelCatchingFlatMapBlocking {
|
val regIsUrl = "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)".toRegex()
|
||||||
val url = it.attr("onclick")
|
val encryptedList = docResponse.select("#PlayerDisplay div[class*=\"OptionsLangDisp\"] div[class*=\"ODDIV\"] div[class*=\"OD\"] li")
|
||||||
.substringAfter("go_to_player('")
|
encryptedList.flatMap {
|
||||||
.substringAfter("go_to_playerVast('")
|
runCatching {
|
||||||
.substringBefore("?cover_url=")
|
val url = it.attr("onclick")
|
||||||
.substringBefore("')")
|
.substringAfter("go_to_player('")
|
||||||
.substringBefore("',")
|
.substringAfter("go_to_playerVast('")
|
||||||
.substringBefore("?poster")
|
.substringBefore("?cover_url=")
|
||||||
.substringBefore("?c_poster=")
|
.substringBefore("')")
|
||||||
.substringBefore("?thumb=")
|
.substringBefore("',")
|
||||||
.substringBefore("#poster=")
|
.substringBefore("?poster")
|
||||||
|
.substringBefore("?c_poster=")
|
||||||
|
.substringBefore("?thumb=")
|
||||||
|
.substringBefore("#poster=")
|
||||||
|
|
||||||
val realUrl = if (!regIsUrl.containsMatchIn(url)) {
|
val realUrl = if (!regIsUrl.containsMatchIn(url)) {
|
||||||
String(Base64.decode(url, Base64.DEFAULT))
|
String(Base64.decode(url, Base64.DEFAULT))
|
||||||
} else if (url.contains("?data=")) {
|
} else if (url.contains("?data=")) {
|
||||||
val apiPageSoup = client.newCall(GET(url)).execute().asJsoup()
|
val apiPageSoup = client.newCall(GET(url)).execute().asJsoup()
|
||||||
apiPageSoup.selectFirst("iframe")?.attr("src") ?: ""
|
apiPageSoup.selectFirst("iframe")?.attr("src") ?: ""
|
||||||
} else {
|
} else {
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
serverVideoResolver(realUrl)
|
serverVideoResolver(realUrl)
|
||||||
}.also(videoList::addAll)
|
}.getOrNull() ?: emptyList()
|
||||||
|
}.also(videoList::addAll)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifier for old series
|
// verifier for old series
|
||||||
|
@ -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("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "Fastream:")
|
||||||
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url)
|
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("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()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}.getOrNull() ?: emptyList()
|
}.getOrNull() ?: emptyList()
|
||||||
|
|
|
@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||||
|
|
||||||
class PelisplushdFactory : AnimeSourceFactory {
|
class PelisplushdFactory : AnimeSourceFactory {
|
||||||
override fun createSources(): List<AnimeSource> = listOf(
|
override fun createSources(): List<AnimeSource> = listOf(
|
||||||
Pelisplushd("PelisPlusHD", "https://ww1.pelisplushd.nu"),
|
Pelisplushd("PelisPlusHD", "https://pelisplushd.bz"),
|
||||||
Pelisplusto("PelisPlusTo", "https://ww3.pelisplus.to"),
|
Pelisplusto("PelisPlusTo", "https://ww3.pelisplus.to"),
|
||||||
Pelisplusph("PelisPlusPh", "https://www.pelisplushd.ph"),
|
Pelisplusph("PelisPlusPh", "https://www.pelisplushd.ph"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.animeextension.es.pelisplushd
|
||||||
|
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
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.AnimeFilter
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
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.filemoonextractor.FilemoonExtractor
|
||||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
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.streamlareextractor.StreamlareExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
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("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "$prefix Fastream:")
|
||||||
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url, prefix = prefix)
|
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("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()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}.getOrNull() ?: emptyList()
|
}.getOrNull() ?: emptyList()
|
||||||
|
|
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.animeextension.es.pelisplushd
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
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.AnimeFilter
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
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.filemoonextractor.FilemoonExtractor
|
||||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
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.streamlareextractor.StreamlareExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
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("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "Fastream:")
|
||||||
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url)
|
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("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()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}.getOrNull() ?: emptyList()
|
}.getOrNull() ?: emptyList()
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -72,6 +72,7 @@ data class Episode(
|
||||||
@SerialName("lang") val languages: EpisodeLanguages,
|
@SerialName("lang") val languages: EpisodeLanguages,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EpisodeLanguages(
|
data class EpisodeLanguages(
|
||||||
@SerialName("vf") val vf: EpisodeLanguage,
|
@SerialName("vf") val vf: EpisodeLanguage,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'HentaiZM'
|
extName = 'HentaiZM'
|
||||||
extClass = '.HentaiZM'
|
extClass = '.HentaiZM'
|
||||||
extVersionCode = 3
|
extVersionCode = 4
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ class HentaiZM : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
override val name = "HentaiZM"
|
override val name = "HentaiZM"
|
||||||
|
|
||||||
override val baseUrl = "https://www.hentaizm.life"
|
override val baseUrl = "https://www.hentaizm.pro"
|
||||||
|
|
||||||
override val lang = "tr"
|
override val lang = "tr"
|
||||||
|
|
||||||
|
|