Split VyvyManga and VyvyManga.org extensions (#5704)
* Revert "VyvyManga: Migrate theme (#5573)" This reverts commit bba2693814d306417366fc03dcc8b21bf2e1d740. * VyvyManga: bump version * VyvyManga.org: move to other extension
This commit is contained in:
parent
da0bb1b8eb
commit
17fc1acd6c
|
@ -1,9 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'VyvyManga'
|
extName = 'VyvyManga'
|
||||||
extClass = '.VyvyManga'
|
extClass = '.VyvyManga'
|
||||||
themePkg = 'madara'
|
extVersionCode = 37
|
||||||
baseUrl = 'https://vyvymanga.org'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,176 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.vyvymanga
|
package eu.kanade.tachiyomi.extension.en.vyvymanga
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
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.ParsedHttpSource
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class VyvyManga : Madara(
|
class VyvyManga : ParsedHttpSource() {
|
||||||
name = "VyvyManga",
|
override val name = "VyvyManga"
|
||||||
baseUrl = "https://vyvymanga.org",
|
|
||||||
lang = "en",
|
|
||||||
) {
|
|
||||||
override val versionId = 2
|
|
||||||
|
|
||||||
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
override val baseUrl = "https://vymanga.net"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val dateFormat = SimpleDateFormat("MMM dd, yyy", Locale.US)
|
||||||
|
|
||||||
|
// Popular
|
||||||
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/search" + if (page != 1) "?page=$page" else "", headers)
|
||||||
|
|
||||||
|
override fun popularMangaSelector(): String =
|
||||||
|
searchMangaSelector()
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga =
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector(): String =
|
||||||
|
searchMangaNextPageSelector()
|
||||||
|
|
||||||
|
// Search
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = "$baseUrl/search".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("q", query)
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is SearchType -> url.addQueryParameter("search_po", filter.selected)
|
||||||
|
is SearchDescription -> if (filter.state) url.addQueryParameter("check_search_desc", "1")
|
||||||
|
is AuthorSearchType -> url.addQueryParameter("author_po", filter.selected)
|
||||||
|
is AuthorFilter -> url.addQueryParameter("author", filter.state)
|
||||||
|
is StatusFilter -> url.addQueryParameter("completed", filter.selected)
|
||||||
|
is SortFilter -> url.addQueryParameter("sort", filter.selected)
|
||||||
|
is SortType -> url.addQueryParameter("sort_type", filter.selected)
|
||||||
|
is GenreFilter -> {
|
||||||
|
filter.state.forEach {
|
||||||
|
if (!it.isIgnored()) url.addQueryParameter(if (it.isIncluded()) "genre[]" else "exclude_genre[]", it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GET(url.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector(): String = ".comic-item"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||||
|
title = element.selectFirst(".comic-title")!!.text()
|
||||||
|
thumbnail_url = element.selectFirst(".comic-image")!!.absUrl("data-background-image")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector(): String = "[rel=next]"
|
||||||
|
|
||||||
|
// Latest
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/search?sort=updated_at" + if (page != 1) "&page=$page" else "", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String =
|
||||||
|
searchMangaSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() =
|
||||||
|
searchMangaNextPageSelector()
|
||||||
|
|
||||||
|
// Details
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||||
|
title = document.selectFirst("h1")!!.text()
|
||||||
|
artist = document.selectFirst(".pre-title:contains(Artist) ~ a")?.text()
|
||||||
|
author = document.selectFirst(".pre-title:contains(Author) ~ a")?.text()
|
||||||
|
description = document.selectFirst(".summary > .content")!!.text()
|
||||||
|
genre = document.select(".pre-title:contains(Genres) ~ a").joinToString { it.text() }
|
||||||
|
status = when (document.selectFirst(".pre-title:contains(Status) ~ span:not(.space)")?.text()) {
|
||||||
|
"Ongoing" -> SManga.ONGOING
|
||||||
|
"Completed" -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
thumbnail_url = document.selectFirst(".img-manga")!!.absUrl("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapters
|
||||||
|
override fun chapterListSelector(): String =
|
||||||
|
".list-group > a"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
|
url = element.absUrl("href")
|
||||||
|
name = element.selectFirst("span")!!.text()
|
||||||
|
date_upload = parseChapterDate(element.selectFirst("> p")?.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request =
|
||||||
|
GET(chapter.url, headers)
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select("img.d-block").mapIndexed { index, element ->
|
||||||
|
Page(index, "", element.absUrl("data-src"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other
|
||||||
|
// Date logic lifted from Madara
|
||||||
|
private fun parseChapterDate(date: String?): Long {
|
||||||
|
date ?: return 0
|
||||||
|
|
||||||
|
fun SimpleDateFormat.tryParse(string: String): Long {
|
||||||
|
return try {
|
||||||
|
parse(string)?.time ?: 0
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return when {
|
||||||
|
"ago".endsWith(date) -> {
|
||||||
|
parseRelativeDate(date)
|
||||||
|
}
|
||||||
|
else -> dateFormat.tryParse(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRelativeDate(date: String): Long {
|
||||||
|
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
date.contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||||
|
date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||||
|
date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||||
|
date.contains("second") -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
launchIO { fetchGenres(baseUrl, headers, client) }
|
||||||
|
return FilterList(
|
||||||
|
SearchType(),
|
||||||
|
SearchDescription(),
|
||||||
|
AuthorSearchType(),
|
||||||
|
AuthorFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
SortFilter(),
|
||||||
|
SortType(),
|
||||||
|
GenreFilter(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.vyvymanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
|
||||||
|
abstract class SelectFilter(displayName: String, private val options: Array<Pair<String, String>>) :
|
||||||
|
Filter.Select<String>(
|
||||||
|
displayName,
|
||||||
|
options.map { it.first }.toTypedArray(),
|
||||||
|
) {
|
||||||
|
open val selected get() = options[state].second.takeUnless { it.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchType : SelectFilter(
|
||||||
|
"Title should contain/begin/end with typed text",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Contain", "0"),
|
||||||
|
Pair("Begin", "1"),
|
||||||
|
Pair("End", "2"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SearchDescription : Filter.CheckBox("Search In Description")
|
||||||
|
|
||||||
|
class AuthorSearchType : SelectFilter(
|
||||||
|
"Author should contain/begin/end with typed text",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Contain", "0"),
|
||||||
|
Pair("Begin", "1"),
|
||||||
|
Pair("End", "2"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class AuthorFilter : Filter.Text("Author")
|
||||||
|
|
||||||
|
class StatusFilter : SelectFilter(
|
||||||
|
"Status",
|
||||||
|
arrayOf(
|
||||||
|
Pair("All", "2"),
|
||||||
|
Pair("Ongoing", "0"),
|
||||||
|
Pair("Completed", "1"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortFilter : SelectFilter(
|
||||||
|
"Sort by",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Viewed", "viewed"),
|
||||||
|
Pair("Scored", "scored"),
|
||||||
|
Pair("Newest", "created_at"),
|
||||||
|
Pair("Latest Update", "updated_at"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortType : SelectFilter(
|
||||||
|
"Sort order",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Descending", "desc"),
|
||||||
|
Pair("Ascending", "asc"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||||
|
|
||||||
|
class GenreFilter : Filter.Group<Genre>("Genre", genrePairs.map { Genre(it.name, it.id) })
|
||||||
|
|
||||||
|
private var genrePairs: List<Genre> = emptyList()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
fun launchIO(block: () -> Unit) = scope.launch { block() }
|
||||||
|
|
||||||
|
private var fetchGenresAttempts: Int = 0
|
||||||
|
|
||||||
|
fun fetchGenres(baseUrl: String, headers: okhttp3.Headers, client: okhttp3.OkHttpClient) {
|
||||||
|
if (fetchGenresAttempts < 3 && genrePairs.isEmpty()) {
|
||||||
|
try {
|
||||||
|
genrePairs =
|
||||||
|
client.newCall(genresRequest(baseUrl, headers)).execute()
|
||||||
|
.asJsoup()
|
||||||
|
.let(::parseGenres)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
} finally {
|
||||||
|
fetchGenresAttempts++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun genresRequest(baseUrl: String, headers: okhttp3.Headers) = GET("$baseUrl/search", headers)
|
||||||
|
|
||||||
|
private const val genresSelector = ".check-genre div div:has(.checkbox-genre)"
|
||||||
|
|
||||||
|
private fun parseGenres(document: Document): List<Genre> {
|
||||||
|
val items = document.select(genresSelector)
|
||||||
|
return buildList(items.size) {
|
||||||
|
items.mapTo(this) {
|
||||||
|
Genre(
|
||||||
|
it.select("label").text(),
|
||||||
|
it.select(".checkbox-genre").attr("data-value"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
ext {
|
||||||
|
extName = 'VyvyManga.org'
|
||||||
|
extClass = '.VyvyMangaOrg'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://vyvymanga.org'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.vyvymangaorg
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class VyvyMangaOrg : Madara(
|
||||||
|
name = "VyvyManga.org",
|
||||||
|
baseUrl = "https://vyvymanga.org",
|
||||||
|
lang = "en",
|
||||||
|
) {
|
||||||
|
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||||
|
}
|
Loading…
Reference in New Issue