feat(multisrc): New theme: AnimeStream (#1653)

This commit is contained in:
Claudemirovsky 2023-05-30 09:51:11 +00:00 committed by GitHub
parent 26a6f43dce
commit 4b42d5569f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 880 additions and 1432 deletions

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="AnimeStreamGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="aniyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamGenerator" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=animestream" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=animestream" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DooPlayGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="aniyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.dooplay.DooPlayGenerator" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=dooplay" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=dooplay" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DopeFlixGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="aniyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.dopeflix.DopeFlixGenerator" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=dopeflix" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=dopeflix" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,6 @@
dependencies {
implementation(project(':lib-okru-extractor'))
implementation(project(':lib-gdriveplayer-extractor'))
implementation(project(':lib-streamsb-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,110 @@
package eu.kanade.tachiyomi.animeextension.all.animexin
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DailymotionExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DoodExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.VidstreamingExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.YouTubeExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
class AnimeXin : AnimeStream(
"all",
"AnimeXin",
"https://animexin.vip",
) {
override val id = 4620219025406449669
// ============================ Video Links =============================
override fun getVideoList(url: String, name: String): List<Video> {
val streamSbDomains = listOf(
"sbhight", "sbrity", "sbembed.com", "sbembed1.com", "sbplay.org",
"sbvideo.net", "streamsb.net", "sbplay.one", "cloudemb.com",
"playersb.com", "tubesb.com", "sbplay1.com", "embedsb.com",
"watchsb.com", "sbplay2.com", "japopav.tv", "viewsb.com",
"sbfast", "sbfull.com", "javplaya.com", "ssbstream.net",
"p1ayerjavseen.com", "sbthe.com", "vidmovie.xyz", "sbspeed.com",
"streamsss.net", "sblanh.com", "tvmshow.com", "sbanh.com",
"streamovies.xyz",
)
val prefix = "$name - "
return when {
url.contains("ok.ru") -> {
OkruExtractor(client).videosFromUrl(url, prefix = prefix)
}
streamSbDomains.any { it in url } -> {
StreamSBExtractor(client).videosFromUrl(url, headers, prefix = prefix)
}
url.contains("dailymotion") -> {
DailymotionExtractor(client).videosFromUrl(url, prefix = prefix)
}
url.contains("https://dood") -> {
DoodExtractor(client).videosFromUrl(url, quality = name)
}
url.contains("gdriveplayer") -> {
val gdriveHeaders = headersBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", "gdriveplayer.to")
.add("Referer", "$baseUrl/")
.build()
GdrivePlayerExtractor(client).videosFromUrl(url, name = name, headers = gdriveHeaders)
}
url.contains("youtube.com") -> {
YouTubeExtractor(client).videosFromUrl(url, prefix = prefix)
}
url.contains("vidstreaming") -> {
VidstreamingExtractor(client).videosFromUrl(url, prefix = prefix)
}
else -> emptyList()
}
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
super.setupPreferenceScreen(screen) // Quality preferences
val videoLangPref = ListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = PREF_LANG_TITLE
entries = PREF_LANG_VALUES
entryValues = PREF_LANG_VALUES
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()
}
}
screen.addPreference(videoLangPref)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
val language = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(language, true) },
),
).reversed()
}
companion object {
private const val PREF_LANG_KEY = "preferred_language"
private const val PREF_LANG_TITLE = "Preferred Video Language"
private const val PREF_LANG_DEFAULT = "All Sub"
private val PREF_LANG_VALUES = arrayOf(
"All Sub", "Arabic", "English", "German", "Indonesia", "Italian",
"Polish", "Portuguese", "Spanish", "Thai", "Turkish",
)
}
}

View File

@ -18,8 +18,7 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.CharacterIterator import kotlin.math.abs
import java.text.StringCharacterIterator
class YouTubeExtractor(private val client: OkHttpClient) { class YouTubeExtractor(private val client: OkHttpClient) {
@ -167,16 +166,17 @@ class YouTubeExtractor(private val client: OkHttpClient) {
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
fun formatBits(bits: Long): String? { fun formatBits(size: Long): String {
var bits = bits var bits = abs(size)
if (-1000 < bits && bits < 1000) { if (bits < 1000) {
return "${bits}b" return "${bits}b"
} }
val ci: CharacterIterator = StringCharacterIterator("kMGTPE") val iterator = "kMGTPE".iterator()
while (bits <= -999950 || bits >= 999950) { var currentChar = iterator.next()
while (bits >= 999950 && iterator.hasNext()) {
bits /= 1000 bits /= 1000
ci.next() currentChar = iterator.next()
} }
return java.lang.String.format("%.0f%cb", bits / 1000.0, ci.current()) return "%.0f%cb".format(bits / 1000.0, currentChar)
} }
} }

View File

@ -3,7 +3,7 @@
<application> <application>
<activity <activity
android:name=".all.lmanime.LMAnimeUrlActivity" android:name="eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">
@ -14,9 +14,9 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="lmanime.com" android:host="${SOURCEHOST}"
android:pathPattern="/..*/" android:pathPattern="/..*"
android:scheme="https" /> android:scheme="${SOURCESCHEME}" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>

View File

@ -0,0 +1,3 @@
dependencies {
implementation(project(":lib-okru-extractor"))
}

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,113 @@
package eu.kanade.tachiyomi.animeextension.all.lmanime
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.model.Video
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
class LMAnime : AnimeStream(
"all",
"LMAnime",
"https://lmanime.com",
) {
// ============================ Video Links =============================
override val prefQualityValues = arrayOf("144p", "288p", "480p", "720p", "1080p")
override val prefQualityEntries = prefQualityValues
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)
getVideoList(url, language)
}.flatten()
}
override fun getVideoList(url: String, name: String): List<Video> {
val prefix = "$name -"
return when {
"ok.ru" in url ->
OkruExtractor(client).videosFromUrl(url, prefix)
"dailymotion.com" in url ->
DailymotionExtractor(client).videosFromUrl(url, "Dailymotion ($name)")
else -> emptyList()
}
}
// ============================== Settings ==============================
@Suppress("UNCHECKED_CAST")
override fun setupPreferenceScreen(screen: PreferenceScreen) {
super.setupPreferenceScreen(screen) // Quality preferences
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(langPref)
screen.addPreference(allowedPref)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(lang, true) },
),
).reversed()
}
companion object {
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()
}
}

