Initial commit
15
src/pl/desuonline/build.gradle
Normal 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'))
|
||||
}
|
BIN
src/pl/desuonline/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/pl/desuonline/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/pl/desuonline/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/pl/desuonline/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
src/pl/desuonline/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/pl/desuonline/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 126 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
17
src/pl/ogladajanime/build.gradle
Normal 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'))
|
||||
}
|
BIN
src/pl/ogladajanime/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/pl/ogladajanime/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
src/pl/ogladajanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
src/pl/ogladajanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
src/pl/ogladajanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
14
src/pl/wbijam/build.gradle
Normal 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"
|
||||
}
|
BIN
src/pl/wbijam/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/pl/wbijam/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/pl/wbijam/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/pl/wbijam/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
src/pl/wbijam/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/pl/wbijam/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 58 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|