Add docchi, fix lycoris/vk, add lib lulustream(luluvdo)

This commit is contained in:
Hayanek 2025-02-14 20:34:57 +01:00
parent d2d3ec186a
commit 77f8bfac04
11 changed files with 684 additions and 44 deletions

View file

@ -0,0 +1,6 @@
plugins {
id("lib-android")
}
dependencies {
implementation(project(":lib:unpacker"))
}

View file

@ -0,0 +1,136 @@
package eu.kanade.tachiyomi.lib.luluextractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
import java.util.regex.Pattern
class LuluExtractor(private val client: OkHttpClient) {
private val headers = Headers.Builder()
.add("Referer", "https://luluvdo.com")
.add("Origin", "https://luluvdo.com")
.build()
fun videosFromUrl(url: String, prefix: String): List<Video> {
val videos = mutableListOf<Video>()
try {
val html = client.newCall(GET(url, headers)).execute().use { it.body.string() }
val m3u8Url = extractM3u8Url(html) ?: return emptyList()
val fixedUrl = fixM3u8Link(m3u8Url)
val quality = getResolution(fixedUrl)
videos.add(Video(fixedUrl, "${prefix}Lulu - $quality", fixedUrl))
} catch (e: Exception) {
e.printStackTrace()
}
return videos
}
private fun extractM3u8Url(html: String): String? {
return when {
html.contains("eval(function(p,a,c,k,e") -> {
val unpacked = JavaScriptUnpacker.unpack(html) ?: return null
Pattern.compile("sources:\\[\\{file:\"([^\"]+)\"")
.matcher(unpacked)
.takeIf { it.find() }
?.group(1)
}
else -> {
Pattern.compile("sources: \\[\\{file:\"(https?://[^\"]+)\"")
.matcher(html)
.takeIf { it.find() }
?.group(1)
}
}
}
private fun fixM3u8Link(link: String): String {
val paramOrder = listOf("t", "s", "e", "f")
val baseUrl = link.split("?").first()
val params = link.split("?").getOrNull(1)?.split("&") ?: emptyList()
val paramMap = mutableMapOf<String, String>()
val extraParams = mutableMapOf(
"i" to "0.3",
"sp" to "0"
)
params.forEachIndexed { index, param ->
val parts = param.split("=")
when {
parts.size == 2 -> {
val (key, value) = parts
if (key in paramOrder) paramMap[key] = value
else extraParams[key] = value
}
index < paramOrder.size -> paramMap[paramOrder[index]] = parts.firstOrNull() ?: ""
}
}
return buildString {
append(baseUrl)
append("?")
append(paramOrder.joinToString("&") { "$it=${paramMap[it]}" })
append("&")
append(extraParams.map { "${it.key}=${it.value}" }.joinToString("&"))
}
}
private fun getResolution(m3u8Url: String): String {
return try {
val content = client.newCall(GET(m3u8Url, headers)).execute()
.use { it.body.string() }
Pattern.compile("RESOLUTION=\\d+x(\\d+)")
.matcher(content)
.takeIf { it.find() }
?.group(1)
?.let { "${it}p" }
?: "Unknown"
} catch (e: Exception) {
"Unknown"
}
}
}
object JavaScriptUnpacker {
private val UNPACK_REGEX = Regex(
"""}\('(.*)', *(\d+), *(\d+), *'(.*?)'\.split\('\|'\)""",
RegexOption.DOT_MATCHES_ALL
)
fun unpack(encodedJs: String): String? {
val match = UNPACK_REGEX.find(encodedJs) ?: return null
val (payload, radixStr, countStr, symtabStr) = match.destructured
val radix = radixStr.toIntOrNull() ?: return null
val count = countStr.toIntOrNull() ?: return null
val symtab = symtabStr.split('|')
if (symtab.size != count) throw IllegalArgumentException("Invalid symtab size")
val baseDict = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
.take(radix)
.withIndex()
.associate { it.value to it.index }
return Regex("""\b\w+\b""").replace(payload) { mr ->
symtab.getOrNull(unbase(mr.value, radix, baseDict)) ?: mr.value
}.replace("\\", "")
}
private fun unbase(value: String, radix: Int, dict: Map<Char, Int>): Int {
var result = 0
var multiplier = 1
for (char in value.reversed()) {
result += dict[char]?.times(multiplier) ?: 0
multiplier *= radix
}
return result
}
}

