Add search intent to MangAdventure (#1490)

Add search intent to MangAdventure
This commit is contained in:
ObserverOfTime 2019-09-10 02:26:03 +03:00 committed by arkon
parent 2140e40bbb
commit 4ffb41d957
6 changed files with 200 additions and 135 deletions

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".MangAdventureActivity"
android:theme="@android:style/Theme.NoDisplay"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<!-- MangAdventure sites can be added here. -->
<data
android:scheme="https"
android:host="arc-relight.com"
android:pathPattern="/reader/..*"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -5,10 +5,8 @@ ext {
appName = 'Tachiyomi: MangAdventure'
pkgNameSuffix = 'all.mangadventure'
extClass = '.MangAdventureFactory'
extVersionCode = 2
extVersionCode = 3
libVersion = '1.2'
}
apply from: "$rootDir/common.gradle"

View File

@ -21,20 +21,26 @@ import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
/** MangAdventure source. */
/**
* MangAdventure base source.
*
* @property categories the available manga categories of the site.
*/
open class MangAdventure(
override val name: String,
override val baseUrl: String,
val categories: Array<String> = DEFAULT_CATEGORIES,
override val lang: String = "en",
override val versionId: Int = 1,
apiPath: String = "/api") : HttpSource() {
val categories: Array<String> = DEFAULT_CATEGORIES
) : HttpSource() {
/** The URL to the site's API. */
open val apiUrl by lazy { "$baseUrl/$apiPath/v$versionId" }
override val versionId = 1
override val lang = "en"
override val supportsLatest = true
/** The full URL to the site's API. */
open val apiUrl by lazy { "$baseUrl/api/v$versionId" }
/**
* A user agent representing Tachiyomi.
* Includes the user's Android version
@ -49,31 +55,31 @@ open class MangAdventure(
add("Referer", baseUrl)
}
override fun latestUpdatesRequest(page: Int) = GET(
"$apiUrl/releases/", headers
)
override fun latestUpdatesRequest(page: Int) =
GET("$apiUrl/releases/", headers)
override fun pageListRequest(chapter: SChapter) = GET(
"$apiUrl/series/${chapter.url.substringAfter("/reader/")}", headers
)
override fun pageListRequest(chapter: SChapter) =
GET("$apiUrl/series/${chapter.path}", headers)
override fun chapterListRequest(manga: SManga) = GET(
"$apiUrl/series/${Uri.parse(manga.url).lastPathSegment}/", headers
)
override fun chapterListRequest(manga: SManga) =
GET("$apiUrl/series/${manga.slug}/", headers)
// Workaround to allow "Open in browser" to use the real URL
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = client
.newCall(chapterListRequest(manga))
.asObservableSuccess().map { res ->
mangaDetailsParse(res).also { it.initialized = true }
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
client.newCall(chapterListRequest(manga)).asObservableSuccess()
.map { mangaDetailsParse(it).apply { initialized = true } }
// Return the real URL for "Open in browser"
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers)
override fun searchMangaRequest(page: Int, query: String,
filters: FilterList): Request {
override fun searchMangaRequest(
page: Int, query: String, filters: FilterList
): Request {
val uri = Uri.parse("$apiUrl/series/").buildUpon()
if (query.startsWith(SLUG_QUERY)) {
uri.appendQueryParameter("slug", query.substringAfter(SLUG_QUERY))
return GET(uri.toString(), headers)
}
uri.appendQueryParameter("q", query)
val cat = mutableListOf<String>()
filters.forEach {
@ -88,12 +94,11 @@ open class MangAdventure(
return GET("$uri&categories=${cat.joinToString(",")}", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val arr = JSONArray(response.body()!!.string())
val ret = mutableListOf<SManga>()
for (i in 0 until arr.length()) {
val obj = arr.getJSONObject(i)
ret.add(SManga.create().apply {
override fun latestUpdatesParse(response: Response) =
JSONArray(response.asString()).run {
MangasPage((0 until length()).map {
val obj = getJSONObject(it)
SManga.create().apply {
url = obj.getString("url")
title = obj.getString("title")
thumbnail_url = obj.getString("cover")
@ -101,58 +106,47 @@ open class MangAdventure(
description = httpDateToTimestamp(
obj.getJSONObject("latest_chapter").getString("date")
).toString()
})
}
return MangasPage(ret.sortedByDescending { it.description }, false)
}.sortedByDescending(SManga::description), false)
}
override fun chapterListParse(response: Response): List<SChapter> {
val res = JSONObject(response.body()!!.string())
val volumes = res.getJSONObject("volumes")
val ret = mutableListOf<SChapter>()
volumes.keys().forEach { vol ->
val chapters = volumes.getJSONObject(vol)
chapters.keys().forEach { ch ->
ret.add(SChapter.create().fromJSON(
override fun chapterListParse(response: Response) =
JSONObject(response.asString()).getJSONObject("volumes").run {
keys().asSequence().flatMap { vol ->
val chapters = getJSONObject(vol)
chapters.keys().asSequence().map { ch ->
SChapter.create().fromJSON(
chapters.getJSONObject(ch).also {
it.put("volume", vol)
it.put("chapter", ch)
}
))
)
}
}
return ret.sortedByDescending { it.name }
}.sortedByDescending(SChapter::name).toList()
}
override fun mangaDetailsParse(response: Response) =
SManga.create().fromJSON(JSONObject(response.body()!!.string()))
SManga.create().fromJSON(JSONObject(response.asString()))
override fun pageListParse(response: Response): List<Page> {
val obj = JSONObject(response.body()!!.string())
val url = obj.getString("url")
val root = obj.getString("pages_root")
val arr = obj.getJSONArray("pages_list")
val ret = mutableListOf<Page>()
for (i in 0 until arr.length()) {
ret.add(Page(i, "$url${i + 1}", root + arr.getString(i)))
override fun pageListParse(response: Response) =
JSONObject(response.asString()).run {
val url = getString("url")
val root = getString("pages_root")
val arr = getJSONArray("pages_list")
(0 until arr.length()).map {
Page(it, "$url${it + 1}", "$root${arr.getString(it)}")
}
return ret
}
override fun searchMangaParse(response: Response): MangasPage {
val arr = JSONArray(response.body()!!.string())
val ret = mutableListOf<SManga>()
for (i in 0 until arr.length()) {
ret.add(SManga.create().apply {
fromJSON(arr.getJSONObject(i))
})
}
return MangasPage(ret.sortedBy { it.title }, false)
override fun searchMangaParse(response: Response) =
JSONArray(response.asString()).run {
MangasPage((0 until length()).map {
SManga.create().fromJSON(getJSONObject(it))
}.sortedBy(SManga::title), false)
}
override fun getFilterList() = FilterList(
Person(), Status(), CategoryList()
)
override fun getFilterList() =
FilterList(Person(), Status(), CategoryList())
override fun fetchPopularManga(page: Int) =
fetchSearchManga(page, "", FilterList())
@ -213,6 +207,9 @@ open class MangAdventure(
"Yuri"
)
/** Query to search by manga slug. */
internal const val SLUG_QUERY = "slug:"
/**
* The HTTP date format specified in
* [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55).
@ -260,7 +257,7 @@ open class MangAdventure(
* @constructor Creates a [Filter.Group] object with categories.
*/
inner class CategoryList : Filter.Group<Category>(
"Categories", categories.map { Category(it) }
"Categories", categories.map(::Category)
)
/**
@ -270,4 +267,3 @@ open class MangAdventure(
*/
inner class Person : Filter.Text("Author/Artist")
}

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.extension.all.mangadventure
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 {baseUrl}/reader/{slug}
* intents and redirects them to the main Tachiyomi process.
*/
class MangAdventureActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.data?.pathSegments?.takeIf { it.size > 1 }?.let {
try {
startActivity(Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", MangAdventure.SLUG_QUERY + it[1])
putExtra("filter", packageName)
})
} catch (ex: ActivityNotFoundException) {
Log.e("MangAdventureActivity", ex.message, ex)
}
} ?: logInvalidIntent(intent)
finish()
exitProcess(0)
}
private fun logInvalidIntent(intent: Intent) {
val msg = "Failed to parse URI from intent"
Log.e("MangAdventureActivity", "$msg $intent")
}
}

