Update Fansubs.cat API calls, add new NSFW site, and implement search filters (#19060)

* Split Fansubs.cat into two sites (SFW and NSFW), update API calls, and implement search filters

* Unify Fansubs.cat and Fansubs.cat Hentai extensions into a single multisrc theme

* Add missing trailing comma

* Explicitly name custom argument

* Explicitly keep the pkgName for the main extension

* Remove unneeded workaround
This commit is contained in:
Fansubs.cat 2023-11-25 00:32:53 +01:00 committed by GitHub
parent f0ec2254c9
commit a861b95ff4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 470 additions and 178 deletions

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="FansubsCatGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="tachiyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.fansubscat.FansubsCatGenerator" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=fansubscat" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=fansubscat" />
</method>
</configuration>
</component>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.extension.ca.fansubscat
import eu.kanade.tachiyomi.multisrc.fansubscat.FansubsCat
class FansubsCatMain : FansubsCat(
"Fansubs.cat",
"https://manga.fansubs.cat",
"ca",
isHentaiSite = false,
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.extension.ca.fansubscathentai
import eu.kanade.tachiyomi.multisrc.fansubscat.FansubsCat
class FansubsCatHentai : FansubsCat(
"Fansubs.cat - Hentai",
"https://hentai.fansubs.cat/manga",
"ca",
isHentaiSite = true,
)

View File

@ -0,0 +1,403 @@
package eu.kanade.tachiyomi.multisrc.fansubscat
import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.network.GET
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.float
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
abstract class FansubsCat(
override val name: String,
override val baseUrl: String,
override val lang: String,
val isHentaiSite: Boolean,
) : HttpSource() {
private val apiBaseUrl = "https://api.fansubs.cat"
override val supportsLatest = true
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Tachiyomi/${AppInfo.getVersionName()}")
override val client: OkHttpClient = network.client
private val json: Json by injectLazy()
private fun parseMangaFromJson(response: Response): MangasPage {
val jsonObject = json.decodeFromString<JsonObject>(response.body.string())
val mangas = jsonObject["result"]!!.jsonArray.map { json ->
SManga.create().apply {
url = json.jsonObject["slug"]!!.jsonPrimitive.content
title = json.jsonObject["name"]!!.jsonPrimitive.content
thumbnail_url = json.jsonObject["thumbnail_url"]!!.jsonPrimitive.content
author = json.jsonObject["author"]!!.jsonPrimitive.contentOrNull
description = json.jsonObject["synopsis"]!!.jsonPrimitive.contentOrNull
status = json.jsonObject["status"]!!.jsonPrimitive.content.toStatus()
genre = json.jsonObject["genres"]!!.jsonPrimitive.contentOrNull
}
}
return MangasPage(mangas, mangas.size >= 20)
}
private fun parseChapterListFromJson(response: Response): List<SChapter> {
val jsonObject = json.decodeFromString<JsonObject>(response.body.string())
return jsonObject["result"]!!.jsonArray.map { json ->
SChapter.create().apply {
url = json.jsonObject["id"]!!.jsonPrimitive.content
name = json.jsonObject["title"]!!.jsonPrimitive.content
chapter_number = json.jsonObject["number"]!!.jsonPrimitive.float
scanlator = json.jsonObject["fansub"]!!.jsonPrimitive.content
date_upload = json.jsonObject["created"]!!.jsonPrimitive.long
}
}
}
private fun parsePageListFromJson(response: Response): List<Page> {
val jsonObject = json.decodeFromString<JsonObject>(response.body.string())
return jsonObject["result"]!!.jsonArray.mapIndexed { i, it ->
Page(
i,
it.jsonObject["url"]!!.jsonPrimitive.content,
it.jsonObject["url"]!!.jsonPrimitive.content,
)
}
}
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$apiBaseUrl/manga/popular/$page?hentai=$isHentaiSite", headers)
}
override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$apiBaseUrl/manga/recent/$page?hentai=$isHentaiSite", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response)
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val mangaTypeFilter = filterList.find { it is MangaTypeFilter } as MangaTypeFilter
val stateFilter = filterList.find { it is StateFilter } as StateFilter
val demographyFilter = filterList.find { it is DemographyFilter } as DemographyFilter
val genreFilter = filterList.find { it is GenreTagFilter } as GenreTagFilter
val themeFilter = filterList.find { it is ThemeTagFilter } as ThemeTagFilter
val builder = "$apiBaseUrl/manga/search/$page?hentai=$isHentaiSite".toHttpUrl().newBuilder()
mangaTypeFilter.addQueryParameter(builder)
stateFilter.addQueryParameter(builder)
demographyFilter.addQueryParameter(builder)
genreFilter.addQueryParameter(builder)
themeFilter.addQueryParameter(builder)
if (query.isNotBlank()) {
builder.addQueryParameter("query", query)
}
return GET(builder.toString(), headers)
}
override fun searchMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
// Details
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(
"$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}?hentai=$isHentaiSite",
headers,
)
}
override fun getMangaUrl(manga: SManga): String {
return "$baseUrl/${manga.url}"
}
override fun mangaDetailsParse(response: Response): SManga {
val jsonObject = json.decodeFromString<JsonObject>(response.body.string())
val resultObject = jsonObject.jsonObject["result"]!!.jsonObject
return SManga.create().apply {
url = resultObject["slug"]!!.jsonPrimitive.content
title = resultObject["name"]!!.jsonPrimitive.content
thumbnail_url = resultObject["thumbnail_url"]!!.jsonPrimitive.content
author = resultObject["author"]!!.jsonPrimitive.contentOrNull
description = resultObject["synopsis"]!!.jsonPrimitive.contentOrNull
status = resultObject["status"]!!.jsonPrimitive.content.toStatus()
genre = resultObject["genres"]!!.jsonPrimitive.contentOrNull
}
}
private fun String?.toStatus() = when {
this == null -> SManga.UNKNOWN
this.contains("ongoing", ignoreCase = true) -> SManga.ONGOING
this.contains("finished", ignoreCase = true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
// Chapters
override fun chapterListRequest(manga: SManga): Request {
return GET(
"$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}?hentai=$isHentaiSite",
headers,
)
}
override fun chapterListParse(response: Response): List<SChapter> =
parseChapterListFromJson(response)
// Pages
override fun pageListRequest(chapter: SChapter): Request {
return GET(
"$apiBaseUrl/manga/pages/${chapter.url.substringAfterLast('/')}?hentai=$isHentaiSite",
headers,
)
}
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl/${chapter.url.replace("/", "?f=")}"
}
override fun pageListParse(response: Response): List<Page> = parsePageListFromJson(response)
override fun imageUrlParse(response: Response): String =
throw UnsupportedOperationException("Not used")
// Filter
override fun getFilterList() = FilterList(
listOfNotNull(
MangaTypeFilter("Tipus", getMangaTypeList()),
StateFilter("Estat", getStateList()),
if (!isHentaiSite) {
DemographyFilter("Demografies", getDemographyList())
} else {
null
},
GenreTagFilter("Gèneres (inclou/exclou)", getGenreList()),
ThemeTagFilter("Temàtiques (inclou/exclou)", getThemeList()),
),
)
private fun getMangaTypeList() = listOf(
MangaType("oneshot", "One-shots"),
MangaType("serialized", "Serialitzats"),
)
private fun getStateList() = listOf(
State(1, "Completat"),
State(2, "En procés"),
State(3, "Parcialment completat"),
State(4, "Abandonat"),
State(5, "Cancel·lat"),
)
private fun getDemographyList() = listOf(
Demography(35, "Infantil"),
Demography(27, "Josei"),
Demography(12, "Seinen"),
Demography(16, "Shōjo"),
Demography(1, "Shōnen"),
Demography(-1, "No definida"),
)
private fun getGenreList() = listOfNotNull(
Tag(4, "Acció"),
Tag(7, "Amor"),
Tag(38, "Amor entre noies"),
Tag(23, "Amor entre nois"),
Tag(31, "Avantguardisme"),
Tag(6, "Aventura"),
Tag(10, "Ciència-ficció"),
Tag(2, "Comèdia"),
Tag(47, "De prestigi"),
Tag(3, "Drama"),
Tag(19, "Ecchi"),
Tag(46, "Erotisme"),
Tag(20, "Esports"),
Tag(5, "Fantasia"),
Tag(48, "Gastronomia"),
if (isHentaiSite) {
Tag(34, "Hentai")
} else {
null
},
Tag(11, "Misteri"),
Tag(8, "Sobrenatural"),
Tag(17, "Suspens"),
Tag(21, "Terror"),
Tag(42, "Vida quotidiana"),
)
private fun getThemeList() = listOf(
Tag(71, "Animals de companyia"),
Tag(50, "Antropomorfisme"),
Tag(70, "Arts escèniques"),
Tag(18, "Arts marcials"),
Tag(81, "Arts visuals"),
Tag(64, "Canvi de gènere màgic"),
Tag(56, "Comèdia de gags"),
Tag(68, "Crim organitzat"),
Tag(69, "Cultura otaku"),
Tag(30, "Curses"),
Tag(54, "Delinqüència"),
Tag(43, "Detectivesc"),
Tag(55, "Educatiu"),
Tag(9, "Escolar"),
Tag(39, "Espai"),
Tag(77, "Esports dequip"),
Tag(53, "Esports de combat"),
Tag(25, "Harem"),
Tag(73, "Harem invers"),
Tag(15, "Històric"),
Tag(59, "Idols femenines"),
Tag(60, "Idols masculins"),
Tag(75, "Indústria de lentreteniment"),
Tag(61, "Isekai"),
Tag(58, "Joc dalt risc"),
Tag(33, "Joc destratègia"),
Tag(82, "Laboral"),
Tag(29, "Mecha"),
Tag(66, "Medicina"),
Tag(67, "Memòries"),
Tag(22, "Militar"),
Tag(32, "Mitologia"),
Tag(26, "Música"),
Tag(65, "Noies màgiques"),
Tag(36, "Paròdia"),
Tag(49, "Personatges adults"),
Tag(51, "Personatges bufons"),
Tag(63, "Polígon amorós"),
Tag(13, "Psicològic"),
Tag(52, "Puericultura"),
Tag(72, "Reencarnació"),
Tag(62, "Relaxant"),
Tag(74, "Rerefons romàntic"),
Tag(37, "Samurais"),
Tag(57, "Sang i fetge"),
Tag(40, "Superpoders"),
Tag(76, "Supervivència"),
Tag(80, "Tirana"),
Tag(45, "Transformisme"),
Tag(41, "Vampirs"),
Tag(78, "Viatges en el temps"),
Tag(79, "Videojocs"),
)
private interface UrlQueryFilter {
fun addQueryParameter(url: HttpUrl.Builder)
}
internal class MangaType(val id: String, name: String) : Filter.CheckBox(name)
internal class State(val id: Int, name: String) : Filter.CheckBox(name)
internal class Tag(val id: Int, name: String) : Filter.TriState(name)
internal class Demography(val id: Int, name: String) : Filter.CheckBox(name)
private class MangaTypeFilter(collection: String, mangaTypes: List<MangaType>) :
Filter.Group<MangaType>(collection, mangaTypes),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder) {
var oneShotSelected = false
var serializedSelected = false
state.forEach { mangaType ->
if (mangaType.id.equals("oneshot") && mangaType.state) {
oneShotSelected = true
} else if (mangaType.id.equals("serialized") && mangaType.state) {
serializedSelected = true
}
}
if (oneShotSelected && !serializedSelected) {
url.addQueryParameter("type", "oneshot")
} else if (!oneShotSelected && serializedSelected) {
url.addQueryParameter("type", "serialized")
} else {
url.addQueryParameter("type", "all")
}
}
}
private class StateFilter(collection: String, states: List<State>) :
Filter.Group<State>(collection, states),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder) {
state.forEach { state ->
if (state.state) {
url.addQueryParameter("status[]", state.id.toString())
}
}
}
}
private class DemographyFilter(collection: String, demographies: List<Demography>) :
Filter.Group<Demography>(collection, demographies),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder) {
state.forEach { demography ->
if (demography.state) {
url.addQueryParameter("demographies[]", demography.id.toString())
}
}
}
}
private class GenreTagFilter(collection: String, tags: List<Tag>) :
Filter.Group<Tag>(collection, tags),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder) {
state.forEach { tag ->
if (tag.isIncluded()) {
url.addQueryParameter("genres_include[]", tag.id.toString())
} else if (tag.isExcluded()) {
url.addQueryParameter("genres_exclude[]", tag.id.toString())
}
}
}
}
private class ThemeTagFilter(collection: String, tags: List<Tag>) :
Filter.Group<Tag>(collection, tags),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder) {
state.forEach { tag ->
if (tag.isIncluded()) {
url.addQueryParameter("themes_include[]", tag.id.toString())
} else if (tag.isExcluded()) {
url.addQueryParameter("themes_exclude[]", tag.id.toString())
}
}
}
}
}

