diff --git a/src/all/mangadventure/AndroidManifest.xml b/src/all/mangadventure/AndroidManifest.xml
new file mode 100644
index 000000000..49746e9ef
--- /dev/null
+++ b/src/all/mangadventure/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/all/mangadventure/build.gradle b/src/all/mangadventure/build.gradle
index 43ae0bf68..f4386eebe 100644
--- a/src/all/mangadventure/build.gradle
+++ b/src/all/mangadventure/build.gradle
@@ -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"
-
-
diff --git a/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventure.kt b/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventure.kt
index b15a6ad53..fbd67b871 100644
--- a/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventure.kt
+++ b/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventure.kt
@@ -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 = 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 = 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 = client
- .newCall(chapterListRequest(manga))
- .asObservableSuccess().map { res ->
- mangaDetailsParse(res).also { it.initialized = true }
- }
+ override fun fetchMangaDetails(manga: SManga): Observable =
+ 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()
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()
- 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 {
- val res = JSONObject(response.body()!!.string())
- val volumes = res.getJSONObject("volumes")
- val ret = mutableListOf()
- 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 {
- 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()
- 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()
- 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(
- "Categories", categories.map { Category(it) }
+ "Categories", categories.map(::Category)
)
/**
@@ -270,4 +267,3 @@ open class MangAdventure(
*/
inner class Person : Filter.Text("Author/Artist")
}
-
diff --git a/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureActivity.kt b/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureActivity.kt
new file mode 100644
index 000000000..62e953d9b
--- /dev/null
+++ b/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureActivity.kt
@@ -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")
+ }
+}
diff --git a/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureExtensions.kt b/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureExtensions.kt
index ebf1fb40c..cf917c968 100644
--- a/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureExtensions.kt
+++ b/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureExtensions.kt
@@ -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()
- 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 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]")
}
}
-
diff --git a/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureFactory.kt b/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureFactory.kt
index 32bfd0544..d6c66a404 100644
--- a/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureFactory.kt
+++ b/src/all/mangadventure/src/eu/kanade/tachiyomi/extension/all/mangadventure/MangAdventureFactory.kt
@@ -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"
+ )
)
-)
-
+}