Add Dm5 (#492)
This commit is contained in:
parent
f3b39d57ef
commit
a4c0420bf7
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest />
|
|
@ -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 |
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue