Merge branch 'Kohi-den:main' into main

This commit is contained in:
Arkai1 2025-02-10 17:44:23 +05:30 committed by GitHub
commit 2b4bf1e4ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
414 changed files with 12240 additions and 1128 deletions

View file

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 2
baseVersionCode = 3

View file

@ -117,7 +117,11 @@ abstract class AnimeStream(
}
protected open fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 1
baseVersionCode = 2

View file

@ -155,7 +155,11 @@ abstract class DooPlay(
// =============================== Search ===============================
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.lib.chillxextractor
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
@ -51,6 +52,7 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
val subtitleList = buildList {
val subtitles = REGEX_SUBS.findAll(decryptedScript)
subtitles.forEach {
Log.d("ChillxExtractor", "Found subtitle: ${it.groupValues}")
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
}
}

View file

@ -5,36 +5,50 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
import java.net.URI
class DoodExtractor(private val client: OkHttpClient) {
fun videoFromUrl(
url: String,
quality: String? = null,
prefix: String? = null,
redirect: Boolean = true,
externalSubs: List<Track> = emptyList(),
): Video? {
val newQuality = quality ?: ("Doodstream" + if (redirect) " mirror" else "")
return runCatching {
val response = client.newCall(GET(url)).execute()
val newUrl = if (redirect) response.request.url.toString() else url
val doodHost = Regex("https://(.*?)/").find(newUrl)!!.groupValues[1]
val doodHost = getBaseUrl(newUrl)
val content = response.body.string()
if (!content.contains("'/pass_md5/")) return null
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
// Obtener la calidad del título de la página
val extractedQuality = Regex("\\d{3,4}p")
.find(content.substringAfter("<title>").substringBefore("</title>"))
?.groupValues
?.getOrNull(0)
// Determinar la calidad a usar
val newQuality = extractedQuality ?: ( if (redirect) " mirror" else "")
// Obtener el hash MD5
val md5 = doodHost + (Regex("/pass_md5/[^']*").find(content)?.value ?: return null)
val token = md5.substringAfterLast("/")
val randomString = getRandomString()
val randomString = createHashTable()
val expiry = System.currentTimeMillis()
// Obtener la URL del video
val videoUrlStart = client.newCall(
GET(
"https://$doodHost/pass_md5/$md5",
md5,
Headers.headersOf("referer", newUrl),
),
).execute().body.string()
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
Video(videoUrl, newQuality, videoUrl, headers = doodHeaders(doodHost), subtitleTracks = externalSubs)
val trueUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
Video(trueUrl, prefix + "Doodstream " + newQuality , trueUrl, headers = doodHeaders(doodHost), subtitleTracks = externalSubs)
}.getOrNull()
}
@ -44,16 +58,27 @@ class DoodExtractor(private val client: OkHttpClient) {
redirect: Boolean = true,
): List<Video> {
val video = videoFromUrl(url, quality, redirect)
return video?.let(::listOf) ?: emptyList<Video>()
return video?.let(::listOf) ?: emptyList()
}
private fun getRandomString(length: Int = 10): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
// Método para generar una cadena aleatoria
private fun createHashTable(): String {
val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return buildString {
repeat(10) {
append(alphabet.random())
}
}
}
// Método para obtener la base de la URL
private fun getBaseUrl(url: String): String {
return URI(url).let {
"${it.scheme}://${it.host}"
}
}
// Método para obtener headers personalizados
private fun doodHeaders(host: String) = Headers.Builder().apply {
add("User-Agent", "Aniyomi")
add("Referer", "https://$host/")

View file

@ -0,0 +1,3 @@
plugins {
id("lib-android")
}

View file

@ -0,0 +1,33 @@
package eu.kanade.tachiyomi.lib.goodstramextractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class GoodStreamExtractor(private val client: OkHttpClient, private val headers: Headers) {
fun videosFromUrl(url: String, name: String): List<Video> {
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
val videos = mutableListOf<Video>()
doc.select("script").forEach { script ->
if (script.data().contains(Regex("file|player"))) {
val urlRegex = Regex("file: \"(https:\\/\\/[a-z0-9.\\/-_?=&]+)\",")
urlRegex.find(script.data())?.groupValues?.get(1)?.let { link ->
videos.add(
Video(
url = link,
quality = name,
videoUrl = link,
headers = headers
)
)
}
}
}
return videos
}
}

View file

@ -8,6 +8,7 @@ import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.internal.commonEmptyHeaders
import kotlin.math.abs
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
@ -126,10 +127,16 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
}.toList()
return masterPlaylist.substringAfter(PLAYLIST_SEPARATOR).split(PLAYLIST_SEPARATOR).mapNotNull {
val codec = it.substringAfter("CODECS=\"", "").substringBefore("\"", "")
if (codec.isNotEmpty()) {
if (codec.startsWith("mp4a")) return@mapNotNull null
}
val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n")
.substringAfter("x")
.substringBefore(",") + "p"
.substringBefore(",").let(::stnQuality)
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
@ -328,6 +335,13 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
// ============================= Utilities ==============================
private fun stnQuality(quality: String): String {
val intQuality = quality.toInt()
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
return "${result}p"
}
companion object {
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"

View file

@ -1,25 +1,48 @@
package eu.kanade.tachiyomi.lib.streamhidevidextractor
import android.util.Log
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class StreamHideVidExtractor(private val client: OkHttpClient) {
class StreamHideVidExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client) }
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val page = client.newCall(GET(url)).execute().body.string()
val playlistUrl = (JsUnpacker.unpackAndCombine(page) ?: page)
.substringAfter("sources:")
.substringAfter("file:\"") // StreamHide
.substringAfter("src:\"") // StreamVid
.substringBefore('"')
if (!playlistUrl.startsWith("http")) return emptyList()
return playlistUtils.extractFromHls(playlistUrl,
videoNameGen = { "${prefix}StreamHideVid - $it" }
)
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "StreamHideVid - $quality" }): List<Video> {
val doc = client.newCall(GET(getEmbedUrl(url), headers)).execute().asJsoup()
val scriptBody = doc.selectFirst("script:containsData(m3u8)")?.data()
?.let { script ->
if (script.contains("eval(function(p,a,c")) {
JsUnpacker.unpackAndCombine(script)
} else {
script
}
}
val masterUrl = scriptBody
?.substringAfter("source", "")
?.substringAfter("file:\"", "")
?.substringBefore("\"", "")
?.takeIf(String::isNotBlank)
?: return emptyList()
Log.d("StreamHideVidExtractor", "Playlist URL: $masterUrl")
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen)
}
private fun getEmbedUrl(url: String): String {
return when {
url.contains("/d/") -> url.replace("/d/", "/v/")
url.contains("/download/") -> url.replace("/download/", "/v/")
url.contains("/file/") -> url.replace("/file/", "/v/")
else -> url.replace("/f/", "/v/")
}
}
}

View file

@ -0,0 +1,7 @@
plugins {
id("lib-android")
}
dependencies {
implementation(project(":lib:playlist-utils"))
}

View file