View File

@ -0,0 +1,3 @@
dependencies {
implementation(project(":lib-unpacker"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.animeextension.pt.rinecloud
import eu.kanade.tachiyomi.animeextension.pt.rinecloud.extractors.RineCloudExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
class RineCloud : AnimeStream(
"pt-BR",
"RineCloud",
"https://rine.cloud",
) {
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
// ============================ Video Links =============================
override val prefQualityValues = arrayOf("1080p", "720p", "480p", "360p", "240p")
override val prefQualityEntries = prefQualityValues
override fun getVideoList(url: String, name: String): List<Video> {
return when {
"rine.cloud" in url -> {
RineCloudExtractor(client).videosFromUrl(url, headers)
}
else -> emptyList()
}
}
}

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.animeextension.pt.rinecloud.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class RineCloudExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, headers: Headers): List<Video> {
val playerDoc = client.newCall(GET(url, headers)).execute().asJsoup()
val scriptData = playerDoc.selectFirst("script:containsData(JuicyCodes.Run)")
?.data()
?: return emptyList()
val decodedData = scriptData.substringAfter("(").substringBefore(")")
.split("+\"")
.joinToString("") { it.replace("\"", "") }
.let { Base64.decode(it, Base64.DEFAULT) }
.let(::String)
val unpackedJs = Unpacker.unpack(decodedData).ifEmpty { return emptyList() }
val masterPlaylistUrl = unpackedJs.substringAfter("sources:[")
.substringAfter("file\":\"")
.substringBefore('"')
val playlistData = client.newCall(GET(masterPlaylistUrl, headers)).execute()
.body.string()
val separator = "#EXT-X-STREAM-INF:"
return playlistData.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore("\n")
.substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, "RineCloud - $quality", videoUrl, headers = headers)
}
}
}

View File

