Add extension: Kissanime (#1275)

* Fix json parsing

* Add extension
This commit is contained in:
Secozzi
2023-02-13 22:43:23 +01:00
committed by GitHub
parent 9f536def43
commit 23f2c1210b
14 changed files with 821 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -0,0 +1,19 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'KissAnime'
pkgNameSuffix = 'en.kissanime'
extClass = '.KissAnime'
extVersionCode = 1
libVersion = '13'
}
dependencies {
compileOnly libs.bundles.coroutines
implementation(project(':lib-fembed-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -0,0 +1,365 @@
package eu.kanade.tachiyomi.animeextension.en.kissanime
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.kissanime.extractors.DailymotionExtractor
import eu.kanade.tachiyomi.animeextension.en.kissanime.extractors.Mp4uploadExtractor
import eu.kanade.tachiyomi.animeextension.en.kissanime.extractors.VodstreamExtractor
import eu.kanade.tachiyomi.animeextension.en.kissanime.extractors.YourUploadExtractor
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.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.fembedextractor.FembedExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "kissanime.com.ru"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://kissanime.com.ru")!! }
override val lang = "en"
override val supportsLatest = true
override val client = network.cloudflareClient
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private val DateFormatter by lazy {
SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH)
}
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/AnimeListOnline/Trending?page=$page")
override fun popularAnimeSelector(): String = "div.listing > div.item_movies_in_cat"
override fun popularAnimeNextPageSelector(): String = "div.pagination > ul > li.current ~ li"
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a").attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img").attr("src")
title = element.selectFirst("div.title_in_cat_container > a").text()
}
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/AnimeListOnline/LatestUpdate?page=$page")
override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val params = KissAnimeFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
private fun searchAnimeRequest(page: Int, query: String, filters: KissAnimeFilters.FilterSearchParams): Request {
return when {
filters.subpage.isNotBlank() -> GET("$baseUrl/${filters.subpage}/?page=$page")
filters.schedule.isNotBlank() -> GET("$baseUrl/Schedule#${filters.schedule}")
else -> GET("$baseUrl/AdvanceSearch/?name=$query&status=${filters.status}&genre=${filters.genre}&page=$page", headers = headers)
}
}
override fun searchAnimeParse(response: Response): AnimesPage {
return if (response.request.url.encodedPath.startsWith("/Schedule")) {
val document = response.asJsoup()
val name = response.request.url.encodedFragment!!
val animes = document.select("div.barContent > div.schedule_container > div.schedule_item:has(div.schedule_block_title:contains($name)) div.schedule_row > div.schedule_block").map {
SAnime.create().apply {
title = it.selectFirst("h2 > a > span.jtitle").text()
thumbnail_url = it.selectFirst("img").attr("src")
setUrlWithoutDomain(it.selectFirst("a").attr("href").toHttpUrl().encodedPath)
}
}
AnimesPage(animes, false)
} else {
super.searchAnimeParse(response)
}
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
// ============================== FILTERS ===============================
override fun getFilterList(): AnimeFilterList = KissAnimeFilters.filterList
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val rating = document.selectFirst("div.Votes > div.Prct > div[data-percent]")?.let { "\n\nUser rating: ${it.attr("data-percent")}%" } ?: ""
return SAnime.create().apply {
title = document.selectFirst("div.barContent > div.full > h2").text()
thumbnail_url = document.selectFirst("div.cover_anime img").attr("src")
status = parseStatus(document.selectFirst("div.full > div.static_single > p:has(span:contains(Status))").ownText())
description = (document.selectFirst("div.full > div.summary > p")?.text() ?: "") + rating
genre = document.select("div.full > p.info:has(span:contains(Genre)) > a").joinToString(", ") { it.text() }
}
}
// ============================== Episodes ==============================
override fun episodeListSelector(): String = "div.listing > div:not([class])"
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
name = element.selectFirst("a").text()
episode_number = element.selectFirst("a").text().substringAfter("Episode ").toFloatOrNull() ?: 0F
date_upload = parseDate(element.selectFirst("div:not(:has(a))").text())
setUrlWithoutDomain(element.selectFirst("a").attr("href").substringAfter(baseUrl))
}
}
// ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val videoList = mutableListOf<Video>()
val serverList = mutableListOf<Server>()
// GET VIDEO HOSTERS
val episodeId = (baseUrl + episode.url).toHttpUrl().queryParameter("id")!!
var document = client.newCall(
GET(baseUrl + episode.url, headers = headers)
).execute().asJsoup()
var newDocument = document
for (server in document.select("select#selectServer > option")) {
val url = baseUrl + server.attr("value")
if (!server.hasAttr("selected")) {
newDocument = client.newCall(
GET(url, headers = headers)
).execute().asJsoup()
}
val ctk = newDocument.selectFirst("script:containsData(ctk)").data().substringAfter("var ctk = '").substringBefore("';")
val getIframeHeaders = Headers.headersOf(
"Accept", "application/json, text/javascript, */*; q=0.01",
"Alt-Used", baseUrl.toHttpUrl().host,
"Content-Type", "application/x-www-form-urlencoded; charset=UTF-8",
"Host", baseUrl.toHttpUrl().host,
"Origin", baseUrl,
"Referer", url,
"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0",
"X-Requested-With", "XMLHttpRequest"
)
val getIframeBody = "episode_id=$episodeId&ctk=$ctk".toRequestBody("application/x-www-form-urlencoded".toMediaType())
val serverName = url.toHttpUrl().queryParameter("s")!!
val iframe = json.decodeFromString<IframeResponse>(
client.newCall(
POST("$baseUrl/ajax/anime/load_episodes_v2?s=$serverName", body = getIframeBody, headers = getIframeHeaders)
).execute().body!!.string()
)
var iframeUrl = Jsoup.parse(iframe.value).selectFirst("iframe").attr("src")
val password = if (iframe.value.contains("password: ")) {
iframe.value.substringAfter("password: ").substringBefore(" <button")
} else {
null
}
if (!iframeUrl.startsWith("http")) iframeUrl = "https:$iframeUrl"
serverList.add(Server(server.text(), iframeUrl, password))
}
// GET VIDEO URLS
videoList.addAll(
serverList.parallelMap { server ->
runCatching {
val url = server.url
when {
url.contains("fembed") ||
url.contains("anime789.com") || url.contains("24hd.club") || url.contains("fembad.org") ||
url.contains("vcdn.io") || url.contains("sharinglink.club") || url.contains("moviemaniac.org") ||
url.contains("votrefiles.club") || url.contains("femoload.xyz") || url.contains("albavido.xyz") ||
url.contains("feurl.com") || url.contains("dailyplanet.pw") || url.contains("ncdnstm.com") ||
url.contains("jplayer.net") || url.contains("xstreamcdn.com") || url.contains("fembed-hd.com") ||
url.contains("gcloud.live") || url.contains("vcdnplay.com") || url.contains("superplayxyz.club") ||
url.contains("vidohd.com") || url.contains("vidsource.me") || url.contains("cinegrabber.com") ||
url.contains("votrefile.xyz") || url.contains("zidiplay.com") || url.contains("ndrama.xyz") ||
url.contains("fcdn.stream") || url.contains("mediashore.org") || url.contains("suzihaza.com") ||
url.contains("there.to") || url.contains("femax20.com") || url.contains("javstream.top") ||
url.contains("viplayer.cc") || url.contains("sexhd.co") || url.contains("fembed.net") ||
url.contains("mrdhan.com") || url.contains("votrefilms.xyz") || // url.contains("") ||
url.contains("embedsito.com") || url.contains("dutrag.com") || // url.contains("") ||
url.contains("youvideos.ru") || url.contains("streamm4u.club") || // url.contains("") ||
url.contains("moviepl.xyz") || url.contains("asianclub.tv") || // url.contains("") ||
url.contains("vidcloud.fun") || url.contains("fplayer.info") || // url.contains("") ||
url.contains("diasfem.com") || url.contains("javpoll.com") || url.contains("reeoov.tube") ||
url.contains("suzihaza.com") || url.contains("ezsubz.com") || url.contains("vidsrc.xyz") ||
url.contains("diampokusy.com") || url.contains("diampokusy.com") || url.contains("i18n.pw") ||
url.contains("vanfem.com") || url.contains("fembed9hd.com") || url.contains("votrefilms.xyz") || url.contains("watchjavnow.xyz")
-> {
val newUrl = url.replace("https://www.fembed.com", "https://vanfem.com")
FembedExtractor(client).videosFromUrl(newUrl, prefix = "${server.name} - ")
}
url.contains("yourupload") -> {
val headers = headers.newBuilder().add("referer", "https://www.yourupload.com/").build()
YourUploadExtractor(client).videoFromUrl(url, headers = headers, name = server.name)
}
url.contains("mp4upload") -> {
val headers = headers.newBuilder().set("referer", "https://mp4upload.com/").build()
Mp4uploadExtractor(client).getVideoFromUrl(url, headers = headers, name = server.name)
}
url.contains("embed.vodstream.xyz") -> {
val referer = "$baseUrl/"
VodstreamExtractor(client).getVideosFromUrl(url, referer = referer, prefix = "${server.name} - ")
}
url.contains("dailymotion") -> {
DailymotionExtractor(client).videosFromUrl(url, prefix = "${server.name} - ", password = server.password, baseUrl = baseUrl)
}
else -> null
}
}.getOrNull()
}.filterNotNull().flatten()
)
return Observable.just(videoList.sort())
}
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoListSelector(): String = throw Exception("Not Used")
override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!!
return this.sortedWith(
compareBy { it.quality.contains(quality) }
).reversed()
}
data class Server(
val name: String,
val url: String,
val password: String? = null
)
@Serializable
data class IframeResponse(
val value: String
)
private fun parseDate(dateStr: String): Long {
return runCatching { DateFormatter.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
private fun parseStatus(statusString: String): Int {
return when (statusString.trim()) {
"Ongoing" -> SAnime.ONGOING
"Completed" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Preferred domain (requires app restart)"
entries = arrayOf("kissanime.com.ru", "kissanime.co", "kissanime.sx", "kissanime.org.ru")
entryValues = arrayOf("https://kissanime.com.ru", "https://kissanime.co", "https://kissanime.sx", "https://kissanime.org.ru")
setDefaultValue("https://kissanime.com.ru")
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()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080")
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(domainPref)
screen.addPreference(videoQualityPref)
}
// From Dopebox
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
}

View File

@ -0,0 +1,152 @@
package eu.kanade.tachiyomi.animeextension.en.kissanime
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object KissAnimeFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray()
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.filterIsInstance<R>().joinToString("") {
(it as QueryPartFilter).toQueryPart()
}
}
class StatusFilter : QueryPartFilter("Status", KissAnimeFiltersData.status)
class GenreFilter : CheckBoxFilterList(
"Genre",
KissAnimeFiltersData.genre.map { CheckBoxVal(it.first, false) }
)
class SubPageFilter : QueryPartFilter("Sub-page", KissAnimeFiltersData.subpage)
class ScheduleFilter : QueryPartFilter("Schedule", KissAnimeFiltersData.schedule)
val filterList = AnimeFilterList(
StatusFilter(),
GenreFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("Ignores other filters & text search"),
SubPageFilter(),
ScheduleFilter()
)
data class FilterSearchParams(
val status: String = "",
val genre: String = "",
val subpage: String = "",
val schedule: String = ""
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val genre: String = filters.filterIsInstance<GenreFilter>()
.first()
.state.mapNotNull { format ->
if (format.state) {
KissAnimeFiltersData.genre.find { it.first == format.name }!!.second
} else { null }
}.joinToString(separator = "")
return FilterSearchParams(
filters.asQueryPart<StatusFilter>(),
genre,
filters.asQueryPart<SubPageFilter>(),
filters.asQueryPart<ScheduleFilter>(),
)
}
private object KissAnimeFiltersData {
val status = arrayOf(
Pair("Any", ""),
Pair("Ongoing", "upcoming"),
Pair("Completed", "complete"),
)
val genre = arrayOf(
Pair("Action", "Action_"),
Pair("Adventure", "Adventure_"),
Pair("Cars", "Cars_"),
Pair("Cartoon", "Cartoon_"),
Pair("Comedy", "Comedy_"),
Pair("Dementia", "Dementia_"),
Pair("Demons", "Demons_"),
Pair("Drama", "Drama_"),
Pair("Dub", "Dub_"),
Pair("Ecchi", "Ecchi_"),
Pair("Fantasy", "Fantasy_"),
Pair("Game", "Game_"),
Pair("Harem", "Harem_"),
Pair("Historical", "Historical_"),
Pair("Horror", "Horror_"),
Pair("Josei", "Josei_"),
Pair("Kids", "Kids_"),
Pair("Magic", "Magic_"),
Pair("Martial Arts", "Martial Arts_"),
Pair("Mecha", "Mecha_"),
Pair("Military", "Military_"),
Pair("Movie", "Movie_"),
Pair("Music", "Music_"),
Pair("Mystery", "Mystery_"),
Pair("ONA", "ONA_"),
Pair("OVA", "OVA_"),
Pair("Parody", "Parody_"),
Pair("Police", "Police_"),
Pair("Psychological", "Psychological_"),
Pair("Romance", "Romance_"),
Pair("Samurai", "Samurai_"),
Pair("School", "School_"),
Pair("Sci-Fi", "Sci-Fi_"),
Pair("Seinen", "Seinen_"),
Pair("Shoujo", "Shoujo_"),
Pair("Shoujo Ai", "Shoujo Ai_"),
Pair("Shounen", "Shounen_"),
Pair("Shounen Ai", "Shounen Ai_"),
Pair("Slice of Life", "Slice of Life_"),
Pair("Space", "Space_"),
Pair("Special", "Special_"),
Pair("Sports", "Sports_"),
Pair("Super Power", "Super Power_"),
Pair("Supernatural", "Supernatural_"),
Pair("Thriller", "Thriller_"),
Pair("Vampire", "Vampire_"),
Pair("Yuri", "Yuri_")
)
val subpage = arrayOf(
Pair("<select>", ""),
Pair("List A-Z", "AnimeListOnline"),
Pair("List Most Popular", "AnimeListOnline/MostPopular"),
Pair("List Latest Update", "AnimeListOnline/LatestUpdate"),
Pair("List Upcoming Anime", "UpcomingAnime"),
Pair("List New Anime", "AnimeListOnline/Newest"),
)
val schedule = arrayOf(
Pair("<select>", ""),
Pair("Yesterday", "Yesterday"),
Pair("Today", "Today"),
Pair("Tomorrow", "Tomorrow"),
Pair("Monday", "Monday"),
Pair("Tuesday", "Tuesday"),
Pair("Wednesday", "Wednesday"),
Pair("Thursday", "Thursday"),
Pair("Friday", "Friday"),
Pair("Saturday", "Saturday"),
Pair("Sunday", "Sunday"),
)
}
}

