en/slothanime 522

This commit is contained in:
Your Name 2025-02-11 14:34:33 +08:00
parent e8499efe16
commit e7892f6b13
8 changed files with 0 additions and 326 deletions

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,122 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.slothanime
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 }
}
}
open class UriMultiTriSelectOption(name: String, val value: String) : AnimeFilter.TriState(name)
open class UriMultiTriSelectFilter(
name: String,
private val vals: Array<Pair<String, String>>,
) : AnimeFilter.Group<UriMultiTriSelectOption>(name, vals.map { UriMultiTriSelectOption(it.first, it.second) }) {
fun getIncluded(): List<String> {
return state.filter { it.state == TriState.STATE_INCLUDE }.map { it.value }
}
fun getExcluded(): List<String> {
return state.filter { it.state == TriState.STATE_EXCLUDE }.map { it.value }
}
}
class GenreFilter : UriMultiTriSelectFilter(
"Genre",
arrayOf(
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Fantasy", "fantasy"),
Pair("Martial Arts", "martial_arts"),
Pair("Comedy", "comedy"),
Pair("School", "school"),
Pair("Slice of Life", "slice_of_life"),
Pair("Military", "military"),
Pair("Sci-Fi", "scifi"),
Pair("Isekai", "isekai"),
Pair("Kids", "kids"),
Pair("Iyashikei", "iyashikei"),
Pair("Horror", "horror"),
Pair("Supernatural", "supernatural"),
Pair("Avant Garde", "avant_garde"),
Pair("Demons", "demons"),
Pair("Gourmet", "gourmet"),
Pair("Music", "music"),
Pair("Drama", "drama"),
Pair("Seinen", "seinen"),
Pair("Ecchi", "ecchi"),
Pair("Harem", "harem"),
Pair("Romance", "romance"),
Pair("Magic", "magic"),
Pair("Mystery", "mystery"),
Pair("Suspense", "suspense"),
Pair("Parody", "parody"),
Pair("Psychological", "psychological"),
Pair("Super Power", "super_power"),
Pair("Vampire", "vampire"),
Pair("Shounen", "shounen"),
Pair("Space", "space"),
Pair("Mecha", "mecha"),
Pair("Sports", "sports"),
Pair("Shoujo", "shoujo"),
Pair("Girls Love", "girls_love"),
Pair("Josei", "josei"),
Pair("Mahou Shoujo", "mahou_shoujo"),
Pair("Thriller", "thriller"),
Pair("Reverse Harem", "reverse_harem"),
Pair("Boys Love", "boys_love"),
Pair("Uncategorized", "uncategorized"),
),
)
class TypeFilter : UriMultiSelectFilter(
"Type",
arrayOf(
Pair("ONA", "ona"),
Pair("TV", "tv"),
Pair("MOVIE", "movie"),
Pair("SPECIAL", "special"),
Pair("OVA", "ova"),
Pair("MUSIC", "music"),
),
)
class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("All", "2"),
Pair("Completed", "1"),
Pair("Releasing", "0"),
),
)
class SortFilter : UriPartFilter(
"Sort",
arrayOf(
Pair("Most Watched", "viewed"),
Pair("Scored", "scored"),
Pair("Newest", "created_at"),
Pair("Latest Update", "updated_at"),
),
)

View file

