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() + } + } +}