MangAdventure split (#5667)

This commit is contained in:
ObserverOfTime
2021-02-11 17:48:48 +02:00
committed by GitHub
parent 3a02da8d35
commit f4c4489e6b
21 changed files with 82 additions and 90 deletions

View File

@ -0,0 +1,280 @@
package eu.kanade.tachiyomi.multisrc.mangadventure
import android.net.Uri
import android.os.Build.VERSION
import eu.kanade.tachiyomi.extensions.BuildConfig
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
/**
* MangAdventure base source.
*
* @property categories the available manga categories of the site.
*/
abstract class MangAdventure(
override val name: String,
override val baseUrl: String,
val categories: List<String> = DEFAULT_CATEGORIES
) : HttpSource() {
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}"
override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", userAgent)
add("Referer", baseUrl)
}
override fun latestUpdatesRequest(page: Int) =
GET("$apiUrl/releases/", headers)
override fun pageListRequest(chapter: SChapter) =
GET("$apiUrl/series/${chapter.path}", 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 { 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 {
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 {
when (it) {
is Person -> uri.appendQueryParameter("author", it.state)
is Status -> uri.appendQueryParameter("status", it.string())
is CategoryList -> cat.addAll(
it.state.mapNotNull { c ->
Uri.encode(c.optString())
}
)
else -> Unit
}
}
return GET("$uri&categories=${cat.joinToString(",")}", headers)
}
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
)
}
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)
}
)
}
}.toList().reversed()
}
override fun mangaDetailsParse(response: Response) =
SManga.create().fromJSON(JSONObject(response.asString()))
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)}")
}
}
override fun searchMangaParse(response: Response) =
JSONArray(response.asString()).run {
MangasPage(
(0 until length()).map {
SManga.create().fromJSON(getJSONObject(it))
}.sortedBy(SManga::title),
false
)
}
override fun getFilterList() =
FilterList(Person(), Status(), CategoryList())
override fun fetchPopularManga(page: Int) =
fetchSearchManga(page, "", FilterList())
override fun popularMangaRequest(page: Int) =
throw UnsupportedOperationException(
"This method should not be called!"
)
override fun popularMangaParse(response: Response) =
throw UnsupportedOperationException(
"This method should not be called!"
)
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException(
"This method should not be called!"
)
companion object {
/** The possible statuses of a manga. */
private val STATUSES = arrayOf("Any", "Completed", "Ongoing")
/** Manga categories from MangAdventure `categories.xml` fixture. */
internal val DEFAULT_CATEGORIES = listOf(
"4-Koma",
"Action",
"Adventure",
"Comedy",
"Doujinshi",
"Drama",
"Ecchi",
"Fantasy",
"Gender Bender",
"Harem",
"Hentai",
"Historical",
"Horror",
"Josei",
"Martial Arts",
"Mecha",
"Mystery",
"Psychological",
"Romance",
"School Life",
"Sci-Fi",
"Seinen",
"Shoujo",
"Shoujo Ai",
"Shounen",
"Shounen Ai",
"Slice of Life",
"Smut",
"Sports",
"Supernatural",
"Tragedy",
"Yaoi",
"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).
*/
private const val HTTP_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz"
/**
* Converts a date in the [HTTP_DATE] format to a Unix timestamp.
*
* @param date The date to convert.
* @return The timestamp of the date.
*/
internal fun httpDateToTimestamp(date: String) =
SimpleDateFormat(HTTP_DATE, Locale.US).parse(date)?.time ?: 0L
}
/**
* Filter representing the status of a manga.
*
* @constructor Creates a [Filter.Select] object with [STATUSES].
*/
inner class Status : Filter.Select<String>("Status", STATUSES) {
/** Returns the [state] as a string. */
fun string() = values[state].toLowerCase(Locale.ENGLISH)
}
/**
* Filter representing a manga category.
*
* @property name The display name of the category.
* @constructor Creates a [Filter.TriState] object using [name].
*/
inner class Category(name: String) : Filter.TriState(name) {
/** Returns the [state] as a string, or null if [isIgnored]. */
fun optString() = when (state) {
STATE_INCLUDE -> name.toLowerCase(Locale(lang))
STATE_EXCLUDE -> "-" + name.toLowerCase(Locale(lang))
else -> null
}
}
/**
* Filter representing the [categories][Category] of a manga.
*
* @constructor Creates a [Filter.Group] object with categories.
*/
inner class CategoryList : Filter.Group<Category>(
"Categories", categories.map(::Category)
)
/**
* Filter representing the name of an author or artist.
*
* @constructor Creates a [Filter.Text] object.
*/
inner class Person : Filter.Text("Author/Artist")
}

