Merge branch 'almightyhak:main' into main

This commit is contained in:
Dark25 2024-08-11 13:27:41 +01:00 committed by GitHub
commit 29a1f7d934
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 1900 additions and 121 deletions

View file

@ -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

View file

@ -40,6 +40,14 @@ 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'

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
plugins {
id("lib-android")
}
dependencies {
implementation(project(":lib:playlist-utils"))
}

View file

@ -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,
)
}
}
}

View file

@ -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(

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".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>

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -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)
}
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

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

View file

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

View file

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

View file

@ -282,9 +282,8 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val parsed = response.parseAs<ServerResponse>() val parsed = response.parseAs<ServerResponse>()
val embedLink = utils.vrfDecrypt(DECRYPTION_KEY, 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)
vidsrcExtractor.videosFromUrl(embedLink, server.serverName, server.type) "megaf" -> vidsrcExtractor.videosFromUrl(embedLink, "MegaF", server.type)
}
"moonf" -> filemoonExtractor.videosFromUrl(embedLink, "MoonF - ${server.type} - ") "moonf" -> filemoonExtractor.videosFromUrl(embedLink, "MoonF - ${server.type} - ")
"streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList() "streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList()
"mp4u" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}") "mp4u" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}")

View 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'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -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)
}
}

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

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

View file

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

View file

@ -2,21 +2,21 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application> <application>
<activity <activity
android:name=".fr.franime.FrAnimeUrlActivity" android:name=".fr.franime.FrAnimeUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay" android:theme="@android:style/Theme.NoDisplay"
> >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:scheme="https" android:scheme="https"
android:host="franime.fr" android:host="franime.fr"
android:pathPattern="/anime/..*" android:pathPattern="/anime/..*"
/> />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

View file

@ -11,4 +11,5 @@ dependencies {
implementation(project(':lib:vk-extractor')) implementation(project(':lib:vk-extractor'))
implementation(project(':lib:sendvid-extractor')) implementation(project(':lib:sendvid-extractor'))
implementation(project(':lib:sibnet-extractor')) implementation(project(':lib:sibnet-extractor'))
implementation project(':lib:vidmoly-extractor')
} }

