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