Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1,456 @@
package eu.kanade.tachiyomi.animeextension.it.animesaturn
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.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.util.asJsoup
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class AnimeSaturn : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeSaturn"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://animesaturn.in")!! }
override val lang = "it"
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeSelector(): String = "div.sebox"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animeincorso?page=$page")
private fun formatTitle(titlestring: String): String = titlestring.replace("(ITA) ITA", "Dub ITA").replace("(ITA)", "Dub ITA").replace("Sub ITA", "")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("div.msebox div.headsebox div.tisebox h2 a")!!.attr("href"))
anime.title = formatTitle(element.selectFirst("div.msebox div.headsebox div.tisebox h2 a")!!.text())
anime.thumbnail_url = element.selectFirst("div.msebox div.bigsebox div.l img.attachment-post-thumbnail.size-post-thumbnail.wp-post-image")!!.attr("src")
return anime
}
override fun popularAnimeNextPageSelector(): String = "li.page-item.active:not(li:last-child)"
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return document.select(episodeListSelector()).map { episodeFromElement(it) }.reversed()
}
override fun episodeListSelector() = "div.btn-group.episodes-button.episodi-link-button"
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(element.selectFirst("a.btn.btn-dark.mb-1.bottone-ep")!!.attr("href"))
val epText = element.selectFirst("a.btn.btn-dark.mb-1.bottone-ep")!!.text()
val epNumber = epText.substringAfter("Episodio ")
if (epNumber.contains("-", true)) {
episode.episode_number = epNumber.substringBefore("-").toFloat()
} else {
episode.episode_number = epNumber.toFloat()
}
episode.name = epText
return episode
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val standardVideos = videosFromElement(document)
val videoList = mutableListOf<Video>()
videoList.addAll(standardVideos)
return videoList
}
override fun videoListRequest(episode: SEpisode): Request {
val episodePage = client.newCall(GET(baseUrl + episode.url)).execute().asJsoup()
val watchUrl = episodePage.select("a[href*=/watch]").attr("href")
return GET("$watchUrl&s=alt")
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
private fun videosFromElement(document: Document): List<Video> {
val url = if (document.html().contains("jwplayer(")) {
document.html().substringAfter("file: \"").substringBefore("\"")
} else {
document.select("source").attr("src")
}
val referer = document.location()
return if (url.endsWith("playlist.m3u8")) {
val playlist = client.newCall(GET(url)).execute().body.string()
val linkRegex = """(?<=\n)./.+""".toRegex()
val qualityRegex = """(?<=RESOLUTION=)\d+x\d+""".toRegex()
val qualities = qualityRegex.findAll(playlist).map {
it.value.substringAfter('x') + "p"
}.toList()
val videoLinks = linkRegex.findAll(playlist).map {
url.substringBefore("playlist.m3u8") + it.value.substringAfter("./")
}.toList()
videoLinks.mapIndexed { i, link ->
Video(
link,
qualities[i],
link,
)
}
} else {
listOf(
Video(
url,
"Qualità predefinita",
url,
headers = Headers.headersOf("Referer", referer),
),
)
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!!
val qualityList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
qualityList.add(preferred, video)
preferred++
} else {
qualityList.add(video)
}
}
return qualityList
}
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
if (filterSearch) {
// filter search
anime.setUrlWithoutDomain(element.selectFirst("div.card.mb-4.shadow-sm a")!!.attr("href"))
anime.title = formatTitle(element.selectFirst("div.card.mb-4.shadow-sm a")!!.attr("title"))
anime.thumbnail_url = element.selectFirst("div.card.mb-4.shadow-sm a img.new-anime")!!.attr("src")
} else {
// word search
anime.setUrlWithoutDomain(element.selectFirst("li.list-group-item.bg-dark-as-box-shadow div.item-archivio div.info-archivio h3 a.badge.badge-archivio.badge-light")!!.attr("href"))
anime.title = formatTitle(element.selectFirst("li.list-group-item.bg-dark-as-box-shadow div.item-archivio div.info-archivio h3 a.badge.badge-archivio.badge-light")!!.text())
anime.thumbnail_url = element.select("li.list-group-item.bg-dark-as-box-shadow div.item-archivio a.thumb.image-wrapper img.rounded.locandina-archivio").attr("src")
}
return anime
}
override fun searchAnimeNextPageSelector(): String = "li.page-item.active:not(li:last-child)"
private var filterSearch = false
override fun searchAnimeSelector(): String {
return if (filterSearch) {
"div.anime-card-newanime.main-anime-card" // filter search
} else {
"ul.list-group" // regular search
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val parameters = getSearchParameters(filters)
return if (parameters.isEmpty()) {
filterSearch = false
GET("$baseUrl/animelist?search=$query") // regular search
} else {
filterSearch = true
GET("$baseUrl/filter?$parameters&page=$page") // with filters
}
}
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title =
formatTitle(document.select("div.container.anime-title-as.mb-3.w-100 b").text())
val tempDetails =
document.select("div.container.shadow.rounded.bg-dark-as-box.mb-3.p-3.w-100.text-white")
.text()
val indexA = tempDetails.indexOf("Stato:")
anime.author = tempDetails.substring(7, indexA).trim()
val indexS1 = tempDetails.indexOf("Stato:") + 6
val indexS2 = tempDetails.indexOf("Data di uscita:")
anime.status = parseStatus(tempDetails.substring(indexS1, indexS2).trim())
anime.genre =
document.select("div.container.shadow.rounded.bg-dark-as-box.mb-3.p-3.w-100 a.badge.badge-dark.generi-as.mb-1")
.joinToString { it.text() }
anime.thumbnail_url = document.selectFirst("img.img-fluid.cover-anime.rounded")!!.attr("src")
val alterTitle = formatTitle(
document.selectFirst("div.box-trasparente-alternativo.rounded")!!.text(),
).replace("Dub ITA", "").trim()
val description1 = document.selectFirst("div#trama div#shown-trama")?.ownText()
val description2 = document.selectFirst("div#full-trama.d-none")?.ownText()
when {
description1 == null -> {
anime.description = description2
}
description2 == null -> {
anime.description = description1
} description1.length > description2.length -> {
anime.description = description1
} else -> {
anime.description = description2
}
}
if (!anime.title.contains(alterTitle, true)) anime.description = anime.description + "\n\nTitolo Alternativo: " + alterTitle
return anime
}
private fun parseStatus(statusString: String): Int {
return when {
statusString.contains("In corso") -> {
SAnime.ONGOING
}
statusString.contains("Finito") -> {
SAnime.COMPLETED
}
else -> {
SAnime.UNKNOWN
}
}
}
override fun latestUpdatesSelector(): String = "div.card.mb-4.shadow-sm"
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.title = formatTitle(element.selectFirst("a")!!.attr("title"))
anime.thumbnail_url = element.selectFirst("a img.new-anime")!!.attr("src")
return anime
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newest?page=$page")
override fun latestUpdatesNextPageSelector(): String = "li.page-item.active:not(li:last-child)"
// Filters
internal class Genre(val id: String) : AnimeFilter.CheckBox(id)
private class GenreList(genres: List<Genre>) : AnimeFilter.Group<Genre>("Generi", genres)
private fun getGenres() = listOf(
Genre("Arti Marziali"),
Genre("Avventura"),
Genre("Azione"),
Genre("Bambini"),
Genre("Commedia"),
Genre("Demenziale"),
Genre("Demoni"),
Genre("Drammatico"),
Genre("Ecchi"),
Genre("Fantasy"),
Genre("Gioco"),
Genre("Harem"),
Genre("Hentai"),
Genre("Horror"),
Genre("Josei"),
Genre("Magia"),
Genre("Mecha"),
Genre("Militari"),
Genre("Mistero"),
Genre("Musicale"),
Genre("Parodia"),
Genre("Polizia"),
Genre("Psicologico"),
Genre("Romantico"),
Genre("Samurai"),
Genre("Sci-Fi"),
Genre("Scolastico"),
Genre("Seinen"),
Genre("Sentimentale"),
Genre("Shoujo Ai"),
Genre("Shoujo"),
Genre("Shounen Ai"),
Genre("Shounen"),
Genre("Slice of Life"),
Genre("Soprannaturale"),
Genre("Spazio"),
Genre("Sport"),
Genre("Storico"),
Genre("Superpoteri"),
Genre("Thriller"),
Genre("Vampiri"),
Genre("Veicoli"),
Genre("Yaoi"),
Genre("Yuri"),
)
internal class Year(val id: String) : AnimeFilter.CheckBox(id)
private class YearList(years: List<Year>) : AnimeFilter.Group<Year>("Anno di Uscita", years)
private fun getYears() = listOf(
Year("1969"),
Year("1970"),
Year("1975"),
Year("1978"),
Year("1979"),
Year("1981"),
Year("1983"),
Year("1984"),
Year("1986"),
Year("1987"),
Year("1988"),
Year("1989"),
Year("1990"),
Year("1991"),
Year("1992"),
Year("1993"),
Year("1994"),
Year("1995"),
Year("1996"),
Year("1997"),
Year("1998"),
Year("1999"),
Year("2000"),
Year("2001"),
Year("2002"),
Year("2003"),
Year("2004"),
Year("2005"),
Year("2006"),
Year("2007"),
Year("2008"),
Year("2009"),
Year("2010"),
Year("2011"),
Year("2012"),
Year("2013"),
Year("2014"),
Year("2015"),
Year("2016"),
Year("2017"),
Year("2018"),
Year("2019"),
Year("2020"),
Year("2021"),
Year("2022"),
Year("2023"),
Year("2024"),
)
internal class State(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class StateList(states: List<State>) : AnimeFilter.Group<State>("Stato", states)
private fun getStates() = listOf(
State("0", "In corso"),
State("1", "Finito"),
State("2", "Non rilasciato"),
State("3", "Droppato"),
)
internal class Lang(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class LangList(langs: List<Lang>) : AnimeFilter.Group<Lang>("Lingua", langs)
private fun getLangs() = listOf(
Lang("0", "Subbato"),
Lang("1", "Doppiato"),
)
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Ricerca per titolo ignora i filtri e viceversa"),
GenreList(getGenres()),
YearList(getYears()),
StateList(getStates()),
LangList(getLangs()),
)
private fun getSearchParameters(filters: AnimeFilterList): String {
var totalstring = ""
var variantgenre = 0
var variantstate = 0
var variantyear = 0
filters.forEach { filter ->
when (filter) {
is GenreList -> { // ---Genre
filter.state.forEach { Genre ->
if (Genre.state) {
totalstring = totalstring + "&categories%5B" + variantgenre.toString() + "%5D=" + Genre.id
variantgenre++
}
}
}
is YearList -> { // ---Year
filter.state.forEach { Year ->
if (Year.state) {
totalstring = totalstring + "&years%5B" + variantyear.toString() + "%5D=" + Year.id
variantyear++
}
}
}
is StateList -> { // ---State
filter.state.forEach { State ->
if (State.state) {
totalstring = totalstring + "&states%5B" + variantstate.toString() + "%5D=" + State.id
variantstate++
}
}
}
is LangList -> { // ---Lang
filter.state.forEach { Lang ->
if (Lang.state) {
totalstring = totalstring + "&language%5B0%5D=" + Lang.id
}
}
}
else -> {}
}
}
return totalstring
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Qualità preferita"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p", "144p")
entryValues = arrayOf("1080", "720", "480", "360", "240", "144")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Domain in uso (riavvio dell'app richiesto)"
entries = arrayOf("animesaturn.in")
entryValues = arrayOf("https://animesaturn.in")
setDefaultValue("https://animesaturn.in")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
screen.addPreference(domainPref)
}
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1,517 @@
package eu.kanade.tachiyomi.animeextension.it.animeunity
import android.annotation.SuppressLint
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.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.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
class AnimeUnity :
AnimeHttpSource(),
ConfigurableAnimeSource {
override val name = "AnimeUnity"
// TODO: Check frequency of url changes to potentially
// add back overridable baseurl preference
override val baseUrl = "https://www.animeunity.to"
override val lang = "it"
override val supportsLatest = true
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 = GET("$baseUrl/top-anime?popular=true&page=$page", headers = headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed =
response.parseAs<AnimeResponse> {
it
.substringAfter("top-anime animes=\"")
.substringBefore("\"></top-anime>")
.replace("&quot;", "\"")
}
val animeList =
parsed.data.map { ani ->
SAnime.create().apply {
title = ani.title_eng
url = "${ani.id}-${ani.slug}"
thumbnail_url = ani.imageurl ?: ""
}
}
return AnimesPage(animeList, parsed.current_page < parsed.last_page)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/?anime=$page", headers = headers)
override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animeList =
document.select("div.home-wrapper-body > div.row > div.latest-anime-container").map {
SAnime.create().apply {
title = it.select("a > strong").text()
url = it.selectFirst("a")!!.attr("href").substringAfter("/anime/")
thumbnail_url = it.select("img").attr("src")
}
}
val hasNextPage = document.select("ul.pagination > li.active ~ li").first() != null
return AnimesPage(animeList, hasNextPage)
}
// =============================== Search ===============================
override fun searchAnimeRequest(
page: Int,
query: String,
filters: AnimeFilterList,
): Request = throw UnsupportedOperationException()
override suspend fun getSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList,
): AnimesPage {
val params = AnimeUnityFilters.getSearchParameters(filters)
return client
.newCall(searchAnimeRequest(page, query, params))
.awaitSuccess()
.use { response ->
searchAnimeParse(response, page)
}
}
private fun searchAnimeRequest(
page: Int,
query: String,
filters: AnimeUnityFilters.FilterSearchParams,
): Request {
val archivioResponse =
client
.newCall(
GET("$baseUrl/archivio", headers = headers),
).execute()
val document = archivioResponse.asJsoup()
val crsfToken = document.select("meta[name=csrf-token]").attr("content")
var newHeadersBuilder = headers.newBuilder()
for (cookie in archivioResponse.headers) {
if (cookie.first == "set-cookie" && cookie.second.startsWith("XSRF-TOKEN")) {
newHeadersBuilder.add(
"X-XSRF-TOKEN",
cookie
.second
.substringAfter("=")
.substringBefore(";")
.replace("%3D", "="),
)
}
if (cookie.first == "set-cookie" && cookie.second.startsWith("animeunity_session")) {
newHeadersBuilder.add("Cookie", cookie.second.substringBefore(";").replace("%3D", "="))
}
}
newHeadersBuilder
.add("X-CSRF-TOKEN", crsfToken)
.add("Accept-Language", "en-US,en;q=0.5")
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0")
if (filters.top.isNotEmpty()) {
val topHeaders =
newHeadersBuilder
.add("X-CSRF-TOKEN", crsfToken)
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Referer", "$baseUrl/${filters.top}")
return GET("$baseUrl/${filters.top}", headers = topHeaders.build())
}
val searchHeaders =
newHeadersBuilder
.add("Accept", "application/json, text/plain, */*")
.add("Content-Type", "application/json;charset=utf-8")
.add("Origin", baseUrl)
.add("Referer", archivioResponse.request.url.toString())
.add("X-Requested-With", "XMLHttpRequest")
.build()
val body =
"""
{
"title": ${query.falseIfEmpty()},
"type": ${filters.type.falseIfEmpty()},
"year": ${filters.year.falseIfEmpty()},
"order": ${filters.order.falseIfEmpty()},
"status": ${filters.state.falseIfEmpty()},
"genres": ${filters.genre.ifEmpty { "false" }},
"offset": ${(page - 1) * 30},
"dubbed": ${if (filters.dub.isEmpty()) "false" else "true"},
"season": ${filters.season.falseIfEmpty()}
}
""".trimIndent().toRequestBody("application/json".toMediaType())
return POST("$baseUrl/archivio/get-animes", body = body, headers = searchHeaders)
}
override fun searchAnimeParse(response: Response): AnimesPage = throw UnsupportedOperationException()
private fun searchAnimeParse(
response: Response,
page: Int,
): AnimesPage =
if (response.request.method == "POST") {
val data = response.parseAs<SearchResponse>()
val animeList =
data.records.map {
SAnime.create().apply {
title = it.title_eng
thumbnail_url = it.imageurl
url = "${it.id}-${it.slug}"
}
}
AnimesPage(animeList, data.tot - page * 30 >= 30 && data.tot > 30)
} else {
popularAnimeParse(response)
}
override fun getFilterList(): AnimeFilterList = AnimeUnityFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request = GET("$baseUrl/anime/${anime.url}")
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val videoPlayer = document.selectFirst("video-player[episodes_count]")!!
val animeDetails =
json.decodeFromString<AnimeInfo>(
videoPlayer.attr("anime").replace("&quot;", "\""),
)
return SAnime.create().apply {
title = animeDetails.title_eng
status = parseStatus(animeDetails.status)
artist = animeDetails.studio ?: ""
genre = animeDetails.genres.joinToString(", ") { it.name }
description =
buildString {
append(animeDetails.plot)
append("\n\nTipo: ${animeDetails.type}")
append("\nStagione: ${animeDetails.season} ${animeDetails.date}")
append("\nValutazione: ★${animeDetails.score ?: "-"}")
}
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request = GET("$baseUrl/anime/${anime.url}", headers = headers)
override fun episodeListParse(response: Response): List<SEpisode> {
val episodeList = mutableListOf<SEpisode>()
val document = response.asJsoup()
val crsfToken = document.select("meta[name=csrf-token]").attr("content")
var newHeadersBuilder = headers.newBuilder()
for (cookie in response.headers) {
if (cookie.first == "set-cookie" && cookie.second.startsWith("XSRF-TOKEN")) {
newHeadersBuilder.add(
"X-XSRF-TOKEN",
cookie
.second
.substringAfter("=")
.substringBefore(";")
.replace("%3D", "="),
)
}
if (cookie.first == "set-cookie" && cookie.second.startsWith("animeunity_session")) {
newHeadersBuilder.add("Cookie", cookie.second.substringBefore(";").replace("%3D", "="))
}
}
newHeadersBuilder
.add("X-CSRF-TOKEN", crsfToken)
.add("Content-Type", "application/json")
.add("Referer", response.request.url.toString())
.add("Accept", "application/json, text/plain, */*")
.add("Accept-Language", "en-US,en;q=0.5")
.add("X-Requested-With", "XMLHttpRequest")
val newHeaders = newHeadersBuilder.build()
val videoPlayer = document.selectFirst("video-player[episodes_count]")!!
val episodeCount = videoPlayer.attr("episodes_count").toInt()
val animeId =
response
.request
.url
.toString()
.substringAfter("/anime/")
.substringBefore("-")
val episodes =
json.decodeFromString<List<Episode>>(
videoPlayer.attr("episodes").replace("&quot;", "\""),
)
episodeList.addAll(
episodes
.filter {
it.id != null
}.map {
SEpisode.create().apply {
name = "Episode ${it.number}"
date_upload = parseDate(it.created_at)
episode_number = it.number.split("-")[0].toFloatOrNull() ?: 0F
setUrlWithoutDomain(
response
.request
.url
.newBuilder()
.addPathSegment(it.id.toString())
.toString(),
)
}
},
)
if (episodeCount > 120) {
var start = 121
var end = 240
while (end < episodeCount) {
episodeList.addAll(
addFromApi(start, end, animeId, newHeaders, response.request.url),
)
start += 120
end += 120
}
if (episodeCount >= start) {
episodeList.addAll(
addFromApi(start, episodeCount, animeId, newHeaders, response.request.url),
)
}
}
return episodeList.sortedBy { it.episode_number }.reversed()
}
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val videoList = mutableListOf<Video>()
val doc =
client
.newCall(
GET(baseUrl + episode.url, headers),
).execute()
.asJsoup()
val iframeUrl =
doc.selectFirst("video-player[embed_url]")?.attr("abs:embed_url")
?: error("Failed to extract iframe")
val iframeHeaders =
headers
.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", iframeUrl.toHttpUrl().host)
.add("Referer", "$baseUrl/")
.build()
val iframe =
client
.newCall(
GET(iframeUrl, headers = iframeHeaders),
).execute()
.asJsoup()
val scripts = iframe.select("script")
val script = scripts.find { it.data().contains("masterPlaylist") }!!.data().replace("\n", "\t")
var playlistUrl = Regex("""url: ?'(.*?)'""").find(script)!!.groupValues[1]
val filename = playlistUrl.slice(playlistUrl.lastIndexOf("/") + 1 until playlistUrl.length)
if (!filename.endsWith(".m3u8")) {
playlistUrl = playlistUrl.replace(filename, filename + ".m3u8")
}
val expires = Regex("""'expires': ?'(\d+)'""").find(script)!!.groupValues[1]
val token = Regex("""'token': ?'([\w-]+)'""").find(script)!!.groupValues[1]
// Get subtitles
val masterPlUrl = "$playlistUrl?token=$token&expires=$expires&n=1"
val masterPl =
client
.newCall(GET(masterPlUrl))
.execute()
.body
.string()
val subList =
Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
.findAll(masterPl)
.map {
Track(it.groupValues[2], it.groupValues[1])
}.toList()
Regex("""'token(\d+p?)': ?'([\w-]+)'""").findAll(script).forEach { match ->
val quality = match.groupValues[1]
val videoUrl =
buildString {
append(playlistUrl)
append("?type=video&rendition=")
append(quality)
append("&token=")
append(match.groupValues[2])
append("&expires=$expires")
append("&n=1")
}
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subList))
}
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return videoList.sort()
}
override fun videoListRequest(episode: SEpisode): Request = throw UnsupportedOperationException()
override fun videoListParse(response: Response): List<Video> = throw UnsupportedOperationException()
// ============================= Utilities ==============================
private fun parseStatus(statusString: String): Int =
when (statusString) {
"In Corso" -> SAnime.ONGOING
"Terminato" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
private fun addFromApi(
start: Int,
end: Int,
animeId: String,
headers: Headers,
url: HttpUrl,
): List<SEpisode> {
val response =
client
.newCall(
GET("$baseUrl/info_api/$animeId/1?start_range=$start&end_range=$end", headers = headers),
).execute()
val json = response.parseAs<ApiResponse>()
return json
.episodes
.filter {
it.id != null
}.map {
SEpisode.create().apply {
name = "Episode ${it.number}"
date_upload = parseDate(it.created_at)
episode_number = it.number.split("-")[0].toFloatOrNull() ?: 0F
setUrlWithoutDomain(
url
.newBuilder()
.addPathSegment(it.id.toString())
.toString(),
)
}
}
}
private fun String.falseIfEmpty(): String =
if (this.isEmpty()) {
"false"
} else {
"\"${this}\""
}
@SuppressLint("SimpleDateFormat")
private fun parseDate(date: String): Long {
val knownPatterns: MutableList<SimpleDateFormat> = ArrayList()
knownPatterns.add(SimpleDateFormat("yyyy-MM-dd hh:mm:ss"))
for (pattern in knownPatterns) {
try {
// Take a try
return pattern.parse(date)!!.time
} catch (e: Throwable) {
// Loop on
}
}
return System.currentTimeMillis()
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this
.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.substringBefore("p").toIntOrNull() ?: 0 },
),
).reversed()
}
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context)
.apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p", "80p")
entryValues = arrayOf("1080", "720", "480", "360", "240", "80")
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,77 @@
package eu.kanade.tachiyomi.animeextension.it.animeunity
import kotlinx.serialization.Serializable
@Serializable
data class AnimeResponse(
val current_page: Int,
val last_page: Int,
val data: List<Anime>,
) {
@Serializable
data class Anime(
val id: Int,
val slug: String,
val title_eng: String,
val imageurl: String? = null,
)
}
@Serializable
data class Episode(
val number: String,
val created_at: String,
val id: Int? = null,
)
@Serializable
data class ApiResponse(
val episodes: List<Episode>,
)
@Serializable
data class ServerResponse(
val name: String,
val client_ip: String,
val folder_id: String,
val proxy_download: Int,
val storage_download: StorageDownload,
) {
@Serializable
data class StorageDownload(
val number: Int,
)
}
@Serializable
data class LinkData(
val id: String,
val file_name: String,
)
@Serializable
data class AnimeInfo(
val title_eng: String,
val imageurl: String,
val plot: String,
val date: String,
val season: String,
val slug: String,
val id: Int,
val type: String,
val status: String,
val genres: List<Genre>,
val studio: String? = null,
val score: String? = null,
) {
@Serializable
data class Genre(
val name: String,
)
}
@Serializable
data class SearchResponse(
val records: List<AnimeInfo>,
val tot: Int,
)

