diff --git a/multisrc/overrides/mangathemesia/opscans/res/web_hi_res_512.png b/multisrc/overrides/mangathemesia/opscans/res/web_hi_res_512.png
deleted file mode 100644
index c4e354264..000000000
Binary files a/multisrc/overrides/mangathemesia/opscans/res/web_hi_res_512.png and /dev/null differ
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaGenerator.kt
index dd54c786e..9e7097237 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaGenerator.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesiaGenerator.kt
@@ -116,7 +116,6 @@ class MangaThemesiaGenerator : ThemeSourceGenerator {
SingleLang("Nocturnal Scans", "https://nocturnalscans.com", "en", overrideVersionCode = 1),
SingleLang("Nonbiri", "https://nonbiri.space", "id"),
SingleLang("Noromax", "https://noromax.my.id", "id"),
- SingleLang("OPSCANS", "https://opscans.com", "all"),
SingleLang("Origami Orpheans", "https://origami-orpheans.com", "pt-BR", overrideVersionCode = 10),
SingleLang("Otsugami", "https://otsugami.id", "id"),
SingleLang("Ozul Scans", "https://kingofmanga.com", "ar", overrideVersionCode = 2),
diff --git a/src/en/opscans/AndroidManifest.xml b/src/en/opscans/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/src/en/opscans/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/en/opscans/build.gradle b/src/en/opscans/build.gradle
new file mode 100644
index 000000000..0746c85eb
--- /dev/null
+++ b/src/en/opscans/build.gradle
@@ -0,0 +1,11 @@
+ext {
+ extName = 'OPSCANS'
+ extClass = '.OpScans'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
+
+dependencies {
+ implementation(project(":lib:dataimage"))
+}
diff --git a/multisrc/overrides/mangathemesia/opscans/res/mipmap-hdpi/ic_launcher.png b/src/en/opscans/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangathemesia/opscans/res/mipmap-hdpi/ic_launcher.png
rename to src/en/opscans/res/mipmap-hdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangathemesia/opscans/res/mipmap-mdpi/ic_launcher.png b/src/en/opscans/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangathemesia/opscans/res/mipmap-mdpi/ic_launcher.png
rename to src/en/opscans/res/mipmap-mdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangathemesia/opscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/opscans/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangathemesia/opscans/res/mipmap-xhdpi/ic_launcher.png
rename to src/en/opscans/res/mipmap-xhdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangathemesia/opscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/opscans/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangathemesia/opscans/res/mipmap-xxhdpi/ic_launcher.png
rename to src/en/opscans/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangathemesia/opscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/opscans/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangathemesia/opscans/res/mipmap-xxxhdpi/ic_launcher.png
rename to src/en/opscans/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/src/en/opscans/src/eu/kanade/tachiyomi/extension/en/opscans/OpScans.kt b/src/en/opscans/src/eu/kanade/tachiyomi/extension/en/opscans/OpScans.kt
new file mode 100644
index 000000000..55fb9cc1b
--- /dev/null
+++ b/src/en/opscans/src/eu/kanade/tachiyomi/extension/en/opscans/OpScans.kt
@@ -0,0 +1,181 @@
+package eu.kanade.tachiyomi.extension.en.opscans
+
+import eu.kanade.tachiyomi.lib.dataimage.DataImageInterceptor
+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 kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.Headers
+import okhttp3.Interceptor
+import okhttp3.MultipartBody
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import uy.kohesive.injekt.injectLazy
+import java.lang.UnsupportedOperationException
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class OpScans : HttpSource() {
+
+ override val name = "OPSCANS"
+
+ override val lang = "en"
+
+ override val baseUrl = "https://opchapters.com"
+
+ private val apiUrl = "https://opscanlations.com"
+
+ override val supportsLatest = false
+
+ private val json: Json by injectLazy()
+
+ override val versionId = 2
+
+ override val client = network.cloudflareClient.newBuilder()
+ .addInterceptor(::imageInterceptor)
+ .addInterceptor(DataImageInterceptor())
+ .build()
+
+ override fun headersBuilder() = super.headersBuilder()
+ .set("Referer", "$baseUrl/")
+
+ override fun popularMangaRequest(page: Int): Request {
+ return GET("$apiUrl/api/mangaData", headers)
+ }
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val mangaData = response.parseAs>()
+
+ return MangasPage(
+ mangaData.map { it.toSManga() },
+ false,
+ )
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ return GET("$apiUrl/api/mangaData#${query.trim()}", headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val mangaData = response.parseAs>()
+ val query = response.request.url.fragment!!
+
+ return MangasPage(
+ mangaData.filter {
+ it.name.contains(query, true) ||
+ it.author.contains(query, true) ||
+ it.info.contains(query, true)
+ }.map { it.toSManga() },
+ false,
+ )
+ }
+
+ override fun mangaDetailsRequest(manga: SManga): Request {
+ return GET("$apiUrl/api/mangaData#${manga.url}", headers)
+ }
+
+ override fun getMangaUrl(manga: SManga) = "$baseUrl/${manga.url}"
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ val mangaData = response.parseAs>()
+ val mangaId = response.request.url.fragment!!
+
+ return mangaData.firstOrNull { it.id == mangaId }!!.toSManga()
+ }
+
+ override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
+
+ override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}"
+
+ override fun chapterListParse(response: Response): List {
+ val mangaData = response.parseAs>()
+ val mangaId = response.request.url.fragment!!
+
+ return mangaData.firstOrNull { it.id == mangaId }
+ ?.chapters.orEmpty().map {
+ SChapter.create().apply {
+ url = "/$mangaId/${it.id}"
+ name = it.number + if (it.title.isNullOrEmpty()) "" else ": ${it.title}"
+ date_upload = runCatching {
+ dateFormat.parse(it.date!!)!!.time
+ }.getOrDefault(0L)
+ }
+ }.reversed()
+ }
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ return GET("$apiUrl/api/mangaData#${chapter.url}", headers)
+ }
+
+ override fun pageListParse(response: Response): List {
+ val mangaData = response.parseAs>()
+ val ids = response.request.url.fragment!!.split("/")
+ val mangaId = ids[1]
+ val chapterId = ids[2]
+
+ return mangaData.firstOrNull { it.id == mangaId }
+ ?.chapters?.firstOrNull { it.id == chapterId }
+ ?.images.orEmpty().mapIndexed { idx, img ->
+ Page(idx, "", "https://127.0.0.1/image#${img.source}")
+ }
+ }
+
+ private fun imageInterceptor(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+ val url = request.url
+
+ if (url.pathSegments.lastOrNull() != "image" || url.fragment.isNullOrEmpty()) {
+ return chain.proceed(request)
+ }
+
+ val image = url.fragment!!
+
+ val boundary = buildString {
+ append((1..9).random())
+ repeat(28) {
+ append((0..9).random())
+ }
+ }
+
+ val form = MultipartBody.Builder("-----------------------------$boundary").apply {
+ setType(MultipartBody.FORM)
+ addPart(
+ Headers.headersOf("Content-Disposition", "form-data; name=\"image\""),
+ image.toRequestBody(null),
+ )
+ }.build()
+
+ val response = client.newCall(
+ POST("$apiUrl/api/loadImages", headers, form),
+ ).execute().parseAs()
+
+ val newUrl = "https://127.0.0.1/?${response.image.substringAfter(":")}"
+
+ return chain.proceed(
+ request.newBuilder()
+ .url(newUrl)
+ .build(),
+ )
+ }
+
+ private inline fun Response.parseAs(): T = use {
+ json.decodeFromString(it.body.string())
+ }
+
+ companion object {
+ private val dateFormat by lazy {
+ SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
+ }
+ }
+
+ override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
+ override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
+ override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
+}
diff --git a/src/en/opscans/src/eu/kanade/tachiyomi/extension/en/opscans/OpScansDto.kt b/src/en/opscans/src/eu/kanade/tachiyomi/extension/en/opscans/OpScansDto.kt
new file mode 100644
index 000000000..5feed9525
--- /dev/null
+++ b/src/en/opscans/src/eu/kanade/tachiyomi/extension/en/opscans/OpScansDto.kt
@@ -0,0 +1,46 @@
+package eu.kanade.tachiyomi.extension.en.opscans
+
+import eu.kanade.tachiyomi.source.model.SManga
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MangaData(
+ val id: String,
+ val name: String,
+ val author: String,
+ val info: String,
+ val genre1: String,
+ val genre2: String,
+ val genre3: String,
+ val cover: String,
+ val chapters: List,
+) {
+ fun toSManga() = SManga.create().apply {
+ url = id
+ title = name
+ author = this@MangaData.author
+ description = info
+ genre = listOf(genre1, genre2, genre3).joinToString()
+ thumbnail_url = "https://127.0.0.1/image#$cover"
+ initialized = true
+ }
+}
+
+@Serializable
+data class Chapter(
+ val id: String,
+ val title: String? = "",
+ val date: String? = "",
+ val number: String,
+ val images: List? = emptyList(),
+)
+
+@Serializable
+data class Image(
+ val source: String,
+)
+
+@Serializable
+data class ImageResponse(
+ val image: String,
+)