@ -0,0 +1,398 @@
package eu.kanade.tachiyomi.multisrc.animestream
import android.app.Application
import android.util.Base64
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
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.multisrc.animestream.AnimeStreamFilters.GenresFilter
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.OrderFilter
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.SeasonFilter
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.StatusFilter
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.StudioFilter
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.SubFilter
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.TypeFilter
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.coroutines.withContext
import okhttp3.Request
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 java.text.SimpleDateFormat
import java.util.Locale
abstract class AnimeStream(
override val lang: String,
override val name: String,
override val baseUrl: String,
) : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient
protected open val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
const val PREFIX_SEARCH = "path:"
}
protected open val prefQualityDefault = "720p"
protected open val prefQualityKey = "preferred_quality"
protected open val prefQualityTitle = when (lang) {
"pt-BR" -> "Qualidade preferida"
else -> "Preferred quality"
}
protected open val prefQualityValues = arrayOf("1080p", "720p", "480p", "360p")
protected open val prefQualityEntries = prefQualityValues
protected open val videoSortPrefKey = prefQualityKey
protected open val videoSortPrefDefault = prefQualityDefault
protected open val dateFormatter by lazy {
val locale = when (lang) {
"pt-BR" -> Locale("pt", "BR")
else -> Locale.ENGLISH
}
SimpleDateFormat("MMMM d, yyyy", locale)
}
protected open val animeListUrl = "$baseUrl/anime"
// ============================== Popular ===============================
override fun fetchPopularAnime(page: Int): Observable<AnimesPage> {
fetchFilterList()
return super.fetchPopularAnime(page)
}
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")!!.getImageUrl()
}
}
override fun popularAnimeNextPageSelector() = null
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
/* Possible classes: wpop-weekly, wpop-monthly, wpop-alltime */
override fun popularAnimeSelector() = "div.serieslist.wpop-alltime li"
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.asJsoup()
return doc.select(episodeListSelector()).map(::episodeFromElement)
}
protected open val episodePrefix = when (lang) {
"pt-BR" -> "Episódio"
else -> "Episode"
}
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst("div.epl-num")!!.text().let {
name = "$episodePrefix $it"
episode_number = it.substringBefore(" ").toFloatOrNull() ?: 0F
}
element.selectFirst("div.epl-sub")?.text()?.let { scanlator = it }
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 {
return SAnime.create().apply {
setUrlWithoutDomain(document.location())
title = document.selectFirst("h1.entry-title")!!.text()
thumbnail_url = document.selectFirst("div.thumb > img")!!.getImageUrl()
val infos = document.selectFirst("div.info-content")!!
genre = infos.select("div.genxed > a").eachText().joinToString()
status = parseStatus(infos.getInfo("Status"))
artist = infos.getInfo("tudio")
author = infos.getInfo("Fansub")
description = buildString {
document.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())
return items.parallelMap { element ->
runCatching {
val name = element.text()
val url = getHosterUrl(element)
getVideoList(url, name)
}.onFailure { it.printStackTrace() }.getOrElse { emptyList() }
}.flatten()
}
protected open fun getHosterUrl(element: Element): String {
return Base64.decode(element.attr("value"), Base64.DEFAULT)
.let(::String) // bytearray -> string
.let(Jsoup::parse) // string -> document
.selectFirst("iframe[src~=.]")!!
.attr("src")
.let { // sometimes the url dont specify its protocol
when {
it.startsWith("http") -> it
else -> "https:$it"
}
}
}
protected open fun getVideoList(url: String, name: String): List<Video> {
Log.i(name, "getVideoList -> URL => $url || Name => $name")
return emptyList()
}
override fun videoFromElement(element: Element) = throw Exception("Not Used")
override fun videoUrlParse(document: Document) = throw Exception("Not Used")
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("div.tt")!!.ownText()
thumbnail_url = element.selectFirst("img")!!.getImageUrl()
}
}
override fun searchAnimeNextPageSelector() = "div.pagination a.next, div.hpage > a.r"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimeStreamFilters.getSearchParameters(filters)
return if (query.isNotEmpty()) {
GET("$baseUrl/page/$page/?s=$query")
} else {
val multiString = buildString {
if (params.genres.isNotEmpty()) append(params.genres + "&")
if (params.seasons.isNotEmpty()) append(params.seasons + "&")
if (params.studios.isNotEmpty()) append(params.studios + "&")
}
GET("$baseUrl/anime/?page=$page&$multiString&status=${params.status}&type=${params.type}&sub=${params.sub}&order=${params.order}")
}
}
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 path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path"))
.asObservableSuccess()
.map(::searchAnimeByPathParse)
} else {
super.fetchSearchAnime(page, query, filters)
}
}
protected open fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
// =============================== Latest ===============================
override fun fetchLatestUpdates(page: Int): Observable<AnimesPage> {
fetchFilterList()
return super.fetchLatestUpdates(page)
}
override fun latestUpdatesRequest(page: Int) = GET("$animeListUrl/?page=$page&order=update")
override fun latestUpdatesSelector() = searchAnimeSelector()
override fun latestUpdatesNextPageSelector() = searchAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element) = searchAnimeFromElement(element)
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = prefQualityKey
title = prefQualityTitle
entries = prefQualityEntries
entryValues = prefQualityValues
setDefaultValue(prefQualityDefault)
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(videoQualityPref)
}
// ============================== Filters ===============================
/**
* Disable it if you don't want the filters to be automatically fetched.
*/
protected open val fetchFilters = true
private fun fetchFilterList() {
if (fetchFilters && !AnimeStreamFilters.filterInitialized()) {
AnimeStreamFilters.filterElements = runBlocking {
withContext(Dispatchers.IO) {
client.newCall(GET(animeListUrl)).execute()
.asJsoup()
.select("span.sec1 > div.filter > ul")
}
}
}
}
protected open val filtersHeader = when (lang) {
"pt-BR" -> "NOTA: Filtros serão ignorados se usar a pesquisa por nome!"
else -> "NOTE: Filters are going to be ignored if using search text!"
}
protected open val filtersMissingWarning: String = when (lang) {
"pt-BR" -> "Aperte 'Redefinir' para tentar mostrar os filtros"
else -> "Press 'Reset' to attempt to show the filters"
}
protected open val genresFilterText = when (lang) {
"pt-BR" -> "Gêneros"
else -> "Genres"
}
protected open val seasonsFilterText = when (lang) {
"pt-BR" -> "Temporadas"
else -> "Seasons"
}
protected open val studioFilterText = when (lang) {
"pt-BR" -> "Estúdios"
else -> "Studios"
}
protected open val statusFilterText = "Status"
protected open val typeFilterText = when (lang) {
"pt-BR" -> "Tipo"
else -> "Type"
}
protected open val subFilterText = when (lang) {
"pt-BR" -> "Legenda"
else -> "Subtitle"
}
protected open val orderFilterText = when (lang) {
"pt-BR" -> "Ordem"
else -> "Order"
}
override fun getFilterList(): AnimeFilterList {
return if (fetchFilters && AnimeStreamFilters.filterInitialized()) {
AnimeFilterList(
GenresFilter(genresFilterText),
SeasonFilter(seasonsFilterText),
StudioFilter(studioFilterText),
AnimeFilter.Separator(),
StatusFilter(statusFilterText),
TypeFilter(typeFilterText),
SubFilter(subFilterText),
OrderFilter(orderFilterText),
)
} else if (fetchFilters) {
AnimeFilterList(AnimeFilter.Header(filtersMissingWarning))
} else {
AnimeFilterList()
}
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(videoSortPrefKey, videoSortPrefDefault)!!
return sortedWith(
compareBy { it.quality.contains(quality, true) },
).reversed()
}
protected open fun parseStatus(statusString: String?): Int {
return when (statusString?.trim()?.lowercase()) {
"completed", "completo" -> SAnime.COMPLETED
"ongoing", "lançamento" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
protected open fun Element.getInfo(text: String): String? {
return selectFirst("span:contains($text)")
?.run {
selectFirst("a")?.text() ?: ownText()
}
}
protected open fun String?.toDate(): Long {
return this?.let {
runCatching {
dateFormatter.parse(trim())?.time
}.getOrNull()
} ?: 0L
}
protected inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
/**
* Tries to get the image url via various possible attributes.
* Taken from Tachiyomi's Madara multisrc.
*/
protected open fun Element.getImageUrl(): String? {
return when {
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
else -> attr("abs:src")
}.substringBefore("?resize")
}
}

View File

@ -0,0 +1,103 @@
package eu.kanade.tachiyomi.multisrc.animestream
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import org.jsoup.select.Elements
object AnimeStreamFilters {
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, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
when {
checkbox.state -> {
options.find { it.first == checkbox.name }!!.second
}
else -> null
}
}.joinToString("&$name[]=").let {
when {
it.isBlank() -> ""
else -> "$name[]=$it"
}
}
}
class GenresFilter(name: String) : CheckBoxFilterList(name, GENRES_LIST)
class SeasonFilter(name: String) : CheckBoxFilterList(name, SEASON_LIST)
class StudioFilter(name: String) : CheckBoxFilterList(name, STUDIO_LIST)
class StatusFilter(name: String) : QueryPartFilter(name, STATUS_LIST)
class TypeFilter(name: String) : QueryPartFilter(name, TYPE_LIST)
class SubFilter(name: String) : QueryPartFilter(name, SUB_LIST)
class OrderFilter(name: String) : QueryPartFilter(name, ORDER_LIST)
data class FilterSearchParams(
val genres: String = "",
val seasons: String = "",
val studios: String = "",
val status: String = "",
val type: String = "",
val sub: String = "",
val order: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenresFilter>(GENRES_LIST, "genre"),
filters.parseCheckbox<SeasonFilter>(SEASON_LIST, "season"),
filters.parseCheckbox<StudioFilter>(STUDIO_LIST, "studio"),
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<SubFilter>(),
filters.asQueryPart<OrderFilter>(),
)
}
internal lateinit var filterElements: Elements
internal fun filterInitialized() = ::filterElements.isInitialized
private fun getPairListByIndex(index: Int) = filterElements.get(index)
.select("li")
.map { element ->
val key = element.selectFirst("label")!!.text()
val value = element.selectFirst("input")!!.attr("value")
Pair(key, value)
}.toTypedArray()
private val GENRES_LIST by lazy { getPairListByIndex(0) }
private val SEASON_LIST by lazy { getPairListByIndex(1) }
private val STUDIO_LIST by lazy { getPairListByIndex(2) }
private val STATUS_LIST by lazy { getPairListByIndex(3) }
private val TYPE_LIST by lazy { getPairListByIndex(4) }
private val SUB_LIST by lazy { getPairListByIndex(5) }
private val ORDER_LIST by lazy { getPairListByIndex(6) }
}