View file

@ -0,0 +1,204 @@
package eu.kanade.tachiyomi.animeextension.it.animeunity
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
object AnimeUnityFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.filterIsInstance<R>().joinToString("") {
(it as QueryPartFilter).toQueryPart()
}
}
class TopFilter : QueryPartFilter("Top Anime", AnimeUnityFiltersData.TOP)
class GenreFilter : CheckBoxFilterList(
"Genere",
AnimeUnityFiltersData.GENRE.map { CheckBoxVal(it.first, false) },
)
class YearFilter : QueryPartFilter("Anno", AnimeUnityFiltersData.YEAR)
class OrderFilter : QueryPartFilter("Ordina", AnimeUnityFiltersData.ORDER)
class StateFilter : QueryPartFilter("Stato", AnimeUnityFiltersData.STATE)
class TypeFilter : QueryPartFilter("Tipo", AnimeUnityFiltersData.TYPE)
class SeasonFilter : QueryPartFilter("Stagione", AnimeUnityFiltersData.SEASON)
class DubFilter : QueryPartFilter("Dub ITA", AnimeUnityFiltersData.DUB)
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("Le migliori pagine di anime"),
AnimeFilter.Header("Nota: ignora altri filtri"),
TopFilter(),
AnimeFilter.Separator(),
GenreFilter(),
YearFilter(),
OrderFilter(),
StateFilter(),
TypeFilter(),
SeasonFilter(),
DubFilter(),
)
data class FilterSearchParams(
val top: String = "",
val genre: String = "",
val year: String = "",
val order: String = "",
val state: String = "",
val type: String = "",
val season: String = "",
val dub: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val genre: String = filters.filterIsInstance<GenreFilter>()
.first()
.state.filter { it.state }.joinToString(",") { format ->
buildJsonObject {
put("id", AnimeUnityFiltersData.GENRE.find { it.first == format.name }!!.second.toInt())
put("name", AnimeUnityFiltersData.GENRE.find { it.first == format.name }!!.first)
}.toString()
}
return FilterSearchParams(
filters.asQueryPart<TopFilter>(),
if (genre.isEmpty()) "" else "[$genre]",
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<OrderFilter>(),
filters.asQueryPart<StateFilter>(),
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.asQueryPart<DubFilter>(),
)
}
private object AnimeUnityFiltersData {
val ANY = Pair("Any", "")
val TOP = arrayOf(
Pair("Nessuno", ""),
Pair("Tutto", "top-anime"),
Pair("In corso", "top-anime?status=In Corso"),
Pair("In arrivo", "top-anime?status=In uscita prossimamente"),
Pair("TV", "top-anime?type=TV"),
Pair("Movie", "top-anime?type=Movie"),
Pair("OVA", "top-anime?type=OVA"),
Pair("ONA", "top-anime?type=ONA"),
Pair("Special", "top-anime?type=Special"),
Pair("Popolari", "top-anime?popular=true"),
Pair("Preferiti", "top-anime?order=favorites"),
Pair("Più visti", "top-anime?order=most_viewed"),
)
val GENRE = arrayOf(
Pair("Action", "51"),
Pair("Adventure", "21"),
Pair("Cars", "29"),
Pair("Comedy", "37"),
Pair("Dementia", "43"),
Pair("Demons", "13"),
Pair("Drama", "22"),
Pair("Ecchi", "5"),
Pair("Fantasy", "9"),
Pair("Game", "44"),
Pair("Harem", "15"),
Pair("Hentai", "4"),
Pair("Historical", "30"),
Pair("Horror", "3"),
Pair("Josei", "45"),
Pair("Kids", "14"),
Pair("Magic", "23"),
Pair("Martial Arts", "Martial 31"),
Pair("Mecha", "38"),
Pair("Military", "46"),
Pair("Music", "16"),
Pair("Mystery", "24"),
Pair("Parody", "32"),
Pair("Police", "39"),
Pair("Psychological", "47"),
Pair("Romance", "17"),
Pair("Samurai", "25"),
Pair("School", "33"),
Pair("Sci-fi", "Sci-40"),
Pair("Seinen", "49"),
Pair("Shoujo", "18"),
Pair("Shoujo Ai", "Shoujo 26"),
Pair("Shounen", "34"),
Pair("Shounen Ai", "Shounen 41"),
Pair("Slice of Life", "Slice of 50"),
Pair("Space", "19"),
Pair("Splatter", "52"),
Pair("Sports", "27"),
Pair("Super Power", "Super 35"),
Pair("Supernatural", "42"),
Pair("Thriller", "48"),
Pair("Vampire", "20"),
Pair("Yaoi", "28"),
Pair("Yuri", "36"),
)
val ORDER = arrayOf(
ANY,
Pair("Lista A-Z", "Lista A-Z"),
Pair("Lista Z-A", "Lista Z-A"),
Pair("Popolarità", "Popolarità"),
Pair("Valutazione", "Valutazione"),
)
val STATE = arrayOf(
ANY,
Pair("In Corso", "In Corso"),
Pair("Terminato", "Terminato"),
Pair("In Uscita", "In Uscita"),
Pair("Droppato", "Droppato"),
)
val TYPE = arrayOf(
ANY,
Pair("TV", "TV"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Special", "Special"),
Pair("Movie", "Movie"),
)
val SEASON = arrayOf(
ANY,
Pair("Inverno", "Inverno"),
Pair("Primavera", "Primavera"),
Pair("Estate", "Estate"),
Pair("Autunno", "Autunno"),
)
val DUB = arrayOf(
Pair("No", ""),
Pair("", "true"),
)
val YEAR = arrayOf(ANY) + (1969..2024).map {
Pair(it.toString(), it.toString())
}.reversed().toTypedArray()
}
}

