feat(lib): Convert DailymotionExtractor to a shared lib (#2129)
This commit is contained in:
19
lib/dailymotion-extractor/build.gradle.kts
Normal file
19
lib/dailymotion-extractor/build.gradle.kts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
kotlin("android")
|
||||||
|
id("kotlinx-serialization")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = AndroidConfig.compileSdk
|
||||||
|
namespace = "eu.kanade.tachiyomi.lib.dailymotionextractor"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = AndroidConfig.minSdk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.bundles.common)
|
||||||
|
implementation(project(":lib-playlist-utils"))
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package eu.kanade.tachiyomi.lib.dailymotionextractor
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonTransformingSerializer
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DailyQuality(
|
||||||
|
val qualities: Auto? = null,
|
||||||
|
val subtitles: Subtitle? = null,
|
||||||
|
val error: Error? = null,
|
||||||
|
val id: String? = null,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Error(val type: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Auto(val auto: List<Item>) {
|
||||||
|
@Serializable
|
||||||
|
data class Item(val type: String, val url: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Subtitle(
|
||||||
|
@Serializable(with = SubtitleListSerializer::class)
|
||||||
|
val data: List<SubtitleDto>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SubtitleDto(val label: String, val urls: List<String>)
|
||||||
|
|
||||||
|
object SubtitleListSerializer :
|
||||||
|
JsonTransformingSerializer<List<SubtitleDto>>(ListSerializer(SubtitleDto.serializer())) {
|
||||||
|
override fun transformDeserialize(element: JsonElement): JsonElement =
|
||||||
|
when (element) {
|
||||||
|
is JsonObject -> JsonArray(element.values.toList())
|
||||||
|
else -> JsonArray(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TokenResponse(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ProtectedResponse(val data: DataObject) {
|
||||||
|
@Serializable
|
||||||
|
data class DataObject(val video: VideoObject) {
|
||||||
|
@Serializable
|
||||||
|
data class VideoObject(
|
||||||
|
val id: String,
|
||||||
|
val xid: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
package eu.kanade.tachiyomi.lib.dailymotionextractor
|
||||||
|
|
||||||
|
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.network.POST
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class DailymotionExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DAILYMOTION_URL = "https://www.dailymotion.com"
|
||||||
|
private const val GRAPHQL_URL = "https://graphql.api.dailymotion.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun headersBuilder(block: Headers.Builder.() -> Unit = {}) = headers.newBuilder()
|
||||||
|
.add("Accept", "*/*")
|
||||||
|
.set("Referer", "$DAILYMOTION_URL/")
|
||||||
|
.set("Origin", DAILYMOTION_URL)
|
||||||
|
.apply { block() }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
|
||||||
|
fun videosFromUrl(url: String, prefix: String, baseUrl: String = "", password: String? = null): 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().run {
|
||||||
|
queryParameter("video") ?: pathSegments.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonUrl = "$DAILYMOTION_URL/player/metadata/video/$videoQuery?locale=en-US&dmV1st=$v1st&dmTs=$ts&is_native_app=0"
|
||||||
|
val parsed = client.newCall(GET(jsonUrl)).execute().parseAs<DailyQuality>()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
parsed.qualities != null && parsed.error == null -> videosFromDailyResponse(parsed, prefix)
|
||||||
|
parsed.error?.type == "password_protected" && parsed.id != null -> {
|
||||||
|
videosFromProtectedUrl(url, prefix, parsed.id, htmlString, ts, v1st, baseUrl, password)
|
||||||
|
}
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun videosFromProtectedUrl(
|
||||||
|
url: String,
|
||||||
|
prefix: String,
|
||||||
|
videoId: String,
|
||||||
|
htmlString: String,
|
||||||
|
ts: String,
|
||||||
|
v1st: String,
|
||||||
|
baseUrl: String,
|
||||||
|
password: String?,
|
||||||
|
): List<Video> {
|
||||||
|
val postUrl = "$GRAPHQL_URL/oauth/token"
|
||||||
|
val clientId = htmlString.substringAfter("client_id\":\"").substringBefore('"')
|
||||||
|
val clientSecret = htmlString.substringAfter("client_secret\":\"").substringBefore('"')
|
||||||
|
val scope = htmlString.substringAfter("client_scope\":\"").substringBefore('"')
|
||||||
|
|
||||||
|
val tokenBody = FormBody.Builder()
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("traffic_segment", ts)
|
||||||
|
.add("visitor_id", v1st)
|
||||||
|
.add("grant_type", "client_credentials")
|
||||||
|
.add("scope", scope)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val tokenResponse = client.newCall(POST(postUrl, headersBuilder(), tokenBody)).execute()
|
||||||
|
val tokenParsed = tokenResponse.parseAs<TokenResponse>()
|
||||||
|
|
||||||
|
val idUrl = "$GRAPHQL_URL/"
|
||||||
|
val idHeaders = headersBuilder {
|
||||||
|
set("Accept", "application/json, text/plain, */*")
|
||||||
|
add("Authorization", "${tokenParsed.token_type} ${tokenParsed.access_token}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val idData = """
|
||||||
|
{
|
||||||
|
"query":"query playerPasswordQuery(${'$'}videoId:String!,${'$'}password:String!){video(xid:${'$'}videoId,password:${'$'}password){id xid}}",
|
||||||
|
"variables":{
|
||||||
|
"videoId":"$videoId",
|
||||||
|
"password":"$password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent().toRequestBody("application/json".toMediaType())
|
||||||
|
|
||||||
|
val idResponse = client.newCall(POST(idUrl, idHeaders, idData)).execute()
|
||||||
|
val idParsed = idResponse.parseAs<ProtectedResponse>().data.video
|
||||||
|
|
||||||
|
val dmvk = htmlString.substringAfter("\"dmvk\":\"").substringBefore('"')
|
||||||
|
val getVideoIdUrl = "$DAILYMOTION_URL/player/metadata/video/${idParsed.xid}?embedder=${"$baseUrl/"}&locale=en-US&dmV1st=$v1st&dmTs=$ts&is_native_app=0"
|
||||||
|
val getVideoIdHeaders = headersBuilder {
|
||||||
|
add("Cookie", "dmvk=$dmvk; ts=$ts; v1st=$v1st; usprivacy=1---; client_token=${tokenParsed.access_token}")
|
||||||
|
set("Referer", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsed = client.newCall(GET(getVideoIdUrl, getVideoIdHeaders)).execute()
|
||||||
|
.parseAs<DailyQuality>()
|
||||||
|
|
||||||
|
return videosFromDailyResponse(parsed, prefix, getVideoIdHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun videosFromDailyResponse(parsed: DailyQuality, prefix: String, playlistHeaders: Headers? = null): List<Video> {
|
||||||
|
val masterUrl = parsed.qualities?.auto?.firstOrNull()?.url
|
||||||
|
?: return emptyList<Video>()
|
||||||
|
|
||||||
|
val subtitleList = parsed.subtitles?.data?.map {
|
||||||
|
Track(it.urls.first(), it.label)
|
||||||
|
} ?: emptyList<Track>()
|
||||||
|
|
||||||
|
val masterHeaders = playlistHeaders ?: headersBuilder()
|
||||||
|
|
||||||
|
return playlistUtils.extractFromHls(
|
||||||
|
masterUrl,
|
||||||
|
masterHeadersGen = { _, _ -> masterHeaders },
|
||||||
|
subtitleList = subtitleList,
|
||||||
|
videoNameGen = { "$prefix$it" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
|
return use { it.body.string() }.let(json::decodeFromString)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user