Manta Comics: new extension (#9635)
This commit is contained in:
parent
7ec5b66189
commit
6a5bad1f42
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Manta Comics'
|
||||
pkgNameSuffix = 'en.manta'
|
||||
extClass = '.MantaComics'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
|
@ -0,0 +1,127 @@
|
|||
package eu.kanade.tachiyomi.extension.en.manta
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS as MS
|
||||
|
||||
private val isoDate by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT)
|
||||
}
|
||||
|
||||
private inline val String?.timestamp: Long
|
||||
get() = this?.substringBefore('.')?.let(isoDate::parse)?.time ?: 0L
|
||||
|
||||
@Serializable
|
||||
data class Series<T : Any>(
|
||||
val data: T,
|
||||
val id: Int,
|
||||
val image: Cover,
|
||||
val episodes: List<Episode>? = null
|
||||
) {
|
||||
override fun toString() = data.toString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Title(private val title: Name) {
|
||||
override fun toString() = title.toString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Details(
|
||||
val tags: List<Tag>,
|
||||
val isCompleted: Boolean? = null,
|
||||
private val description: Description,
|
||||
private val creators: List<Creator>
|
||||
) {
|
||||
val artists by lazy {
|
||||
creators.filter { it.role == "Illustration" }
|
||||
}
|
||||
|
||||
val authors by lazy {
|
||||
creators.filter { it.role != "Illustration" }.ifEmpty { creators }
|
||||
}
|
||||
|
||||
override fun toString() = description.toString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Episode(
|
||||
val id: Int,
|
||||
val ord: Int,
|
||||
val data: Data?,
|
||||
private val createdAt: String,
|
||||
val cutImages: List<Image>? = null
|
||||
) {
|
||||
val timestamp: Long
|
||||
get() = createdAt.timestamp
|
||||
|
||||
val isLocked: Boolean
|
||||
get() = timeTillFree > 0
|
||||
|
||||
val waitingTime: String
|
||||
get() = when (val days = MS.toDays(timeTillFree)) {
|
||||
0L -> "later today"
|
||||
1L -> "tomorrow"
|
||||
else -> "in $days days"
|
||||
}
|
||||
|
||||
private val timeTillFree by lazy {
|
||||
data?.freeAt.timestamp - System.currentTimeMillis()
|
||||
}
|
||||
|
||||
override fun toString() = buildString {
|
||||
append(data?.title ?: "Episode $ord")
|
||||
if (isLocked) append(" \uD83D\uDD12")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Data(
|
||||
val title: String? = null,
|
||||
val freeAt: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Creator(
|
||||
private val name: String,
|
||||
val role: String
|
||||
) {
|
||||
override fun toString() = name
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Description(
|
||||
private val long: String,
|
||||
private val short: String
|
||||
) {
|
||||
override fun toString() = "$short\n\n$long"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Cover(private val `1280x1840_480`: Image) {
|
||||
override fun toString() = `1280x1840_480`.toString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Image(private val downloadUrl: String) {
|
||||
override fun toString() = downloadUrl
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Tag(private val name: Name) {
|
||||
override fun toString() = name.toString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Name(private val en: String) {
|
||||
override fun toString() = en
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Status(
|
||||
private val description: String,
|
||||
private val message: String
|
||||
) {
|
||||
override fun toString() = "$description: $message"
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package eu.kanade.tachiyomi.extension.en.manta
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
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 kotlinx.serialization.json.jsonObject
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MantaComics : HttpSource() {
|
||||
override val name = "Manta"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val baseUrl = "https://manta.net"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val json by injectLazy<Json>()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("User-Agent", "Manta/167").set("Origin", baseUrl)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/manta/v1/search/series?cat=New", headers)
|
||||
|
||||
override fun fetchPopularManga(page: Int) =
|
||||
latestUpdatesRequest(page).fetch(::searchMangaParse)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
filters.category.ifEmpty { if (query.isEmpty()) "New" else "" }.let {
|
||||
GET("$baseUrl/manta/v1/search/series?cat=$it&q=$query", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) =
|
||||
response.parse<List<Series<Title>>>().map {
|
||||
SManga.create().apply {
|
||||
title = it.toString()
|
||||
url = it.id.toString()
|
||||
thumbnail_url = it.image.toString()
|
||||
}
|
||||
}.let { MangasPage(it, false) }
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
searchMangaRequest(page, query, filters).fetch(::searchMangaParse)
|
||||
|
||||
// Request the actual manga URL for the webview
|
||||
override fun mangaDetailsRequest(manga: SManga) =
|
||||
GET("$baseUrl/series/${manga.url}")
|
||||
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
SManga.create().apply {
|
||||
val data = response.parse<Series<Details>>().data
|
||||
description = data.toString()
|
||||
genre = data.tags.joinToString()
|
||||
artist = data.artists.joinToString()
|
||||
author = data.authors.joinToString()
|
||||
status = when (data.isCompleted) {
|
||||
true -> SManga.COMPLETED
|
||||
else -> SManga.ONGOING
|
||||
}
|
||||
initialized = true
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga) =
|
||||
chapterListRequest(manga).fetch(::mangaDetailsParse)
|
||||
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
GET("$baseUrl/front/v1/series/${manga.url}", headers)
|
||||
|
||||
override fun chapterListParse(response: Response) =
|
||||
response.parse<Series<Title>>().episodes!!.map {
|
||||
SChapter.create().apply {
|
||||
name = it.toString()
|
||||
url = it.id.toString()
|
||||
date_upload = it.timestamp
|
||||
chapter_number = it.ord.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga) =
|
||||
chapterListRequest(manga).fetch(::chapterListParse)
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) =
|
||||
GET("$baseUrl/front/v1/episodes/${chapter.url}", headers)
|
||||
|
||||
override fun pageListParse(response: Response) =
|
||||
response.parse<Episode>().run {
|
||||
if (!isLocked) return@run cutImages!!
|
||||
error("This episode will be available $waitingTime.")
|
||||
}.mapIndexed { idx, img -> Page(idx, "", img.toString()) }
|
||||
|
||||
override fun fetchPageList(chapter: SChapter) =
|
||||
pageListRequest(chapter).fetch(::pageListParse)
|
||||
|
||||
override fun getFilterList() = FilterList(Category())
|
||||
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
private fun <R> Request.fetch(parse: (Response) -> R) =
|
||||
client.newCall(this).asObservable().map { res ->
|
||||
if (res.isSuccessful) return@map parse(res)
|
||||
error(res.parse<Status>("status").toString())
|
||||
}!!
|
||||
|
||||
private inline fun <reified T> Response.parse(key: String = "data") =
|
||||
json.decodeFromJsonElement<T>(
|
||||
json.parseToJsonElement(body!!.string()).jsonObject[key]!!
|
||||
)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package eu.kanade.tachiyomi.extension.en.manta
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
private val categories = arrayOf(
|
||||
"",
|
||||
"New",
|
||||
"Exclusive",
|
||||
"Completed",
|
||||
"Romance",
|
||||
"BL / GL",
|
||||
"Drama",
|
||||
"Fantasy",
|
||||
"Thriller",
|
||||
"Slice of life"
|
||||
)
|
||||
|
||||
class Category(
|
||||
values: Array<String> = categories
|
||||
) : Filter.Select<String>("Category", values)
|
||||
|
||||
inline val List<Filter<*>>.category: String
|
||||
get() = (firstOrNull() as? Category)?.run { values[state] } ?: ""
|
Loading…
Reference in New Issue