@ -0,0 +1,115 @@
package eu.kanade.tachiyomi.lib.universalextractor
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class UniversalExtractor(private val client: OkHttpClient) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
@SuppressLint("SetJavaScriptEnabled")
fun videosFromUrl(origRequestUrl: String, origRequestHeader: Headers, customQuality: String? = null, prefix: String = ""): List<Video> {
val host = origRequestUrl.toHttpUrl().host.substringBefore(".").proper()
val latch = CountDownLatch(1)
var webView: WebView? = null
var resultUrl = ""
val playlistUtils by lazy { PlaylistUtils(client, origRequestHeader) }
val headers = origRequestHeader.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
handler.post {
val newView = WebView(context)
webView = newView
with(newView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = origRequestHeader["User-Agent"]
}
newView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
val url = request.url.toString()
if (VIDEO_REGEX.containsMatchIn(url)) {
resultUrl = url
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(origRequestUrl, headers)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
// terabox special case start
if ("M3U8_AUTO_360" in resultUrl) {
val qualities = listOf("1080", "720", "480", "360")
val allVideos = mutableListOf<Video>()
for (quality in qualities) {
val modifiedUrl = resultUrl.replace("M3U8_AUTO_360", "M3U8_AUTO_$quality")
val videos = playlistUtils.extractFromHls(modifiedUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it $quality" + "p" })
if (videos.isNotEmpty()) {
allVideos.addAll(videos)
}
}
if (allVideos.isNotEmpty()) {
return allVideos
}
}
// terabox special case end
return when {
"m3u8" in resultUrl -> {
Log.d("UniversalExtractor", "m3u8 URL: $resultUrl")
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it" })
}
"mpd" in resultUrl -> {
Log.d("UniversalExtractor", "mpd URL: $resultUrl")
playlistUtils.extractFromDash(resultUrl, { it -> "$prefix - $host: $it" }, referer = origRequestUrl)
}
"mp4" in resultUrl -> {
Log.d("UniversalExtractor", "mp4 URL: $resultUrl")
Video(resultUrl, "$prefix - $host: ${customQuality ?: "Mirror"}", resultUrl, origRequestHeader.newBuilder().add("referer", origRequestUrl).build()).let(::listOf)
}
else -> emptyList()
}
}
private fun String.proper(): String {
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(
Locale.getDefault()) else it.toString() }
}
companion object {
const val TIMEOUT_SEC: Long = 10
private val VIDEO_REGEX by lazy { Regex(".*\\.(mp4|m3u8|mpd)(\\?.*)?$") }
}
}

View file

@ -17,7 +17,7 @@ class UqloadExtractor(private val client: OkHttpClient) {
?.takeIf { it.startsWith("http") }
?: return emptyList()
val videoHeaders = Headers.headersOf("Referer", "https://uqload.co/")
val videoHeaders = Headers.headersOf("Referer", "https://uqload.ws/")
val quality = if (prefix.isNotBlank()) "$prefix Uqload" else "Uqload"
return listOf(Video(videoUrl, quality, videoUrl, videoHeaders))

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimeWorld India'
extClass = '.AnimeWorldIndiaFactory'
extVersionCode = 12
extVersionCode = 13
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AnimeXin'
themePkg = 'animestream'
baseUrl = 'https://animexin.vip'
overrideVersionCode = 9
overrideVersionCode = 10
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'AniZone'
extClass = '.AniZone'
extVersionCode = 1
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.ChineseAnime'
themePkg = 'animestream'
baseUrl = 'https://www.chineseanime.vip'
overrideVersionCode = 11
overrideVersionCode = 12
}
apply from: "$rootDir/common.gradle"

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.debridindex.DebirdIndexUrlActivity"
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="torrentio.strem.fun"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,8 @@
ext {
extName = 'Debrid Index'
extClass = '.DebridIndex'
extVersionCode = 1
containsNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.animeextension.all.debridindex
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://torrentio.strem.fun/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class DebirdIndexUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${DebridIndex.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)
}
}

View file

