diff --git a/src/en/vyvymanga/build.gradle b/src/en/vyvymanga/build.gradle index 706bef44e..eccf2d6d6 100644 --- a/src/en/vyvymanga/build.gradle +++ b/src/en/vyvymanga/build.gradle @@ -1,9 +1,7 @@ ext { extName = 'VyvyManga' extClass = '.VyvyManga' - themePkg = 'madara' - baseUrl = 'https://vyvymanga.org' - overrideVersionCode = 0 + extVersionCode = 37 isNsfw = true } diff --git a/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyManga.kt b/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyManga.kt index 49a597873..6841c0e61 100644 --- a/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyManga.kt +++ b/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyManga.kt @@ -1,13 +1,176 @@ 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( - name = "VyvyManga", - baseUrl = "https://vyvymanga.org", - lang = "en", -) { - override val versionId = 2 +class VyvyManga : ParsedHttpSource() { + override val name = "VyvyManga" - 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 { + 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(), + ) + } } diff --git a/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyMangaFilters.kt b/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyMangaFilters.kt new file mode 100644 index 000000000..4785fc530 --- /dev/null +++ b/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyMangaFilters.kt @@ -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>) : + Filter.Select( + 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", genrePairs.map { Genre(it.name, it.id) }) + +private var genrePairs: List = 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 { + val items = document.select(genresSelector) + return buildList(items.size) { + items.mapTo(this) { + Genre( + it.select("label").text(), + it.select(".checkbox-genre").attr("data-value"), + ) + } + } +} diff --git a/src/en/vyvymangaorg/build.gradle b/src/en/vyvymangaorg/build.gradle new file mode 100644 index 000000000..f54620411 --- /dev/null +++ b/src/en/vyvymangaorg/build.gradle @@ -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" diff --git a/src/en/vyvymangaorg/res/mipmap-hdpi/ic_launcher.png b/src/en/vyvymangaorg/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..5db1f69a0 Binary files /dev/null and b/src/en/vyvymangaorg/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/vyvymangaorg/res/mipmap-mdpi/ic_launcher.png b/src/en/vyvymangaorg/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..5bc11d78a Binary files /dev/null and b/src/en/vyvymangaorg/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/vyvymangaorg/res/mipmap-xhdpi/ic_launcher.png b/src/en/vyvymangaorg/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..247ffb80d Binary files /dev/null and b/src/en/vyvymangaorg/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/vyvymangaorg/res/mipmap-xxhdpi/ic_launcher.png b/src/en/vyvymangaorg/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..24498e972 Binary files /dev/null and b/src/en/vyvymangaorg/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/vyvymangaorg/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/vyvymangaorg/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..e4b3d44e6 Binary files /dev/null and b/src/en/vyvymangaorg/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/vyvymangaorg/src/eu/kanade/tachiyomi/extension/en/vyvymangaorg/VyvyMangaOrg.kt b/src/en/vyvymangaorg/src/eu/kanade/tachiyomi/extension/en/vyvymangaorg/VyvyMangaOrg.kt new file mode 100644 index 000000000..b6ccfd6d9 --- /dev/null +++ b/src/en/vyvymangaorg/src/eu/kanade/tachiyomi/extension/en/vyvymangaorg/VyvyMangaOrg.kt @@ -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 +}