Stuff (#1520)
This commit is contained in:
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'Kayoanime'
|
||||
pkgNameSuffix = 'en.kayoanime'
|
||||
extClass = '.Kayoanime'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,151 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.kayoanime
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
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.URLEncoder
|
||||
|
||||
class DriveIndexExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun getEpisodesFromIndex(indexUrl: String, path: String, flipOrder: Boolean): List<SEpisode> {
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
|
||||
val basePathCounter = indexUrl.toHttpUrl().pathSegments.size
|
||||
|
||||
var counter = 1
|
||||
|
||||
fun traverseDirectory(url: String) {
|
||||
var newToken: String? = ""
|
||||
var newPageIndex = 0
|
||||
|
||||
while (newToken != null) {
|
||||
val popHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Host", url.toHttpUrl().host)
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", URLEncoder.encode(url, "UTF-8"))
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
val popBody = "password=&page_token=$newToken&page_index=$newPageIndex".toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
||||
|
||||
val parsedBody = client.newCall(
|
||||
POST(url, body = popBody, headers = popHeaders),
|
||||
).execute().body.string().decrypt()
|
||||
val parsed = json.decodeFromString<ResponseData>(parsedBody)
|
||||
|
||||
parsed.data.files.forEach { item ->
|
||||
if (item.mimeType.endsWith("folder")) {
|
||||
val newUrl = joinUrl(url, item.name).addSuffix("/")
|
||||
traverseDirectory(newUrl)
|
||||
}
|
||||
if (item.mimeType.startsWith("video/")) {
|
||||
val episode = SEpisode.create()
|
||||
val epUrl = joinUrl(url, item.name)
|
||||
val paths = epUrl.toHttpUrl().pathSegments
|
||||
|
||||
// Get other info
|
||||
val extraInfo = if (paths.size > basePathCounter) {
|
||||
"/$path/" + paths.subList(basePathCounter - 1, paths.size - 1).joinToString("/") { it.trimInfo() }
|
||||
} else {
|
||||
"/$path"
|
||||
}
|
||||
val size = item.size?.toLongOrNull()?.let { formatFileSize(it) }
|
||||
|
||||
episode.name = item.name.trimInfo()
|
||||
episode.url = epUrl
|
||||
episode.scanlator = if (flipOrder) {
|
||||
"$extraInfo • ${size ?: "N/A"}"
|
||||
} else {
|
||||
"${size ?: "N/A"} • $extraInfo"
|
||||
}
|
||||
episode.episode_number = counter.toFloat()
|
||||
episode.date_upload = -1L
|
||||
counter++
|
||||
|
||||
episodeList.add(episode)
|
||||
}
|
||||
}
|
||||
|
||||
newToken = parsed.nextPageToken
|
||||
newPageIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
traverseDirectory(indexUrl)
|
||||
|
||||
return episodeList
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResponseData(
|
||||
val nextPageToken: String? = null,
|
||||
val data: DataObject,
|
||||
) {
|
||||
@Serializable
|
||||
data class DataObject(
|
||||
val files: List<FileObject>,
|
||||
) {
|
||||
@Serializable
|
||||
data class FileObject(
|
||||
val mimeType: String,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val modifiedTime: String? = null,
|
||||
val size: String? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.trimInfo(): String {
|
||||
var newString = this.replaceFirst("""^\[\w+\] ?""".toRegex(), "")
|
||||
val regex = """( ?\[[\s\w-]+\]| ?\([\s\w-]+\))(\.mkv|\.mp4|\.avi)?${'$'}""".toRegex()
|
||||
|
||||
while (regex.containsMatchIn(newString)) {
|
||||
newString = regex.replace(newString) { matchResult ->
|
||||
matchResult.groups[2]?.value ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
return newString.trim()
|
||||
}
|
||||
|
||||
private fun String.addSuffix(suffix: String): String {
|
||||
return if (this.endsWith(suffix)) {
|
||||
this
|
||||
} else {
|
||||
this.plus(suffix)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.decrypt(): String {
|
||||
return Base64.decode(this.reversed().substring(24, this.length - 20), Base64.DEFAULT).toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun joinUrl(path1: String, path2: String): String {
|
||||
return path1.removeSuffix("/") + "/" + path2.removePrefix("/")
|
||||
}
|
||||
|
||||
private fun formatFileSize(bytes: Long): String {
|
||||
return when {
|
||||
bytes >= 1073741824 -> "%.2f GB".format(bytes / 1073741824.0)
|
||||
bytes >= 1048576 -> "%.2f MB".format(bytes / 1048576.0)
|
||||
bytes >= 1024 -> "%.2f KB".format(bytes / 1024.0)
|
||||
bytes > 1 -> "$bytes bytes"
|
||||
bytes == 1L -> "$bytes byte"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
@ -17,10 +17,6 @@ import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -130,7 +126,7 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
popularAnimeFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = popularAnimeNextPageSelector()?.let { selector ->
|
||||
val hasNextPage = popularAnimeNextPageSelector().let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
if (hasNextPage) {
|
||||
@ -354,9 +350,9 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
if (type.startsWith("video")) {
|
||||
val episode = SEpisode.create()
|
||||
episode.scanlator = if (preferences.getBoolean("scanlator_order", false)) {
|
||||
"/$path • $size"
|
||||
"/${path.trim()} • $size"
|
||||
} else {
|
||||
"$size • /$path"
|
||||
"$size • /${path.trim()}"
|
||||
}
|
||||
episode.name = name.removePrefix("[Kayoanime] ")
|
||||
episode.url = "https://drive.google.com/uc?id=$id"
|
||||
@ -383,12 +379,34 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
||||
val indexExtractor = DriveIndexExtractor(client, headers)
|
||||
document.select("div.toggle:has(> div.toggle-content > a[href*=tinyurl.com])").forEach { season ->
|
||||
season.select("a[href*=tinyurl.com]").forEach {
|
||||
val url = it.selectFirst("a[href*=tinyurl.com]")!!.attr("href")
|
||||
val redirected = noRedirectClient.newCall(GET(url)).execute()
|
||||
redirected.headers["location"]?.let { location ->
|
||||
if (location.toHttpUrl().host.contains("workers.dev")) {
|
||||
episodeList.addAll(
|
||||
indexExtractor.getEpisodesFromIndex(
|
||||
location,
|
||||
getVideoPathsFromElement(season) + " " + it.text(),
|
||||
preferences.getBoolean("scanlator_order", false),
|
||||
),
|
||||
)
|
||||
// getVideoPathsFromElement(season) + " " + it.text()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
private fun getVideoPathsFromElement(element: Element): String {
|
||||
return element.selectFirst("h3")!!.text()
|
||||
.substringBefore("480p").substringBefore("720p").substringBefore("1080p")
|
||||
.replace("Download The Anime From Drive", "", true)
|
||||
}
|
||||
|
||||
override fun episodeListSelector(): String = throw Exception("Not used")
|
||||
@ -398,7 +416,15 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
|
||||
val videoList = GoogleDriveExtractor(client, headers).videosFromUrl(episode.url)
|
||||
val host = episode.url.toHttpUrl().host
|
||||
val videoList = if (host == "drive.google.com") {
|
||||
GoogleDriveExtractor(client, headers).videosFromUrl(episode.url)
|
||||
} else if (host.contains("workers.dev")) {
|
||||
getIndexVideoUrl(episode.url)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
return Observable.just(videoList)
|
||||
}
|
||||
|
||||
@ -410,6 +436,38 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun getIndexVideoUrl(url: String): List<Video> {
|
||||
val doc = client.newCall(
|
||||
GET("$url?a=view"),
|
||||
).execute().asJsoup()
|
||||
|
||||
val script = doc.selectFirst("script:containsData(videodomain)")?.data()
|
||||
?: doc.selectFirst("script:containsData(downloaddomain)")?.data()
|
||||
?: return listOf(Video(url, "Video", url))
|
||||
|
||||
if (script.contains("\"second_domain_for_dl\":false")) {
|
||||
return listOf(Video(url, "Video", url))
|
||||
}
|
||||
|
||||
val domainUrl = if (script.contains("videodomain", true)) {
|
||||
script
|
||||
.substringAfter("\"videodomain\":\"")
|
||||
.substringBefore("\"")
|
||||
} else {
|
||||
script
|
||||
.substringAfter("\"downloaddomain\":\"")
|
||||
.substringBefore("\"")
|
||||
}
|
||||
|
||||
val videoUrl = if (domainUrl.isBlank()) {
|
||||
url
|
||||
} else {
|
||||
domainUrl + url.toHttpUrl().encodedPath
|
||||
}
|
||||
|
||||
return listOf(Video(videoUrl, "Video", videoUrl))
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PostResponse(
|
||||
val hide_next: Boolean,
|
||||
@ -451,12 +509,6 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
// From Dopebox
|
||||
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
||||
runBlocking {
|
||||
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val scanlatorOrder = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = "scanlator_order"
|
||||
|
Reference in New Issue
Block a user