View file

@ -2,52 +2,73 @@ package eu.kanade.tachiyomi.lib.lycorisextractor
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import android.util.Base64
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.nio.charset.Charset
class LycorisCafeExtractor(private val client: OkHttpClient) { class LycorisCafeExtractor(private val client: OkHttpClient) {
private val urlApi = "https://zglyjsqsvevnyudbazgy.supabase.co" private val GETSECONDARYURL = "https://www.lycoris.cafe/api/watch/getSecondaryLink"
private val apiLycoris = "https://www.lycoris.cafe" private val GETLNKURL = "https://www.lycoris.cafe/api/watch/getLink"
private val apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpnbHlqc3FzdmV2bnl1ZGJhemd5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTM0ODYxNjYsImV4cCI6MjAwOTA2MjE2Nn0.H-_D56Tk5_8ebK9X700aFFI-zOPavq7ikhRNtU2njQ0"
private val json: Json by injectLazy() private val json: Json by injectLazy()
// Credit: https://github.com/skoruppa/docchi-stremio-addon/blob/main/app/players/lycoris.py
fun getVideosFromUrl(url: String, headers: Headers, prefix: String): List<Video> { fun getVideosFromUrl(url: String, headers: Headers, prefix: String): List<Video> {
val videos = mutableListOf<Video>()
val embedHeaders = headers.newBuilder() val embedHeaders = headers.newBuilder()
.add("apikey", apiKey) .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36")
.add("Host", "zglyjsqsvevnyudbazgy.supabase.co")
.build() .build()
val httpUrl = url.toHttpUrl() val document = client.newCall(
val title = httpUrl.queryParameter("title") GET(url, headers = embedHeaders),
val episode = httpUrl.queryParameter("episode") ).execute().asJsoup()
val response = client.newCall( val scripts = document.select("script")
GET("$urlApi/rest/v1/anime?select=video_links&anime_title=eq.${title}&episode_number=eq.${episode}", headers = embedHeaders)
).execute()
// Parse the document to extract JSON val episodeDataPattern = Regex("episodeInfo\\s*:\\s*(\\{.*?\\}),", RegexOption.DOT_MATCHES_ALL)
val document = response.asJsoup() var episodeData: String? = null
val jsonString = document.body().text() // Extracts the text content of the body tag
// Deserialize JSON for (script in scripts) {
val data: List<PlayerData> = json.decodeFromString(jsonString) val content = script.data()
val match = episodeDataPattern.find(content)
// Create Video objects for each quality if (match != null) {
val videos = mutableListOf<Video>() episodeData = match.groupValues[1]
data.firstOrNull()?.video_links?.let { videoLinks -> break
val sdLink = resolveLink(videoLinks.SD, headers) }
val hdLink = resolveLink(videoLinks.HD, headers) }
val fhdLink = resolveLink(videoLinks.FHD, headers)
val result = mutableMapOf<String, String?>()
val patterns = listOf(
"id" to Regex("id\\s*:\\s*(\\d+)"),
"FHD" to Regex("FHD\\s*:\\s*\"([^\"]+)\""),
"HD" to Regex("HD\\s*:\\s*\"([^\"]+)\""),
"SD" to Regex("SD\\s*:\\s*\"([^\"]+)\"")
)
patterns.forEach { (key, pattern) ->
result[key] = episodeData?.let { pattern.find(it)?.groups?.get(1)?.value }
}
var linkList: String? = fetchAndDecodeVideo(client, result["id"].toString(), isSecondary = false).toString()
val fhdLink = fetchAndDecodeVideo(client, result["FHD"].toString(), isSecondary = true).toString()
val sdLink = fetchAndDecodeVideo(client, result["SD"].toString(), isSecondary = true).toString()
val hdLink = fetchAndDecodeVideo(client, result["HD"].toString(), isSecondary = true).toString()
if (linkList.isNullOrBlank() || linkList == "{}") {
if (fhdLink.isNotEmpty()) { if (fhdLink.isNotEmpty()) {
videos.add(Video(fhdLink, "${prefix}lycoris.cafe - 1080p", fhdLink)) videos.add(Video(fhdLink, "${prefix}lycoris.cafe - 1080p", fhdLink))
} }
@ -57,29 +78,149 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
if (sdLink.isNotEmpty()) { if (sdLink.isNotEmpty()) {
videos.add(Video(sdLink, "${prefix}lycoris.cafe - 480p", sdLink)) videos.add(Video(sdLink, "${prefix}lycoris.cafe - 480p", sdLink))
} }
}else {
val videoLinks = Json.decodeFromString<VideoLinks>(linkList)
videoLinks.FHD?.takeIf { checkLinks(client, it) }?.let {
videos.add(Video(it, "${prefix}lycoris.cafe - 1080p", it))
}?: videos.add(Video(fhdLink, "${prefix}lycoris.cafe - 1080p", fhdLink))
videoLinks.HD?.takeIf { checkLinks(client, it) }?.let {
videos.add(Video(it, "${prefix}lycoris.cafe - 720p", it))
}?: videos.add(Video(hdLink, "${prefix}lycoris.cafe - 720p", hdLink))
videoLinks.SD?.takeIf { checkLinks(client, it) }?.let {
videos.add(Video(it, "${prefix}lycoris.cafe - 480p", it))
}?: videos.add(Video(sdLink, "${prefix}lycoris.cafe - 480p", sdLink))
} }
return videos return videos
}
private fun resolveLink(link: String, headers: Headers): String {
return if(!link.startsWith("https://")) decodeOrFetchLink(link, headers) else link
} }
private fun decodeOrFetchLink(encodedUrl: String, headers: Headers): String { private fun decodeVideoLinks(encodedUrl: String?): Any? {
val response = client.newCall(GET("$apiLycoris/api/getLink?id=$encodedUrl", headers = headers)).execute() if (encodedUrl.isNullOrEmpty()) {
return response.body?.string().orEmpty() return null
}
if (!encodedUrl.endsWith("LC")) {
return encodedUrl
}
val encodedUrlWithoutSignature = encodedUrl.dropLast(2)
val decoded = encodedUrlWithoutSignature
.reversed()
.map { (it.code - 7).toChar() }
.joinToString("")
return try {
val base64Decoded = Base64.decode(decoded, Base64.DEFAULT)
base64Decoded.toString(Charsets.UTF_8)
} catch (e: Exception) {
null
}
}
private fun fetchAndDecodeVideo(client: OkHttpClient, episodeId: String, isSecondary: Boolean = false): Any? {
val url: HttpUrl
if (isSecondary) {
val convertedText = episodeId.toByteArray(Charset.forName("UTF-8")).toString(Charset.forName("ISO-8859-1"))
val unicodeEscape = decodePythonEscape(convertedText)
val finalText = unicodeEscape.toByteArray(Charsets.ISO_8859_1).toString(Charsets.UTF_8)
url = GETLNKURL.toHttpUrl().newBuilder()
?.addQueryParameter("link", finalText)
?.build() ?: throw IllegalStateException("Invalid URL")
} else {
url = GETSECONDARYURL.toHttpUrl().newBuilder()
?.addQueryParameter("id", episodeId)
?.build() ?: throw IllegalStateException("Invalid URL")
}
client.newCall(GET(url))
.execute()
.use { response ->
val data = response.body.string() ?: ""
return decodeVideoLinks(data)
}
}
private fun checkLinks(client: OkHttpClient, link: String): Boolean {
client.newCall(GET(link)).execute().use { response ->
return response.code.toString() == "200"
}
}
//thx deepseek
private fun decodePythonEscape(text: String): String {
// 1. Obsługa kontynuacji linii (backslash + newline)
val withoutLineContinuation = text.replace("\\\n", "")
// 2. Regex do wykrywania wszystkich sekwencji escape
val regex = Regex(
"""\\U([0-9a-fA-F]{8})|""" + // \UXXXXXXXX
"""\\u([0-9a-fA-F]{4})|""" + // \uXXXX
"""\\x([0-9a-fA-F]{2})|""" + // \xHH
"""\\([0-7]{1,3})|""" + // \OOO (octal)
"""\\([btnfr"'$\\\\])""" // \n, \t, itd.
)
return regex.replace(withoutLineContinuation) { match ->
val (u8, u4, x2, octal, simple) = match.destructured
when {
u8.isNotEmpty() -> handleUnicode8(u8)
u4.isNotEmpty() -> handleUnicode4(u4)
x2.isNotEmpty() -> handleHex(x2)
octal.isNotEmpty() -> handleOctal(octal)
simple.isNotEmpty() -> handleSimple(simple)
else -> match.value
}
}
}
private fun handleUnicode8(hex: String): String {
val codePoint = hex.toInt(16)
return if (codePoint in 0..0x10FFFF) {
String(intArrayOf(codePoint), 0, 1)
} else {
"\\U$hex"
}
}
private fun handleUnicode4(hex: String) = hex.toInt(16).toChar().toString()
private fun handleHex(hex: String) = hex.toInt(16).toChar().toString()
private fun handleOctal(octal: String): String {
val value = octal.toInt(8)
return (value and 0xFF).toChar().toString()
}
private fun handleSimple(c: String): String = when (c) {
"b" -> "\u0008"
"t" -> "\t"
"n" -> "\n"
"f" -> "\u000C"
"r" -> "\r"
"\"" -> "\""
"'" -> "'"
"$" -> "$"
"\\" -> "\\"
else -> "\\$c"
} }
@Serializable @Serializable
data class PlayerData( data class VideoLinks(
val video_links: VideoLinks, val HD: String? = null,
) { val SD: String? = null,
@Serializable val FHD: String? = null,
data class VideoLinks( val Source: String? = null,
val HD: String = "", val preview: String? = null,
val SD: String = "", val SourceMKV: String? = null
val FHD: String = "", )
val Source: String = "",
val SourceMKV: String = ""
)
}
} }

