diff --git a/src/all/animexin/AndroidManifest.xml b/src/all/animexin/AndroidManifest.xml new file mode 100644 index 000000000..acb4de356 --- /dev/null +++ b/src/all/animexin/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/all/animexin/build.gradle b/src/all/animexin/build.gradle new file mode 100644 index 000000000..9b821677d --- /dev/null +++ b/src/all/animexin/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'AnimeXin' + pkgNameSuffix = 'all.animexin' + extClass = '.AnimeXin' + extVersionCode = 1 + libVersion = '13' +} + +dependencies { + compileOnly libs.bundles.coroutines + implementation(project(':lib-okru-extractor')) + implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1" +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/animexin/res/mipmap-hdpi/ic_launcher.png b/src/all/animexin/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..2efedb051 Binary files /dev/null and b/src/all/animexin/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/animexin/res/mipmap-mdpi/ic_launcher.png b/src/all/animexin/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..9c97ed6cb Binary files /dev/null and b/src/all/animexin/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/animexin/res/mipmap-xhdpi/ic_launcher.png b/src/all/animexin/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..0e218a763 Binary files /dev/null and b/src/all/animexin/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/animexin/res/mipmap-xxhdpi/ic_launcher.png b/src/all/animexin/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..35e4ed4e4 Binary files /dev/null and b/src/all/animexin/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/animexin/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/animexin/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..87c8ced85 Binary files /dev/null and b/src/all/animexin/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/animexin/res/web_hi_res_512.png b/src/all/animexin/res/web_hi_res_512.png new file mode 100644 index 000000000..02d35c055 Binary files /dev/null and b/src/all/animexin/res/web_hi_res_512.png differ diff --git a/src/all/animexin/src/eu/kanade/tachiyomi/animeextension/all/animexin/AnimeXin.kt b/src/all/animexin/src/eu/kanade/tachiyomi/animeextension/all/animexin/AnimeXin.kt new file mode 100644 index 000000000..15c6ffb6d --- /dev/null +++ b/src/all/animexin/src/eu/kanade/tachiyomi/animeextension/all/animexin/AnimeXin.kt @@ -0,0 +1,333 @@ +package eu.kanade.tachiyomi.animeextension.all.animexin + +import android.app.Application +import android.content.SharedPreferences +import android.util.Base64 +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DailymotionExtractor +import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DoodExtractor +import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.FembedExtractor +import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.GdrivePlayerExtractor +import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.StreamSBExtractor +import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.YouTubeExtractor +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.ParsedAnimeHttpSource +import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.text.SimpleDateFormat +import java.util.Locale + +class AnimeXin : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "AnimeXin" + + override val baseUrl by lazy { preferences.getString("preferred_domain", "https://animexin.vip")!! } + + override val lang = "en" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + companion object { + private val DateFormatter by lazy { + SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH) + } + } + + // ============================== Popular =============================== + + override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/") + + override fun popularAnimeSelector(): String = "div.wpop-weekly > ul > li" + + override fun popularAnimeNextPageSelector(): String? = null + + override fun popularAnimeFromElement(element: Element): SAnime { + return SAnime.create().apply { + setUrlWithoutDomain(element.selectFirst("a.series").attr("href").toHttpUrl().encodedPath) + thumbnail_url = element.selectFirst("img").attr("src").substringBefore("?resize") + title = element.selectFirst("a.series:not(:has(img))").text() + } + } + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/anime/?page=$page&status=&type=&order=update") + + override fun latestUpdatesSelector(): String = searchAnimeSelector() + + override fun latestUpdatesNextPageSelector(): String = searchAnimeNextPageSelector() + + override fun latestUpdatesFromElement(element: Element): SAnime = searchAnimeFromElement(element) + + // =============================== Search =============================== + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used") + + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + val params = AnimeXinFilters.getSearchParameters(filters) + return client.newCall(searchAnimeRequest(page, query, params)) + .asObservableSuccess() + .map { response -> + searchAnimeParse(response) + } + } + + private fun searchAnimeRequest(page: Int, query: String, filters: AnimeXinFilters.FilterSearchParams): Request { + return if (query.isNotEmpty()) { + GET("$baseUrl/page/$page/?s=$query") + } else { + val multiChoose = mutableListOf() + if (filters.genres.isNotEmpty()) multiChoose.add(filters.genres) + if (filters.seasons.isNotEmpty()) multiChoose.add(filters.seasons) + if (filters.studios.isNotEmpty()) multiChoose.add(filters.studios) + val multiString = if (multiChoose.isEmpty()) "" else multiChoose.joinToString("&") + "&" + GET("$baseUrl/anime/?page=$page&${multiString}status=${filters.status}&type=${filters.type}&sub=${filters.sub}&order=${filters.order}") + } + } + + override fun searchAnimeSelector(): String = "div.listupd > article" + + override fun searchAnimeNextPageSelector(): String = "div.hpage > a:contains(Next)" + + override fun searchAnimeFromElement(element: Element): SAnime { + return SAnime.create().apply { + setUrlWithoutDomain(element.selectFirst("a").attr("href").toHttpUrl().encodedPath) + thumbnail_url = element.selectFirst("img").attr("src").substringBefore("?resize") + title = element.selectFirst("div.tt").text() + } + } + + override fun getFilterList(): AnimeFilterList = AnimeXinFilters.filterList + + // =========================== Anime Details ============================ + + override fun animeDetailsParse(document: Document): SAnime { + return SAnime.create().apply { + title = document.selectFirst("h1.entry-title").text() + thumbnail_url = document.selectFirst("div.thumb > img").attr("src").substringBefore("?resize") + status = SAnime.COMPLETED + description = document.select("div[itemprop=description] p")?.let { + it.joinToString("\n\n") { t -> t.text() } + + "\n\n" + + document.select("div.info-content > div > span").joinToString("\n") { info -> + info.text().replace(":", ": ") + } + } ?: "" + } + } + + // ============================== Episodes ============================== + + override fun episodeListParse(response: Response): List { + val document = response.asJsoup() + + return document.select("div.eplister > ul > li").map { episodeElement -> + val numberText = episodeElement.selectFirst("div.epl-num").text() + val numberString = numberText.substringBefore(" ") + val episodeNumber = if (numberText.contains("part 2", true)) { + numberString.toFloatOrNull()?.plus(0.5F) ?: 0F + } else { + numberString.toFloatOrNull() ?: 0F + } + + SEpisode.create().apply { + episode_number = episodeNumber + name = numberText + date_upload = parseDate(episodeElement.selectFirst("div.epl-date")?.text() ?: "") + setUrlWithoutDomain(episodeElement.selectFirst("a").attr("href").toHttpUrl().encodedPath) + } + } + } + + override fun episodeListSelector(): String = throw Exception("Not Used") + + override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not Used") + + // ============================ Video Links ============================= + + override fun videoListParse(response: Response): List