new theme: Otaku Sanctuary (#14802)
* new theme: Otaku Sanctuary * linting i guess * i LOVE linting * add run configuration * rename and implement fixes from my other PR * make class open * otakusantheme -> otakusanctuary
|
@ -0,0 +1,11 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="OtakuSanctuaryGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
|
||||||
|
<module name="tachiyomi-extensions.multisrc.main" />
|
||||||
|
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.otakusanctuary.OtakuSanctuaryGenerator" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=otakusanctuary" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=otakusanctuary" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 200 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 194 KiB |
|
@ -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<SManga> {
|
||||||
|
val page = emptyList<SManga>().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<SChapter> {
|
||||||
|
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<Page> {
|
||||||
|
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<List<String>>(rawPagesArray).mapIndexed { idx, it ->
|
||||||
|
Page(idx, imageUrl = helper.processUrl(it, vi))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String>) {
|
||||||
|
OtakuSanctuaryGenerator().createAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|