This commit is contained in:
anenasa 2024-01-23 20:12:25 +08:00 committed by GitHub
parent 6db59cbe0d
commit b5cca97594
9 changed files with 323 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

12
src/zh/dm5/build.gradle Normal file
View File

@ -0,0 +1,12 @@
ext {
extName = 'Dm5'
extClass = '.Dm5'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:unpacker'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -0,0 +1,91 @@
package eu.kanade.tachiyomi.extension.zh.dm5
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.parser.Parser
import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream
// This file is modified from DMZJ extension
val json: Json by injectLazy()
@Serializable
class ChapterCommentDto(
val PostContent: String,
val Poster: String,
) {
override fun toString() = "$Poster: $PostContent"
}
fun parseChapterComments(response: Response): List<String> {
val result: List<ChapterCommentDto> = json.decodeFromString(response.body.string())
if (result.isEmpty()) return listOf("没有吐槽")
return result.map {
Parser.unescapeEntities(it.toString(), false)
}
}
object CommentsInterceptor : Interceptor {
private const val MAX_HEIGHT = 1920
private const val WIDTH = 1080
private const val UNIT = 32
private const val UNIT_F = UNIT.toFloat()
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (!response.request.url.toString().contains("pagerdata.ashx")) return response
val comments = parseChapterComments(response)
val paint = TextPaint().apply {
color = Color.BLACK
textSize = UNIT_F
isAntiAlias = true
}
var height = UNIT
val layouts = comments.map {
@Suppress("DEPRECATION")
StaticLayout(it, paint, WIDTH - 2 * UNIT, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
}.takeWhile {
val lineHeight = it.height + UNIT
if (height + lineHeight <= MAX_HEIGHT) {
height += lineHeight
true
} else {
false
}
}
val bitmap = Bitmap.createBitmap(WIDTH, height, Bitmap.Config.ARGB_8888)
bitmap.eraseColor(Color.WHITE)
val canvas = Canvas(bitmap)
var y = UNIT
for (layout in layouts) {
canvas.save()
canvas.translate(UNIT_F, y.toFloat())
layout.draw(canvas)
canvas.restore()
y += layout.height + UNIT
}
val output = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 0, output)
val body = output.toByteArray().toResponseBody("image/png".toMediaType())
return response.newBuilder().body(body).build()
}
}

View File

