diff --git a/lib/playlist-utils/src/main/java/eu/kanade/tachiyomi/lib/playlistutils/PlaylistUtils.kt b/lib/playlist-utils/src/main/java/eu/kanade/tachiyomi/lib/playlistutils/PlaylistUtils.kt index 3591258d64..634a23ffc0 100644 --- a/lib/playlist-utils/src/main/java/eu/kanade/tachiyomi/lib/playlistutils/PlaylistUtils.kt +++ b/lib/playlist-utils/src/main/java/eu/kanade/tachiyomi/lib/playlistutils/PlaylistUtils.kt @@ -126,6 +126,11 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade }.toList() return masterPlaylist.substringAfter(PLAYLIST_SEPARATOR).split(PLAYLIST_SEPARATOR).mapNotNull { + val codec = it.substringAfter("CODECS=\"", "").substringBefore("\"", "") + if (codec.isNotEmpty()) { + if (codec.startsWith("mp4a")) return@mapNotNull null + } + val resolution = it.substringAfter("RESOLUTION=") .substringBefore("\n") .substringAfter("x") diff --git a/src/all/anizone/build.gradle b/src/all/anizone/build.gradle new file mode 100644 index 0000000000..9c32fe40a0 --- /dev/null +++ b/src/all/anizone/build.gradle @@ -0,0 +1,11 @@ +ext { + extName = 'AniZone' + extClass = '.AniZone' + extVersionCode = 2 +} + +apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(":lib:playlist-utils")) +} diff --git a/src/all/anizone/res/mipmap-hdpi/ic_launcher.png b/src/all/anizone/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..30ff4ee895 Binary files /dev/null and b/src/all/anizone/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/anizone/res/mipmap-mdpi/ic_launcher.png b/src/all/anizone/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..4e0d7b6a8b Binary files /dev/null and b/src/all/anizone/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/anizone/res/mipmap-xhdpi/ic_launcher.png b/src/all/anizone/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..79cd46eeb5 Binary files /dev/null and b/src/all/anizone/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/anizone/res/mipmap-xxhdpi/ic_launcher.png b/src/all/anizone/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..47d0a46c31 Binary files /dev/null and b/src/all/anizone/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/anizone/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/anizone/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..f44767b98c Binary files /dev/null and b/src/all/anizone/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/anizone/src/eu/kanade/tachiyomi/animeextension/all/anizone/AniZone.kt b/src/all/anizone/src/eu/kanade/tachiyomi/animeextension/all/anizone/AniZone.kt new file mode 100644 index 0000000000..92377042cc --- /dev/null +++ b/src/all/anizone/src/eu/kanade/tachiyomi/animeextension/all/anizone/AniZone.kt @@ -0,0 +1,495 @@ +package eu.kanade.tachiyomi.animeextension.all.anizone + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +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.Track +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.POST +import eu.kanade.tachiyomi.util.asJsoup +import eu.kanade.tachiyomi.util.parseAs +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.addJsonObject +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.jsoup.Jsoup.parseBodyFragment +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Locale + +class AniZone : AnimeHttpSource(), ConfigurableAnimeSource { + + override val name = "AniZone" + + override val baseUrl = "https://anizone.to" + + override val lang = "all" + + override val supportsLatest = true + + private val json: Json by injectLazy() + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + private var token: String = "" + + private val snapShots: MutableMap = mutableMapOf( + ANIME_SNAPSHOT_KEY to "", + EPISODE_SNAPSHOT_KEY to "", + VIDEO_SNAPSHOT_KEY to "", + ) + + private var loadCount: Int = 0 + + // ============================== Popular =============================== + + override fun popularAnimeRequest(page: Int): Request { + return if (page == 1) { + loadCount = 0 + snapShots[ANIME_SNAPSHOT_KEY] = "" + + val updates = buildJsonObject { + put("sort", "title-asc") + } + val calls = buildJsonArray { } + + createLivewireReq(ANIME_SNAPSHOT_KEY, updates, calls) + } else { + val updates = buildJsonObject { } + val calls = buildJsonArray { + addJsonObject { + put("path", "") + put("method", "loadMore") + putJsonArray("params") { } + } + } + + createLivewireReq(ANIME_SNAPSHOT_KEY, updates, calls) + } + } + + override fun popularAnimeParse(response: Response): AnimesPage { + val html = response.parseAs().getHtml(ANIME_SNAPSHOT_KEY) + + val animeList = html.select("div.grid > div").drop(loadCount) + .map(::animeFromElement) + val hasNextPage = html.selectFirst("div[x-intersect~=loadMore]") != null + + loadCount += animeList.size + + return AnimesPage(animeList, hasNextPage) + } + + private fun animeFromElement(element: Element): SAnime { + return SAnime.create().apply { + thumbnail_url = element.selectFirst("img")!!.attr("src") + with(element.selectFirst("a.inline")!!) { + setUrlWithoutDomain(attr("href")) + title = text() + } + } + } + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int): Request { + return if (page == 1) { + loadCount = 0 + snapShots[ANIME_SNAPSHOT_KEY] = "" + + val updates = buildJsonObject { + put("sort", "release-desc") + } + val calls = buildJsonArray { } + + createLivewireReq(ANIME_SNAPSHOT_KEY, updates, calls) + } else { + popularAnimeRequest(page) + } + } + + override fun latestUpdatesParse(response: Response): AnimesPage { + return popularAnimeParse(response) + } + + // =============================== Search =============================== + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val sortFilter = filters.filterIsInstance().first() + + return if (page == 1) { + loadCount = 0 + snapShots[ANIME_SNAPSHOT_KEY] = "" + + val updates = buildJsonObject { + if (query.isNotEmpty()) { + put("search", query) + } + put("sort", sortFilter.toUriPart()) + } + val calls = buildJsonArray { } + + createLivewireReq(ANIME_SNAPSHOT_KEY, updates, calls) + } else { + popularAnimeRequest(page) + } + } + + override fun searchAnimeParse(response: Response): AnimesPage { + return popularAnimeParse(response) + } + + // ============================== Filters =============================== + + override fun getFilterList(): AnimeFilterList { + return AnimeFilterList(SortFilter()) + } + + private class SortFilter : UriPartFilter( + "Sort", + arrayOf( + Pair("A-Z", "title-asc"), + Pair("Z-A", "title-desc"), + Pair("Earliest Release", "release-asc"), + Pair("Latest Release", "release-desc"), + Pair("First Added", "added-asc"), + Pair("Last Added", "added-desc"), + ), + ) + + private open class UriPartFilter(displayName: String, val vals: Array>) : + AnimeFilter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } + + // =========================== Anime Details ============================ + + override fun animeDetailsParse(response: Response): SAnime { + val document = response.asJsoup() + + val infoDiv = document.select("div.flex.items-start > div")[1] + + return SAnime.create().apply { + thumbnail_url = document.selectFirst("div.flex.items-start img")!!.attr("abs:img") + + with(infoDiv) { + title = selectFirst("h1")!!.text() + status = select("span.flex")[1].parseStatus() + description = selectFirst("div:has(>h3:contains(Synopsis)) > div")?.html() + ?.replace("
", "\n") + ?.replace(MULTILINE_REGEX, "\n\n") + genre = select("div > a").joinToString { it.text() } + } + } + } + + private fun Element.parseStatus(): Int = when (this.text().lowercase()) { + "completed" -> SAnime.COMPLETED + "ongoing" -> SAnime.ONGOING + else -> SAnime.UNKNOWN + } + + // ============================== Episodes ============================== + + private fun getPredefinedSnapshots(slug: String): String { + return when (slug) { + "/anime/uyyyn4kf" -> """{"data":{"anime":[null,{"class":"anime","key":68,"s":"mdl"}],"title":null,"search":"","listSize":1104,"sort":"release-asc","sortOptions":[{"release-asc":"First Aired","release-desc":"Last Aired"},{"s":"arr"}],"view":"list","paginators":[{"page":1},{"s":"arr"}]},"memo":{"id":"GD1OiEMOJq6UQDQt1OBt","name":"pages.anime-detail","path":"anime\/uyyyn4kf","method":"GET","children":[],"scripts":[],"assets":[],"errors":[],"locale":"en"},"checksum":"5800932dd82e4862f34f6fd72d8098243b32643e8accb8da6a6a39cd0ee86acd"}""" + else -> "" + } + } + + override fun episodeListRequest(anime: SAnime): Request { + snapShots[EPISODE_SNAPSHOT_KEY] = getPredefinedSnapshots(anime.url) + + val updates = buildJsonObject { + put("sort", "release-desc") + } + val calls = buildJsonArray { } + + return createLivewireReq(EPISODE_SNAPSHOT_KEY, updates, calls, anime.url) + } + + override fun episodeListParse(response: Response): List { + val document = response.parseAs().getHtml(EPISODE_SNAPSHOT_KEY) + val episodeList = document.select(episodeSelector) + .map(::episodeFromElement) + .toMutableList() + loadCount = episodeList.size + + var hasMore = document.selectFirst("div[x-intersect~=loadMore]") != null + + while (hasMore) { + val updates = buildJsonObject { } + val calls = buildJsonArray { + addJsonObject { + put("path", "") + put("method", "loadMore") + putJsonArray("params") { } + } + } + + val resp = client.newCall( + createLivewireReq(EPISODE_SNAPSHOT_KEY, updates, calls), + ).execute().parseAs().getHtml(EPISODE_SNAPSHOT_KEY) + + val episodes = resp.select(episodeSelector) + .drop(loadCount) + .map(::episodeFromElement) + + episodeList.addAll(episodes) + loadCount += episodes.size + + hasMore = resp.selectFirst("div[x-intersect~=loadMore]") != null + } + + return episodeList + } + + private val episodeSelector = "ul > li" + + private fun episodeFromElement(element: Element): SEpisode { + val url = element.selectFirst("a[href]")!!.attr("abs:href") + + return SEpisode.create().apply { + setUrlWithoutDomain(url) + episode_number = url.substringAfterLast("/").toFloat() + name = element.selectFirst("h3")!!.text() + date_upload = element.select("div.flex-row > span").getOrNull(1) + ?.text() + ?.let { parseDate(it) } + ?: 0L + } + } + + // ============================ Video Links ============================= + + override fun videoListRequest(episode: SEpisode): Request { + return GET(baseUrl + episode.url, headers) + } + + private val playlistUtils: PlaylistUtils by lazy { PlaylistUtils(client, headers) } + + override fun videoListParse(response: Response): List