View File

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.multisrc.animestream
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class AnimeStreamGenerator : ThemeSourceGenerator {
override val themePkg = "animestream"
override val themeClass = "AnimeStream"
override val baseVersionCode = 1
override val sources = listOf(
SingleLang("AnimeXin", "https://animexin.vip", "all", isNsfw = false, overrideVersionCode = 4),
SingleLang("LMAnime", "https://lmanime.com", "all", isNsfw = false, overrideVersionCode = 2),
SingleLang("RineCloud", "https://rine.cloud", "pt-BR", isNsfw = false),
)
companion object {
@JvmStatic
fun main(args: Array<String>) = AnimeStreamGenerator().createAll()
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.animeextension.all.lmanime package eu.kanade.tachiyomi.multisrc.animestream
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
@ -7,22 +7,18 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** class AnimeStreamUrlActivity : Activity() {
* Springboard that accepts https://lmanime.com/<item> intents
* and redirects them to the main Aniyomi process.
*/
class LMAnimeUrlActivity : Activity() {
private val tag = javaClass.simpleName private val tag by lazy { javaClass.simpleName }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 0) { if (pathSegments != null && pathSegments.isNotEmpty()) {
val item = pathSegments[0] val path = pathSegments.joinToString("/")
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH" action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${LMAnime.PREFIX_SEARCH}$item") putExtra("query", "${AnimeStream.PREFIX_SEARCH}$path")
putExtra("filter", packageName) putExtra("filter", packageName)
} }

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -1,18 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'AnimeXin'
pkgNameSuffix = 'all.animexin'
extClass = '.AnimeXin'
extVersionCode = 4
libVersion = '13'
}
dependencies {
implementation(project(':lib-okru-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

View File

@ -1,339 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animexin
import android.app.Application
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DailymotionExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DoodExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.FembedExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.GdrivePlayerExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.StreamSBExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.VidstreamingExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.YouTubeExtractor
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.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.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
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 java.text.SimpleDateFormat
import java.util.Locale
class AnimeXin : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeXin"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://animexin.vip")!! }
override val lang = "all"
override val id = 4620219025406449669
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
}
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/")
override fun popularAnimeSelector(): String = "div.wpop-weekly > ul > li"
override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a.series")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img")!!.attr("src").substringBefore("?resize")
title = element.selectFirst("a.series:not(:has(img))")!!.text()
}
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/anime/?page=$page&status=&type=&order=update")
override fun latestUpdatesSelector(): String = searchAnimeSelector()
override fun latestUpdatesNextPageSelector(): String = searchAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = searchAnimeFromElement(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 = AnimeXinFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
private fun searchAnimeRequest(page: Int, query: String, filters: AnimeXinFilters.FilterSearchParams): Request {
return if (query.isNotEmpty()) {
GET("$baseUrl/page/$page/?s=$query")
} else {
val multiChoose = mutableListOf<String>()
if (filters.genres.isNotEmpty()) multiChoose.add(filters.genres)
if (filters.SEASONS.isNotEmpty()) multiChoose.add(filters.SEASONS)
if (filters.studios.isNotEmpty()) multiChoose.add(filters.studios)
val multiString = if (multiChoose.isEmpty()) "" else multiChoose.joinToString("&") + "&"
GET("$baseUrl/anime/?page=$page&${multiString}status=${filters.status}&type=${filters.type}&sub=${filters.sub}&order=${filters.order}")
}
}
override fun searchAnimeSelector(): String = "div.listupd > article"
override fun searchAnimeNextPageSelector(): String = "div.hpage > a:contains(Next)"
override fun searchAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img")!!.attr("src").substringBefore("?resize")
title = element.selectFirst("div.tt")!!.text()
}
}
override fun getFilterList(): AnimeFilterList = AnimeXinFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
return SAnime.create().apply {
title = document.selectFirst("h1.entry-title")!!.text()
thumbnail_url = document.selectFirst("div.thumb > img")!!.attr("src").substringBefore("?resize")
status = SAnime.COMPLETED
description = document.select("div[itemprop=description] p")?.let {
it.joinToString("\n\n") { t -> t.text() } +
"\n\n" +
document.select("div.info-content > div > span").joinToString("\n") { info ->
info.text().replace(":", ": ")
}
} ?: ""
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return document.select("div.eplister > ul > li").map { episodeElement ->
val numberText = episodeElement.selectFirst("div.epl-num")!!.text()
val numberString = numberText.substringBefore(" ")
val episodeNumber = if (numberText.contains("part 2", true)) {
numberString.toFloatOrNull()?.plus(0.5F) ?: 0F
} else {
numberString.toFloatOrNull() ?: 0F
}
SEpisode.create().apply {
episode_number = episodeNumber
name = numberText
date_upload = parseDate(episodeElement.selectFirst("div.epl-date")?.text() ?: "")
setUrlWithoutDomain(episodeElement.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
}
}
}
override fun episodeListSelector(): String = throw Exception("Not Used")
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not Used")
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
videoList.addAll(
document.select("select.mirror > option[value~=.]").parallelMap { source ->
runCatching {
var decoded = Jsoup.parse(
String(Base64.decode(source.attr("value"), Base64.DEFAULT)),
).select("iframe[src~=.]").attr("src")
if (!decoded.startsWith("http")) decoded = "https:$decoded"
val prefix = "${source.text()} - "
when {
decoded.contains("ok.ru") -> {
OkruExtractor(client).videosFromUrl(decoded, prefix = prefix)
}
decoded.contains("sbhight") || decoded.contains("sbrity") || decoded.contains("sbembed.com") || decoded.contains("sbembed1.com") || decoded.contains("sbplay.org") ||
decoded.contains("sbvideo.net") || decoded.contains("streamsb.net") || decoded.contains("sbplay.one") ||
decoded.contains("cloudemb.com") || decoded.contains("playersb.com") || decoded.contains("tubesb.com") ||
decoded.contains("sbplay1.com") || decoded.contains("embedsb.com") || decoded.contains("watchsb.com") ||
decoded.contains("sbplay2.com") || decoded.contains("japopav.tv") || decoded.contains("viewsb.com") ||
decoded.contains("sbfast") || decoded.contains("sbfull.com") || decoded.contains("javplaya.com") ||
decoded.contains("ssbstream.net") || decoded.contains("p1ayerjavseen.com") || decoded.contains("sbthe.com") ||
decoded.contains("vidmovie.xyz") || decoded.contains("sbspeed.com") || decoded.contains("streamsss.net") ||
decoded.contains("sblanh.com") || decoded.contains("tvmshow.com") || decoded.contains("sbanh.com") ||
decoded.contains("streamovies.xyz") -> {
StreamSBExtractor(client).videosFromUrl(decoded, headers, prefix = prefix)
}
decoded.contains("dailymotion") -> {
DailymotionExtractor(client).videosFromUrl(decoded, prefix = prefix)
}
decoded.contains("https://dood") -> {
DoodExtractor(client).videosFromUrl(decoded, quality = source.text())
}
decoded.contains("fembed") ||
decoded.contains("anime789.com") || decoded.contains("24hd.club") || decoded.contains("fembad.org") ||
decoded.contains("vcdn.io") || decoded.contains("sharinglink.club") || decoded.contains("moviemaniac.org") ||
decoded.contains("votrefiles.club") || decoded.contains("femoload.xyz") || decoded.contains("albavido.xyz") ||
decoded.contains("feurl.com") || decoded.contains("dailyplanet.pw") || decoded.contains("ncdnstm.com") ||
decoded.contains("jplayer.net") || decoded.contains("xstreamcdn.com") || decoded.contains("fembed-hd.com") ||
decoded.contains("gcloud.live") || decoded.contains("vcdnplay.com") || decoded.contains("superplayxyz.club") ||
decoded.contains("vidohd.com") || decoded.contains("vidsource.me") || decoded.contains("cinegrabber.com") ||
decoded.contains("votrefile.xyz") || decoded.contains("zidiplay.com") || decoded.contains("ndrama.xyz") ||
decoded.contains("fcdn.stream") || decoded.contains("mediashore.org") || decoded.contains("suzihaza.com") ||
decoded.contains("there.to") || decoded.contains("femax20.com") || decoded.contains("javstream.top") ||
decoded.contains("viplayer.cc") || decoded.contains("sexhd.co") || decoded.contains("fembed.net") ||
decoded.contains("mrdhan.com") || decoded.contains("votrefilms.xyz") || // decoded.contains("") ||
decoded.contains("embedsito.com") || decoded.contains("dutrag.com") || // decoded.contains("") ||
decoded.contains("youvideos.ru") || decoded.contains("streamm4u.club") || // decoded.contains("") ||
decoded.contains("moviepl.xyz") || decoded.contains("asianclub.tv") || // decoded.contains("") ||
decoded.contains("vidcloud.fun") || decoded.contains("fplayer.info") || // decoded.contains("") ||
decoded.contains("diasfem.com") || decoded.contains("javpoll.com") || decoded.contains("reeoov.tube") ||
decoded.contains("suzihaza.com") || decoded.contains("ezsubz.com") || decoded.contains("vidsrc.xyz") ||
decoded.contains("diampokusy.com") || decoded.contains("diampokusy.com") || decoded.contains("i18n.pw") ||
decoded.contains("vanfem.com") || decoded.contains("fembed9hd.com") || decoded.contains("votrefilms.xyz") || decoded.contains("watchjavnow.xyz")
-> {
val newUrl = decoded.replace("https://www.fembed.com", "https://vanfem.com")
FembedExtractor(client).videosFromUrl(newUrl, prefix = prefix)
}
decoded.contains("gdriveplayer") -> {
GdrivePlayerExtractor(client).videosFromUrl(decoded, name = source.text())
}
decoded.contains("youtube.com") -> {
YouTubeExtractor(client).videosFromUrl(decoded, prefix = prefix)
}
decoded.contains("vidstreaming") -> {
VidstreamingExtractor(client).videosFromUrl(decoded, prefix = prefix)
}
else -> null
}
}.getOrNull()
}.filterNotNull().flatten(),
)
return 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")!!
val language = preferences.getString("preferred_language", "All Sub")!!
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(language, true) },
),
).reversed()
}
private fun parseDate(dateStr: String): Long {
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Preferred domain (requires app restart)"
entries = arrayOf("animexin.vip")
entryValues = arrayOf("https://animexin.vip")
setDefaultValue("https://animexin.vip")
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()
}
}
val videoLangPref = ListPreference(screen.context).apply {
key = "preferred_language"
title = "Preferred Video Language"
entries = arrayOf("All Sub", "English", "Spanish", "Arabic", "German", "Indonesia", "Italian", "Polish", "Portuguese", "Thai", "Turkish")
entryValues = arrayOf("All Sub", "English", "Spanish", "Arabic", "German", "Indonesia", "Italian", "Polish", "Portuguese", "Thai", "Turkish")
setDefaultValue("All Sub")
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)
screen.addPreference(videoLangPref)
}
// 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

