Tachiyomi-Extensions/lib-multisrc/mangadventure/src/eu/kanade/tachiyomi/multisrc/mangadventure/MangAdventure.kt

210 lines
6.9 KiB
Kotlin

package eu.kanade.tachiyomi.multisrc.mangadventure
import android.os.Build.VERSION
import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.network.GET
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
/** MangAdventure base source. */
abstract class MangAdventure(
override val name: String,
override val baseUrl: String,
override val lang: String = "en",
) : HttpSource() {
/** The site's manga categories. */
protected open val categories = DEFAULT_CATEGORIES
/** The site's manga status names. */
protected open val statuses = arrayOf("Any", "Completed", "Ongoing", "Hiatus", "Cancelled")
/** The site's sort order labels that correspond to [SortOrder.values]. */
protected open val orders = arrayOf(
"Title",
"Views",
"Latest upload",
"Chapter count",
)
/** A user agent representing Tachiyomi. */
private val userAgent =
"Mozilla/5.0 (Android ${VERSION.RELEASE}; Mobile) Tachiyomi/${AppInfo.getVersionName()}"
/** The URL of the site's API. */
private val apiUrl by lazy { "$baseUrl/api/v2" }
/** The JSON parser of the class. */
private val json by injectLazy<Json>()
override val versionId = 3
override val supportsLatest = true
override fun headersBuilder() =
super.headersBuilder().set("User-Agent", userAgent)
override fun latestUpdatesRequest(page: Int) =
GET("$apiUrl/series?page=$page&sort=-latest_upload", headers)
override fun popularMangaRequest(page: Int) =
GET("$apiUrl/series?page=$page&sort=-views", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
apiUrl.toHttpUrl().newBuilder().addEncodedPathSegment("series").run {
if (query.startsWith(SLUG_QUERY)) {
addQueryParameter("slug", query.substringAfter(SLUG_QUERY))
} else {
addQueryParameter("page", page.toString())
addQueryParameter("title", query)
filters.filterIsInstance<UriFilter>().forEach {
addQueryParameter(it.param, it.toString())
}
}
GET(build(), headers)
}
override fun mangaDetailsRequest(manga: SManga) =
GET("$apiUrl/series/${manga.url}", headers)
override fun chapterListRequest(manga: SManga) =
GET("$apiUrl/series/${manga.url}/chapters?date_format=timestamp", headers)
override fun pageListRequest(chapter: SChapter) =
GET("$apiUrl/chapters/${chapter.url}/pages?track=true", headers)
override fun latestUpdatesParse(response: Response) =
response.decode<Paginator<Series>>().let {
MangasPage(it.map(::mangaFromJSON), !it.last)
}
override fun searchMangaParse(response: Response) =
latestUpdatesParse(response)
override fun popularMangaParse(response: Response) =
latestUpdatesParse(response)
override fun chapterListParse(response: Response) =
response.decode<Results<Chapter>>().map { chapter ->
SChapter.create().apply {
url = chapter.id.toString()
name = buildString {
append(chapter.fullTitle)
if (chapter.final) append(" [END]")
}
chapter_number = chapter.number
date_upload = chapter.published.toLong()
scanlator = chapter.groups.joinToString()
}
}
override fun mangaDetailsParse(response: Response) =
response.decode<Series>().let(::mangaFromJSON)
override fun pageListParse(response: Response) =
response.decode<Results<MAPage>>().map { page ->
Page(page.number, imageUrl = page.image)
}
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException()
override fun getMangaUrl(manga: SManga) = "$baseUrl/reader/${manga.url}"
override fun getChapterUrl(chapter: SChapter) = "$apiUrl/chapters/${chapter.url}/read"
override fun getFilterList() =
FilterList(
Author(),
Artist(),
Status(statuses),
SortOrder(orders),
CategoryList(categories),
)
/** 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.slug
title = series.title
thumbnail_url = series.cover
description = buildString {
series.description?.let(::append)
series.aliases.let {
if (!it.isNullOrEmpty()) {
it.joinTo(this, "\n", "\n\nAlternative titles:\n")
}
}
}
author = series.authors?.joinToString()
artist = series.artists?.joinToString()
genre = series.categories?.joinToString()
status = if (series.licensed == true) {
SManga.LICENSED
} else {
when (series.status) {
"completed" -> SManga.COMPLETED
"ongoing" -> SManga.ONGOING
"hiatus" -> SManga.ON_HIATUS
"canceled" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
}
companion object {
/** Manga categories from MangAdventure `categories.xml` fixture. */
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:"
}
}