View File

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.multisrc.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)
}
} ?: Log.e(
"MangAdventureActivity",
"Failed to parse URI from intent: $intent"
)
finish()
exitProcess(0)
}
}

View File

@ -0,0 +1,95 @@
package eu.kanade.tachiyomi.multisrc.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()
/**
* Formats the number according to [fmt].
*
* @param fmt A [DecimalFormat] string.
* @return A string representation of the number.
*/
fun Number.format(fmt: String): String = DecimalFormat(fmt).format(this)
/**
* Joins each value of a given [field] of the array using [sep].
*
* @param field 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.
* @return The joined string, or `null` if the array is empty.
*/
fun JSONArray.joinField(field: Int, sep: String = ", ") =
length().takeIf { it != 0 }?.run {
(0 until this).joinToString(sep) {
getJSONArray(it).getString(field)
}
}
/**
* Joins each value of a given [field] of the array using [sep].
*
* @param field The key of a [JSONObject].
* @param sep The separator used to join the array.
* @return The joined string, or `null` if the array is empty.
*/
fun JSONArray.joinField(field: String, sep: String = ", ") =
length().takeIf { it != 0 }?.run {
(0 until this).joinToString(sep) {
getJSONObject(it).getString(field)
}
}
/** The slug of a manga. */
val SManga.slug: String
get() = Uri.parse(url).lastPathSegment!!
/**
* Creates a [SManga] by parsing a [JSONObject].
*
* @param obj The object containing the manga info.
*/
fun SManga.fromJSON(obj: JSONObject) = apply {
url = obj.getString("url")
title = obj.getString("title")
description = obj.getString("description")
thumbnail_url = obj.getString("cover")
author = obj.getJSONArray("authors").joinField(0)
artist = obj.getJSONArray("artists").joinField(0)
genre = obj.getJSONArray("categories").joinField("name")
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].
*
* @param obj The object containing the chapter info.
*/
fun SChapter.fromJSON(obj: JSONObject) = apply {
url = obj.getString("url")
chapter_number = obj.optString("chapter", "-1").toFloat()
date_upload = MangAdventure.httpDateToTimestamp(obj.getString("date"))
scanlator = obj.getJSONArray("groups").joinField("name", " & ")
name = obj.optString(
"full_title",
buildString {
obj.optInt("volume").let { if (it != 0) append("Vol. $it, ") }
append("Ch. ${chapter_number.format("#.#")}: ")
append(obj.getString("title"))
}
)
if (obj.getBoolean("final")) name += " [END]"
}

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.multisrc.mangadventure
import eu.kanade.tachiyomi.multisrc.ThemeSourceData.SingleLang
import eu.kanade.tachiyomi.multisrc.ThemeSourceGenerator
/** [MangAdventure] source generator. */
class MangAdventureGenerator : ThemeSourceGenerator {
override val themePkg = "mangadventure"
override val themeClass = "MangAdventure"
override val baseVersionCode = 1
override val sources = listOf(
SingleLang("Arc-Relight", "https://arc-relight.com", "en", className = "ArcRelight"),
)
companion object {
@JvmStatic fun main(args: Array<String>) = MangAdventureGenerator().createAll()
}
}