@ -1,226 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animexin
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimeXinFilters {
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.getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("&$name[]=").let {
if (it.isBlank()) {
""
} else {
"$name[]=$it"
}
}
}
class GenresFilter : CheckBoxFilterList(
"Genres",
AnimeXinFiltersData.GENRES.map { CheckBoxVal(it.first, false) },
)
class SeasonsFilter : CheckBoxFilterList(
"Seasons",
AnimeXinFiltersData.SEASONS.map { CheckBoxVal(it.first, false) },
)
class StudiosFilter : CheckBoxFilterList(
"Studios",
AnimeXinFiltersData.STUDIOS.map { CheckBoxVal(it.first, false) },
)
class StatusFilter : QueryPartFilter("Status", AnimeXinFiltersData.STATUS)
class TypeFilter : QueryPartFilter("Type", AnimeXinFiltersData.TYPE)
class SubFilter : QueryPartFilter("Sub", AnimeXinFiltersData.SUB)
class OrderFilter : QueryPartFilter("Order", AnimeXinFiltersData.ORDER)
val FILTER_LIST = AnimeFilterList(
GenresFilter(),
SeasonsFilter(),
StudiosFilter(),
AnimeFilter.Separator(),
StatusFilter(),
TypeFilter(),
SubFilter(),
OrderFilter(),
)
data class FilterSearchParams(
val genres: String = "",
val SEASONS: String = "",
val studios: String = "",
val status: String = "",
val type: String = "",
val sub: String = "",
val order: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenresFilter>(AnimeXinFiltersData.GENRES, "genre"),
filters.parseCheckbox<SeasonsFilter>(AnimeXinFiltersData.SEASONS, "season"),
filters.parseCheckbox<StudiosFilter>(AnimeXinFiltersData.STUDIOS, "studio"),
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<SubFilter>(),
filters.asQueryPart<OrderFilter>(),
)
}
private object AnimeXinFiltersData {
val ALL = Pair("All", "")
val GENRES = arrayOf(
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Comedy", "comedy"),
Pair("Demon", "demon"),
Pair("Drama", "drama"),
Pair("Fantasy", "fantasy"),
Pair("Game", "game"),
Pair("Historical", "historical"),
Pair("Isekai", "isekai"),
Pair("Magic", "magic"),
Pair("Martial Arts", "martial-arts"),
Pair("Mystery", "mystery"),
Pair("Over Power", "over-power"),
Pair("Reincarnation", "reincarnation"),
Pair("Romance", "romance"),
Pair("School", "school"),
Pair("Sci-fi", "sci-fi"),
Pair("Supernatural", "supernatural"),
Pair("War", "war"),
)
val SEASONS = arrayOf(
Pair("4", "4"),
Pair("OVA", "ova"),
Pair("Season 1", "season-1"),
Pair("Season 2", "season-2"),
Pair("Season 3", "season-3"),
Pair("Season 4", "season-4"),
Pair("Season 5", "season-5"),
Pair("Season 7", "season-7"),
Pair("Season 8", "season-8"),
Pair("season1", "season1"),
Pair("Winter 2023", "winter-2023"),
)
val STUDIOS = arrayOf(
Pair("2:10 Animatión", "210-animation"),
Pair("ASK Animation Studio", "ask-animation-studio"),
Pair("Axis Studios", "axis-studios"),
Pair("Azure Sea Studios", "azure-sea-studios"),
Pair("B.C May Pictures", "b-c-may-pictures"),
Pair("B.CMAY PICTURES", "b-cmay-pictures"),
Pair("BigFireBird Animation", "bigfirebird-animation"),
Pair("Bili Bili", "bili-bili"),
Pair("Bilibili", "bilibili"),
Pair("Build Dream", "build-dream"),
Pair("BYMENT", "byment"),
Pair("CG Year", "cg-year"),
Pair("CHOSEN", "chosen"),
Pair("Cloud Art", "cloud-art"),
Pair("Colored Pencil Animation", "colored-pencil-animation"),
Pair("D.ROCK-ART", "d-rock-art"),
Pair("Djinn Power", "djinn-power"),
Pair("Green Monster Team", "green-monster-team"),
Pair("Haoliners Animation", "haoliners-animation"),
Pair("He Zhou Culture", "he-zhou-culture"),
Pair("L²Studio", "l%c2%b2studio"),
Pair("Lingsanwu Animation", "lingsanwu-animation"),
Pair("Mili Pictures", "mili-pictures"),
Pair("Nice Boat Animation", "nice-boat-animation"),
Pair("Original Force", "original-force"),
Pair("Pb Animation Co. Ltd.", "pb-animation-co-ltd"),
Pair("Qing Xiang", "qing-xiang"),
Pair("Ruo Hong Culture", "ruo-hong-culture"),
Pair("Samsara Animation Studio", "samsara-animation-studio"),
Pair("Shanghai Foch Film Culture Investment", "shanghai-foch-film-culture-investment"),
Pair("Shanghai Motion Magic", "shanghai-motion-magic"),
Pair("Shenman Entertainment", "shenman-entertainment"),
Pair("Soyep", "soyep"),
Pair("soyep.cn", "soyep-cn"),
Pair("Sparkly Key Animation Studio", "sparkly-key-animation-studio"),
Pair("Tencent Penguin Pictures.", "tencent-penguin-pictures"),
Pair("Wan Wei Mao Donghua", "wan-wei-mao-donghua"),
Pair("Wawayu Animation", "wawayu-animation"),
Pair("Wonder Cat Animation", "wonder-cat-animation"),
Pair("Xing Yi Kai Chen", "xing-yi-kai-chen"),
Pair("Xuan Yuan", "xuan-yuan"),
Pair("Year Young Culture", "year-young-culture"),
)
val STATUS = arrayOf(
ALL,
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
Pair("Upcoming", "upcoming"),
Pair("Hiatus", "hiatus"),
)
val TYPE = arrayOf(
ALL,
Pair("TV Series", "tv"),
Pair("OVA", "ova"),
Pair("Movie", "movie"),
Pair("Live Action", "live action"),
Pair("Special", "special"),
Pair("BD", "bd"),
Pair("ONA", "ona"),
Pair("Music", "music"),
)
val SUB = arrayOf(
ALL,
Pair("Sub", "sub"),
Pair("Dub", "dub"),
Pair("RAW", "raw"),
)
val ORDER = arrayOf(
Pair("Default", ""),
Pair("A-Z", "title"),
Pair("Z-A", "titlereverse"),
Pair("Latest Update", "update"),
Pair("Latest Added", "latest"),
Pair("Popular", "popular"),
Pair("Rating", "rating"),
)
}
}