@ -0,0 +1,208 @@
package eu.kanade.tachiyomi.animeextension.all.debridindex
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.animeextension.all.debridindex.dto.RootFiles
import eu.kanade.tachiyomi.animeextension.all.debridindex.dto.SubFiles
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.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Locale
class DebridIndex : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Debrid Index"
override val baseUrl = "https://torrentio.strem.fun"
override val lang = "all"
override val supportsLatest = false
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
val tokenKey = preferences.getString(PREF_TOKEN_KEY, null)
val debridProvider = preferences.getString(PREF_DEBRID_KEY, "RealDebrid")
when {
tokenKey.isNullOrBlank() -> throw Exception("Please enter the token in extension settings.")
else -> {
return GET("$baseUrl/${debridProvider!!.lowercase()}=$tokenKey/catalog/other/torrentio-${debridProvider.lowercase()}.json")
}
}
}
override fun popularAnimeParse(response: Response): AnimesPage {
val animeList = json.decodeFromString<RootFiles>(response.body.string()).metas?.map { meta ->
SAnime.create().apply {
title = meta.name
url = meta.id
thumbnail_url = if (meta.name == "Downloads") {
"https://i.ibb.co/MGmhmJg/download.png"
} else {
"https://i.ibb.co/Q9GPtbC/default.png"
}
}
} ?: emptyList()
return AnimesPage(animeList, false)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesParse(response: Response): AnimesPage = throw Exception("Not used")
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val tokenKey = preferences.getString(PREF_TOKEN_KEY, null)
val debridProvider = preferences.getString(PREF_DEBRID_KEY, "RealDebrid")
when {
tokenKey.isNullOrBlank() -> throw Exception("Please enter the token in extension settings.")
else -> {
// Used Debrid Search v0.1.8 https://68d69db7dc40-debrid-search.baby-beamup.club/configure
return GET("https://68d69db7dc40-debrid-search.baby-beamup.club/%7B%22DebridProvider%22%3A%22$debridProvider%22%2C%22DebridApiKey%22%3A%22$tokenKey%22%7D/catalog/other/debridsearch/search=$query.json")
}
}
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
return anime
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val tokenKey = preferences.getString(PREF_TOKEN_KEY, null)
val debridProvider = preferences.getString(PREF_DEBRID_KEY, "RealDebrid")
return GET("$baseUrl/${debridProvider!!.lowercase()}=$tokenKey/meta/other/${anime.url}.json")
}
override fun episodeListParse(response: Response): List<SEpisode> {
val jsonData = response.body.string()
return json.decodeFromString<SubFiles>(jsonData).meta?.videos?.mapIndexed { index, video ->
SEpisode.create().apply {
episode_number = (index + 1).toFloat()
name = if (preferences.getBoolean(IS_FILENAME_KEY, IS_FILENAME_DEFAULT)) {
video.title.trim().split('/').last()
} else {
video.title.trim()
.replace("[", "(")
.replace(Regex("]"), ")")
.replace("/", "\uD83D\uDCC2 ")
}
url = video.streams.firstOrNull()?.url.orEmpty()
date_upload = parseDate(video.released)
}
}?.reversed() ?: emptyList()
}
private fun parseDate(dateStr: String): Long {
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
override suspend fun getVideoList(episode: SEpisode): List<Video> {
return listOf(Video(episode.url, episode.name.split("/").last(), episode.url))
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Debrid provider
ListPreference(screen.context).apply {
key = PREF_DEBRID_KEY
title = "Debird Provider"
entries = PREF_DEBRID_ENTRIES
entryValues = PREF_DEBRID_VALUES
setDefaultValue("realdebrid")
summary = "Don't forget to enter your token key."
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
// Token
EditTextPreference(screen.context).apply {
key = PREF_TOKEN_KEY
title = "Real Debrid Token"
setDefaultValue(PREF_TOKEN_DEFAULT)
summary = PREF_TOKEN_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
runCatching {
val value = (newValue as String).trim().ifBlank { PREF_TOKEN_DEFAULT }
Toast.makeText(screen.context, "Restart app to apply new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = IS_FILENAME_KEY
title = "Only display filename"
setDefaultValue(IS_FILENAME_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
summary = "Will note display full path of episode."
}.also(screen::addPreference)
}
companion object {
const val PREFIX_SEARCH = "id:"
// Token
private const val PREF_TOKEN_KEY = "token"
private const val PREF_TOKEN_DEFAULT = "none"
private const val PREF_TOKEN_SUMMARY = "For temporary uses. Updating the extension will erase this setting."
// Debird
private const val PREF_DEBRID_KEY = "debrid_provider"
private val PREF_DEBRID_ENTRIES = arrayOf(
"RealDebrid",
"Premiumize",
"AllDebrid",
"DebridLink",
)
private val PREF_DEBRID_VALUES = arrayOf(
"RealDebrid",
"Premiumize",
"AllDebrid",
"DebridLink",
)
private const val IS_FILENAME_KEY = "filename"
private const val IS_FILENAME_DEFAULT = false
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
}
}
}

View file

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.animeextension.all.debridindex.dto
import kotlinx.serialization.Serializable
// Root
@Serializable
data class RootFiles(
val metas: List<Meta>? = null,
)
@Serializable
data class SubFiles(
val meta: Meta? = null,
)
@Serializable
data class Meta(
val id: String,
val type: String,
val name: String,
val videos: List<Video>? = null,
)
@Serializable
data class Video(
val id: String,
val title: String,
val released: String,
val streams: List<Stream>,
)
@Serializable
data class Stream(
val url: String,
)

View file

@ -1,7 +1,7 @@
ext {
extName = 'Hikari'
extClass = '.Hikari'
extVersionCode = 14
extVersionCode = 16
}
apply from: "$rootDir/common.gradle"

View file

@ -208,11 +208,17 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override fun episodeListSelector() = "a[class~=ep-item]"
override fun episodeFromElement(element: Element): SEpisode {
val ep = element.selectFirst(".ssli-order")!!.text()
val epText = element.selectFirst(".ssli-order")?.text()?.trim()
?: element.attr("data-number").trim()
val ep = epText.toFloatOrNull() ?: 0F
val nameText = element.selectFirst(".ep-name")?.text()?.trim()
?: element.attr("title").replace("Episode-", "Ep. ") ?: ""
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
episode_number = ep.toFloat()
name = "Ep. $ep - ${element.selectFirst(".ep-name")?.text() ?: ""}"
episode_number = ep
name = "Ep. $ep - $nameText"
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'JavGG'
extClass = '.Javgg'
extVersionCode = 3
extVersionCode = 5
isNsfw = true
}

View file

@ -154,8 +154,7 @@ class Javgg : ConfigurableAnimeSource, AnimeHttpSource() {
.build()
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
}
embedUrl.contains("vidhide") || embedUrl.contains("streamhide") ||
embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideVidExtractor(client).videosFromUrl(url)
embedUrl.contains("vidhide") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideVidExtractor(client, headers).videosFromUrl(url)
embedUrl.contains("voe") -> VoeExtractor(client).videosFromUrl(url)
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
embedUrl.contains("turboplay") -> {

View file

@ -1,7 +1,7 @@
ext {
extName = 'Jav Guru'
extClass = '.JavGuru'
extVersionCode = 19
extVersionCode = 25
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'MissAV'
extClass = '.MissAV'
extVersionCode = 12
extVersionCode = 13
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Sudatchi'
extClass = '.Sudatchi'
extVersionCode = 5
extVersionCode = 11
isNsfw = true
}

View file

@ -59,7 +59,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
private fun Int.parseStatus() = when (this) {
1 -> SAnime.UNKNOWN // Not Yet Released
1 -> SAnime.LICENSED // Not Yet Released
2 -> SAnime.ONGOING
3 -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
@ -86,7 +86,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
val titleLang = preferences.title
val document = response.asJsoup()
val data = document.parseAs<HomePageDto>().animeSpotlight
return AnimesPage(data.map { it.toSAnime(titleLang) }, false)
return AnimesPage(data.map { it.toSAnime(titleLang) }.filterNot { it.status == SAnime.LICENSED }, false)
}
// =============================== Latest ===============================
@ -96,7 +96,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
sudatchiFilters.fetchFilters()
val titleLang = preferences.title
return response.parseAs<DirectoryDto>().let {
AnimesPage(it.animes.map { it.toSAnime(titleLang) }, it.page != it.pages)
AnimesPage(it.animes.map { it.toSAnime(titleLang) }.filterNot { it.status == SAnime.LICENSED }, it.page != it.pages)
}
}
@ -176,7 +176,13 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
videoUrl,
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" },
subtitleList = subtitles.map {
Track("$ipfsUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})")
Track(
when {
it.url.startsWith("/ipfs") -> "$ipfsUrl${it.url}"
else -> "$baseUrl${it.url}"
},
"${it.SubtitlesName.name} (${it.SubtitlesName.language})",
)
}.sort(),
)
}

View file

@ -79,7 +79,7 @@ data class SubtitleLangDto(
data class SubtitleDto(
val url: String,
@SerialName("SubtitlesName")
val subtitlesName: SubtitleLangDto,
val SubtitlesName: SubtitleLangDto,
)
@Serializable

View file

@ -1,7 +1,7 @@
ext {
extName = 'SupJav'
extClass = '.SupJavFactory'
extVersionCode = 12
extVersionCode = 13
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Torrentio (Torrent / Debrid)'
extClass = '.Torrentio'
extVersionCode = 2
extVersionCode = 5
containsNsfw = false
}

View file

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
@ -60,7 +59,12 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
{"query": "${query.replace("\n", "")}", "variables": $variables}
""".trimIndent().toRequestBody("application/json; charset=utf-8".toMediaType())
return POST("https://apis.justwatch.com/graphql", headers = headers, body = requestBody)
val request = Request.Builder()
.url("https://apis.justwatch.com/graphql")
.post(requestBody)
.build()
return request
}
// ============================== JustWatch Api Query ======================
@ -135,7 +139,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
val content = node.content ?: return@mapNotNull null
SAnime.create().apply {
url = "${content.externalIds?.imdbId ?: ""},${node.objectType ?: ""},${content.fullPath ?: ""}"
url = "${content.externalIds?.imdbId ?: ""},${if (node.objectType == "SHOW") "series" else node.objectType ?: ""},${content.fullPath ?: ""}"
title = content.title ?: ""
thumbnail_url = "https://images.justwatch.com${content.posterUrl?.replace("{profile}", "s276")?.replace("{format}", "webp")}"
description = content.shortDescription ?: ""
@ -155,7 +159,31 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
return searchAnimeRequest(page, "", AnimeFilterList())
val country = preferences.getString(PREF_REGION_KEY, PREF_REGION_DEFAULT)
val language = preferences.getString(PREF_JW_LANG_KEY, PREF_JW_LANG_DEFAULT)
val perPage = 40
val packages = ""
val year = 0
val objectTypes = ""
val variables = """
{
"first": $perPage,
"offset": ${(page - 1) * perPage},
"platform": "WEB",
"country": "$country",
"language": "$language",
"searchQuery": "",
"packages": [$packages],
"objectTypes": [$objectTypes],
"popularTitlesSortBy": "TRENDING",
"releaseYear": {
"min": $year,
"max": $year
}
}
""".trimIndent()
return makeGraphQLRequest(justWatchQuery(), variables)
}
override fun popularAnimeParse(response: Response): AnimesPage {
@ -171,7 +199,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/anime/$id", headers))
client.newCall(GET("$baseUrl/anime/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
@ -198,7 +226,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"platform": "WEB",
"country": "$country",
"language": "$language",
"searchQuery": "${query.replace(searchQueryRegex, "").trim()}",
"searchQuery": "${query.replace(Regex("[^A-Za-z0-9 ]"), "").trim()}",
"packages": [$packages],
"objectTypes": [$objectTypes],
"popularTitlesSortBy": "TRENDING",
@ -212,10 +240,6 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
return makeGraphQLRequest(justWatchQuery(), variables)
}
private val searchQueryRegex by lazy {
Regex("[^A-Za-z0-9 ]")
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// =========================== Anime Details ============================
@ -288,18 +312,18 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
val responseString = response.body.string()
val episodeList = json.decodeFromString<EpisodeList>(responseString)
return when (episodeList.meta?.type) {
"show" -> {
"series" -> {
episodeList.meta.videos
?.let { videos ->
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.firstAired?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.released?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
}
?.map { video ->
SEpisode.create().apply {
episode_number = "${video.season}.${video.number}".toFloat()
url = "/stream/series/${video.id}.json"
date_upload = video.firstAired?.let { parseDate(it) } ?: 0L
name = "S${video.season.toString().trim()}:E${video.number} - ${video.name}"
scanlator = (video.firstAired?.let { parseDate(it) } ?: 0L)
date_upload = video.released?.let { parseDate(it) } ?: 0L
name = "S${video.season.toString().trim()}:E${video.number} - ${video.title}"
scanlator = (video.released?.let { parseDate(it) } ?: 0L)
.takeIf { it > System.currentTimeMillis() }
?.let { "Upcoming" }
?: ""
@ -402,7 +426,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
udp://tracker.tiny-vps.com:6969/announce,
udp://tracker.torrent.eu.org:451/announce,
udp://valakas.rollo.dnsabr.com:2710/announce,
udp://www.torrent.eu.org:451/announce
udp://www.torrent.eu.org:451/announce,
${fetchTrackers().split("\n").joinToString(",")}
""".trimIndent()
return streamList.streams?.map { stream ->
@ -428,6 +453,17 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
)
}
private fun fetchTrackers(): String {
val request = Request.Builder()
.url("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Exception("Unexpected code $response")
return response.body.string().trim()
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Debrid provider
ListPreference(screen.context).apply {
@ -652,7 +688,10 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"🇫🇷 Torrent9",
"🇪🇸 MejorTorrent",
"🇲🇽 Cinecalidad",
"🇮🇹 ilCorsaroNero",
"🇪🇸 Wolfmax4k",
)
private val PREF_PROVIDERS_VALUE = arrayOf(
"yts",
"eztv",
@ -673,6 +712,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"torrent9",
"mejortorrent",
"cinecalidad",
"ilcorsaronero",
"wolfmax4k",
)
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf(
@ -691,12 +732,15 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
)
private val PREF_PROVIDERS_DEFAULT = PREF_DEFAULT_PROVIDERS_VALUE.toSet()
// Qualities/Resolutions
// / Qualities/Resolutions
private const val PREF_QUALITY_KEY = "quality_selection"
private val PREF_QUALITY = arrayOf(
"BluRay REMUX",
"HDR/HDR10+/Dolby Vision",
"Dolby Vision",
"Dolby Vision + HDR",
"3D",
"Non 3D (DO NOT SELECT IF NOT SURE)",
"4k",
"1080p",
"720p",
@ -706,10 +750,14 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"Cam",
"Unknown",
)
private val PREF_QUALITY_VALUE = arrayOf(
"brremux",
"hdrall",
"dolbyvision",
"dolbyvisionwithhdr",
"threed",
"nonthreed",
"4k",
"1080p",
"720p",
@ -832,7 +880,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
private val PREF_LANG_DEFAULT = setOf<String>()
private const val UPCOMING_EP_KEY = "upcoming_ep"
private const val UPCOMING_EP_DEFAULT = true
private const val UPCOMING_EP_DEFAULT = false
private const val IS_DUB_KEY = "dubbed"
private const val IS_DUB_DEFAULT = false

View file

@ -110,6 +110,6 @@ class EpisodeVideo(
val id: String? = null,
val season: Int? = null,
val number: Int? = null,
val firstAired: String? = null,
val name: String? = null,
val released: String? = null,
val title: String? = null,
)

View file

@ -1,7 +1,7 @@
ext {
extName = 'Torrentio Anime (Torrent / Debrid)'
extClass = '.Torrentio'
extVersionCode = 11
extVersionCode = 14
containsNsfw = false
}

View file

@ -0,0 +1,155 @@
package eu.kanade.tachiyomi.animeextension.all.torrentioanime
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniListFilters {
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.parseCheckboxList(
options: Array<Pair<String, String>>,
): List<String> {
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkBox -> options.find { it.first == checkBox.name }!!.second }
.filter(String::isNotBlank)
}
private inline fun <reified R> AnimeFilterList.getSort(): String {
val state = (getFirst<R>() as AnimeFilter.Sort).state ?: return ""
val index = state.index
val suffix = if (state.ascending) "" else "_DESC"
return AniListFiltersData.SORT_LIST[index].second + suffix
}
class GenreFilter : CheckBoxFilterList("Genres", AniListFiltersData.GENRE_LIST)
class YearFilter : QueryPartFilter("Year", AniListFiltersData.YEAR_LIST)
class SeasonFilter : QueryPartFilter("Season", AniListFiltersData.SEASON_LIST)
class FormatFilter : CheckBoxFilterList("Format", AniListFiltersData.FORMAT_LIST)
class StatusFilter : QueryPartFilter("Airing Status", AniListFiltersData.STATUS_LIST)
class SortFilter : AnimeFilter.Sort(
"Sort",
AniListFiltersData.SORT_LIST.map { it.first }.toTypedArray(),
Selection(1, false),
)
val FILTER_LIST get() = AnimeFilterList(
SortFilter(),
FormatFilter(),
GenreFilter(),
YearFilter(),
SeasonFilter(),
StatusFilter(),
)
class FilterSearchParams(
val sort: String = "",
val format: List<String> = emptyList(),
val genres: List<String> = emptyList(),
val year: String = "",
val season: String = "",
val status: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.getSort<SortFilter>(),
filters.parseCheckboxList<FormatFilter>(AniListFiltersData.FORMAT_LIST),
filters.parseCheckboxList<GenreFilter>(AniListFiltersData.GENRE_LIST),
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.asQueryPart<StatusFilter>(),
)
}
private object AniListFiltersData {
val GENRE_LIST = arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Comedy", "Comedy"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Horror", "Horror"),
Pair("Mahou Shoujo", "Mahou Shoujo"),
Pair("Mecha", "Mecha"),
Pair("Music", "Music"),
Pair("Mystery", "Mystery"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Slice of Life", "Slice of Life"),
Pair("Sports", "Sports"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
)
val YEAR_LIST: Array<Pair<String, String>> = arrayOf(
Pair("Any", ""),
) + (1940..2025).reversed().map { Pair(it.toString(), it.toString()) }.toTypedArray()
val SEASON_LIST = arrayOf(
Pair("Any", ""),
Pair("Winter", "WINTER"),
Pair("Spring", "SPRING"),
Pair("Summer", "SUMMER"),
Pair("Fall", "FALL"),
)
val FORMAT_LIST = arrayOf(
Pair("Any", ""),
Pair("TV Show", "TV"),
Pair("Movie", "MOVIE"),
Pair("TV Short", "TV_SHORT"),
Pair("Special", "SPECIAL"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Music", "MUSIC"),
)
val STATUS_LIST = arrayOf(
Pair("Any", ""),
Pair("Airing", "RELEASING"),
Pair("Finished", "FINISHED"),
Pair("Not Yet Aired", "NOT_YET_RELEASED"),
Pair("Cancelled", "CANCELLED"),
)
val SORT_LIST = arrayOf(
Pair("Title", "TITLE_ENGLISH"),
Pair("Popularity", "POPULARITY"),
Pair("Average Score", "SCORE"),
Pair("Trending", "TRENDING"),
Pair("Favorites", "FAVOURITES"),
Pair("Date Added", "ID"),
Pair("Release Date", "START_DATE"),
)
}
}

View file

@ -0,0 +1,159 @@
package eu.kanade.tachiyomi.animeextension.all.torrentioanime
private fun String.toQuery() = this.trimIndent().replace("%", "$")
fun anilistQuery() = """
query (
%page: Int,
%perPage: Int,
%sort: [MediaSort],
%search: String,
%genres: [String],
%year: String,
%seasonYear: Int,
%season: MediaSeason,
%format: [MediaFormat],
%status: [MediaStatus],
) {
Page(page: %page, perPage: %perPage) {
pageInfo {
currentPage
hasNextPage
}
media(
type: ANIME,
sort: %sort,
search: %search,
status_in: %status,
genre_in: %genres,
startDate_like: %year,
seasonYear: %seasonYear,
season: %season,
format_in: %format
) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
""".toQuery()
fun anilistLatestQuery() = """
query (%page: Int, %perPage: Int, %sort: [AiringSort]) {
Page(page: %page, perPage: %perPage) {
pageInfo {
currentPage
hasNextPage
}
airingSchedules(
airingAt_greater: 0,
airingAt_lesser: ${System.currentTimeMillis() / 1000 - 10000},
sort: %sort
) {
media {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
}
""".toQuery()
fun getDetailsQuery() = """
query media(%id: Int) {
Media(id: %id) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
medium
}
description
season
seasonYear
format
status
genres
episodes
format
countryOfOrigin
isAdult
tags{
name
}
studios {
nodes {
id
name
}
}
}
}
""".toQuery()
fun getEpisodeQuery() = """
query media(%id: Int, %type: MediaType) {
Media(id: %id, type: %type) {
episodes
nextAiringEpisode {
episode
}
}
}
""".toQuery()
fun getMalIdQuery() = """
query media(%id: Int, %type: MediaType) {
Media(id: %id, type: %type) {
idMal
id
}
}
""".toQuery()

View file

@ -25,15 +25,19 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.json.JSONObject
import org.jsoup.Jsoup
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Locale
@ -62,93 +66,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
.add("query", query)
.add("variables", variables)
.build()
return POST("https://graphql.anilist.co", body = requestBody)
}
// ============================== Anilist Meta List ======================
private fun anilistQuery(): String {
return """
query (${"$"}page: Int, ${"$"}perPage: Int, ${"$"}sort: [MediaSort], ${"$"}search: String) {
Page(page: ${"$"}page, perPage: ${"$"}perPage) {
pageInfo{
currentPage
hasNextPage
}
media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in:[RELEASING,FINISHED,NOT_YET_RELEASED]) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags{
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
""".trimIndent()
}
private fun anilistLatestQuery(): String {
return """
query (${"$"}page: Int, ${"$"}perPage: Int, ${"$"}sort: [AiringSort]) {
Page(page: ${"$"}page, perPage: ${"$"}perPage) {
pageInfo {
currentPage
hasNextPage
}
airingSchedules(
airingAt_greater: 0
airingAt_lesser: ${System.currentTimeMillis() / 1000 - 10000}
sort: ${"$"}sort
) {
media{
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags{
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
}
""".trimIndent()
}
private fun parseSearchJson(jsonLine: String?, isLatestQuery: Boolean = false): AnimesPage {
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
val metaData: Any = if (!isLatestQuery) {
@ -218,7 +138,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
{
"page": $page,
"perPage": 30,
"sort": "TRENDING_DESC"
"sort": "TRENDING_DESC",
"status": ["FINISHED", "RELEASING"]
}
""".trimIndent()
@ -227,8 +148,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
override fun popularAnimeParse(response: Response): AnimesPage {
val jsonData = response.body.string()
return parseSearchJson(jsonData)
}
return parseSearchJson(jsonData) }
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
@ -261,67 +181,73 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
val details = animeDetailsParse(response)
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val variables = """
{
"page": $page,
"perPage": 30,
"sort": "POPULARITY_DESC",
"search": "$query"
val params = AniListFilters.getSearchParameters(filters)
val variablesObject = buildJsonObject {
put("page", page)
put("perPage", 30)
put("sort", params.sort)
if (query.isNotBlank()) put("search", query)
if (params.genres.isNotEmpty()) {
putJsonArray("genres") {
params.genres.forEach { add(it) }
}
""".trimIndent()
}
if (params.format.isNotEmpty()) {
putJsonArray("format") {
params.format.forEach { add(it) }
}
}
if (params.season.isBlank() && params.year.isNotBlank()) {
put("year", "${params.year}%")
}
if (params.season.isNotBlank() && params.year.isBlank()) {
throw Exception("Year cannot be blank if season is set")
}
if (params.season.isNotBlank() && params.year.isNotBlank()) {
put("season", params.season)
put("seasonYear", params.year)
}
if (params.status.isNotBlank()) {
putJsonArray("status") {
params.status.forEach { add(it.toString()) }
}
}
}
val variables = json.encodeToString(variablesObject)
println(anilistQuery())
println(variables)
return makeGraphQLRequest(anilistQuery(), variables)
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AniListFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
val query = """
query(${"$"}id: Int){
Media(id: ${"$"}id){
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags{
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
""".trimIndent()
val variables = """{"id": ${anime.url}}"""
val metaData = runCatching {
json.decodeFromString<DetailsById>(client.newCall(makeGraphQLRequest(query, variables)).execute().body.string())
json.decodeFromString<DetailsById>(client.newCall(makeGraphQLRequest(getDetailsQuery(), variables)).execute().body.string())
}.getOrNull()?.data?.media
anime.title = metaData?.title?.let { title ->
@ -334,10 +260,24 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
} ?: ""
anime.thumbnail_url = metaData?.coverImage?.extraLarge
anime.description = metaData?.description
?.replace(Regex("<br><br>"), "\n")
?.replace(Regex("<.*?>"), "")
?: "No Description"
anime.description = buildString {
append(
metaData?.description?.let {
Jsoup.parseBodyFragment(
it.replace("<br>\n", "br2n")
.replace("<br>", "br2n")
.replace("\n", "br2n"),
).text().replace("br2n", "\n")
},
)
append("\n\n")
if (!(metaData?.season == null && metaData?.seasonYear == null)) {
append("Release: ${ metaData.season ?: ""} ${ metaData.seasonYear ?: ""}")
}
metaData?.format?.let { append("\nType: ${metaData.format}") }
metaData?.episodes?.let { append("\nTotal Episode Count: ${metaData.episodes}") }
}.trim()
anime.status = when (metaData?.status) {
"RELEASING" -> SAnime.ONGOING
@ -360,9 +300,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val res = URL("https://api.ani.zip/mappings?anilist_id=${anime.url}").readText()
val kitsuId = JSONObject(res).getJSONObject("mappings").getInt("kitsu_id").toString()
return GET("https://anime-kitsu.strem.fun/meta/series/kitsu%3A$kitsuId.json")
return GET("https://anime-kitsu.strem.fun/meta/series/anilist%3A${anime.url}.json")
}
override fun episodeListParse(response: Response): List<SEpisode> {
@ -375,7 +313,6 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
?.let { videos ->
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.released?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
}
?.filter { it.thumbnail != null }
?.map { video ->
SEpisode.create().apply {
episode_number = video.episode?.toFloat() ?: 0.0F
@ -481,9 +418,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
udp://tracker.tiny-vps.com:6969/announce,
udp://tracker.torrent.eu.org:451/announce,
udp://valakas.rollo.dnsabr.com:2710/announce,
udp://www.torrent.eu.org:451/announce
udp://www.torrent.eu.org:451/announce,
${fetchTrackers().split("\n").joinToString(",")}
""".trimIndent()
return streamList.streams?.map { stream ->
val urlOrHash =
if (debridProvider == "none") {
@ -509,6 +446,17 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
)
}
private fun fetchTrackers(): String {
val request = Request.Builder()
.url("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Exception("Unexpected code $response")
return response.body.string().trim()
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Debrid provider
ListPreference(screen.context).apply {
@ -714,7 +662,10 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"🇫🇷 Torrent9",
"🇪🇸 MejorTorrent",
"🇲🇽 Cinecalidad",
"🇮🇹 ilCorsaroNero",
"🇪🇸 Wolfmax4k",
)
private val PREF_PROVIDERS_VALUE = arrayOf(
"yts",
"eztv",
@ -735,6 +686,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"torrent9",
"mejortorrent",
"cinecalidad",
"ilcorsaronero",
"wolfmax4k",
)
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf(
@ -759,6 +712,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"BluRay REMUX",
"HDR/HDR10+/Dolby Vision",
"Dolby Vision",
"Dolby Vision + HDR",
"3D",
"Non 3D (DO NOT SELECT IF NOT SURE)",
"4k",
"1080p",
"720p",
@ -768,10 +724,14 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"Cam",
"Unknown",
)
private val PREF_QUALITY_VALUE = arrayOf(
"brremux",
"hdrall",
"dolbyvision",
"dolbyvisionwithhdr",
"threed",
"nonthreed",
"4k",
"1080p",
"720p",

View file

@ -73,7 +73,11 @@ data class AnilistMedia(
val status: String? = null,
val tags: List<AnilistTag>? = null,
val genres: List<String>? = null,
val episodes: Int? = null,
val format: String? = null,
val studios: AnilistStudios? = null,
val season: String? = null,
val seasonYear: Int? = null,
val countryOfOrigin: String? = null,
val isAdult: Boolean = false,
)

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime4up'
extClass = '.Anime4Up'
extVersionCode = 61
extVersionCode = 62
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimeLek'
extClass = '.AnimeLek'
extVersionCode = 30
extVersionCode = 31
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Animerco'
extClass = '.Animerco'
extVersionCode = 40
extVersionCode = 41
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Arab Seed'
extClass = '.ArabSeed'
extVersionCode = 16
extVersionCode = 17
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'asia2tv'
extClass = '.Asia2TV'
extVersionCode = 21
extVersionCode = 22
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Cimaleek'
extClass = '.Cimaleek'
extVersionCode = 1
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Egy Dead'
extClass = '.EgyDead'
extVersionCode = 16
extVersionCode = 17
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'FASELHD'
extClass = '.FASELHD'
extVersionCode = 15
extVersionCode = 16
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'MY CIMA'
extClass = '.MyCima'
extVersionCode = 22
extVersionCode = 23
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Okanime'
extClass = '.Okanime'
extVersionCode = 12
extVersionCode = 13
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Tuktuk Cinema'
extClass = '.Tuktukcinema'
extVersionCode = 22
extVersionCode = 23
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'WIT ANIME'
extClass = '.WitAnime'
extVersionCode = 50
extVersionCode = 51
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime-Base'
extClass = '.AnimeBase'
extVersionCode = 29
extVersionCode = 30
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime-Loads'
extClass = '.AnimeLoads'
extVersionCode = 16
extVersionCode = 17
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimeToast'
extClass = '.AnimeToast'
extVersionCode = 16
extVersionCode = 21
}
apply from: "$rootDir/common.gradle"

View file

@ -166,7 +166,6 @@ class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
DoodExtractor(client).videoFromUrl(
link,
quality,
false,
)
if (video != null) {
videoList.add(video)
@ -224,7 +223,7 @@ class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
) == true -> {
val quality = "DoodStream"
val video =
DoodExtractor(client).videoFromUrl(link, quality, false)
DoodExtractor(client).videoFromUrl(link, quality)
if (video != null) {
videoList.add(video)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'AniWorld'
extClass = '.AniWorld'
extVersionCode = 24
extVersionCode = 25
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'CineClix'
extClass = '.CineClix'
extVersionCode = 16
extVersionCode = 17
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Cinemathek'
themePkg = 'dooplay'
baseUrl = 'https://cinemathek.net'
overrideVersionCode = 23
overrideVersionCode = 24
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Einfach'
extClass = '.Einfach'
extVersionCode = 14
extVersionCode = 15
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Kinoking'
themePkg = 'dooplay'
baseUrl = 'https://kinoking.cc'
overrideVersionCode = 22
overrideVersionCode = 23
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Moflix-Stream'
extClass = '.MoflixStream'
extVersionCode = 13
extVersionCode = 14
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Movie2k'
extClass = '.Movie2k'
extVersionCode = 7
extVersionCode = 8
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Serienstream'
extClass = '.Serienstream'
extVersionCode = 20
extVersionCode = 24
}
apply from: "$rootDir/common.gradle"

View file

@ -37,7 +37,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Serienstream"
override val baseUrl = "https://s.to"
override val baseUrl = "http://186.2.175.5"
override val lang = "de"
@ -91,7 +91,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val headers = Headers.Builder()
.add("Referer", "https://s.to/search")
.add("Referer", "http://186.2.175.5/search")
.add("origin", baseUrl)
.add("connection", "keep-alive")
.add("user-agent", "Mozilla/5.0 (Linux; Android 12; Pixel 5 Build/SP2A.220405.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Safari/537.36")

View file

@ -1,7 +1,7 @@
ext {
extName = 'StreamCloud'
extClass = '.StreamCloud'
extVersionCode = 9
extVersionCode = 10
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'AllAnime'
extClass = '.AllAnime'
extVersionCode = 34
extVersionCode = 35
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'AllAnimeChi'
extClass = '.AllAnimeChi'
extVersionCode = 10
extVersionCode = 11
}
apply from: "$rootDir/common.gradle"

View file

@ -0,0 +1,7 @@
ext {
extName = 'AnimeFlix'
extClass = '.AnimeFlix'
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,398 @@
package eu.kanade.tachiyomi.animeextension.en.animeflix
import android.app.Application
import android.util.Base64
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.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.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MultipartBody
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeFlix"
override val baseUrl = "https://animeflix.mobi"
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page/")
override fun popularAnimeSelector() = "div#content_box > div.post-cards > article"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
// prevent base64 images
thumbnail_url = element.selectFirst("img")!!.run {
attr("data-pagespeed-high-res-src").ifEmpty { attr("src") }
}
title = element.selectFirst("header")!!.text()
}
override fun popularAnimeNextPageSelector() = "div.nav-links > a.next"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val cleanQuery = query.replace(" ", "+").lowercase()
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
val subpageFilter = filterList.find { it is SubPageFilter } as SubPageFilter
return when {
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers)
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers)
subpageFilter.state != 0 -> GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers)
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter(),
SubPageFilter(),
)
private class GenreFilter : UriPartFilter(
"Genres",
arrayOf(
Pair("<select>", ""),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Isekai", "isekai"),
Pair("Drama", "drama"),
Pair("Psychological", "psychological"),
Pair("Ecchi", "ecchi"),
Pair("Sci-Fi", "sci-fi"),
Pair("Magic", "magic"),
Pair("Slice Of Life", "slice-of-life"),
Pair("Sports", "sports"),
Pair("Comedy", "comedy"),
Pair("Fantasy", "fantasy"),
Pair("Horror", "horror"),
Pair("Yaoi", "yaoi"),
),
)
private class SubPageFilter : UriPartFilter(
"Sub-page",
arrayOf(
Pair("<select>", ""),
Pair("Ongoing", "ongoing"),
Pair("Latest Release", "latest-release"),
Pair("Movies", "movies"),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("div.single_post > header > h1")!!.text()
thumbnail_url = document.selectFirst("img.imdbwp__img")?.attr("src")
val infosDiv = document.selectFirst("div.thecontent h3:contains(Anime Info) ~ ul")!!
status = when (infosDiv.getInfo("Status").toString()) {
"Completed" -> SAnime.COMPLETED
"Currently Airing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
artist = infosDiv.getInfo("Studios")
author = infosDiv.getInfo("Producers")
genre = infosDiv.getInfo("Genres")
val animeInfo = infosDiv.select("li").joinToString("\n") { it.text() }
description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)")
.joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
}
private fun Element.getInfo(info: String) =
selectFirst("li:contains($info)")?.ownText()?.trim()
// ============================== Episodes ==============================
val seasonRegex by lazy { Regex("""season (\d+)""", RegexOption.IGNORE_CASE) }
val qualityRegex by lazy { """(\d+)p""".toRegex() }
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
val document = client.newCall(GET(baseUrl + anime.url)).execute()
.asJsoup()
val seasonList = document.select("div.inline > h3:contains(Season),div.thecontent > h3:contains(Season)")
val episodeList = if (seasonList.distinctBy { seasonRegex.find(it.text())!!.groupValues[1] }.size > 1) {
val seasonsLinks = document.select("div.thecontent p:has(span:contains(Gdrive))").groupBy {
seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
}
seasonsLinks.flatMap { (seasonNumber, season) ->
val serverListSeason = season.map {
val previousText = it.previousElementSibling()!!.text()
val quality = qualityRegex.find(previousText)?.groupValues?.get(1) ?: "Unknown quality"
val url = it.selectFirst("a")!!.attr("href")
val episodesDocument = client.newCall(GET(url)).execute()
.asJsoup()
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(quality, it.attr("href"), "Season $seasonNumber ${it.text()}")
}
}
transposeEpisodes(serverListSeason)
}
} else {
val driveList = document.select("div.thecontent p:has(span:contains(Gdrive))").map {
val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
Pair(it.selectFirst("a")!!.attr("href"), quality)
}
// Load episodes
val serversList = driveList.map { drive ->
val episodesDocument = client.newCall(GET(drive.first)).execute()
.asJsoup()
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(drive.second, it.attr("href"), it.text())
}
}
transposeEpisodes(serversList)
}
return episodeList.reversed()
}
private fun transposeEpisodes(serversList: List<List<EpUrl>>) =
transpose(serversList).mapIndexed { index, serverList ->
SEpisode.create().apply {
name = serverList.first().name
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(json.encodeToString(serverList))
}
}
override fun episodeListSelector(): String = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val urls = json.decodeFromString<List<EpUrl>>(episode.url)
val leechUrls = urls.map {
val firstLeech = client.newCall(GET(it.url)).execute()
.asJsoup()
.selectFirst("script:containsData(downlaod_button)")!!
.data()
.substringAfter("<a href=\"")
.substringBefore("\">")
val path = client.newCall(GET(firstLeech)).execute()
.body.string()
.substringAfter("replace(\"")
.substringBefore("\"")
val link = "https://" + firstLeech.toHttpUrl().host + path
EpUrl(it.quality, link, it.name)
}
val videoList = leechUrls.parallelCatchingFlatMap { url ->
if (url.url.toHttpUrl().encodedPath == "/404") return@parallelCatchingFlatMap emptyList()
val (videos, mediaUrl) = extractVideo(url)
when {
videos.isEmpty() -> {
extractGDriveLink(mediaUrl, url.quality).ifEmpty {
getDirectLink(mediaUrl, "instant", "/mfile/")?.let {
listOf(Video(it, "${url.quality}p - GDrive Instant link", it))
} ?: emptyList()
}
}
else -> videos
}
}
return videoList.sort()
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ==============================
// https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
val matchResult = qualityRegex.find(epUrl.name)
val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
return (1..3).toList().flatMap { type ->
extractWorkerLinks(epUrl.url, quality, type)
}.let { Pair(it, epUrl.url) }
}
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
val resp = client.newCall(GET(reqLink)).execute().asJsoup()
val sizeMatch = SIZE_REGEX.find(resp.select("div.card-header").text().trim())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
val link = linkElement.attr("href")
val decodedLink = if (link.contains("workers.dev")) {
link
} else {
String(Base64.decode(link.substringAfter("download?url="), Base64.DEFAULT))
}
Video(
url = decodedLink,
quality = "${quality}p - CF $type Worker ${index + 1}$size",
videoUrl = decodedLink,
)
}
}
private fun getDirectLink(url: String, action: String = "direct", newPath: String = "/file/"): String? {
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
val script = doc.selectFirst("script:containsData(async function taskaction)")
?.data()
?: return url
val key = script.substringAfter("key\", \"").substringBefore('"')
val form = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("action", action)
.addFormDataPart("key", key)
.addFormDataPart("action_token", "")
.build()
val headers = headersBuilder().set("x-token", url.toHttpUrl().host).build()
val req = client.newCall(POST(url.replace("/file/", newPath), headers, form)).execute()
return runCatching {
json.decodeFromString<DriveLeechDirect>(req.body.string()).url
}.getOrNull()
}
private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> {
val neoUrl = getDirectLink(mediaUrl) ?: mediaUrl
val response = client.newCall(GET(neoUrl)).execute().asJsoup()
val gdBtn = response.selectFirst("div.card-body a.btn")!!
val gdLink = gdBtn.attr("href")
val sizeMatch = SIZE_REGEX.find(gdBtn.text())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup()
val link = gdResponse.select("form#download-form")
return if (link.isNullOrEmpty()) {
emptyList()
} else {
val realLink = link.attr("action")
listOf(Video(realLink, "$quality - Gdrive$size", realLink))
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun <E> transpose(xs: List<List<E>>): List<List<E>> {
// Helpers
fun <E> List<E>.head(): E = this.first()
fun <E> List<E>.tail(): List<E> = this.takeLast(this.size - 1)
fun <E> E.append(xs: List<E>): List<E> = listOf(this).plus(xs)
xs.filter { it.isNotEmpty() }.let { ys ->
return when (ys.isNotEmpty()) {
true -> ys.map { it.head() }.append(transpose(ys.map { it.tail() }))
else -> emptyList()
}
}
}
@Serializable
data class EpUrl(
val quality: String,
val url: String,
val name: String,
)
@Serializable
data class DriveLeechDirect(val url: String? = null)
companion object {
private val SIZE_REGEX = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
private const val PREF_QUALITY_KEY = "pref_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
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()
}
}.also(screen::addPreference)
}
}

View file

@ -0,0 +1,12 @@
ext {
extName = 'Animeflix.live'
extClass = '.AnimeflixLive'
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:gogostream-extractor'))
implementation(project(':lib:playlist-utils'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,503 @@
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
import GenreFilter
import SortFilter
import SubPageFilter
import TypeFilter
import android.app.Application
import android.content.SharedPreferences
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.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URLDecoder
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import kotlin.math.min
class AnimeflixLive : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Animeflix.live"
override val baseUrl by lazy { preferences.baseUrl }
private val apiUrl by lazy { preferences.apiUrl }
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val apiHeaders = headersBuilder().apply {
add("Accept", "*/*")
add("Host", apiUrl.toHttpUrl().host)
add("Origin", baseUrl)
add("Referer", "$baseUrl/")
}.build()
private val docHeaders = headersBuilder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
add("Host", apiUrl.toHttpUrl().host)
add("Referer", "$baseUrl/")
}.build()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request =
GET("$apiUrl/popular?page=${page - 1}", apiHeaders)
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<List<AnimeDto>>()
val titlePref = preferences.titleType
val animeList = parsed.map {
it.toSAnime(titlePref)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$apiUrl/trending?page=${page - 1}", apiHeaders)
override fun latestUpdatesParse(response: Response): AnimesPage {
val parsed = response.parseAs<TrendingDto>()
val titlePref = preferences.titleType
val animeList = parsed.trending.map {
it.toSAnime(titlePref)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val sort = filters.filterIsInstance<SortFilter>().first().getValue()
val type = filters.filterIsInstance<TypeFilter>().first().getValues()
val genre = filters.filterIsInstance<GenreFilter>().first().getValues()
val subPage = filters.filterIsInstance<SubPageFilter>().first().getValue()
if (subPage.isNotBlank()) {
return GET("$apiUrl/$subPage?page=${page - 1}", apiHeaders)
}
if (query.isEmpty()) {
throw Exception("Search must not be empty")
}
val filtersObj = buildJsonObject {
put("sort", sort)
if (type.isNotEmpty()) {
put("type", json.encodeToString(type))
}
if (genre.isNotEmpty()) {
put("genre", json.encodeToString(genre))
}
}.toJsonString()
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("info")
addPathSegment("")
addQueryParameter("query", query)
addQueryParameter("limit", "15")
addQueryParameter("filters", filtersObj)
addQueryParameter("k", query.substr(0, 3).sk())
}.build()
return GET(url, apiHeaders)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<List<AnimeDto>>()
val titlePref = preferences.titleType
val animeList = parsed.map {
it.toSAnime(titlePref)
}
val hasNextPage = if (response.request.url.queryParameter("limit") == null) {
animeList.size == 44
} else {
animeList.size == 15
}
return AnimesPage(animeList, hasNextPage)
}
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
SortFilter(),
TypeFilter(),
GenreFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("NOTE: Subpage overrides search and other filters!"),
SubPageFilter(),
)
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request {
return GET("$apiUrl/getslug/${anime.url}", apiHeaders)
}
override fun getAnimeUrl(anime: SAnime): String {
return "$baseUrl/search/${anime.title}?anime=${anime.url}"
}
override fun animeDetailsParse(response: Response): SAnime {
val titlePref = preferences.titleType
return response.parseAs<DetailsDto>().toSAnime(titlePref)
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val lang = preferences.lang
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("episodes")
addQueryParameter("id", anime.url)
addQueryParameter("dub", (lang == "Dub").toString())
addQueryParameter("c", anime.url.sk())
}.build()
return GET(url, apiHeaders)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val slug = response.request.url.queryParameter("id")!!
return response.parseAs<EpisodeResponseDto>().episodes.map {
it.toSEpisode(slug)
}.sortedByDescending { it.episode_number }
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val url = "$apiUrl${episode.url}".toHttpUrl().newBuilder().apply {
addQueryParameter("server", "")
addQueryParameter("c", episode.url.substringAfter("/watch/").sk())
}.build()
return GET(url, apiHeaders)
}
override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>()
val initialPlayerUrl = apiUrl + response.parseAs<ServerDto>().source
val initialServer = initialPlayerUrl.toHttpUrl().queryParameter("server")!!
val initialPlayerDocument = client.newCall(
GET(initialPlayerUrl, docHeaders),
).execute().asJsoup().unescape()
videoList.addAll(
videosFromPlayer(
initialPlayerDocument,
initialServer.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
),
)
// Go through rest of servers
val servers = initialPlayerDocument.selectFirst("script:containsData(server-settings)")!!.data()
val serversHtml = SERVER_REGEX.findAll(servers).map {
Jsoup.parseBodyFragment(it.groupValues[1])
}.toList()
videoList.addAll(
serversHtml.parallelCatchingFlatMapBlocking {
val server = serverMapping[
it.selectFirst("button")!!
.attr("onclick")
.substringAfter("postMessage('")
.substringBefore("'"),
]
if (server == initialServer) {
return@parallelCatchingFlatMapBlocking emptyList()
}
val serverUrl = response.request.url.newBuilder()
.setQueryParameter("server", server)
.build()
val playerUrl = apiUrl + client.newCall(
GET(serverUrl, apiHeaders),
).execute().parseAs<ServerDto>().source
if (server != playerUrl.toHttpUrl().queryParameter("server")!!) {
return@parallelCatchingFlatMapBlocking emptyList()
}
val playerDocument = client.newCall(
GET(playerUrl, docHeaders),
).execute().asJsoup().unescape()
videosFromPlayer(
playerDocument,
server.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
)
},
)
return videoList
}
private val serverMapping = mapOf(
"settings-0" to "moon",
"settings-1" to "sun",
"settings-2" to "zoro",
"settings-3" to "gogo",
)
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private fun getVideoHeaders(baseHeaders: Headers, referer: String, videoUrl: String): Headers {
return baseHeaders.newBuilder().apply {
add("Accept", "*/*")
add("Accept-Language", "en-US,en;q=0.5")
add("Host", videoUrl.toHttpUrl().host)
add("Origin", "https://${apiUrl.toHttpUrl().host}")
add("Referer", "$apiUrl/")
add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "cross-site")
}.build()
}
private fun Document.unescape(): Document {
val unescapeScript = this.selectFirst("script:containsData(unescape)")
return if (unescapeScript == null) {
this
} else {
val data = URLDecoder.decode(unescapeScript.data(), "UTF-8")
Jsoup.parse(data, this.location())
}
}
private fun videosFromPlayer(document: Document, name: String): List<Video> {
val dataScript = document.selectFirst("script:containsData(m3u8)")
?.data() ?: return emptyList()
val subtitleList = document.select("video > track[kind=captions]").map {
Track(it.attr("id"), it.attr("label"))
}
var masterPlaylist = M3U8_REGEX.find(dataScript)?.groupValues?.get(1)
?: return emptyList()
if (name.equals("moon", true)) {
masterPlaylist += dataScript.substringAfter("`${'$'}{url}")
.substringBefore("`")
}
return playlistUtils.extractFromHls(
masterPlaylist,
videoHeadersGen = ::getVideoHeaders,
videoNameGen = { q -> "$name - $q" },
subtitleList = subtitleList,
)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.quality
val server = preferences.server
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(server, true) },
),
).reversed()
}
private fun JsonObject.toJsonString(): String {
return json.encodeToString(this)
}
private fun String.sk(): String {
val t = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val n = 17 + (t.get(Calendar.DAY_OF_MONTH) - t.get(Calendar.MONTH)) / 2
return this.toCharArray().fold("") { acc, c ->
acc + c.code.toString(n).padStart(2, '0')
}
}
private fun String.substr(start: Int, end: Int): String {
val stop = min(end, this.length)
return this.substring(start, stop)
}
companion object {
private val SERVER_REGEX = Regex("""'1' === '1'.*?(<button.*?</button>)""", RegexOption.DOT_MATCHES_ALL)
private val M3U8_REGEX = Regex("""const ?\w*? ?= ?`(.*?)`""")
private const val PAGE_SIZE = 24
private const val PREF_DOMAIN_KEY = "pref_domain_key"
private const val PREF_DOMAIN_DEFAULT = "https://animeflix.live,https://api.animeflix.dev"
private val PREF_DOMAIN_ENTRIES = arrayOf("animeflix.live", "animeflix.ro")
private val PREF_DOMAIN_ENTRY_VALUES = arrayOf(
"https://animeflix.live,https://api.animeflix.dev",
"https://animeflix.ro,https://api.animeflixtv.to",
)
private const val PREF_TITLE_KEY = "pref_title_type_key"
private const val PREF_TITLE_DEFAULT = "English"
private val PREF_TITLE_ENTRIES = arrayOf("English", "Native", "Romaji")
private const val PREF_LANG_KEY = "pref_lang_key"
private const val PREF_LANG_DEFAULT = "Sub"
private val PREF_LANG_ENTRIES = arrayOf("Sub", "Dub")
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360")
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_ENTRY_VALUES.map { "${it}p" }.toTypedArray()
private const val PREF_SERVER_KEY = "pref_server_key"
private const val PREF_SERVER_DEFAULT = "Moon"
private val PREF_SERVER_ENTRIES = arrayOf("Moon", "Sun", "Zoro", "Gogo")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = "Preferred domain (requires app restart)"
entries = PREF_DOMAIN_ENTRIES
entryValues = PREF_DOMAIN_ENTRY_VALUES
setDefaultValue(PREF_DOMAIN_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()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_TITLE_KEY
title = "Preferred Title Type"
entries = PREF_TITLE_ENTRIES
entryValues = PREF_TITLE_ENTRIES
setDefaultValue(PREF_TITLE_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()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = "Preferred Language"
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()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRY_VALUES
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()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = PREF_SERVER_ENTRIES
entryValues = PREF_SERVER_ENTRIES
setDefaultValue(PREF_SERVER_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()
}
}.also(screen::addPreference)
}
private val SharedPreferences.baseUrl
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
.split(",").first()
private val SharedPreferences.apiUrl
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
.split(",").last()
private val SharedPreferences.titleType
get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!
private val SharedPreferences.lang
get() = getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
private val SharedPreferences.quality
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.server
get() = getString(PREF_SERVER_KEY, PREF_QUALITY_DEFAULT)!!
}

