diff --git a/src/de/animetoast/AndroidManifest.xml b/src/de/animetoast/AndroidManifest.xml
new file mode 100644
index 000000000..acb4de356
--- /dev/null
+++ b/src/de/animetoast/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/de/animetoast/build.gradle b/src/de/animetoast/build.gradle
new file mode 100644
index 000000000..01cb9b2d4
--- /dev/null
+++ b/src/de/animetoast/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'AnimeToast'
+ pkgNameSuffix = 'de.animetoast'
+ extClass = '.AnimeToast'
+ extVersionCode = 1
+ libVersion = '13'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/de/animetoast/res/mipmap-hdpi/ic_launcher.png b/src/de/animetoast/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..22d14673e
Binary files /dev/null and b/src/de/animetoast/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/de/animetoast/res/mipmap-hdpi/ic_launcher_adaptive_back.png b/src/de/animetoast/res/mipmap-hdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..8dc560dd4
Binary files /dev/null and b/src/de/animetoast/res/mipmap-hdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/de/animetoast/res/mipmap-hdpi/ic_launcher_adaptive_fore.png b/src/de/animetoast/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..ee83e8b80
Binary files /dev/null and b/src/de/animetoast/res/mipmap-hdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/de/animetoast/res/mipmap-mdpi/ic_launcher.png b/src/de/animetoast/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..0966db527
Binary files /dev/null and b/src/de/animetoast/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/de/animetoast/res/mipmap-mdpi/ic_launcher_adaptive_back.png b/src/de/animetoast/res/mipmap-mdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..f8feb6e59
Binary files /dev/null and b/src/de/animetoast/res/mipmap-mdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/de/animetoast/res/mipmap-mdpi/ic_launcher_adaptive_fore.png b/src/de/animetoast/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..13d578b06
Binary files /dev/null and b/src/de/animetoast/res/mipmap-mdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/de/animetoast/res/mipmap-xhdpi/ic_launcher.png b/src/de/animetoast/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..49450cdb0
Binary files /dev/null and b/src/de/animetoast/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/de/animetoast/res/mipmap-xhdpi/ic_launcher_adaptive_back.png b/src/de/animetoast/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..1b560124d
Binary files /dev/null and b/src/de/animetoast/res/mipmap-xhdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/de/animetoast/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/src/de/animetoast/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..6d854db3f
Binary files /dev/null and b/src/de/animetoast/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/de/animetoast/res/mipmap-xxhdpi/ic_launcher.png b/src/de/animetoast/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..4bf800876
Binary files /dev/null and b/src/de/animetoast/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/de/animetoast/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/src/de/animetoast/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..369383ac8
Binary files /dev/null and b/src/de/animetoast/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/de/animetoast/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/src/de/animetoast/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..32c497a93
Binary files /dev/null and b/src/de/animetoast/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher.png b/src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..0992df881
Binary files /dev/null and b/src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..de26e98f5
Binary files /dev/null and b/src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..5d33ff6ee
Binary files /dev/null and b/src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/de/animetoast/src/eu/kanade/tachiyomi/animeextension/de/animetoast/AnimeToast.kt b/src/de/animetoast/src/eu/kanade/tachiyomi/animeextension/de/animetoast/AnimeToast.kt
new file mode 100644
index 000000000..ebda30ead
--- /dev/null
+++ b/src/de/animetoast/src/eu/kanade/tachiyomi/animeextension/de/animetoast/AnimeToast.kt
@@ -0,0 +1,247 @@
+package eu.kanade.tachiyomi.animeextension.de.animetoast
+
+import android.app.Application
+import android.content.SharedPreferences
+import androidx.preference.ListPreference
+import androidx.preference.MultiSelectListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.animeextension.de.animetoast.extractors.DoodExtractor
+import eu.kanade.tachiyomi.animeextension.de.animetoast.extractors.VoeExtractor
+import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
+import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
+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.network.GET
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import kotlin.Exception
+
+class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
+
+ override val name = "AnimeToast"
+
+ override val baseUrl = "https://www.animetoast.cc"
+
+ override val lang = "de"
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ override fun popularAnimeSelector(): String = "div.row div.col-md-4 div.video-item"
+
+ override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
+
+ override fun popularAnimeFromElement(element: Element): SAnime {
+ val anime = SAnime.create()
+ anime.setUrlWithoutDomain(element.select("div.item-thumbnail a").attr("href"))
+ val document = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
+ anime.thumbnail_url = document.select(".item-content p img").attr("src")
+ anime.title = element.select("div.item-thumbnail a").attr("title")
+ return anime
+ }
+
+ override fun popularAnimeNextPageSelector(): String? = null
+
+ // episodes
+
+ override fun episodeListSelector() = throw Exception("not used")
+
+ override fun episodeListParse(response: Response): List {
+ val document = response.asJsoup()
+ val episodeList = mutableListOf()
+ val file = document.select("a[rel=\"category tag\"]").text()
+ if (file.contains("Serie")) {
+ val elements = document.select("#multi_link_tab0")
+ elements.forEach {
+ val episode = parseEpisodesFromSeries(it)
+ episodeList.addAll(episode)
+ }
+ } else {
+ val episode = SEpisode.create()
+ episode.setUrlWithoutDomain(document.select("link[rel=canonical]").attr("href"))
+ episode.name = document.select("h1.light-title").text()
+ episode.episode_number = 1F
+ episodeList.add(episode)
+ }
+ return episodeList.reversed()
+ }
+
+ private fun parseEpisodesFromSeries(element: Element): List {
+ val episodeElements = element.select("div.tab-pane a")
+ return episodeElements.map { episodeFromElement(it) }
+ }
+
+ override fun episodeFromElement(element: Element): SEpisode {
+ val episode = SEpisode.create()
+ episode.episode_number = element.text().replace("Ep. ", "").toFloat()
+ episode.name = element.text()
+ episode.setUrlWithoutDomain(element.attr("href"))
+ return episode
+ }
+
+ // Video Extractor
+
+ override fun videoListParse(response: Response): List
", "").substringBefore("Genre:").replace("", "")
+ anime.status = parseStatus(document.select("a[rel=\"category tag\"]").text())
+ return anime
+ }
+
+ private fun parseStatus(status: String?) = when {
+ status == null -> SAnime.UNKNOWN
+ status.contains("Airing", ignoreCase = true) -> SAnime.ONGOING
+ else -> SAnime.COMPLETED
+ }
+
+ // Latest
+
+ override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
+
+ override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
+
+ override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
+
+ override fun latestUpdatesSelector(): String = throw Exception("Not used")
+
+ // Preferences
+
+ override fun setupPreferenceScreen(screen: PreferenceScreen) {
+ val hosterPref = ListPreference(screen.context).apply {
+ key = "preferred_hoster"
+ title = "Standard-Hoster"
+ entries = arrayOf("Voe", "DoodStream")
+ entryValues = arrayOf("https://voe.sx", "https://dood")
+ setDefaultValue("https://voe.sx")
+ summary = "%s"
+
+ setOnPreferenceChangeListener { _, newValue ->
+ val selected = newValue as String
+ val index = findIndexOfValue(selected)
+ val entry = entryValues[index] as String
+ preferences.edit().putString(key, entry).commit()
+ }
+ }
+ val subSelection = MultiSelectListPreference(screen.context).apply {
+ key = "hoster_selection"
+ title = "Hoster auswählen"
+ entries = arrayOf("Voe", "DoodStream")
+ entryValues = arrayOf("voe", "dood")
+ setDefaultValue(setOf("voe", "dood"))
+
+ setOnPreferenceChangeListener { _, newValue ->
+ preferences.edit().putStringSet(key, newValue as Set).commit()
+ }
+ }
+ screen.addPreference(hosterPref)
+ screen.addPreference(subSelection)
+ }
+}
diff --git a/src/de/animetoast/src/eu/kanade/tachiyomi/animeextension/de/animetoast/extractors/DoodExtractor.kt b/src/de/animetoast/src/eu/kanade/tachiyomi/animeextension/de/animetoast/extractors/DoodExtractor.kt
new file mode 100644
index 000000000..a494137c0
--- /dev/null
+++ b/src/de/animetoast/src/eu/kanade/tachiyomi/animeextension/de/animetoast/extractors/DoodExtractor.kt
@@ -0,0 +1,40 @@
+package eu.kanade.tachiyomi.animeextension.de.animetoast.extractors
+
+import eu.kanade.tachiyomi.animesource.model.Video
+import eu.kanade.tachiyomi.network.GET
+import okhttp3.Headers
+import okhttp3.OkHttpClient
+
+class DoodExtractor(private val client: OkHttpClient) {
+ fun videoFromUrl(url: String, quality: String): Video? {
+ val response = client.newCall(GET(url)).execute()
+ val doodTld = url.substringAfter("https://dood.").substringBefore("/")
+ val content = response.body!!.string()
+ if (!content.contains("'/pass_md5/")) return null
+ val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
+ val token = md5.substringAfterLast("/")
+ val randomString = getRandomString()
+ val expiry = System.currentTimeMillis()
+ val videoUrlStart = client.newCall(
+ GET(
+ "https://dood.$doodTld/pass_md5/$md5",
+ Headers.headersOf("referer", url)
+ )
+ ).execute().body!!.string()
+ val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
+
+ return Video(url, quality, videoUrl, null, doodHeaders(doodTld))
+ }
+
+ private fun getRandomString(length: Int = 10): String {
+ val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
+ return (1..length)
+ .map { allowedChars.random() }
+ .joinToString("")
+ }
+
+ private fun doodHeaders(tld: String) = Headers.Builder().apply {
+ add("User-Agent", "Aniyomi")
+ add("Referer", "https://dood.$tld/")
+ }.build()
+}
diff --git a/src/de/animetoast/src/eu/kanade/tachiyomi/animeextension/de/animetoast/extractors/VoeExtractor.kt b/src/de/animetoast/src/eu/kanade/tachiyomi/animeextension/de/animetoast/extractors/VoeExtractor.kt
new file mode 100644
index 000000000..b9c353c00
--- /dev/null
+++ b/src/de/animetoast/src/eu/kanade/tachiyomi/animeextension/de/animetoast/extractors/VoeExtractor.kt
@@ -0,0 +1,17 @@
+package eu.kanade.tachiyomi.animeextension.de.animetoast.extractors
+
+import eu.kanade.tachiyomi.animesource.model.Video
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.OkHttpClient
+
+class VoeExtractor(private val client: OkHttpClient) {
+
+ fun videoFromUrl(url: String, quality: String): Video? {
+ val document = client.newCall(GET(url)).execute().asJsoup()
+ val script = document.select("script:containsData(function d04ad2e48229ae25a282e15c7c2f69a2(dea04c5949242bfd216e35def894b930))")
+ .firstOrNull()?.data()?.substringAfter("\"hls\": \"") ?: return null
+ val videoUrl = script.substringAfter("\"hls\": \"").substringBefore("\",")
+ return Video(url, quality, videoUrl)
+ }
+}