View File

@ -1,89 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
@Serializable
data class FembedResponse(
val success: Boolean,
val data: List<FembedVideo> = emptyList(),
val captions: List<Caption> = emptyList(),
) {
@Serializable
data class FembedVideo(
val file: String,
val label: String,
)
@Serializable
data class Caption(
val id: String,
val hash: String,
val language: String,
val extension: String,
)
}
class FembedExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, prefix: String = "", redirect: Boolean = false): List<Video> {
val videoApi = if (redirect) {
(
runCatching {
client.newCall(GET(url)).execute().request.url.toString()
.replace("/v/", "/api/source/")
}.getOrNull() ?: return emptyList<Video>()
)
} else {
url.replace("/v/", "/api/source/")
}
val body = runCatching {
client.newCall(POST(videoApi)).execute().body.string()
}.getOrNull() ?: return emptyList()
val userId = client.newCall(GET(url)).execute().asJsoup()
.selectFirst("script:containsData(USER_ID)")!!
.data()
.substringAfter("USER_ID")
.substringAfter("'")
.substringBefore("'")
val jsonResponse = try { Json { ignoreUnknownKeys = true }.decodeFromString<FembedResponse>(body) } catch (e: Exception) { FembedResponse(false, emptyList(), emptyList()) }
return if (jsonResponse.success) {
val subtitleList = mutableListOf<Track>()
try {
subtitleList.addAll(
jsonResponse.captions.map {
Track(
"https://${url.toHttpUrl().host}/asset/userdata/$userId/caption/${it.hash}/${it.id}.${it.extension}",
it.language,
)
},
)
} catch (a: Exception) { }
jsonResponse.data.map {
val quality = ("Fembed:${it.label}").let {
if (prefix.isNotBlank()) {
"$prefix $it"
} else {
it
}
}
try {
Video(it.file, quality, it.file, subtitleTracks = subtitleList)
} catch (a: Exception) {
Video(it.file, quality, it.file)
}
}
} else { emptyList<Video>() }
}
}

