feat(animestream/en): New source Donghuastream (#2118)
This commit is contained in:
@ -0,0 +1,3 @@
|
|||||||
|
dependencies {
|
||||||
|
implementation(project(':lib-playlist-utils'))
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.donghuastream
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.donghuastream.extractors.DailymotionExtractor
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.donghuastream.extractors.StreamPlayExtractor
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
|
||||||
|
|
||||||
|
class DonghuaStream : AnimeStream(
|
||||||
|
"en",
|
||||||
|
"DonghuaStream",
|
||||||
|
"https://donghuastream.co.in",
|
||||||
|
) {
|
||||||
|
override val fetchFilters: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
|
private val dailymotionExtractor by lazy { DailymotionExtractor(client, headers) }
|
||||||
|
private val streamPlayExtractor by lazy { StreamPlayExtractor(client, headers) }
|
||||||
|
|
||||||
|
override fun getVideoList(url: String, name: String): List<Video> {
|
||||||
|
val prefix = "$name - "
|
||||||
|
return when {
|
||||||
|
url.contains("dailymotion") -> dailymotionExtractor.videosFromUrl(url, prefix = prefix)
|
||||||
|
url.contains("streamplay") -> streamPlayExtractor.videosFromUrl(url, prefix = prefix)
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
override fun List<Video>.sort(): List<Video> {
|
||||||
|
val quality = preferences.getString(videoSortPrefKey, videoSortPrefDefault)!!
|
||||||
|
return sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ it.quality.contains(quality) },
|
||||||
|
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||||
|
),
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.donghuastream.extractors
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class DailymotionExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
|
||||||
|
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||||
|
val htmlString = client.newCall(GET(url)).execute().use { it.body.string() }
|
||||||
|
|
||||||
|
val internalData = htmlString.substringAfter("\"dmInternalData\":").substringBefore("</script>")
|
||||||
|
val ts = internalData.substringAfter("\"ts\":").substringBefore(",")
|
||||||
|
val v1st = internalData.substringAfter("\"v1st\":\"").substringBefore("\",")
|
||||||
|
|
||||||
|
val videoQuery = url.toHttpUrl().queryParameter("video") ?: url.toHttpUrl().encodedPath
|
||||||
|
|
||||||
|
val jsonUrl = "https://www.dailymotion.com/player/metadata/video/$videoQuery?locale=en-US&dmV1st=$v1st&dmTs=$ts&is_native_app=0"
|
||||||
|
val parsed = client.newCall(GET(jsonUrl)).execute().parseAs<DailyQuality>()
|
||||||
|
|
||||||
|
val subtitleList = parsed.subtitles?.data?.map { k ->
|
||||||
|
Track(
|
||||||
|
k.value.urls.first(),
|
||||||
|
k.value.label,
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
val masterUrl = parsed.qualities.auto.first().url
|
||||||
|
|
||||||
|
return playlistUtils.extractFromHls(masterUrl, subtitleList = subtitleList, videoNameGen = { q -> "$prefix$q" })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DailyQuality(
|
||||||
|
val qualities: Auto,
|
||||||
|
val subtitles: Subtitle? = null,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Auto(
|
||||||
|
val auto: List<Item>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Item(
|
||||||
|
val type: String,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Subtitle(
|
||||||
|
val data: Map<String, SubtitleObject>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SubtitleObject(
|
||||||
|
val label: String,
|
||||||
|
val urls: List<String>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(transform: (String) -> String = { it }): T {
|
||||||
|
val responseBody = use { transform(it.body.string()) }
|
||||||
|
return json.decodeFromString(responseBody)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.donghuastream.extractors
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
|
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 kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class StreamPlayExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
|
||||||
|
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||||
|
val document = client.newCall(
|
||||||
|
GET(url, headers),
|
||||||
|
).execute().use { it.asJsoup() }
|
||||||
|
|
||||||
|
val apiUrl = document.selectFirst("script:containsData(/api/)")
|
||||||
|
?.data()
|
||||||
|
?.substringAfter("url:")
|
||||||
|
?.substringAfter("\"")
|
||||||
|
?.substringBefore("\"")
|
||||||
|
?: return emptyList()
|
||||||
|
|
||||||
|
val apiHeaders = headers.newBuilder().apply {
|
||||||
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
|
add("Host", apiUrl.toHttpUrl().host)
|
||||||
|
add("Referer", url)
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val apiResponse = client.newCall(
|
||||||
|
GET("$apiUrl&_=${System.currentTimeMillis() / 1000}", headers = apiHeaders),
|
||||||
|
).execute().parseAs<APIResponse>()
|
||||||
|
|
||||||
|
val subtitleList = apiResponse.tracks?.let { t ->
|
||||||
|
t.map { Track(it.file, it.label) }
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
return apiResponse.sources.flatMap { source ->
|
||||||
|
val sourceUrl = source.file.replace("^//".toRegex(), "https://")
|
||||||
|
playlistUtils.extractFromHls(sourceUrl, referer = url, subtitleList = subtitleList, videoNameGen = { q -> "$prefix$q (StreamPlay)" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class APIResponse(
|
||||||
|
val sources: List<SourceObject>,
|
||||||
|
val tracks: List<TrackObject>? = null,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SourceObject(
|
||||||
|
val file: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TrackObject(
|
||||||
|
val file: String,
|
||||||
|
val label: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(transform: (String) -> String = { it }): T {
|
||||||
|
val responseBody = use { transform(it.body.string()) }
|
||||||
|
return json.decodeFromString(responseBody)
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ class AnimeStreamGenerator : ThemeSourceGenerator {
|
|||||||
SingleLang("AnimeTitans", "https://animetitans.com", "ar", isNsfw = false, overrideVersionCode = 12),
|
SingleLang("AnimeTitans", "https://animetitans.com", "ar", isNsfw = false, overrideVersionCode = 12),
|
||||||
SingleLang("AnimeXin", "https://animexin.vip", "all", isNsfw = false, overrideVersionCode = 5),
|
SingleLang("AnimeXin", "https://animexin.vip", "all", isNsfw = false, overrideVersionCode = 5),
|
||||||
SingleLang("desu-online", "https://desu-online.pl", "pl", className = "DesuOnline", isNsfw = false),
|
SingleLang("desu-online", "https://desu-online.pl", "pl", className = "DesuOnline", isNsfw = false),
|
||||||
|
SingleLang("DonghuaStream", "https://donghuastream.co.in", "en", isNsfw = false),
|
||||||
SingleLang("Hstream", "https://hstream.moe", "en", isNsfw = true, overrideVersionCode = 3),
|
SingleLang("Hstream", "https://hstream.moe", "en", isNsfw = true, overrideVersionCode = 3),
|
||||||
SingleLang("LMAnime", "https://lmanime.com", "all", isNsfw = false, overrideVersionCode = 2),
|
SingleLang("LMAnime", "https://lmanime.com", "all", isNsfw = false, overrideVersionCode = 2),
|
||||||
SingleLang("MiniOppai", "https://minioppai.org", "id", isNsfw = true, overrideVersionCode = 2),
|
SingleLang("MiniOppai", "https://minioppai.org", "id", isNsfw = true, overrideVersionCode = 2),
|
||||||
|
Reference in New Issue
Block a user