View File

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.multisrc.fansubscat
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class FansubsCatGenerator : ThemeSourceGenerator {
override val themePkg = "fansubscat"
override val themeClass = "FansubsCat"
override val baseVersionCode = 4
override val sources = listOf(
SingleLang(
name = "Fansubs.cat",
baseUrl = "https://manga.fansubs.cat",
lang = "ca",
className = "FansubsCatMain",
isNsfw = false,
pkgName = "fansubscat",
),
SingleLang(
name = "Fansubs.cat - Hentai",
baseUrl = "https://hentai.fansubs.cat/manga",
lang = "ca",
className = "FansubsCatHentai",
isNsfw = true,
),
)
companion object {
@JvmStatic
fun main(args: Array<String>) = FansubsCatGenerator().createAll()
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Fansubs.cat'
pkgNameSuffix = 'ca.fansubscat'
extClass = '.FansubsCat'
extVersionCode = 3
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

View File

@ -1,163 +0,0 @@
package eu.kanade.tachiyomi.extension.ca.fansubscat
import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.float
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
class FansubsCat : HttpSource() {
override val name = "Fansubs.cat"
override val baseUrl = "https://manga.fansubs.cat"
override val lang = "ca"
override val supportsLatest = true
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Tachiyomi/FansubsCat/${AppInfo.getVersionName()}")
override val client: OkHttpClient = network.client
private val json: Json by injectLazy()
private val apiBaseUrl = "https://api.fansubs.cat"
private fun parseMangaFromJson(response: Response): MangasPage {
val jsonObject = json.decodeFromString<JsonObject>(response.body.string())
val mangas = jsonObject["result"]!!.jsonArray.map { json ->
SManga.create().apply {
url = json.jsonObject["slug"]!!.jsonPrimitive.content
title = json.jsonObject["name"]!!.jsonPrimitive.content
thumbnail_url = json.jsonObject["thumbnail_url"]!!.jsonPrimitive.content
author = json.jsonObject["author"]!!.jsonPrimitive.contentOrNull
description = json.jsonObject["synopsis"]!!.jsonPrimitive.contentOrNull
status = json.jsonObject["status"]!!.jsonPrimitive.content.toStatus()
genre = json.jsonObject["genres"]!!.jsonPrimitive.contentOrNull
}
}
return MangasPage(mangas, mangas.size >= 20)
}
private fun parseChapterListFromJson(response: Response): List<SChapter> {
val jsonObject = json.decodeFromString<JsonObject>(response.body.string())
return jsonObject["result"]!!.jsonArray.map { json ->
SChapter.create().apply {
url = json.jsonObject["id"]!!.jsonPrimitive.content
name = json.jsonObject["title"]!!.jsonPrimitive.content
chapter_number = json.jsonObject["number"]!!.jsonPrimitive.float
scanlator = json.jsonObject["fansub"]!!.jsonPrimitive.content
date_upload = json.jsonObject["created"]!!.jsonPrimitive.long
}
}
}
private fun parsePageListFromJson(response: Response): List<Page> {
val jsonObject = json.decodeFromString<JsonObject>(response.body.string())
return jsonObject["result"]!!.jsonArray.mapIndexed { i, it ->
Page(i, it.jsonObject["url"]!!.jsonPrimitive.content, it.jsonObject["url"]!!.jsonPrimitive.content)
}
}
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$apiBaseUrl/manga/popular/$page", headers)
}
override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$apiBaseUrl/manga/recent/$page", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response)
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiBaseUrl/manga/search/$page".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("query", query)
return GET(url.toString(), headers)
}
override fun searchMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
// Details
// Workaround to allow "Open in browser" to use the real URL
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
client.newCall(apiMangaDetailsRequest(manga)).asObservableSuccess()
.map { mangaDetailsParse(it).apply { initialized = true } }
// Return the real URL for "Open in browser"
override fun mangaDetailsRequest(manga: SManga) = GET("$baseUrl/${manga.url}", headers)
private fun apiMangaDetailsRequest(manga: SManga): Request {
return GET("$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val jsonObject = json.decodeFromString<JsonObject>(response.body.string())
val resultObject = jsonObject.jsonObject["result"]!!.jsonObject
return SManga.create().apply {
url = resultObject["slug"]!!.jsonPrimitive.content
title = resultObject["name"]!!.jsonPrimitive.content
thumbnail_url = resultObject["thumbnail_url"]!!.jsonPrimitive.content
author = resultObject["author"]!!.jsonPrimitive.contentOrNull
description = resultObject["synopsis"]!!.jsonPrimitive.contentOrNull
status = resultObject["status"]!!.jsonPrimitive.content.toStatus()
genre = resultObject["genres"]!!.jsonPrimitive.contentOrNull
}
}
private fun String?.toStatus() = when {
this == null -> SManga.UNKNOWN
this.contains("ongoing", ignoreCase = true) -> SManga.ONGOING
this.contains("finished", ignoreCase = true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
// Chapters
override fun chapterListRequest(manga: SManga): Request = GET("$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}", headers)
override fun chapterListParse(response: Response): List<SChapter> = parseChapterListFromJson(response)
// Pages
override fun pageListRequest(chapter: SChapter): Request = GET("$apiBaseUrl/manga/pages/${chapter.url}", headers)
override fun pageListParse(response: Response): List<Page> = parsePageListFromJson(response)
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
}