New multisrc: Makaru (#980)

* New multisrc: Makaru

* remove YumeKomik from MangaThemesia

* Also remove from generator

* extension sdk is 21 actually
This commit is contained in:
beerpsi 2024-02-04 21:47:26 +07:00 committed by GitHub
parent 9c1fc837e8
commit bd52a178f7
22 changed files with 378 additions and 15 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 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: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -1,14 +0,0 @@
package eu.kanade.tachiyomi.extension.id.inazumanga
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import okhttp3.OkHttpClient
class YumeKomik : MangaThemesia("YumeKomik", "https://yumekomik.com", "id") {
override val client: OkHttpClient = super.client.newBuilder()
.rateLimit(3)
.build()
override val hasProjectPage = true
}

View File

@ -0,0 +1,217 @@
package eu.kanade.tachiyomi.multisrc.makaru
import android.annotation.SuppressLint
import android.os.Build
import eu.kanade.tachiyomi.multisrc.makaru.MakaruUtils.imgAttr
import eu.kanade.tachiyomi.multisrc.makaru.MakaruUtils.textWithNewlines
import eu.kanade.tachiyomi.network.GET
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
// multisrc SDK is 29 but extension SDK is 21.
@SuppressLint("ObsoleteSdkInt")
abstract class Makaru(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val supportsLatest = false
private val json: Json by injectLazy()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request {
val url = apiUrlBuilder("Series", page, MAX_MANGA_RESULTS).build()
return GET(url, headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val data = response.parseAs<MakaruDto>()
val manga = data.feed.entry.map { entry ->
val content = Jsoup.parseBodyFragment(entry.content.t, baseUrl)
SManga.create().apply {
setUrlWithoutDomain(entry.link.first { it.rel == "alternate" }.href)
title = entry.title.t
thumbnail_url = content.selectFirst("img")?.imgAttr()
}
}
val hasNextPage = (data.feed.startIndex.t.toInt() + data.feed.itemsPerPage.t.toInt()) <= data.feed.totalResults.t.toInt()
return MangasPage(manga, hasNextPage)
}
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = filters.ifEmpty { getFilterList() }
val searchQuery = buildString(13) {
append("label:Series")
filterList.filterIsInstance<LabelFilter>().forEach {
it.state
.filter { f -> f.state }
.forEach { f ->
append(" label:\"")
append(f.name)
append("\"")
}
}
if (query.isNotEmpty()) {
append(" ")
append(query)
}
}
val url = apiUrlBuilder("Series", page, MAX_MANGA_RESULTS).apply {
addQueryParameter("q", searchQuery)
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup()
val (_, data) = getMangaFeed(document)
val mangaContent = data.feed.entry.first { it.category.all { c -> c.term != "Chapter" } }
val mangaInfo = Jsoup.parseBodyFragment(mangaContent.content.t, baseUrl)
title = document.selectFirst("h1[itemprop=headline]")!!.text()
genre = document.select("div.genres a").joinToString { it.text() }
thumbnail_url = document.selectFirst("div.thumbnail img")?.imgAttr()
author = mangaInfo.select("span.komikus").joinToString { it.text() }
status = when (document.selectFirst("div.info-single-list ul li:contains(Status)")?.ownText()) {
"Ongoing" -> SManga.ONGOING
"Completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
val descriptionBuilder = StringBuilder()
val descriptionElement = mangaInfo.selectFirst("div.sinopsis")
for (entry in mangaInfo.select("li.info_x")) {
when (entry.selectFirst("strong")!!.text().removeSuffix(":")) {
"Artist" -> artist = entry.ownText()
else -> descriptionBuilder.appendLine(entry.text())
}
}
if (descriptionElement != null) {
descriptionBuilder.appendLine()
descriptionBuilder.append(descriptionElement.textWithNewlines())
}
description = descriptionBuilder.toString().trim()
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val (feed, data) = getMangaFeed(document)
return data.feed.entry
.filter { it.category.any { c -> c.term == "Chapter" } }
.map { entry ->
SChapter.create().apply {
setUrlWithoutDomain(entry.link.first { it.rel == "alternate" }.href)
name = entry.title.t.replace(feed, "").trim()
date_upload = try {
dateFormat.parse(entry.published.t)!!.time
} catch (e: ParseException) {
0L
}
}
}
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val script = document.selectFirst("script:containsData(data_content)")
?: throw Exception("Chapter image script not found")
val dataContent = script.data()
.substringAfter("let data_content = `")
.substringBefore("`;")
.replace(hexEscapeRegex) {
it.groupValues[1].toInt(16).toChar().toString()
}
return Jsoup.parseBodyFragment(dataContent, baseUrl).select("img").mapIndexed { i, it ->
Page(i, imageUrl = it.imgAttr())
}
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
override fun getFilterList() = FilterList(
LabelFilter("Status", getStatusList()),
LabelFilter("Type", getTypeList()),
LabelFilter("Genre", getGenreList()),
)
private val hexEscapeRegex = Regex("""\\x([0-9A-Za-z]{2})""")
private val dateFormat by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.getDefault())
} else {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
}
}
private fun getMangaFeed(document: Document): Pair<String, MakaruDto> {
val script = document.selectFirst("script:containsData(clwd.run)")
?: throw Exception("Cannot find manga feed name")
val feed = script.data().substringAfter("clwd.run('").substringBefore("');")
return feed to client.newCall(
GET(
apiUrlBuilder(feed, 1, MAX_CHAPTER_RESULTS).build(),
headers,
),
)
.execute()
.parseAs<MakaruDto>()
}
private fun apiUrlBuilder(feed: String, page: Int, maxResults: Int) = baseUrl.toHttpUrl().newBuilder().apply {
// Blogger indices start from 1
val startIndex = maxResults * (page - 1) + 1
addPathSegments("feeds/posts/default/-/")
addPathSegment(feed)
addQueryParameter("alt", "json")
addQueryParameter("max-results", maxResults.toString())
addQueryParameter("start-index", startIndex.toString())
}
private inline fun <reified T> Response.parseAs(): T =
json.decodeFromString(body.string())
companion object {
private const val MAX_MANGA_RESULTS = 20
private const val MAX_CHAPTER_RESULTS = 999999
}
}

View File

@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.multisrc.makaru
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class MakaruDto(
val feed: MakaruFeedDto,
)
@Serializable
data class MakaruFeedDto(
@SerialName("openSearch\$totalResults") val totalResults: MakaruTextDto,
@SerialName("openSearch\$startIndex") val startIndex: MakaruTextDto,
@SerialName("openSearch\$itemsPerPage") val itemsPerPage: MakaruTextDto,
val entry: List<MakaruFeedEntryDto> = emptyList(),
)
@Serializable
data class MakaruFeedEntryDto(
val published: MakaruTextDto,
val category: List<MakaruCategoryDto>,
val title: MakaruTextDto,
val content: MakaruTextDto,
val link: List<MakaruLinkDto>,
val author: List<MakaruAuthorDto>,
)
@Serializable
data class MakaruLinkDto(
val rel: String,
val href: String,
)
@Serializable
data class MakaruCategoryDto(
val term: String,
)
@Serializable
data class MakaruAuthorDto(
val name: MakaruTextDto,
)
@Serializable
data class MakaruTextDto(
@SerialName("\$t") val t: String,
)

View File

@ -0,0 +1,69 @@
package eu.kanade.tachiyomi.multisrc.makaru
import eu.kanade.tachiyomi.source.model.Filter
class LabelFilter(name: String, labels: List<Label>) : Filter.Group<Label>(name, labels)
class Label(name: String) : Filter.CheckBox(name)
fun getStatusList() = listOf(
Label("Ongoing"),
Label("Completed"),
)
fun getTypeList() = listOf(
Label("Manga"),
Label("Manhwa"),
Label("Manhua"),
)
fun getGenreList() = listOf(
Label("Action"),
Label("Adventure"),
Label("Comedy"),
Label("Drama"),
Label("Fantasy"),
Label("Horror"),
Label("Mystery"),
Label("Romance"),
Label("Sci-Fi"),
Label("Slice of Life"),
Label("Sports"),
Label("Supernatural"),
Label("Suspense"),
Label("Ecchi"),
Label("Detective"),
Label("Educational"),
Label("Harem"),
Label("Game"),
Label("Historical"),
Label("Isekai"),
Label("Mahou Shoujo"),
Label("Martial Arts"),
Label("Mecha"),
Label("Medical"),
Label("Memoir"),
Label("Military"),
Label("Music"),
Label("Mythology"),
Label("Parody"),
Label("Psychological"),
Label("Racing"),
Label("Reincarnation"),
Label("Samurai"),
Label("School"),
Label("Strategy"),
Label("Super Power"),
Label("Survival"),
Label("Time Travel"),
Label("Vampire"),
Label("Villain"),
Label("Workplace"),
Label("Josei"),
Label("Kids"),
Label("Seinen"),
Label("Shoujo"),
Label("Shounen"),
Label("Project"),
Label("Mirror"),
)

View File

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.multisrc.makaru
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class MakaruGenerator : ThemeSourceGenerator {
override val themePkg = "makaru"
override val themeClass = "Makaru"
override val baseVersionCode = 1
override val sources = listOf(
SingleLang("KomikGes", "https://www.komikges.my.id", "id"),
SingleLang("ReYume", "https://www.re-yume.my.id", "id", pkgName = "inazumanga", overrideVersionCode = 34),
SingleLang("YuraManga", "https://www.yuramanga.my.id", "id"),
)
companion object {
@JvmStatic
fun main(args: Array<String>) {
MakaruGenerator().createAll()
}
}
}

View File

@ -0,0 +1,18 @@
package eu.kanade.tachiyomi.multisrc.makaru
import org.jsoup.nodes.Element
object MakaruUtils {
fun Element.textWithNewlines() = run {
select("p, br").prepend("\\n")
text().replace("\\n", "\n").replace("\n ", "\n")
}
fun Element.imgAttr(): String = when {
hasAttr("data-cfsrc") -> absUrl("data-cfsrc")
hasAttr("data-lazy-src") -> absUrl("data-lazy-src")
hasAttr("data-src") -> absUrl("data-src").substringBefore(" ")
hasAttr("srcset") -> absUrl("srcset").substringBefore(" ")
else -> absUrl("src")
}
}

View File

@ -155,7 +155,6 @@ class MangaThemesiaGenerator : ThemeSourceGenerator {
SingleLang("Walpurgi Scan", "https://www.walpurgiscan.it", "it", overrideVersionCode = 7, className = "WalpurgisScan"),
SingleLang("West Manga", "https://westmanga.fun", "id", overrideVersionCode = 3),
SingleLang("xCaliBR Scans", "https://xcalibrscans.com", "en", overrideVersionCode = 5),
SingleLang("YumeKomik", "https://yumekomik.com", "id", isNsfw = true, className = "YumeKomik", pkgName = "inazumanga", overrideVersionCode = 6),
SingleLang("Zahard", "https://zahard.xyz", "en"),
SingleLang("Area Manga", "https://www.areascans.net", "ar", sourceName = "أريا مانجا"),
SingleLang("Vex Manga", "https://vexmanga.com", "ar", sourceName = "فيكس مانجا", overrideVersionCode = 3),