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