View File

@ -0,0 +1,161 @@
package eu.kanade.tachiyomi.animeextension.en.kissanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import uy.kohesive.injekt.injectLazy
class DailymotionExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String, baseUrl: String, password: String?): List<Video> {
val videoList = mutableListOf<Video>()
val htmlString = client.newCall(GET(url)).execute().body!!.string()
val internalData = htmlString.substringAfter("\"dmInternalData\":").substringBefore("</script>")
val ts = internalData.substringAfter("\"ts\":").substringBefore(",")
val v1st = internalData.substringAfter("\"v1st\":\"").substringBefore("\",")
val jsonUrl = "https://www.dailymotion.com/player/metadata/video${url.toHttpUrl().encodedPath}?locale=en-US&dmV1st=$v1st&dmTs=$ts&is_native_app=0"
var parsed = json.decodeFromString<DailyQuality>(
client.newCall(GET(jsonUrl))
.execute().body!!.string()
)
var playlistHeaders = Headers.headersOf()
val videoHeaders = Headers.headersOf(
"Accept", "*/*",
"Origin", "https://www.dailymotion.com",
"Referer", "https://www.dailymotion.com/"
)
if (parsed.error != null) {
if (parsed.error!!.type == "password_protected") {
val postUrl = "https://graphql.api.dailymotion.com/oauth/token"
val clientId = htmlString.substringAfter("client_id\":\"").substringBefore("\"")
val clientSecret = htmlString.substringAfter("client_secret\":\"").substringBefore("\"")
val scope = htmlString.substringAfter("client_scope\":\"").substringBefore("\"")
val tokenHeaders = Headers.headersOf(
"Accept", "application/json, text/plain, */*",
"Content-Type", "application/x-www-form-urlencoded",
"Origin", "https://www.dailymotion.com",
"Referer", "https://www.dailymotion.com/"
)
val tokenBody = "client_id=$clientId&client_secret=$clientSecret&traffic_segment=$ts&visitor_id=$v1st&grant_type=client_credentials&scope=$scope".toRequestBody("application/x-www-form-urlencoded".toMediaType())
val tokenResponse = client.newCall(
POST(postUrl, body = tokenBody, headers = tokenHeaders)
).execute()
val tokenParsed = json.decodeFromString<TokenResponse>(tokenResponse.body!!.string())
val idUrl = "https://graphql.api.dailymotion.com/"
val idHeaders = Headers.headersOf(
"Accept", "application/json, text/plain, */*",
"Authorization", "${tokenParsed.token_type} ${tokenParsed.access_token}",
"Content-Type", "application/json",
"Origin", "https://www.dailymotion.com",
"Referer", "https://www.dailymotion.com/"
)
val idData = """
{
"query":"query playerPasswordQuery(${'$'}videoId:String!,${'$'}password:String!){video(xid:${'$'}videoId,password:${'$'}password){id xid}}",
"variables":{
"videoId":"${parsed.id!!}",
"password":"$password"
}
}
""".trimIndent().toRequestBody("application/json".toMediaType())
val idResponse = client.newCall(
POST(idUrl, body = idData, headers = idHeaders)
).execute()
val idParsed = json.decodeFromString<ProtectedResponse>(idResponse.body!!.string()).data.video
val dmvk = htmlString.substringAfter("\"dmvk\":\"").substringBefore("\"")
val getVideoIdUrl = "https://www.dailymotion.com/player/metadata/video/${idParsed.xid}?embedder=${"$baseUrl/"}&locale=en-US&dmV1st=$v1st&dmTs=$ts&is_native_app=0"
val getVideoIdHeaders = Headers.headersOf(
"Accept", "*/*",
"Cookie", "dmvk=$dmvk; ts=$ts; v1st=$v1st; usprivacy=1---; client_token=${tokenParsed.access_token}",
"Referer", url
)
playlistHeaders = Headers.headersOf(
"Accept", "*/*",
"Cookie", "dmvk=$dmvk; ts=$ts; v1st=$v1st; usprivacy=1---; client_token=${tokenParsed.access_token}",
"Referer", url
)
val getVideoIdResponse = client.newCall(GET(getVideoIdUrl, headers = getVideoIdHeaders)).execute()
val videoQualityBody = getVideoIdResponse.body!!.string()
parsed = json.decodeFromString<DailyQuality>(videoQualityBody)
}
}
val masterUrl = parsed.qualities!!.auto.first().url
val masterPlaylist = client.newCall(GET(masterUrl, headers = playlistHeaders)).execute().body!!.string()
val separator = "#EXT-X-STREAM-INF"
masterPlaylist.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",NAME") + "p"
var videoUrl = it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, prefix + quality, videoUrl, headers = videoHeaders))
}
return videoList
}
@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
)
}
}
@Serializable
data class DailyQuality(
val error: Error? = null,
val id: String? = null,
val qualities: Auto? = 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
)
}
}
}

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.animeextension.en.kissanime.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class Mp4uploadExtractor(private val client: OkHttpClient) {
fun getVideoFromUrl(url: String, headers: Headers, name: String): List<Video> {
val body = client.newCall(GET(url, headers = headers)).execute().body!!.string()
val packed = body.substringAfter("<script type='text/javascript'>eval(function(p,a,c,k,e,d)")
.substringBefore("</script>")
val unpacked = JsUnpacker.unpackAndCombine("eval(function(p,a,c,k,e,d)" + packed) ?: return emptyList()
val videoUrl = unpacked.substringAfter("player.src(\"").substringBefore("\");")
return listOf(
Video(videoUrl, name, videoUrl, headers = Headers.headersOf("Referer", "https://www.mp4upload.com/"))
)
}
}

