MangAdventure API v2 (#9344)
This commit is contained in:
parent
8be6df8f3b
commit
62537462ac
|
@ -3,10 +3,8 @@ package eu.kanade.tachiyomi.extension.en.arcrelight
|
||||||
import eu.kanade.tachiyomi.multisrc.mangadventure.MangAdventure
|
import eu.kanade.tachiyomi.multisrc.mangadventure.MangAdventure
|
||||||
|
|
||||||
/** Arc-Relight source. */
|
/** Arc-Relight source. */
|
||||||
class ArcRelight : MangAdventure(
|
class ArcRelight : MangAdventure("Arc-Relight", "https://arc-relight.com") {
|
||||||
"Arc-Relight",
|
override val categories = listOf(
|
||||||
"https://arc-relight.com",
|
|
||||||
categories = listOf(
|
|
||||||
"4-Koma",
|
"4-Koma",
|
||||||
"Chaos;Head",
|
"Chaos;Head",
|
||||||
"Collection",
|
"Collection",
|
||||||
|
@ -24,4 +22,4 @@ class ArcRelight : MangAdventure(
|
||||||
"Supernatural",
|
"Supernatural",
|
||||||
"Tragedy"
|
"Tragedy"
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
|
|
@ -5,197 +5,187 @@ import android.os.Build.VERSION
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page as SPage
|
||||||
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 eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
/**
|
/** MangAdventure base source. */
|
||||||
* MangAdventure base source.
|
|
||||||
*
|
|
||||||
* @property categories the available manga categories of the site.
|
|
||||||
*/
|
|
||||||
abstract class MangAdventure(
|
abstract class MangAdventure(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
override val lang: String = "en",
|
override val lang: String = "en"
|
||||||
val categories: List<String> = DEFAULT_CATEGORIES
|
|
||||||
) : HttpSource() {
|
) : HttpSource() {
|
||||||
|
/** The site's manga categories. */
|
||||||
|
protected open val categories = DEFAULT_CATEGORIES
|
||||||
|
|
||||||
override val versionId = 1
|
/** The site's manga status names. */
|
||||||
|
protected open val statuses = arrayOf("Any", "Completed", "Ongoing")
|
||||||
|
|
||||||
override val supportsLatest = true
|
/** The site's sort order labels that correspond to [SortOrder.values]. */
|
||||||
|
protected open val orders = arrayOf("Title", "Latest upload", "Chapter count")
|
||||||
|
|
||||||
/** The full URL to the site's API. */
|
/** A user agent representing Tachiyomi. */
|
||||||
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 " +
|
private val userAgent = "Mozilla/5.0 " +
|
||||||
"(Android ${VERSION.RELEASE}; Mobile) " +
|
"(Android ${VERSION.RELEASE}; Mobile) " +
|
||||||
"Tachiyomi/${BuildConfig.VERSION_NAME}"
|
"Tachiyomi/${BuildConfig.VERSION_NAME}"
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
/** The URI of the site's API. */
|
||||||
|
private val apiUri by lazy { Uri.parse("$baseUrl/api/v2")!! }
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
/** The JSON parser of the class. */
|
||||||
add("User-Agent", userAgent)
|
private val json by injectLazy<Json>()
|
||||||
add("Referer", baseUrl)
|
|
||||||
}
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun headersBuilder() =
|
||||||
|
super.headersBuilder().set("User-Agent", userAgent)
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) =
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
GET("$apiUrl/releases/", headers)
|
apiUri.buildUpon().appendEncodedPath("series").run {
|
||||||
|
appendQueryParameter("page", page.toString())
|
||||||
|
appendQueryParameter("sort", "-latest_upload")
|
||||||
|
GET(toString(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter) =
|
override fun popularMangaRequest(page: Int) =
|
||||||
GET("$apiUrl/series/${chapter.path}", headers)
|
apiUri.buildUpon().appendEncodedPath("series").run {
|
||||||
|
appendQueryParameter("page", page.toString())
|
||||||
|
appendQueryParameter("sort", "-chapter_count")
|
||||||
|
GET(toString(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
|
apiUri.buildUpon().appendEncodedPath("series").run {
|
||||||
|
if (query.startsWith(SLUG_QUERY)) {
|
||||||
|
appendQueryParameter("slug", query.substring(SLUG_QUERY.length))
|
||||||
|
} else {
|
||||||
|
appendQueryParameter("page", page.toString())
|
||||||
|
appendQueryParameter("title", query)
|
||||||
|
filters.forEach {
|
||||||
|
when (it) {
|
||||||
|
is Author -> appendQueryParameter("author", it.toString())
|
||||||
|
is Artist -> appendQueryParameter("artist", it.toString())
|
||||||
|
is Status -> appendQueryParameter("status", it.toString())
|
||||||
|
is SortOrder -> appendQueryParameter("sort", it.toString())
|
||||||
|
is CategoryList -> appendQueryParameter("categories", it.toString())
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GET(toString(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga) =
|
override fun chapterListRequest(manga: SManga) =
|
||||||
GET("$apiUrl/series/${manga.slug}/", headers)
|
apiUri.buildUpon().appendEncodedPath("chapters").run {
|
||||||
|
appendQueryParameter("series", manga.slug)
|
||||||
// Workaround to allow "Open in browser" to use the real URL
|
appendQueryParameter("date_format", "timestamp")
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
GET(toString(), headers)
|
||||||
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>()
|
override fun pageListRequest(chapter: SChapter) =
|
||||||
filters.forEach {
|
apiUri.buildUpon().appendEncodedPath("pages").run {
|
||||||
when (it) {
|
val (slug, vol, num) = chapter.components
|
||||||
is Person -> uri.appendQueryParameter("author", it.state)
|
appendQueryParameter("series", slug)
|
||||||
is Status -> uri.appendQueryParameter("status", it.string())
|
appendQueryParameter("volume", vol)
|
||||||
is CategoryList -> cat.addAll(
|
appendQueryParameter("number", num)
|
||||||
it.state.mapNotNull { c ->
|
GET(toString(), headers)
|
||||||
Uri.encode(c.optString())
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return GET("$uri&categories=${cat.joinToString(",")}", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) =
|
override fun latestUpdatesParse(response: Response) =
|
||||||
json.parseToJsonElement(response.body!!.string()).run {
|
response.decode<Paginator<Series>>().let {
|
||||||
MangasPage(
|
MangasPage(it.map(::mangaFromJSON), !it.last)
|
||||||
jsonArray.map {
|
|
||||||
val obj = it.jsonObject
|
|
||||||
SManga.create().apply {
|
|
||||||
url = obj["url"]!!.jsonPrimitive.content
|
|
||||||
title = obj["title"]!!.jsonPrimitive.content
|
|
||||||
thumbnail_url = obj["cover"]!!.jsonPrimitive.content
|
|
||||||
// A bit of a hack to sort by date
|
|
||||||
val latest = obj["latest_chapter"]!!.jsonObject
|
|
||||||
description = httpDateToTimestamp(
|
|
||||||
latest["date"]!!.jsonPrimitive.content
|
|
||||||
).toString()
|
|
||||||
}
|
|
||||||
}.sortedByDescending(SManga::description),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response) =
|
||||||
|
latestUpdatesParse(response)
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response) =
|
||||||
|
latestUpdatesParse(response)
|
||||||
|
|
||||||
override fun chapterListParse(response: Response) =
|
override fun chapterListParse(response: Response) =
|
||||||
json.parseToJsonElement(response.body!!.string())
|
response.decode<Results<Chapter>>().map { chapter ->
|
||||||
.jsonObject["volumes"]!!.jsonObject.entries.flatMap { vol ->
|
SChapter.create().apply {
|
||||||
vol.value.jsonObject.entries.map { ch ->
|
url = chapter.url
|
||||||
SChapter.create().fromJSON(
|
name = buildString {
|
||||||
JsonObject(
|
append(chapter.full_title)
|
||||||
ch.value.jsonObject.toMutableMap().also {
|
if (chapter.final) append(" [END]")
|
||||||
it["volume"] = JsonPrimitive(vol.key)
|
}
|
||||||
it["chapter"] = JsonPrimitive(ch.key)
|
chapter_number = chapter.number
|
||||||
}
|
date_upload = chapter.published.toLong()
|
||||||
)
|
scanlator = chapter.groups.joinToString()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response) =
|
override fun mangaDetailsParse(response: Response) =
|
||||||
SManga.create().fromJSON(
|
response.decode<Series>().let(::mangaFromJSON)
|
||||||
json.parseToJsonElement(response.body!!.string()).jsonObject
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response) =
|
override fun pageListParse(response: Response) =
|
||||||
json.parseToJsonElement(response.body!!.string()).jsonObject.run {
|
response.decode<Results<Page>>().map { page ->
|
||||||
val url = get("url")!!.jsonPrimitive.content
|
SPage(page.number, page.url, page.image)
|
||||||
val root = get("pages_root")!!.jsonPrimitive.content
|
}
|
||||||
get("pages_list")!!.jsonArray.mapIndexed { i, e ->
|
|
||||||
Page(i, "$url${i + 1}", "$root${e.jsonPrimitive.content}")
|
// Return the real URL for "Open in browser"
|
||||||
|
override fun mangaDetailsRequest(manga: SManga) =
|
||||||
|
GET(baseUrl + manga.url, headers)
|
||||||
|
|
||||||
|
// Workaround to allow "Open in browser" to use the real URL
|
||||||
|
override fun fetchMangaDetails(manga: SManga) =
|
||||||
|
client.newCall(GET("$apiUri/series/${manga.slug}", headers))
|
||||||
|
.asObservableSuccess().map {
|
||||||
|
mangaDetailsParse(it).apply { initialized = true }
|
||||||
|
}!!
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used!")
|
||||||
|
|
||||||
|
override fun getFilterList() =
|
||||||
|
FilterList(
|
||||||
|
Author(),
|
||||||
|
Artist(),
|
||||||
|
SortOrder(orders),
|
||||||
|
Status(statuses),
|
||||||
|
CategoryList(categories)
|
||||||
|
)
|
||||||
|
|
||||||
|
/** The slug of the manga. */
|
||||||
|
private inline val SManga.slug
|
||||||
|
get() = url.split('/')[2]
|
||||||
|
|
||||||
|
/** The components (series, volume, number) of the chapter. */
|
||||||
|
private inline val SChapter.components
|
||||||
|
get() = url.split('/').slice(2..5)
|
||||||
|
|
||||||
|
/** Decodes the JSON response as an object. */
|
||||||
|
private inline fun <reified T> Response.decode() =
|
||||||
|
json.decodeFromJsonElement<T>(json.parseToJsonElement(body!!.string()))
|
||||||
|
|
||||||
|
/** Converts a [Series] object to an [SManga]. */
|
||||||
|
private fun mangaFromJSON(series: Series) =
|
||||||
|
SManga.create().apply {
|
||||||
|
url = series.url
|
||||||
|
title = series.title
|
||||||
|
thumbnail_url = series.cover
|
||||||
|
description = series.description
|
||||||
|
author = series.authors?.joinToString()
|
||||||
|
artist = series.artists?.joinToString()
|
||||||
|
genre = series.categories?.joinToString()
|
||||||
|
status = when (series.completed) {
|
||||||
|
true -> SManga.COMPLETED
|
||||||
|
false -> SManga.ONGOING
|
||||||
|
null -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response) =
|
|
||||||
json.parseToJsonElement(response.body!!.string()).run {
|
|
||||||
MangasPage(
|
|
||||||
jsonArray.map {
|
|
||||||
val obj = it.jsonObject
|
|
||||||
SManga.create().apply {
|
|
||||||
url = obj["url"]!!.jsonPrimitive.content
|
|
||||||
title = obj["title"]!!.jsonPrimitive.content
|
|
||||||
thumbnail_url = obj["cover"]!!.jsonPrimitive.content
|
|
||||||
}
|
|
||||||
}.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 {
|
companion object {
|
||||||
/** The possible statuses of a manga. */
|
|
||||||
private val STATUSES = arrayOf("Any", "Completed", "Ongoing")
|
|
||||||
|
|
||||||
/** Manga categories from MangAdventure `categories.xml` fixture. */
|
/** Manga categories from MangAdventure `categories.xml` fixture. */
|
||||||
internal val DEFAULT_CATEGORIES = listOf(
|
val DEFAULT_CATEGORIES = listOf(
|
||||||
"4-Koma",
|
"4-Koma",
|
||||||
"Action",
|
"Action",
|
||||||
"Adventure",
|
"Adventure",
|
||||||
|
@ -233,61 +223,5 @@ abstract class MangAdventure(
|
||||||
|
|
||||||
/** Query to search by manga slug. */
|
/** Query to search by manga slug. */
|
||||||
internal const val SLUG_QUERY = "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(lang))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.mangadventure
|
||||||
|
|
||||||
|
/** Generic results wrapper schema. */
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
internal class Results<T>(
|
||||||
|
private val results: List<T>
|
||||||
|
) : Iterable<T> by results
|
||||||
|
|
||||||
|
/** Generic paginator schema. */
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
internal class Paginator<T>(
|
||||||
|
val last: Boolean,
|
||||||
|
private val results: List<T>
|
||||||
|
) : Iterable<T> by results
|
||||||
|
|
||||||
|
/** Page model schema. */
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
internal data class Page(
|
||||||
|
private val id: Int,
|
||||||
|
val image: String,
|
||||||
|
val number: Int,
|
||||||
|
val url: String
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
this === other || other is Page && id == other.id
|
||||||
|
|
||||||
|
override fun hashCode() = id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Chapter model schema. */
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
internal data class Chapter(
|
||||||
|
private val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val number: Float,
|
||||||
|
val volume: Int,
|
||||||
|
val published: String,
|
||||||
|
val final: Boolean,
|
||||||
|
val series: String,
|
||||||
|
val groups: List<String>,
|
||||||
|
val full_title: String,
|
||||||
|
val url: String
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
this === other || other is Chapter && id == other.id
|
||||||
|
|
||||||
|
override fun hashCode() = id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Series model schema. */
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
internal data class Series(
|
||||||
|
private val slug: String,
|
||||||
|
val title: String,
|
||||||
|
val url: String,
|
||||||
|
val cover: String?,
|
||||||
|
val description: String? = null,
|
||||||
|
val completed: Boolean? = null,
|
||||||
|
val authors: List<String>? = null,
|
||||||
|
val artists: List<String>? = null,
|
||||||
|
val categories: List<String>? = null
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
this === other || other is Series && slug == other.slug
|
||||||
|
|
||||||
|
override fun hashCode() = slug.hashCode()
|
||||||
|
}
|
|
@ -1,93 +0,0 @@
|
||||||
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 kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.boolean
|
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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].
|
|
||||||
* @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 = ", ") =
|
|
||||||
size.takeIf { it != 0 }?.run {
|
|
||||||
joinToString(sep) { it.jsonArray[field].jsonPrimitive.content }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = ", ") =
|
|
||||||
size.takeIf { it != 0 }?.run {
|
|
||||||
joinToString(sep) { it.jsonObject[field]!!.jsonPrimitive.content }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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["url"]!!.jsonPrimitive.content
|
|
||||||
title = obj["title"]!!.jsonPrimitive.content
|
|
||||||
description = obj["description"]!!.jsonPrimitive.content
|
|
||||||
thumbnail_url = obj["cover"]!!.jsonPrimitive.content
|
|
||||||
author = obj["authors"]!!.jsonArray.joinField(0)
|
|
||||||
artist = obj["artists"]!!.jsonArray.joinField(0)
|
|
||||||
genre = obj["categories"]!!.jsonArray.joinField("name")
|
|
||||||
status = if (obj["completed"]!!.jsonPrimitive.boolean)
|
|
||||||
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["url"]!!.jsonPrimitive.content
|
|
||||||
chapter_number = obj["chapter"]?.jsonPrimitive?.content?.toFloat() ?: -1f
|
|
||||||
date_upload = MangAdventure.httpDateToTimestamp(
|
|
||||||
obj["date"]!!.jsonPrimitive.content
|
|
||||||
)
|
|
||||||
scanlator = obj["groups"]!!.jsonArray.joinField("name", " & ")
|
|
||||||
name = obj["full_title"]?.jsonPrimitive?.contentOrNull ?: buildString {
|
|
||||||
obj["volume"]?.jsonPrimitive?.intOrNull?.let {
|
|
||||||
if (it != 0) append("Vol. $it, ")
|
|
||||||
}
|
|
||||||
append("Ch. ${chapter_number.format("#.#")}: ")
|
|
||||||
append(obj["title"]!!.jsonPrimitive.content)
|
|
||||||
}
|
|
||||||
if (obj["final"]!!.jsonPrimitive.boolean) name += " [END]"
|
|
||||||
}
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.mangadventure
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
/** Filter representing the name of an author. */
|
||||||
|
internal class Author : Filter.Text("Author") {
|
||||||
|
override fun toString() = state
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filter representing the name of an artist. */
|
||||||
|
internal class Artist : Filter.Text("Artist") {
|
||||||
|
override fun toString() = state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter representing the sort order.
|
||||||
|
*
|
||||||
|
* @param labels The site's sort order labels.
|
||||||
|
*/
|
||||||
|
internal class SortOrder(
|
||||||
|
private val labels: Array<String>
|
||||||
|
) : Filter.Sort("Sort", values, null) {
|
||||||
|
override fun toString() = when (state?.ascending) {
|
||||||
|
null -> ""
|
||||||
|
true -> labels[state!!.index]
|
||||||
|
false -> "-" + labels[state!!.index]
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The available sort order values. */
|
||||||
|
private val values = arrayOf("title", "latest_upload", "chapter_count")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter representing the status of a manga.
|
||||||
|
*
|
||||||
|
* @param statuses The site's status names.
|
||||||
|
*/
|
||||||
|
internal class Status(
|
||||||
|
statuses: Array<String>
|
||||||
|
) : Filter.Select<String>("Status", statuses) {
|
||||||
|
override fun toString() = values[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter representing a manga category.
|
||||||
|
*
|
||||||
|
* @param name The display name of the category.
|
||||||
|
*/
|
||||||
|
internal class Category(name: String) : Filter.TriState(name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter representing the [categories][Category] of a manga.
|
||||||
|
*
|
||||||
|
* @param categories The site's manga categories.
|
||||||
|
*/
|
||||||
|
internal class CategoryList(
|
||||||
|
categories: List<String>
|
||||||
|
) : Filter.Group<Category>("Categories", categories.map(::Category)) {
|
||||||
|
override fun toString() = state.filterNot { it.isIgnored() }
|
||||||
|
.joinToString(",") { if (it.isIncluded()) it.name else "-" + it.name }
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ class MangAdventureGenerator : ThemeSourceGenerator {
|
||||||
|
|
||||||
override val themeClass = "MangAdventure"
|
override val themeClass = "MangAdventure"
|
||||||
|
|
||||||
override val baseVersionCode = 4
|
override val baseVersionCode = 5
|
||||||
|
|
||||||
override val sources = listOf(
|
override val sources = listOf(
|
||||||
SingleLang("Arc-Relight", "https://arc-relight.com", "en", className = "ArcRelight"),
|
SingleLang("Arc-Relight", "https://arc-relight.com", "en", className = "ArcRelight"),
|
||||||
|
|
Loading…
Reference in New Issue