fix(fr/nekosama): Fix video extraction (#2026)
This commit is contained in:
18
lib/fusevideo-extractor/build.gradle.kts
Normal file
18
lib/fusevideo-extractor/build.gradle.kts
Normal file
@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
namespace = "eu.kanade.tachiyomi.lib.fusevideoextractor"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = AndroidConfig.minSdk
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.bundles.common)
|
||||
implementation(project(":lib-playlist-utils"))
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.lib.fusevideoextractor
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class FusevideoExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
return runCatching {
|
||||
val newHeaders = headers.newBuilder()
|
||||
.set("Accept", "*/*")
|
||||
.set("Host", url.toHttpUrl().host)
|
||||
.set("Accept-Language", "en-US,en;q=0.5")
|
||||
.build()
|
||||
val document = client.newCall(GET(url, newHeaders)).execute().use { it.asJsoup() }
|
||||
val dataUrl = document.selectFirst("script[src~=f/u/u/u/u]")?.attr("src")!!
|
||||
val dataDoc = client.newCall(GET(dataUrl, newHeaders)).execute().use { it.body.string() }
|
||||
val encoded = Regex("atob\\(\"(.*?)\"\\)").find(dataDoc)?.groupValues?.get(1)!!
|
||||
val data = Base64.decode(encoded, Base64.DEFAULT).toString(Charsets.UTF_8)
|
||||
val jsonData = data.split("|||")[1].replace("\\", "")
|
||||
val videoUrl = Regex("\"(https://.*?/m/.*)\"").find(jsonData)?.groupValues?.get(1)!!
|
||||
PlaylistUtils(client, newHeaders).extractFromHls(videoUrl, videoNameGen = { "${prefix}Fusevideo - $it" })
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
}
|
@ -6,13 +6,14 @@ ext {
|
||||
extName = 'NekoSama'
|
||||
pkgNameSuffix = 'fr.nekosama'
|
||||
extClass = '.NekoSama'
|
||||
extVersionCode = 4
|
||||
extVersionCode = 5
|
||||
libVersion = '13'
|
||||
containsNsfw = false
|
||||
containsNsfw = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib-streamtape-extractor'))
|
||||
implementation(project(':lib-fusevideo-extractor'))
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.animeextension.fr.nekosama
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
@ -13,18 +12,15 @@ 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.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.fusevideoextractor.FusevideoExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -96,25 +92,18 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
// probably exists a better way to make this idk
|
||||
val script = document.selectFirst("script:containsData(var video = [];)")!!.data()
|
||||
|
||||
val firstVideo = script.substringBefore("else {").substringAfter("video[0] = '").substringBefore("'").lowercase()
|
||||
val secondVideo = script.substringAfter("else {").substringAfter("video[0] = '").substringBefore("'").lowercase()
|
||||
|
||||
when {
|
||||
firstVideo.contains("fusevideo") -> videoList.addAll(extractFuse(firstVideo))
|
||||
firstVideo.contains("streamtape") -> StreamTapeExtractor(client).videoFromUrl(firstVideo, "StreamTape")?.let { videoList.add(it) }
|
||||
firstVideo.contains("pstream") || firstVideo.contains("veestream") -> videoList.addAll(pstreamExtractor(firstVideo))
|
||||
}
|
||||
when {
|
||||
secondVideo.contains("fusevideo") -> videoList.addAll(extractFuse(secondVideo))
|
||||
secondVideo.contains("streamtape") -> StreamTapeExtractor(client).videoFromUrl(secondVideo, "StreamTape")?.let { videoList.add(it) }
|
||||
secondVideo.contains("pstream") || secondVideo.contains("veestream") -> videoList.addAll(pstreamExtractor(secondVideo))
|
||||
}
|
||||
|
||||
return videoList.sort()
|
||||
val playersRegex = Regex("video\\s*\\[\\d*]\\s*=\\s*'(.*?)'")
|
||||
return playersRegex.findAll(script).flatMap {
|
||||
val url = it.groupValues[1]
|
||||
with(url) {
|
||||
when {
|
||||
contains("fusevideo") -> FusevideoExtractor(client, headers).videosFromUrl(this)
|
||||
contains("streamtape") -> listOfNotNull(StreamTapeExtractor(client).videoFromUrl(this))
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw Exception("not used")
|
||||
@ -125,13 +114,9 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", "1080")!!
|
||||
val server = preferences.getString("preferred_server", "Pstream")!!
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(quality) },
|
||||
{ it.quality.contains(server, true) },
|
||||
),
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
@ -300,139 +285,7 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
|
||||
val serverPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_server"
|
||||
title = "Preferred server"
|
||||
entries = arrayOf("Pstream/Veestream", "Streamtape")
|
||||
entryValues = arrayOf("Pstream", "Streamtape")
|
||||
setDefaultValue("Pstream")
|
||||
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(serverPref)
|
||||
}
|
||||
|
||||
private fun pstreamExtractor(url: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val document = Jsoup.connect(url).headers(
|
||||
mapOf(
|
||||
"Accept" to "*/*",
|
||||
"Accept-Encoding" to "gzip, deflate, br",
|
||||
"Accept-Language" to "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Connection" to "keep-alive",
|
||||
),
|
||||
).get()
|
||||
document.select("script").forEach { Script ->
|
||||
if (Script.attr("src").contains("https://www.pstream.net/u/player-script") || Script.attr("src").contains("https://veestream.net/u/player-script")) {
|
||||
val playerScript = Jsoup.connect(Script.attr("src")).headers(
|
||||
mapOf(
|
||||
"Accept" to "*/*",
|
||||
"Accept-Encoding" to "gzip, deflate, br",
|
||||
"Accept-Language" to "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Connection" to "keep-alive",
|
||||
),
|
||||
).ignoreContentType(true).execute().body()
|
||||
|
||||
val base64Data = playerScript.substringAfter("e.parseJSON(atob(t).slice(2))}(\"").substringBefore("\"")
|
||||
|
||||
val base64Decoded = Base64.decode(base64Data, Base64.DEFAULT).toString(Charsets.UTF_8)
|
||||
|
||||
val videoUrl = base64Decoded.substringAfter("mmmm\":\"").substringBefore("\"")
|
||||
|
||||
val videoUrlDecoded = videoUrl.replace("\\", "")
|
||||
val headers = headers.newBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Accept-Encoding", "gzip, deflate, br")
|
||||
add("Accept-Language", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7")
|
||||
add("Connection", "keep-alive")
|
||||
add("Referer", url)
|
||||
}.build()
|
||||
|
||||
val masterPlaylist = client.newCall(GET(videoUrlDecoded, headers))
|
||||
.execute()
|
||||
.body.string()
|
||||
|
||||
val separator = "#EXT-X-STREAM-INF"
|
||||
masterPlaylist.substringAfter(separator).split(separator).map {
|
||||
val resolution = it.substringAfter("NAME=\"")
|
||||
.substringBefore("\"") + "p"
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
videoList.add(
|
||||
Video(videoUrl, "$resolution (Pstream)", videoUrl, headers = headers),
|
||||
)
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun extractFuse(videoUrl: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val iframeHeaders = Headers.headersOf(
|
||||
"Accept",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"Accept-Language",
|
||||
"en-US,en;q=0.5",
|
||||
"Host",
|
||||
videoUrl.toHttpUrl().host,
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63",
|
||||
)
|
||||
|
||||
val soup = client.newCall(
|
||||
GET(videoUrl, headers = iframeHeaders),
|
||||
).execute().asJsoup()
|
||||
|
||||
val jsUrl = soup.selectFirst("script[src~=player-script]")!!.attr("src")
|
||||
|
||||
val jsHeaders = Headers.headersOf(
|
||||
"Accept", "*/*",
|
||||
"Accept-Language", "en-US,en;q=0.5",
|
||||
"Host", videoUrl.toHttpUrl().host,
|
||||
"Referer", videoUrl,
|
||||
"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63",
|
||||
)
|
||||
val jsString = client.newCall(
|
||||
GET(jsUrl, headers = jsHeaders),
|
||||
).execute().body.string()
|
||||
val base64Data = jsString.substringAfter("e.parseJSON(atob(t).slice(2))}(\"").substringBefore("\"")
|
||||
val base64Decoded = Base64.decode(base64Data, Base64.DEFAULT).toString(Charsets.UTF_8)
|
||||
val playlistUrl = "https:" + base64Decoded.substringAfter("https:").substringBefore("\"}").replace("\\", "")
|
||||
|
||||
val masterPlaylist = client.newCall(
|
||||
GET(playlistUrl, headers = jsHeaders),
|
||||
).execute().body.string()
|
||||
|
||||
masterPlaylist.substringAfter("#EXT-X-STREAM-INF").split("#EXT-X-STREAM-INF").map {
|
||||
val resolution = it.substringAfter("NAME=\"")
|
||||
.substringBefore("\"") + "p"
|
||||
val newUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
val videoHeaders = Headers.headersOf(
|
||||
"Accept",
|
||||
"*/*",
|
||||
"Accept-Language",
|
||||
"en-US,en;q=0.5",
|
||||
"Host",
|
||||
videoUrl.toHttpUrl().host,
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63",
|
||||
)
|
||||
videoList.add(
|
||||
Video(videoUrl, "$resolution (fusevideo)", newUrl, headers = videoHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
return videoList.sort()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
Reference in New Issue
Block a user