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' 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"

View File

@ -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")
} }

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 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,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 [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]")
} }
} }

View File

@ -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",
@ -29,5 +28,5 @@ class ArcRelight : MangAdventure(
"Supernatural", "Supernatural",
"Tragedy" "Tragedy"
) )
) )
}