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 {
 | 
			
		||||
    extName = 'VyvyManga'
 | 
			
		||||
    extClass = '.VyvyManga'
 | 
			
		||||
    themePkg = 'madara'
 | 
			
		||||
    baseUrl = 'https://vyvymanga.org'
 | 
			
		||||
    overrideVersionCode = 0
 | 
			
		||||
    extVersionCode = 37
 | 
			
		||||
    isNsfw = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<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"),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/en/vyvymangaorg/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/en/vyvymangaorg/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -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"
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 5.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 9.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/vyvymangaorg/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user