Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,15 @@
ext {
extName = 'desu-online'
extClass = '.DesuOnline'
themePkg = 'animestream'
baseUrl = 'https://desu-online.pl'
overrideVersionCode = 5
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:googledrive-extractor'))
implementation(project(':lib:sibnet-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View file

@ -0,0 +1,86 @@
package eu.kanade.tachiyomi.animeextension.pl.desuonline
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pl.desuonline.extractors.CDAExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.googledriveextractor.GoogleDriveExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
import okhttp3.Response
import java.text.SimpleDateFormat
import java.util.Locale
class DesuOnline : AnimeStream(
"pl",
"desu-online",
"https://desu-online.pl",
) {
override val dateFormatter by lazy {
SimpleDateFormat("d MMMM, yyyy", Locale("pl", "PL"))
}
private val prefServerKey = "preferred_server"
private val prefServerDefault = "CDA"
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> =
super.videoListParse(response).ifEmpty { throw Exception("Failed to fetch videos") }
private val okruExtractor by lazy { OkruExtractor(client) }
private val cdaExtractor by lazy { CDAExtractor(client, headers, "$baseUrl/") }
private val sibnetExtractor by lazy { SibnetExtractor(client) }
private val gdriveExtractor by lazy { GoogleDriveExtractor(client, headers) }
override fun getVideoList(url: String, name: String): List<Video> {
return when {
url.contains("ok.ru") -> okruExtractor.videosFromUrl(url, name)
url.contains("cda.pl") -> cdaExtractor.videosFromUrl(url, name)
url.contains("sibnet") -> sibnetExtractor.videosFromUrl(url, prefix = "$name - ")
url.contains("drive.google.com") -> {
val id = Regex("[\\w-]{28,}").find(url)?.groupValues?.get(0) ?: return emptyList()
gdriveExtractor.videosFromUrl("https://drive.google.com/uc?id=$id", videoName = name)
}
else -> emptyList()
}
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(videoSortPrefKey, videoSortPrefDefault)!!
val server = preferences.getString(prefServerKey, prefServerDefault)!!
return sortedWith(
compareBy(
{ it.quality.contains(server, true) },
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
super.setupPreferenceScreen(screen) // Quality preferences
ListPreference(screen.context).apply {
key = prefServerKey
title = "Preferred server"
entries = arrayOf("CDA", "Sibnet", "Google Drive", "ok.ru")
entryValues = arrayOf("CDA", "sibnet", "gd", "okru")
setDefaultValue(prefServerDefault)
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,105 @@
package eu.kanade.tachiyomi.animeextension.pl.desuonline.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import uy.kohesive.injekt.injectLazy
class CDAExtractor(private val client: OkHttpClient, private val headers: Headers, private val referer: String) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String, name: String): List<Video> {
val urlHost = url.toHttpUrl().host
val docHeaders = headers.newBuilder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
add("Host", urlHost)
add("Referer", referer)
}.build()
val doc = client.newCall(
GET(url, headers = docHeaders),
).execute().asJsoup()
val playerData = doc.selectFirst("div[id~=mediaplayer][player_data]")
?.attr("player_data")
?.let { json.decodeFromString<PlayerData>(it) }
?: return emptyList()
val timestamp = playerData.api.ts.substringBefore("_")
val videoData = playerData.video
var idCounter = 1
return videoData.qualities.map { (quality, qualityId) ->
val postBody = json.encodeToString(
buildJsonObject {
put("id", idCounter)
put("jsonrpc", "2.0")
put("method", "videoGetLink")
putJsonArray("params") {
add(url.toHttpUrl().pathSegments.last())
add(qualityId)
add(timestamp.toInt())
add(videoData.hash2)
}
},
).toRequestBody("application/json; charset=utf-8".toMediaType())
val postHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Host", "www.cda.pl")
add("Origin", "https://$urlHost")
add("Referer", url)
}.build()
val videoUrl = client.newCall(
POST("https://www.cda.pl/", headers = postHeaders, body = postBody),
).execute().parseAs<PostResponse>().result.resp
idCounter++
Video(videoUrl, "$name - $quality", videoUrl)
}
}
@Serializable
data class PlayerData(
val api: PlayerApi,
val video: PlayerVideoData,
) {
@Serializable
data class PlayerApi(
val ts: String,
)
@Serializable
data class PlayerVideoData(
val hash2: String,
val qualities: Map<String, String>,
)
}
@Serializable
data class PostResponse(
val result: PostResult,
) {
@Serializable
data class PostResult(
val resp: String,
)
}
}

View file

@ -0,0 +1,17 @@
ext {
extName = 'OgladajAnime'
extClass = '.OgladajAnime'
extVersionCode = 3
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:dailymotion-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:sibnet-extractor'))
implementation(project(':lib:vk-extractor'))
implementation(project(':lib:googledrive-extractor'))
implementation(project(':lib:cda-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,312 @@
package eu.kanade.tachiyomi.animeextension.pl.ogladajanime
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.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.cdaextractor.CdaPlExtractor
import eu.kanade.tachiyomi.lib.dailymotionextractor.DailymotionExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
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 uy.kohesive.injekt.injectLazy
class OgladajAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "OgladajAnime"
override val baseUrl = "https://ogladajanime.pl"
override val lang = "pl"
override val supportsLatest = true
private val json: Json by injectLazy()
private val apiHeaders = Headers.Builder()
.set("Accept", "application/json, text/plain, */*")
.set("Referer", "$baseUrl/")
.set("Origin", baseUrl)
.set("Accept-Language", "pl,en-US;q=0.7,en;q=0.3")
.set("Host", baseUrl.toHttpUrl().host)
.build()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
return GET("$baseUrl/search/page/$page", headers)
}
override fun popularAnimeSelector(): String = "div#anime_main div.card.bg-white"
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
thumbnail_url = element.selectFirst("img")?.attr("data-srcset")
title = element.selectFirst("h5.card-title > a")!!.text()
}
}
override fun popularAnimeNextPageSelector(): String = "section:has(div#anime_main)" // To nie działa zostało to tylko dlatego by ładowało ale na końcu niestety wyskakuje ze "nie znaleziono" i tak zostaje zamiast zniknać możliwe ze zle fetchuje.
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/search/new/$page", headers)
override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/search/name/$query", headers)
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeNextPageSelector(): String? = null
// prosta bez filtrów jak na razie :) są dziury ale to kiedyś sie naprawi hihi. Wystarczy dobrze wyszukać animca i powinno wyszukać.
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
return SAnime.create().apply {
// status = document.selectFirst("div.toggle-content > ul > li:contains(Status)")?.let { parseStatus(it.text()) } ?: SAnime.UNKNOWN // Nie pamietam kiedyś sie to naprawi.
description = document.selectFirst("p#animeDesc")?.text()
genre = document.select("div.row > div.col-12 > span.badge[href^=/search/name/]").joinToString(", ") {
it.text()
}
author = document.select("div.row > div.col-12:contains(Studio:) > span.badge[href=#]").joinToString(", ") {
it.text()
}
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val url = baseUrl + anime.url
return GET(url, apiHeaders)
}
override fun episodeListParse(response: Response): List<SEpisode> {
return super.episodeListParse(response).reversed()
}
override fun episodeListSelector(): String = "ul#ep_list > li:has(div > img)"
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val episodeNumber = element.attr("value").toFloatOrNull() ?: 0f
val episodeText = element.select("div > div > p").text()
val episodeImg = element.select("div > img").attr("alt").uppercase()
if (episodeText.isNotEmpty()) {
episode.name = if (episodeImg == "PL") {
"${episodeNumber.toInt()} $episodeText"
} else {
"${episodeNumber.toInt()} [$episodeImg] $episodeText"
}
} else {
episode.name = if (episodeImg == "PL") {
"${episodeNumber.toInt()} Odcinek"
} else {
"${episodeNumber.toInt()} [$episodeImg] Odcinek"
}
}
episode.episode_number = episodeNumber
episode.url = element.attr("ep_id")
return episode
}
// ============================ Video Links =============================
private fun getPlayerUrl(id: String): String {
val body = FormBody.Builder()
.add("action", "change_player_url")
.add("id", id)
.build()
return client.newCall(POST("$baseUrl/manager.php", apiHeaders, body))
.execute()
.use { response ->
response.body.string()
.substringAfter("\"data\":\"")
.substringBefore("\",")
.replace("\\", "")
}
}
override fun videoListRequest(episode: SEpisode): Request {
val body = FormBody.Builder()
.add("action", "get_player_list")
.add("id", episode.url)
.build()
return POST("$baseUrl/manager.php", apiHeaders, body)
}
private val vkExtractor by lazy { VkExtractor(client, headers) }
private val cdaExtractor by lazy { CdaPlExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val dailymotionExtractor by lazy { DailymotionExtractor(client, headers) }
private val sibnetExtractor by lazy { SibnetExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val jsonResponse = json.decodeFromString<ApiResponse>(response.body.string())
val dataObject = json.decodeFromString<ApiData>(jsonResponse.data)
val serverList = dataObject.players.mapNotNull { player ->
var sub = player.sub.uppercase()
if (player.audio == "pl") {
sub = "Lektor"
} else if (player.sub.isEmpty() && sub != "Lektor") {
sub = "Dub " + player.sub.uppercase()
}
val subGroup = if (sub == player.sub_group?.uppercase()) "" else player.sub_group
val subGroupPart = if (subGroup?.isNotEmpty() == true) " $subGroup - " else " "
val prefix = if (player.ismy > 0) {
if (player.sub == "pl" && player.sub_group?.isNotEmpty() == true) {
"[Odwrócone Kolory] $subGroup - "
} else {
"[$sub/Odwrócone Kolory]$subGroupPart"
}
} else {
if (player.sub == "pl" && player.sub_group?.isNotEmpty() == true) {
"$subGroup - "
} else {
"[$sub]$subGroupPart"
}
}
if (player.url !in listOf("vk", "cda", "mp4upload", "sibnet", "dailymotion")) {
return@mapNotNull null
}
val url = getPlayerUrl(player.id)
Pair(url, prefix)
}
// Jeśli dodadzą opcje z mozliwością edytowania mpv to zrobić tak ze jak bedą odwrócone kolory to ustawia dane do mkv <3
return serverList.parallelCatchingFlatMapBlocking { (serverUrl, prefix) ->
when {
serverUrl.contains("vk.com") -> {
vkExtractor.videosFromUrl(serverUrl, prefix)
}
serverUrl.contains("mp4upload") -> {
mp4uploadExtractor.videosFromUrl(serverUrl, headers, prefix)
}
serverUrl.contains("cda.pl") -> {
cdaExtractor.getVideosFromUrl(serverUrl, headers, prefix)
}
serverUrl.contains("dailymotion") -> {
dailymotionExtractor.videosFromUrl(serverUrl, "$prefix Dailymotion -")
}
serverUrl.contains("sibnet.ru") -> {
sibnetExtractor.videosFromUrl(serverUrl, prefix)
}
else -> emptyList()
}
}
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ==============================
@Serializable
data class ApiPlayer(
val id: String,
val audio: String? = null,
val sub: String,
val url: String,
val sub_group: String? = null,
val ismy: Int,
)
@Serializable
data class ApiData(
val players: List<ApiPlayer>,
)
@Serializable
data class ApiResponse(
val data: String,
)
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!!
val server = preferences.getString("preferred_server", "cda.pl")!!
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(server, true) },
),
).reversed()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferowana jakość"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
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()
}
}
val videoServerPref = ListPreference(screen.context).apply {
key = "preferred_server"
title = "Preferowany serwer"
entries = arrayOf("cda.pl", "Dailymotion", "Mp4upload", "Sibnet", "vk.com")
entryValues = arrayOf("cda.pl", "Dailymotion", "Mp4upload", "Sibnet", "vk.com")
setDefaultValue("cda.pl")
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(videoQualityPref)
screen.addPreference(videoServerPref)
}
}

View file

@ -0,0 +1,14 @@
ext {
extName = 'Wbijam'
extClass = '.Wbijam'
extVersionCode = 4
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:dailymotion-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:sibnet-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1,370 @@
package eu.kanade.tachiyomi.animeextension.pl.wbijam
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.animeextension.pl.wbijam.extractors.CdaPlExtractor
import eu.kanade.tachiyomi.animeextension.pl.wbijam.extractors.VkExtractor
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.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.dailymotionextractor.DailymotionExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.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 uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class Wbijam : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Wbijam"
override val baseUrl = "https://wbijam.pl"
override val lang = "pl"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN)
}
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
override fun popularAnimeSelector(): String = "button:contains(Lista anime) + div.dropdown-content > a"
override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
url = element.selectFirst("a")!!.attr("href")
thumbnail_url = ""
title = element.selectFirst("a")!!.text()
}
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
override fun latestUpdatesSelector(): String = "button:contains(Wychodzące) + div.dropdown-content > a"
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
// =============================== Search ===============================
// button:contains(Lista anime) + div.dropdown-content > a:contains(chainsaw)
override suspend fun getSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList,
): AnimesPage {
return client.newCall(searchAnimeRequest(page, query, filters))
.awaitSuccess()
.let { response ->
searchAnimeParse(response, query)
}
}
private fun searchAnimeParse(response: Response, query: String): AnimesPage {
val document = response.asJsoup()
val animes = document.select(searchAnimeSelector(query)).map { element ->
searchAnimeFromElement(element)
}
return AnimesPage(animes, false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = popularAnimeRequest(page)
private fun searchAnimeSelector(query: String): String = "button:contains(Lista anime) + div.dropdown-content > a:contains($query)"
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeSelector(): String = throw UnsupportedOperationException()
override fun searchAnimeNextPageSelector(): String? = null
// =========================== Anime Details ============================
override suspend fun getAnimeDetails(anime: SAnime) = anime
override fun animeDetailsParse(document: Document): SAnime = throw UnsupportedOperationException()
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
return GET(anime.url, headers = headers)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
var counter = 1
document.select("button:not(:contains(Wychodzące)):not(:contains(Warsztat)):not(:contains(Lista anime)) + div.dropdown-content > a").forEach seasons@{ season ->
val seasonDoc = client.newCall(
GET(response.request.url.toString() + "/${season.attr("href")}", headers = headers),
).execute().asJsoup()
seasonDoc.select("table.lista > tbody > tr").reversed().forEach { ep ->
val episode = SEpisode.create()
// Skip over openings and engings
if (preferences.getBoolean("preferred_opening", true)) {
if (season.text().contains("Openingi", true) || season.text().contains("Endingi", true)) {
return@seasons
}
}
if (ep.selectFirst("td > a") == null) {
val (name, scanlator) = if (preferences.getBoolean("preferred_season_view", true)) {
Pair(
ep.selectFirst("td")!!.text(),
season.text(),
)
} else {
Pair(
"[${season.text()}] ${ep.selectFirst("td")!!.text()}",
null,
)
}
val notUploaded = ep.selectFirst("td:contains(??.??.????)") != null
episode.name = name
episode.scanlator = if (notUploaded) {
"(Jeszcze nie przesłane) $scanlator"
} else {
scanlator
}
episode.episode_number = counter.toFloat()
episode.date_upload = ep.selectFirst("td:matches(\\d+\\.\\d+\\.\\d)")?.let { parseDate(it.text()) } ?: 0L
val urls = ep.select("td > span[class*=link]").map {
"https://${response.request.url.host}/${it.className().substringBefore("_link")}-${it.attr("rel")}.html"
}
episode.url = EpisodeType(
"single",
urls,
).toJsonString()
} else {
val (name, scanlator) = if (preferences.getBoolean("preferred_season_view", true)) {
Pair(
ep.selectFirst("td")!!.text(),
"${season.text()}${ep.selectFirst("td:matches([a-zA-Z]+):not(:has(a))")?.text()}",
)
} else {
Pair(
"[${season.text()}] ${ep.selectFirst("td")!!.text()}",
ep.selectFirst("td:matches([a-zA-Z]+):not(:has(a))")?.text(),
)
}
val notUploaded = ep.selectFirst("td:contains(??.??.????)") != null
episode.name = name
episode.episode_number = counter.toFloat()
episode.date_upload = ep.selectFirst("td:matches(\\d+\\.\\d+\\.\\d)")?.let { parseDate(it.text()) } ?: 0L
episode.scanlator = if (notUploaded) {
"(Jeszcze nie przesłane) $scanlator"
} else {
scanlator
}
episode.url = EpisodeType(
"multi",
listOf("https://${response.request.url.host}/${ep.selectFirst("td a")!!.attr("href")}"),
).toJsonString()
}
episodeList.add(episode)
counter++
}
}
return episodeList.reversed()
}
override fun episodeListSelector(): String = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val parsed = json.decodeFromString<EpisodeType>(episode.url)
val serverList = mutableListOf<String>()
parsed.url.forEach {
val document = client.newCall(GET(it)).execute().asJsoup()
if (parsed.type == "single") {
serverList.add(
document.selectFirst("iframe")?.attr("src")
?: document.selectFirst("span.odtwarzaj_vk")?.let { t -> "https://vk.com/video${t.attr("rel")}_${t.attr("id")}" } ?: "",
)
} else if (parsed.type == "multi") {
document.select("table.lista > tbody > tr.lista_hover").forEach { server ->
val urlSpan = server.selectFirst("span[class*=link]")!!
val serverDoc = client.newCall(
GET("https://${it.toHttpUrl().host}/${urlSpan.className().substringBefore("_link")}-${urlSpan.attr("rel")}.html"),
).execute().asJsoup()
serverList.add(
serverDoc.selectFirst("iframe")?.attr("src")
?: serverDoc.selectFirst("span.odtwarzaj_vk")?.let { t -> "https://vk.com/video${t.attr("rel")}_${t.attr("id")}" } ?: "",
)
}
}
}
val videoList = serverList.parallelCatchingFlatMap { serverUrl ->
when {
serverUrl.contains("mp4upload") -> {
Mp4uploadExtractor(client).videosFromUrl(serverUrl, headers)
}
serverUrl.contains("cda.pl") -> {
CdaPlExtractor(client).getVideosFromUrl(serverUrl, headers)
}
serverUrl.contains("sibnet.ru") -> {
SibnetExtractor(client).videosFromUrl(serverUrl)
}
serverUrl.contains("vk.com") -> {
VkExtractor(client).getVideosFromUrl(serverUrl, headers)
}
serverUrl.contains("dailymotion") -> {
DailymotionExtractor(client, headers).videosFromUrl(serverUrl)
}
else -> null
}.orEmpty()
}
return videoList.sort()
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ==============================
private fun EpisodeType.toJsonString(): String {
return json.encodeToString(this)
}
@Serializable
data class EpisodeType(
val type: String,
val url: List<String>,
)
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!!
val server = preferences.getString("preferred_server", "vstream")!!
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(server, true) },
),
).reversed()
}
private fun parseDate(dateStr: String): Long {
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferowana jakość"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
entryValues = arrayOf("1080", "720", "480", "360", "240")
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()
}
}
val videoServerPref = ListPreference(screen.context).apply {
key = "preferred_server"
title = "Preferowany serwer"
entries = arrayOf("cda.pl", "Dailymotion", "Mp4upload", "Sibnet", "vk.com")
entryValues = arrayOf("cda.pl", "Dailymotion", "Mp4upload", "Sibnet", "vk.com")
setDefaultValue("cda.pl")
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()
}
}
val seasonViewPref = SwitchPreferenceCompat(screen.context).apply {
key = "preferred_season_view"
title = "Przenieś nazwę sezonu do skanera"
setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit()
}
}
val openEndPref = SwitchPreferenceCompat(screen.context).apply {
key = "preferred_opening"
title = "Usuń zakończenia i otwory"
summary = "Usuń zakończenia i otwarcia z listy odcinków"
setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit()
}
}
screen.addPreference(videoQualityPref)
screen.addPreference(videoServerPref)
screen.addPreference(seasonViewPref)
screen.addPreference(openEndPref)
}
}