View File

@ -1,157 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import android.util.Base64
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.OkHttpClient
import org.jsoup.Jsoup
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class GdrivePlayerExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, name: String): List<Video> {
val headers = Headers.headersOf(
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Host",
"gdriveplayer.to",
"Referer",
"https://animexin.vip/",
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0",
)
val body = client.newCall(GET(url.replace(".me", ".to"), headers = headers)).execute()
.body.string()
val subtitleUrl = Jsoup.parse(body).selectFirst("div:contains(\\.srt)")
val subtitleList = mutableListOf<Track>()
if (subtitleUrl != null) {
try {
subtitleList.add(
Track(
"https://gdriveplayer.to/?subtitle=" + subtitleUrl.text(),
"Subtitles",
),
)
} catch (a: Exception) { }
}
val eval = JsUnpacker.unpackAndCombine(body)!!.replace("\\", "")
val json = Json.decodeFromString<JsonObject>(REGEX_DATAJSON.getFirst(eval))
val sojson = REGEX_SOJSON.getFirst(eval)
.split(Regex("\\D+"))
.joinToString("") {
Char(it.toInt()).toString()
}
val password = REGEX_PASSWORD.getFirst(sojson).toByteArray()
val decrypted = decryptAES(password, json)!!
val secondEval = JsUnpacker.unpackAndCombine(decrypted)!!.replace("\\", "")
return REGEX_VIDEOURL.findAll(secondEval)
.distinctBy { it.groupValues[2] } // remove duplicates by quality
.map {
val qualityStr = it.groupValues[2]
val quality = "$PLAYER_NAME ${qualityStr}p - $name"
val videoUrl = "https:" + it.groupValues[1] + "&res=$qualityStr"
try {
Video(videoUrl, quality, videoUrl, subtitleTracks = subtitleList)
} catch (a: Exception) {
Video(videoUrl, quality, videoUrl)
}
}.toList()
}
private fun decryptAES(password: ByteArray, json: JsonObject): String? {
val salt = json["s"]!!.jsonPrimitive.content
val encodedCiphetext = json["ct"]!!.jsonPrimitive.content
val ciphertext = Base64.decode(encodedCiphetext, Base64.DEFAULT)
val (key, iv) = generateKeyAndIv(password, salt.decodeHex())
?: return null
val keySpec = SecretKeySpec(key, "AES")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
val decryptedData = String(cipher.doFinal(ciphertext))
return decryptedData
}
// https://stackoverflow.com/a/41434590/8166854
private fun generateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1,
): List<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.getDigestLength()
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
var generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0) {
md.update(
generatedData,
generatedLength - digestLength,
digestLength,
)
}
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
val result = listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize),
)
return result
} catch (e: DigestException) {
return null
}
}
private fun Regex.getFirst(item: String): String {
return find(item)?.groups?.elementAt(1)?.value!!
}
// Stolen from AnimixPlay(EN) / GogoCdnExtractor
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
companion object {
private const val PLAYER_NAME = "GDRIVE"
private val REGEX_DATAJSON = Regex("data='(\\S+?)'")
private val REGEX_PASSWORD = Regex("var pass = \"(\\S+?)\"")
private val REGEX_SOJSON = Regex("null,['|\"](\\w+)['|\"]")
private val REGEX_VIDEOURL = Regex("file\":\"(\\S+?)\".*?res=(\\d+)")
}
}

