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
This commit is contained in:
beerpsi 2023-01-09 17:28:07 +07:00 committed by GitHub
parent 7d7552af06
commit b417d07ad9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 418 additions and 0 deletions

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

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

View File

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

View File

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