diff --git a/src/it/toonitalia/build.gradle b/src/it/toonitalia/build.gradle index b7e3486a1..fdde11a22 100644 --- a/src/it/toonitalia/build.gradle +++ b/src/it/toonitalia/build.gradle @@ -1,16 +1,21 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} ext { extName = 'Toonitalia' pkgNameSuffix = 'it.toonitalia' extClass = '.Toonitalia' - extVersionCode = 9 + extVersionCode = 10 libVersion = '13' } dependencies { implementation(project(':lib-voe-extractor')) + implementation(project(':lib-streamtape-extractor')) + implementation(project(':lib-playlist-utils')) + implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1") } apply from: "$rootDir/common.gradle" diff --git a/src/it/toonitalia/src/eu/kanade/tachiyomi/animeextension/it/toonitalia/Toonitalia.kt b/src/it/toonitalia/src/eu/kanade/tachiyomi/animeextension/it/toonitalia/Toonitalia.kt index 3a656576b..90e16fcbe 100644 --- a/src/it/toonitalia/src/eu/kanade/tachiyomi/animeextension/it/toonitalia/Toonitalia.kt +++ b/src/it/toonitalia/src/eu/kanade/tachiyomi/animeextension/it/toonitalia/Toonitalia.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.animeextension.it.toonitalia import android.app.Application -import android.content.SharedPreferences 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 @@ -13,11 +13,11 @@ 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.toHttpUrlOrNull -import okhttp3.OkHttpClient +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document @@ -30,38 +30,34 @@ class Toonitalia : ConfigurableAnimeSource, ParsedAnimeHttpSource() { override val name = "Toonitalia" - override val baseUrl = "https://toonitalia.co" + override val baseUrl = "https://toonitalia.green" override val lang = "it" override val supportsLatest = false - override val client: OkHttpClient = network.cloudflareClient + override val client = network.cloudflareClient - private val preferences: SharedPreferences by lazy { + private val preferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page", headers) - override fun popularAnimeRequest(page: Int): Request { - return GET("$baseUrl/page/$page", headers = 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 popularAnimeSelector(): String = "div#primary > main#main > article" - - override fun popularAnimeFromElement(element: Element): SAnime { - val anime = SAnime.create() - anime.title = element.select("h2 > a").text() - anime.thumbnail_url = element.selectFirst("img")!!.attr("src") - anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").substringAfter(baseUrl)) - return anime - } - - override fun popularAnimeNextPageSelector(): String = "div.nav-links > span.current ~ a" + override fun popularAnimeNextPageSelector() = "nav.pagination a.next" // =============================== Latest =============================== - override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used") override fun latestUpdatesSelector(): String = throw Exception("Not used") @@ -71,292 +67,153 @@ class Toonitalia : ConfigurableAnimeSource, ParsedAnimeHttpSource() { override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used") // =============================== Search =============================== - override fun searchAnimeParse(response: Response): AnimesPage { - val document = response.asJsoup() + val document = response.use { it.asJsoup() } - val animes = if (response.request.url.toString().substringAfter(baseUrl).startsWith("/?s=")) { - document.select(searchAnimeSelector()).map { element -> - searchAnimeFromElement(element) - } + val isNormalSearch = document.location().contains("/?s=") + val animes = if (isNormalSearch) { + document.select(searchAnimeSelector()).map(::searchAnimeFromElement) } else { - document.select(searchIndexAnimeSelector()).map { element -> - searchIndexAnimeFromElement(element) - } + document.select(searchIndexAnimeSelector()).map(::searchIndexAnimeFromElement) } - val hasNextPage = searchAnimeNextPageSelector()?.let { selector -> - document.select(selector).first() - } != null + 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/?s=$query", headers = headers) + GET("$baseUrl/page/$page/?s=$query", headers = headers) } else { - val url = "$baseUrl".toHttpUrlOrNull()!!.newBuilder() - filters.forEach { filter -> - when (filter) { - is IndexFilter -> url.addPathSegment(filter.toUriPart()) - else -> {} - } + val url = "$baseUrl".toHttpUrl().newBuilder().apply { + filters.filterIsInstance() + .firstOrNull() + ?.also { addPathSegment(it.toUriPart()) } } - var newUrl = url.toString() - if (page > 1) { - newUrl += "/?lcp_page0=$page#lcp_instance_0" - } - GET(newUrl, headers = headers) + val newUrl = url.toString() + "/?lcp_page0=$page#lcp_instance_0" + GET(newUrl, headers) } } - override fun searchAnimeSelector(): String = "section#primary > main#main > article" + override fun searchAnimeSelector() = popularAnimeSelector() - private fun searchIndexAnimeSelector(): String = "div.entry-content > ul.lcp_catlist > li" + private fun searchIndexAnimeSelector() = "div.entry-content > ul.lcp_catlist > li" - override fun searchAnimeFromElement(element: Element): SAnime { - val anime = SAnime.create() - anime.title = element.selectFirst("h2")!!.text() - anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").substringAfter(baseUrl)) - return anime + override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element) + + private fun searchIndexAnimeFromElement(element: Element) = SAnime.create().apply { + element.selectFirst("a")!!.run { + title = text() + setUrlWithoutDomain(attr("href")) + } } - private fun searchIndexAnimeFromElement(element: Element): SAnime { - val anime = SAnime.create() - anime.title = element.select("a").text() - anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").substringAfter(baseUrl)) - return anime - } - - override fun searchAnimeNextPageSelector(): String = "ul.lcp_paginator > li.lcp_currentpage ~ li" + 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") - override fun animeDetailsParse(document: Document): SAnime { - val anime = SAnime.create() - anime.thumbnail_url = document.select("div.entry-content > h2 > img").attr("src") - anime.title = document.select("header.entry-header > h1.entry-title").text() - - var descInfo = "" - document.selectFirst("div.entry-content > h2 + p + p")!!.childNodes().filter { - s -> - s.nodeName() != "br" - }.forEach { - if (it.nodeName() == "span") { - if (it.nextSibling() != null) { - descInfo += "\n" - } - descInfo += "${it.childNode(0)} " - } else if (it.nodeName() == "#text") { - val infoStr = it.toString().trim() - if (infoStr.isNotBlank()) descInfo += infoStr + // 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: ") } - } - - var descElement = document.selectFirst("div.entry-content > h3:contains(Trama:) + p") - if (descElement == null) { - descElement = document.selectFirst("div.entry-content > p:has(span:contains(Trama:))") - } - - val description = if (descElement == null) { - "Nessuna descrizione disponibile\n\n$descInfo" - } else { - descElement.childNodes().filter { - s -> - s.nodeName() == "#text" - }.joinToString(separator = "\n\n") { it.toString() }.trim() + "\n\n" + descInfo - } - - anime.description = description - - anime.genre = document.select("footer.entry-footer > span.cat-links > a").joinToString(separator = ", ") { it.text() } - - return anime + .joinToString("\n") + .substringAfter("Trama: ") } // ============================== Episodes ============================== + private val episodeNumRegex by lazy { Regex("\\s(\\d+x\\d+)\\s?") } override fun episodeListParse(response: Response): List { - val document = response.asJsoup() - val episodeList = mutableListOf() + val doc = response.use { it.asJsoup() } + val url = doc.location() - // Select single seasons episodes - val singleEpisode = document.select("div.entry-content > h3:contains(Episodi) + p") - if (singleEpisode.isNotEmpty() && singleEpisode.text().isNotEmpty()) { - var episode = SEpisode.create() - - var isValid = false - var counter = 1 - for (child in singleEpisode.first()!!.childNodes()) { - if (child.nodeName() == "br" || (child.nextSibling() == null && child.nodeName() == "a")) { - episode.url = response.request.url.toString() + "#$counter" - - if (isValid) { - episodeList.add(episode) - isValid = false - } - episode = SEpisode.create() - counter++ - } else if (child.nodeName() == "a") { - isValid = true - } else { - val name = child.toString().trim().substringBeforeLast("–") - if (name.isNotEmpty()) { - episode.name = "Episode ${name.trim()}" - episode.episode_number = counter.toFloat() - } - } - } + if ("/film-anime/" in url) { + return listOf( + SEpisode.create().apply { + setUrlWithoutDomain("$url#0") + episode_number = 1F + name = doc.selectFirst("h1.entry-title")!!.text() + }, + ) } - // Select multiple seasons - val seasons = document.select("div.entry-content > h3:contains(Stagione) + p") - if (seasons.isNotEmpty()) { - var counter = 1 - seasons.forEach { - var episode = SEpisode.create() - - var isValid = false - for (child in it.childNodes()) { - if (child.nodeName() == "br" || (child.nextSibling() == null && child.nodeName() == "a")) { - episode.url = response.request.url.toString() + "#$counter" - if (isValid) { - episodeList.add(episode) - isValid = false - } - episode = SEpisode.create() - counter++ - } else if (child.nodeName() == "a") { - isValid = true - } else { - val name = child.toString().trim().substringBeforeLast("–") - if (name.isNotEmpty()) { - episode.name = "Episode ${name.trim()}" - episode.episode_number = counter.toFloat() - } - } - } + 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 } - } - - // Select movie - val movie = document.select("div.entry-content > p:contains(Link Streaming)") - if (movie.isNotEmpty()) { - val episode = SEpisode.create() - for (child in movie.first()!!.childNodes()) { - if (child.nodeName() == "br" || (child.nextSibling() == null && child.nodeName() == "a")) { - // episode.url = links.joinToString(separator = "///") - episode.url = response.request.url.toString() + "#1" - } else if (child.nodeName() == "a") { - } else { - val name = child.toString().trim().substringBeforeLast("–") - if (name.isNotEmpty()) { - episode.name = "Movie" - episode.episode_number = 1F - } - } - } - episodeList.add(episode) - } - - return episodeList.reversed() + }.reversed() } - override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used") + override fun episodeFromElement(element: Element) = throw Exception("Not used") - override fun episodeListSelector(): String = throw Exception("Not used") + override fun episodeListSelector() = "article > div.entry-content table tr:has(a)" // ============================ Video Links ============================= - - override fun videoListRequest(episode: SEpisode): Request { - return GET(episode.url, headers = headers) - } - override fun videoListParse(response: Response): List