This commit is contained in:
Secozzi
2023-04-20 15:02:33 +02:00
committed by GitHub
parent ad89aab19f
commit d998959703
3 changed files with 218 additions and 15 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Kayoanime'
pkgNameSuffix = 'en.kayoanime'
extClass = '.Kayoanime'
extVersionCode = 1
extVersionCode = 2
libVersion = '13'
}

View File

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

View File

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