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, +)