View file

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor 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.lib.vkextractor.VkExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
@ -101,7 +102,7 @@ class FrAnime : AnimeHttpSource() {
SEpisode.create().apply { SEpisode.create().apply {
setUrlWithoutDomain(anime.url + "&ep=${index + 1}") setUrlWithoutDomain(anime.url + "&ep=${index + 1}")
name = episode.title name = episode.title ?: "Episode ${index + 1}"
episode_number = (index + 1).toFloat() 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 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 videos = players.withIndex().parallelCatchingFlatMap { (index, playerName) ->
val apiUrl = "$videoBaseUrl/$episodeLang/$index" val apiUrl = "$videoBaseUrl/$episodeLang/$index"
val playerUrl = client.newCall(GET(apiUrl, headers)).await().body.string() val playerUrl = client.newCall(GET(apiUrl, headers)).await().body.string()
when (playerName) { when (playerName) {
"vido" -> listOf(Video(playerUrl, "FRAnime (Vido)", playerUrl)) "sendvid" -> sendvidExtractor.videosFromUrl(playerUrl)
"sendvid" -> SendvidExtractor(client, headers).videosFromUrl(playerUrl) "sibnet" -> sibnetExtractor.videosFromUrl(playerUrl)
"sibnet" -> SibnetExtractor(client).videosFromUrl(playerUrl) "vk" -> vkExtractor.videosFromUrl(playerUrl)
"vk" -> VkExtractor(client, headers).videosFromUrl(playerUrl) "vidmoly" -> vidMolyExtractor.videosFromUrl(playerUrl)
else -> emptyList() else -> emptyList()
} }
} }

View file

@ -20,7 +20,6 @@ typealias BigIntegerJson =
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
private object BigIntegerSerializer : KSerializer<BigInteger> { private object BigIntegerSerializer : KSerializer<BigInteger> {
override val descriptor = PrimitiveSerialDescriptor("java.math.BigInteger", PrimitiveKind.LONG) override val descriptor = PrimitiveSerialDescriptor("java.math.BigInteger", PrimitiveKind.LONG)
override fun deserialize(decoder: Decoder): BigInteger = override fun deserialize(decoder: Decoder): BigInteger =
@ -68,7 +67,7 @@ data class Season(
@Serializable @Serializable
data class Episode( data class Episode(
@SerialName("title") val title: String = "!No Title!", @SerialName("title") val title: String?,
@SerialName("lang") val languages: EpisodeLanguages, @SerialName("lang") val languages: EpisodeLanguages,
) )

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'NekoSama' extName = 'NekoSama'
extClass = '.NekoSama' extClass = '.NekoSama'
extVersionCode = 11 extVersionCode = 12
isNsfw = true isNsfw = true
} }

View file

@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import okhttp3.HttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -28,7 +29,14 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Neko-Sama" 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" override val lang = "fr"
@ -72,17 +80,19 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
else -> "vostfr" else -> "vostfr"
} }
return when { val url = when {
query.isNotBlank() -> GET("$baseUrl/animes-search-$typeSearch.json?$query") query.isNotBlank() -> "$baseUrl/animes-search-$typeSearch.json?$query"
typeFilter.state != 0 || query.isNotBlank() -> when (page) { typeFilter.state != 0 || query.isNotBlank() -> when (page) {
1 -> GET("$baseUrl/${typeFilter.toUriPart()}") 1 -> "$baseUrl/${typeFilter.toUriPart()}"
else -> GET("$baseUrl/${typeFilter.toUriPart()}/$page") else -> "$baseUrl/${typeFilter.toUriPart()}/page$page"
} }
else -> when (page) { else -> when (page) {
1 -> GET("$baseUrl/anime/") 1 -> "$baseUrl/anime/"
else -> GET("$baseUrl/anime/page/$page") else -> "$baseUrl/anime/page$page"
} }
} }
return GET(url)
} }
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
@ -95,11 +105,13 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val animes = jsonSearch val animes = jsonSearch
.filter { it.title.orEmpty().lowercase().contains(query) } .filter { it.title.orEmpty().lowercase().contains(query) }
.mapNotNull { .mapNotNull {
SAnime.create().apply { val anime = SAnime.create().apply {
url = it.url ?: return@mapNotNull null url = it.url?.substringAfterLast("/")?.substringBefore("-") ?: return@mapNotNull null
title = it.title ?: return@mapNotNull null title = it.title ?: return@mapNotNull null
thumbnail_url = it.url_image ?: "$baseUrl/images/default_poster.png" thumbnail_url = it.url_image ?: "$baseUrl/images/default_poster.png"
setUrlWithoutDomain(url) // call setUrlWithoutDomain on the SAnime instance
} }
anime
} }
AnimesPage(animes, false) AnimesPage(animes, false)
} }
@ -131,9 +143,10 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
SAnime.create().apply { SAnime.create().apply {
val itemUrl = item.url ?: return@mapNotNull null val itemUrl = item.url ?: return@mapNotNull null
title = item.title ?: return@mapNotNull null title = item.title ?: return@mapNotNull null
val type = itemUrl.substringAfterLast("-") val animeId = itemUrl.substringAfterLast("/").substringBefore("-")
url = itemUrl.replace("episode", "info").substringBeforeLast("-").substringBeforeLast("-") + "-$type" val titleSlug = title.replace("[^a-zA-Z0-9 -]".toRegex(), "").replace(" ", "-").lowercase()
thumbnail_url = item.url_image ?: "$baseUrl/images/default_poster.png" 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 { override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
val text = element.text() val text = element.text()
name = text.substringBeforeLast(" - ") val episodeNumber = text.substringAfterLast("- ").toFloatOrNull() ?: 0F
episode_number = text.substringAfterLast("- ").toFloatOrNull() ?: 0F name = "Épisode ${episodeNumber.toInt()}"
episode_number = episodeNumber
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
@ -291,8 +305,8 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val PLAYERS_REGEX = Regex("video\\s*\\[\\d*]\\s*=\\s*'(.*?)'") private val PLAYERS_REGEX = Regex("video\\s*\\[\\d*]\\s*=\\s*'(.*?)'")
private const val PREF_DOMAIN_KEY = "pref_domain_key" private const val PREF_DOMAIN_KEY = "pref_domain_key"
private const val PREF_DOMAIN_TITLE = "Preferred domain" 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 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_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality" private const val PREF_QUALITY_TITLE = "Preferred quality"