@ -0,0 +1,218 @@
package eu.kanade.tachiyomi.extension.zh.dm5
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class Dm5 : ParsedHttpSource(), ConfigurableSource {
override val lang = "zh"
override val supportsLatest = true
override val name = "动漫屋"
override val baseUrl = "https://www.dm5.cn"
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(CommentsInterceptor)
.build()
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
// Some mangas are blocked without this
override fun headersBuilder() = super.headersBuilder().set("Accept-Language", "zh-TW")
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manhua-list-p$page/", headers)
override fun popularMangaNextPageSelector(): String = "div.page-pagination a:contains(>)"
override fun popularMangaSelector(): String = "ul.mh-list > li > div.mh-item"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
title = element.selectFirst("h2.title > a")!!.text()
thumbnail_url = element.selectFirst("p.mh-cover")!!.attr("style")
.substringAfter("url(").substringBefore(")")
url = element.selectFirst("h2.title > a")!!.attr("href")
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manhua-list-s2-p$page/", headers)
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/search?title=$query&language=1&page=$page", headers)
}
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
override fun searchMangaSelector(): String = "ul.mh-list > li, div.banner_detail_form"
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
title = element.selectFirst(".title > a")!!.text()
thumbnail_url = element.selectFirst("img")?.attr("src")
?: element.selectFirst("p.mh-cover")!!.attr("style")
.substringAfter("url(").substringBefore(")")
url = element.selectFirst(".title > a")!!.attr("href")
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("div.banner_detail_form p.title")!!.ownText()
thumbnail_url = document.selectFirst("div.banner_detail_form img")!!.attr("abs:src")
author = document.selectFirst("div.banner_detail_form p.subtitle > a")!!.text()
artist = author
genre = document.select("div.banner_detail_form p.tip a").eachText().joinToString(", ")
val el = document.selectFirst("div.banner_detail_form p.content")!!
description = el.ownText() + el.selectFirst("span")?.ownText().orEmpty()
status = when (document.selectFirst("div.banner_detail_form p.tip > span > span")!!.text()) {
"连载中" -> SManga.ONGOING
"已完结" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
// May need to click button on website to read
document.selectFirst("ul#detail-list-select-1")?.attr("class")
?: throw Exception("請到webview確認")
val li = document.select("div#chapterlistload li > a").map {
SChapter.create().apply {
url = it.attr("href")
name = if (it.selectFirst("span.detail-lock, span.view-lock") != null) {
"\uD83D\uDD12"
} else {
""
} + (it.selectFirst("p.title")?.text() ?: it.text())
val dateStr = it.selectFirst("p.tip")
if (dateStr != null) {
date_upload = dateFormat.parse(dateStr.text())?.time ?: 0L
}
}
}
// Sort chapter by url (related to upload time)
if (preferences.getBoolean(SORT_CHAPTER_PREF, false)) {
return li.sortedByDescending { it.url.drop(2).dropLast(1).toInt() }
}
// Sometimes list is in ascending order, probably unread paid manga
return if (document.selectFirst("div.detail-list-title a.order")!!.text() == "正序") {
li.reversed()
} else {
li
}
}
override fun chapterListSelector(): String = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
override fun pageListParse(document: Document): List<Page> {
val images = document.select("div#barChapter > img.load-src")
val result: ArrayList<Page>
val script = document.selectFirst("script:containsData(DM5_MID)")!!.data()
if (!script.contains("DM5_VIEWSIGN_DT")) {
throw Exception(document.selectFirst("div.view-pay-form p.subtitle")!!.text())
}
val cid = script.substringAfter("var DM5_CID=").substringBefore(";")
if (!images.isEmpty()) {
result = images.mapIndexed { index, it ->
Page(index, "", it.attr("data-src"))
} as ArrayList<Page>
} else {
val mid = script.substringAfter("var DM5_MID=").substringBefore(";")
val dt = script.substringAfter("var DM5_VIEWSIGN_DT=\"").substringBefore("\";")
val sign = script.substringAfter("var DM5_VIEWSIGN=\"").substringBefore("\";")
val requestUrl = document.location()
val imageCount = script.substringAfter("var DM5_IMAGE_COUNT=").substringBefore(";").toInt()
result = (1..imageCount).map {
val url = requestUrl.toHttpUrl().newBuilder()
.addPathSegment("chapterfun.ashx")
.addQueryParameter("cid", cid)
.addQueryParameter("page", it.toString())
.addQueryParameter("key", "")
.addQueryParameter("language", "1")
.addQueryParameter("gtk", "6")
.addQueryParameter("_cid", cid)
.addQueryParameter("_mid", mid)
.addQueryParameter("_dt", dt)
.addQueryParameter("_sign", sign)
.build()
Page(it, url.toString())
} as ArrayList<Page>
}
if (preferences.getBoolean(CHAPTER_COMMENTS_PREF, false)) {
val pageSize = script.substringAfter("var DM5_PAGEPCOUNT = ").substringBefore(";")
val tid = script.substringAfter("var DM5_TIEBATOPICID='").substringBefore("'")
for (i in 1..pageSize.toInt()) {
result.add(
Page(
result.size,
"",
"$baseUrl/m$cid/pagerdata.ashx?pageindex=$i&pagesize=$pageSize&tid=$tid&cid=$cid&t=9",
),
)
}
}
return result
}
override fun imageUrlRequest(page: Page): Request {
val referer = page.url.substringBefore("chapterfun.ashx")
val header = headers.newBuilder().add("Referer", referer).build()
return GET(page.url, header)
}
override fun imageUrlParse(response: Response): String {
val script = Unpacker.unpack(response.body.string())
val pix = script.substringAfter("var pix=\"").substringBefore("\"")
val pvalue = script.substringAfter("var pvalue=[\"").substringBefore("\"")
val query = script.substringAfter("pix+pvalue[i]+\"").substringBefore("\"")
return pix + pvalue + query
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun imageRequest(page: Page): Request {
if (!page.imageUrl!!.contains("pagerdata.ashx")) {
return GET(page.imageUrl!!, headers)
}
val referer = page.imageUrl!!.substringBefore("pagerdata.ashx")
val header = headers.newBuilder().add("Referer", referer).build()
return GET(page.imageUrl!!, header)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val chapterCommentsPreference = SwitchPreferenceCompat(screen.context).apply {
key = CHAPTER_COMMENTS_PREF
title = "章末吐槽页"
summary = "修改后,已加载的章节需要清除章节缓存才能生效。"
setDefaultValue(false)
}
val sortChapterPreference = SwitchPreferenceCompat(screen.context).apply {
key = SORT_CHAPTER_PREF
title = "依照上傳時間排序章節"
setDefaultValue(false)
}
screen.addPreference(chapterCommentsPreference)
screen.addPreference(sortChapterPreference)
}
companion object {
private const val CHAPTER_COMMENTS_PREF = "chapterComments"
private const val SORT_CHAPTER_PREF = "sortChapter"
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
}
}