[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"?> <?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' extName = 'Dmzj'
pkgNameSuffix = 'zh.dmzj' pkgNameSuffix = 'zh.dmzj'
extClass = '.Dmzj' extClass = '.Dmzj'
extVersionCode = 15 extVersionCode = 16
libVersion = '1.2' 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -37,7 +39,9 @@ class Dmzj : ConfigurableSource, HttpSource() {
override val supportsLatest = true override val supportsLatest = true
override val name = "动漫之家" override val name = "动漫之家"
override val baseUrl = "https://m.dmzj1.com" 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 val imageCDNUrl = "https://images.dmzj1.com"
private fun cleanUrl(url: String) = if (url.startsWith("//")) private fun cleanUrl(url: String) = if (url.startsWith("//"))
@ -48,6 +52,10 @@ class Dmzj : ConfigurableSource, HttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) 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( private val apiRateLimitInterceptor = SpecificHostRateLimitInterceptor(
HttpUrl.parse(apiUrl)!!, HttpUrl.parse(apiUrl)!!,
preferences.getString(API_RATELIMIT_PREF, "5")!!.toInt() preferences.getString(API_RATELIMIT_PREF, "5")!!.toInt()
@ -59,12 +67,13 @@ class Dmzj : ConfigurableSource, HttpSource() {
override val client: OkHttpClient = network.client.newBuilder() override val client: OkHttpClient = network.client.newBuilder()
.addNetworkInterceptor(apiRateLimitInterceptor) .addNetworkInterceptor(apiRateLimitInterceptor)
.addNetworkInterceptor(v3apiRateLimitInterceptor)
.addNetworkInterceptor(imageCDNRateLimitInterceptor) .addNetworkInterceptor(imageCDNRateLimitInterceptor)
.build() .build()
private fun myGet(url: String) = GET(url) override fun headersBuilder() = Headers.Builder().apply {
.newBuilder() set("Referer", "https://www.dmzj1.com/")
.header( set(
"User-Agent", "User-Agent",
"Mozilla/5.0 (Linux; Android 10) " + "Mozilla/5.0 (Linux; Android 10) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " + "AppleWebKit/537.36 (KHTML, like Gecko) " +
@ -72,7 +81,7 @@ class Dmzj : ConfigurableSource, HttpSource() {
"Mobile Safari/537.36 " + "Mobile Safari/537.36 " +
"Tachiyomi/1.0" "Tachiyomi/1.0"
) )
.build()!! }
// for simple searches (query only, no filters) // for simple searches (query only, no filters)
private fun simpleSearchJsonParse(json: String): MangasPage { private fun simpleSearchJsonParse(json: String): MangasPage {
@ -117,19 +126,53 @@ class Dmzj : ConfigurableSource, HttpSource() {
return MangasPage(ret, arr.length() != 0) 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 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) 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 { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query != "") { if (query != "") {
val uri = Uri.parse("http://s.acg.dmzj1.com/comicsum/search.php").buildUpon() val uri = Uri.parse("http://s.acg.dmzj1.com/comicsum/search.php").buildUpon()
uri.appendQueryParameter("s", query) uri.appendQueryParameter("s", query)
return myGet(uri.toString()) return GET(uri.toString())
} else { } else {
var params = filters.map { var params = filters.map {
if (it !is SortFilter && it is UriPartFilter) { 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() } 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> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(GET(apiUrl + manga.url, headers)) val cid = extractComicIdFromMangaUrlRegex.find(manga.url)!!.groups[1]!!.value
.asObservableSuccess() return try {
.map { response -> // Not using client.newCall().asObservableSuccess() to ensure we can catch exception here.
mangaDetailsParse(response).apply { initialized = true } 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. // Workaround to allow "Open in browser" use human readable webpage url.
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl/info/${re1.find(manga.url)!!.value}.html") val cid = extractComicIdFromMangaUrlRegex.find(manga.url)!!.groups[1]!!.value
} return GET("$baseUrl/info/$cid.html")
override fun chapterListRequest(manga: SManga): Request {
return GET(apiUrl + manga.url, headers)
} }
override fun mangaDetailsParse(response: Response) = SManga.create().apply { override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val obj = JSONObject(response.body()!!.string()) val obj = JSONObject(response.body()!!.string())
if (response.request().url().toString().startsWith(v3apiUrl)) {
title = obj.getString("title") title = obj.getString("title")
thumbnail_url = obj.getString("cover") thumbnail_url = obj.getString("cover")
var arr = obj.getJSONArray("authors") var arr = obj.getJSONArray("authors")
val tmparr = ArrayList<String>(arr.length()) val tmparr = ArrayList<String>(arr.length())
for (i in 0 until arr.length()) { for (i in 0 until arr.length()) {
@ -201,19 +248,55 @@ class Dmzj : ConfigurableSource, HttpSource() {
} }
description = obj.getString("description") 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> { override fun chapterListParse(response: Response): List<SChapter> {
val obj = JSONObject(response.body()!!.string()) val obj = JSONObject(response.body()!!.string())
val ret = ArrayList<SChapter>() val ret = ArrayList<SChapter>()
if (response.request().url().toString().startsWith(v3apiUrl)) {
val cid = obj.getString("id") val cid = obj.getString("id")
val arr = obj.getJSONArray("chapters") val chaptersList = obj.getJSONArray("chapters")
for (i in 0 until arr.length()) { for (i in 0 until chaptersList.length()) {
val obj2 = arr.getJSONObject(i) val chapterObj = chaptersList.getJSONObject(i)
val arr2 = obj2.getJSONArray("data") val chapterData = chapterObj.getJSONArray("data")
val prefix = obj2.getString("title") val prefix = chapterObj.getString("title")
for (j in 0 until arr2.length()) { for (j in 0 until chapterData.length()) {
val chapter = arr2.getJSONObject(j) val chapter = chapterData.getJSONObject(j)
ret.add( ret.add(
SChapter.create().apply { SChapter.create().apply {
name = "$prefix: ${chapter.getString("chapter_title")}" 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 return ret
} }
override fun pageListRequest(chapter: SChapter) = GET(chapter.url, headers) // Bypass base url override fun pageListRequest(chapter: SChapter) = GET(chapter.url, headers) // Bypass base url
override fun pageListParse(response: Response): List<Page> { 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) // 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()) val obj = JSONObject(response.body()!!.string())
obj.getJSONObject("chapter").getJSONArray("page_url") // api.m.dmzj1.com already return HD image url obj.getJSONObject("chapter").getJSONArray("page_url") // api.m.dmzj1.com already return HD image url
} catch (_: Exception) { } catch (_: Exception) {
@ -241,10 +341,22 @@ class Dmzj : ConfigurableSource, HttpSource() {
.replace(".html", ".json") .replace(".html", ".json")
val obj = client.newCall(GET(url, headers)).execute().let { JSONObject(it.body()!!.string()) } 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. 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()) val ret = ArrayList<Page>(arr.length())
for (i in 0 until 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 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( private open class UriPartFilter(
displayName: String, displayName: String,
val vals: Array<Pair<String, 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_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 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() 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)
}
}