Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,15 @@
ext {
extName = 'Moflix-Stream'
extClass = '.MoflixStream'
extVersionCode = 8
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:streamvid-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:playlist-utils'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -0,0 +1,257 @@
package eu.kanade.tachiyomi.animeextension.de.moflixstream
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.AnimeDetailsDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.EpisodeListDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.EpisodePageDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.ItemInfo
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.PopularPaginationDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.SearchDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.SeasonPaginationDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.VideoResponseDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.extractors.UnpackerExtractor
import eu.kanade.tachiyomi.animeextension.de.moflixstream.extractors.VidGuardExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
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.AnimeHttpSource
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamvidextractor.StreamVidExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.Exception
class MoflixStream : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Moflix-Stream"
override val baseUrl = "https://moflix-stream.xyz"
override val lang = "de"
override val supportsLatest = false
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
private val apiUrl = "$baseUrl/api/v1"
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(
"$apiUrl/channel/345?returnContentOnly=true&restriction=&order=rating:desc&paginate=simple&perPage=50&query=&page=$page",
headers = Headers.headersOf("referer", "$baseUrl/movies?order=rating%3Adesc"),
)
override fun popularAnimeParse(response: Response): AnimesPage {
val pagination = response.parseAs<PopularPaginationDto>().pagination
val animeList = pagination.data.parseItems()
val hasNextPage = pagination.current_page < pagination.next_page ?: 1
return AnimesPage(animeList, hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET(
"$apiUrl/search/$query?query=$query",
headers = Headers.headersOf("referer", "$baseUrl/search/$query"),
)
override fun searchAnimeParse(response: Response): AnimesPage {
val data = response.parseAs<SearchDto>()
val animeList = data.results.parseItems()
return AnimesPage(animeList, false)
}
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val data = response.parseAs<AnimeDetailsDto>().title
setUrlWithoutDomain("$apiUrl/titles/${data.id}?$ANIME_URL_QUERIES")
title = data.name
thumbnail_url = data.thumbnail
genre = data.genres.joinToString { it.name }
description = data.description
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val data = response.parseAs<EpisodePageDto>()
val id = data.title.id
val seasonsUrl = "$apiUrl/titles/$id/seasons"
val seasons = data.seasons
return when (seasons) {
null -> {
SEpisode.create().apply {
name = "Film"
episode_number = 1F
setUrlWithoutDomain(response.request.url.toString())
}.let(::listOf)
}
else -> {
val seasonsList = buildList {
addAll(seasons.data)
var nextPage = seasons.next_page
while (nextPage != null) {
val req = GET("$seasonsUrl?perPage=8&query=&page=$nextPage", headers)
val res = client.newCall(req).execute().parseAs<SeasonPaginationDto>()
addAll(res.pagination.data)
nextPage = res.pagination.next_page
}
}
seasonsList.flatMap { season ->
val seasonNum = season.number
val episodesRequest = GET("$seasonsUrl/$seasonNum?load=episodes,primaryVideo", headers)
val episodesData = client.newCall(episodesRequest).execute()
.parseAs<EpisodeListDto>()
.episodes
.data
.reversed()
episodesData.map { episode ->
SEpisode.create().apply {
val epNum = episode.episode_number
episode_number = epNum.toFloat()
name = "Staffel $seasonNum Folge $epNum : " + episode.name
setUrlWithoutDomain("$seasonsUrl/$seasonNum/episodes/$epNum?load=videos,compactCredits,primaryVideo")
}
}
}
}
}
}
// ============================ Video Links =============================
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamvidExtractor by lazy { StreamVidExtractor(client) }
private val vidguardExtractor by lazy { VidGuardExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val luluExtractor by lazy { UnpackerExtractor(client, headers) }
override fun videoListParse(response: Response): List<Video> {
val selection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!!
val data = response.parseAs<VideoResponseDto>().run { episode ?: title }
return data!!.videos.flatMap { video ->
val name = video.name
val url = video.src
runCatching { getVideosFromUrl(url, name, selection) }.getOrElse { emptyList() }
}.ifEmpty { throw Exception("No videos!") }
}
private fun getVideosFromUrl(url: String, name: String, selection: Set<String>): List<Video> {
return when {
name.contains("Streamtape") && selection.contains("stape") -> {
streamtapeExtractor.videoFromUrl(url)?.let(::listOf) ?: emptyList()
}
name.contains("Streamvid") && selection.contains("svid") -> {
streamvidExtractor.videosFromUrl(url)
}
name.contains("Highstream") && selection.contains("hstream") -> {
streamvidExtractor.videosFromUrl(url, prefix = "Highstream - ")
}
name.contains("VidGuard") && selection.contains("vidg") -> {
vidguardExtractor.videosFromUrl(url)
}
name.contains("Filelions") && selection.contains("flions") -> {
streamwishExtractor.videosFromUrl(url, videoNameGen = { "FileLions - $it" })
}
name.contains("LuluStream") && selection.contains("lstream") -> {
luluExtractor.videosFromUrl(url, "LuluStream")
}
else -> emptyList()
}
}
override fun List<Video>.sort(): List<Video> {
val hoster = preferences.getString(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(hoster) },
).reversed()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = PREF_HOSTER_TITLE
entries = PREF_HOSTER_ENTRIES
entryValues = PREF_HOSTER_VALUES
setDefaultValue(PREF_HOSTER_DEFAULT)
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()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_SELECTION_KEY
title = PREF_HOSTER_SELECTION_TITLE
entries = PREF_HOSTER_SELECTION_ENTRIES
entryValues = PREF_HOSTER_SELECTION_ENTRIES
setDefaultValue(PREF_HOSTER_SELECTION_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun List<ItemInfo>.parseItems() = map {
SAnime.create().apply {
title = it.name
setUrlWithoutDomain("$apiUrl/titles/${it.id}?$ANIME_URL_QUERIES")
thumbnail_url = it.thumbnail
}
}
companion object {
private const val ANIME_URL_QUERIES = "load=images,genres,productionCountries,keywords,videos,primaryVideo,seasons,compactCredits"
private const val PREF_HOSTER_KEY = "preferred_hoster"
private const val PREF_HOSTER_TITLE = "Standard-Hoster"
private const val PREF_HOSTER_DEFAULT = "https://streamtape"
private val PREF_HOSTER_ENTRIES = arrayOf("Streamtape", "VidGuard", "Streamvid", "Highstream", "Filelions", "LuluStream")
private val PREF_HOSTER_VALUES = arrayOf("https://streamtape", "https://moflix-stream", "https://streamvid", "https://highstream", "https://moflix-stream", "https://luluvdo")
private const val PREF_HOSTER_SELECTION_KEY = "hoster_selection"
private const val PREF_HOSTER_SELECTION_TITLE = "auswählen"
private val PREF_HOSTER_SELECTION_ENTRIES = arrayOf("Streamtape", "VidGuard", "Streamvid", "Highstream", "Filelions", "LuluStream")
private val PREF_HOSTER_SELECTION_VALUES = arrayOf("stape", "vidg", "svid", "hstream", "flions", "lstream")
private val PREF_HOSTER_SELECTION_DEFAULT by lazy { PREF_HOSTER_SELECTION_VALUES.toSet() }
}
}

View file

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.animeextension.de.moflixstream.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.JsonTransformingSerializer
@Serializable
data class PopularPaginationDto(val pagination: PopularData) {
@Serializable
data class PopularData(val data: List<ItemInfo>, val next_page: Int? = null, val current_page: Int)
}
@Serializable
data class ItemInfo(
val poster: String?,
val backdrop: String?,
val description: String = "",
@Serializable(with = StringSerializer::class)
val id: String,
val name: String,
val genres: List<GenreDto> = emptyList(),
) {
val thumbnail by lazy { poster ?: backdrop }
}
@Serializable
data class GenreDto(@SerialName("display_name") val name: String)
@Serializable
data class SearchDto(val results: List<ItemInfo> = emptyList())
@Serializable
data class AnimeDetailsDto(val title: ItemInfo)
@Serializable
data class EpisodePageDto(
val title: SimpleItemDto,
val seasons: SeasonListDto? = null,
) {
@Serializable
data class SimpleItemDto(
@Serializable(with = StringSerializer::class)
val id: String,
)
}
@Serializable
data class SeasonListDto(
val data: List<SeasonDto> = emptyList(),
val next_page: Int? = null,
)
@Serializable
data class SeasonPaginationDto(val pagination: SeasonListDto)
@Serializable
data class SeasonDto(val number: Int)
@Serializable
data class EpisodeListDto(val episodes: EpisodesDataDto) {
@Serializable
data class EpisodesDataDto(val data: List<EpisodeDto>)
@Serializable
data class EpisodeDto(val name: String, val episode_number: Int)
}
@Serializable
data class VideoResponseDto(
val episode: VideoListDto? = null,
val title: VideoListDto? = null,
)
@Serializable
data class VideoListDto(val videos: List<VideoDto> = emptyList())
@Serializable
data class VideoDto(val name: String, val src: String)
object StringSerializer : JsonTransformingSerializer<String>(String.serializer()) {
override fun transformDeserialize(element: JsonElement) =
when (element) {
is JsonPrimitive -> JsonPrimitive(element.content)
else -> JsonPrimitive("")
}
}

View file

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.animeextension.de.moflixstream.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class UnpackerExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, hoster: String): List<Video> {
val doc = client.newCall(GET(url, headers)).execute()
.asJsoup()
val script = doc.selectFirst("script:containsData(eval)")
?.data()
?.let(JsUnpacker::unpackAndCombine)
?: return emptyList()
val playlistUrl = script.substringAfter("file:\"").substringBefore('"')
return playlistUtils.extractFromHls(
playlistUrl,
referer = playlistUrl,
videoNameGen = { "$hoster - $it" },
)
}
}

View file

@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.animeextension.de.moflixstream.extractors
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class VidGuardExtractor(private val client: OkHttpClient) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(private val latch: CountDownLatch) {
var payload: String = ""
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
val scriptUrl = doc.selectFirst("script[src*=ad/plugin]")
?.absUrl("src")
?: return emptyList()
val headers = Headers.headersOf("Referer", url)
val script = client.newCall(GET(scriptUrl, headers)).execute()
.body.string()
val sources = getSourcesFromScript(script, url)
.takeIf { it.isNotBlank() && it != "undefined" }
?: return emptyList()
return sources.substringAfter("stream:[").substringBefore("}]")
.split('{')
.drop(1)
.mapNotNull { line ->
val resolution = line.substringAfter("Label\":\"").substringBefore('"')
val videoUrl = line.substringAfter("URL\":\"").substringBefore('"')
.takeIf(String::isNotBlank)
?.let(::fixUrl)
?: return@mapNotNull null
Video(videoUrl, "VidGuard - $resolution", videoUrl, headers)
}
}
private fun getSourcesFromScript(script: String, url: String): String {
val latch = CountDownLatch(1)
var webView: WebView? = null
val jsinterface = JsObject(latch)
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
cacheMode = WebSettings.LOAD_NO_CACHE
}
webview.addJavascriptInterface(jsinterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.clearCache(true)
view?.clearFormData()
view?.evaluateJavascript(script) {}
view?.evaluateJavascript("window.android.passPayload(JSON.stringify(window.svg))") {}
}
}
webview.loadDataWithBaseURL(url, "<html></html>", "text/html", "UTF-8", null)
}
latch.await(5, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return jsinterface.payload
}
private fun fixUrl(url: String): String {
val httpUrl = url.toHttpUrl()
val originalSign = httpUrl.queryParameter("sig")!!
val newSign = originalSign.chunked(2).joinToString("") {
Char(it.toInt(16) xor 2).toString()
}
.let { String(Base64.decode(it, Base64.DEFAULT)) }
.substring(5)
.chunked(2)
.reversed()
.joinToString("")
.substring(5)
return httpUrl.newBuilder()
.removeAllQueryParameters("sig")
.addQueryParameter("sig", newSign)
.build()
.toString()
}
}