View file

@ -0,0 +1,129 @@
package eu.kanade.tachiyomi.animeextension.pl.wbijam.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import uy.kohesive.injekt.injectLazy
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
class CdaPlExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun getVideosFromUrl(url: String, headers: Headers): List<Video> {
val videoList = mutableListOf<Video>()
val embedHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", url.toHttpUrl().host)
.build()
val document = client.newCall(
GET(url, headers = embedHeaders),
).execute().asJsoup()
val data = json.decodeFromString<PlayerData>(
document.selectFirst("div[player_data]")!!.attr("player_data"),
)
data.video.qualities.forEach { quality ->
if (quality.value == data.video.quality) {
val videoUrl = decryptFile(data.video.file)
videoList.add(
Video(videoUrl, "cda.pl - ${quality.key}", videoUrl),
)
} else {
val jsonBody = """
{
"jsonrpc": "2.0",
"method": "videoGetLink",
"id": 1,
"params": [
"${data.video.id}",
"${quality.value}",
${data.video.ts},
"${data.video.hash2}",
{}
]
}
""".trimIndent().toRequestBody("application/json".toMediaType())
val postHeaders = Headers.headersOf(
"Content-Type",
"application/json",
"X-Requested-With",
"XMLHttpRequest",
)
val response = client.newCall(
POST("https://www.cda.pl/", headers = postHeaders, body = jsonBody),
).execute()
val parsed = json.decodeFromString<PostResponse>(
response.body.string(),
)
videoList.add(
Video(parsed.result.resp, "cda.pl - ${quality.key}", parsed.result.resp),
)
}
}
return videoList
}
// Credit: https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/cda.py
private fun decryptFile(a: String): String {
var decrypted = a
listOf("_XDDD", "_CDA", "_ADC", "_CXD", "_QWE", "_Q5", "_IKSDE").forEach { p ->
decrypted = decrypted.replace(p, "")
}
decrypted = URLDecoder.decode(decrypted, StandardCharsets.UTF_8.toString())
val b = mutableListOf<Char>()
decrypted.forEach { c ->
val f = c.code
b.add(if (f in 33..126) (33 + (f + 14) % 94).toChar() else c)
}
decrypted = b.joinToString("")
decrypted = decrypted.replace(".cda.mp4", "")
listOf(".2cda.pl", ".3cda.pl").forEach { p ->
decrypted = decrypted.replace(p, ".cda.pl")
}
if ("/upstream" in decrypted) {
decrypted = decrypted.replace("/upstream", ".mp4/upstream")
return "https://$decrypted"
}
return "https://$decrypted.mp4"
}
@Serializable
data class PlayerData(
val video: VideoObject,
) {
@Serializable
data class VideoObject(
val id: String,
val file: String,
val quality: String,
val qualities: Map<String, String>,
val ts: Int,
val hash2: String,
)
}
@Serializable
data class PostResponse(
val result: ResultObject,
) {
@Serializable
data class ResultObject(
val resp: String,
)
}
}

View file

@ -0,0 +1,39 @@
package eu.kanade.tachiyomi.animeextension.pl.wbijam.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class VkExtractor(private val client: OkHttpClient) {
fun getVideosFromUrl(url: String, headers: Headers): List<Video> {
val videoList = mutableListOf<Video>()
val documentHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", "vk.com")
.build()
val data = client.newCall(
GET(url, headers = documentHeaders),
).execute().body.string()
val videoRegex = """\"url(\d+)\":\"(.*?)\"""".toRegex()
videoRegex.findAll(data).forEach {
val quality = it.groupValues[1]
val videoUrl = it.groupValues[2].replace("\\/", "/")
val videoHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", videoUrl.toHttpUrl().host)
.add("Origin", "https://vk.com")
.add("Referer", "https://vk.com/")
.build()
videoList.add(
Video(videoUrl, "vk.com - ${quality}p", videoUrl, headers = videoHeaders),
)
}
return videoList
}
}