View file

@ -0,0 +1,14 @@
ext {
extName = 'ANIMEWORLD.tv'
extClass = '.ANIMEWORLD'
extVersionCode = 37
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:dood-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -0,0 +1,542 @@
package eu.kanade.tachiyomi.animeextension.it.animeworld
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.it.animeworld.extractors.StreamHideExtractor
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.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
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
import java.lang.Exception
class ANIMEWORLD : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "ANIMEWORLD.tv"
// TODO: Check frequency of url changes to potentially
// add back overridable baseurl preference
override val baseUrl = "https://www.animeworld.so"
override val lang = "it"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// Popular Anime - Same Format as Search
override fun popularAnimeSelector(): String = searchAnimeSelector()
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/filter?sort=6&page=$page")
override fun popularAnimeFromElement(element: Element): SAnime = searchAnimeFromElement(element)
override fun popularAnimeNextPageSelector(): String = searchAnimeNextPageSelector()
// Episodes
override fun episodeListParse(response: Response): List<SEpisode> {
return super.episodeListParse(response).reversed()
}
override fun episodeListSelector() = "div.server.active ul.episodes li.episode a"
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(element.attr("abs:href"))
episode.name = "Episode: " + element.text()
val epNum = getNumberFromEpsString(element.text())
episode.episode_number = when {
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
else -> 1F
}
return episode
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
// Video urls
override fun videoListRequest(episode: SEpisode): Request {
val iframe = baseUrl + episode.url
return GET(iframe)
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return videosFromElement(document)
}
override fun videoListSelector() = "center a[href*=https://doo]," +
"center a[href*=streamtape]," +
"center a[href*=animeworld.biz]," +
"center a[href*=streamingaw.online][id=alternativeDownloadLink]"
private fun videosFromElement(document: Document): List<Video> {
// afaik this element appears when videos are taken down, in this case instead of
// displaying Videolist empty show the element's text
val copyrightError = document.select("div.alert.alert-primary:contains(Copyright)")
if (copyrightError.hasText()) throw Exception(copyrightError.text())
val serverList = mutableListOf<Pair<String, String>>()
val elements = document.select(videoListSelector())
val epId = document.selectFirst("div#player[data-episode-id]")?.attr("data-episode-id")
val altServers = mutableListOf<Pair<String, String>>()
val altList = listOf("StreamHide", "FileMoon")
document.select("div.servers > div.widget-title span.server-tab").forEach {
val name = it.text()
if (altList.any { t -> t.contains(name, true) }) {
altServers.add(Pair(name, it.attr("data-name")))
}
}
altServers.forEach { serverPair ->
val dataId = document.selectFirst("div.server[data-name=${serverPair.second}] li.episode a[data-episode-id=$epId]")?.attr("data-id")
dataId?.let {
val apiUrl = "$baseUrl/api/episode/info?id=$it&alt=0"
val apiHeaders = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Content-Type", "application/json")
.add("Host", baseUrl.toHttpUrl().host)
.add("Referer", document.location())
.add("X-Requested-With", "XMLHttpRequest")
.build()
val target = json.decodeFromString<ServerResponse>(
client.newCall(GET(apiUrl, headers = apiHeaders)).execute().body.string(),
).target
serverList.add(Pair(serverPair.first, target))
}
}
for (element in elements) {
val url = element.attr("href")
val name = element.text().substringAfter("ownload ").substringBefore(" ")
serverList.add(Pair(name, url))
}
val videoList = serverList.parallelCatchingFlatMapBlocking { server ->
val url = server.second
when {
url.contains("streamingaw") -> {
listOf(Video(url, "AnimeWorld Server", url))
}
url.contains("https://doo") -> {
DoodExtractor(client).videoFromUrl(url, redirect = true)
?.let(::listOf)
}
url.contains("streamtape") -> {
StreamTapeExtractor(client).videoFromUrl(url.replace("/v/", "/e/"))
?.let(::listOf)
}
url.contains("filemoon") -> {
FilemoonExtractor(client).videosFromUrl(url, prefix = "${server.first} - ", headers = headers)
}
server.first.contains("streamhide", true) -> {
StreamHideExtractor(client).videosFromUrl(url, headers)
}
else -> null
}.orEmpty()
}
return videoList
}
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!!
val server = preferences.getString("preferred_server", "Animeworld server")!!
return sortedWith(
compareBy(
{ it.quality.lowercase().contains(server.lowercase()) },
{ it.quality.lowercase().contains(quality.lowercase()) },
),
).reversed()
}
// search
override fun searchAnimeSelector(): String = "div.film-list div.item div.inner a.poster"
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("abs:href"))
anime.thumbnail_url = element.select("img").attr("src")
anime.title = element.select("img").attr("alt")
return anime
}
override fun searchAnimeNextPageSelector(): String = "div.paging-wrapper a#go-next-page"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
GET("$baseUrl/filter?${getSearchParameters(filters)}&keyword=$query&page=$page")
// Details
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = document.selectFirst("div.thumb img")!!.attr("src")
anime.title = document.select("div.c1 h2.title").text()
val dl = document.select("div.info dl")
anime.genre = dl.select("dd:has(a[href*=language]) a, dd:has(a[href*=genre]) a").joinToString(", ") { it.text() }
anime.description = document.select("div.desc").text()
anime.author = dl.select("dd:has(a[href*=studio]) a").joinToString(", ") { it.text() }
anime.status = parseStatus(dl.select("dd:has(a[href*=status]) a").text().replace("Status: ", ""))
return anime
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"In corso" -> SAnime.ONGOING
"Finito" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
// Latest - Same format as search
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/updated?page=$page")
override fun latestUpdatesSelector(): String = searchAnimeSelector()
override fun latestUpdatesNextPageSelector(): String = searchAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = searchAnimeFromElement(element)
// Filters
internal class Genre(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class GenreList(genres: List<Genre>) : AnimeFilter.Group<Genre>("Generi", genres)
private fun getGenres() = listOf(
Genre("and", "Mode: AND"),
Genre("3", "Arti Marziali"),
Genre("5", "Avanguardia"),
Genre("2", "Avventura"),
Genre("1", "Azione"),
Genre("47", "Bambini"),
Genre("4", "Commedia"),
Genre("6", "Demoni"),
Genre("7", "Drammatico"),
Genre("8", "Ecchi"),
Genre("9", "Fantasy"),
Genre("10", "Gioco"),
Genre("11", "Harem"),
Genre("43", "Hentai"),
Genre("13", "Horror"),
Genre("14", "Josei"),
Genre("16", "Magia"),
Genre("18", "Mecha"),
Genre("19", "Militari"),
Genre("21", "Mistero"),
Genre("20", "Musicale"),
Genre("22", "Parodia"),
Genre("23", "Polizia"),
Genre("24", "Psicologico"),
Genre("46", "Romantico"),
Genre("26", "Samurai"),
Genre("28", "Sci-Fi"),
Genre("27", "Scolastico"),
Genre("29", "Seinen"),
Genre("25", "Sentimentale"),
Genre("30", "Shoujo"),
Genre("31", "Shoujo Ai"),
Genre("32", "Shounen"),
Genre("33", "Shounen Ai"),
Genre("34", "Slice of Life"),
Genre("35", "Spazio"),
Genre("37", "Soprannaturale"),
Genre("36", "Sport"),
Genre("12", "Storico"),
Genre("38", "Superpoteri"),
Genre("39", "Thriller"),
Genre("40", "Vampiri"),
Genre("48", "Veicoli"),
Genre("41", "Yaoi"),
Genre("42", "Yuri"),
)
internal class Season(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class SeasonList(seasons: List<Season>) : AnimeFilter.Group<Season>("Stagioni", seasons)
private fun getSeasons() = listOf(
Season("winter", "Inverno"),
Season("spring", "Primavera"),
Season("summer", "Estate"),
Season("fall", "Autunno"),
Season("unknown", "Sconosciuto"),
)
internal class Year(val id: String) : AnimeFilter.CheckBox(id)
private class YearList(years: List<Year>) : AnimeFilter.Group<Year>("Anno di Uscita", years)
private fun getYears() = listOf(
Year("1966"),
Year("1967"),
Year("1969"),
Year("1970"),
Year("1973"),
Year("1974"),
Year("1975"),
Year("1977"),
Year("1978"),
Year("1979"),
Year("1980"),
Year("1981"),
Year("1982"),
Year("1983"),
Year("1984"),
Year("1985"),
Year("1986"),
Year("1987"),
Year("1988"),
Year("1989"),
Year("1990"),
Year("1991"),
Year("1992"),
Year("1993"),
Year("1994"),
Year("1995"),
Year("1996"),
Year("1997"),
Year("1998"),
Year("1999"),
Year("2000"),
Year("2001"),
Year("2002"),
Year("2003"),
Year("2004"),
Year("2005"),
Year("2006"),
Year("2007"),
Year("2008"),
Year("2009"),
Year("2010"),
Year("2011"),
Year("2012"),
Year("2013"),
Year("2014"),
Year("2015"),
Year("2016"),
Year("2017"),
Year("2018"),
Year("2019"),
Year("2020"),
Year("2021"),
Year("2022"),
)
internal class Type(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class TypeList(types: List<Type>) : AnimeFilter.Group<Type>("Tipo", types)
private fun getTypes() = listOf(
Type("0", "Anime"),
Type("4", "Movie"),
Type("1", "OVA"),
Type("2", "ONA"),
Type("3", "Special"),
Type("5", "Music"),
)
internal class State(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class StateList(states: List<State>) : AnimeFilter.Group<State>("Stato", states)
private fun getStates() = listOf(
State("0", "In corso"),
State("1", "Finito"),
State("2", "Non rilasciato"),
State("3", "Droppato"),
)
internal class Studio(val input: String, name: String) : AnimeFilter.Text(name)
internal class Sub(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class SubList(subs: List<Sub>) : AnimeFilter.Group<Sub>("Sottotitoli", subs)
private fun getSubs() = listOf(
Sub("0", "Subbato"),
Sub("1", "Doppiato"),
)
internal class Audio(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class AudioList(audios: List<Audio>) : AnimeFilter.Group<Audio>("Audio", audios)
private fun getAudios() = listOf(
Audio("jp", "Giapponese"),
Audio("it", "Italiano"),
Audio("ch", "Cinese"),
Audio("kr", "Coreano"),
Audio("en", "Inglese"),
)
private class OrderFilter :
AnimeFilter.Select<String>(
"Ordine",
arrayOf(
"Standard",
"Ultime Aggiunte",
"Lista A-Z",
"Lista Z-A",
"Più Vecchi",
"Più Recenti",
"Più Visti",
),
0,
)
private fun getSearchParameters(filters: AnimeFilterList): String {
var totalstring = ""
filters.forEach { filter ->
when (filter) {
is GenreList -> { // ---Genre
filter.state.forEach { Genre ->
if (Genre.state) {
totalstring += if (Genre.id == "and") {
"&genre_mode=and"
} else {
"&genre=" + Genre.id
}
}
}
}
is SeasonList -> { // ---Season
filter.state.forEach { Season ->
if (Season.state) {
totalstring += "&season=" + Season.id
}
}
}
is YearList -> { // ---Year
filter.state.forEach { Year ->
if (Year.state) {
totalstring += "&year=" + Year.id
}
}
}
is TypeList -> { // ---Type
filter.state.forEach { Type ->
if (Type.state) {
totalstring += "&type=" + Type.id
}
}
}
is StateList -> { // ---State
filter.state.forEach { State ->
if (State.state) {
totalstring += "&status=" + State.id
}
}
}
is Studio -> {
if (filter.state.isNotEmpty()) {
val studios = filter.state.split(",").toTypedArray()
for (x in studios.indices) {
totalstring += "&studio=" + studios[x]
}
}
}
is SubList -> { // ---Subs
filter.state.forEach { Sub ->
if (Sub.state) {
totalstring += "&dub=" + Sub.id
}
}
}
is AudioList -> { // ---Audio
filter.state.forEach { Audio ->
if (Audio.state) {
totalstring += "&language=" + Audio.id
}
}
}
is OrderFilter -> {
if (filter.values[filter.state] == "Standard") totalstring += "&sort=0"
if (filter.values[filter.state] == "Ultime Aggiunte") totalstring += "&sort=1"
if (filter.values[filter.state] == "Lista A-Z") totalstring += "&sort=2"
if (filter.values[filter.state] == "Lista Z-A") totalstring += "&sort=3"
if (filter.values[filter.state] == "Più Vecchi") totalstring += "&sort=4"
if (filter.values[filter.state] == "Più Recenti") totalstring += "&sort=5"
if (filter.values[filter.state] == "Più Visti") totalstring += "&sort=6"
}
else -> {}
}
}
return totalstring
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
GenreList(getGenres()),
SeasonList(getSeasons()),
YearList(getYears()),
TypeList(getTypes()),
StateList(getStates()),
AnimeFilter.Header("Usa la virgola per separare i diversi studio"),
Studio("", "Studio"),
SubList(getSubs()),
AudioList(getAudios()),
OrderFilter(),
)
// Preferences
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = "preferred_server"
title = "Preferred server"
entries = arrayOf("Animeworld server", "FileMoon", "StreamHide", "Doodstream", "StreamTape")
entryValues = arrayOf("Animeworld server", "FileMoon", "StreamHide", "Doodstream", "StreamTape")
setDefaultValue("Animeworld server")
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)
}
// Utilities
@Serializable
data class ServerResponse(
val target: String,
)
}

View file

@ -0,0 +1,52 @@
package eu.kanade.tachiyomi.animeextension.it.animeworld.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class StreamHideExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, headers: Headers): List<Video> {
val videoList = mutableListOf<Video>()
val url = OkHttpClient().newBuilder().followRedirects(false).build()
.newCall(GET(url, headers)).execute().request.url.toString()
val packed = client.newCall(GET(url)).execute()
.asJsoup().selectFirst("script:containsData(eval)")?.data() ?: return emptyList()
val unpacked = JsUnpacker.unpackAndCombine(packed) ?: return emptyList()
val masterUrl = Regex("""file: ?"(.*?)"""").find(unpacked)?.groupValues?.get(1) ?: return emptyList()
val masterHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", masterUrl.toHttpUrl().host)
.add("Origin", "https://${url.toHttpUrl().host}")
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
val masterPlaylist = client.newCall(
GET(masterUrl, headers = masterHeaders),
).execute().body.string()
val masterBase = "https://${masterUrl.toHttpUrl().host}${masterUrl.toHttpUrl().encodedPath}"
.substringBeforeLast("/") + "/"
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
.forEach {
val quality = "StreamHide - " + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p "
val videoUrl = masterBase + it.substringAfter("\n").substringBefore("\n")
val videoHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", videoUrl.toHttpUrl().host)
.add("Origin", "https://${url.toHttpUrl().host}")
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
videoList.add(Video(videoUrl, quality, videoUrl, headers = videoHeaders))
}
return videoList
}
}

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=".it.aniplay.AniPlayUrlActivity"
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="aniplay.co"
android:pathPattern="/series/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,11 @@
ext {
extName = 'AniPlay'
extClass = '.AniPlay'
extVersionCode = 10
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -0,0 +1,238 @@
package eu.kanade.tachiyomi.animeextension.it.aniplay
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.AnimeInfoDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.LatestItemDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.PopularAnimeDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.PopularResponseDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.VideoDto
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.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class AniPlay : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "AniPlay"
override val baseUrl = "https://aniplay.co"
override val lang = "it"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Origin", baseUrl)
override val versionId = 2 // Source was rewritten in Svelte
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) =
GET("$API_URL/advancedSearch?sort=7&page=$page&origin=,,,,,,", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<PopularResponseDto>()
val animes = parsed.data.map(PopularAnimeDto::toSAnime)
return AnimesPage(animes, parsed.pagination.hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$API_URL/latest-episodes?page=$page&type=All")
override fun latestUpdatesParse(response: Response): AnimesPage {
val items = response.parseAs<List<LatestItemDto>>()
val animes = items.mapNotNull { it.serie.firstOrNull()?.toSAnime() }
return AnimesPage(animes, items.size == 20)
}
// =============================== Search ===============================
override fun getFilterList() = AniPlayFilters.FILTER_LIST
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/series/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
.apply { setUrlWithoutDomain(response.request.url.toString()) }
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AniPlayFilters.getSearchParameters(filters)
val url = "$API_URL/advancedSearch".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("origin", ",,,,,,")
.addQueryParameter("sort", params.order)
.addIfNotBlank("_q", query)
.addIfNotBlank("genres", params.genres)
.addIfNotBlank("country", params.countries)
.addIfNotBlank("types", params.types)
.addIfNotBlank("studios", params.studios)
.addIfNotBlank("status", params.status)
.addIfNotBlank("subbed", params.languages)
.build()
return GET(url, headers)
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val script = response.getPageScript()
val jsonString = script.substringAfter("{serie:").substringBefore(",tags") + "}"
val parsed = jsonString.fixJsonString().parseAs<AnimeInfoDto>()
title = parsed.title
genre = parsed.genres.joinToString { it.name }
artist = parsed.studios.joinToString { it.name }
thumbnail_url = parsed.thumbnailUrl
status = when (parsed.status) {
"Completato" -> SAnime.COMPLETED
"In corso" -> SAnime.ONGOING
"Sospeso" -> SAnime.ON_HIATUS
else -> SAnime.UNKNOWN
}
description = buildString {
parsed.description?.also {
append(it, "\n\n")
}
listOf(
"Titolo Alternativo" to parsed.alternative,
"Origine" to parsed.origin,
"Giorno di lancio" to parsed.release_day,
).forEach { (title, value) ->
if (value != null) append(title, ": ", value, "\n")
}
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val script = response.getPageScript()
val jsonString = script.substringAfter(",episodes:").substringBefore("]},") + "]"
val parsed = jsonString.fixJsonString().parseAs<List<EpisodeDto>>()
return parsed.map {
SEpisode.create().apply {
episode_number = it.number?.toFloatOrNull() ?: 1F
url = "/watch/${it.id}"
name = it.title ?: "Episodio ${it.number}"
date_upload = it.release_date.toDate()
}
}.reversed()
}
// ============================ Video Links =============================
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
override fun videoListParse(response: Response): List<Video> {
val script = response.getPageScript()
val jsonString = script.substringAfter("{episode:").substringBefore(",views") + "}"
val videoUrl = jsonString.fixJsonString().parseAs<VideoDto>().videoLink
return when {
videoUrl.contains(".m3u8") -> playlistUtils.extractFromHls(videoUrl)
else -> listOf(Video(videoUrl, "Default", videoUrl, headers = headers))
}
}
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()
}
// ============================== 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_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String) = apply {
if (value.isNotBlank()) {
addQueryParameter(query, value)
}
}
// {key:"value"} -> {"key":"value"}
private fun String.fixJsonString() = replace(WRONG_KEY_REGEX) {
"\"${it.groupValues[1]}\":${it.groupValues[2]}"
}
private fun Response.getPageScript() =
asJsoup().selectFirst("script:containsData(const data = )")!!.data()
private fun String?.toDate(): Long {
if (this == null) return 0L
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
companion object {
const val PREFIX_SEARCH = "id:"
private const val API_URL = "https://api.aniplay.co/api/series"
private val WRONG_KEY_REGEX by lazy { Regex("([a-zA-Z_]+):\\s?([\"|0-9|f|t|n|\\[|\\{])") }
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "540p", "480p", "360p", "244p", "144p")
}
}

View file

@ -0,0 +1,281 @@
package eu.kanade.tachiyomi.animeextension.it.aniplay
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniPlayFilters {
open class SelectFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
val selected get() = vals[state].second
}
private inline fun <reified R> AnimeFilterList.getSelected(): String {
return (first { it is R } as SelectFilter).selected
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first) })
private class CheckBoxVal(name: String) : AnimeFilter.CheckBox(name, false)
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): String {
return (first { it is R } as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.joinToString(",")
}
internal class OrderFilter : SelectFilter("Ordina per", ORDER_LIST)
internal class GenreFilter : CheckBoxFilterList("Generi", GENRE_LIST)
internal class CountryFilter : CheckBoxFilterList("Paesi", COUNTRY_LIST)
internal class TypeFilter : CheckBoxFilterList("Tipi", TYPE_LIST)
internal class StudioFilter : CheckBoxFilterList("Studio", STUDIO_LIST)
internal class StatusFilter : CheckBoxFilterList("Stato", STATUS_LIST)
internal class LanguageFilter : CheckBoxFilterList("Lingua", LANGUAGE_LIST)
internal val FILTER_LIST get() = AnimeFilterList(
OrderFilter(),
GenreFilter(),
CountryFilter(),
TypeFilter(),
StudioFilter(),
StatusFilter(),
LanguageFilter(),
)
internal data class FilterSearchParams(
val order: String = "1",
val genres: String = "",
val countries: String = "",
val types: String = "",
val studios: String = "",
val status: String = "",
val languages: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.getSelected<OrderFilter>(),
filters.parseCheckbox<GenreFilter>(GENRE_LIST),
filters.parseCheckbox<CountryFilter>(COUNTRY_LIST),
filters.parseCheckbox<TypeFilter>(TYPE_LIST),
filters.parseCheckbox<StudioFilter>(STUDIO_LIST),
filters.parseCheckbox<StatusFilter>(STATUS_LIST),
filters.parseCheckbox<LanguageFilter>(LANGUAGE_LIST),
)
}
private val GENRE_LIST = arrayOf(
Pair("Arti marziali", "35"),
Pair("Automobilismo", "49"),
Pair("Avventura", "3"),
Pair("Azione", "7"),
Pair("Boys Love", "52"),
Pair("Combattimento", "27"),
Pair("Commedia", "13"),
Pair("Cucina", "38"),
Pair("Demenziale", "32"),
Pair("Demoni", "26"),
Pair("Drammatico", "2"),
Pair("Ecchi", "21"),
Pair("Fantasy", "1"),
Pair("Giallo", "34"),
Pair("Gioco", "31"),
Pair("Guerra", "39"),
Pair("Harem", "30"),
Pair("Horror", "14"),
Pair("Isekai", "43"),
Pair("Josei", "47"),
Pair("Magia", "18"),
Pair("Mecha", "25"),
Pair("Militare", "23"),
Pair("Mistero", "5"),
Pair("Musica", "40"),
Pair("Parodia", "42"),
Pair("Politica", "24"),
Pair("Poliziesco", "29"),
Pair("Psicologico", "6"),
Pair("Reverse-harem", "45"),
Pair("Romantico", "8"),
Pair("Samurai", "36"),
Pair("Sci-Fi", "20"),
Pair("Scolastico", "10"),
Pair("Seinen", "28"),
Pair("Sentimentale", "12"),
Pair("Shoujo", "11"),
Pair("Shoujo Ai", "37"),
Pair("Shounen", "16"),
Pair("Shounen Ai", "51"),
Pair("Slice of Life", "19"),
Pair("Sovrannaturale", "22"),
Pair("Spaziale", "48"),
Pair("Splatter", "15"),
Pair("Sport", "41"),
Pair("Storico", "17"),
Pair("Superpoteri", "9"),
Pair("Thriller", "4"),
Pair("Vampiri", "33"),
Pair("Videogame", "44"),
Pair("Yaoi", "50"),
Pair("Yuri", "46"),
)
private val COUNTRY_LIST = arrayOf(
Pair("Corea del Sud", "KR"),
Pair("Cina", "CN"),
Pair("Hong Kong", "HK"),
Pair("Filippine", "PH"),
Pair("Giappone", "JP"),
Pair("Taiwan", "TW"),
Pair("Thailandia", "TH"),
)
private val TYPE_LIST = arrayOf(
Pair("Serie", "1"),
Pair("Movie", "2"),
Pair("OVA", "3"),
Pair("ONA", "4"),
Pair("Special", "5"),
)
private val STUDIO_LIST = arrayOf(
Pair("2:10 AM Animation", "190"),
Pair("5 Inc.", "309"),
Pair("8bit", "17"),
Pair("A-1 Picture", "11"),
Pair("Acca Effe", "180"),
Pair("A.C.G.T.", "77"),
Pair("Actas", "153"),
Pair("AIC ASTA", "150"),
Pair("AIC Build", "46"),
Pair("AIC Classic", "99"),
Pair("AIC Plus+", "26"),
Pair("AIC Spirits", "128"),
Pair("Ajia-Do", "39"),
Pair("Akatsuki", "289"),
Pair("Albacrow", "229"),
Pair("Anima&Co", "161"),
Pair("Animation Planet", "224"),
Pair("Animax", "103"),
Pair("Anpro", "178"),
Pair("APPP", "220"),
Pair("AQUA ARIS", "245"),
Pair("A-Real", "211"),
Pair("ARECT", "273"),
Pair("Arms", "33"),
Pair("Artland", "81"),
Pair("Arvo Animation", "239"),
Pair("Asahi Production", "160"),
Pair("Ashi Production", "307"),
Pair("ASK Animation Studio", "296"),
Pair("Asread", "76"),
Pair("Atelier Pontdarc", "298"),
Pair("AtelierPontdarc", "271"),
Pair("AXsiZ", "70"),
Pair("Bakken Record", "195"),
Pair("Bandai Namco Pictures", "108"),
Pair("Barnum Studio", "191"),
Pair("B.CMAY PICTURES", "135"),
Pair("Bee Media", "262"),
Pair("Bee Train", "98"),
Pair("Bibury Animation Studios", "139"),
Pair("Big FireBird Animation", "141"),
Pair("blade", "212"),
Pair("Bones", "22"),
Pair("Bouncy", "174"),
Pair("Brain's Base", "18"),
Pair("Bridge", "88"),
Pair("B&T", "193"),
Pair("Buemon", "236"),
Pair("BUG FILMS", "314"),
Pair("Bushiroad", "249"),
Pair("C2C", "126"),
Pair("Chaos Project", "247"),
Pair("Charaction", "250"),
Pair("Children's Playground Entertainment", "184"),
Pair("CLAP", "292"),
Pair("CloverWorks", "51"),
Pair("Colored Pencil Animation", "268"),
Pair("CoMix Wave Films", "83"),
Pair("Connect", "185"),
Pair("Craftar Studios", "146"),
Pair("Creators in Pack", "84"),
Pair("C-Station", "72"),
Pair("CyberConnect2", "217"),
Pair("CygamesPictures", "233"),
Pair("DandeLion Animation Studio", "116"),
Pair("Daume", "102"),
Pair("David Production", "73"),
Pair("De Mas & Partners", "207"),
Pair("Diomedea", "21"),
Pair("DLE", "155"),
Pair("DMM.futureworks", "241"),
Pair("DMM pictures", "248"),
Pair("Doga Kobo", "50"),
Pair("domerica", "302"),
Pair("Drive", "226"),
Pair("DR Movie", "113"),
Pair("drop", "130"),
Pair("Dynamo Pictures", "231"),
Pair("E&H Production", "333"),
Pair("EKACHI EPILKA", "151"),
Pair("Emon Animation Company", "149"),
Pair("Emon, Blade", "123"),
Pair("EMT²", "90"),
Pair("Encourage Films", "100"),
Pair("ENGI", "158"),
Pair("evg", "322"),
Pair("EXNOA", "274"),
Pair("Ezo'la", "35"),
Pair("Fanworks", "121"),
Pair("feel.", "37"),
Pair("Felix Film", "163"),
Pair("Frederator Studios", "147"),
Pair("Fugaku", "326"),
Pair("Funimation", "106"),
Pair("Gainax", "43"),
Pair("Gainax Kyoto", "225"),
Pair("Gallop", "109"),
Pair("Gambit", "272"),
Pair("G-angle", "222"),
Pair("Garden Culture", "324"),
)
private val STATUS_LIST = arrayOf(
Pair("Completato", "1"),
Pair("In corso", "2"),
Pair("Sospeso", "3"),
Pair("Annunciato", "4"),
Pair("Non rilasciato", "5"),
)
private val LANGUAGE_LIST = arrayOf(
Pair("Doppiato", "2"),
Pair("RAW", "3"),
Pair("Sottotitolato", "1"),
)
private val ORDER_LIST = arrayOf(
Pair("Rilevanza", "1"),
Pair("Modificato di recente", "2"),
Pair("Aggiunto di recente", "3"),
Pair("Data di rilascio", "4"),
Pair("Nome", "5"),
Pair("Voto", "6"),
Pair("Visualizzazioni", "7"),
Pair("Episodi", "8"),
Pair("Casuale", "9"),
)
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.it.aniplay
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://aniplay.co/series/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AniPlayUrlActivity : 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", "${AniPlay.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,68 @@
package eu.kanade.tachiyomi.animeextension.it.aniplay.dto
import eu.kanade.tachiyomi.animesource.model.SAnime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PopularResponseDto(
val data: List<PopularAnimeDto>,
val pagination: PaginationDto,
)
@Serializable
data class PopularAnimeDto(
val id: Int,
@SerialName("title") val name: String,
private val cover: String? = null,
private val main_image: String? = null,
) {
fun toSAnime() = SAnime.create().apply {
url = "/series/$id"
title = name
thumbnail_url = cover ?: main_image
}
}
@Serializable
data class PaginationDto(val page: Int, val pageCount: Int) {
val hasNextPage get() = page < pageCount
}
@Serializable
data class LatestItemDto(val serie: List<PopularAnimeDto>)
@Serializable
data class AnimeInfoDto(
val title: String,
val description: String? = null,
val alternative: String? = null,
val status: String? = null,
val origin: String? = null,
val release_day: String? = null,
val genres: List<NameDto> = emptyList(),
val studios: List<NameDto> = emptyList(),
private val cover: String? = null,
private val main_image: String? = null,
) {
val thumbnailUrl = cover ?: main_image
}
@Serializable
data class NameDto(val name: String)
@Serializable
data class EpisodeDto(
val id: Int,
val title: String? = null,
val number: String? = null,
val release_date: String? = null,
)
@Serializable
data class VideoDto(
private val download_link: String? = null,
private val streaming_link: String? = null,
) {
val videoLink = streaming_link ?: download_link!!
}

