feat(src/en): New source: My Running Man (#2340)

This commit is contained in:
Claudemirovsky 2023-10-09 08:27:11 -03:00 committed by GitHub
parent ad850973e9
commit fad7314a37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 344 additions and 14 deletions

View File

@ -15,8 +15,10 @@ class MixDropExtractor(private val client: OkHttpClient) {
lang: String = "",
prefix: String = "",
externalSubs: List<Track> = emptyList(),
referer: String = DEFAULT_REFERER
): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
val headers = Headers.headersOf("Referer", referer)
val doc = client.newCall(GET(url, headers)).execute().use { it.asJsoup() }
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
?.data()
?.let(Unpacker::unpack)
@ -25,21 +27,26 @@ class MixDropExtractor(private val client: OkHttpClient) {
val videoUrl = "https:" + unpacked.substringAfter("Core.wurl=\"")
.substringBefore("\"")
val subs = if ("Core.remotesub" in unpacked) {
val subUrl = unpacked.substringAfter("Core.remotesub=\"").substringBefore("\"")
listOf(Track(URLDecoder.decode(subUrl, "utf-8"), "sub"))
} else {
emptyList()
val subs = unpacked.substringAfter("Core.remotesub=\"").substringBefore('"')
.takeIf(String::isNotBlank)
?.let { listOf(Track(URLDecoder.decode(it, "utf-8"), "sub")) }
?: emptyList()
val quality = buildString {
append("${prefix}MixDrop")
if (lang.isNotBlank()) append("($lang)")
}
val quality = prefix + ("MixDrop").let {
when {
lang.isNotBlank() -> "$it($lang)"
else -> it
}
}
val headers = Headers.headersOf("Referer", "https://mixdrop.co/")
return listOf(Video(videoUrl, quality, videoUrl, headers = headers, subtitleTracks = subs + externalSubs))
}
fun videosFromUrl(
url: String,
lang: String = "",
prefix: String = "",
externalSubs: List<Track> = emptyList(),
referer: String = DEFAULT_REFERER,
) = videoFromUrl(url, lang, prefix, externalSubs, referer)
}
private const val DEFAULT_REFERER = "https://mixdrop.co/"

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".en.myrunningman.MyRunningManUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="www.myrunningman.com"
android:pathPattern="/ep/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,21 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext {
extName = 'My Running Man'
pkgNameSuffix = 'en.myrunningman'
extClass = '.MyRunningMan'
extVersionCode = 1
libVersion = '13'
}
dependencies {
implementation(project(":lib-dood-extractor"))
implementation(project(":lib-mixdrop-extractor"))
implementation(project(":lib-streamtape-extractor"))
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,239 @@
package eu.kanade.tachiyomi.animeextension.en.myrunningman
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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET
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.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class MyRunningMan : ParsedAnimeHttpSource() {
override val name = "My Running Man"
override val baseUrl = "https://www.myrunningman.com"
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/episodes/mostwatched/$page")
override fun popularAnimeSelector() = "table > tbody > tr"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
element.selectFirst("p > strong > a")!!.run {
title = text()
setUrlWithoutDomain(attr("href"))
}
thumbnail_url = element.selectFirst("img")?.absUrl("src")
}
override fun popularAnimeNextPageSelector() = "li > a[aria-label=Next]"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/episodes/newest/$page")
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/ep/$id"))
.asObservableSuccess()
.map(::searchAnimeByIdParse)
} else {
super.fetchSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.use { it.asJsoup() })
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
GET("$baseUrl/_search.php?q=$query", headersBuilder().add("X-Requested-With", "XMLHttpRequest").build())
@Serializable
data class ResultDto(val value: String, val label: String)
override fun searchAnimeParse(response: Response): AnimesPage {
val animes = response.parseAs<List<ResultDto>>().map {
SAnime.create().apply {
url = "/ep/" + it.value
title = it.label
thumbnail_url = buildString {
append("$baseUrl/assets/epimg/${it.value.padStart(3, '0')}")
if ((it.value.toIntOrNull() ?: 1) > 396) append("_temp")
append(".jpg")
}
}
}
return AnimesPage(animes, false)
}
override fun searchAnimeSelector(): String {
throw UnsupportedOperationException("Not used.")
}
override fun searchAnimeFromElement(element: Element): SAnime {
throw UnsupportedOperationException("Not used.")
}
override fun searchAnimeNextPageSelector() = null
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("div.container h1")!!.text()
val row = document.selectFirst("div.row")!!
thumbnail_url = row.selectFirst("p > img")?.absUrl("src")
artist = row.select("li > a[href*=guest/]").eachText().joinToString().takeIf(String::isNotBlank)
genre = row.select("li > a[href*=tag/]").eachText().joinToString().takeIf(String::isNotBlank)
description = row.select("p:has(i.fa)").eachText().joinToString("\n") {
when {
it.startsWith("Watches") || it.startsWith("Faves") -> it.substringBefore(" (")
else -> it
}
}
status = when (document.selectFirst("div.alert:contains(Coming soon)")) {
null -> SAnime.COMPLETED
else -> SAnime.ONGOING
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.use { it.asJsoup() }
return listOf(
SEpisode.create().apply {
setUrlWithoutDomain(doc.location())
name = doc.selectFirst("div.container h1")!!.text()
episode_number = doc.selectFirst("div#userepoptions")
?.attr("data-ep")
?.toFloatOrNull()
?: 1F
date_upload = doc.selectFirst("p:contains(Broadcast Date)")
?.text()
?.substringAfter(": ")
?.substringBefore(" ")
?.toDate()
?: 0L
},
)
}
override fun episodeListSelector(): String {
throw UnsupportedOperationException("Not used.")
}
override fun episodeFromElement(element: Element): SEpisode {
throw UnsupportedOperationException("Not used.")
}
// ============================ Video Links =============================
private val doodExtractor by lazy { DoodExtractor(client) }
private val mixdropExtractor by lazy { MixDropExtractor(client) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.use { it.asJsoup() }
return doc.select("a.changePlayer")
.mapNotNull { getUrlById(it.attr("data-url")) }
.parallelMap { url ->
runCatching {
when {
url.contains("dooo") -> doodExtractor.videosFromUrl(url)
url.contains("mixdro") -> mixdropExtractor.videoFromUrl(url, referer = doc.location())
url.contains("streamtape.com") -> streamtapeExtractor.videoFromUrl(url)?.let(::listOf)
else -> null
}
}.getOrNull() ?: emptyList()
}.flatten().ifEmpty { throw Exception("No videos!") }
}
override fun videoListSelector(): String {
throw UnsupportedOperationException("Not used.")
}
private fun getUrlById(id: String): String? {
val decoded = id.replace(Regex("[a-zA-Z]")) {
val item = it.value
val offset = if (item.lowercase().single() < 'n') 13 else -13
Char(item.single().code + offset).toString()
}
val videoId = decoded.drop(1)
return when (decoded.first()) {
'd' -> "https://dooood.com/e/$videoId"
'm' -> "https://mixdroop.bz/e/$videoId"
't' -> "https://streamtape.com/e/$videoId"
else -> null
}
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException("Not used.")
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException("Not used.")
}
// ============================= Utilities ==============================
private inline fun <reified T> Response.parseAs(): T {
return use { it.body.string() }.let(json::decodeFromString)
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
companion object {
const val PREFIX_SEARCH = "id:"
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.en.myrunningman
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://www.myrunningman.com/ep/<item> intents
* and redirects them to the main Aniyomi process.
*/
class MyRunningManUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${MyRunningMan.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}