fix(fr/nekosama): Fix video extraction (#2026)

This commit is contained in:
hollow
2023-08-08 20:56:22 +00:00
committed by GitHub
parent 5b0da5a8cd
commit 87ec82ff2b
4 changed files with 66 additions and 162 deletions

View 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"))
}

View File

@ -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())
}
}

View File

@ -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'))
}

View File

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