@ -1,197 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.slothanime
import android.util.Base64
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.math.floor
class SlothAnime : ParsedAnimeHttpSource() {
override val name = "SlothAnime"
override val baseUrl = "https://slothanime.com"
override val lang = "en"
override val supportsLatest = true
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
val url = if (page > 1) {
"$baseUrl/list/viewed?page=$page"
} else {
"$baseUrl/list/viewed"
}
return GET(url, headers)
}
override fun popularAnimeSelector(): String = ".row > div > .anime-card-md"
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
thumbnail_url = element.selectFirst("img")!!.imgAttr()
with(element.selectFirst("a[href~=/anime]")!!) {
title = text()
setUrlWithoutDomain(attr("abs:href"))
}
}
override fun popularAnimeNextPageSelector(): String = ".pagination > .active ~ li:has(a)"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = if (page > 1) {
"$baseUrl/list/latest?page=$page"
} else {
"$baseUrl/list/latest"
}
return GET(url, headers)
}
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 genreFilter = filters.filterIsInstance<GenreFilter>().first()
val typeFilter = filters.filterIsInstance<TypeFilter>().first()
val statusFilter = filters.filterIsInstance<StatusFilter>().first()
val sortFilter = filters.filterIsInstance<SortFilter>().first()
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("search")
addQueryParameter("q", query)
genreFilter.getIncluded().forEachIndexed { idx, value ->
addQueryParameter("genre[$idx]", value)
}
typeFilter.getValues().forEachIndexed { idx, value ->
addQueryParameter("type[$idx]", value)
}
addQueryParameter("status", statusFilter.getValue())
addQueryParameter("sort", sortFilter.getValue())
genreFilter.getExcluded().forEachIndexed { idx, value ->
addQueryParameter("ignore_genre[$idx]", value)
}
if (page > 1) {
addQueryParameter("page", page.toString())
}
}.build()
return GET(url, headers)
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
GenreFilter(),
TypeFilter(),
StatusFilter(),
SortFilter(),
)
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
title = document.selectFirst(".single-title > *:not(.single-altername)")!!.text()
thumbnail_url = document.selectFirst(".single-cover > img")!!.imgAttr()
description = document.selectFirst(".single-detail:has(span:contains(Description)) .more-content")?.text()
genre = document.select(".single-tag > a.tag").joinToString { it.text() }
author = document.select(".single-detail:has(span:contains(Studios)) .value a").joinToString { it.text() }
}
// ============================== Episodes ==============================
override fun episodeListSelector() = ".list-episodes-container > a[class~=episode]"
override fun episodeFromElement(element: Element): SEpisode = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
name = element.text()
.replace(Regex("""^EP """), "Episode ")
.replace(Regex("""^\d+""")) { m -> "Episode ${m.value}" }
}
// ============================ Video Links =============================
fun encryptAES(input: String, key: ByteArray, iv: ByteArray): String {
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
val secretKey = SecretKeySpec(key, "AES")
val ivParameterSpec = IvParameterSpec(iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
val paddedInput = zeroPad(input)
val encryptedBytes = cipher.doFinal(paddedInput.toByteArray(Charsets.UTF_8))
return Base64.encodeToString(encryptedBytes, Base64.NO_WRAP)
}
fun zeroPad(input: String): String {
val blockSize = 16
val padLength = blockSize - input.length % blockSize
return input.padEnd(input.length + padLength, '\u0000')
}
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val key = String(Base64.decode(KEY, Base64.DEFAULT)).chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val iv = String(Base64.decode(IV, Base64.DEFAULT)).chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val time = floor(System.currentTimeMillis() / 1000.0)
val vrf = encryptAES(time.toString(), key, iv)
val id = episode.url.substringAfterLast("/")
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("player-url")
addPathSegment(id)
addQueryParameter("vrf", vrf)
}.build().toString()
val videoHeaders = headersBuilder().apply {
add("Accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5")
add("Referer", baseUrl + episode.url)
}.build()
return listOf(
Video(url, "Video", url, videoHeaders),
)
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================= Utilities ==============================
private fun Element.imgAttr(): String = when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
else -> attr("abs:src")
}
companion object {
private const val KEY = "YWI0OWZkYjllYzE5M2I0YWQzYWFkMGVmMTU4N2Q2OGE0YmYxY2Y5YjJkMjA4YjRjYzIzMDYwZTkwNThiMjA0NA=="
private const val IV = "NDI4MzEzNjcxMThiMzFmYjVhNTI1MTMzNTc0ZmJmNGI="
}
}