Implement ReaderFront multisrc (#8545)
This commit is contained in:
parent
7ea52804f1
commit
d061b6597f
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
|
@ -0,0 +1,20 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.ravensscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||||
|
import eu.kanade.tachiyomi.multisrc.readerfront.ReaderFront
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
|
class RavensScansFactory : SourceFactory {
|
||||||
|
override fun createSources() = listOf(
|
||||||
|
RavensScans("es", 1),
|
||||||
|
RavensScans("en", 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Nsfw
|
||||||
|
class RavensScans(override val lang: String, override val langId: Int) :
|
||||||
|
ReaderFront("Ravens Scans", "https://ravens-scans.com/", lang, langId) {
|
||||||
|
override fun getImageCDN(path: String, width: Int) =
|
||||||
|
"https://i${(0..2).random()}.wp.com/img-cdn1.ravens-scans.com" +
|
||||||
|
"$path?strip=all&quality=100&w=$width"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.readerfront
|
||||||
|
|
||||||
|
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.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
abstract class ReaderFront(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
override val lang: String,
|
||||||
|
open val langId: Int
|
||||||
|
) : HttpSource() {
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val json by injectLazy<Json>()
|
||||||
|
|
||||||
|
open val apiUrl by lazy { baseUrl.replaceFirst("://", "://api.") }
|
||||||
|
|
||||||
|
abstract fun getImageCDN(path: String, width: Int = 350): String
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
|
GET("$apiUrl?query=${works(langId, "updatedAt", "DESC", page, 12)}", headers)
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) =
|
||||||
|
GET("$apiUrl?query=${works(langId, "stub", "ASC", page, 120)}", headers)
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga) =
|
||||||
|
GET("$baseUrl/work/$lang/${manga.url}", headers)
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) =
|
||||||
|
GET("$apiUrl?query=${chaptersByWork(langId, manga.url)}", headers)
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter) =
|
||||||
|
GET("$apiUrl?query=${chapterById(chapter.url.toInt())}", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response) =
|
||||||
|
response.parse<List<Work>>("works").map {
|
||||||
|
SManga.create().apply {
|
||||||
|
url = it.stub
|
||||||
|
title = it.toString()
|
||||||
|
thumbnail_url = getImageCDN(it.thumbnail_path)
|
||||||
|
}
|
||||||
|
}.let { MangasPage(it, false) }
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response) =
|
||||||
|
latestUpdatesParse(response)
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response) =
|
||||||
|
response.parse<Work>("work").let {
|
||||||
|
SManga.create().apply {
|
||||||
|
url = it.stub
|
||||||
|
title = it.toString()
|
||||||
|
thumbnail_url = getImageCDN(it.thumbnail_path)
|
||||||
|
description = it.description
|
||||||
|
author = it.authors!!.joinToString()
|
||||||
|
artist = it.artists!!.joinToString()
|
||||||
|
genre = buildString {
|
||||||
|
if (it.adult!!) append("18+, ")
|
||||||
|
append(it.demographic_name!!)
|
||||||
|
if (it.genres!!.isNotEmpty()) {
|
||||||
|
append(", ")
|
||||||
|
it.genres.joinTo(this, transform = ::capitalize)
|
||||||
|
}
|
||||||
|
append(", ")
|
||||||
|
append(it.type!!)
|
||||||
|
}
|
||||||
|
status = when {
|
||||||
|
it.licensed!! -> SManga.LICENSED
|
||||||
|
it.status_name == "on_going" -> SManga.ONGOING
|
||||||
|
it.status_name == "completed" -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response) =
|
||||||
|
response.parse<List<Chapter>>("chaptersByWork").map {
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = it.id.toString()
|
||||||
|
name = it.toString()
|
||||||
|
chapter_number = it.number
|
||||||
|
date_upload = it.timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response) =
|
||||||
|
response.parse<PageList>("chapterById").let {
|
||||||
|
it.mapIndexed { idx, page ->
|
||||||
|
Page(idx, "", getImageCDN(it.path(page), page.width))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga) =
|
||||||
|
GET("$apiUrl?query=${work(langId, manga.url)}", headers).let {
|
||||||
|
client.newCall(it).asObservableSuccess().map(::mangaDetailsParse)
|
||||||
|
}!!
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
|
client.newCall(popularMangaRequest(page)).asObservableSuccess().map { res ->
|
||||||
|
popularMangaParse(res).let { mp ->
|
||||||
|
mp.copy(mp.mangas.filter { it.title.contains(query, true) })
|
||||||
|
}
|
||||||
|
}!!
|
||||||
|
|
||||||
|
private fun capitalize(name: Name) =
|
||||||
|
name.split('_').joinToString(" ") { it.capitalize(Locale(lang)) }
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parse(name: String) =
|
||||||
|
json.parseToJsonElement(body!!.string()).jsonObject.run {
|
||||||
|
if (containsKey("errors")) {
|
||||||
|
throw Error(get("errors")!![0]["message"].content)
|
||||||
|
}
|
||||||
|
json.decodeFromJsonElement<T>(get("data")!![name])
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun JsonElement.get(key: String) = jsonObject[key]!!
|
||||||
|
|
||||||
|
private operator fun JsonElement.get(index: Int) = jsonArray[index]
|
||||||
|
|
||||||
|
private inline val JsonElement.content get() = jsonPrimitive.content
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
|
throw UnsupportedOperationException("Not used!")
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used!")
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used!")
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.readerfront
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Work(
|
||||||
|
private val name: String,
|
||||||
|
val stub: String,
|
||||||
|
val thumbnail_path: String,
|
||||||
|
val adult: Boolean? = null,
|
||||||
|
val type: String? = null,
|
||||||
|
val licensed: Boolean? = null,
|
||||||
|
val status_name: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val demographic_name: String? = null,
|
||||||
|
val genres: List<Name>? = null,
|
||||||
|
private val people_works: List<People>? = null
|
||||||
|
) {
|
||||||
|
@Transient
|
||||||
|
val authors = people_works?.filter { it.role == 1 }
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val artists = people_works?.filter { it.role == 2 }
|
||||||
|
|
||||||
|
override fun toString() = name
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Chapter(
|
||||||
|
val id: Int,
|
||||||
|
private val chapter: Int,
|
||||||
|
private val subchapter: Int,
|
||||||
|
private val volume: Int,
|
||||||
|
private val name: String,
|
||||||
|
private val releaseDate: String
|
||||||
|
) {
|
||||||
|
@Transient
|
||||||
|
val number = "$chapter.$subchapter".toFloat()
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val timestamp = dateFormat.parse(releaseDate)?.time ?: 0L
|
||||||
|
|
||||||
|
override fun toString() = buildString {
|
||||||
|
if (volume > 0) append("Volume $volume ")
|
||||||
|
if (number > 0) append("Chapter ${decimalFormat.format(number)}: ")
|
||||||
|
append(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val decimalFormat = DecimalFormat("#.##")
|
||||||
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PageList(
|
||||||
|
private val uniqid: String,
|
||||||
|
private val work: Uniqid,
|
||||||
|
private val pages: List<Page>
|
||||||
|
) : Iterable<Page> by pages {
|
||||||
|
/** Get the path of a page in the list. */
|
||||||
|
fun path(page: Page) = "/works/$work/$this/$page"
|
||||||
|
|
||||||
|
override fun toString() = uniqid
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Page(private val filename: String, val width: Int) {
|
||||||
|
override fun toString() = filename
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Uniqid(private val uniqid: String) {
|
||||||
|
override fun toString() = uniqid
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class People(val role: Int, private val people: Name) {
|
||||||
|
override fun toString() = people.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Name(private val name: String) : CharSequence by name {
|
||||||
|
override fun toString() = name
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.readerfront
|
||||||
|
|
||||||
|
import generator.ThemeSourceData.MultiLang
|
||||||
|
import generator.ThemeSourceGenerator
|
||||||
|
|
||||||
|
class ReaderFrontGenerator : ThemeSourceGenerator {
|
||||||
|
override val themePkg = "readerfront"
|
||||||
|
|
||||||
|
override val themeClass = "ReaderFront"
|
||||||
|
|
||||||
|
override val baseVersionCode = 1
|
||||||
|
|
||||||
|
override val sources = listOf(
|
||||||
|
MultiLang("Ravens Scans", "https://ravens-scans.com/", listOf("es", "en"), true),
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) = ReaderFrontGenerator().createAll()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.readerfront
|
||||||
|
|
||||||
|
private fun String.encodeUri() =
|
||||||
|
android.net.Uri.encode(trimMargin())!!
|
||||||
|
|
||||||
|
fun works(lang: Int, sort: String, order: String, page: Int, limit: Int) = """{
|
||||||
|
|works(
|
||||||
|
| orderBy: "$order"
|
||||||
|
| sortBy: "$sort"
|
||||||
|
| first: $limit
|
||||||
|
| offset: ${(page - 1) * limit}
|
||||||
|
| languages: [$lang]
|
||||||
|
| showHidden: false
|
||||||
|
|) {
|
||||||
|
| name
|
||||||
|
| stub
|
||||||
|
| thumbnail_path
|
||||||
|
|}
|
||||||
|
}""".encodeUri()
|
||||||
|
|
||||||
|
fun work(lang: Int, stub: String) = """{
|
||||||
|
|work(
|
||||||
|
| stub: "$stub"
|
||||||
|
| language: $lang
|
||||||
|
| showHidden: false
|
||||||
|
|) {
|
||||||
|
| name
|
||||||
|
| stub
|
||||||
|
| thumbnail_path
|
||||||
|
| status_name
|
||||||
|
| adult
|
||||||
|
| type
|
||||||
|
| licensed
|
||||||
|
| description
|
||||||
|
| demographic_name
|
||||||
|
| genres { name }
|
||||||
|
| people_works {
|
||||||
|
| role: rol
|
||||||
|
| people { name }
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
}""".encodeUri()
|
||||||
|
|
||||||
|
fun chaptersByWork(lang: Int, stub: String) = """{
|
||||||
|
|chaptersByWork(
|
||||||
|
| workStub: "$stub"
|
||||||
|
| languages: [$lang]
|
||||||
|
| showHidden: false
|
||||||
|
|) {
|
||||||
|
| id
|
||||||
|
| chapter
|
||||||
|
| subchapter
|
||||||
|
| volume
|
||||||
|
| name
|
||||||
|
| releaseDate
|
||||||
|
|}
|
||||||
|
}""".encodeUri()
|
||||||
|
|
||||||
|
fun chapterById(id: Int) = """{
|
||||||
|
|chapterById(
|
||||||
|
| id: $id
|
||||||
|
| showHidden: false
|
||||||
|
|) {
|
||||||
|
| uniqid
|
||||||
|
| work { uniqid }
|
||||||
|
| pages {
|
||||||
|
| filename
|
||||||
|
| width
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
}""".encodeUri()
|
Loading…
Reference in New Issue