View file

@ -0,0 +1,8 @@
ext {
extName = 'Hentai Saturn'
extClass = '.HentaiSaturn'
extVersionCode = 5
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,435 @@
package eu.kanade.tachiyomi.animeextension.it.hentaisaturn
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.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.util.asJsoup
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class HentaiSaturn : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "HentaiSaturn"
override val baseUrl = "https://www.hentaisaturn.com"
override val lang = "it"
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeSelector(): String = "div.col-md-2.float-left.hentai-img-box-col.hentai-padding-top"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/toplist")
private fun formatTitle(titlestring: String): String = titlestring.replace("(ITA) ITA", "Dub ITA").replace("(ITA)", "Dub ITA").replace("Sub ITA", "")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.title = formatTitle(element.selectFirst("a img.img-fluid.w-100.rounded.hentai-img")!!.attr("title"))
anime.thumbnail_url = element.selectFirst("a img.img-fluid.w-100.rounded.hentai-img")!!.attr("src")
return anime
}
override fun popularAnimeNextPageSelector(): String = "li.page-item.active:not(li:last-child)"
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return document.select(episodeListSelector()).map { episodeFromElement(it) }.reversed()
}
override fun episodeListSelector() = "div.btn-group.episodes-button.episodi-link-button"
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(element.selectFirst("a.btn.btn-dark.mb-1.bottone-ep")!!.attr("href"))
val epText = element.selectFirst("a.btn.btn-dark.mb-1.bottone-ep")!!.text()
val epNumber = epText.substringAfter("Episodio ")
if (epNumber.contains("-", true)) {
episode.episode_number = epNumber.substringBefore("-").toFloat()
} else {
episode.episode_number = epNumber.toFloat()
}
episode.name = epText
return episode
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val standardVideos = videosFromElement(document)
val videoList = mutableListOf<Video>()
videoList.addAll(standardVideos)
return videoList
}
override fun videoListRequest(episode: SEpisode): Request {
val episodePage = client.newCall(GET(baseUrl + episode.url)).execute().asJsoup()
val watchUrl = episodePage.select("a[href*=/watch]").attr("href")
return GET("$watchUrl&s=alt")
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
private fun videosFromElement(document: Document): List<Video> {
val url = if (document.html().contains("jwplayer(")) {
document.html().substringAfter("file: \"").substringBefore("\"")
} else {
document.select("source").attr("src")
}
val referer = document.location()
return if (url.endsWith("playlist.m3u8")) {
val playlist = client.newCall(GET(url)).execute().body.string()
val linkRegex = """(?<=\n)./.+""".toRegex()
val qualityRegex = """(?<=RESOLUTION=)\d+x\d+""".toRegex()
val qualities = qualityRegex.findAll(playlist).map {
it.value.substringAfter('x') + "p"
}.toList()
val videoLinks = linkRegex.findAll(playlist).map {
url.substringBefore("playlist.m3u8") + it.value.substringAfter("./")
}.toList()
videoLinks.mapIndexed { i, link ->
Video(
link,
qualities[i],
link,
)
}
} else {
listOf(
Video(
url,
"Qualità predefinita",
url,
headers = Headers.headersOf("Referer", referer),
),
)
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!!
val qualityList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
qualityList.add(preferred, video)
preferred++
} else {
qualityList.add(video)
}
}
return qualityList
}
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
if (filterSearch) {
// filter search
anime.setUrlWithoutDomain(element.selectFirst("div.card.mb-4.shadow-sm a")!!.attr("href"))
anime.title = formatTitle(element.selectFirst("div.card.mb-4.shadow-sm a")!!.attr("title"))
anime.thumbnail_url = element.selectFirst("div.card.mb-4.shadow-sm a img.new-hentai")!!.attr("src")
} else {
// word search
anime.setUrlWithoutDomain(element.selectFirst("a.thumb.image-wrapper")!!.attr("href"))
anime.title = formatTitle(element.selectFirst("a.thumb.image-wrapper img.rounded.copertina-archivio")!!.attr("alt"))
anime.thumbnail_url = element.select("a.thumb.image-wrapper img.rounded.copertina-archivio").attr("src")
}
return anime
}
override fun searchAnimeNextPageSelector(): String = "li.page-item.active:not(li:last-child)"
private var filterSearch = false
override fun searchAnimeSelector(): String {
return if (filterSearch) {
"div.hentai-card-newhentai.main-hentai-card" // filter search
} else {
"div.item-archivio" // regular search
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val parameters = getSearchParameters(filters)
return if (parameters.isEmpty()) {
filterSearch = false
GET("$baseUrl/hentailist?search=$query") // regular search
} else {
filterSearch = true
GET("$baseUrl/filter?$parameters&page=$page") // with filters
}
}
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = formatTitle(document.select("div.container.hentai-title-as.mb-3.w-100 b").text())
val tempDetails = document.select("div.container.shadow.rounded.bg-dark-as-box.mb-3.p-3.w-100.text-white").text()
val indexA = tempDetails.indexOf("Stato:")
anime.author = tempDetails.substring(7, indexA).trim()
val indexS1 = tempDetails.indexOf("Stato:") + 6
val indexS2 = tempDetails.indexOf("Data di uscita:")
anime.status = parseStatus(tempDetails.substring(indexS1, indexS2).trim())
anime.genre = document.select("div.container.shadow.rounded.bg-dark-as-box.mb-3.p-3.w-100 a.badge.badge-dark.generi-as.mb-1").joinToString { it.text() }
anime.thumbnail_url = document.selectFirst("img.img-fluid.cover-anime.rounded")!!.attr("src")
val alterTitle = formatTitle(document.selectFirst("div.box-trasparente-alternativo.rounded")!!.text()).replace("Dub ITA", "").trim()
val description1 = document.selectFirst("div#trama div#shown-trama")?.ownText()
val description2 = document.selectFirst("div#full-trama.d-none")?.ownText()
when {
description1 == null -> {
anime.description = description2
}
description2 == null -> {
anime.description = description1
}
description1.length > description2.length -> {
anime.description = description1
}
else -> {
anime.description = description2
}
}
if (!anime.title.contains(alterTitle, true)) {
anime.description = anime.description + "\n\nTitolo Alternativo: " + alterTitle
}
return anime
}
private fun parseStatus(statusString: String): Int {
return when {
statusString.contains("In corso") -> {
SAnime.ONGOING
}
statusString.contains("Finito") -> {
SAnime.COMPLETED
}
else -> {
SAnime.UNKNOWN
}
}
}
override fun latestUpdatesSelector(): String = "div.card.mb-4.shadow-sm"
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.title = formatTitle(element.selectFirst("a")!!.attr("title"))
anime.thumbnail_url = element.selectFirst("a img.new-hentai")!!.attr("src")
return anime
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newest?page=$page")
override fun latestUpdatesNextPageSelector(): String = "li.page-item.active:not(li:last-child)"
// Filters
internal class Genre(val id: String) : AnimeFilter.CheckBox(id)
private class GenreList(genres: List<Genre>) : AnimeFilter.Group<Genre>("Generi", genres)
private fun getGenres() = listOf(
Genre("3D"),
Genre("Ahegao"),
Genre("Anal"),
Genre("BDSM"),
Genre("Big Boobs"),
Genre("Blow Job"),
Genre("Bondage"),
Genre("Boob Job"),
Genre("Censored"),
Genre("Comedy"),
Genre("Cosplay"),
Genre("Creampie"),
Genre("Dark Skin"),
Genre("Erotic Game"),
Genre("Facial"),
Genre("Fantasy"),
Genre("Filmed"),
Genre("Foot Job"),
Genre("Futanari"),
Genre("Gangbang"),
Genre("Glasses"),
Genre("Hand Job"),
Genre("Harem"),
Genre("HD"),
Genre("Horror"),
Genre("Incest"),
Genre("Inflation"),
Genre("Lactation"),
Genre("Loli"),
Genre("Maid"),
Genre("Masturbation"),
Genre("Milf"),
Genre("Mind Break"),
Genre("Mind Control"),
Genre("Monster"),
Genre("NTR"),
Genre("Nurse"),
Genre("Orgy"),
Genre("Plot"),
Genre("POV"),
Genre("Pregnant"),
Genre("Public Sex"),
Genre("Rape"),
Genre("Reverse Rape"),
Genre("Rimjob"),
Genre("Scat"),
Genre("School Girl"),
Genre("Shota"),
Genre("Softcore"),
Genre("Swimsuit"),
Genre("Teacher"),
Genre("Tentacle"),
Genre("Threesome"),
Genre("Toys"),
Genre("Trap"),
Genre("Tsundere"),
Genre("Ugly Bastard"),
Genre("Uncensored"),
Genre("Vanilla"),
Genre("Virgin"),
Genre("Watersports"),
Genre("X-Ray"),
Genre("Yaoi"),
Genre("Yuri"),
)
internal class Year(val id: String) : AnimeFilter.CheckBox(id)
private class YearList(years: List<Year>) : AnimeFilter.Group<Year>("Anno di Uscita", years)
private fun getYears() = listOf(
Year("1996"),
Year("1997"),
Year("1999"),
Year("2000"),
Year("2001"),
Year("2002"),
Year("2003"),
Year("2004"),
Year("2005"),
Year("2006"),
Year("2007"),
Year("2008"),
Year("2009"),
Year("2010"),
Year("2011"),
Year("2012"),
Year("2013"),
Year("2014"),
Year("2015"),
Year("2016"),
Year("2017"),
Year("2018"),
Year("2019"),
Year("2020"),
Year("2021"),
)
internal class State(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class StateList(states: List<State>) : AnimeFilter.Group<State>("Stato", states)
private fun getStates() = listOf(
State("0", "In corso"),
State("1", "Finito"),
State("2", "Non rilasciato"),
State("3", "Droppato"),
)
internal class Lang(val id: String, name: String) : AnimeFilter.CheckBox(name)
private class LangList(langs: List<Lang>) : AnimeFilter.Group<Lang>("Lingua", langs)
private fun getLangs() = listOf(
Lang("0", "Subbato"),
Lang("1", "Doppiato"),
)
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Ricerca per titolo ignora i filtri e viceversa"),
GenreList(getGenres()),
YearList(getYears()),
StateList(getStates()),
LangList(getLangs()),
)
private fun getSearchParameters(filters: AnimeFilterList): String {
var totalstring = ""
var variantgenre = 0
var variantstate = 0
var variantyear = 0
filters.forEach { filter ->
when (filter) {
is GenreList -> { // ---Genre
filter.state.forEach { Genre ->
if (Genre.state) {
totalstring = totalstring + "&categories%5B" + variantgenre.toString() + "%5D=" + Genre.id
variantgenre++
}
}
}
is YearList -> { // ---Year
filter.state.forEach { Year ->
if (Year.state) {
totalstring = totalstring + "&years%5B" + variantyear.toString() + "%5D=" + Year.id
variantyear++
}
}
}
is StateList -> { // ---State
filter.state.forEach { State ->
if (State.state) {
totalstring = totalstring + "&states%5B" + variantstate.toString() + "%5D=" + State.id
variantstate++
}
}
}
is LangList -> { // ---Lang
filter.state.forEach { Lang ->
if (Lang.state) {
totalstring = totalstring + "&language%5B0%5D=" + Lang.id
}
}
}
else -> {}
}
}
return totalstring
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Qualità preferita"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p", "144p")
entryValues = arrayOf("1080", "720", "480", "360", "240", "144")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View file

