New source: LMAnime(all) (#1474)
This commit is contained in:
24
src/all/lmanime/AndroidManifest.xml
Normal file
24
src/all/lmanime/AndroidManifest.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="eu.kanade.tachiyomi.animeextension">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name=".all.lmanime.LMAnimeUrlActivity"
|
||||||
|
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="lmanime.com"
|
||||||
|
android:pathPattern="/..*/"
|
||||||
|
android:scheme="https" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
19
src/all/lmanime/build.gradle
Normal file
19
src/all/lmanime/build.gradle
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'LMAnime'
|
||||||
|
pkgNameSuffix = 'all.lmanime'
|
||||||
|
extClass = '.LMAnime'
|
||||||
|
extVersionCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib-fembed-extractor"))
|
||||||
|
implementation(project(":lib-okru-extractor"))
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/all/lmanime/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/lmanime/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
src/all/lmanime/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/lmanime/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
src/all/lmanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/lmanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
src/all/lmanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/lmanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
BIN
src/all/lmanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/lmanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,344 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.all.lmanime
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.MultiSelectListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.animeextension.all.lmanime.extractors.DailymotionExtractor
|
||||||
|
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.lib.okruextractor.OkruExtractor
|
||||||
|
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 okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
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 java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LMAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
|
override val name = "LMAnime"
|
||||||
|
|
||||||
|
override val baseUrl = "https://lmanime.com"
|
||||||
|
|
||||||
|
override val lang = "all"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||||
|
return SAnime.create().apply {
|
||||||
|
val ahref = element.selectFirst("h4 > a.series")!!
|
||||||
|
setUrlWithoutDomain(ahref.attr("href"))
|
||||||
|
title = ahref.text()
|
||||||
|
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeNextPageSelector() = null
|
||||||
|
|
||||||
|
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
|
||||||
|
|
||||||
|
override fun popularAnimeSelector() = "div.serieslist.wpop-alltime li"
|
||||||
|
|
||||||
|
// ============================== Episodes ==============================
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val doc = getRealDoc(response.asJsoup())
|
||||||
|
return doc.select(episodeListSelector()).map(::episodeFromElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeFromElement(element: Element): SEpisode {
|
||||||
|
return SEpisode.create().apply {
|
||||||
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
|
element.selectFirst("div.epl-title")!!.text().let {
|
||||||
|
name = it
|
||||||
|
episode_number = it.substringBefore(" (")
|
||||||
|
.substringAfterLast(" ")
|
||||||
|
.toFloatOrNull() ?: 0F
|
||||||
|
}
|
||||||
|
|
||||||
|
date_upload = element.selectFirst("div.epl-date")?.text().toDate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListSelector() = "div.eplister > ul > li > a"
|
||||||
|
|
||||||
|
// =========================== Anime Details ============================
|
||||||
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
|
val doc = getRealDoc(document)
|
||||||
|
return SAnime.create().apply {
|
||||||
|
setUrlWithoutDomain(doc.location())
|
||||||
|
title = doc.selectFirst("h1.entry-title")!!.text()
|
||||||
|
thumbnail_url = doc.selectFirst("div.thumb > img")!!.attr("src")
|
||||||
|
|
||||||
|
val infos = doc.selectFirst("div.info-content")!!
|
||||||
|
genre = infos.select("div.genxed > a").eachText().joinToString()
|
||||||
|
status = parseStatus(infos.getInfo("Status"))
|
||||||
|
artist = infos.getInfo("Studio")
|
||||||
|
author = infos.getInfo("Fansub")
|
||||||
|
|
||||||
|
description = buildString {
|
||||||
|
doc.selectFirst("div.entry-content")?.text()?.let {
|
||||||
|
append("$it\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
infos.select("div.spe > span").eachText().forEach {
|
||||||
|
append("$it\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Video Links =============================
|
||||||
|
override fun videoListSelector() = "select.mirror > option[data-index]"
|
||||||
|
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val items = response.asJsoup().select(videoListSelector())
|
||||||
|
val allowed = preferences.getStringSet(PREF_ALLOWED_LANGS_KEY, PREF_ALLOWED_LANGS_DEFAULT)!!
|
||||||
|
return items
|
||||||
|
.filter { element ->
|
||||||
|
val text = element.text()
|
||||||
|
allowed.any { it in text }
|
||||||
|
}.parallelMap {
|
||||||
|
val language = it.text().substringBefore(" ")
|
||||||
|
val url = getHosterUrl(it.attr("value"))
|
||||||
|
getVideoList(url, language)
|
||||||
|
}.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHosterUrl(encodedStr: String): String {
|
||||||
|
return Base64.decode(encodedStr, Base64.DEFAULT)
|
||||||
|
.let(::String) // bytearray -> string
|
||||||
|
.substringAfter("iframe")
|
||||||
|
.substringAfter("src=\"")
|
||||||
|
.substringBefore('"')
|
||||||
|
.let {
|
||||||
|
// sometimes the url doesnt specify its protocol
|
||||||
|
if (it.startsWith("http")) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
"https:$it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVideoList(url: String, language: String): List<Video> {
|
||||||
|
return runCatching {
|
||||||
|
when {
|
||||||
|
"ok.ru" in url ->
|
||||||
|
OkruExtractor(client).videosFromUrl(url, "$language -")
|
||||||
|
"fembed" in url ->
|
||||||
|
FembedExtractor(client).videosFromUrl(url, "$language -")
|
||||||
|
"dailymotion.com" in url ->
|
||||||
|
DailymotionExtractor(client).videosFromUrl(url, "Dailymotion ($language)")
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.getOrNull() ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoFromElement(element: Element): Video {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoUrlParse(document: Document): String {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||||
|
|
||||||
|
override fun searchAnimeNextPageSelector() = "div.pagination a.next"
|
||||||
|
|
||||||
|
override fun getFilterList() = LMAnimeFilters.filterList
|
||||||
|
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
|
return if (query.isNotBlank()) {
|
||||||
|
GET("$baseUrl/page/$page/?s=$query")
|
||||||
|
} else {
|
||||||
|
val genre = LMAnimeFilters.getGenre(filters)
|
||||||
|
GET("$baseUrl/genres/$genre/page/$page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeSelector() = "div.listupd article a.tip"
|
||||||
|
|
||||||
|
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/$id"))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map(::searchAnimeByIdParse)
|
||||||
|
} else {
|
||||||
|
super.fetchSearchAnime(page, query, filters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||||
|
val details = animeDetailsParse(response.asJsoup())
|
||||||
|
return AnimesPage(listOf(details), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||||
|
return SAnime.create().apply {
|
||||||
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
|
title = element.selectFirst("div.tt")!!.ownText()
|
||||||
|
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "div.hpage a:contains(Next)"
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page")
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.listupd.normal article a.tip"
|
||||||
|
|
||||||
|
// ============================== Settings ==============================
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val videoQualityPref = ListPreference(screen.context).apply {
|
||||||
|
key = PREF_QUALITY_KEY
|
||||||
|
title = PREF_QUALITY_TITLE
|
||||||
|
entries = PREF_QUALITY_ENTRIES
|
||||||
|
entryValues = PREF_QUALITY_ENTRIES
|
||||||
|
setDefaultValue(PREF_QUALITY_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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val langPref = ListPreference(screen.context).apply {
|
||||||
|
key = PREF_LANG_KEY
|
||||||
|
title = PREF_LANG_TITLE
|
||||||
|
entries = PREF_LANG_ENTRIES
|
||||||
|
entryValues = PREF_LANG_ENTRIES
|
||||||
|
setDefaultValue(PREF_LANG_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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowedPref = MultiSelectListPreference(screen.context).apply {
|
||||||
|
key = PREF_ALLOWED_LANGS_KEY
|
||||||
|
title = PREF_ALLOWED_LANGS_TITLE
|
||||||
|
entries = PREF_ALLOWED_LANGS_ENTRIES
|
||||||
|
entryValues = PREF_ALLOWED_LANGS_ENTRIES
|
||||||
|
setDefaultValue(PREF_ALLOWED_LANGS_DEFAULT)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
screen.addPreference(videoQualityPref)
|
||||||
|
screen.addPreference(langPref)
|
||||||
|
screen.addPreference(allowedPref)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
private fun getRealDoc(document: Document): Document {
|
||||||
|
return document.selectFirst("div.naveps a:contains(All episodes)")?.let { link ->
|
||||||
|
client.newCall(GET(link.attr("href")))
|
||||||
|
.execute()
|
||||||
|
.asJsoup()
|
||||||
|
} ?: document
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(statusString: String?): Int {
|
||||||
|
return when (statusString?.trim()) {
|
||||||
|
"Completed" -> SAnime.COMPLETED
|
||||||
|
"Ongoing" -> SAnime.ONGOING
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.getInfo(text: String): String? {
|
||||||
|
return selectFirst("span:contains($text)")
|
||||||
|
?.run {
|
||||||
|
selectFirst("a")?.text() ?: ownText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String?.toDate(): Long {
|
||||||
|
return this?.let {
|
||||||
|
runCatching {
|
||||||
|
DATE_FORMATTER.parse(this)?.time
|
||||||
|
}.getOrNull()
|
||||||
|
} ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun List<Video>.sort(): List<Video> {
|
||||||
|
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
|
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||||
|
return sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ it.quality.contains(quality) },
|
||||||
|
{ it.quality.contains(lang) },
|
||||||
|
),
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
||||||
|
runBlocking {
|
||||||
|
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DATE_FORMATTER by lazy { SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH) }
|
||||||
|
|
||||||
|
const val PREFIX_SEARCH = "id:"
|
||||||
|
|
||||||
|
private const val PREF_QUALITY_KEY = "pref_quality"
|
||||||
|
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||||
|
private const val PREF_QUALITY_DEFAULT = "720p"
|
||||||
|
private val PREF_QUALITY_ENTRIES = arrayOf("144p", "288p", "480p", "720p", "1080p")
|
||||||
|
|
||||||
|
private const val PREF_LANG_KEY = "pref_language"
|
||||||
|
private const val PREF_LANG_TITLE = "Preferred language"
|
||||||
|
private const val PREF_LANG_DEFAULT = "English"
|
||||||
|
private val PREF_LANG_ENTRIES = arrayOf(
|
||||||
|
"English",
|
||||||
|
"Español",
|
||||||
|
"Indonesian",
|
||||||
|
"Portugués",
|
||||||
|
"Türkçe",
|
||||||
|
"العَرَبِيَّة",
|
||||||
|
"ไทย",
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val PREF_ALLOWED_LANGS_KEY = "pref_allowed_languages"
|
||||||
|
private const val PREF_ALLOWED_LANGS_TITLE = "Allowed languages to fetch videos"
|
||||||
|
private val PREF_ALLOWED_LANGS_ENTRIES = PREF_LANG_ENTRIES
|
||||||
|
private val PREF_ALLOWED_LANGS_DEFAULT = PREF_ALLOWED_LANGS_ENTRIES.toSet()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.all.lmanime
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
|
||||||
|
object LMAnimeFilters {
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||||
|
return this.first { it is R }.let {
|
||||||
|
(it as QueryPartFilter).toQueryPart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenreFilter : QueryPartFilter("Genre", LMAnimeFiltersData.genres)
|
||||||
|
|
||||||
|
val filterList = AnimeFilterList(
|
||||||
|
AnimeFilter.Header(LMAnimeFiltersData.IGNORE_SEARCH_MSG),
|
||||||
|
GenreFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getGenre(filters: AnimeFilterList) = filters.asQueryPart<GenreFilter>()
|
||||||
|
|
||||||
|
private object LMAnimeFiltersData {
|
||||||
|
const val IGNORE_SEARCH_MSG = "NOTE: Ignored if using text search."
|
||||||
|
|
||||||
|
val genres = arrayOf(
|
||||||
|
Pair("Action", "action"),
|
||||||
|
Pair("Adventure", "adventure"),
|
||||||
|
Pair("Angel", "angel"),
|
||||||
|
Pair("cats", "cats"),
|
||||||
|
Pair("Comedy", "comedy"),
|
||||||
|
Pair("Crime", "crime"),
|
||||||
|
Pair("Cultivation", "cultivation"),
|
||||||
|
Pair("cure", "cure"),
|
||||||
|
Pair("Demon", "demon"),
|
||||||
|
Pair("Drama", "drama"),
|
||||||
|
Pair("Fantasy", "fantasy"),
|
||||||
|
Pair("fight", "fight"),
|
||||||
|
Pair("god", "god"),
|
||||||
|
Pair("growth", "growth"),
|
||||||
|
Pair("Harem", "harem"),
|
||||||
|
Pair("Historical", "historical"),
|
||||||
|
Pair("Horror", "horror"),
|
||||||
|
Pair("inspirational", "inspirational"),
|
||||||
|
Pair("isekei", "isekei"),
|
||||||
|
Pair("Magic", "magic"),
|
||||||
|
Pair("Martial Arts", "martial-arts"),
|
||||||
|
Pair("Mecha", "mecha"),
|
||||||
|
Pair("Military", "military"),
|
||||||
|
Pair("Mystery", "mystery"),
|
||||||
|
Pair("Mythology", "mythology"),
|
||||||
|
Pair("Original", "original"),
|
||||||
|
Pair("Poetry", "poetry"),
|
||||||
|
Pair("Psychological", "psychological"),
|
||||||
|
Pair("Romance", "romance"),
|
||||||
|
Pair("school", "school"),
|
||||||
|
Pair("Sci-Fi", "sci-fi"),
|
||||||
|
Pair("Shounen", "shounen"),
|
||||||
|
Pair("Slice of Life", "slice-of-life"),
|
||||||
|
Pair("Space", "space"),
|
||||||
|
Pair("Spirit", "spirit"),
|
||||||
|
Pair("Super Power", "super-power"),
|
||||||
|
Pair("Supernatural", "supernatural"),
|
||||||
|
Pair("Suspense", "suspense"),
|
||||||
|
Pair("Thriller", "thriller"),
|
||||||
|
Pair("War", "war"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.all.lmanime
|
||||||
|
|
||||||
|
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://lmanime.com/<item> intents
|
||||||
|
* and redirects them to the main Aniyomi process.
|
||||||
|
*/
|
||||||
|
class LMAnimeUrlActivity : Activity() {
|
||||||
|
|
||||||
|
private val TAG = javaClass.simpleName
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
if (pathSegments != null && pathSegments.size > 0) {
|
||||||
|
val item = pathSegments[0]
|
||||||
|
val mainIntent = Intent().apply {
|
||||||
|
action = "eu.kanade.tachiyomi.ANIMESEARCH"
|
||||||
|
putExtra("query", "${LMAnime.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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.all.lmanime.extractors
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DailyQuality(
|
||||||
|
val qualities: Auto,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Auto(
|
||||||
|
val auto: List<Video>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Video(
|
||||||
|
val type: String,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DailymotionExtractor(private val client: OkHttpClient) {
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||||
|
val id = url.substringBefore("?").substringAfterLast("/")
|
||||||
|
val jsonUrl = "https://www.dailymotion.com/player/metadata/video/$id"
|
||||||
|
val jsonRequest = client.newCall(GET(jsonUrl)).execute().body.string()
|
||||||
|
val parsed = json.decodeFromString<DailyQuality>(jsonRequest)
|
||||||
|
|
||||||
|
val masterUrl = parsed.qualities.auto.first().url
|
||||||
|
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string()
|
||||||
|
|
||||||
|
val separator = "#EXT-X-STREAM-INF"
|
||||||
|
return masterPlaylist.substringAfter(separator).split(separator).map {
|
||||||
|
val resolution = it.substringAfter("RESOLUTION=")
|
||||||
|
.substringAfter("x")
|
||||||
|
.substringBefore(",NAME") + "p"
|
||||||
|
val quality = "$prefix $resolution"
|
||||||
|
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||||
|
|
||||||
|
Video(videoUrl, quality, videoUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user