Iken Multisrc, move Vortex Scan and MangaGalaxy (#4043)
|
@ -0,0 +1,5 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-multisrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseVersionCode = 1
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.arvenscans
|
package eu.kanade.tachiyomi.multisrc.iken
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
@ -18,7 +18,7 @@ class SearchResponse(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Manga(
|
class Manga(
|
||||||
val id: Int,
|
private val id: Int,
|
||||||
val slug: String,
|
val slug: String,
|
||||||
private val postTitle: String,
|
private val postTitle: String,
|
||||||
private val postContent: String? = null,
|
private val postContent: String? = null,
|
||||||
|
@ -29,7 +29,7 @@ class Manga(
|
||||||
private val artist: String? = null,
|
private val artist: String? = null,
|
||||||
private val seriesType: String? = null,
|
private val seriesType: String? = null,
|
||||||
private val seriesStatus: String? = null,
|
private val seriesStatus: String? = null,
|
||||||
private val genres: List<Name>? = emptyList(),
|
val genres: List<Genre> = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun toSManga(baseUrl: String) = SManga.create().apply {
|
fun toSManga(baseUrl: String) = SManga.create().apply {
|
||||||
url = "$slug#$id"
|
url = "$slug#$id"
|
||||||
|
@ -70,10 +70,16 @@ class Manga(
|
||||||
"MANHWA" -> add("Manhwa")
|
"MANHWA" -> add("Manhwa")
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
genres?.forEach { add(it.name) }
|
genres.forEach { add(it.name) }
|
||||||
}.distinct().joinToString()
|
}.distinct().joinToString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Genre(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Name(val name: String)
|
class Name(val name: String)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.arvenscans
|
package eu.kanade.tachiyomi.multisrc.iken
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
@ -65,37 +65,8 @@ class TypeFilter : SelectFilter(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class GenreFilter : CheckBoxGroup(
|
class GenreFilter(genres: List<Pair<String, String>>) : CheckBoxGroup(
|
||||||
"Genres",
|
"Genres",
|
||||||
"genreIds",
|
"genreIds",
|
||||||
listOf(
|
genres,
|
||||||
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"),
|
|
||||||
),
|
|
||||||
)
|
)
|
|
@ -0,0 +1,153 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.iken
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
abstract class Iken(
|
||||||
|
override val name: String,
|
||||||
|
override val lang: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
) : HttpSource() {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
private val json by injectLazy<Json>()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
private var genres = emptyList<Pair<String, String>>()
|
||||||
|
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 }
|
||||||
|
.also { posts ->
|
||||||
|
genres = posts.flatMap {
|
||||||
|
it.genres.map { genre ->
|
||||||
|
genre.name to genre.id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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(genres),
|
||||||
|
Filter.Header("Open popular mangas if genre filter is empty"),
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
|
@ -1,7 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Vortex Scans'
|
extName = 'Vortex Scans'
|
||||||
extClass = '.VortexScans'
|
extClass = '.VortexScans'
|
||||||
extVersionCode = 33
|
themePkg = 'iken'
|
||||||
|
overrideVersionCode = 33
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,145 +1,9 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.arvenscans
|
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.multisrc.iken.Iken
|
||||||
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() {
|
class VortexScans : Iken(
|
||||||
|
"Vortex Scans",
|
||||||
override val name = "Vortex Scans"
|
"en",
|
||||||
|
"https://vortexscans.org",
|
||||||
override val baseUrl = "https://vortexscans.org"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Manga Galaxy'
|
extName = 'Manga Galaxy'
|
||||||
extClass = '.MangaGalaxy'
|
extClass = '.MangaGalaxy'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'iken'
|
||||||
baseUrl = 'https://mangagalaxy.me'
|
baseUrl = 'https://mangagalaxy.org'
|
||||||
overrideVersionCode = 9
|
overrideVersionCode = 39
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
|
@ -1,13 +1,12 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangagalaxy
|
package eu.kanade.tachiyomi.extension.en.mangagalaxy
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.iken.Iken
|
||||||
|
|
||||||
class MangaGalaxy : MangaThemesia(
|
class MangaGalaxy : Iken(
|
||||||
"Manga Galaxy",
|
"Manga Galaxy",
|
||||||
"https://mangagalaxy.me",
|
|
||||||
"en",
|
"en",
|
||||||
mangaUrlDirectory = "/series",
|
"https://mangagalaxy.org",
|
||||||
) {
|
) {
|
||||||
// moved from Madara to MangaThemesia
|
// moved from Madara to MangaThemesia to Iken
|
||||||
override val versionId = 2
|
override val versionId = 3
|
||||||
}
|
}
|
||||||
|
|