@ -0,0 +1,432 @@
package eu.kanade.tachiyomi.animeextension.it.streamingcommunity
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.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
class StreamingCommunity : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "StreamingCommunity"
override val baseUrl = "https://streamingcommunity.forum"
override val lang = "it"
override val supportsLatest = true
override val client: OkHttpClient = network.client
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 {
return if (page == 1) {
GET("$baseUrl/browse/trending", headers)
} else {
val apiHeaders = headers.newBuilder()
.add("Accept", "application/json, text/plain, */*")
.add("Host", baseUrl.toHttpUrl().host)
.add("Referer", "$baseUrl/browse/trending")
.build()
GET("$baseUrl/api/browse/trending?offset=${(page - 1) * 60}", headers = apiHeaders)
}
}
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = if (response.request.url.encodedPath.startsWith("/api/")) {
json.decodeFromString(response.body.string())
} else {
val data = response.asJsoup().getData()
json.decodeFromString<ShowsResponse>(data).props
}
val imageUrl = "https://cdn.${baseUrl.toHttpUrl().host}/images/"
val animeList = parsed.titles.map { item ->
SAnime.create().apply {
title = item.name
url = "${item.id}-${item.slug}"
thumbnail_url = item.images.firstOrNull {
it.type == "poster"
}?.let {
imageUrl + it.filename
} ?: item.images.firstOrNull {
it.type == "cover"
}?.let {
imageUrl + it.filename
} ?: item.images.firstOrNull {
it.type == "background"
}?.let {
imageUrl + it.filename
}
}
}
val hasNextPage = response.request.url.queryParameter("offset")
?.toIntOrNull()
?.let { it < 120 } ?: true && animeList.size == 60
return AnimesPage(animeList, hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
return if (page == 1) {
GET("$baseUrl/browse/latest", headers)
} else {
val apiHeaders = headers.newBuilder()
.add("Accept", "application/json, text/plain, */*")
.add("Host", baseUrl.toHttpUrl().host)
.add("Referer", "$baseUrl/browse/trending")
.build()
GET("$baseUrl/api/browse/latest?offset=${(page - 1) * 60}", headers = apiHeaders)
}
}
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val genreFilter = filters.find { it is GenreFilter } as GenreFilter
val slug = if (genreFilter.state != 0) {
"browse/genre?g=${URLEncoder.encode(genreFilter.toUriPart(), "utf-8")}"
} else {
"search?q=$query"
}
return if (page == 1) {
GET("$baseUrl/$slug")
} else {
val apiHeaders = headers.newBuilder()
.add("Accept", "application/json, text/plain, */*")
.add("Host", baseUrl.toHttpUrl().host)
.add("Referer", "$baseUrl/$slug")
.build()
GET("$baseUrl/api/$slug&offset=${(page - 1) * 60}", headers = apiHeaders)
}
}
override fun searchAnimeParse(response: Response): AnimesPage {
val path = response.request.url.encodedPath
val parsed = if (path.startsWith("/api/")) {
if (path.contains("search")) {
json.decodeFromString<SearchAPIResponse>(response.body.string()).data
} else {
json.decodeFromString<GenreAPIResponse>(response.body.string()).titles
}
} else {
val data = response.asJsoup().getData()
json.decodeFromString<ShowsResponse>(data).props.titles
}
val imageUrl = "https://cdn.${baseUrl.toHttpUrl().host}/images/"
val animeList = parsed.map { item ->
SAnime.create().apply {
title = item.name
url = "${item.id}-${item.slug}"
thumbnail_url = item.images.firstOrNull {
it.type == "poster"
}?.let {
imageUrl + it.filename
} ?: item.images.firstOrNull {
it.type == "cover"
}?.let {
imageUrl + it.filename
} ?: item.images.firstOrNull {
it.type == "background"
}?.let {
imageUrl + it.filename
}
}
}
val hasNextPage = response.request.url.queryParameter("offset")
?.toIntOrNull()
?.let { it < 120 } ?: true && animeList.size == 60
return AnimesPage(animeList, hasNextPage)
}
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request = GET("$baseUrl/titles/${anime.url}", headers)
override fun animeDetailsParse(response: Response): SAnime {
val parsed = json.decodeFromString<SingleShowResponse>(
response.asJsoup().getData(),
).props.title!!
return SAnime.create().apply {
description = parsed.plot
status = parseStatus(parsed.status)
genre = parsed.genres?.joinToString(", ") { it.name }
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request = GET("$baseUrl/titles/${anime.url}", headers)
override fun episodeListParse(response: Response): List<SEpisode> {
val parsed = json.decodeFromString<SingleShowResponse>(response.asJsoup().getData())
val data = parsed.props
val episodeList = mutableListOf<SEpisode>()
if (data.loadedSeason == null) {
episodeList.add(
SEpisode.create().apply {
name = "Film"
episode_number = 1F
url = data.title!!.id.toString()
},
)
} else {
data.title!!.seasons.forEach { season ->
val episodeData = if (season.id == data.loadedSeason.id) {
data.loadedSeason.episodes
} else {
val inertiaHeaders = headers.newBuilder()
.add("Accept", "text/html, application/xhtml+xml")
.add("Content-Type", "application/json")
.add("Host", baseUrl.toHttpUrl().host)
.add("Referer", "${response.request.url}/")
.add("X-Inertia", "true")
.add("X-Inertia-Partial-Component", "Titles/Title")
.add("X-Inertia-Partial-Data", "loadedSeason,flash")
.add("X-Inertia-Version", parsed.version!!)
.add("X-Requested-With", "XMLHttpRequest")
.build()
val body = client.newCall(
GET("${response.request.url}/stagione-${season.number}", headers = inertiaHeaders),
).execute().body.string()
json.decodeFromString<SingleShowResponse>(body).props.loadedSeason!!.episodes
}
episodeData.forEach { episode ->
episodeList.add(
SEpisode.create().apply {
name = "Stagione ${season.number} episodio ${episode.number} - ${episode.name}"
episode_number = episode.number.toFloat()
url = "${data.title.id}?episode_id=${episode.id}&next_episode=1"
},
)
}
}
}
return episodeList.reversed()
}
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val videoList = mutableListOf<Video>()
val doc =
client
.newCall(
GET("$baseUrl/iframe/${episode.url}", headers),
).execute()
.asJsoup()
val iframeUrl =
doc.selectFirst("iframe[src]")?.attr("abs:src")
?: error("Failed to extract iframe")
val iframeHeaders =
headers
.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", iframeUrl.toHttpUrl().host)
.add("Referer", "$baseUrl/")
.build()
val iframe =
client
.newCall(
GET(iframeUrl, headers = iframeHeaders),
).execute()
.asJsoup()
val script = iframe.selectFirst("script:containsData(masterPlaylist)")!!.data().replace("\n", "\t")
var playlistUrl = PLAYLIST_URL_REGEX.find(script)!!.groupValues[1]
val filename = playlistUrl.substringAfterLast("/")
if (!filename.endsWith(".m3u8")) {
playlistUrl = playlistUrl.replace(filename, filename + ".m3u8")
}
val expires = EXPIRES_REGEX.find(script)!!.groupValues[1]
val token = TOKEN_REGEX.find(script)!!.groupValues[1]
// Get subtitles
val masterPlUrl = "$playlistUrl?token=$token&expires=$expires&n=1"
val masterPl =
client
.newCall(GET(masterPlUrl))
.execute()
.body
.string()
val subList =
SUBTITLES_REGEX.findAll(masterPl)
.map {
Track(it.groupValues[2], it.groupValues[1])
}.toList()
TOKEN_QUALITY_REGEX.findAll(script).forEach { match ->
val quality = match.groupValues[1]
val videoUrl =
buildString {
append(playlistUrl)
append("?type=video&rendition=")
append(quality)
append("&token=")
append(match.groupValues[2])
append("&expires=$expires")
append("&n=1")
}
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subList))
}
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return videoList.sort()
}
override fun videoListRequest(episode: SEpisode): Request = throw Exception("Not used")
override fun videoListParse(response: Response): List<Video> = throw Exception("Not used")
// ============================= Utilities ==============================
private fun Document.getData(): String {
return this.selectFirst("div#app[data-page]")!!
.attr("data-page")
.replace("&quot;", "\"")
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
private fun parseStatus(statusString: String?): Int {
return when (statusString) {
"Ended" -> SAnime.COMPLETED
"Released" -> SAnime.COMPLETED
"Returning Series" -> SAnime.ONGOING
"Canceled" -> SAnime.CANCELLED
else -> SAnime.UNKNOWN
}
}
companion object {
private val PLAYLIST_URL_REGEX = Regex("""url: ?'(.*?)'""")
private val EXPIRES_REGEX = Regex("""'expires': ?'(\d+)'""")
private val TOKEN_REGEX = Regex("""'token': ?'([\w-]+)'""")
private val TOKEN_QUALITY_REGEX = Regex("""'token(\d+p?)': ?'([\w-]+)'""")
private val SUBTITLES_REGEX = Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "720"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
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)
}
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter(),
)
private class GenreFilter : UriPartFilter(
"Genres",
arrayOf(
Pair("<select>", ""),
Pair("Action & Adventure", "Action & Adventure"),
Pair("Animazione", "Animazione"),
Pair("Avventura", "Avventura"),
Pair("Azione", "Azione"),
Pair("Commedia", "Commedia"),
Pair("Crime", "Crime"),
Pair("Documentario", "Documentario"),
Pair("Dramma", "Dramma"),
Pair("Famiglia", "Famiglia"),
Pair("Fantascienza", "Fantascienza"),
Pair("Fantasy", "Fantasy"),
Pair("Guerra", "Guerra"),
Pair("Horror", "Horror"),
Pair("Kids", "Kids"),
Pair("Korean drama", "Korean drama"),
Pair("Mistero", "Mistero"),
Pair("Musica", "Musica"),
Pair("Reality", "Reality"),
Pair("Romance", "Romance"),
Pair("Sci-Fi & Fantasy", "Sci-Fi & Fantasy"),
Pair("Soap", "Soap"),
Pair("Storia", "Storia"),
Pair("televisione film", "televisione film"),
Pair("Thriller", "Thriller"),
Pair("War & Politics", "War & Politics"),
Pair("Western", "Western"),
),
)
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
}
}