View file

@ -20,9 +20,9 @@ class VkExtractor(private val client: OkHttpClient, private val headers: Headers
.build() .build()
} }
fun videosFromUrl(url: String, prefix: String) = videosFromUrl(url) { "${prefix}Vk:$it" } fun videosFromUrl(url: String, prefix: String) = videosFromUrl(url) { "${prefix}Vk - $it" }
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "Vk:$quality" }): List<Video> { fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "Vk - $quality" }): List<Video> {
val data = client.newCall(GET(url, documentHeaders)).execute().body.string() val data = client.newCall(GET(url, documentHeaders)).execute().body.string()
return REGEX_VIDEO.findAll(data).map { return REGEX_VIDEO.findAll(data).map {

View file

@ -0,0 +1,20 @@
ext {
extName = 'Docchi'
extClass = '.Docchi'
extVersionCode = 1
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'))
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:lycoris-extractor'))
implementation(project(':lib:lulu-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,337 @@
package eu.kanade.tachiyomi.animeextension.pl.docchi
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.lib.cdaextractor.CdaPlExtractor
import eu.kanade.tachiyomi.lib.dailymotionextractor.DailymotionExtractor
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.luluextractor.LuluExtractor
import eu.kanade.tachiyomi.lib.lycorisextractor.LycorisCafeExtractor
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.util.parallelCatchingFlatMapBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class Docchi : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Docchi"
override val baseUrl = "https://docchi.pl"
private val baseApiUrl = "https://api.docchi.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)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) =
GET("$baseApiUrl/v1/series/list?limit=20&before=${(page - 1) * 20}")
override fun popularAnimeParse(response: Response): AnimesPage {
val animeArray: List<ApiList> = json.decodeFromString(response.body.string())
val entries = animeArray.map { animeDetail ->
SAnime.create().apply {
title = animeDetail.title
url = "$baseUrl/production/as/${animeDetail.slug}"
thumbnail_url = animeDetail.cover
}
}
val hasNextPage = animeArray.isNotEmpty()
return AnimesPage(entries, hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseApiUrl/v1/series/list?limit=20&before=${(page - 1) * 20}&sort=DESC")
override fun latestUpdatesParse(response: Response): AnimesPage = popularAnimeParse(response)
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
GET("$baseApiUrl/v1/series/related/$query")
override fun searchAnimeParse(response: Response): AnimesPage {
val animeArray: List<ApiSearch> = json.decodeFromString(response.body.string())
val entries = animeArray.map { animeDetail ->
SAnime.create().apply {
title = animeDetail.title
url = "$baseUrl/production/as/${animeDetail.slug}"
thumbnail_url = animeDetail.cover
}
}
return AnimesPage(entries, false)
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request =
GET("$baseApiUrl/v1/episodes/count/${anime.url.substringAfterLast("/")}")
override fun episodeListParse(response: Response): List<SEpisode> {
val episodeList: List<EpisodeList> = json.decodeFromString(response.body.string())
return episodeList.map { episode ->
SEpisode.create().apply {
name = "${episode.anime_episode_number.toInt()} Odcinek"
url = "$baseUrl/production/as/${episode.anime_id}/${episode.anime_episode_number}"
episode_number = episode.anime_episode_number
// date_upload = episode.created_at.toLong()
}
}.reversed()
}
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request =
GET("$baseApiUrl/v1/series/find/${anime.url.substringAfterLast("/")}")
override fun animeDetailsParse(response: Response): SAnime {
val animeDetail: ApiDetail = json.decodeFromString(response.body.string())
return SAnime.create().apply {
title = animeDetail.title
description = animeDetail.description
genre = animeDetail.genres.joinToString(", ")
}
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request = GET(
"$baseApiUrl/v1/episodes/find/${
episode.url.substringBeforeLast("/").substringAfterLast("/")
}/${episode.episode_number}",
)
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) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val lycorisExtractor by lazy { LycorisCafeExtractor(client) }
private val luluExtractor by lazy { LuluExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val videolist: List<VideoList> = json.decodeFromString(response.body.string())
val serverList = videolist.mapNotNull { player ->
var sub = player.translator_title.uppercase()
val prefix = if (player.isInverted) {
"[Odwrócone Kolory] $sub - "
} else {
"$sub - "
}
val playerName = player.player_hosting.lowercase()
if (playerName !in listOf(
"vk",
"cda",
"mp4upload",
"sibnet",
"dailymotion",
"dood",
"lycoris.cafe",
"lulustream",
)
) {
return@mapNotNull null
}
Pair(player.player, 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)
}
serverUrl.contains("dood") -> {
doodExtractor.videosFromUrl(serverUrl, "$prefix Dood")
}
serverUrl.contains("lycoris.cafe") -> {
lycorisExtractor.getVideosFromUrl(serverUrl, headers, prefix)
}
serverUrl.contains("luluvdo.com") -> {
luluExtractor.videosFromUrl(serverUrl, prefix)
}
else -> emptyList()
}
}
}
// ============================= Utilities ==============================
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)
}
@Serializable
data class ApiList(
val mal_id: Int,
val adult_content: String,
val title: String,
val title_en: String,
val slug: String,
val cover: String,
val genres: List<String>,
val broadcast_day: String?,
val aired_from: String?,
val episodes: Int,
val season: String,
val season_year: Int,
val series_type: String,
)
@Serializable
data class ApiSearch(
val mal_id: Int,
val ani_id: Int?,
val title: String,
val title_en: String,
val slug: String,
val cover: String,
val adult_content: String,
val series_type: String,
val episodes: Int,
val season: String,
val season_year: Int,
)
@Serializable
data class ApiDetail(
val id: Int,
val mal_id: Int,
val ani_id: Int?,
val adult_content: String,
val title: String,
val title_en: String,
val slug: String,
val slug_oa: String?,
val description: String,
val cover: String,
val bg: String?,
val genres: List<String>,
val broadcast_day: String?,
val aired_from: String?,
val episodes: Int,
val season: String,
val season_year: Int,
val series_type: String,
val ads: String?,
val modified: String?,
)
@Serializable
data class EpisodeList(
val anime_id: String,
val anime_episode_number: Float,
val isInverted: String,
val created_at: String,
val bg: String?,
)
@Serializable
data class VideoList(
val id: Int,
val anime_id: String,
val anime_episode_number: Float,
val player: String,
val player_hosting: String,
val created_at: String,
val translator: Boolean,
val translator_title: String,
val translator_url: String?,
val isInverted: Boolean,
val bg: String?,
)
}