[zh-dmzj]Add fallback api to fetch hidden manga and URL intent filter. (#5624)

This commit is contained in:
Oldwangtouchtouchdoge 2021-02-03 01:56:39 +08:00 committed by GitHub
parent bba235621c
commit b1d483f293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 268 additions and 70 deletions

View File

@ -1,2 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".zh.dmzj.DmzjUrlActivity"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="m.dmzj.com"
android:pathPattern="/info/..*"
android:scheme="https" />
<data
android:host="www.dmzj.com"
android:pathPattern="/info/..*"
android:scheme="https" />
<data
android:host="manhua.dmzj.com"
android:pathPattern="/..*"
android:scheme="https" />
<data
android:host="m.dmzj1.com"
android:pathPattern="/info/..*"
android:scheme="https" />
<data
android:host="www.dmzj1.com"
android:pathPattern="/info/..*"
android:scheme="https" />
<data
android:host="manhua.dmzj1.com"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -5,7 +5,7 @@ ext {
extName = 'Dmzj'
pkgNameSuffix = 'zh.dmzj'
extClass = '.Dmzj'
extVersionCode = 15
extVersionCode = 16
libVersion = '1.2'
}

View File

@ -16,6 +16,8 @@ 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 okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
@ -37,7 +39,9 @@ class Dmzj : ConfigurableSource, HttpSource() {
override val supportsLatest = true
override val name = "动漫之家"
override val baseUrl = "https://m.dmzj1.com"
private val apiUrl = "https://v3api.dmzj1.com"
private val v3apiUrl = "https://v3api.dmzj1.com"
private val apiUrl = "https://api.dmzj.com"
private val oldPageListApiUrl = "https://m.dmzj.com/chapinfo"
private val imageCDNUrl = "https://images.dmzj1.com"
private fun cleanUrl(url: String) = if (url.startsWith("//"))
@ -48,6 +52,10 @@ class Dmzj : ConfigurableSource, HttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val v3apiRateLimitInterceptor = SpecificHostRateLimitInterceptor(
HttpUrl.parse(v3apiUrl)!!,
preferences.getString(API_RATELIMIT_PREF, "5")!!.toInt()
)
private val apiRateLimitInterceptor = SpecificHostRateLimitInterceptor(
HttpUrl.parse(apiUrl)!!,
preferences.getString(API_RATELIMIT_PREF, "5")!!.toInt()
@ -59,12 +67,13 @@ class Dmzj : ConfigurableSource, HttpSource() {
override val client: OkHttpClient = network.client.newBuilder()
.addNetworkInterceptor(apiRateLimitInterceptor)
.addNetworkInterceptor(v3apiRateLimitInterceptor)
.addNetworkInterceptor(imageCDNRateLimitInterceptor)
.build()
private fun myGet(url: String) = GET(url)
.newBuilder()
.header(
override fun headersBuilder() = Headers.Builder().apply {
set("Referer", "https://www.dmzj1.com/")
set(
"User-Agent",
"Mozilla/5.0 (Linux; Android 10) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
@ -72,7 +81,7 @@ class Dmzj : ConfigurableSource, HttpSource() {
"Mobile Safari/537.36 " +
"Tachiyomi/1.0"
)
.build()!!
}
// for simple searches (query only, no filters)
private fun simpleSearchJsonParse(json: String): MangasPage {
@ -117,19 +126,53 @@ class Dmzj : ConfigurableSource, HttpSource() {
return MangasPage(ret, arr.length() != 0)
}
override fun popularMangaRequest(page: Int) = myGet("$apiUrl/classify/0/0/${page - 1}.json")
override fun popularMangaRequest(page: Int) = GET("$v3apiUrl/classify/0/0/${page - 1}.json")
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int) = myGet("$apiUrl/classify/0/1/${page - 1}.json")
override fun latestUpdatesRequest(page: Int) = GET("$v3apiUrl/classify/0/1/${page - 1}.json")
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
private fun searchMangaById(id: String): MangasPage {
val comicNumberID = if (checkComicIdIsNumericalRegex.matches(id)) {
id
} else {
val document = client.newCall(GET("$baseUrl/info/$id.html", headers)).execute().asJsoup()
extractComicIdFromWebpageRegex.find(document.select("#Subscribe").attr("onclick"))!!.groups[1]!!.value // onclick="addSubscribe('{comicNumberID}')"
}
val sManga = try {
val r = client.newCall(GET("$v3apiUrl/comic/comic_$comicNumberID.json", headers)).execute()
mangaDetailsParse(r)
} catch (_: Exception) {
val r = client.newCall(GET("$apiUrl/dynamic/comicinfo/$comicNumberID.json", headers)).execute()
mangaDetailsParse(r)
}
sManga.url = "$baseUrl/info/$comicNumberID.html"
return MangasPage(listOf(sManga), false)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) {
// ID may be numbers or Chinese pinyin
val id = query.removePrefix(PREFIX_ID_SEARCH).removeSuffix(".html")
Observable.just(searchMangaById(id))
} else {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response)
}
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query != "") {
val uri = Uri.parse("http://s.acg.dmzj1.com/comicsum/search.php").buildUpon()
uri.appendQueryParameter("s", query)
return myGet(uri.toString())
return GET(uri.toString())
} else {
var params = filters.map {
if (it !is SortFilter && it is UriPartFilter) {
@ -142,7 +185,7 @@ class Dmzj : ConfigurableSource, HttpSource() {
val order = filters.filterIsInstance<SortFilter>().joinToString("") { (it as UriPartFilter).toUriPart() }
return myGet("$apiUrl/classify/$params/$order/${page - 1}.json")
return GET("$v3apiUrl/classify/$params/$order/${page - 1}.json")
}
}
@ -156,31 +199,35 @@ class Dmzj : ConfigurableSource, HttpSource() {
}
}
// Bypass mangaDetailsRequest, fetch v3api url directly
// Bypass mangaDetailsRequest, fetch api url directly
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(GET(apiUrl + manga.url, headers))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true }
val cid = extractComicIdFromMangaUrlRegex.find(manga.url)!!.groups[1]!!.value
return try {
// Not using client.newCall().asObservableSuccess() to ensure we can catch exception here.
val response = client.newCall(GET("$v3apiUrl/comic/comic_$cid.json", headers)).execute()
val sManga = mangaDetailsParse(response).apply { initialized = true }
Observable.just(sManga)
} catch (e: Exception) {
val response = client.newCall(GET("$apiUrl/dynamic/comicinfo/$cid.json", headers)).execute()
val sManga = mangaDetailsParse(response).apply { initialized = true }
Observable.just(sManga)
} catch (e: Exception) {
Observable.error(e)
}
}
private val re1 = Regex("""\d+""") // Get comic ID from manga.url
// Workaround to allow "Open in browser" use human readable webpage url.
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl/info/${re1.find(manga.url)!!.value}.html")
}
override fun chapterListRequest(manga: SManga): Request {
return GET(apiUrl + manga.url, headers)
val cid = extractComicIdFromMangaUrlRegex.find(manga.url)!!.groups[1]!!.value
return GET("$baseUrl/info/$cid.html")
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val obj = JSONObject(response.body()!!.string())
if (response.request().url().toString().startsWith(v3apiUrl)) {
title = obj.getString("title")
thumbnail_url = obj.getString("cover")
var arr = obj.getJSONArray("authors")
val tmparr = ArrayList<String>(arr.length())
for (i in 0 until arr.length()) {
@ -201,19 +248,55 @@ class Dmzj : ConfigurableSource, HttpSource() {
}
description = obj.getString("description")
} else {
val data = obj.getJSONObject("data").getJSONObject("info")
title = data.getString("title")
thumbnail_url = data.getString("cover")
author = data.getString("authors")
genre = data.getString("types").replace("/", ", ")
status = when (data.getString("status")) {
"连载中" -> SManga.ONGOING
"已完结" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
description = data.getString("description")
}
}
override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException("Not used.")
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val cid = extractComicIdFromMangaUrlRegex.find(manga.url)!!.groups[1]!!.value
return if (manga.status != SManga.LICENSED) {
try {
val response = client.newCall(GET("$v3apiUrl/comic/comic_$cid.json", headers)).execute()
val sChapter = chapterListParse(response)
Observable.just(sChapter)
} catch (e: Exception) {
val response = client.newCall(GET("$apiUrl/dynamic/comicinfo/$cid.json", headers)).execute()
val sChapter = chapterListParse(response)
Observable.just(sChapter)
} catch (e: Exception) {
Observable.error(e)
}
} else {
Observable.error(Exception("Licensed - No chapters to show"))
}
}
override fun chapterListParse(response: Response): List<SChapter> {
val obj = JSONObject(response.body()!!.string())
val ret = ArrayList<SChapter>()
if (response.request().url().toString().startsWith(v3apiUrl)) {
val cid = obj.getString("id")
val arr = obj.getJSONArray("chapters")
for (i in 0 until arr.length()) {
val obj2 = arr.getJSONObject(i)
val arr2 = obj2.getJSONArray("data")
val prefix = obj2.getString("title")
for (j in 0 until arr2.length()) {
val chapter = arr2.getJSONObject(j)
val chaptersList = obj.getJSONArray("chapters")
for (i in 0 until chaptersList.length()) {
val chapterObj = chaptersList.getJSONObject(i)
val chapterData = chapterObj.getJSONArray("data")
val prefix = chapterObj.getString("title")
for (j in 0 until chapterData.length()) {
val chapter = chapterData.getJSONObject(j)
ret.add(
SChapter.create().apply {
name = "$prefix: ${chapter.getString("chapter_title")}"
@ -223,14 +306,31 @@ class Dmzj : ConfigurableSource, HttpSource() {
)
}
}
} else {
// Fallback to old api
val chaptersList = obj.getJSONObject("data").getJSONArray("list")
for (i in 0 until chaptersList.length()) {
val chapter = chaptersList.getJSONObject(i)
ret.add(
SChapter.create().apply {
name = chapter.getString("chapter_name")
date_upload = chapter.getString("updatetime").toLong() * 1000
url = "$oldPageListApiUrl/${chapter.getString("comic_id")}/${chapter.getString("id")}.html"
}
)
}
}
return ret
}
override fun pageListRequest(chapter: SChapter) = GET(chapter.url, headers) // Bypass base url
override fun pageListParse(response: Response): List<Page> {
val arr = if (response.request().url().toString().startsWith(oldPageListApiUrl)) {
JSONObject(response.body()!!.string()).getJSONArray("page_url")
} else {
// some chapters are hidden and won't return a JSONObject from api.m.dmzj, have to get them through v3api (but images won't be as HQ)
val arr = try {
try {
val obj = JSONObject(response.body()!!.string())
obj.getJSONObject("chapter").getJSONArray("page_url") // api.m.dmzj1.com already return HD image url
} catch (_: Exception) {
@ -241,10 +341,22 @@ class Dmzj : ConfigurableSource, HttpSource() {
.replace(".html", ".json")
val obj = client.newCall(GET(url, headers)).execute().let { JSONObject(it.body()!!.string()) }
obj.getJSONArray("page_url_hd") // page_url in v3api.dmzj1.com will return compressed image, page_url_hd will return HD image url as api.m.dmzj1.com does.
} catch (_: Exception) {
// Fallback to old api
// example url: https://m.dmzj.com/chapinfo/44253/101852.html
val url = response.request().url().toString()
.replaceFirst("api.", "")
.replaceFirst(".dmzj1.", ".dmzj.")
.replaceFirst("comic/chapter", "chapinfo")
val obj = client.newCall(GET(url, headers)).execute().let { JSONObject(it.body()!!.string()) }
obj.getJSONArray("page_url")
}
}
val ret = ArrayList<Page>(arr.length())
for (i in 0 until arr.length()) {
ret.add(Page(i, "", arr.getString(i).replace("http:", "https:")))
ret.add(
Page(i, "", arr.getString(i).replace("http:", "https:").replace("dmzj.com", "dmzj1.com"))
)
}
return ret
}
@ -359,10 +471,6 @@ class Dmzj : ConfigurableSource, HttpSource() {
)
)
// Headers
override fun headersBuilder() =
super.headersBuilder().add("Referer", "https://www.dmzj1.com/")!!
private open class UriPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
@ -466,6 +574,11 @@ class Dmzj : ConfigurableSource, HttpSource() {
private const val IMAGE_CDN_RATELIMIT_PREF_TITLE = "图片CDN每秒连接数限制" // "Ratelimit permits per second for image CDN"
private const val IMAGE_CDN_RATELIMIT_PREF_SUMMARY = "此值影响加载图片时发起连接请求的数量。调低此值可能减小图片加载错误的几率,但加载速度也会变慢。需要重启软件以生效。\n当前值:%s" // "This value affects network request amount for loading image. Lower this value may reduce the chance to get error when loading image, but loading speed will be slower too. Tachiyomi restart required. Current value: %s"
private val extractComicIdFromWebpageRegex = Regex("""addSubscribe\((\d+)\)""")
private val checkComicIdIsNumericalRegex = Regex("""^\d+$""")
private val extractComicIdFromMangaUrlRegex = Regex("""(\d+)\.(json|html)""") // Get comic ID from manga.url
private val ENTRIES_ARRAY = (1..10).map { i -> i.toString() }.toTypedArray()
const val PREFIX_ID_SEARCH = "id:"
}
}

View File

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.extension.zh.dmzj
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://www.dmzj.com/info/xxx intents and redirects them to
* the main tachiyomi process. The idea is to not install the intent filter unless
* you have this extension installed, but still let the main tachiyomi app control
* things.
*/
class DmzjUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 0) {
val titleId = if (pathSegments.size > 1) {
pathSegments[1] // [m,www].dmzj.com/info/{titleId}
} else {
pathSegments[0] // manhua.dmzj.com/{titleId}
}
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Dmzj.PREFIX_ID_SEARCH}$titleId")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("DmzjUrlActivity", e.toString())
}
} else {
Log.e("DmzjUrlActivity", "Could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}