diff --git a/lib/streamsb-extractor/src/main/java/eu/kanade/tachiyomi/lib/streamsbextractor/StreamSBExtractor.kt b/lib/streamsb-extractor/src/main/java/eu/kanade/tachiyomi/lib/streamsbextractor/StreamSBExtractor.kt index ec965f115..400087579 100644 --- a/lib/streamsb-extractor/src/main/java/eu/kanade/tachiyomi/lib/streamsbextractor/StreamSBExtractor.kt +++ b/lib/streamsb-extractor/src/main/java/eu/kanade/tachiyomi/lib/streamsbextractor/StreamSBExtractor.kt @@ -26,8 +26,9 @@ class StreamSBExtractor(private val client: OkHttpClient) { // animension, asianload and dramacool uses "common = false" private fun fixUrl(url: String, common: Boolean): String { - val sbUrl = url.substringBefore("/e/") + val sbUrl = url.substringBefore("/e/").substringBefore("/embed-") val id = url.substringAfter("/e/") + .substringAfter("/embed-") .substringBefore("?") .substringBefore(".html") return if (common) { diff --git a/src/en/animedao/AndroidManifest.xml b/src/en/animedao/AndroidManifest.xml new file mode 100644 index 000000000..acb4de356 --- /dev/null +++ b/src/en/animedao/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/en/animedao/build.gradle b/src/en/animedao/build.gradle new file mode 100644 index 000000000..f2ab315f5 --- /dev/null +++ b/src/en/animedao/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'AnimeDao' + pkgNameSuffix = 'en.animedao' + extClass = '.AnimeDao' + extVersionCode = 1 + libVersion = '13' +} + +dependencies { + compileOnly libs.bundles.coroutines + implementation(project(':lib-streamtape-extractor')) + implementation(project(':lib-streamsb-extractor')) + implementation(project(':lib-fembed-extractor')) + implementation(project(':lib-dood-extractor')) + implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1" +} + + +apply from: "$rootDir/common.gradle" diff --git a/src/en/animedao/res/mipmap-hdpi/ic_launcher.png b/src/en/animedao/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..8984de5f5 Binary files /dev/null and b/src/en/animedao/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/animedao/res/mipmap-mdpi/ic_launcher.png b/src/en/animedao/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..17e3f9ee7 Binary files /dev/null and b/src/en/animedao/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/animedao/res/mipmap-xhdpi/ic_launcher.png b/src/en/animedao/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..e792a6905 Binary files /dev/null and b/src/en/animedao/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/animedao/res/mipmap-xxhdpi/ic_launcher.png b/src/en/animedao/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..09869e437 Binary files /dev/null and b/src/en/animedao/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/animedao/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/animedao/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..5a747f19e Binary files /dev/null and b/src/en/animedao/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/animedao/res/web_hi_res_512.png b/src/en/animedao/res/web_hi_res_512.png new file mode 100644 index 000000000..082b0b122 Binary files /dev/null and b/src/en/animedao/res/web_hi_res_512.png differ diff --git a/src/en/animedao/src/eu/kanade/tachiyomi/animeextension/en/animedao/AnimeDao.kt b/src/en/animedao/src/eu/kanade/tachiyomi/animeextension/en/animedao/AnimeDao.kt new file mode 100644 index 000000000..208246722 --- /dev/null +++ b/src/en/animedao/src/eu/kanade/tachiyomi/animeextension/en/animedao/AnimeDao.kt @@ -0,0 +1,391 @@ +package eu.kanade.tachiyomi.animeextension.en.animedao + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.animeextension.en.animedao.extractors.MixDropExtractor +import eu.kanade.tachiyomi.animeextension.en.animedao.extractors.Mp4uploadExtractor +import eu.kanade.tachiyomi.animeextension.en.animedao.extractors.VidstreamingExtractor +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.doodextractor.DoodExtractor +import eu.kanade.tachiyomi.lib.fembedextractor.FembedExtractor +import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor +import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor +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 kotlinx.serialization.json.Json +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +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 uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale + +class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "AnimeDao" + + override val baseUrl by lazy { preferences.getString("preferred_domain", "https://animedao.to")!! } + + override val lang = "en" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + private val json: Json by injectLazy() + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + companion object { + private val DateFormatter by lazy { + SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH) + } + } + + // ============================== Popular =============================== + + override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animelist/popular") + + override fun popularAnimeSelector(): String = "div.container > div.row > div.col-md-6" + + override fun popularAnimeNextPageSelector(): String? = null + + override fun popularAnimeFromElement(element: Element): SAnime { + val thumbnailUrl = element.selectFirst("img").attr("data-src") + + return SAnime.create().apply { + setUrlWithoutDomain(element.selectFirst("a").attr("href")) + thumbnail_url = if (thumbnailUrl.contains(baseUrl.toHttpUrl().host)) { + thumbnailUrl + } else { + baseUrl + thumbnailUrl + } + title = element.selectFirst("span.animename").text() + } + } + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl) + + override fun latestUpdatesSelector(): String = "div#latest-tab-pane > div.row > div.col-md-6" + + override fun latestUpdatesNextPageSelector(): String? = popularAnimeNextPageSelector() + + override fun latestUpdatesFromElement(element: Element): SAnime { + val thumbnailUrl = element.selectFirst("img").attr("data-src") + + return SAnime.create().apply { + setUrlWithoutDomain(element.selectFirst("a.animeparent").attr("href")) + thumbnail_url = if (thumbnailUrl.contains(baseUrl.toHttpUrl().host)) { + thumbnailUrl + } else { + baseUrl + thumbnailUrl + } + title = element.selectFirst("span.animename").text() + } + } + + // =============================== 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 = AnimeDaoFilters.getSearchParameters(filters) + return client.newCall(searchAnimeRequest(page, query, params)) + .asObservableSuccess() + .map { response -> + searchAnimeParse(response) + } + } + + override fun searchAnimeParse(response: Response): AnimesPage { + val document = response.asJsoup() + + val animes = if (response.request.url.encodedPath.startsWith("/animelist/")) { + document.select(searchAnimeSelectorFilter()).map { element -> + searchAnimeFromElement(element) + } + } else { + document.select(searchAnimeSelector()).map { element -> + searchAnimeFromElement(element) + } + } + + val hasNextPage = searchAnimeNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return AnimesPage(animes, hasNextPage) + } + + private fun searchAnimeRequest(page: Int, query: String, filters: AnimeDaoFilters.FilterSearchParams): Request { + return if (query.isNotBlank()) { + val cleanQuery = query.replace(" ", "+") + GET("$baseUrl/search/?search=$cleanQuery", headers = headers) + } else { + var url = "$baseUrl/animelist/".toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("status[]=", filters.status) + .addQueryParameter("order[]=", filters.order) + .build().toString() + + if (filters.genre.isNotBlank()) url += "&${filters.genre}" + if (filters.rating.isNotBlank()) url += "&${filters.rating}" + if (filters.letter.isNotBlank()) url += "&${filters.letter}" + if (filters.year.isNotBlank()) url += "&${filters.year}" + if (filters.score.isNotBlank()) url += "&${filters.score}" + url += "&page=$page" + + GET(url, headers = headers) + } + } + + override fun searchAnimeSelector(): String = popularAnimeSelector() + + private fun searchAnimeSelectorFilter(): String = "div.container div.col-12 > div.row > div.col-md-6" + + override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.page-item:has(i.fa-arrow-right):not(.disabled)" + + override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) + + // ============================== FILTERS =============================== + + override fun getFilterList(): AnimeFilterList = AnimeDaoFilters.filterList + + // =========================== Anime Details ============================ + + override fun animeDetailsParse(document: Document): SAnime { + val thumbnailUrl = document.selectFirst("div.card-body img").attr("data-src") + val moreInfo = document.select("div.card-body table > tbody > tr").joinToString("\n") { it.text() } + + return SAnime.create().apply { + title = document.selectFirst("div.card-body h2").text() + thumbnail_url = if (thumbnailUrl.contains(baseUrl.toHttpUrl().host)) { + thumbnailUrl + } else { + baseUrl + thumbnailUrl + } + status = document.selectFirst("div.card-body table > tbody > tr:has(>td:contains(Status)) td:not(:contains(Status))")?.let { + parseStatus(it.text()) + } ?: SAnime.UNKNOWN + description = (document.selectFirst("div.card-body div:has(>b:contains(Description))")?.ownText() ?: "") + "\n\n$moreInfo" + genre = document.select("div.card-body table > tbody > tr:has(>td:contains(Genres)) td > a").joinToString(", ") { it.text() } + } + } + + // ============================== Episodes ============================== + + override fun episodeListParse(response: Response): List { + return if (preferences.getBoolean("preferred_episode_sorting", false)) { + super.episodeListParse(response).sortedWith( + compareBy( + { it.episode_number }, + { it.name } + ) + ).reversed() + } else { + super.episodeListParse(response) + } + } + + override fun episodeListSelector(): String = "div#episodes-tab-pane > div.row > div > div.card" + + override fun episodeFromElement(element: Element): SEpisode { + val episodeName = element.selectFirst("span.animename").text() + val episodeTitle = element.selectFirst("div.animetitle")?.text() ?: "" + + return SEpisode.create().apply { + name = "$episodeName $episodeTitle" + episode_number = if (episodeName.contains("Episode ", true)) { + episodeName.substringAfter("Episode ").substringBefore(" ").toFloatOrNull() ?: 0F + } else { 0F } + date_upload = element.selectFirst("span.date")?.let { parseDate(it.text()) } ?: 0L + setUrlWithoutDomain(element.selectFirst("a[href]").attr("href")) + } + } + + // ============================ Video Links ============================= + + override fun videoListParse(response: Response): List