View file

@ -0,0 +1,123 @@
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import kotlin.math.ceil
import kotlin.math.floor
@Serializable
class TrendingDto(
val trending: List<AnimeDto>,
)
@Serializable
class AnimeDto(
val slug: String,
@SerialName("title") val titleObj: TitleObject,
val images: ImageObject,
) {
@Serializable
class TitleObject(
val english: String? = null,
val native: String? = null,
val romaji: String? = null,
)
@Serializable
class ImageObject(
val large: String? = null,
val medium: String? = null,
val small: String? = null,
)
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
}
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
url = slug
}
}
@Serializable
class DetailsDto(
val slug: String,
@SerialName("title") val titleObj: TitleObject,
val description: String,
val genres: List<String>,
val status: String? = null,
val images: ImageObject,
) {
@Serializable
class TitleObject(
val english: String? = null,
val native: String? = null,
val romaji: String? = null,
)
@Serializable
class ImageObject(
val large: String? = null,
val medium: String? = null,
val small: String? = null,
)
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
}
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
url = slug
genre = genres.joinToString()
status = this@DetailsDto.status.parseStatus()
description = Jsoup.parseBodyFragment(
this@DetailsDto.description.replace("<br>", "br2n"),
).text().replace("br2n", "\n")
}
private fun String?.parseStatus(): Int = when (this?.lowercase()) {
"releasing" -> SAnime.ONGOING
"finished" -> SAnime.COMPLETED
"cancelled" -> SAnime.CANCELLED
else -> SAnime.UNKNOWN
}
}
@Serializable
class EpisodeResponseDto(
val episodes: List<EpisodeDto>,
) {
@Serializable
class EpisodeDto(
val number: Float,
val title: String? = null,
) {
fun toSEpisode(slug: String): SEpisode = SEpisode.create().apply {
val epNum = if (floor(number) == ceil(number)) {
number.toInt().toString()
} else {
number.toString()
}
url = "/watch/$slug-episode-$epNum"
episode_number = number
name = if (title == null) {
"Episode $epNum"
} else {
"Ep. $epNum - $title"
}
}
}
}
@Serializable
class ServerDto(
val source: String,
)