View File

@ -1,136 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.OkHttpClient
class StreamSBExtractor(private val client: OkHttpClient) {
protected fun bytesToHex(bytes: ByteArray): String {
val hexArray = "0123456789ABCDEF".toCharArray()
val hexChars = CharArray(bytes.size * 2)
for (j in bytes.indices) {
val v = bytes[j].toInt() and 0xFF
hexChars[j * 2] = hexArray[v ushr 4]
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
}
return String(hexChars)
}
// animension, asianload and dramacool uses "common = false"
private fun fixUrl(url: String, common: Boolean): String {
val sbUrl = url.substringBefore("/e/")
val id = url.substringAfter("/e/")
.substringBefore("?")
.substringBefore(".html")
return if (common) {
val hexBytes = bytesToHex(id.toByteArray())
"$sbUrl/sources50/625a364258615242766475327c7c${hexBytes}7c7c4761574550654f7461566d347c7c73747265616d7362"
} else {
"$sbUrl/sources50/${bytesToHex("||$id||||streamsb".toByteArray())}/"
}
}
fun videosFromUrl(url: String, headers: Headers, prefix: String = "", suffix: String = "", common: Boolean = true): List<Video> {
val newHeaders = headers.newBuilder()
.set("referer", url)
.set("watchsb", "sbstream")
.set("authority", "embedsb.com")
.build()
return try {
val master = fixUrl(url, common)
val json = Json.decodeFromString<JsonObject>(
client.newCall(GET(master, newHeaders))
.execute().body.string(),
)
val subtitleList = mutableListOf<Track>()
val subsList = json["stream_data"]!!.jsonObject["subs"]
if (subsList != null) {
try {
subtitleList.addAll(
subsList.jsonArray.map {
Track(
it.jsonObject["file"]!!.jsonPrimitive.content,
it.jsonObject["label"]!!.jsonPrimitive.content,
)
},
)
} catch (a: Exception) { }
}
val masterUrl = json["stream_data"]!!.jsonObject["file"].toString().trim('"')
val masterPlaylist = client.newCall(GET(masterUrl, newHeaders))
.execute()
.body.string()
val separator = "#EXT-X-STREAM-INF"
masterPlaylist.substringAfter(separator).split(separator).map {
val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n")
.substringAfter("x")
.substringBefore(",") + "p"
val quality = ("StreamSB:" + resolution).let {
if (prefix.isNotBlank()) {
"$prefix $it"
} else {
it
}
}.let {
if (suffix.isNotBlank()) {
"$it $suffix"
} else {
it
}
}
val videoUrl = it.substringAfter("\n").substringBefore("\n")
try {
Video(videoUrl, quality, videoUrl, headers = newHeaders, subtitleTracks = subtitleList)
} catch (a: Exception) {
Video(videoUrl, quality, videoUrl, headers = newHeaders)
}
}
} catch (e: Exception) {
emptyList<Video>()
}
}
fun videosFromDecryptedUrl(realUrl: String, headers: Headers, prefix: String = "", suffix: String = ""): List<Video> {
return try {
val json = Json.decodeFromString<JsonObject>(client.newCall(GET(realUrl, headers)).execute().body.string())
val masterUrl = json["stream_data"]!!.jsonObject["file"].toString().trim('"')
val masterPlaylist = client.newCall(GET(masterUrl, headers)).execute().body.string()
val separator = "#EXT-X-STREAM-INF"
masterPlaylist.substringAfter(separator).split(separator).map {
val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n")
.substringAfter("x")
.substringBefore(",") + "p"
val quality = ("StreamSB:$resolution").let {
if (prefix.isNotBlank()) {
"$prefix $it"
} else {
it
}
}.let {
if (suffix.isNotBlank()) {
"$it $suffix"
} else {
it
}
}
val videoUrl = it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, quality, videoUrl, headers = headers)
}
} catch (e: Exception) {
emptyList()
}
}
}

View File

@ -1,19 +0,0 @@
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 = 2
}
dependencies {
implementation(project(":lib-fembed-extractor"))
implementation(project(":lib-okru-extractor"))
}
apply from: "$rootDir/common.gradle"

View File

@ -1,344 +0,0 @@
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.FILTER_LIST
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()
}
}

View File

@ -1,79 +0,0 @@
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 FILTER_LIST = 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"),
)
}
}