diff --git a/src/it/aniplay/AndroidManifest.xml b/src/it/aniplay/AndroidManifest.xml index 568741e54..c8c0aac73 100644 --- a/src/it/aniplay/AndroidManifest.xml +++ b/src/it/aniplay/AndroidManifest.xml @@ -1,2 +1,22 @@ - \ No newline at end of file + + + + + + + + + + + + + + diff --git a/src/it/aniplay/build.gradle b/src/it/aniplay/build.gradle index f65a7e4da..ad3b4122d 100644 --- a/src/it/aniplay/build.gradle +++ b/src/it/aniplay/build.gradle @@ -1,7 +1,11 @@ ext { extName = 'AniPlay' extClass = '.AniPlay' - extVersionCode = 6 + extVersionCode = 7 } apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(":lib:playlist-utils")) +} diff --git a/src/it/aniplay/res/web_hi_res_512.png b/src/it/aniplay/res/web_hi_res_512.png deleted file mode 100644 index 46c3a588e..000000000 Binary files a/src/it/aniplay/res/web_hi_res_512.png and /dev/null 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 index 7c952a355..0f3865965 100644 --- 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 @@ -1,12 +1,14 @@ package eu.kanade.tachiyomi.animeextension.it.aniplay import android.app.Application -import android.content.SharedPreferences -import android.widget.Toast -import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.PreferenceScreen -import eu.kanade.tachiyomi.animeextension.BuildConfig +import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.AnimeInfoDto +import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.EpisodeDto +import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.LatestItemDto +import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.PopularAnimeDto +import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.PopularResponseDto +import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.VideoDto import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimesPage @@ -14,17 +16,17 @@ 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.lib.playlistutils.PlaylistUtils import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.awaitSuccess -import kotlinx.serialization.json.Json -import okhttp3.Headers +import eu.kanade.tachiyomi.util.asJsoup +import eu.kanade.tachiyomi.util.parseAs import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.Response import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale @@ -32,264 +34,158 @@ class AniPlay : ConfigurableAnimeSource, AnimeHttpSource() { override val name = "AniPlay" - override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! } + override val baseUrl = "https://aniplay.co" override val lang = "it" - override val supportsLatest = false + override val supportsLatest = true - private val json: Json by injectLazy() + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + .add("Origin", baseUrl) - private val preferences: SharedPreferences by lazy { + override val versionId = 2 // Source was rewritten in Svelte + + private val preferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = + GET("$API_URL/advancedSearch?sort=7&page=$page&origin=,,,,,,", headers) - override fun popularAnimeParse(response: Response): AnimesPage = 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") + override fun popularAnimeParse(response: Response): AnimesPage { + val parsed = response.parseAs() + val animes = parsed.data.map(PopularAnimeDto::toSAnime) + return AnimesPage(animes, parsed.pagination.hasNextPage) + } // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET("$API_URL/latest-episodes?page=$page&type=All") - override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() - - override fun latestUpdatesParse(response: Response): AnimesPage = throw UnsupportedOperationException() + override fun latestUpdatesParse(response: Response): AnimesPage { + val items = response.parseAs>() + val animes = items.mapNotNull { it.serie.firstOrNull()?.toSAnime() } + return AnimesPage(animes, items.size == 20) + } // =============================== Search =============================== + override fun getFilterList() = AniPlayFilters.FILTER_LIST override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage { - val params = AniPlayFilters.getSearchParameters(filters) - return client.newCall(searchAnimeRequest(page, query, params)) - .awaitSuccess() - .use(::searchAnimeParse) - } - - override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw UnsupportedOperationException() - - private fun searchAnimeRequest(page: Int, query: String, filters: AniPlayFilters.FilterSearchParams): Request { - if ((filters.year.isNotEmpty() && filters.season.isEmpty()) || - (filters.year.isEmpty() && filters.season.isNotEmpty()) - ) { - error("Per gli anime stagionali, seleziona sia l'anno che la stagione") - } - - val url = if (filters.year.isNotEmpty()) { - "$baseUrl/api/seasonal-view".toHttpUrlOrNull()!!.newBuilder() - .addPathSegment("${filters.season}-${filters.year}") - .addQueryParameter("page", (page - 1).toString()) - .addQueryParameter("size", "36") - .addQueryParameter("sort", filters.order) - .addQueryParameter("sort", "id") + return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler + val id = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/series/$id")) + .awaitSuccess() + .use(::searchAnimeByIdParse) } else { - "$baseUrl/api/anime/advanced-similar-search".toHttpUrlOrNull()!!.newBuilder() - .addQueryParameter("page", (page - 1).toString()) - .addQueryParameter("size", "36") - .addQueryParameter("sort", filters.order) - .addQueryParameter("sort", "id") - .addIfNotBlank("query", query) - .addIfNotBlank("genreIds", filters.genre) - .addIfNotBlank("typeIds", filters.type) - .addIfNotBlank("statusIds", filters.status) - .addIfNotBlank("originIds", filters.origin) - .addIfNotBlank("studioIds", filters.studio) - .addIfNotBlank("startYear", filters.start) - .addIfNotBlank("endYear", filters.end) + super.getSearchAnime(page, query, filters) } - - 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) + private fun searchAnimeByIdParse(response: Response): AnimesPage { + val details = animeDetailsParse(response) + .apply { setUrlWithoutDomain(response.request.url.toString()) } + return AnimesPage(listOf(details), false) } - // ============================== Filters =============================== + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val params = AniPlayFilters.getSearchParameters(filters) + val url = "$API_URL/advancedSearch".toHttpUrl().newBuilder() + .addQueryParameter("page", page.toString()) + .addQueryParameter("origin", ",,,,,,") + .addQueryParameter("sort", params.order) + .addIfNotBlank("_q", query) + .addIfNotBlank("genres", params.genres) + .addIfNotBlank("country", params.countries) + .addIfNotBlank("types", params.types) + .addIfNotBlank("studios", params.studios) + .addIfNotBlank("status", params.status) + .addIfNotBlank("subbed", params.languages) + .build() - override fun getFilterList(): AnimeFilterList = AniPlayFilters.FILTER_LIST + return GET(url, headers) + } + + override fun searchAnimeParse(response: Response) = popularAnimeParse(response) // =========================== Anime Details ============================ - override fun getAnimeUrl(anime: SAnime) = "$baseUrl/anime/${anime.url}" + override fun animeDetailsParse(response: Response) = SAnime.create().apply { + val script = response.getPageScript() + val jsonString = script.substringAfter("{serie:").substringBefore(",tags") + "}" + val parsed = jsonString.fixJsonString().parseAs() - override fun animeDetailsRequest(anime: SAnime) = GET("$baseUrl/api/anime/${anime.url}") + title = parsed.title + genre = parsed.genres.joinToString { it.name } + artist = parsed.studios.joinToString { it.name } + thumbnail_url = parsed.thumbnailUrl + status = when (parsed.status) { + "Completato" -> SAnime.COMPLETED + "In corso" -> SAnime.ONGOING + "Sospeso" -> SAnime.ON_HIATUS + else -> SAnime.UNKNOWN + } - override fun animeDetailsParse(response: Response): SAnime { - val detailsJson = json.decodeFromString(response.body.string()) + description = buildString { + parsed.description?.also { + append(it, "\n\n") + } - return SAnime.create().apply { - title = detailsJson.title - author = detailsJson.studio - status = parseStatus(detailsJson.status) - description = buildString { - append(detailsJson.storyline) - append("\n\nTipologia: ${detailsJson.type}") - append("\nOrigine: ${detailsJson.origin}") - if (detailsJson.startDate != null) append("\nData di inizio: ${detailsJson.startDate}") - append("\nStato: ${detailsJson.status}") + listOf( + "Titolo Alternativo" to parsed.alternative, + "Origine" to parsed.origin, + "Giorno di lancio" to parsed.release_day, + ).forEach { (title, value) -> + if (value != null) append(title, ": ", value, "\n") } } } // ============================== Episodes ============================== - - override fun episodeListRequest(anime: SAnime): Request = GET("$baseUrl/api/anime/${anime.url}") - override fun episodeListParse(response: Response): List { - val animeJson = json.decodeFromString(response.body.string()) - val episodeList = mutableListOf() + val script = response.getPageScript() + val jsonString = script.substringAfter(",episodes:").substringBefore("]},") + "]" + val parsed = jsonString.fixJsonString().parseAs>() - 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()) - - episodeList.addAll( - episodesJson.map { ep -> - SEpisode.create().apply { - name = "Episode ${ep.episodeNumber.toIntOrNull() ?: (ep.episodeNumber.toFloatOrNull() ?: 1)} ${ep.title ?: ""}" - episode_number = ep.episodeNumber.toFloatOrNull() ?: 0F - if (ep.airingDate != null) date_upload = SimpleDateFormat("yyyy-MM-dd", Locale.ITALY).parse(ep.airingDate)!!.time - url = ep.id.toString() - } - }, - ) + return parsed.map { + SEpisode.create().apply { + episode_number = it.number?.toFloatOrNull() ?: 1F + url = "/watch/${it.id}" + name = it.title ?: "Episodio ${it.number}" + date_upload = it.release_date.toDate() } - } else if (animeJson.episodes.isNotEmpty()) { - episodeList.addAll( - animeJson.episodes.map { ep -> - SEpisode.create().apply { - name = "Episode ${ep.episodeNumber.toIntOrNull() ?: (ep.episodeNumber.toFloatOrNull() ?: 1)} ${ep.title ?: ""}" - episode_number = ep.episodeNumber.toFloatOrNull() ?: 0F - if (ep.airingDate != null) date_upload = SimpleDateFormat("yyyy-MM-dd", Locale.ITALY).parse(ep.airingDate)!!.time - url = ep.id.toString() - } - }, - ) - } - - return episodeList.sortedBy { it.episode_number }.reversed() + }.reversed() } // ============================ Video Links ============================= - - override fun videoListRequest(episode: SEpisode): Request = GET("$baseUrl/api/episode/${episode.url}") + private val playlistUtils by lazy { PlaylistUtils(client, headers) } override fun videoListParse(response: Response): List