View file

@ -0,0 +1,92 @@
package eu.kanade.tachiyomi.animeextension.it.streamingcommunity
import kotlinx.serialization.Serializable
@Serializable
data class ShowsResponse(
val props: PropObject,
)
@Serializable
data class PropObject(
val titles: List<TitleObject>,
) {
@Serializable
data class TitleObject(
val id: Int,
val slug: String,
val name: String,
val images: List<ImageObject>,
) {
@Serializable
data class ImageObject(
val filename: String,
val type: String,
)
}
}
@Serializable
data class SingleShowResponse(
val props: SingleShowObject,
val version: String? = null,
) {
@Serializable
data class SingleShowObject(
val title: ShowObject? = null,
val loadedSeason: LoadedSeasonObject? = null,
) {
@Serializable
data class ShowObject(
val id: Int,
val plot: String? = null,
val status: String? = null,
val seasons: List<SeasonObject>,
val genres: List<GenreObject>? = null,
) {
@Serializable
data class SeasonObject(
val id: Int,
val number: Int,
)
@Serializable
data class GenreObject(
val name: String,
)
}
@Serializable
data class LoadedSeasonObject(
val id: Int,
val episodes: List<EpisodeObject>,
) {
@Serializable
data class EpisodeObject(
val id: Int,
val number: Int,
val name: String,
)
}
}
}
@Serializable
data class SearchAPIResponse(
val data: List<PropObject.TitleObject>,
)
@Serializable
data class GenreAPIResponse(
val titles: List<PropObject.TitleObject>,
)
@Serializable
data class VideoResponse(
val props: VideoPropObject,
) {
@Serializable
data class VideoPropObject(
val embedUrl: String,
)
}

View file

@ -0,0 +1,14 @@
ext {
extName = 'Toonitalia'
extClass = '.Toonitalia'
extVersionCode = 21
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:playlist-utils'))
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,299 @@
package eu.kanade.tachiyomi.animeextension.it.toonitalia
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.it.toonitalia.extractors.MaxStreamExtractor
import eu.kanade.tachiyomi.animeextension.it.toonitalia.extractors.StreamZExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class Toonitalia : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Toonitalia"
override val baseUrl = "https://toonitalia.green"
override val lang = "it"
override val supportsLatest = false
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page", headers)
override fun popularAnimeSelector() = "#primary > main#main > article"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
element.selectFirst("h2 > a")!!.run {
title = text()
setUrlWithoutDomain(attr("href"))
}
thumbnail_url = element.selectFirst("img")!!.attr("src")
}
override fun popularAnimeNextPageSelector() = "nav.pagination a.next"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
// =============================== Search ===============================
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val isNormalSearch = document.location().contains("/?s=")
val animes = if (isNormalSearch) {
document.select(searchAnimeSelector()).map(::searchAnimeFromElement)
} else {
document.select(searchIndexAnimeSelector()).map(::searchIndexAnimeFromElement)
}
val hasNextPage = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNextPage)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return if (query.isNotBlank()) {
GET("$baseUrl/page/$page/?s=$query", headers = headers)
} else {
val url = "$baseUrl".toHttpUrl().newBuilder().apply {
filters.filterIsInstance<IndexFilter>()
.firstOrNull()
?.also { addPathSegment(it.toUriPart()) }
}
val newUrl = url.toString() + "/?lcp_page0=$page#lcp_instance_0"
GET(newUrl, headers)
}
}
override fun searchAnimeSelector() = popularAnimeSelector()
private fun searchIndexAnimeSelector() = "div.entry-content > ul.lcp_catlist > li"
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
private fun searchIndexAnimeFromElement(element: Element) = SAnime.create().apply {
element.selectFirst("a")!!.run {
title = text()
setUrlWithoutDomain(attr("href"))
}
}
override fun searchAnimeNextPageSelector() =
"nav.navigation div.nav-previous, " + // Normal search
"ul.lcp_paginator > li > a.lcp_nextlink" // Index search
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("h1.entry-title")!!.text()
thumbnail_url = document.selectFirst("header.entry-header img")!!.attr("abs:src")
// Cursed sources should have cursed code!
description = document.selectFirst("article > div.entry-content")!!
.also { it.select("center").remove() } // Remove unnecessary data
.wholeText()
.replace(",", ", ").replace(" ", " ") // Fix text
.lines()
.map(String::trim)
.filterNot { it.startsWith("Titolo:") }
.also { lines ->
genre = lines.firstOrNull { it.startsWith("Genere:") }
?.substringAfter("Genere: ")
}
.joinToString("\n")
.substringAfter("Trama: ")
}
// ============================== Episodes ==============================
private val episodeNumRegex by lazy { Regex("\\s(\\d+x\\d+)\\s?") }
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.asJsoup()
val url = doc.location()
if ("/film-anime/" in url) {
return listOf(
SEpisode.create().apply {
setUrlWithoutDomain("$url#0")
episode_number = 1F
name = doc.selectFirst("h1.entry-title")!!.text()
},
)
}
val epNames = doc.select(episodeListSelector() + ">td:not(:has(a))").eachText()
return epNames.mapIndexed { index, item ->
SEpisode.create().apply {
setUrlWithoutDomain("$url#$index")
val (season, episode) = episodeNumRegex.find(item)
?.groupValues
?.last()
?.split("x")
?: listOf("01", "01")
name = "Stagione $season - Episodi $episode"
episode_number = "$season.${episode.padStart(3, '0')}".toFloatOrNull() ?: 1F
}
}.reversed()
}
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
override fun episodeListSelector() = "article > div.entry-content table tr:has(a)"
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val episodeNumber = response.request.url.fragment!!.toInt()
val episode = document.select(episodeListSelector())
.getOrNull(episodeNumber)
?: return emptyList()
return episode.select("a").flatMap {
runCatching {
val url = it.attr("href")
val hosterUrl = when {
url.contains("uprot.net") -> bypassUprot(url)
else -> url
}
hosterUrl?.let(::extractVideos)
}.getOrNull() ?: emptyList()
}
}
private val voeExtractor by lazy { VoeExtractor(client) }
private val streamZExtractor by lazy { StreamZExtractor(client) }
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val maxStreamExtractor by lazy { MaxStreamExtractor(client, headers) }
private fun extractVideos(url: String): List<Video> =
when {
"https://voe.sx" in url -> voeExtractor.videosFromUrl(url)
"https://streamtape" in url -> streamTapeExtractor.videoFromUrl(url)?.let(::listOf)
"https://maxstream" in url -> maxStreamExtractor.videosFromUrl(url)
"https://streamz" in url || "streamz.cc" in url -> {
streamZExtractor.videoFromUrl(url, "StreamZ")?.let(::listOf)
}
else -> null
} ?: emptyList()
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================== Filters ===============================
override fun getFilterList() = AnimeFilterList(
AnimeFilter.Header("NOTA: ignorato se si utilizza la ricerca di testo!"),
AnimeFilter.Separator(),
IndexFilter(getIndexList()),
)
private class IndexFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Indice", vals)
private fun getIndexList() = arrayOf(
Pair("<selezionare>", ""),
Pair("Lista Anime e Cartoni", "lista-anime-e-cartoni"),
Pair("Lista Film Anime", "lista-film-anime"),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(server) },
{ it.quality.contains(quality) },
),
).reversed()
}
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)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = PREF_SERVER_TITLE
entries = PREF_SERVER_ENTRIES
entryValues = PREF_SERVER_VALUES
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)
}
// ============================= Utilities ==============================
private fun bypassUprot(url: String): String? =
client.newCall(GET(url, headers)).execute()
.asJsoup()
.selectFirst("a:has(button.button.is-info)")
?.attr("href")
companion object {
private const val PREF_QUALITY_KEY = "preferred_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", "240p", "80p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360", "240", "80")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Preferred server"
private const val PREF_SERVER_DEFAULT = "StreamZ"
private val PREF_SERVER_ENTRIES = arrayOf("StreamZ", "VOE", "StreamZ Sub-Ita", "VOE Sub-Ita", "MaxStream", "StreamTape")
private val PREF_SERVER_VALUES = PREF_SERVER_ENTRIES
}
}

View file

@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.animeextension.it.toonitalia.extractors
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 MaxStreamExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url, headers)).execute()
.asJsoup()
val location = doc.location()
if (location.contains("/dd/")) return videosFromCast(location.replace("/dd/", "/cast3/"))
val scripts = doc.select(SCRIPT_SELECTOR).ifEmpty {
return emptyList()
}
val playlists = scripts.mapNotNull {
JsUnpacker.unpackAndCombine(it.data())
?.substringAfter("src:\"", "")
?.substringBefore('"', "")
?.takeIf(String::isNotBlank)
}
return playlists.flatMap { link ->
playlistUtils.extractFromHls(link, location, videoNameGen = { "MaxStream - $it" })
}
}
private fun videosFromCast(url: String): List<Video> {
val script = client.newCall(GET(url, headers)).execute()
.asJsoup()
.selectFirst("script:containsData(document.write)")
?.data()
?: return emptyList()
val numberList = NUMBER_LIST_REGEX.find(script)?.groupValues?.last()
?.split(", ")
?.mapNotNull(String::toIntOrNull)
?: return emptyList()
val offset = numberList.first() - 32
val decodedData = numberList.joinToString("") {
Char(it - offset).toString()
}.trim()
val newHeaders = headers.newBuilder().set("Referer", url).build()
val newUrl = decodedData.substringAfter("get('").substringBefore("'")
val docBody = client.newCall(GET(newUrl, newHeaders)).execute()
.body.string()
val videoUrl = docBody.substringAfter(".cast('").substringBefore("'")
return listOf(Video(videoUrl, "MaxStream CAST Scarica", videoUrl, newHeaders))
}
companion object {
private const val SCRIPT_SELECTOR = "script:containsData(eval):containsData(m3u8)"
private val NUMBER_LIST_REGEX by lazy { Regex("\\[(.*)\\]") }
}
}

View file

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.animeextension.it.toonitalia.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class StreamZExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String, quality: String): Video? {
// o.href = "
val response = client.newCall(GET(url)).execute()
val link = response.request.url.toString()
val dllpart = if (url.contains("streamcrypt.net")) {
response.body.string().substringAfter("o.href = \"")
.substringBefore("\"").substringAfter("download")
} else {
link.substringAfter("/y")
}
val videoUrl = client.newCall(
GET(
"https://get.streamz.tw/getlink-$dllpart.dll",
headers = Headers.headersOf("referer", "https://streamz.ws/", "accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5", "range", "bytes=0-"),
),
)
.execute().request.url.toString()
return Video(url, quality, videoUrl)
}
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View file