View File

@ -0,0 +1,73 @@
package eu.kanade.tachiyomi.animeextension.en.kissanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class VodstreamExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun getVideosFromUrl(url: String, referer: String, prefix: String): List<Video> {
val videoList = mutableListOf<Video>()
val getIframeHeaders = Headers.headersOf(
"Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Host", url.toHttpUrl().host,
"Referer", referer,
"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0"
)
val iframe = client.newCall(
GET(url, headers = getIframeHeaders)
).execute().asJsoup()
val sourcesData = iframe.selectFirst("script:containsData(playerInstance)").data()
val sources = json.decodeFromString<List<Source>>("[${sourcesData.substringAfter("sources: [").substringBefore("],")}]")
sources.forEach { source ->
val videoHeaders = Headers.headersOf(
"Accept", "*/*",
"Host", source.file.toHttpUrl().host,
"Origin", "https://${url.toHttpUrl().host}",
"Referer", "https://${url.toHttpUrl().host}/",
"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0"
)
if (source.file.contains(".m3u8")) {
val masterPlaylist = client.newCall(GET(source.file, headers = videoHeaders)).execute().body!!.string()
val separator = "#EXT-X-STREAM-INF"
masterPlaylist.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
var videoUrl = it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, prefix + quality, videoUrl, headers = videoHeaders))
}
} else {
videoList.add(
Video(
source.file,
prefix + source.label,
source.file,
headers = videoHeaders
)
)
}
}
return videoList
}
@Serializable
data class Source(
val file: String,
val label: String
)
}

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.animeextension.en.kissanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class YourUploadExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String, headers: Headers, name: String): List<Video> {
val videoList = mutableListOf<Video>()
return try {
val document = client.newCall(GET(url, headers = headers)).execute()
if (document.isSuccessful) {
val content = document.asJsoup()
val baseData =
content!!.selectFirst("script:containsData(jwplayerOptions)")!!.data()
if (!baseData.isNullOrEmpty()) {
val basicUrl = baseData.substringAfter("file: '").substringBefore("',")
videoList.add(Video(basicUrl, name, basicUrl, headers = headers))
}
}
videoList
} catch (e: Exception) {
videoList
}
}
}