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' extName = 'NekoSama'
pkgNameSuffix = 'fr.nekosama' pkgNameSuffix = 'fr.nekosama'
extClass = '.NekoSama' extClass = '.NekoSama'
extVersionCode = 4 extVersionCode = 5
libVersion = '13' libVersion = '13'
containsNsfw = false containsNsfw = true
} }
dependencies { dependencies {
implementation(project(':lib-streamtape-extractor')) 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.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource 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.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource 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.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -96,25 +92,18 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup() 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 script = document.selectFirst("script:containsData(var video = [];)")!!.data()
val playersRegex = Regex("video\\s*\\[\\d*]\\s*=\\s*'(.*?)'")
val firstVideo = script.substringBefore("else {").substringAfter("video[0] = '").substringBefore("'").lowercase() return playersRegex.findAll(script).flatMap {
val secondVideo = script.substringAfter("else {").substringAfter("video[0] = '").substringBefore("'").lowercase() val url = it.groupValues[1]
with(url) {
when { when {
firstVideo.contains("fusevideo") -> videoList.addAll(extractFuse(firstVideo)) contains("fusevideo") -> FusevideoExtractor(client, headers).videosFromUrl(this)
firstVideo.contains("streamtape") -> StreamTapeExtractor(client).videoFromUrl(firstVideo, "StreamTape")?.let { videoList.add(it) } contains("streamtape") -> listOfNotNull(StreamTapeExtractor(client).videoFromUrl(this))
firstVideo.contains("pstream") || firstVideo.contains("veestream") -> videoList.addAll(pstreamExtractor(firstVideo)) else -> emptyList()
} }
when { }
secondVideo.contains("fusevideo") -> videoList.addAll(extractFuse(secondVideo)) }.toList()
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()
} }
override fun videoListSelector() = throw Exception("not used") override fun videoListSelector() = throw Exception("not used")
@ -125,13 +114,9 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!! val quality = preferences.getString("preferred_quality", "1080")!!
val server = preferences.getString("preferred_server", "Pstream")!!
return this.sortedWith( return this.sortedWith(
compareBy( compareBy { it.quality.contains(quality) },
{ it.quality.contains(quality) },
{ it.quality.contains(server, true) },
),
).reversed() ).reversed()
} }
@ -300,139 +285,7 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
preferences.edit().putString(key, entry).commit() 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(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 @Serializable