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