diff --git a/src/en/kayoanime/build.gradle b/src/en/kayoanime/build.gradle index 191c40fd7..26858d0ed 100644 --- a/src/en/kayoanime/build.gradle +++ b/src/en/kayoanime/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'Kayoanime' pkgNameSuffix = 'en.kayoanime' extClass = '.Kayoanime' - extVersionCode = 1 + extVersionCode = 2 libVersion = '13' } diff --git a/src/en/kayoanime/src/eu/kanade/tachiyomi/animeextension/en/kayoanime/DriveIndexExtractor.kt b/src/en/kayoanime/src/eu/kanade/tachiyomi/animeextension/en/kayoanime/DriveIndexExtractor.kt new file mode 100644 index 000000000..a70616e15 --- /dev/null +++ b/src/en/kayoanime/src/eu/kanade/tachiyomi/animeextension/en/kayoanime/DriveIndexExtractor.kt @@ -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 { + val episodeList = mutableListOf() + + 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(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, + ) { + @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 -> "" + } + } +} diff --git a/src/en/kayoanime/src/eu/kanade/tachiyomi/animeextension/en/kayoanime/Kayoanime.kt b/src/en/kayoanime/src/eu/kanade/tachiyomi/animeextension/en/kayoanime/Kayoanime.kt index ba0e8c5ee..4af7a7b98 100644 --- a/src/en/kayoanime/src/eu/kanade/tachiyomi/animeextension/en/kayoanime/Kayoanime.kt +++ b/src/en/kayoanime/src/eu/kanade/tachiyomi/animeextension/en/kayoanime/Kayoanime.kt @@ -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> { - 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