Add search intent to MangAdventure (#1490)
Add search intent to MangAdventure
This commit is contained in:
parent
2140e40bbb
commit
4ffb41d957
|
@ -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>
|
|
@ -5,10 +5,8 @@ ext {
|
||||||
appName = 'Tachiyomi: MangAdventure'
|
appName = 'Tachiyomi: MangAdventure'
|
||||||
pkgNameSuffix = 'all.mangadventure'
|
pkgNameSuffix = 'all.mangadventure'
|
||||||
extClass = '.MangAdventureFactory'
|
extClass = '.MangAdventureFactory'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,20 +21,26 @@ import rx.Observable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/** MangAdventure source. */
|
/**
|
||||||
|
* MangAdventure base source.
|
||||||
|
*
|
||||||
|
* @property categories the available manga categories of the site.
|
||||||
|
*/
|
||||||
open class MangAdventure(
|
open class MangAdventure(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
val categories: Array<String> = DEFAULT_CATEGORIES,
|
val categories: Array<String> = DEFAULT_CATEGORIES
|
||||||
override val lang: String = "en",
|
) : HttpSource() {
|
||||||
override val versionId: Int = 1,
|
|
||||||
apiPath: String = "/api") : HttpSource() {
|
|
||||||
|
|
||||||
/** The URL to the site's API. */
|
override val versionId = 1
|
||||||
open val apiUrl by lazy { "$baseUrl/$apiPath/v$versionId" }
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
override val supportsLatest = true
|
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.
|
* A user agent representing Tachiyomi.
|
||||||
* Includes the user's Android version
|
* Includes the user's Android version
|
||||||
|
@ -49,31 +55,31 @@ open class MangAdventure(
|
||||||
add("Referer", baseUrl)
|
add("Referer", baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET(
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
"$apiUrl/releases/", headers
|
GET("$apiUrl/releases/", headers)
|
||||||
)
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter) = GET(
|
override fun pageListRequest(chapter: SChapter) =
|
||||||
"$apiUrl/series/${chapter.url.substringAfter("/reader/")}", headers
|
GET("$apiUrl/series/${chapter.path}", headers)
|
||||||
)
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga) = GET(
|
override fun chapterListRequest(manga: SManga) =
|
||||||
"$apiUrl/series/${Uri.parse(manga.url).lastPathSegment}/", headers
|
GET("$apiUrl/series/${manga.slug}/", headers)
|
||||||
)
|
|
||||||
|
|
||||||
// Workaround to allow "Open in browser" to use the real URL
|
// Workaround to allow "Open in browser" to use the real URL
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = client
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||||
.newCall(chapterListRequest(manga))
|
client.newCall(chapterListRequest(manga)).asObservableSuccess()
|
||||||
.asObservableSuccess().map { res ->
|
.map { mangaDetailsParse(it).apply { initialized = true } }
|
||||||
mangaDetailsParse(res).also { it.initialized = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the real URL for "Open in browser"
|
// Return the real URL for "Open in browser"
|
||||||
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers)
|
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers)
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String,
|
override fun searchMangaRequest(
|
||||||
filters: FilterList): Request {
|
page: Int, query: String, filters: FilterList
|
||||||
|
): Request {
|
||||||
val uri = Uri.parse("$apiUrl/series/").buildUpon()
|
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)
|
uri.appendQueryParameter("q", query)
|
||||||
val cat = mutableListOf<String>()
|
val cat = mutableListOf<String>()
|
||||||
filters.forEach {
|
filters.forEach {
|
||||||
|
@ -88,12 +94,11 @@ open class MangAdventure(
|
||||||
return GET("$uri&categories=${cat.joinToString(",")}", headers)
|
return GET("$uri&categories=${cat.joinToString(",")}", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
override fun latestUpdatesParse(response: Response) =
|
||||||
val arr = JSONArray(response.body()!!.string())
|
JSONArray(response.asString()).run {
|
||||||
val ret = mutableListOf<SManga>()
|
MangasPage((0 until length()).map {
|
||||||
for (i in 0 until arr.length()) {
|
val obj = getJSONObject(it)
|
||||||
val obj = arr.getJSONObject(i)
|
SManga.create().apply {
|
||||||
ret.add(SManga.create().apply {
|
|
||||||
url = obj.getString("url")
|
url = obj.getString("url")
|
||||||
title = obj.getString("title")
|
title = obj.getString("title")
|
||||||
thumbnail_url = obj.getString("cover")
|
thumbnail_url = obj.getString("cover")
|
||||||
|
@ -101,58 +106,47 @@ open class MangAdventure(
|
||||||
description = httpDateToTimestamp(
|
description = httpDateToTimestamp(
|
||||||
obj.getJSONObject("latest_chapter").getString("date")
|
obj.getJSONObject("latest_chapter").getString("date")
|
||||||
).toString()
|
).toString()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return MangasPage(ret.sortedByDescending { it.description }, false)
|
}.sortedByDescending(SManga::description), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response) =
|
||||||
val res = JSONObject(response.body()!!.string())
|
JSONObject(response.asString()).getJSONObject("volumes").run {
|
||||||
val volumes = res.getJSONObject("volumes")
|
keys().asSequence().flatMap { vol ->
|
||||||
val ret = mutableListOf<SChapter>()
|
val chapters = getJSONObject(vol)
|
||||||
volumes.keys().forEach { vol ->
|
chapters.keys().asSequence().map { ch ->
|
||||||
val chapters = volumes.getJSONObject(vol)
|
SChapter.create().fromJSON(
|
||||||
chapters.keys().forEach { ch ->
|
|
||||||
ret.add(SChapter.create().fromJSON(
|
|
||||||
chapters.getJSONObject(ch).also {
|
chapters.getJSONObject(ch).also {
|
||||||
it.put("volume", vol)
|
it.put("volume", vol)
|
||||||
it.put("chapter", ch)
|
it.put("chapter", ch)
|
||||||
}
|
}
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
}
|
}.sortedByDescending(SChapter::name).toList()
|
||||||
return ret.sortedByDescending { it.name }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response) =
|
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> {
|
override fun pageListParse(response: Response) =
|
||||||
val obj = JSONObject(response.body()!!.string())
|
JSONObject(response.asString()).run {
|
||||||
val url = obj.getString("url")
|
val url = getString("url")
|
||||||
val root = obj.getString("pages_root")
|
val root = getString("pages_root")
|
||||||
val arr = obj.getJSONArray("pages_list")
|
val arr = getJSONArray("pages_list")
|
||||||
val ret = mutableListOf<Page>()
|
(0 until arr.length()).map {
|
||||||
for (i in 0 until arr.length()) {
|
Page(it, "$url${it + 1}", "$root${arr.getString(it)}")
|
||||||
ret.add(Page(i, "$url${i + 1}", root + arr.getString(i)))
|
|
||||||
}
|
}
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response) =
|
||||||
val arr = JSONArray(response.body()!!.string())
|
JSONArray(response.asString()).run {
|
||||||
val ret = mutableListOf<SManga>()
|
MangasPage((0 until length()).map {
|
||||||
for (i in 0 until arr.length()) {
|
SManga.create().fromJSON(getJSONObject(it))
|
||||||
ret.add(SManga.create().apply {
|
}.sortedBy(SManga::title), false)
|
||||||
fromJSON(arr.getJSONObject(i))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return MangasPage(ret.sortedBy { it.title }, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
override fun getFilterList() =
|
||||||
Person(), Status(), CategoryList()
|
FilterList(Person(), Status(), CategoryList())
|
||||||
)
|
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int) =
|
override fun fetchPopularManga(page: Int) =
|
||||||
fetchSearchManga(page, "", FilterList())
|
fetchSearchManga(page, "", FilterList())
|
||||||
|
@ -213,6 +207,9 @@ open class MangAdventure(
|
||||||
"Yuri"
|
"Yuri"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Query to search by manga slug. */
|
||||||
|
internal const val SLUG_QUERY = "slug:"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP date format specified in
|
* The HTTP date format specified in
|
||||||
* [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55).
|
* [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.
|
* @constructor Creates a [Filter.Group] object with categories.
|
||||||
*/
|
*/
|
||||||
inner class CategoryList : Filter.Group<Category>(
|
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")
|
inner class Person : Filter.Text("Author/Artist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,16 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.mangadventure
|
package eu.kanade.tachiyomi.extension.all.mangadventure
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
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 okhttp3.Response
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.text.DecimalFormat
|
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].
|
* Joins each value of a given [field] of the array using [sep].
|
||||||
*
|
*
|
||||||
|
@ -13,22 +18,28 @@ import java.text.DecimalFormat
|
||||||
* When its type is [Int], it is treated as the index of a [JSONArray].
|
* 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].
|
* When its type is [String], it is treated as the key of a [JSONObject].
|
||||||
* @param sep The separator used to join the array.
|
* @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.
|
* @return The joined string, or null if the array is empty.
|
||||||
* @throws IllegalArgumentException when [field] is of an invalid type.
|
* @throws IllegalArgumentException when [field] is of an invalid type.
|
||||||
*/
|
*/
|
||||||
fun JSONArray.joinField(field: Any, sep: String = ", "): String? {
|
fun <T> JSONArray.joinField(field: T, sep: String = ", "): String? {
|
||||||
if (!(field is Int || field is String))
|
require(field is Int || field is String) {
|
||||||
throw IllegalArgumentException("field must be a String or Int")
|
"field must be a String or Int"
|
||||||
if (this.length() == 0) return null
|
}
|
||||||
val list = mutableListOf<String>()
|
return length().takeIf { it != 0 }?.let { len ->
|
||||||
for (i in 0 until this.length()) {
|
(0 until len).joinToString(sep) {
|
||||||
when (field) {
|
when (field) {
|
||||||
is Int -> list.add(this.getJSONArray(i).getString(field))
|
is Int -> getJSONArray(it).getString(field)
|
||||||
is String -> list.add(this.getJSONObject(i).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].
|
* Creates a [SManga] by parsing a [JSONObject].
|
||||||
|
@ -43,12 +54,14 @@ fun SManga.fromJSON(obj: JSONObject) = apply {
|
||||||
author = obj.getJSONArray("authors")?.joinField(0)
|
author = obj.getJSONArray("authors")?.joinField(0)
|
||||||
artist = obj.getJSONArray("artists")?.joinField(0)
|
artist = obj.getJSONArray("artists")?.joinField(0)
|
||||||
genre = obj.getJSONArray("categories")?.joinField("name")
|
genre = obj.getJSONArray("categories")?.joinField("name")
|
||||||
status = when (obj.getBoolean("completed")) {
|
status = if (obj.getBoolean("completed"))
|
||||||
true -> SManga.COMPLETED
|
SManga.COMPLETED else SManga.ONGOING
|
||||||
false -> SManga.ONGOING
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The unique path of a chapter. */
|
||||||
|
val SChapter.path: String
|
||||||
|
get() = url.substringAfter("/reader/")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a [SChapter] by parsing a [JSONObject].
|
* Creates a [SChapter] by parsing a [JSONObject].
|
||||||
*
|
*
|
||||||
|
@ -66,4 +79,3 @@ fun SChapter.fromJSON(obj: JSONObject) = apply {
|
||||||
if (obj.getBoolean("final")) append(" [END]")
|
if (obj.getBoolean("final")) append(" [END]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,10 @@ class MangAdventureFactory : SourceFactory {
|
||||||
override fun createSources() = listOf(
|
override fun createSources() = listOf(
|
||||||
ArcRelight()
|
ArcRelight()
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
/** Arc-Relight source. */
|
/** Arc-Relight source. */
|
||||||
class ArcRelight : MangAdventure(
|
class ArcRelight : MangAdventure(
|
||||||
"Arc-Relight", "https://arc-relight.site", arrayOf(
|
"Arc-Relight", "https://arc-relight.com", arrayOf(
|
||||||
"4-Koma",
|
"4-Koma",
|
||||||
"Chaos;Head",
|
"Chaos;Head",
|
||||||
"Collection",
|
"Collection",
|
||||||
|
@ -30,4 +29,4 @@ class ArcRelight : MangAdventure(
|
||||||
"Tragedy"
|
"Tragedy"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue