diff --git a/src/en/ripcrabbyanime/AndroidManifest.xml b/src/en/ripcrabbyanime/AndroidManifest.xml
new file mode 100644
index 000000000..acb4de356
--- /dev/null
+++ b/src/en/ripcrabbyanime/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/en/ripcrabbyanime/build.gradle b/src/en/ripcrabbyanime/build.gradle
new file mode 100644
index 000000000..c6cb3561b
--- /dev/null
+++ b/src/en/ripcrabbyanime/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Ripcrabbyanime'
+ pkgNameSuffix = 'en.ripcrabbyanime'
+ extClass = '.Ripcrabbyanime'
+ extVersionCode = 1
+ libVersion = '13'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/ripcrabbyanime/res/mipmap-hdpi/ic_launcher.png b/src/en/ripcrabbyanime/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..d422a9a4c
Binary files /dev/null and b/src/en/ripcrabbyanime/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/ripcrabbyanime/res/mipmap-mdpi/ic_launcher.png b/src/en/ripcrabbyanime/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..92fb26095
Binary files /dev/null and b/src/en/ripcrabbyanime/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/ripcrabbyanime/res/mipmap-xhdpi/ic_launcher.png b/src/en/ripcrabbyanime/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..de3ed3e34
Binary files /dev/null and b/src/en/ripcrabbyanime/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/ripcrabbyanime/res/mipmap-xxhdpi/ic_launcher.png b/src/en/ripcrabbyanime/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..073c19b51
Binary files /dev/null and b/src/en/ripcrabbyanime/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/ripcrabbyanime/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/ripcrabbyanime/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..9e716cd67
Binary files /dev/null and b/src/en/ripcrabbyanime/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/ripcrabbyanime/res/web_hi_res_512.png b/src/en/ripcrabbyanime/res/web_hi_res_512.png
new file mode 100644
index 000000000..ce14e2e28
Binary files /dev/null and b/src/en/ripcrabbyanime/res/web_hi_res_512.png differ
diff --git a/src/en/ripcrabbyanime/src/eu/kanade/tachiyomi/animeextension/en/ripcrabbyanime/Ripcrabbyanime.kt b/src/en/ripcrabbyanime/src/eu/kanade/tachiyomi/animeextension/en/ripcrabbyanime/Ripcrabbyanime.kt
new file mode 100644
index 000000000..dee77bd4d
--- /dev/null
+++ b/src/en/ripcrabbyanime/src/eu/kanade/tachiyomi/animeextension/en/ripcrabbyanime/Ripcrabbyanime.kt
@@ -0,0 +1,471 @@
+package eu.kanade.tachiyomi.animeextension.en.ripcrabbyanime
+
+import android.app.Application
+import android.content.SharedPreferences
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
+import eu.kanade.tachiyomi.animeextension.en.ripcrabbyanime.extractors.GoogleDriveExtractor
+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.Video
+import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+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.security.MessageDigest
+import java.text.CharacterIterator
+import java.text.StringCharacterIterator
+
+class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
+
+ override val name = "Ripcrabbyanime (Experimental)"
+
+ override val baseUrl = "https://ripcrabbyanime.in"
+
+ override val lang = "en"
+
+ override val supportsLatest = false
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ private val MAX_RECURSION_DEPTH = 2
+
+ private val json: Json by injectLazy()
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ // ============================== Popular ===============================
+
+ override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/ongoing-series/")
+
+ override fun popularAnimeSelector(): String = "section#movies-list > div.movies-box"
+
+ override fun popularAnimeNextPageSelector(): String? = null
+
+ override fun popularAnimeFromElement(element: Element): SAnime {
+ return SAnime.create().apply {
+ setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
+ thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
+ title = element.selectFirst("a:matches(.)")!!.text().substringBefore(" | Episode").trimEnd()
+ }
+ }
+
+ // =============================== Latest ===============================
+
+ override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not Used")
+
+ override fun latestUpdatesSelector(): String = throw Exception("Not Used")
+
+ override fun latestUpdatesNextPageSelector(): String = throw Exception("Not Used")
+
+ override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not Used")
+
+ // =============================== Search ===============================
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
+ val filterList = if (filters.isEmpty()) getFilterList() else filters
+ val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
+ val subPageFilter = filterList.find { it is SubPageFilter } as SubPageFilter
+
+ return when {
+ query.isNotBlank() -> {
+ val cleanQuery = query.replace(" ", "+")
+ GET("$baseUrl/search/?s=$cleanQuery")
+ }
+ genreFilter.state != 0 -> {
+ val url = "$baseUrl${genreFilter.toUriPart()}"
+ val body = "start=${(page - 1) * 40}&limit=40".toRequestBody("application/x-www-form-urlencoded".toMediaType())
+ POST(url, body = body)
+ }
+ subPageFilter.state != 0 -> {
+ val url = "$baseUrl${subPageFilter.toUriPart()}"
+ val body = "start=${(page - 1) * 40}&limit=40".toRequestBody("application/x-www-form-urlencoded".toMediaType())
+ POST(url, body = body)
+ }
+ else -> popularAnimeRequest(page)
+ }
+ }
+
+ override fun searchAnimeParse(response: Response): AnimesPage {
+ return if (response.request.url.encodedPath.startsWith("/search/")) {
+ popularAnimeParse(response)
+ } else {
+ val document = response.asJsoup()
+
+ val animes = document.select(searchAnimeSelector()).map { element ->
+ popularAnimeFromElement(element)
+ }
+
+ return AnimesPage(animes, animes.size == 40)
+ }
+ }
+
+ override fun searchAnimeSelector(): String = "div#infinite-list"
+
+ override fun searchAnimeNextPageSelector(): String? = popularAnimeNextPageSelector()
+
+ override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
+
+ // ============================== Filters ===============================
+
+ override fun getFilterList(): AnimeFilterList = AnimeFilterList(
+ AnimeFilter.Header("Text search ignores filters"),
+ SubPageFilter(),
+ GenreFilter(),
+ )
+
+ private class SubPageFilter : UriPartFilter(
+ "Sub-page",
+ arrayOf(
+ Pair("