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