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