diff --git a/src/it/toonitalia/AndroidManifest.xml b/src/it/toonitalia/AndroidManifest.xml new file mode 100644 index 000000000..acb4de356 --- /dev/null +++ b/src/it/toonitalia/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/it/toonitalia/build.gradle b/src/it/toonitalia/build.gradle new file mode 100644 index 000000000..40db3e623 --- /dev/null +++ b/src/it/toonitalia/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Toonitalia' + pkgNameSuffix = 'it.toonitalia' + extClass = '.Toonitalia' + extVersionCode = 1 + libVersion = '13' +} + +dependencies { + implementation(project(':lib-voe-extractor')) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/it/toonitalia/res/mipmap-hdpi/ic_launcher.png b/src/it/toonitalia/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..281969b20 Binary files /dev/null and b/src/it/toonitalia/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/it/toonitalia/res/mipmap-mdpi/ic_launcher.png b/src/it/toonitalia/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..2a476bb4c Binary files /dev/null and b/src/it/toonitalia/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/it/toonitalia/res/mipmap-xhdpi/ic_launcher.png b/src/it/toonitalia/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..4e1007caa Binary files /dev/null and b/src/it/toonitalia/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/it/toonitalia/res/mipmap-xxhdpi/ic_launcher.png b/src/it/toonitalia/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..655bc215f Binary files /dev/null and b/src/it/toonitalia/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/it/toonitalia/res/mipmap-xxxhdpi/ic_launcher.png b/src/it/toonitalia/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..06f33bbaf Binary files /dev/null and b/src/it/toonitalia/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/it/toonitalia/res/web_hi_res_512.png b/src/it/toonitalia/res/web_hi_res_512.png new file mode 100644 index 000000000..bb20f995c Binary files /dev/null and b/src/it/toonitalia/res/web_hi_res_512.png differ 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 new file mode 100644 index 000000000..e74d6c573 --- /dev/null +++ b/src/it/toonitalia/src/eu/kanade/tachiyomi/animeextension/it/toonitalia/Toonitalia.kt @@ -0,0 +1,444 @@ +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.StreamSBExtractor +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.voeextractor.VoeExtractor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +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 java.lang.Exception + +class Toonitalia : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "Toonitalia" + + override val baseUrl = "https://toonitalia.co" + + override val lang = "it" + + override val supportsLatest = false + + override val client: OkHttpClient = network.cloudflareClient + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // ============================== Popular =============================== + + override fun popularAnimeRequest(page: Int): Request { + return GET("$baseUrl/page/$page", headers = headers) + } + + 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" + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used") + + override fun latestUpdatesSelector(): String = throw Exception("Not used") + + override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used") + + override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used") + + // =============================== Search =============================== + + override fun searchAnimeParse(response: Response): AnimesPage { + val document = response.asJsoup() + + val animes = if (response.request.url.toString().substringAfter(baseUrl).startsWith("/?s=")) { + document.select(searchAnimeSelector()).map { element -> + searchAnimeFromElement(element) + } + } else { + document.select(searchIndexAnimeSelector()).map { element -> + searchIndexAnimeFromElement(element) + } + } + + val hasNextPage = searchAnimeNextPageSelector()?.let { selector -> + document.select(selector).first() + } != 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) + } else { + val url = "$baseUrl".toHttpUrlOrNull()!!.newBuilder() + filters.forEach { filter -> + when (filter) { + is IndexFilter -> url.addPathSegment(filter.toUriPart()) + else -> {} + } + } + var newUrl = url.toString() + if (page > 1) { + newUrl += "/?lcp_page0=$page#lcp_instance_0" + } + GET(newUrl, headers = headers) + } + } + + override fun searchAnimeSelector(): String = "section#primary > main#main > article" + + private fun searchIndexAnimeSelector(): String = "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 + } + + 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" + + // =========================== Anime Details ============================ + + override fun animeDetailsParse(document: Document): SAnime { + val anime = SAnime.create() + anime.thumbnail_url = document.select("div.entry-content > h2 > img").attr("src") + + 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 + } + } + + 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 + } + + // ============================== Episodes ============================== + + override fun episodeListParse(response: Response): List { + val document = response.asJsoup() + val episodeList = mutableListOf() + + // 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() + } + } + } + } + + // 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() + } + } + } + } + } + + // 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() + } + + override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used") + + override fun episodeListSelector(): String = throw Exception("Not used") + + // ============================ Video Links ============================= + + override fun videoListRequest(episode: SEpisode): Request { + return GET(episode.url, headers = headers) + } + + override fun videoListParse(response: Response): List