View File

@ -1,11 +1,16 @@
package eu.kanade.tachiyomi.extension.all.mangadventure
import android.net.Uri
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import java.text.DecimalFormat
/** Returns the body of a response as a `String`. */
fun Response.asString(): String = body()!!.string()
/**
* Joins each value of a given [field] of the array using [sep].
*
@ -13,23 +18,29 @@ import java.text.DecimalFormat
* When its type is [Int], it is treated as the index of a [JSONArray].
* When its type is [String], it is treated as the key of a [JSONObject].
* @param sep The separator used to join the array.
* @param T Must be either [Int] or [String].
* @return The joined string, or null if the array is empty.
* @throws IllegalArgumentException when [field] is of an invalid type.
*/
fun JSONArray.joinField(field: Any, sep: String = ", "): String? {
if (!(field is Int || field is String))
throw IllegalArgumentException("field must be a String or Int")
if (this.length() == 0) return null
val list = mutableListOf<String>()
for (i in 0 until this.length()) {
fun <T> JSONArray.joinField(field: T, sep: String = ", "): String? {
require(field is Int || field is String) {
"field must be a String or Int"
}
return length().takeIf { it != 0 }?.let { len ->
(0 until len).joinToString(sep) {
when (field) {
is Int -> list.add(this.getJSONArray(i).getString(field))
is String -> list.add(this.getJSONObject(i).getString(field))
is Int -> getJSONArray(it).getString(field)
is String -> getJSONObject(it).getString(field)
else -> "" // this is here to appease the compiler
}
}
}
return list.joinToString(sep)
}
/** The slug of a manga. */
val SManga.slug: String
get() = Uri.parse(url).lastPathSegment!!
/**
* Creates a [SManga] by parsing a [JSONObject].
*
@ -43,12 +54,14 @@ fun SManga.fromJSON(obj: JSONObject) = apply {
author = obj.getJSONArray("authors")?.joinField(0)
artist = obj.getJSONArray("artists")?.joinField(0)
genre = obj.getJSONArray("categories")?.joinField("name")
status = when (obj.getBoolean("completed")) {
true -> SManga.COMPLETED
false -> SManga.ONGOING
}
status = if (obj.getBoolean("completed"))
SManga.COMPLETED else SManga.ONGOING
}
/** The unique path of a chapter. */
val SChapter.path: String
get() = url.substringAfter("/reader/")
/**
* Creates a [SChapter] by parsing a [JSONObject].
*
@ -66,4 +79,3 @@ fun SChapter.fromJSON(obj: JSONObject) = apply {
if (obj.getBoolean("final")) append(" [END]")
}
}

View File

@ -7,11 +7,10 @@ class MangAdventureFactory : SourceFactory {
override fun createSources() = listOf(
ArcRelight()
)
}
/** Arc-Relight source. */
class ArcRelight : MangAdventure(
"Arc-Relight", "https://arc-relight.site", arrayOf(
/** Arc-Relight source. */
class ArcRelight : MangAdventure(
"Arc-Relight", "https://arc-relight.com", arrayOf(
"4-Koma",
"Chaos;Head",
"Collection",
@ -29,5 +28,5 @@ class ArcRelight : MangAdventure(
"Supernatural",
"Tragedy"
)
)
)
}