Initial commit
7
src/it/animesaturn/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anime Saturn'
|
||||
extClass = '.AnimeSaturn'
|
||||
extVersionCode = 8
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/it/animesaturn/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/it/animesaturn/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/it/animesaturn/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/it/animesaturn/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/it/animesaturn/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 28 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
7
src/it/animeunity/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeUnity'
|
||||
extClass = '.AnimeUnity'
|
||||
extVersionCode = 8
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/it/animeunity/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/it/animeunity/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/it/animeunity/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/it/animeunity/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
src/it/animeunity/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/it/animeunity/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 58 KiB |
|
@ -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(""", "\"")
|
||||
}
|
||||
|
||||
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(""", "\""),
|
||||
)
|
||||
|
||||
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(""", "\""),
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
|
@ -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("Sì", "true"),
|
||||
)
|
||||
|
||||
val YEAR = arrayOf(ANY) + (1969..2024).map {
|
||||
Pair(it.toString(), it.toString())
|
||||
}.reversed().toTypedArray()
|
||||
}
|
||||
}
|
14
src/it/animeworld/build.gradle
Normal 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"
|
||||
}
|
BIN
src/it/animeworld/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/it/animeworld/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/it/animeworld/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
src/it/animeworld/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/it/animeworld/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/it/animeworld/res/play_store_512.png
Normal file
After Width: | Height: | Size: 52 KiB |
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
22
src/it/aniplay/AndroidManifest.xml
Normal 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>
|
11
src/it/aniplay/build.gradle
Normal file
|
@ -0,0 +1,11 @@
|
|||
ext {
|
||||
extName = 'AniPlay'
|
||||
extClass = '.AniPlay'
|
||||
extVersionCode = 10
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
BIN
src/it/aniplay/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/it/aniplay/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/it/aniplay/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/it/aniplay/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/it/aniplay/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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!!
|
||||
}
|
8
src/it/hentaisaturn/build.gradle
Normal file
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'Hentai Saturn'
|
||||
extClass = '.HentaiSaturn'
|
||||
extVersionCode = 5
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/it/hentaisaturn/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
src/it/hentaisaturn/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/it/hentaisaturn/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src/it/hentaisaturn/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
src/it/hentaisaturn/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 42 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
7
src/it/streamingcommunity/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'StreamingCommunity'
|
||||
extClass = '.StreamingCommunity'
|
||||
extVersionCode = 4
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/it/streamingcommunity/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/it/streamingcommunity/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/it/streamingcommunity/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
src/it/streamingcommunity/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/it/streamingcommunity/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/it/streamingcommunity/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 86 KiB |
|
@ -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(""", "\"")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
14
src/it/toonitalia/build.gradle
Normal 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")
|
||||
}
|
BIN
src/it/toonitalia/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/it/toonitalia/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/it/toonitalia/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/it/toonitalia/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/it/toonitalia/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
src/it/toonitalia/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 20 KiB |
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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("\\[(.*)\\]") }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
7
src/it/vvvvid/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'VVVVID'
|
||||
extClass = '.VVVVID'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/it/vvvvid/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/it/vvvvid/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/it/vvvvid/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/it/vvvvid/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
src/it/vvvvid/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/it/vvvvid/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 72 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|