@ -0,0 +1,679 @@
package eu.kanade.tachiyomi.animeextension.it.vvvvid
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.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import kotlin.text.isLetter
class VVVVID : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "VVVVID"
override val baseUrl = "https://www.vvvvid.it"
private var connId = ""
private var sessionId = ""
private var currentPrimaryPage = "anime"
private var currentChannelId = ""
override val lang = "it"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private fun getConnId() {
val headers = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Accept-Language", "en-US,en;q=0.5")
.add("Content-Type", "application/json")
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/channel/0/you")
.add("X-Requested-With", "XMLHttpRequest")
.build()
val body = """
{
"action": "login",
"email": "",
"password": "",
"facebookParams": "",
"isIframe": false,
"mobile": false,
"hls": true,
"dash": true,
"flash": false,
"webm": true,
"wv+mp4": true,
"wv+webm": true,
"pr+mp4": false,
"pr+webm": false,
"fp+mp4": false,
"device_id_seed": "${getRandomIntString()}"
}
""".trimIndent().toRequestBody("application/json".toMediaType())
val response = client.newCall(
POST("$baseUrl/user/login", body = body, headers = headers),
).execute()
if (response.code != 200) error("Failed to log in")
val parsed = json.decodeFromString<LoginResponse>(response.body.string())
connId = parsed.data.conn_id
sessionId = parsed.data.sessionId
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
if (connId.isEmpty()) {
getConnId()
}
val headers = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Accept-Language", "en-US,en;q=0.5")
.add("Cookie", "JSESSIONID=$sessionId")
.add("Referer", "$baseUrl/")
.add("X-Requested-With", "XMLHttpRequest")
.build()
if (page == 1) {
updateFilters("anime", "Popolari")
}
return GET("$baseUrl/vvvvid/ondemand/anime/channel/${currentChannelId}${if (page == 1) "/last" else ""}?conn_id=$connId", headers = headers)
}
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = json.decodeFromString<AnimesResponse>(response.body.string())
val animesList = parsed.data.map { ani ->
SAnime.create().apply {
title = ani.title
thumbnail_url = ani.thumbnail
url = ani.show_id.toString()
}
}
return AnimesPage(animesList, animesList.size == 15)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
if (connId.isEmpty()) {
getConnId()
}
val headers = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Accept-Language", "en-US,en;q=0.5")
.add("Cookie", "JSESSIONID=$sessionId")
.add("Referer", "$baseUrl/")
.add("X-Requested-With", "XMLHttpRequest")
.build()
if (page == 1) {
updateFilters("anime", "Nuove")
}
return GET("$baseUrl/vvvvid/ondemand/anime/channel/${currentChannelId}${if (page == 1) "/last" else ""}?conn_id=$connId", headers = headers)
}
override fun latestUpdatesParse(response: Response): AnimesPage = popularAnimeParse(response)
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
if (connId.isEmpty()) {
getConnId()
}
if (query.isNotEmpty()) {
error("Ricerca non disponibile")
}
val headers = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Accept-Language", "en-US,en;q=0.5")
.add("Cookie", "JSESSIONID=$sessionId")
.add("Referer", "$baseUrl/")
.add("X-Requested-With", "XMLHttpRequest")
.build()
var filterStringFinal = ""
var filterCounter = 0
for (filter in filters) {
when (filter) {
is PrimaryPageFilter -> {
if (filter.selectedValue() != currentPrimaryPage) {
currentPrimaryPage = filter.selectedValue()
updateFilters(currentPrimaryPage)
throw Exception("Apri Filtri e premi reset per reimpostare i filtri")
}
}
is SubPageFilter -> {
var filterString = filter.selectedValue()
if (filterString.isNotEmpty()) {
filterStringFinal = "$filterString${if (page == 1) "/last" else ""}"
filterCounter++
}
}
is GenreFilter -> {
var filterString = filter.selectedValue()
if (filterString.isNotEmpty()) {
val (channelId, value) = filterString.split(",")
filterStringFinal = "$channelId${if (page == 1) "/last" else ""}?category=$value"
filterCounter++
}
}
is AZFilter -> {
var filterString = filter.selectedValue()
if (filterString.isNotEmpty()) {
val (channelId, value) = filterString.split(",")
filterStringFinal = "$channelId${if (page == 1) "/last" else ""}?filter=$value"
filterCounter++
}
}
else -> {}
}
}
if (filterCounter != 1) {
throw Exception("Seleziona solo un sottotipo")
}
val url = "$baseUrl/vvvvid/ondemand/$currentPrimaryPage/channel/$filterStringFinal".toHttpUrl()
.newBuilder()
.addQueryParameter("conn_id", connId)
.build()
.toString()
return GET(url, headers = headers)
}
override fun searchAnimeParse(response: Response): AnimesPage = popularAnimeParse(response)
// ============================== Filters ===============================
private open class SelectFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
fun selectedValue(): String = vals[state].second
}
private class PrimaryPageFilter(defaultOrder: String? = null) : SelectFilter(
"Seleziona la pagina principale",
arrayOf(
Pair("Anime", "anime"),
Pair("Film", "film"),
Pair("Serie TV", "series"),
Pair("Show", "show"),
Pair("Kids", "kids"),
// Pair("Sala VVVVID (Sperimentale)", "tvod"),
),
defaultOrder,
)
override fun getFilterList(): AnimeFilterList {
val filters = mutableListOf<AnimeFilter<*>>(
AnimeFilter.Header("Dopo aver modificato la pagina principale,"),
AnimeFilter.Header("premere filtro per aggiornare i filtri"),
PrimaryPageFilter(currentPrimaryPage),
AnimeFilter.Separator(),
AnimeFilter.Header("Sottotipo (selezionane uno)"),
SubPageFilter(getSubPageList()),
GenreFilter(getGenreList()),
AZFilter(getAZList()),
)
return AnimeFilterList(filters)
}
// Mutable filters
private class SubPageFilter(values: Array<Pair<String, String>>, defaultOrder: String? = null) : SelectFilter(
"Seleziona la sottopagina",
values,
defaultOrder,
)
private var subPageList: Array<Pair<String, String>>? = null
private fun getSubPageList(): Array<Pair<String, String>> {
return subPageList ?: arrayOf(
Pair("Premere reset per aggiornare i filtri", ""),
)
}
private class GenreFilter(values: Array<Pair<String, String>>) : SelectFilter(
"Generi",
values,
)
private var genreList: Array<Pair<String, String>>? = null
private fun getGenreList(): Array<Pair<String, String>> {
return genreList ?: arrayOf(
Pair("Premere reset per aggiornare i filtri", ""),
)
}
private class AZFilter(values: Array<Pair<String, String>>) : SelectFilter(
"A - Z",
values,
)
private var azList: Array<Pair<String, String>>? = null
private fun getAZList(): Array<Pair<String, String>> {
return azList ?: arrayOf(
Pair("Premere reset per aggiornare i filtri", ""),
)
}
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request {
if (connId.isEmpty()) {
getConnId()
}
val headers = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Accept-Language", "en-US,en;q=0.5")
.add("Cookie", "JSESSIONID=$sessionId")
.add("Referer", "$baseUrl/")
.add("X-Requested-With", "XMLHttpRequest")
.build()
return GET("$baseUrl/vvvvid/ondemand/${anime.url}/info/?conn_id=$connId", headers = headers)
}
override fun animeDetailsParse(response: Response): SAnime {
val detailsJson = json.decodeFromString<InfoResponse>(response.body.string()).data
return SAnime.create().apply {
title = detailsJson.title
status = SAnime.UNKNOWN
genre = detailsJson.show_genres?.joinToString(", ") ?: ""
description = buildString {
append(detailsJson.description)
append("\n\nAnno pubblicato: ${detailsJson.date_published}")
append("\n${detailsJson.additional_info.split(" | ").joinToString("\n")}")
}
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
if (connId.isEmpty()) {
getConnId()
}
val headers = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Accept-Language", "en-US,en;q=0.5")
.add("Cookie", "JSESSIONID=$sessionId")
.add("Referer", "$baseUrl/")
.add("X-Requested-With", "XMLHttpRequest")
.build()
return GET("$baseUrl/vvvvid/ondemand/${anime.url}/seasons/?conn_id=$connId", headers = headers)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val animeJson = json.decodeFromString<SeasonsResponse>(response.body.string())
val episodeList = mutableListOf<SEpisode>()
val subDub = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
var counter = 1
animeJson.data.forEach {
val prefix = if (it.name.lowercase().contains("in italiano")) {
if (subDub == "sub") return@forEach
"(Dub) Episodi "
} else if (it.name.lowercase().contains("in giapponese")) {
if (subDub == "dub") return@forEach
"(Sub) Episodi "
} else {
"${it.name} "
}
it.episodes.forEach { ep ->
episodeList.add(
SEpisode.create().apply {
name = "$prefix${ep.number} ${ep.title}"
episode_number = counter.toFloat()
url = LinkData(it.show_id, ep.season_id, ep.video_id).toJsonString()
},
)
counter++
}
}
return episodeList.reversed()
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request = throw UnsupportedOperationException()
override fun videoListParse(response: Response): List<Video> = throw UnsupportedOperationException()
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val (request, videoId) = videoListRequestPair(episode)
return client.newCall(request)
.awaitSuccess()
.let { response ->
videoListParse(response, videoId).sort()
}
}
private fun videoListRequestPair(episode: SEpisode): Pair<Request, Int> {
if (connId.isEmpty()) {
getConnId()
}
val mediaId = json.decodeFromString<LinkData>(episode.url)
val headers = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Accept-Language", "en-US,en;q=0.5")
.add("Cookie", "JSESSIONID=$sessionId")
.add("Referer", "$baseUrl/")
.add("X-Requested-With", "XMLHttpRequest")
.build()
return Pair(
GET(
"$baseUrl/vvvvid/ondemand/${mediaId.show_id}/season/${mediaId.season_id}?video_id=${mediaId.video_id}&conn_id=$connId",
headers = headers,
),
mediaId.video_id,
)
}
private fun videoListParse(response: Response, videoId: Int): List<Video> {
val videoJson = json.decodeFromString<VideosResponse>(response.body.string())
val videoList = mutableListOf<Video>()
val video = videoJson.data.first {
it.video_id == videoId
}
val realUrl = realUrl(video.embed_info)
when {
realUrl.endsWith(".mpd") -> {
videoList.add(videoFromDash(realUrl, "HD"))
if (video.embed_info_sd != null) {
val realUrl = realUrl(video.embed_info_sd)
videoList.add(videoFromDash(realUrl, "SD"))
}
}
}
return videoList.sort()
}
// ============================= Utilities ==============================
private fun updateFilters(channelName: String, setId: String = "") {
val channels = client.newCall(
GET("$baseUrl/vvvvid/ondemand/$channelName/channels?conn_id=$connId"),
).execute()
val channelsJson = json.decodeFromString<ChannelsResponse>(channels.body.string())
val subPages = mutableListOf<Pair<String, String>>()
subPages.add(Pair("Nessuno", ""))
val genrePages = mutableListOf<Pair<String, String>>()
genrePages.add(Pair("Nessuno", ""))
val azPages = mutableListOf<Pair<String, String>>()
azPages.add(Pair("Nessuno", ""))
for (it in channelsJson.data) {
when (it.name) {
"In Evidenza" -> {
subPages.add(Pair(it.name, it.id.toString()))
}
"Popolari" -> {
if (setId == "Popolari") {
currentChannelId = it.id.toString()
}
subPages.add(Pair(it.name, it.id.toString()))
}
"Nuove uscite" -> {
if (setId == "Nuove") {
currentChannelId = it.id.toString()
}
subPages.add(Pair(it.name, it.id.toString()))
}
"Generi" -> {
genrePages.addAll(
it.category!!.map { t ->
Pair(t.name, "${it.id},${t.id}")
},
)
}
"A - Z" -> {
azPages.addAll(
it.filter!!.filter { s -> s[0].isLetter() }.map { t ->
Pair(t.uppercase(), "${it.id},$t")
},
)
}
}
}
subPageList = subPages.toTypedArray()
genreList = genrePages.toTypedArray()
azList = azPages.toTypedArray()
}
private fun videoFromDash(url: String, name: String): Video {
val dashHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Accept-Language", "en-US,en;q=0.5")
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
.build()
val dashContents = client.newCall(
GET(url, headers = dashHeaders),
).execute().body.string()
val baseVideoUrl = url.substringBeforeLast("/")
val videoUrl = dashContents.substringAfter("mimeType=\"video").substringBefore("</BaseURL>").substringAfter("<BaseURL>")
val audioUrl = dashContents.substringAfter("mimeType=\"audio").substringBefore("</BaseURL>").substringAfter("<BaseURL>")
val audioTracks = mutableListOf<Track>()
audioTracks.add(Track("$baseVideoUrl/$audioUrl", "Audio"))
return Video(
baseVideoUrl,
name,
"$baseVideoUrl/$videoUrl",
audioTracks = audioTracks,
)
}
private fun getRandomIntString(): String {
val allowedChars = '0'..'9'
return (1..16)
.map { allowedChars.random() }
.joinToString("")
}
private fun LinkData.toJsonString(): String {
return json.encodeToString(this)
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this.sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun f(m: List<Int>): List<Int> {
val l = mutableListOf<Int>()
var o = 0
var b = false
val mSize = m.size
while (!b && o < mSize) {
var n = m[o] shl 2
o++
var k = -1
var j = -1
if (o < mSize) {
n += m[o] shr 4
o++
if (o < mSize) {
k = (m[o - 1] shl 4) and 255
k += m[o] shr 2
o++
if (o < mSize) {
j = (m[o - 1] shl 6) and 255
j += m[o]
o++
} else {
b = true
}
} else {
b = true
}
} else {
b = true
}
l.add(n)
if (k != -1) {
l.add(k)
}
if (j != -1) {
l.add(j)
}
}
return l
}
private fun realUrl(h: String): String {
val g = "MNOPIJKL89+/4567UVWXQRSTEFGHABCDcdefYZabstuvopqr0123wxyzklmnghij"
val c = mutableListOf<Int>()
h.forEach {
c.add(g.indexOf(it))
}
val cSize = c.size
for (e in cSize * 2 - 1 downTo 0) {
val a = c[e % cSize] xor c[(e + 1) % cSize]
c[e % cSize] = a
}
val newC = f(c)
var d = ""
newC.forEach { e ->
d += e.toChar()
}
return d
}
companion object {
private const val PREF_SUB_KEY = "preferred_sub"
private const val PREF_SUB_DEFAULT = "none"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "HD"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SUB_KEY
title = "Preferenza sub/dub"
entries = arrayOf("Nessuno", "Sub", "Dub")
entryValues = arrayOf("none", "sub", "dub")
setDefaultValue(PREF_SUB_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 = "Qualità preferita"
entries = arrayOf("HD", "SD")
entryValues = arrayOf("HD", "SD")
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,101 @@
package eu.kanade.tachiyomi.animeextension.it.vvvvid
import kotlinx.serialization.Serializable
@Serializable
data class LoginResponse(
val data: LoginData,
) {
@Serializable
data class LoginData(
val conn_id: String,
val sessionId: String,
)
}
@Serializable
data class AnimesResponse(
val data: List<AnimeData>,
) {
@Serializable
data class AnimeData(
val id: Int,
val show_id: Int,
val title: String,
val thumbnail: String,
)
}
@Serializable
data class SeasonsResponse(
val data: List<SeasonObject>,
) {
@Serializable
data class SeasonObject(
val name: String,
val show_id: Int,
val episodes: List<EpisodeObject>,
) {
@Serializable
data class EpisodeObject(
val id: Int,
val season_id: Int,
val video_id: Int,
val number: String,
val title: String,
)
}
}
@Serializable
data class InfoResponse(
val data: InfoObject,
) {
@Serializable
data class InfoObject(
val title: String,
val thumbnail: String,
val description: String,
val date_published: String,
val additional_info: String,
val show_genres: List<String>? = null,
)
}
@Serializable
data class ChannelsResponse(
val data: List<ChannelsObject>,
) {
@Serializable
data class ChannelsObject(
val id: Int,
val name: String,
val category: List<Category>? = null,
val filter: List<String>? = null,
) {
@Serializable
data class Category(
val name: String,
val id: Int,
)
}
}
@Serializable
data class VideosResponse(
val data: List<VideoObject>,
) {
@Serializable
data class VideoObject(
val video_id: Int,
val embed_info: String,
val embed_info_sd: String? = null,
)
}
@Serializable
data class LinkData(
val show_id: Int,
val season_id: Int,
val video_id: Int,
)