feat(lib): Convert FilemoonExtractor into a shared lib (#1910)
This commit is contained in:
21
lib/filemoon-extractor/build.gradle.kts
Normal file
21
lib/filemoon-extractor/build.gradle.kts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("kotlinx-serialization")
|
||||||
|
kotlin("android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = AndroidConfig.compileSdk
|
||||||
|
namespace = "eu.kanade.tachiyomi.lib.filemoonextractor"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = AndroidConfig.minSdk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1") {
|
||||||
|
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8")
|
||||||
|
}
|
||||||
|
compileOnly(libs.bundles.common)
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
package eu.kanade.tachiyomi.lib.filemoonextractor
|
||||||
|
|
||||||
|
import dev.datlag.jsunpacker.JsUnpacker
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class FilemoonExtractor(private val client: OkHttpClient) {
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
fun videosFromUrl(url: String, prefix: String = "Filemoon - ", headers: Headers? = null): List<Video> {
|
||||||
|
return runCatching {
|
||||||
|
val doc = client.newCall(GET(url)).execute().asJsoup()
|
||||||
|
val jsEval = doc.selectFirst("script:containsData(eval)")!!.data()
|
||||||
|
val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty()
|
||||||
|
val masterUrl = unpacked.takeIf(String::isNotBlank)
|
||||||
|
?.substringAfter("{file:\"")
|
||||||
|
?.substringBefore("\"}")
|
||||||
|
?: return emptyList()
|
||||||
|
|
||||||
|
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string()
|
||||||
|
|
||||||
|
val httpUrl = url.toHttpUrl()
|
||||||
|
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
||||||
|
.set("Referer", url)
|
||||||
|
.set("Origin", "https://${httpUrl.host}")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val subtitleTracks = buildList {
|
||||||
|
// Subtitles from a external URL
|
||||||
|
val subUrl = httpUrl.queryParameter("sub.info")
|
||||||
|
?: unpacked.substringAfter("fetch('", "")
|
||||||
|
.substringBefore("').")
|
||||||
|
.takeIf(String::isNotBlank)
|
||||||
|
if (subUrl != null) {
|
||||||
|
runCatching { // to prevent failures on serialization errors
|
||||||
|
client.newCall(GET(subUrl, videoHeaders)).execute()
|
||||||
|
.use { it.body.string() }
|
||||||
|
.let { json.decodeFromString<List<SubtitleDto>>(it) }
|
||||||
|
.forEach { add(Track(it.file, it.label)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBTITLES_REGEX // Subtitles from the playlist
|
||||||
|
.findAll(masterPlaylist)
|
||||||
|
.forEach { add(Track(it.groupValues[2], it.groupValues[1])) }
|
||||||
|
}
|
||||||
|
val audioTracks = AUDIO_REGEX
|
||||||
|
.findAll(masterPlaylist)
|
||||||
|
.map { Track(it.groupValues[2], it.groupValues[1]) }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val separator = "#EXT-X-STREAM-INF:"
|
||||||
|
masterPlaylist.substringAfter(separator).split(separator).map {
|
||||||
|
val resolution = it.substringAfter("RESOLUTION=")
|
||||||
|
.substringAfter("x")
|
||||||
|
.substringBefore(",") + "p"
|
||||||
|
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||||
|
|
||||||
|
Video(
|
||||||
|
videoUrl,
|
||||||
|
prefix + resolution,
|
||||||
|
videoUrl,
|
||||||
|
headers = videoHeaders,
|
||||||
|
subtitleTracks = subtitleTracks,
|
||||||
|
audioTracks = audioTracks,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.getOrElse { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SubtitleDto(
|
||||||
|
val file: String,
|
||||||
|
val label: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val SUBTITLES_REGEX by lazy {
|
||||||
|
Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val AUDIO_REGEX by lazy {
|
||||||
|
Regex("""#EXT-X-MEDIA:TYPE=AUDIO.*?NAME="(.*?)".*?URI="(.*?)"""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user