diff --git a/lib-multisrc/madtheme/build.gradle.kts b/lib-multisrc/madtheme/build.gradle.kts index f5a077b4b..bbea4c992 100644 --- a/lib-multisrc/madtheme/build.gradle.kts +++ b/lib-multisrc/madtheme/build.gradle.kts @@ -2,4 +2,4 @@ plugins { id("lib-multisrc") } -baseVersionCode = 13 +baseVersionCode = 14 diff --git a/lib-multisrc/madtheme/src/eu/kanade/tachiyomi/multisrc/madtheme/MadTheme.kt b/lib-multisrc/madtheme/src/eu/kanade/tachiyomi/multisrc/madtheme/MadTheme.kt index fdb65c3ac..db482dea0 100644 --- a/lib-multisrc/madtheme/src/eu/kanade/tachiyomi/multisrc/madtheme/MadTheme.kt +++ b/lib-multisrc/madtheme/src/eu/kanade/tachiyomi/multisrc/madtheme/MadTheme.kt @@ -21,6 +21,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.jsoup.nodes.Element import rx.Observable @@ -29,24 +30,25 @@ import java.text.ParseException import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale +import java.util.concurrent.TimeUnit abstract class MadTheme( override val name: String, override val baseUrl: String, override val lang: String, - private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM dd, yyy", Locale.US), + private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH), ) : ParsedHttpSource() { override val supportsLatest = true override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .rateLimit(1, 1) + .rateLimit(1, 1, TimeUnit.SECONDS) .build() // TODO: better cookie sharing // TODO: don't count cached responses against rate limit private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder() - .rateLimit(1, 12) + .rateLimit(1, 12, TimeUnit.SECONDS) .build() override fun headersBuilder() = Headers.Builder().apply { @@ -55,6 +57,8 @@ abstract class MadTheme( private val json: Json by injectLazy() + private var genreKey = "genre[]" + // Popular override fun popularMangaRequest(page: Int): Request = searchMangaRequest(page, "", FilterList(OrderFilter(0))) @@ -100,7 +104,7 @@ abstract class MadTheme( .filter { it.state } .let { list -> if (list.isNotEmpty()) { - list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) } + list.forEach { genre -> url.addQueryParameter(genreKey, genre.id) } } } } @@ -120,11 +124,11 @@ abstract class MadTheme( override fun searchMangaSelector(): String = ".book-detailed-item" override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { - setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href")) - title = element.select("a").first()!!.attr("title") - description = element.select(".summary").first()?.text() - genre = element.select(".genres > *").joinToString { it.text() } - thumbnail_url = element.select("img").first()!!.attr("abs:data-src") + setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href")) + title = element.selectFirst("a")!!.attr("title") + element.selectFirst(".summary")?.text()?.let { description = it } + element.select(".genres > *").joinToString { it.text() }.takeIf { it.isNotEmpty() }?.let { genre = it } + thumbnail_url = element.selectFirst("img")!!.attr("abs:data-src") } /* @@ -135,23 +139,25 @@ abstract class MadTheme( // Details override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { - title = document.select(".detail h1").first()!!.text() + title = document.selectFirst(".detail h1")!!.text() author = document.select(".detail .meta > p > strong:contains(Authors) ~ a").joinToString { it.text().trim(',', ' ') } genre = document.select(".detail .meta > p > strong:contains(Genres) ~ a").joinToString { it.text().trim(',', ' ') } - thumbnail_url = document.select("#cover img").first()!!.attr("abs:data-src") + thumbnail_url = document.selectFirst("#cover img")!!.attr("abs:data-src") - val altNames = document.select(".detail h2").first()?.text() + val altNames = document.selectFirst(".detail h2")?.text() ?.split(',', ';') ?.mapNotNull { it.trim().takeIf { it != title } } ?: listOf() - description = document.select(".summary .content").first()?.text() + + description = document.select(".summary .content, .summary .content ~ p").text() + (altNames.takeIf { it.isNotEmpty() }?.let { "\n\nAlt name(s): ${it.joinToString()}" } ?: "") - val statusText = document.select(".detail .meta > p > strong:contains(Status) ~ a").first()!!.text() - status = when (statusText.lowercase(Locale.US)) { + val statusText = document.selectFirst(".detail .meta > p > strong:contains(Status) ~ a")!!.text() + status = when (statusText.lowercase(Locale.ENGLISH)) { "ongoing" -> SManga.ONGOING "completed" -> SManga.COMPLETED + "on-hold" -> SManga.ON_HIATUS + "canceled" -> SManga.CANCELLED else -> SManga.UNKNOWN } } @@ -187,7 +193,14 @@ abstract class MadTheme( } override fun chapterListRequest(manga: SManga): Request = - GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers) + MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)?.let { mangaId -> + val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder() + .addQueryParameter("manga_id", mangaId) + .addQueryParameter("manga_name", manga.title) + .build() + + GET(url, headers) + } ?: GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers) override fun searchMangaParse(response: Response): MangasPage { if (genresList == null) { @@ -204,16 +217,25 @@ abstract class MadTheme( .absUrl("href") .removePrefix(baseUrl) - name = element.select(".chapter-title").first()!!.text() - date_upload = parseChapterDate(element.select(".chapter-update").first()?.text()) + name = element.selectFirst(".chapter-title")!!.text() + date_upload = parseChapterDate(element.selectFirst(".chapter-update")?.text()) } // Pages override fun pageListParse(document: Document): List { - val html = document.html() + val mangaId = MANGA_ID_REGEX.find(document.location())?.groupValues?.get(1) + val chapterId = CHAPTER_ID_REGEX.find(document.html())?.groupValues?.get(1) + + val html = if (mangaId != null && chapterId != null) { + val url = GET("$baseUrl/service/backend/chapterServer/?server_id=1&chapter_id=$chapterId", headers) + client.newCall(url).execute().body.string() + } else { + document.html() + } + val realDocument = Jsoup.parse(html, document.location()) if (!html.contains("var mainServer = \"")) { - val chapterImagesFromHtml = document.select("#chapter-images img") + val chapterImagesFromHtml = realDocument.select("#chapter-images img, .chapter-image[data-src]") // 17/03/2023: Certain hosts only embed two pages in their "#chapter-images" and leave // the rest to be lazily(?) loaded by javascript. Let's extract `chapImages` and compare @@ -292,7 +314,7 @@ abstract class MadTheme( } return when { - "ago".endsWith(date) -> { + " ago" in date -> { parseRelativeDate(date) } else -> dateFormat.tryParse(date) @@ -300,10 +322,12 @@ abstract class MadTheme( } private fun parseRelativeDate(date: String): Long { - val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 + val number = NUMBER_REGEX.find(date)?.groupValues?.getOrNull(0)?.toIntOrNull() ?: return 0 val cal = Calendar.getInstance() return when { + date.contains("year") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis + date.contains("month") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis 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 @@ -314,13 +338,21 @@ abstract class MadTheme( // Dynamic genres private fun parseGenres(document: Document): List? { - return document.select(".checkbox-group.genres").first()?.select("label")?.map { - Genre(it.select(".radio__label").first()!!.text(), it.select("input").`val`()) + return document.selectFirst(".checkbox-group.genres")?.select(".checkbox-wrapper")?.run { + firstOrNull()?.selectFirst("input")?.attr("name")?.takeIf { it.isNotEmpty() }?.let { genreKey = it } + map { + Genre(it.selectFirst(".radio__label")!!.text(), it.selectFirst("input")!!.`val`()) + } } } // Filters override fun getFilterList() = FilterList( + // TODO: Filters for sites that support it: + // excluded genres + // genre inclusion mode + // bookmarks + // author GenreFilter(getGenreList()), StatusFilter(), OrderFilter(), @@ -352,6 +384,7 @@ abstract class MadTheme( Pair("Updated", "updated_at"), Pair("Created", "created_at"), Pair("Name A-Z", "name"), + // Pair("Number of Chapters", "total_chapters"), Pair("Rating", "rating"), ), state, @@ -365,4 +398,10 @@ abstract class MadTheme( Filter.Select(displayName, vals.map { it.first }.toTypedArray(), state) { fun toUriPart() = vals[state].second } + + companion object { + private val MANGA_ID_REGEX = """/manga/(\d+)-""".toRegex() + private val CHAPTER_ID_REGEX = """chapterId\s*=\s*(\d+)""".toRegex() + private val NUMBER_REGEX = """\d+""".toRegex() + } } diff --git a/src/en/kaliscancom/build.gradle b/src/en/kaliscancom/build.gradle new file mode 100644 index 000000000..ac2613ec8 --- /dev/null +++ b/src/en/kaliscancom/build.gradle @@ -0,0 +1,10 @@ +ext { + extName = 'KaliScan.com' + extClass = '.KaliScanCom' + themePkg = 'madtheme' + baseUrl = 'https://kaliscan.com' + overrideVersionCode = 0 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/kaliscancom/res/mipmap-hdpi/ic_launcher.png b/src/en/kaliscancom/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..0ad9fa655 Binary files /dev/null and b/src/en/kaliscancom/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/kaliscancom/res/mipmap-mdpi/ic_launcher.png b/src/en/kaliscancom/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e078344d1 Binary files /dev/null and b/src/en/kaliscancom/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/kaliscancom/res/mipmap-xhdpi/ic_launcher.png b/src/en/kaliscancom/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..0b1eaec17 Binary files /dev/null and b/src/en/kaliscancom/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/kaliscancom/res/mipmap-xxhdpi/ic_launcher.png b/src/en/kaliscancom/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..7d619b1bd Binary files /dev/null and b/src/en/kaliscancom/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/kaliscancom/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/kaliscancom/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..b914d3a5c Binary files /dev/null and b/src/en/kaliscancom/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/kaliscancom/src/eu/kanade/tachiyomi/extension/en/kaliscancom/KaliScanCom.kt b/src/en/kaliscancom/src/eu/kanade/tachiyomi/extension/en/kaliscancom/KaliScanCom.kt new file mode 100644 index 000000000..72f1ace97 --- /dev/null +++ b/src/en/kaliscancom/src/eu/kanade/tachiyomi/extension/en/kaliscancom/KaliScanCom.kt @@ -0,0 +1,5 @@ +package eu.kanade.tachiyomi.extension.en.kaliscancom + +import eu.kanade.tachiyomi.multisrc.madtheme.MadTheme + +class KaliScanCom : MadTheme("KaliScan.com", "https://kaliscan.com", "en") diff --git a/src/en/kaliscanio/build.gradle b/src/en/kaliscanio/build.gradle new file mode 100644 index 000000000..583519345 --- /dev/null +++ b/src/en/kaliscanio/build.gradle @@ -0,0 +1,10 @@ +ext { + extName = 'KaliScan.io' + extClass = '.KaliScanIo' + themePkg = 'madtheme' + baseUrl = 'https://kaliscan.io' + overrideVersionCode = 0 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/kaliscanio/res/mipmap-hdpi/ic_launcher.png b/src/en/kaliscanio/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..0ad9fa655 Binary files /dev/null and b/src/en/kaliscanio/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/kaliscanio/res/mipmap-mdpi/ic_launcher.png b/src/en/kaliscanio/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e078344d1 Binary files /dev/null and b/src/en/kaliscanio/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/kaliscanio/res/mipmap-xhdpi/ic_launcher.png b/src/en/kaliscanio/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..0b1eaec17 Binary files /dev/null and b/src/en/kaliscanio/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/kaliscanio/res/mipmap-xxhdpi/ic_launcher.png b/src/en/kaliscanio/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..7d619b1bd Binary files /dev/null and b/src/en/kaliscanio/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/kaliscanio/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/kaliscanio/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..b914d3a5c Binary files /dev/null and b/src/en/kaliscanio/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/kaliscanio/src/eu/kanade/tachiyomi/extension/en/kaliscanio/KaliScanIo.kt b/src/en/kaliscanio/src/eu/kanade/tachiyomi/extension/en/kaliscanio/KaliScanIo.kt new file mode 100644 index 000000000..703b71458 --- /dev/null +++ b/src/en/kaliscanio/src/eu/kanade/tachiyomi/extension/en/kaliscanio/KaliScanIo.kt @@ -0,0 +1,5 @@ +package eu.kanade.tachiyomi.extension.en.kaliscanio + +import eu.kanade.tachiyomi.multisrc.madtheme.MadTheme + +class KaliScanIo : MadTheme("KaliScan.io", "https://kaliscan.io", "en") diff --git a/src/en/kaliscanme/build.gradle b/src/en/kaliscanme/build.gradle new file mode 100644 index 000000000..04deab7d4 --- /dev/null +++ b/src/en/kaliscanme/build.gradle @@ -0,0 +1,10 @@ +ext { + extName = 'KaliScan.me' + extClass = '.KaliScanMe' + themePkg = 'madtheme' + baseUrl = 'https://kaliscan.me' + overrideVersionCode = 0 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/kaliscanme/res/mipmap-hdpi/ic_launcher.png b/src/en/kaliscanme/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..0ad9fa655 Binary files /dev/null and b/src/en/kaliscanme/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/kaliscanme/res/mipmap-mdpi/ic_launcher.png b/src/en/kaliscanme/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e078344d1 Binary files /dev/null and b/src/en/kaliscanme/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/kaliscanme/res/mipmap-xhdpi/ic_launcher.png b/src/en/kaliscanme/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..0b1eaec17 Binary files /dev/null and b/src/en/kaliscanme/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/kaliscanme/res/mipmap-xxhdpi/ic_launcher.png b/src/en/kaliscanme/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..7d619b1bd Binary files /dev/null and b/src/en/kaliscanme/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/kaliscanme/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/kaliscanme/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..b914d3a5c Binary files /dev/null and b/src/en/kaliscanme/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/kaliscanme/src/eu/kanade/tachiyomi/extension/en/kaliscanme/KaliScanMe.kt b/src/en/kaliscanme/src/eu/kanade/tachiyomi/extension/en/kaliscanme/KaliScanMe.kt new file mode 100644 index 000000000..71d9de211 --- /dev/null +++ b/src/en/kaliscanme/src/eu/kanade/tachiyomi/extension/en/kaliscanme/KaliScanMe.kt @@ -0,0 +1,5 @@ +package eu.kanade.tachiyomi.extension.en.kaliscanme + +import eu.kanade.tachiyomi.multisrc.madtheme.MadTheme + +class KaliScanMe : MadTheme("KaliScan.me", "https://kaliscan.me", "en") diff --git a/src/en/manhuascan/build.gradle b/src/en/manhuascan/build.gradle deleted file mode 100644 index 4252653a7..000000000 --- a/src/en/manhuascan/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -ext { - extName = 'ManhuaScan' - extClass = '.ManhuaScan' - extVersionCode = 8 - isNsfw = true -} - -apply from: "$rootDir/common.gradle" diff --git a/src/en/manhuascan/res/mipmap-hdpi/ic_launcher.png b/src/en/manhuascan/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 2076e770f..000000000 Binary files a/src/en/manhuascan/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/manhuascan/res/mipmap-mdpi/ic_launcher.png b/src/en/manhuascan/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 62461a4cb..000000000 Binary files a/src/en/manhuascan/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/manhuascan/res/mipmap-xhdpi/ic_launcher.png b/src/en/manhuascan/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index d1865d60a..000000000 Binary files a/src/en/manhuascan/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/manhuascan/res/mipmap-xxhdpi/ic_launcher.png b/src/en/manhuascan/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index c408915ff..000000000 Binary files a/src/en/manhuascan/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/manhuascan/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/manhuascan/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 1e08f1789..000000000 Binary files a/src/en/manhuascan/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/manhuascan/src/eu/kanade/tachiyomi/extension/en/manhuascan/ManhuaScan.kt b/src/en/manhuascan/src/eu/kanade/tachiyomi/extension/en/manhuascan/ManhuaScan.kt deleted file mode 100644 index be5504ec0..000000000 --- a/src/en/manhuascan/src/eu/kanade/tachiyomi/extension/en/manhuascan/ManhuaScan.kt +++ /dev/null @@ -1,333 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.manhuascan - -import android.app.Application -import android.content.SharedPreferences -import androidx.preference.ListPreference -import androidx.preference.PreferenceScreen -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.interceptor.rateLimit -import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.model.Filter -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 eu.kanade.tachiyomi.util.asJsoup -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.Calendar - -class ManhuaScan : ConfigurableSource, ParsedHttpSource() { - - override val lang = "en" - - override val supportsLatest = true - - override val name = "ManhuaScan" - - private val preferences: SharedPreferences = - Injekt.get().getSharedPreferences("source_$id", 0x0000) - - override val baseUrl = getMirror() - - override val client by lazy { - network.cloudflareClient.newBuilder() - .rateLimit(2) - .build() - } - - override fun headersBuilder() = super.headersBuilder() - .add("Referer", "$baseUrl/") - - // ============================== Popular =============================== - - override fun popularMangaRequest(page: Int): Request = - GET("$baseUrl/popular${page.getPage()}", headers) - - override fun popularMangaSelector(): String = ".manga-list > .book-item" - - override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { - thumbnail_url = element.selectFirst(".thumb img")?.imgAttr() - element.selectFirst(".title a")!!.run { - title = text() - setUrlWithoutDomain(attr("abs:href")) - } - } - - override fun popularMangaNextPageSelector(): String = ".paginator > .active + a" - - // =============================== Latest =============================== - - override fun latestUpdatesRequest(page: Int): Request = - GET("$baseUrl/latest${page.getPage()}", headers) - - override fun latestUpdatesSelector(): String = - popularMangaSelector() - - override fun latestUpdatesFromElement(element: Element): SManga = - popularMangaFromElement(element) - - override fun latestUpdatesNextPageSelector(): String = - popularMangaNextPageSelector() - - // =============================== Search =============================== - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val genre = filters.firstInstanceOrNull() - val genreInclusion = filters.firstInstanceOrNull() - val status = filters.firstInstanceOrNull() - val orderBy = filters.firstInstanceOrNull() - val author = filters.firstInstanceOrNull() - - val url = "$baseUrl/search${page.getPage()}".toHttpUrl().newBuilder().apply { - genre?.included?.forEach { - addEncodedQueryParameter("include[]", it) - } - genre?.excluded?.forEach { - addEncodedQueryParameter("exclude[]", it) - } - addQueryParameter("include_mode", genreInclusion?.toUriPart()) - addQueryParameter("bookmark", "off") - addQueryParameter("status", status?.toUriPart()) - addQueryParameter("sort", orderBy?.toUriPart()) - if (query.isNotEmpty()) { - addQueryParameter("q", query) - } - if (author?.state?.isNotEmpty() == true) { - addQueryParameter("author", author.state) - } - } - - return GET(url.build(), headers) - } - - override fun searchMangaSelector(): String = popularMangaSelector() - - override fun searchMangaFromElement(element: Element): SManga = - popularMangaFromElement(element) - - override fun searchMangaNextPageSelector(): String = - popularMangaNextPageSelector() - - // =============================== Filters ============================== - - override fun getFilterList(): FilterList = FilterList( - GenreFilter(), - GenreInclusionFilter(), - Filter.Separator(), - StatusFilter(), - OrderByFilter(), - AuthorFilter(), - ) - - // =========================== Manga Details ============================ - - override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { - var alternativeName = "" - - document.selectFirst(".book-info")?.run { - genre = select(".meta p:has(strong:contains(Genres)) a").joinToString(", ") { it.text().removeSuffix(" ,") } - author = select(".meta p:has(strong:contains(Authors)) a").joinToString(", ") { it.text() } - thumbnail_url = selectFirst("#cover img")?.imgAttr() - status = selectFirst(".meta p:has(strong:contains(Status)) a").parseStatus() - title = selectFirst("h1")!!.text() - selectFirst("h2")?.also { - alternativeName = it.text() - } - } - - description = buildString { - document.selectFirst(".summary > p:not([style]):not(:empty)")?.let { - append(it.text()) - if (alternativeName.isNotEmpty()) { - append("\n\n") - } - } - if (alternativeName.isNotEmpty()) { - append("Alternative name(s): $alternativeName") - } - } - } - - private fun Element?.parseStatus(): Int = with(this?.text()) { - return when { - equals("ongoing", true) -> SManga.ONGOING - equals("completed", true) -> SManga.COMPLETED - equals("on-hold", true) -> SManga.ON_HIATUS - equals("canceled", true) -> SManga.CANCELLED - else -> SManga.UNKNOWN - } - } - - // ============================== Chapters ============================== - - override fun chapterListRequest(manga: SManga): Request { - val id = manga.url.substringAfter("manga/").substringBefore("-") - - val chapterHeaders = headersBuilder().apply { - add("Accept", "*/*") - add("Host", baseUrl.toHttpUrl().host) - set("Referer", baseUrl + manga.url) - }.build() - - val url = "$baseUrl/service/backend/chaplist/?manga_id=$id&manga_name=${manga.title}" - - return GET(url, chapterHeaders) - } - - override fun chapterListSelector() = "ul > li" - - override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { - element.selectFirst("time")?.also { - date_upload = it.text().parseRelativeDate() - } - name = element.selectFirst("strong")!!.text() - setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href")) - } - - // From OppaiStream - private fun String.parseRelativeDate(): Long { - val now = Calendar.getInstance().apply { - set(Calendar.HOUR_OF_DAY, 0) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - } - - var parsedDate = 0L - val relativeDate = this.split(" ").firstOrNull() - ?.replace("one", "1") - ?.replace("a", "1") - ?.toIntOrNull() - ?: return 0L - - when { - // parse: 30 seconds ago - "second" in this -> { - parsedDate = now.apply { add(Calendar.SECOND, -relativeDate) }.timeInMillis - } - // parses: "42 minutes ago" - "minute" in this -> { - parsedDate = now.apply { add(Calendar.MINUTE, -relativeDate) }.timeInMillis - } - // parses: "1 hour ago" and "2 hours ago" - "hour" in this -> { - parsedDate = now.apply { add(Calendar.HOUR, -relativeDate) }.timeInMillis - } - // parses: "2 days ago" - "day" in this -> { - parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -relativeDate) }.timeInMillis - } - // parses: "2 weeks ago" - "week" in this -> { - parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -relativeDate) }.timeInMillis - } - // parses: "2 months ago" - "month" in this -> { - parsedDate = now.apply { add(Calendar.MONTH, -relativeDate) }.timeInMillis - } - // parse: "2 years ago" - "year" in this -> { - parsedDate = now.apply { add(Calendar.YEAR, -relativeDate) }.timeInMillis - } - } - return parsedDate - } - - // =============================== Pages ================================ - - override fun pageListParse(document: Document): List { - val scriptData = document.selectFirst("script:containsData(chapterId)")?.data() - ?: throw Exception("Unable to find script data") - val chapterId = CHAPTER_ID_REGEX.find(scriptData)?.groupValues?.get(1) - ?: throw Exception("Unable to retrieve chapterId") - - val pagesHeaders = headersBuilder().apply { - add("Accept", "*/*") - add("Host", baseUrl.toHttpUrl().host) - set("Referer", document.location()) - }.build() - val pagesUrl = "$baseUrl/service/backend/chapterServer/?server_id=$server&chapter_id=$chapterId" - - val pagesDocument = client.newCall( - GET(pagesUrl, pagesHeaders), - ).execute().asJsoup() - - return pagesDocument.select("div").map { page -> - val url = page.imgAttr() - val index = page.id().substringAfterLast("-").toInt() - Page(index, document.location(), url) - }.sortedBy { it.index } - } - - override fun imageUrlParse(document: Document) = "" - - override fun imageRequest(page: Page): Request { - val imgHeaders = headersBuilder().apply { - add("Accept", "*/*") - add("Host", page.imageUrl!!.toHttpUrl().host) - set("Referer", page.url) - }.build() - - return GET(page.imageUrl!!, imgHeaders) - } - - // ============================= Utilities ============================== - - private fun Int.getPage(): String = if (this == 1) "" else "?page=$this" - - private fun Element.imgAttr(): String = when { - hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") - hasAttr("data-src") -> attr("abs:data-src") - else -> attr("abs:src") - } - - companion object { - private val CHAPTER_ID_REGEX = Regex("""chapterId\s*=\s*(\d+)""") - - private const val MIRROR_PREF_KEY = "pref_mirror" - private const val MIRROR_PREF_TITLE = "Select Mirror (Requires Restart)" - private val MIRROR_PREF_ENTRIES = arrayOf("manhuascan.com", "manhuascan.io", "mangajinx.com") - private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray() - private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES.first() - - private const val SERVER_PREF_KEY = "pref_server" - private const val SERVER_PREF_TITLE = "Image Server" - private val SERVER_PREF_ENTRIES = arrayOf("Server 1", "Server 2") - private val SERVER_PREF_ENTRY_VALUES = SERVER_PREF_ENTRIES.map { it.substringAfter(" ") }.toTypedArray() - private val SERVER_PREF_DEFAULT_VALUE = SERVER_PREF_ENTRY_VALUES.first() - } - - // ============================== Settings ============================== - - override fun setupPreferenceScreen(screen: PreferenceScreen) { - ListPreference(screen.context).apply { - key = MIRROR_PREF_KEY - title = MIRROR_PREF_TITLE - entries = MIRROR_PREF_ENTRIES - entryValues = MIRROR_PREF_ENTRY_VALUES - setDefaultValue(MIRROR_PREF_DEFAULT_VALUE) - summary = "%s" - }.also(screen::addPreference) - - ListPreference(screen.context).apply { - key = SERVER_PREF_KEY - title = SERVER_PREF_TITLE - entries = SERVER_PREF_ENTRIES - entryValues = SERVER_PREF_ENTRY_VALUES - setDefaultValue(SERVER_PREF_DEFAULT_VALUE) - summary = "%s" - }.also(screen::addPreference) - } - - private fun getMirror(): String = - preferences.getString(MIRROR_PREF_KEY, MIRROR_PREF_DEFAULT_VALUE)!! - - private val server - get() = preferences.getString(SERVER_PREF_KEY, SERVER_PREF_DEFAULT_VALUE)!! -} diff --git a/src/en/manhuascan/src/eu/kanade/tachiyomi/extension/en/manhuascan/ManhuaScanFilters.kt b/src/en/manhuascan/src/eu/kanade/tachiyomi/extension/en/manhuascan/ManhuaScanFilters.kt deleted file mode 100644 index 84a1b4f1a..000000000 --- a/src/en/manhuascan/src/eu/kanade/tachiyomi/extension/en/manhuascan/ManhuaScanFilters.kt +++ /dev/null @@ -1,129 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.manhuascan - -import eu.kanade.tachiyomi.source.model.Filter - -inline fun List<*>.firstInstanceOrNull() = firstOrNull { it is T } as? T - -open class UriPartFilter(displayName: String, val vals: Array>) : - Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { - fun toUriPart() = vals[state].second -} - -class Genre(val id: String, name: String) : Filter.TriState(name) - -class GenreFilter : Filter.Group( - "Genres", - listOf( - Genre("action", "Action"), - Genre("adaptation", "Adaptation"), - Genre("adult", "Adult"), - Genre("adventure", "Adventure"), - Genre("animal", "Animal"), - Genre("anthology", "Anthology"), - Genre("cartoon", "Cartoon"), - Genre("comedy", "Comedy"), - Genre("comic", "Comic"), - Genre("cooking", "Cooking"), - Genre("demons", "Demons"), - Genre("doujinshi", "Doujinshi"), - Genre("drama", "Drama"), - Genre("ecchi", "Ecchi"), - Genre("fantasy", "Fantasy"), - Genre("full-color", "Full Color"), - Genre("game", "Game"), - Genre("gender-bender", "Gender bender"), - Genre("ghosts", "Ghosts"), - Genre("harem", "Harem"), - Genre("historical", "Historical"), - Genre("horror", "Horror"), - Genre("isekai", "Isekai"), - Genre("josei", "Josei"), - Genre("long-strip", "Long strip"), - Genre("mafia", "Mafia"), - Genre("magic", "Magic"), - Genre("manga", "Manga"), - Genre("manhua", "Manhua"), - Genre("manhwa", "Manhwa"), - Genre("martial-arts", "Martial arts"), - Genre("mature", "Mature"), - Genre("mecha", "Mecha"), - Genre("medical", "Medical"), - Genre("military", "Military"), - Genre("monster", "Monster"), - Genre("monster-girls", "Monster girls"), - Genre("monsters", "Monsters"), - Genre("music", "Music"), - Genre("mystery", "Mystery"), - Genre("office", "Office"), - Genre("office-workers", "Office workers"), - Genre("one-shot", "One shot"), - Genre("police", "Police"), - Genre("psychological", "Psychological"), - Genre("reincarnation", "Reincarnation"), - Genre("romance", "Romance"), - Genre("school-life", "School life"), - Genre("sci-fi", "Sci fi"), - Genre("science-fiction", "Science fiction"), - Genre("seinen", "Seinen"), - Genre("shoujo", "Shoujo"), - Genre("shoujo-ai", "Shoujo ai"), - Genre("shounen", "Shounen"), - Genre("shounen-ai", "Shounen ai"), - Genre("slice-of-life", "Slice of life"), - Genre("smut", "Smut"), - Genre("soft-yaoi", "Soft Yaoi"), - Genre("sports", "Sports"), - Genre("super-power", "Super Power"), - Genre("superhero", "Superhero"), - Genre("supernatural", "Supernatural"), - Genre("thriller", "Thriller"), - Genre("time-travel", "Time travel"), - Genre("tragedy", "Tragedy"), - Genre("vampire", "Vampire"), - Genre("vampires", "Vampires"), - Genre("video-games", "Video games"), - Genre("villainess", "Villainess"), - Genre("web-comic", "Web comic"), - Genre("webtoons", "Webtoons"), - Genre("yaoi", "Yaoi"), - Genre("yuri", "Yuri"), - Genre("zombies", "Zombies"), - ), -) { - val included: List? - get() = state.filter { it.isIncluded() }.map { it.id }.takeUnless { it.isEmpty() } - - val excluded: List? - get() = state.filter { it.isExcluded() }.map { it.id }.takeUnless { it.isEmpty() } -} - -class GenreInclusionFilter : UriPartFilter( - "Genre Inclusion Mode", - arrayOf( - Pair("AND (All Selected Genres)", "and"), - Pair("OR (Any Selected Genres)", "or"), - ), -) - -class StatusFilter : UriPartFilter( - "Status", - arrayOf( - Pair("All", "all"), - Pair("Ongoing", "ongoing"), - Pair("Completed", "completed"), - ), -) - -class OrderByFilter : UriPartFilter( - "Order By", - arrayOf( - Pair("Views", "views"), - Pair("Updated", "updated_at"), - Pair("Created", "created_at"), - Pair("Name A-Z", "name"), - Pair("Number of Chapters", "total_chapters"), - Pair("Rating", "rating"), - ), -) - -class AuthorFilter : Filter.Text("Author name") diff --git a/src/en/mgjinx/build.gradle b/src/en/mgjinx/build.gradle new file mode 100644 index 000000000..c0a41bbdb --- /dev/null +++ b/src/en/mgjinx/build.gradle @@ -0,0 +1,10 @@ +ext { + extName = 'MGJinx' + extClass = '.MGJinx' + themePkg = 'madtheme' + baseUrl = 'https://mgjinx.com' + overrideVersionCode = 0 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/mgjinx/res/mipmap-hdpi/ic_launcher.png b/src/en/mgjinx/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..0ad9fa655 Binary files /dev/null and b/src/en/mgjinx/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/mgjinx/res/mipmap-mdpi/ic_launcher.png b/src/en/mgjinx/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e078344d1 Binary files /dev/null and b/src/en/mgjinx/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/mgjinx/res/mipmap-xhdpi/ic_launcher.png b/src/en/mgjinx/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..0b1eaec17 Binary files /dev/null and b/src/en/mgjinx/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/mgjinx/res/mipmap-xxhdpi/ic_launcher.png b/src/en/mgjinx/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..7d619b1bd Binary files /dev/null and b/src/en/mgjinx/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/mgjinx/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/mgjinx/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..b914d3a5c Binary files /dev/null and b/src/en/mgjinx/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/mgjinx/src/eu/kanade/tachiyomi/extension/en/mgjinx/MGJinx.kt b/src/en/mgjinx/src/eu/kanade/tachiyomi/extension/en/mgjinx/MGJinx.kt new file mode 100644 index 000000000..1783cab07 --- /dev/null +++ b/src/en/mgjinx/src/eu/kanade/tachiyomi/extension/en/mgjinx/MGJinx.kt @@ -0,0 +1,5 @@ +package eu.kanade.tachiyomi.extension.en.mgjinx + +import eu.kanade.tachiyomi.multisrc.madtheme.MadTheme + +class MGJinx : MadTheme("MGJinx", "https://mgjinx.com", "en")