View file

@ -0,0 +1,81 @@
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
open class UriPartFilter(
name: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : AnimeFilter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
fun getValue(): String {
return vals[state].second
}
}
open class UriMultiSelectOption(name: String, val value: String) : AnimeFilter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val vals: Array<Pair<String, String>>,
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }) {
fun getValues(): List<String> {
return state.filter { it.state }.map { it.value }
}
}
class SortFilter : UriPartFilter(
"Sort",
arrayOf(
Pair("Recently Updated", "recently_updated"),
Pair("Recently Added", "recently_added"),
Pair("Release Date ↓", "release_date_down"),
Pair("Release Date ↑", "release_date_up"),
Pair("Name A-Z", "title_az"),
Pair("Best Rating", "scores"),
Pair("Most Watched", "most_watched"),
Pair("Anime Length", "number_of_episodes"),
),
)
class TypeFilter : UriMultiSelectFilter(
"Type",
arrayOf(
Pair("TV", "TV"),
Pair("Movie", "MOVIE"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Special", "SPECIAL"),
),
)
class GenreFilter : UriMultiSelectFilter(
"Genre",
arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Comedy", "Comedy"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Horror", "Horror"),
Pair("Mecha", "Mecha"),
Pair("Mystery", "Mystery"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Sports", "Sports"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
),
)
class SubPageFilter : UriPartFilter(
"Sub-page",
arrayOf(
Pair("<select>", ""),
Pair("Movies", "movies"),
Pair("Series", "series"),
),
)

