diff --git a/src/it/aniplay/AndroidManifest.xml b/src/it/aniplay/AndroidManifest.xml
new file mode 100644
index 000000000..acb4de356
--- /dev/null
+++ b/src/it/aniplay/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/it/aniplay/build.gradle b/src/it/aniplay/build.gradle
new file mode 100644
index 000000000..9b84e525a
--- /dev/null
+++ b/src/it/aniplay/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'AniPlay'
+ pkgNameSuffix = 'it.aniplay'
+ extClass = '.AniPlay'
+ extVersionCode = 1
+ libVersion = '13'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/it/aniplay/res/mipmap-hdpi/ic_launcher.png b/src/it/aniplay/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..6a096f5b3
Binary files /dev/null and b/src/it/aniplay/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/it/aniplay/res/mipmap-mdpi/ic_launcher.png b/src/it/aniplay/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..1768c9956
Binary files /dev/null and b/src/it/aniplay/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/it/aniplay/res/mipmap-xhdpi/ic_launcher.png b/src/it/aniplay/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..45167b5ee
Binary files /dev/null and b/src/it/aniplay/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/it/aniplay/res/mipmap-xxhdpi/ic_launcher.png b/src/it/aniplay/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..946df9ec0
Binary files /dev/null and b/src/it/aniplay/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/it/aniplay/res/mipmap-xxxhdpi/ic_launcher.png b/src/it/aniplay/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..45cd555fa
Binary files /dev/null and b/src/it/aniplay/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/it/aniplay/res/web_hi_res_512.png b/src/it/aniplay/res/web_hi_res_512.png
new file mode 100644
index 000000000..46c3a588e
Binary files /dev/null and b/src/it/aniplay/res/web_hi_res_512.png differ
diff --git a/src/it/aniplay/src/eu/kanade/tachiyomi/animeextension/it/aniplay/AniPlay.kt b/src/it/aniplay/src/eu/kanade/tachiyomi/animeextension/it/aniplay/AniPlay.kt
new file mode 100644
index 000000000..6a267f387
--- /dev/null
+++ b/src/it/aniplay/src/eu/kanade/tachiyomi/animeextension/it/aniplay/AniPlay.kt
@@ -0,0 +1,343 @@
+package eu.kanade.tachiyomi.animeextension.it.aniplay
+
+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.Video
+import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.Headers
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+import java.lang.Exception
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class AniPlay : ConfigurableAnimeSource, AnimeHttpSource() {
+
+ override val name = "AniPlay"
+
+ override val baseUrl = "https://aniplay.it"
+
+ override val lang = "it"
+
+ override val supportsLatest = false
+
+ private val json: Json by injectLazy()
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ // ============================== Popular ===============================
+
+ override fun popularAnimeParse(response: Response): AnimesPage {
+ return searchAnimeParse(response)
+ }
+
+ override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/api/anime/advanced-similar-search?page=${page - 1}&size=36&sort=views,desc&sort=id")
+
+ // =============================== Latest ===============================
+
+ override fun latestUpdatesRequest(page: Int): Request = throw Exception("not used")
+
+ override fun latestUpdatesParse(response: Response): AnimesPage = throw Exception("not used")
+
+ // =============================== Search ===============================
+
+ override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable {
+ val params = AniPlayFilters.getSearchParameters(filters)
+ return client.newCall(searchAnimeRequest(page, query, params))
+ .asObservableSuccess()
+ .map { response ->
+ searchAnimeParse(response)
+ }
+ }
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
+
+ private fun searchAnimeRequest(page: Int, query: String, filters: AniPlayFilters.FilterSearchParams): Request {
+ if ((filters.anni.isNotEmpty() && filters.stagione.isEmpty()) ||
+ (filters.anni.isEmpty() && filters.stagione.isNotEmpty())
+ ) {
+ throw Exception("Per gli anime stagionali, seleziona sia l'anno che la stagione")
+ }
+
+ val url = if (filters.anni.isNotEmpty()) {
+ "$baseUrl/api/seasonal-view".toHttpUrlOrNull()!!.newBuilder()
+ .addPathSegment("${filters.stagione}-${filters.anni}")
+ .addQueryParameter("page", (page - 1).toString())
+ .addQueryParameter("size", "36")
+ .addQueryParameter("sort", filters.ordina)
+ .addQueryParameter("sort", "id")
+ } else {
+ "$baseUrl/api/anime/advanced-similar-search".toHttpUrlOrNull()!!.newBuilder()
+ .addQueryParameter("page", (page - 1).toString())
+ .addQueryParameter("size", "36")
+ .addQueryParameter("sort", filters.ordina)
+ .addQueryParameter("sort", "id")
+ .addIfNotBlank("query", query)
+ .addIfNotBlank("genreIds", filters.genere)
+ .addIfNotBlank("typeIds", filters.tipologia)
+ .addIfNotBlank("statusIds", filters.stato)
+ .addIfNotBlank("originIds", filters.origine)
+ .addIfNotBlank("studioIds", filters.studio)
+ .addIfNotBlank("startYear", filters.inizio)
+ .addIfNotBlank("endYear", filters.fine)
+ }
+
+ return GET(url.build().toString())
+ }
+
+ override fun searchAnimeParse(response: Response): AnimesPage {
+ val parsed = json.decodeFromString>(response.body!!.string())
+
+ val animeList = parsed.map { ani ->
+ SAnime.create().apply {
+ title = ani.title
+ if (ani.verticalImages.isNotEmpty()) {
+ thumbnail_url = ani.verticalImages.first().imageFull
+ }
+ url = ani.id.toString()
+ description = ani.storyline
+ }
+ }
+
+ return AnimesPage(animeList, animeList.size == 36)
+ }
+
+ override fun getFilterList(): AnimeFilterList = AniPlayFilters.filterList
+
+ // =========================== Anime Details ============================
+
+ override fun animeDetailsRequest(anime: SAnime): Request {
+ return GET("$baseUrl/api/anime/${anime.url}")
+ }
+
+ override fun animeDetailsParse(response: Response): SAnime {
+ val detailsJson = json.decodeFromString(response.body!!.string())
+ val anime = SAnime.create()
+
+ anime.title = detailsJson.title
+ anime.author = detailsJson.studio
+ anime.status = parseStatus(detailsJson.status)
+
+ var description = detailsJson.storyline + "\n"
+ description += "\nTipologia: ${detailsJson.type}"
+ description += "\nOrigine: ${detailsJson.origin}"
+ if (detailsJson.startDate != null) description += "\nData di inizio: ${detailsJson.startDate}"
+ description += "\nStato: ${detailsJson.status}"
+
+ anime.description = description
+
+ return anime
+ }
+
+ // ============================== Episodes ==============================
+
+ override fun episodeListRequest(anime: SAnime): Request {
+ return GET("$baseUrl/api/anime/${anime.url}")
+ }
+
+ override fun episodeListParse(response: Response): List {
+ val animeJson = json.decodeFromString(response.body!!.string())
+ val episodeList = mutableListOf()
+
+ if (animeJson.seasons.isNotEmpty()) {
+ for (season in animeJson.seasons) {
+ val episodesResponse = client.newCall(
+ GET("$baseUrl/api/anime/${animeJson.id}/season/${season.id}")
+ ).execute()
+ val episodesJson = json.decodeFromString>(episodesResponse.body!!.string())
+
+ for (ep in episodesJson) {
+ val episode = SEpisode.create()
+
+ episode.name = "Episode ${ep.episodeNumber.toIntOrNull() ?: (ep.episodeNumber.toFloatOrNull() ?: 1)} ${ep.title ?: ""}"
+ episode.episode_number = ep.episodeNumber.toFloatOrNull() ?: 0F
+
+ if (ep.airingDate != null) episode.date_upload = SimpleDateFormat("yyyy-MM-dd", Locale.ITALY).parse(ep.airingDate)!!.time
+
+ episode.url = ep.id.toString()
+
+ episodeList.add(episode)
+ }
+ }
+ } else if (animeJson.episodes.isNotEmpty()) {
+ for (ep in animeJson.episodes) {
+ val episode = SEpisode.create()
+ episode.name = "Episode ${ep.episodeNumber.toIntOrNull() ?: (ep.episodeNumber.toFloatOrNull() ?: 1)} ${ep.title ?: ""}"
+ episode.episode_number = ep.episodeNumber.toFloatOrNull() ?: 0F
+
+ if (ep.airingDate != null) episode.date_upload = SimpleDateFormat("yyyy-MM-dd", Locale.ITALY).parse(ep.airingDate)!!.time
+
+ episode.url = ep.id.toString()
+
+ episodeList.add(episode)
+ }
+ } else {}
+
+ return episodeList.sortedBy { it.episode_number }.reversed()
+ }
+
+ // ============================ Video Links =============================
+
+ override fun videoListRequest(episode: SEpisode): Request {
+ return GET("$baseUrl/api/episode/${episode.url}")
+ }
+
+ override fun videoListParse(response: Response): List