[zh-dmzj]Add fallback api to fetch hidden manga and URL intent filter. (#5624)
This commit is contained in:
parent
bba235621c
commit
b1d483f293
|
@ -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>
|
||||
|
|
|
@ -5,7 +5,7 @@ ext {
|
|||
extName = 'Dmzj'
|
||||
pkgNameSuffix = 'zh.dmzj'
|
||||
extClass = '.Dmzj'
|
||||
extVersionCode = 15
|
||||
extVersionCode = 16
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -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:"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue