diff --git a/src/en/animeflixlive/build.gradle b/src/en/animeflixlive/build.gradle new file mode 100644 index 0000000000..363e5055b2 --- /dev/null +++ b/src/en/animeflixlive/build.gradle @@ -0,0 +1,12 @@ +ext { + extName = 'Animeflix.live' + extClass = '.AnimeflixLive' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(':lib:gogostream-extractor')) + implementation(project(':lib:playlist-utils')) +} diff --git a/src/en/animeflixlive/res/mipmap-hdpi/ic_launcher.png b/src/en/animeflixlive/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..f630587cf4 Binary files /dev/null and b/src/en/animeflixlive/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/animeflixlive/res/mipmap-mdpi/ic_launcher.png b/src/en/animeflixlive/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..d42685d695 Binary files /dev/null and b/src/en/animeflixlive/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/animeflixlive/res/mipmap-xhdpi/ic_launcher.png b/src/en/animeflixlive/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..818568c2ad Binary files /dev/null and b/src/en/animeflixlive/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/animeflixlive/res/mipmap-xxhdpi/ic_launcher.png b/src/en/animeflixlive/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..d9d77e4658 Binary files /dev/null and b/src/en/animeflixlive/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/animeflixlive/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/animeflixlive/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..5ebf96144c Binary files /dev/null and b/src/en/animeflixlive/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/animeflixlive/src/eu/kanade/tachiyomi/animeextension/en/animeflixlive/AnimeflixLive.kt b/src/en/animeflixlive/src/eu/kanade/tachiyomi/animeextension/en/animeflixlive/AnimeflixLive.kt new file mode 100644 index 0000000000..9dd3407d90 --- /dev/null +++ b/src/en/animeflixlive/src/eu/kanade/tachiyomi/animeextension/en/animeflixlive/AnimeflixLive.kt @@ -0,0 +1,491 @@ +package eu.kanade.tachiyomi.animeextension.en.animeflixlive + +import GenreFilter +import SortFilter +import SubPageFilter +import TypeFilter +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.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.util.asJsoup +import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking +import eu.kanade.tachiyomi.util.parseAs +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.util.Calendar +import java.util.Locale +import java.util.TimeZone +import kotlin.math.min + +class AnimeflixLive : ConfigurableAnimeSource, AnimeHttpSource() { + + override val name = "Animeflix.live" + + override val baseUrl by lazy { preferences.baseUrl } + + private val apiUrl by lazy { preferences.apiUrl } + + override val lang = "en" + + override val supportsLatest = true + + private val json: Json by injectLazy() + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + private val apiHeaders = headersBuilder().apply { + add("Accept", "*/*") + add("Host", apiUrl.toHttpUrl().host) + add("Origin", baseUrl) + add("Referer", "$baseUrl/") + }.build() + + private val docHeaders = headersBuilder().apply { + add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + add("Host", apiUrl.toHttpUrl().host) + add("Referer", "$baseUrl/") + }.build() + + // ============================== Popular =============================== + + override fun popularAnimeRequest(page: Int): Request = + GET("$apiUrl/popular?page=${page - 1}", apiHeaders) + + override fun popularAnimeParse(response: Response): AnimesPage { + val parsed = response.parseAs>() + val titlePref = preferences.titleType + + val animeList = parsed.map { + it.toSAnime(titlePref) + } + + return AnimesPage(animeList, animeList.size == PAGE_SIZE) + } + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int): Request = + GET("$apiUrl/trending?page=${page - 1}", apiHeaders) + + override fun latestUpdatesParse(response: Response): AnimesPage { + val parsed = response.parseAs() + val titlePref = preferences.titleType + + val animeList = parsed.trending.map { + it.toSAnime(titlePref) + } + + return AnimesPage(animeList, animeList.size == PAGE_SIZE) + } + + // =============================== Search =============================== + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val sort = filters.filterIsInstance().first().getValue() + val type = filters.filterIsInstance().first().getValues() + val genre = filters.filterIsInstance().first().getValues() + val subPage = filters.filterIsInstance().first().getValue() + + if (subPage.isNotBlank()) { + return GET("$apiUrl/$subPage?page=${page - 1}", apiHeaders) + } + + if (query.isEmpty()) { + throw Exception("Search must not be empty") + } + + val filtersObj = buildJsonObject { + put("sort", sort) + if (type.isNotEmpty()) { + put("type", json.encodeToString(type)) + } + if (genre.isNotEmpty()) { + put("genre", json.encodeToString(genre)) + } + }.toJsonString() + + val url = apiUrl.toHttpUrl().newBuilder().apply { + addPathSegment("info") + addPathSegment("") + addQueryParameter("query", query) + addQueryParameter("limit", "15") + addQueryParameter("filters", filtersObj) + addQueryParameter("k", query.substr(0, 3).sk()) + }.build() + + return GET(url, apiHeaders) + } + + override fun searchAnimeParse(response: Response): AnimesPage { + val parsed = response.parseAs>() + val titlePref = preferences.titleType + + val animeList = parsed.map { + it.toSAnime(titlePref) + } + + val hasNextPage = if (response.request.url.queryParameter("limit") == null) { + animeList.size == 44 + } else { + animeList.size == 15 + } + + return AnimesPage(animeList, hasNextPage) + } + + // ============================== Filters =============================== + + override fun getFilterList(): AnimeFilterList = AnimeFilterList( + SortFilter(), + TypeFilter(), + GenreFilter(), + AnimeFilter.Separator(), + AnimeFilter.Header("NOTE: Subpage overrides search and other filters!"), + SubPageFilter(), + ) + + // =========================== Anime Details ============================ + + override fun animeDetailsRequest(anime: SAnime): Request { + return GET("$apiUrl/getslug/${anime.url}", apiHeaders) + } + + override fun getAnimeUrl(anime: SAnime): String { + return "$baseUrl/search/${anime.title}?anime=${anime.url}" + } + + override fun animeDetailsParse(response: Response): SAnime { + val titlePref = preferences.titleType + return response.parseAs().toSAnime(titlePref) + } + + // ============================== Episodes ============================== + + override fun episodeListRequest(anime: SAnime): Request { + val lang = preferences.lang + + val url = apiUrl.toHttpUrl().newBuilder().apply { + addPathSegment("episodes") + addQueryParameter("id", anime.url) + addQueryParameter("dub", (lang == "Dub").toString()) + addQueryParameter("c", anime.url.sk()) + }.build() + + return GET(url, apiHeaders) + } + + override fun episodeListParse(response: Response): List { + val slug = response.request.url.queryParameter("id")!! + + return response.parseAs().episodes.map { + it.toSEpisode(slug) + }.sortedByDescending { it.episode_number } + } + + // ============================ Video Links ============================= + + override fun videoListRequest(episode: SEpisode): Request { + val url = "$apiUrl${episode.url}".toHttpUrl().newBuilder().apply { + addQueryParameter("server", "") + addQueryParameter("c", episode.url.substringAfter("/watch/").sk()) + }.build() + + return GET(url, apiHeaders) + } + + override fun videoListParse(response: Response): List