Merge branch 'almightyhak:main' into main
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 tested the modifications by compiling and running the extension through Android Studio
|
||||
- [ ] Have removed `web_hi_res_512.png` when adding a new extension
|
||||
- [ ] Have made sure all the icons are in png format
|
||||
|
|
8
.github/workflows/build_push.yml
vendored
|
@ -40,6 +40,14 @@ jobs:
|
|||
files_separator: " "
|
||||
safe_output: false
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6 # v6.1.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
# This step is going to commit, but this will not trigger another workflow.
|
||||
- name: Bump extensions that uses a modified lib
|
||||
if: steps.modified-libs.outputs.any_changed == 'true'
|
||||
|
|
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)
|
||||
}
|
||||
}
|
7
lib/vidmoly-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package eu.kanade.tachiyomi.lib.vidmolyextractor
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.internal.EMPTY_HEADERS
|
||||
|
||||
class VidMolyExtractor(private val client: OkHttpClient, headers: Headers = EMPTY_HEADERS) {
|
||||
|
||||
private val baseUrl = "https://vidmoly.to"
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||
|
||||
private val headers: Headers = headers.newBuilder()
|
||||
.set("Origin", baseUrl)
|
||||
.set("Referer", "$baseUrl/")
|
||||
.build()
|
||||
|
||||
private val sourcesRegex = Regex("sources: (.*?]),")
|
||||
private val urlsRegex = Regex("""file:"(.*?)"""")
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
val document = client.newCall(
|
||||
GET(url, headers.newBuilder().set("Sec-Fetch-Dest", "iframe").build())
|
||||
).execute().asJsoup()
|
||||
val script = document.selectFirst("script:containsData(sources)")!!.data()
|
||||
val sources = sourcesRegex.find(script)!!.groupValues[1]
|
||||
val urls = urlsRegex.findAll(sources).map { it.groupValues[1] }.toList()
|
||||
return urls.flatMap {
|
||||
playlistUtils.extractFromHls(it,
|
||||
videoNameGen = { quality -> "${prefix}VidMoly - $quality" },
|
||||
masterHeaders = headers,
|
||||
videoHeaders = headers,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
package eu.kanade.tachiyomi.lib.vidsrcextractor
|
||||
|
||||
import android.util.Base64
|
||||
import app.cash.quickjs.QuickJs
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.lib.vidsrcextractor.MediaResponseBody.Result
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.CacheControl
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLDecoder
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
@ -21,54 +22,32 @@ import javax.crypto.spec.SecretKeySpec
|
|||
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val cacheControl = CacheControl.Builder().noStore().build()
|
||||
private val noCacheClient = client.newBuilder()
|
||||
.cache(null)
|
||||
.build()
|
||||
|
||||
private val keys by lazy {
|
||||
noCacheClient.newCall(
|
||||
GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
|
||||
).execute().parseAs<List<String>>()
|
||||
}
|
||||
|
||||
fun videosFromUrl(embedLink: String, hosterName: String, type: String = "", subtitleList: List<Track> = emptyList()): List<Video> {
|
||||
fun videosFromUrl(
|
||||
embedLink: String,
|
||||
hosterName: String,
|
||||
type: String = "",
|
||||
subtitleList: List<Track> = emptyList(),
|
||||
): List<Video> {
|
||||
val host = embedLink.toHttpUrl().host
|
||||
val apiUrl = getApiUrl(embedLink, keys)
|
||||
val apiUrl = getApiUrl(embedLink)
|
||||
|
||||
val apiHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Host", host)
|
||||
add("Referer", URLDecoder.decode(embedLink, "UTF-8"))
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
val response = client.newCall(GET(apiUrl)).execute()
|
||||
val data = response.parseAs<MediaResponseBody>()
|
||||
|
||||
val response = client.newCall(
|
||||
GET(apiUrl, apiHeaders),
|
||||
).execute()
|
||||
|
||||
val data = runCatching {
|
||||
response.parseAs<MediaResponseBody>()
|
||||
}.getOrElse { // Keys are out of date
|
||||
val newKeys = noCacheClient.newCall(
|
||||
GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
|
||||
).execute().parseAs<List<String>>()
|
||||
val newApiUrL = getApiUrl(embedLink, newKeys)
|
||||
client.newCall(
|
||||
GET(newApiUrL, apiHeaders),
|
||||
).execute().parseAs()
|
||||
}
|
||||
val decrypted = vrfDecrypt(data.result)
|
||||
val result = json.decodeFromString<Result>(decrypted)
|
||||
|
||||
return playlistUtils.extractFromHls(
|
||||
data.result.sources.first().file,
|
||||
playlistUrl = result.sources.first().file,
|
||||
referer = "https://$host/",
|
||||
videoNameGen = { q -> hosterName + (if (type.isBlank()) "" else " - $type") + " - $q" },
|
||||
subtitleList = subtitleList + data.result.tracks.toTracks(),
|
||||
subtitleList = subtitleList + result.tracks.toTracks(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getApiUrl(embedLink: String, keyList: List<String>): String {
|
||||
private fun getApiUrl(embedLink: String): String {
|
||||
val host = embedLink.toHttpUrl().host
|
||||
val params = embedLink.toHttpUrl().let { url ->
|
||||
url.queryParameterNames.map {
|
||||
|
@ -76,13 +55,13 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
|
|||
}
|
||||
}
|
||||
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
||||
val encodedID = encodeID(vidId, keyList)
|
||||
val apiSlug = callFromFuToken(host, encodedID, embedLink)
|
||||
val apiSlug = encodeID(vidId, ENCRYPTION_KEY1)
|
||||
val h = encodeID(vidId, ENCRYPTION_KEY2)
|
||||
|
||||
return buildString {
|
||||
append("https://")
|
||||
append(host)
|
||||
append("/")
|
||||
append("/mediainfo/")
|
||||
append(apiSlug)
|
||||
if (params.isNotEmpty()) {
|
||||
append("?")
|
||||
|
@ -91,51 +70,23 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
|
|||
"${it.first}=${it.second}"
|
||||
},
|
||||
)
|
||||
append("&h=$h")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeID(videoID: String, keyList: List<String>): String {
|
||||
val rc4Key1 = SecretKeySpec(keyList[0].toByteArray(), "RC4")
|
||||
val rc4Key2 = SecretKeySpec(keyList[1].toByteArray(), "RC4")
|
||||
val cipher1 = Cipher.getInstance("RC4")
|
||||
val cipher2 = Cipher.getInstance("RC4")
|
||||
cipher1.init(Cipher.DECRYPT_MODE, rc4Key1, cipher1.parameters)
|
||||
cipher2.init(Cipher.DECRYPT_MODE, rc4Key2, cipher2.parameters)
|
||||
var encoded = videoID.toByteArray()
|
||||
|
||||
encoded = cipher1.doFinal(encoded)
|
||||
encoded = cipher2.doFinal(encoded)
|
||||
encoded = Base64.encode(encoded, Base64.DEFAULT)
|
||||
return encoded.toString(Charsets.UTF_8).replace("/", "_").trim()
|
||||
private fun encodeID(videoID: String, key: String): String {
|
||||
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
|
||||
val cipher = Cipher.getInstance("RC4")
|
||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||
return Base64.encode(cipher.doFinal(videoID.toByteArray()), Base64.DEFAULT)
|
||||
.toString(Charsets.UTF_8)
|
||||
.replace("+", "-")
|
||||
.replace("/", "_")
|
||||
.trim()
|
||||
}
|
||||
|
||||
private fun callFromFuToken(host: String, data: String, embedLink: String): String {
|
||||
val refererHeaders = headers.newBuilder().apply {
|
||||
add("Referer", embedLink)
|
||||
}.build()
|
||||
|
||||
val fuTokenScript = client.newCall(
|
||||
GET("https://$host/futoken", headers = refererHeaders),
|
||||
).execute().body.string()
|
||||
|
||||
val js = buildString {
|
||||
append("(function")
|
||||
append(
|
||||
fuTokenScript.substringAfter("window")
|
||||
.substringAfter("function")
|
||||
.replace("jQuery.ajax(", "")
|
||||
.substringBefore("+location.search"),
|
||||
)
|
||||
append("}(\"$data\"))")
|
||||
}
|
||||
|
||||
return QuickJs.create().use {
|
||||
it.evaluate(js)?.toString()!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<MediaResponseBody.Result.SubTrack>.toTracks(): List<Track> {
|
||||
private fun List<Result.SubTrack>.toTracks(): List<Track> {
|
||||
return filter {
|
||||
it.kind == "captions"
|
||||
}.mapNotNull {
|
||||
|
@ -147,17 +98,32 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
|
|||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun vrfDecrypt(input: String): String {
|
||||
var vrf = Base64.decode(input.toByteArray(), Base64.URL_SAFE)
|
||||
val rc4Key = SecretKeySpec(DECRYPTION_KEY.toByteArray(), "RC4")
|
||||
val cipher = Cipher.getInstance("RC4")
|
||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||
vrf = cipher.doFinal(vrf)
|
||||
return URLDecoder.decode(vrf.toString(Charsets.UTF_8), "utf-8")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ENCRYPTION_KEY1 = "8Qy3mlM2kod80XIK"
|
||||
private const val ENCRYPTION_KEY2 = "BgKVSrzpH2Enosgm"
|
||||
private const val DECRYPTION_KEY = "9jXDYBZUcTcTZveM"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MediaResponseBody(
|
||||
val status: Int,
|
||||
val result: Result,
|
||||
val result: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class Result(
|
||||
val sources: ArrayList<Source>,
|
||||
val tracks: ArrayList<SubTrack> = ArrayList(),
|
||||
val sources: List<Source>,
|
||||
val tracks: List<SubTrack> = emptyList(),
|
||||
) {
|
||||
@Serializable
|
||||
data class Source(
|
||||
|
|
22
src/all/subsplease/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=".all.subsplease.SubPleaseUrlActivity"
|
||||
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="subsplease.org"
|
||||
android:pathPattern="/anime/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
8
src/all/subsplease/build.gradle
Normal file
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'Subsplease'
|
||||
extClass = '.Subsplease'
|
||||
extVersionCode = 1
|
||||
containsNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/all/subsplease/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
src/all/subsplease/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/all/subsplease/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/all/subsplease/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
src/all/subsplease/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,208 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.subsplease
|
||||
|
||||
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.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.Exception
|
||||
|
||||
class Subsplease : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "Subsplease"
|
||||
|
||||
override val baseUrl = "https://subsplease.org"
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val json = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/api/?f=schedule&tz=Europe/Berlin")
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val responseString = response.body.string()
|
||||
return parsePopularAnimeJson(responseString)
|
||||
}
|
||||
|
||||
private fun parsePopularAnimeJson(jsonLine: String?): AnimesPage {
|
||||
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
|
||||
val jObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
val jOe = jObject.jsonObject["schedule"]!!.jsonObject.entries
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
jOe.forEach {
|
||||
val itJ = it.value.jsonArray
|
||||
for (item in itJ) {
|
||||
val anime = SAnime.create()
|
||||
anime.title = item.jsonObject["title"]!!.jsonPrimitive.content
|
||||
anime.setUrlWithoutDomain("$baseUrl/shows/${item.jsonObject["page"]!!.jsonPrimitive.content}")
|
||||
anime.thumbnail_url = baseUrl + item.jsonObject["image_url"]?.jsonPrimitive?.content
|
||||
animeList.add(anime)
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, hasNextPage = false)
|
||||
}
|
||||
|
||||
// episodes
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val sId = document.select("#show-release-table").attr("sid")
|
||||
val responseString = client.newCall(GET("$baseUrl/api/?f=show&tz=Europe/Berlin&sid=$sId")).execute().body.string()
|
||||
val url = "$baseUrl/api/?f=show&tz=Europe/Berlin&sid=$sId"
|
||||
return parseEpisodeAnimeJson(responseString, url)
|
||||
}
|
||||
|
||||
private fun parseEpisodeAnimeJson(jsonLine: String?, url: String): List<SEpisode> {
|
||||
val jsonData = jsonLine ?: return emptyList()
|
||||
val jObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val epE = jObject["episode"]!!.jsonObject.entries
|
||||
epE.forEach {
|
||||
val itJ = it.value.jsonObject
|
||||
val episode = SEpisode.create()
|
||||
val num = itJ["episode"]!!.jsonPrimitive.content
|
||||
episode.episode_number = num.toFloat()
|
||||
episode.name = "Episode $num"
|
||||
episode.setUrlWithoutDomain("$url&num=$num")
|
||||
episodeList.add(episode)
|
||||
}
|
||||
return episodeList
|
||||
}
|
||||
|
||||
// Video Extractor
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val responseString = response.body.string()
|
||||
val num = response.request.url.toString()
|
||||
.substringAfter("num=")
|
||||
return videosFromElement(responseString, num)
|
||||
}
|
||||
|
||||
private fun videosFromElement(jsonLine: String?, num: String): List<Video> {
|
||||
val jsonData = jsonLine ?: return emptyList()
|
||||
val jObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
val epE = jObject["episode"]!!.jsonObject.entries
|
||||
val videoList = mutableListOf<Video>()
|
||||
epE.forEach {
|
||||
val itJ = it.value.jsonObject
|
||||
val epN = itJ["episode"]!!.jsonPrimitive.content
|
||||
if (num == epN) {
|
||||
val dowArray = itJ["downloads"]!!.jsonArray
|
||||
for (item in dowArray) {
|
||||
val quality = item.jsonObject["res"]!!.jsonPrimitive.content + "p"
|
||||
val videoUrl = item.jsonObject["magnet"]!!.jsonPrimitive.content
|
||||
videoList.add(Video(videoUrl, quality, videoUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", "1080p")
|
||||
if (quality != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality.contains(quality)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/api/?f=search&tz=Europe/Berlin&s=$query")
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val responseString = response.body.string()
|
||||
return parseSearchAnimeJson(responseString)
|
||||
}
|
||||
|
||||
private fun parseSearchAnimeJson(jsonLine: String?): AnimesPage {
|
||||
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
|
||||
val jObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
val jE = jObject.entries
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
jE.forEach {
|
||||
val itJ = it.value.jsonObject
|
||||
val anime = SAnime.create()
|
||||
anime.title = itJ.jsonObject["show"]!!.jsonPrimitive.content
|
||||
anime.setUrlWithoutDomain("$baseUrl/shows/${itJ.jsonObject["page"]!!.jsonPrimitive.content}")
|
||||
anime.thumbnail_url = baseUrl + itJ.jsonObject["image_url"]?.jsonPrimitive?.content
|
||||
animeList.add(anime)
|
||||
}
|
||||
return AnimesPage(animeList, hasNextPage = false)
|
||||
}
|
||||
|
||||
// Details
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val document = response.asJsoup()
|
||||
val anime = SAnime.create()
|
||||
anime.description = document.select("div.series-syn p ").text()
|
||||
return anime
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage = throw Exception("Not used")
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
|
||||
|
||||
// Preferences
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val qualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Default-Quality"
|
||||
entries = arrayOf("1080p", "720p", "480p")
|
||||
entryValues = arrayOf("1080", "720", "480")
|
||||
setDefaultValue("1080")
|
||||
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()
|
||||
}
|
||||
}
|
||||
screen.addPreference(qualityPref)
|
||||
}
|
||||
}
|
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 {
|
||||
extName = 'Aniwave'
|
||||
extClass = '.Aniwave'
|
||||
extVersionCode = 73
|
||||
extVersionCode = 74
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -282,9 +282,8 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
val parsed = response.parseAs<ServerResponse>()
|
||||
val embedLink = utils.vrfDecrypt(DECRYPTION_KEY, parsed.result.url)
|
||||
when (server.serverName) {
|
||||
"vidstream", "megaf" -> {
|
||||
vidsrcExtractor.videosFromUrl(embedLink, server.serverName, server.type)
|
||||
}
|
||||
"vidstream" -> vidsrcExtractor.videosFromUrl(embedLink, "Vidstream", server.type)
|
||||
"megaf" -> vidsrcExtractor.videosFromUrl(embedLink, "MegaF", server.type)
|
||||
"moonf" -> filemoonExtractor.videosFromUrl(embedLink, "MoonF - ${server.type} - ")
|
||||
"streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList()
|
||||
"mp4u" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}")
|
||||
|
|
26
src/es/cinecalidad/build.gradle
Normal file
|
@ -0,0 +1,26 @@
|
|||
ext {
|
||||
extName = 'CineCalidad'
|
||||
extClass = '.CineCalidad'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:vudeo-extractor'))
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:streamlare-extractor'))
|
||||
implementation(project(':lib:yourupload-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:mixdrop-extractor'))
|
||||
implementation(project(':lib:burstcloud-extractor'))
|
||||
implementation(project(':lib:fastream-extractor'))
|
||||
implementation(project(':lib:upstream-extractor'))
|
||||
implementation(project(':lib:streamhidevid-extractor'))
|
||||
}
|
BIN
src/es/cinecalidad/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/es/cinecalidad/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/es/cinecalidad/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/es/cinecalidad/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/es/cinecalidad/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1,325 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.cinecalidad
|
||||
|
||||
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.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.burstcloudextractor.BurstCloudExtractor
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Calendar
|
||||
|
||||
class CineCalidad : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "CineCalidad"
|
||||
|
||||
override val baseUrl = "https://www.cinecalidad.ec"
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "Voe"
|
||||
private val SERVER_LIST = arrayOf(
|
||||
"YourUpload", "BurstCloud", "Voe", "Mp4Upload", "Doodstream",
|
||||
"Upload", "BurstCloud", "Upstream", "StreamTape", "Amazon",
|
||||
"Fastream", "Filemoon", "StreamWish", "Okru", "Streamlare",
|
||||
)
|
||||
|
||||
private val REGEX_EPISODE_NAME = "^S(\\d+)-E(\\d+)$".toRegex()
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = ".item[data-cf] .custom"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/page/$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
title = element.select("img").attr("alt")
|
||||
thumbnail_url = element.select("img").attr("data-src")
|
||||
setUrlWithoutDomain(element.selectFirst("a")?.attr("href") ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = ".nextpostslink"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
return if (document.location().contains("ver-pelicula")) {
|
||||
listOf(
|
||||
SEpisode.create().apply {
|
||||
name = "PELÍCULA"
|
||||
episode_number = 1f
|
||||
setUrlWithoutDomain(document.location())
|
||||
},
|
||||
)
|
||||
} else {
|
||||
document.select(".mark-1").mapIndexed { idx, it ->
|
||||
val epLink = it.select(".episodiotitle a").attr("href")
|
||||
val epName = it.select(".episodiotitle a").text()
|
||||
val nameSeasonEpisode = it.select(".numerando").text()
|
||||
val matchResult = REGEX_EPISODE_NAME.matchEntire(nameSeasonEpisode)
|
||||
|
||||
val episodeName = if (matchResult != null) {
|
||||
val season = matchResult.groups[1]?.value
|
||||
val episode = matchResult.groups[2]?.value
|
||||
"T$season - E$episode - $epName"
|
||||
} else {
|
||||
"$nameSeasonEpisode $epName"
|
||||
}
|
||||
|
||||
SEpisode.create().apply {
|
||||
name = episodeName
|
||||
episode_number = idx + 1f
|
||||
setUrlWithoutDomain(epLink)
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "uwu"
|
||||
|
||||
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
return document.select("#playeroptionsul li").parallelCatchingFlatMapBlocking {
|
||||
val link = it.attr("data-option")
|
||||
serverVideoResolver(link)
|
||||
}
|
||||
}
|
||||
|
||||
private fun serverVideoResolver(url: String): List<Video> {
|
||||
val embedUrl = url.lowercase()
|
||||
return runCatching {
|
||||
when {
|
||||
embedUrl.contains("voe") -> VoeExtractor(client).videosFromUrl(url)
|
||||
embedUrl.contains("ok.ru") || embedUrl.contains("okru") -> OkruExtractor(client).videosFromUrl(url)
|
||||
embedUrl.contains("filemoon") || embedUrl.contains("moonplayer") -> {
|
||||
val vidHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", "https://${url.toHttpUrl().host}/")
|
||||
.build()
|
||||
FilemoonExtractor(client).videosFromUrl(url, prefix = "Filemoon:", headers = vidHeaders)
|
||||
}
|
||||
!embedUrl.contains("disable") && (embedUrl.contains("amazon") || embedUrl.contains("amz")) -> {
|
||||
val body = client.newCall(GET(url)).execute().asJsoup()
|
||||
return if (body.select("script:containsData(var shareId)").toString().isNotBlank()) {
|
||||
val shareId = body.selectFirst("script:containsData(var shareId)")!!.data()
|
||||
.substringAfter("shareId = \"").substringBefore("\"")
|
||||
val amazonApiJson = client.newCall(GET("https://www.amazon.com/drive/v1/shares/$shareId?resourceVersion=V2&ContentType=JSON&asset=ALL"))
|
||||
.execute().asJsoup()
|
||||
val epId = amazonApiJson.toString().substringAfter("\"id\":\"").substringBefore("\"")
|
||||
val amazonApi =
|
||||
client.newCall(GET("https://www.amazon.com/drive/v1/nodes/$epId/children?resourceVersion=V2&ContentType=JSON&limit=200&sort=%5B%22kind+DESC%22%2C+%22modifiedDate+DESC%22%5D&asset=ALL&tempLink=true&shareId=$shareId"))
|
||||
.execute().asJsoup()
|
||||
val videoUrl = amazonApi.toString().substringAfter("\"FOLDER\":").substringAfter("tempLink\":\"").substringBefore("\"")
|
||||
listOf(Video(videoUrl, "Amazon", videoUrl))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
embedUrl.contains("uqload") -> UqloadExtractor(client).videosFromUrl(url)
|
||||
embedUrl.contains("mp4upload") -> Mp4uploadExtractor(client).videosFromUrl(url, headers)
|
||||
embedUrl.contains("wishembed") || embedUrl.contains("streamwish") || embedUrl.contains("strwish") || embedUrl.contains("wish") || embedUrl.contains("wishfast") -> {
|
||||
val docHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://streamwish.to")
|
||||
.add("Referer", "https://streamwish.to/")
|
||||
.build()
|
||||
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
|
||||
}
|
||||
embedUrl.contains("doodstream") || embedUrl.contains("dood.") || embedUrl.contains("ds2play") || embedUrl.contains("doods.") -> {
|
||||
DoodExtractor(client).videosFromUrl(url.replace("https://doodstream.com/e/", "https://dood.to/e/"), "DoodStream", false)
|
||||
}
|
||||
embedUrl.contains("streamlare") -> StreamlareExtractor(client).videosFromUrl(url)
|
||||
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
|
||||
embedUrl.contains("burstcloud") || embedUrl.contains("burst") -> BurstCloudExtractor(client).videoFromUrl(url, headers = headers)
|
||||
embedUrl.contains("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "Fastream:")
|
||||
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url)
|
||||
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> listOf(StreamTapeExtractor(client).videoFromUrl(url, quality = "StreamTape")!!)
|
||||
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") || embedUrl.contains("streamvid") || embedUrl.contains("vidhide") -> StreamHideVidExtractor(client).videosFromUrl(url)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.getOrNull() ?: emptyList()
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
|
||||
val genreQuery = if (genreFilter.toUriPart().contains("fecha-de-lanzamiento")) {
|
||||
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
|
||||
"$baseUrl/${genreFilter.toUriPart()}/$currentYear/page/$page"
|
||||
} else {
|
||||
"$baseUrl/${genreFilter.toUriPart()}/page/$page"
|
||||
}
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$query")
|
||||
genreFilter.state != 0 -> GET(genreQuery)
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||
GenreFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Géneros",
|
||||
arrayOf(
|
||||
Pair("<Selecionar>", ""),
|
||||
Pair("Series", "ver-serie"),
|
||||
Pair("Estrenos", "fecha-de-lanzamiento/"),
|
||||
Pair("Destacadas", "#destacado"),
|
||||
Pair("Acción", "genero-de-la-pelicula/accion"),
|
||||
Pair("Animación", "genero-de-la-pelicula/animacion"),
|
||||
Pair("Anime", "genero-de-la-pelicula/anime"),
|
||||
Pair("Aventura", "genero-de-la-pelicula/aventura"),
|
||||
Pair("Bélico", "genero-de-la-pelicula/belica"),
|
||||
Pair("Ciencia ficción", "genero-de-la-pelicula/ciencia-ficcion"),
|
||||
Pair("Crimen", "genero-de-la-pelicula/crimen"),
|
||||
Pair("Comedia", "genero-de-la-pelicula/comedia"),
|
||||
Pair("Documental", "genero-de-la-pelicula/documental"),
|
||||
Pair("Drama", "genero-de-la-pelicula/drama"),
|
||||
Pair("Familiar", "genero-de-la-pelicula/familia"),
|
||||
Pair("Fantasía", "genero-de-la-pelicula/fantasia"),
|
||||
Pair("Historia", "genero-de-la-pelicula/historia"),
|
||||
Pair("Música", "genero-de-la-pelicula/musica"),
|
||||
Pair("Misterio", "genero-de-la-pelicula/misterio"),
|
||||
Pair("Terror", "genero-de-la-pelicula/terror"),
|
||||
Pair("Suspenso", "genero-de-la-pelicula/suspense"),
|
||||
Pair("Romance", "genero-de-la-pelicula/romance"),
|
||||
Pair("Dc Comics", "genero-de-la-pelicula/peliculas-de-dc-comics-online-cinecalidad"),
|
||||
Pair("Marvel", "genero-de-la-pelicula/universo-marvel"),
|
||||
),
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
return SAnime.create().apply {
|
||||
thumbnail_url = document.selectFirst(".single_left table img")?.attr("data-src")
|
||||
description = document.select(".single_left table p").text().removeSurrounding("\"").substringBefore("Títulos:")
|
||||
status = SAnime.UNKNOWN
|
||||
document.select(".single_left table p > span").map { it.text() }.map { textContent ->
|
||||
when {
|
||||
"Género" in textContent -> genre = textContent.replace("Género:", "").trim().split(", ").joinToString { it }
|
||||
"Creador" in textContent -> author = textContent.replace("Creador:", "").trim().split(", ").firstOrNull()
|
||||
"Elenco" in textContent -> artist = textContent.replace("Elenco:", "").trim().split(", ").firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
|
||||
return GET("$baseUrl/fecha-de-lanzamiento/$currentYear/page/$page")
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularAnimeSelector()
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
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)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
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)
|
||||
}
|
||||
}
|
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"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,21 +2,21 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".fr.franime.FrAnimeUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
>
|
||||
android:name=".fr.franime.FrAnimeUrlActivity"
|
||||
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:scheme="https"
|
||||
android:host="franime.fr"
|
||||
android:pathPattern="/anime/..*"
|
||||
/>
|
||||
android:scheme="https"
|
||||
android:host="franime.fr"
|
||||
android:pathPattern="/anime/..*"
|
||||
/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
|
@ -11,4 +11,5 @@ dependencies {
|
|||
implementation(project(':lib:vk-extractor'))
|
||||
implementation(project(':lib:sendvid-extractor'))
|
||||
implementation(project(':lib:sibnet-extractor'))
|
||||
implementation project(':lib:vidmoly-extractor')
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
|||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
|
||||
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
|
||||
import eu.kanade.tachiyomi.lib.vidmolyextractor.VidMolyExtractor
|
||||
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
|
@ -101,7 +102,7 @@ class FrAnime : AnimeHttpSource() {
|
|||
|
||||
SEpisode.create().apply {
|
||||
setUrlWithoutDomain(anime.url + "&ep=${index + 1}")
|
||||
name = episode.title
|
||||
name = episode.title ?: "Episode ${index + 1}"
|
||||
episode_number = (index + 1).toFloat()
|
||||
}
|
||||
}
|
||||
|
@ -123,14 +124,19 @@ class FrAnime : AnimeHttpSource() {
|
|||
|
||||
val players = if (episodeLang == "vo") episodeData.languages.vo.players else episodeData.languages.vf.players
|
||||
|
||||
val sendvidExtractor by lazy { SendvidExtractor(client, headers) }
|
||||
val sibnetExtractor by lazy { SibnetExtractor(client) }
|
||||
val vkExtractor by lazy { VkExtractor(client, headers) }
|
||||
val vidMolyExtractor by lazy { VidMolyExtractor(client) }
|
||||
|
||||
val videos = players.withIndex().parallelCatchingFlatMap { (index, playerName) ->
|
||||
val apiUrl = "$videoBaseUrl/$episodeLang/$index"
|
||||
val playerUrl = client.newCall(GET(apiUrl, headers)).await().body.string()
|
||||
when (playerName) {
|
||||
"vido" -> listOf(Video(playerUrl, "FRAnime (Vido)", playerUrl))
|
||||
"sendvid" -> SendvidExtractor(client, headers).videosFromUrl(playerUrl)
|
||||
"sibnet" -> SibnetExtractor(client).videosFromUrl(playerUrl)
|
||||
"vk" -> VkExtractor(client, headers).videosFromUrl(playerUrl)
|
||||
"sendvid" -> sendvidExtractor.videosFromUrl(playerUrl)
|
||||
"sibnet" -> sibnetExtractor.videosFromUrl(playerUrl)
|
||||
"vk" -> vkExtractor.videosFromUrl(playerUrl)
|
||||
"vidmoly" -> vidMolyExtractor.videosFromUrl(playerUrl)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ typealias BigIntegerJson =
|
|||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private object BigIntegerSerializer : KSerializer<BigInteger> {
|
||||
|
||||
override val descriptor = PrimitiveSerialDescriptor("java.math.BigInteger", PrimitiveKind.LONG)
|
||||
|
||||
override fun deserialize(decoder: Decoder): BigInteger =
|
||||
|
@ -68,7 +67,7 @@ data class Season(
|
|||
|
||||
@Serializable
|
||||
data class Episode(
|
||||
@SerialName("title") val title: String = "!No Title!",
|
||||
@SerialName("title") val title: String?,
|
||||
@SerialName("lang") val languages: EpisodeLanguages,
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'NekoSama'
|
||||
extClass = '.NekoSama'
|
||||
extVersionCode = 11
|
||||
extVersionCode = 12
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.network.GET
|
|||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
|
@ -28,7 +29,14 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
|
||||
override val name = "Neko-Sama"
|
||||
|
||||
override val baseUrl by lazy { "https://" + preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
|
||||
override val baseUrl by lazy {
|
||||
val domain = preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||
HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host(domain)
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
|
||||
override val lang = "fr"
|
||||
|
||||
|
@ -72,17 +80,19 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
else -> "vostfr"
|
||||
}
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> GET("$baseUrl/animes-search-$typeSearch.json?$query")
|
||||
val url = when {
|
||||
query.isNotBlank() -> "$baseUrl/animes-search-$typeSearch.json?$query"
|
||||
typeFilter.state != 0 || query.isNotBlank() -> when (page) {
|
||||
1 -> GET("$baseUrl/${typeFilter.toUriPart()}")
|
||||
else -> GET("$baseUrl/${typeFilter.toUriPart()}/$page")
|
||||
1 -> "$baseUrl/${typeFilter.toUriPart()}"
|
||||
else -> "$baseUrl/${typeFilter.toUriPart()}/page$page"
|
||||
}
|
||||
else -> when (page) {
|
||||
1 -> GET("$baseUrl/anime/")
|
||||
else -> GET("$baseUrl/anime/page/$page")
|
||||
1 -> "$baseUrl/anime/"
|
||||
else -> "$baseUrl/anime/page$page"
|
||||
}
|
||||
}
|
||||
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
|
@ -95,11 +105,13 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
val animes = jsonSearch
|
||||
.filter { it.title.orEmpty().lowercase().contains(query) }
|
||||
.mapNotNull {
|
||||
SAnime.create().apply {
|
||||
url = it.url ?: return@mapNotNull null
|
||||
val anime = SAnime.create().apply {
|
||||
url = it.url?.substringAfterLast("/")?.substringBefore("-") ?: return@mapNotNull null
|
||||
title = it.title ?: return@mapNotNull null
|
||||
thumbnail_url = it.url_image ?: "$baseUrl/images/default_poster.png"
|
||||
setUrlWithoutDomain(url) // call setUrlWithoutDomain on the SAnime instance
|
||||
}
|
||||
anime
|
||||
}
|
||||
AnimesPage(animes, false)
|
||||
}
|
||||
|
@ -131,9 +143,10 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
SAnime.create().apply {
|
||||
val itemUrl = item.url ?: return@mapNotNull null
|
||||
title = item.title ?: return@mapNotNull null
|
||||
val type = itemUrl.substringAfterLast("-")
|
||||
url = itemUrl.replace("episode", "info").substringBeforeLast("-").substringBeforeLast("-") + "-$type"
|
||||
thumbnail_url = item.url_image ?: "$baseUrl/images/default_poster.png"
|
||||
val animeId = itemUrl.substringAfterLast("/").substringBefore("-")
|
||||
val titleSlug = title.replace("[^a-zA-Z0-9 -]".toRegex(), "").replace(" ", "-").lowercase()
|
||||
url = "anime/info/$animeId-${titleSlug}_vostfr"
|
||||
thumbnail_url = item.url_image ?: "/images/default_poster.png"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,8 +192,9 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
val text = element.text()
|
||||
name = text.substringBeforeLast(" - ")
|
||||
episode_number = text.substringAfterLast("- ").toFloatOrNull() ?: 0F
|
||||
val episodeNumber = text.substringAfterLast("- ").toFloatOrNull() ?: 0F
|
||||
name = "Épisode ${episodeNumber.toInt()}"
|
||||
episode_number = episodeNumber
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
@ -291,8 +305,8 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
private val PLAYERS_REGEX = Regex("video\\s*\\[\\d*]\\s*=\\s*'(.*?)'")
|
||||
private const val PREF_DOMAIN_KEY = "pref_domain_key"
|
||||
private const val PREF_DOMAIN_TITLE = "Preferred domain"
|
||||
private const val PREF_DOMAIN_DEFAULT = "animecat.net"
|
||||
private val PREF_DOMAIN_ENTRIES = arrayOf("animecat.net", "neko-sama.fr")
|
||||
private const val PREF_DOMAIN_DEFAULT = "animecat.net"
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
|
|