diff --git a/.run/OtakuSanctuaryGenerator.run.xml b/.run/OtakuSanctuaryGenerator.run.xml
new file mode 100644
index 000000000..3cc3e7220
--- /dev/null
+++ b/.run/OtakuSanctuaryGenerator.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..bf96faa9e
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..5b6875aa5
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..28e864743
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..5727af549
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..7fca018db
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/myrockmanga/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/myrockmanga/res/web_hi_res_512.png b/multisrc/overrides/otakusanctuary/myrockmanga/res/web_hi_res_512.png
new file mode 100644
index 000000000..322c117e6
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/myrockmanga/res/web_hi_res_512.png differ
diff --git a/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..28c5aca29
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..93fafb48e
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..2027839e3
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..5cd3d0df1
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..96dd9f512
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/otakusanctuary/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/otakusanctuary/otakusanctuary/res/web_hi_res_512.png b/multisrc/overrides/otakusanctuary/otakusanctuary/res/web_hi_res_512.png
new file mode 100644
index 000000000..d417f7345
Binary files /dev/null and b/multisrc/overrides/otakusanctuary/otakusanctuary/res/web_hi_res_512.png differ
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/otakusanctuary/OtakuSanctuary.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/otakusanctuary/OtakuSanctuary.kt
new file mode 100644
index 000000000..e275e712c
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/otakusanctuary/OtakuSanctuary.kt
@@ -0,0 +1,214 @@
+package eu.kanade.tachiyomi.multisrc.otakusanctuary
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import okhttp3.FormBody
+import okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.select.Elements
+import org.jsoup.select.Evaluator
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+open class OtakuSanctuary(
+ override val name: String,
+ override val baseUrl: String,
+ override val lang: String,
+) : HttpSource() {
+
+ override val supportsLatest = false
+
+ override val client = network.cloudflareClient
+
+ override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", "$baseUrl/")
+
+ private val helper = OtakuSanctuaryHelper(lang)
+
+ private val json: Json by injectLazy()
+
+ // There's no popular list, this will have to do
+ override fun popularMangaRequest(page: Int) = POST(
+ "$baseUrl/Manga/Newest",
+ headers,
+ FormBody.Builder().apply {
+ add("Lang", helper.otakusanLang())
+ add("PageSize", "24")
+ }.build()
+ )
+
+ private fun parseMangaCollection(elements: Elements): List {
+ val page = emptyList().toMutableList()
+
+ for (element in elements) {
+ val url = element.select("div.mdl-card__title a").first().attr("abs:href")
+ // ignore external chapters
+ if (url.toHttpUrl().host != baseUrl.toHttpUrl().host) {
+ continue
+ }
+
+ // ignore web novels/light novels
+ val variant = element.select("div.mdl-card__supporting-text div.text-overflow-90 a").text()
+ if (variant.contains("Novel")) {
+ continue
+ }
+
+ // ignore languages that dont match current ext
+ val language = element.select("img.flag").attr("abs:src")
+ .substringAfter("flags/")
+ .substringBefore(".png")
+ if (helper.otakusanLang() != "all" && language != helper.otakusanLang()) {
+ continue
+ }
+
+ page += SManga.create().apply {
+ setUrlWithoutDomain(url)
+ title = element.select("div.mdl-card__supporting-text a[target=_blank]").text()
+ .replaceFirstChar { it.titlecase() }
+ thumbnail_url = element.select("div.container-3-4.background-contain img").first().attr("abs:src")
+ }
+ }
+ return page
+ }
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val document = response.asJsoup()
+ val collection = document.select("div.mdl-card")
+ return MangasPage(parseMangaCollection(collection), collection.size >= 24)
+ }
+
+ override fun latestUpdatesRequest(page: Int) = throw Exception("Unused")
+
+ override fun latestUpdatesParse(response: Response) = throw Exception("Unused")
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
+ GET(
+ baseUrl.toHttpUrl().newBuilder().apply {
+ addPathSegments("Home/Search")
+ addQueryParameter("search", query)
+ }.build().toString(),
+ headers
+ )
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val document = response.asJsoup()
+ val collection = document.select("div.collection:has(.group-header:contains(Manga)) div.mdl-card")
+ return MangasPage(parseMangaCollection(collection), false)
+ }
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ val document = response.asJsoup()
+
+ return SManga.create().apply {
+ title = document.select("h1.title.text-lg-left.text-overflow-2-line")
+ .text()
+ .replaceFirstChar { it.titlecase() }
+ author = document.select("tr:contains(Tác Giả) a.capitalize").first().text()
+ .replaceFirstChar { it.titlecase() }
+ description = document.select("div.summary p").joinToString("\n") {
+ it.run {
+ select(Evaluator.Tag("br")).prepend("\\n")
+ this.text().replace("\\n", "\n").replace("\n ", "\n")
+ }
+ }.trim()
+ genre = document.select("div.genres a").joinToString { it.text() }
+ thumbnail_url = document.select("div.container-3-4.background-contain img").attr("abs:src")
+
+ val statusString = document.select("tr:contains(Tình Trạng) td").first().text().trim()
+ status = when (statusString) {
+ "Ongoing" -> SManga.ONGOING
+ "Done" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ }
+ }
+
+ private val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.US).apply {
+ timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh")
+ }
+
+ private fun parseDate(date: String): Long {
+ if (date.contains("cách đây")) {
+ val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
+ val cal = Calendar.getInstance()
+
+ return when {
+ date.contains("ngày") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
+ date.contains("tiếng") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
+ date.contains("phút") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
+ date.contains("giây") -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
+ else -> 0L
+ }
+ } else {
+ return kotlin.runCatching { dateFormat.parse(date)?.time }.getOrNull() ?: 0L
+ }
+ }
+
+ override fun chapterListParse(response: Response): List {
+ val document = response.asJsoup()
+
+ return document.select("tr.chapter").map {
+ val cells = it.select("td")
+ SChapter.create().apply {
+ setUrlWithoutDomain(cells[1].select("a").attr("href"))
+ name = cells[1].text()
+ date_upload = parseDate(cells[3].text())
+ chapter_number = cells[0].text().toFloatOrNull() ?: -1f
+ }
+ }
+ }
+
+ override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used")
+
+ override fun pageListParse(response: Response): List {
+ val document = response.asJsoup()
+
+ val vi = document.select("#dataip").attr("value")
+ val numericId = document.select("#inpit-c").attr("data-chapter-id")
+
+ val rawPagesArray = try {
+ val data = json.parseToJsonElement(
+ client.newCall(
+ POST(
+ "$baseUrl/Manga/CheckingAlternate",
+ headers,
+ FormBody.Builder().add("chapId", numericId).build()
+ )
+ ).execute().body!!.string()
+ )
+
+ data.jsonObject["Content"]!!.jsonPrimitive.content
+ } catch (_: Exception) {
+ val data = json.parseToJsonElement(
+ client.newCall(
+ POST(
+ "$baseUrl/Manga/UpdateView",
+ headers,
+ FormBody.Builder().add("chapId", numericId).build()
+ )
+ ).execute().body!!.string()
+ )
+
+ data.jsonObject["view"]!!.jsonPrimitive.content
+ }
+
+ return json.decodeFromString>(rawPagesArray).mapIndexed { idx, it ->
+ Page(idx, imageUrl = helper.processUrl(it, vi))
+ }
+ }
+}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/otakusanctuary/OtakuSanctuaryGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/otakusanctuary/OtakuSanctuaryGenerator.kt
new file mode 100644
index 000000000..86736e298
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/otakusanctuary/OtakuSanctuaryGenerator.kt
@@ -0,0 +1,35 @@
+package eu.kanade.tachiyomi.multisrc.otakusanctuary
+
+import generator.ThemeSourceData.MultiLang
+import generator.ThemeSourceGenerator
+
+class OtakuSanctuaryGenerator : ThemeSourceGenerator {
+
+ override val themePkg = "otakusanctuary"
+
+ override val themeClass = "OtakuSanctuary"
+
+ override val baseVersionCode: Int = 1
+
+ override val sources = listOf(
+ MultiLang(
+ "Otaku Sanctuary",
+ "https://otakusan.net",
+ listOf("all", "vi", "en", "it", "fr", "es"),
+ isNsfw = true
+ ),
+ MultiLang(
+ "MyRockManga",
+ "https://myrockmanga.com",
+ listOf("all", "vi", "en", "it", "fr", "es"),
+ isNsfw = true
+ ),
+ )
+
+ companion object {
+ @JvmStatic
+ fun main(args: Array) {
+ OtakuSanctuaryGenerator().createAll()
+ }
+ }
+}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/otakusanctuary/OtakuSanctuaryHelper.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/otakusanctuary/OtakuSanctuaryHelper.kt
new file mode 100644
index 000000000..c9bd13d14
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/otakusanctuary/OtakuSanctuaryHelper.kt
@@ -0,0 +1,158 @@
+package eu.kanade.tachiyomi.multisrc.otakusanctuary
+
+import okhttp3.HttpUrl.Companion.toHttpUrl
+
+class OtakuSanctuaryHelper(private val lang: String) {
+
+ fun otakusanLang() = when (lang) {
+ "vi" -> "vn"
+ "en" -> "us"
+ else -> lang
+ }
+
+ fun processUrl(url: String, vi: String): String {
+ var url = url.replace("_h_", "http")
+ .replace("_e_", "/extendContent/Manga")
+ .replace("_r_", "/extendContent/MangaRaw")
+
+ if (url.startsWith("//")) {
+ url = "https:$url"
+ }
+ if (url.contains("drive.google.com")) {
+ return url
+ }
+
+ url = when (url.slice(0..4)) {
+ "[GDP]" -> url.replace("[GDP]", "https://drive.google.com/uc?export=view&id=")
+ "[GDT]" -> if (otakusanLang() == "us") {
+ url.replace("image2.otakuscan.net", "image3.shopotaku.net")
+ .replace("image2.otakusan.net", "image3.shopotaku.net")
+ } else {
+ url
+ }
+ "[IS1]" -> {
+ var url = url.replace("[IS1]", "https://imagepi.otakuscan.net/")
+ if (url.contains("vi") && url.contains("otakusan.net_")) {
+ url
+ } else {
+ url.toHttpUrl().newBuilder().apply {
+ addQueryParameter("vi", vi)
+ }.build().toString()
+ }
+ }
+ "[IS3]" -> url.replace("[IS3]", "https://image3.otakusan.net/")
+ "[IO3]" -> url.replace("[IO3]", "http://image3.shopotaku.net/")
+ else -> url
+ }
+
+ if (url.contains("/Content/Workshop") || url.contains("otakusan") || url.contains("myrockmanga")) {
+ return url
+ }
+
+ if (url.contains("file-bato-orig.anyacg.co")) {
+ url = url.replace("file-bato-orig.anyacg.co", "file-bato-orig.bato.to")
+ }
+
+ if (url.contains("file-comic")) {
+ if (url.contains("file-comic-1")) {
+ url = url.replace("file-comic-1.anyacg.co", "z-img-01.mangapark.net")
+ }
+ if (url.contains("file-comic-2")) {
+ url = url.replace("file-comic-2.anyacg.co", "z-img-02.mangapark.net")
+ }
+ if (url.contains("file-comic-3")) {
+ url = url.replace("file-comic-3.anyacg.co", "z-img-03.mangapark.net")
+ }
+ if (url.contains("file-comic-4")) {
+ url = url.replace("file-comic-4.anyacg.co", "z-img-04.mangapark.net")
+ }
+ if (url.contains("file-comic-5")) {
+ url = url.replace("file-comic-5.anyacg.co", "z-img-05.mangapark.net")
+ }
+ if (url.contains("file-comic-6")) {
+ url = url.replace("file-comic-6.anyacg.co", "z-img-06.mangapark.net")
+ }
+ if (url.contains("file-comic-9")) {
+ url = url.replace("file-comic-9.anyacg.co", "z-img-09.mangapark.net")
+ }
+ if (url.contains("file-comic-10")) {
+ url = url.replace("file-comic-10.anyacg.co", "z-img-10.mangapark.net")
+ }
+ if (url.contains("file-comic-99")) {
+ url = url.replace("file-comic-99.anyacg.co/uploads", "file-bato-0001.bato.to")
+ }
+ }
+
+ if (url.contains("cdn.nettruyen.com")) {
+ url = url.replace(
+ "cdn.nettruyen.com/Data/Images/",
+ "truyen.cloud/data/images/",
+ )
+ }
+ if (url.contains("url=")) {
+ url = url.substringAfter("url=")
+ }
+ if (url.contains("blogspot") || url.contains("fshare")) {
+ url = url.replace("http:", "https:")
+ }
+ if (url.contains("blogspot") && !url.contains("http")) {
+ url = "https://$url"
+ }
+ if (url.contains("app/manga/uploads/") && !url.contains("http")) {
+ url = "https://lhscan.net$url"
+ }
+ url = url.replace("//cdn.adtrue.com/rtb/async.js", "")
+
+ if (url.contains(".webp")) {
+ url = "https://otakusan.net/api/Value/ImageSyncing?ip=34512351".toHttpUrl().newBuilder()
+ .apply {
+ addQueryParameter("url", url)
+ }.build().toString()
+ } else if (
+ (
+ url.contains("merakiscans") ||
+ url.contains("mangazuki") ||
+ url.contains("ninjascans") ||
+ url.contains("anyacg.co") ||
+ url.contains("mangakatana") ||
+ url.contains("zeroscans") ||
+ url.contains("mangapark") ||
+ url.contains("mangadex") ||
+ url.contains("uptruyen") ||
+ url.contains("hocvientruyentranh") ||
+ url.contains("ntruyen.info") ||
+ url.contains("chancanvas") ||
+ url.contains("bato.to")
+ ) &&
+ (
+ !url.contains("googleusercontent") &&
+ !url.contains("otakusan") &&
+ !url.contains("otakuscan") &&
+ !url.contains("shopotaku")
+ )
+ ) {
+ url =
+ "https://images2-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&no_expand=1&resize_h=0&rewriteMime=image%2F*".toHttpUrl()
+ .newBuilder().apply {
+ addQueryParameter("url", url)
+ }.build().toString()
+ } else if (url.contains("imageinstant.com")) {
+ url = "https://images.weserv.nl/".toHttpUrl().newBuilder().apply {
+ addQueryParameter("url", url)
+ }.build().toString()
+ } else if (!url.contains("otakusan.net")) {
+ url = "https://otakusan.net/api/Value/ImageSyncing?ip=34512351".toHttpUrl().newBuilder()
+ .apply {
+ addQueryParameter("url", url)
+ }.build().toString()
+ }
+
+ return if (url.contains("vi=") && !url.contains("otakusan.net_")) {
+ url
+ } else {
+ url.toHttpUrl().newBuilder().apply {
+ addQueryParameter("vi", vi)
+ }.build().toString()
+ }
+ }
+}