add Vortex Scans (#2350)

* add VortexScans

* remove arven

* Revert "remove arven"

This reverts commit 4c67556a6cc71ff9e9f38b26d870f437d01daca6.

* in place update and actual popular

* use next cached thumbnails
This commit is contained in:
AwkwardPeak7 2024-04-14 10:42:13 +05:00 committed by Draff
parent ada1d19b34
commit 71d2a50a96
10 changed files with 362 additions and 18 deletions

View File

@ -1,9 +1,7 @@
ext { ext {
extName = 'Arven Scans' extName = 'Vortex Scans'
extClass = '.ArvenScans' extClass = '.VortexScans'
themePkg = 'mangathemesia' extVersionCode = 31
baseUrl = 'https://arvenscans.com'
overrideVersionCode = 0
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.extension.en.arvenscans
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
class ArvenScans : MangaThemesia("Arven Scans", "https://arvenscans.com", "en", "/series") {
override val client: OkHttpClient = super.client.newBuilder()
.rateLimit(20, 5, TimeUnit.SECONDS)
.build()
}

View File

@ -0,0 +1,113 @@
package eu.kanade.tachiyomi.extension.en.arvenscans
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.jsoup.Jsoup
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class SearchResponse(
val posts: List<Manga>,
val totalCount: Int,
)
@Serializable
class Manga(
val id: Int,
val slug: String,
private val postTitle: String,
private val postContent: String? = null,
val isNovel: Boolean,
private val featuredImage: String? = null,
private val alternativeTitles: String? = null,
private val author: String? = null,
private val artist: String? = null,
private val seriesType: String? = null,
private val seriesStatus: String? = null,
private val genres: List<Name>? = emptyList(),
) {
fun toSManga(baseUrl: String) = SManga.create().apply {
url = "$slug#$id"
title = postTitle
thumbnail_url = "$baseUrl/_next/image".toHttpUrl().newBuilder().apply {
addQueryParameter("url", featuredImage)
addQueryParameter("w", "828")
addQueryParameter("q", "75")
}.toString()
author = this@Manga.author?.takeUnless { it.isEmpty() }
artist = this@Manga.artist?.takeUnless { it.isEmpty() }
description = buildString {
postContent?.takeUnless { it.isEmpty() }?.let { desc ->
val tmpDesc = desc.replace("\n", "<br>")
append(Jsoup.parse(tmpDesc).text())
}
alternativeTitles?.takeUnless { it.isEmpty() }?.let { altName ->
append("\n\n")
append("Alternative Names: ")
append(altName)
}
}.trim()
genre = getGenres()
status = when (seriesStatus) {
"ONGOING", "COMING_SOON" -> SManga.ONGOING
"COMPLETED" -> SManga.COMPLETED
"CANCELLED", "DROPPED" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
initialized = true
}
fun getGenres() = buildList {
when (seriesType) {
"MANGA" -> add("Manga")
"MANHUA" -> add("Manhua")
"MANHWA" -> add("Manhwa")
else -> {}
}
genres?.forEach { add(it.name) }
}.distinct().joinToString()
}
@Serializable
class Name(val name: String)
@Serializable
class Post<T>(val post: T)
@Serializable
class ChapterListResponse(
val isNovel: Boolean,
val slug: String,
val chapters: List<Chapter>,
)
@Serializable
class Chapter(
private val id: Int,
private val slug: String,
private val number: JsonPrimitive,
private val createdBy: Name,
private val createdAt: String,
private val chapterStatus: String,
) {
fun isPublic() = chapterStatus == "PUBLIC"
fun toSChapter(mangaSlug: String) = SChapter.create().apply {
url = "/series/$mangaSlug/$slug#$id"
name = "Chapter $number"
scanlator = createdBy.name
date_upload = try {
dateFormat.parse(createdAt)!!.time
} catch (_: ParseException) {
0L
}
}
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)

View File

@ -0,0 +1,101 @@
package eu.kanade.tachiyomi.extension.en.arvenscans
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
interface UrlPartFilter {
fun addUrlParameter(url: HttpUrl.Builder)
}
abstract class SelectFilter(
name: String,
private val urlParameter: String,
private val options: List<Pair<String, String>>,
) : UrlPartFilter, Filter.Select<String>(
name,
options.map { it.first }.toTypedArray(),
) {
override fun addUrlParameter(url: HttpUrl.Builder) {
url.addQueryParameter(urlParameter, options[state].second)
}
}
class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name)
abstract class CheckBoxGroup(
name: String,
private val urlParameter: String,
options: List<Pair<String, String>>,
) : UrlPartFilter, Filter.Group<CheckBoxFilter>(
name,
options.map { CheckBoxFilter(it.first, it.second) },
) {
override fun addUrlParameter(url: HttpUrl.Builder) {
val checked = state.filter { it.state }.map { it.value }
if (checked.isNotEmpty()) {
url.addQueryParameter(urlParameter, checked.joinToString(","))
}
}
}
class StatusFilter : SelectFilter(
"Status",
"seriesStatus",
listOf(
Pair("", ""),
Pair("Ongoing", "ONGOING"),
Pair("Completed", "COMPLETED"),
Pair("Cancelled", "CANCELLED"),
Pair("Dropped", "DROPPED"),
Pair("Mass Released", "MASS_RELEASED"),
Pair("Coming Soon", "COMING_SOON"),
),
)
class TypeFilter : SelectFilter(
"Type",
"seriesType",
listOf(
Pair("", ""),
Pair("Manga", "MANGA"),
Pair("Manhua", "MANHUA"),
Pair("Manhwa", "MANHWA"),
Pair("Russian", "RUSSIAN"),
),
)
class GenreFilter : CheckBoxGroup(
"Genres",
"genreIds",
listOf(
Pair("Action", "1"),
Pair("Adventure", "13"),
Pair("Comedy", "7"),
Pair("Drama", "2"),
Pair("elf", "25"),
Pair("Fantas", "28"),
Pair("Fantasy", "8"),
Pair("Historical", "19"),
Pair("Horror", "9"),
Pair("Josei", "21"),
Pair("Manhwa", "5"),
Pair("Martial Arts", "6"),
Pair("Mature", "12"),
Pair("Monsters", "14"),
Pair("Reincarnation", "16"),
Pair("Revenge", "17"),
Pair("Romance", "20"),
Pair("School Life", "23"),
Pair("Seinen", "10"),
Pair("shojo", "26"),
Pair("Shoujo", "22"),
Pair("Shounen", "3"),
Pair("Slice Of Life", "18"),
Pair("Sports", "4"),
Pair("Supernatural", "11"),
Pair("System", "15"),
Pair("terror", "24"),
Pair("Video Games", "27"),
),
)

View File

@ -0,0 +1,145 @@
package eu.kanade.tachiyomi.extension.en.arvenscans
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 eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class VortexScans : HttpSource() {
override val name = "Vortex Scans"
override val baseUrl = "https://vortexscans.com"
override val lang = "en"
override val supportsLatest = true
override val client = network.cloudflareClient
private val json by injectLazy<Json>()
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
private val titleCache by lazy {
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
val data = response.parseAs<SearchResponse>()
data.posts
.filterNot { it.isNovel }
.associateBy { it.slug }
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
.map { it.absUrl("href").substringAfterLast("/series/") }
val entries = slugs.mapNotNull {
titleCache[it]?.toSManga(baseUrl)
}
return MangasPage(entries, false)
}
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
addQueryParameter("page", page.toString())
addQueryParameter("perPage", perPage.toString())
addQueryParameter("searchTerm", query.trim())
filters.filterIsInstance<UrlPartFilter>().forEach {
it.addUrlParameter(this)
}
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val data = response.parseAs<SearchResponse>()
val page = response.request.url.queryParameter("page")!!.toInt()
val entries = data.posts
.filterNot { it.isNovel }
.map { it.toSManga(baseUrl) }
val hasNextPage = data.totalCount > (page * perPage)
return MangasPage(entries, hasNextPage)
}
override fun getFilterList() = FilterList(
StatusFilter(),
TypeFilter(),
GenreFilter(),
)
override fun mangaDetailsRequest(manga: SManga): Request {
val id = manga.url.substringAfterLast("#")
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
return GET(url, headers)
}
override fun getMangaUrl(manga: SManga): String {
val slug = manga.url.substringBeforeLast("#")
return "$baseUrl/series/$slug"
}
override fun mangaDetailsParse(response: Response): SManga {
val data = response.parseAs<Post<Manga>>()
assert(!data.post.isNovel) { "Novels are unsupported" }
// genres are only returned in search call
// and not when fetching details
return data.post.toSManga(baseUrl).apply {
genre = titleCache[data.post.slug]?.getGenres()
}
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val data = response.parseAs<Post<ChapterListResponse>>()
assert(!data.post.isNovel) { "Novels are unsupported" }
return data.post.chapters
.filter { it.isPublic() }
.map { it.toSChapter(data.post.slug) }
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("main > section > img").mapIndexed { idx, img ->
Page(idx, imageUrl = img.absUrl("src"))
}
}
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T =
json.decodeFromString(body.string())
}
private const val perPage = 18