View file

@ -1,7 +1,7 @@
ext {
extName = 'Animension'
extClass = '.Animension'
extVersionCode = 20
extVersionCode = 21
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimeOwl'
extClass = '.AnimeOwl'
extVersionCode = 19
extVersionCode = 20
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimeTake'
extClass = '.AnimeTake'
extVersionCode = 6
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

@ -2,7 +2,7 @@ ext {
extName = 'AniPlay'
extClass = '.AniPlay'
themePkg = 'anilist'
overrideVersionCode = 10
overrideVersionCode = 11
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'AsiaFlix'
extClass = '.AsiaFlix'
extVersionCode = 14
extVersionCode = 15
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.DonghuaStream'
themePkg = 'animestream'
baseUrl = 'https://donghuastream.org'
overrideVersionCode = 7
overrideVersionCode = 8
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.DopeBox'
themePkg = 'dopeflix'
baseUrl = 'https://dopebox.to'
overrideVersionCode = 9
overrideVersionCode = 10
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'DramaCool'
extClass = '.DramaCool'
extVersionCode = 53
extVersionCode = 54
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'GenoAnime'
extClass = '.GenoAnime'
extVersionCode = 33
extVersionCode = 34
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Gogoanime'
extClass = '.GogoAnime'
extVersionCode = 86
extVersionCode = 87
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'KickAssAnime'
extClass = '.KickAssAnime'
extVersionCode = 43
extVersionCode = 44
}
apply from: "$rootDir/common.gradle"

Some files were not shown because too many files have changed in this diff Show more