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'
|
||||
pkgNameSuffix = 'all.mangadventure'
|
||||
extClass = '.MangAdventureFactory'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
|
||||
|
|
|
@ -21,59 +21,65 @@ 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() {
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
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
|
||||
* and the current extension version.
|
||||
*/
|
||||
private val userAgent = "Mozilla/5.0 " +
|
||||
"(Android ${VERSION.RELEASE}; Mobile) " +
|
||||
"Tachiyomi/${BuildConfig.VERSION_NAME}"
|
||||
"(Android ${VERSION.RELEASE}; Mobile) " +
|
||||
"Tachiyomi/${BuildConfig.VERSION_NAME}"
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", userAgent)
|
||||
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,89 +94,77 @@ 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 {
|
||||
url = obj.getString("url")
|
||||
title = obj.getString("title")
|
||||
thumbnail_url = obj.getString("cover")
|
||||
// A bit of a hack to sort by date
|
||||
description = httpDateToTimestamp(
|
||||
obj.getJSONObject("latest_chapter").getString("date")
|
||||
).toString()
|
||||
})
|
||||
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")
|
||||
// A bit of a hack to sort by date
|
||||
description = httpDateToTimestamp(
|
||||
obj.getJSONObject("latest_chapter").getString("date")
|
||||
).toString()
|
||||
}
|
||||
}.sortedByDescending(SManga::description), false)
|
||||
}
|
||||
return MangasPage(ret.sortedByDescending { it.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(
|
||||
chapters.getJSONObject(ch).also {
|
||||
it.put("volume", vol)
|
||||
it.put("chapter", ch)
|
||||
}
|
||||
))
|
||||
}
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}.sortedByDescending(SChapter::name).toList()
|
||||
}
|
||||
return ret.sortedByDescending { it.name }
|
||||
}
|
||||
|
||||
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))
|
||||
})
|
||||
override fun searchMangaParse(response: Response) =
|
||||
JSONArray(response.asString()).run {
|
||||
MangasPage((0 until length()).map {
|
||||
SManga.create().fromJSON(getJSONObject(it))
|
||||
}.sortedBy(SManga::title), false)
|
||||
}
|
||||
return MangasPage(ret.sortedBy { it.title }, false)
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Person(), Status(), CategoryList()
|
||||
)
|
||||
override fun getFilterList() =
|
||||
FilterList(Person(), Status(), CategoryList())
|
||||
|
||||
override fun fetchPopularManga(page: Int) =
|
||||
fetchSearchManga(page, "", FilterList())
|
||||
fetchSearchManga(page, "", FilterList())
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!"
|
||||
)
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!"
|
||||
)
|
||||
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!"
|
||||
)
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!"
|
||||
)
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!"
|
||||
)
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!"
|
||||
)
|
||||
|
||||
companion object {
|
||||
/** The possible statuses of a manga. */
|
||||
|
@ -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).
|
||||
|
@ -226,7 +223,7 @@ open class MangAdventure(
|
|||
* @return The timestamp of the date.
|
||||
*/
|
||||
fun httpDateToTimestamp(date: String) =
|
||||
SimpleDateFormat(HTTP_DATE, Locale.US).parse(date).time
|
||||
SimpleDateFormat(HTTP_DATE, Locale.US).parse(date).time
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
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()) {
|
||||
when (field) {
|
||||
is Int -> list.add(this.getJSONArray(i).getString(field))
|
||||
is String -> list.add(this.getJSONObject(i).getString(field))
|
||||
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 -> 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]")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,27 +7,26 @@ class MangAdventureFactory : SourceFactory {
|
|||
override fun createSources() = listOf(
|
||||
ArcRelight()
|
||||
)
|
||||
}
|
||||
|
||||
/** Arc-Relight source. */
|
||||
class ArcRelight : MangAdventure(
|
||||
"Arc-Relight", "https://arc-relight.site", arrayOf(
|
||||
"4-Koma",
|
||||
"Chaos;Head",
|
||||
"Collection",
|
||||
"Comedy",
|
||||
"Drama",
|
||||
"Jubilee",
|
||||
"Mystery",
|
||||
"Psychological",
|
||||
"Robotics;Notes",
|
||||
"Romance",
|
||||
"Sci-Fi",
|
||||
"Seinen",
|
||||
"Shounen",
|
||||
"Steins;Gate",
|
||||
"Supernatural",
|
||||
"Tragedy"
|
||||
/** Arc-Relight source. */
|
||||
class ArcRelight : MangAdventure(
|
||||
"Arc-Relight", "https://arc-relight.com", arrayOf(
|
||||
"4-Koma",
|
||||
"Chaos;Head",
|
||||
"Collection",
|
||||
"Comedy",
|
||||
"Drama",
|
||||
"Jubilee",
|
||||
"Mystery",
|
||||
"Psychological",
|
||||
"Robotics;Notes",
|
||||
"Romance",
|
||||
"Sci-Fi",
|
||||
"Seinen",
|
||||
"Shounen",
|
||||
"Steins;Gate",
|
||||
"Supernatural",
|
||||
"Tragedy"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue