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'
|
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'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user