diff --git a/lib-multisrc/fuzzydoodle/build.gradle.kts b/lib-multisrc/fuzzydoodle/build.gradle.kts new file mode 100644 index 000000000..dc076cc37 --- /dev/null +++ b/lib-multisrc/fuzzydoodle/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("lib-multisrc") +} + +baseVersionCode = 1 diff --git a/lib-multisrc/fuzzydoodle/src/eu/kanade/tachiyomi/multisrc/fuzzydoodle/Filters.kt b/lib-multisrc/fuzzydoodle/src/eu/kanade/tachiyomi/multisrc/fuzzydoodle/Filters.kt new file mode 100644 index 000000000..3b3ec3cfc --- /dev/null +++ b/lib-multisrc/fuzzydoodle/src/eu/kanade/tachiyomi/multisrc/fuzzydoodle/Filters.kt @@ -0,0 +1,62 @@ +package eu.kanade.tachiyomi.multisrc.fuzzydoodle + +import eu.kanade.tachiyomi.source.model.Filter +import okhttp3.HttpUrl + +interface UrlPartFilter { + fun addUrlParameter(url: HttpUrl.Builder) +} + +abstract class SelectFilter( + name: String, + private val options: List>, + private val urlParameter: String, +) : UrlPartFilter, Filter.Select( + name, + options.map { it.first }.toTypedArray(), +) { + override fun addUrlParameter(url: HttpUrl.Builder) { + url.addQueryParameter(urlParameter, options[state].second) + } +} + +class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name) + +abstract class CheckBoxGroup( + name: String, + options: List>, + private val urlParameter: String, +) : UrlPartFilter, Filter.Group( + name, + options.map { CheckBoxFilter(it.first, it.second) }, +) { + override fun addUrlParameter(url: HttpUrl.Builder) { + state.filter { it.state }.forEach { + url.addQueryParameter(urlParameter, it.value) + } + } +} + +class TypeFilter( + options: List>, +) : SelectFilter( + "Type", + options, + "type", +) + +class StatusFilter( + options: List>, +) : SelectFilter( + "Status", + options, + "status", +) + +class GenreFilter( + options: List>, +) : CheckBoxGroup( + "Genres", + options, + "genre[]", +) diff --git a/lib-multisrc/fuzzydoodle/src/eu/kanade/tachiyomi/multisrc/fuzzydoodle/FuzzyDoodle.kt b/lib-multisrc/fuzzydoodle/src/eu/kanade/tachiyomi/multisrc/fuzzydoodle/FuzzyDoodle.kt new file mode 100644 index 000000000..57883f155 --- /dev/null +++ b/lib-multisrc/fuzzydoodle/src/eu/kanade/tachiyomi/multisrc/fuzzydoodle/FuzzyDoodle.kt @@ -0,0 +1,317 @@ +package eu.kanade.tachiyomi.multisrc.fuzzydoodle + +import android.util.Log +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.await +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.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.lang.Exception +import java.util.Calendar + +/* + * https://github.com/jhin1m/fuzzy-doodle + */ +abstract class FuzzyDoodle( + override val name: String, + override val baseUrl: String, + override val lang: String, +) : ParsedHttpSource() { + + override val supportsLatest = true + + override val client = network.cloudflareClient + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + // Popular + override fun popularMangaRequest(page: Int) = + GET("$baseUrl/manga?page=$page", headers) + + override fun popularMangaSelector() = "div#card-real" + override fun popularMangaNextPageSelector() = "ul.pagination > li:last-child:not(.pagination-disabled)" + + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + + launchIO { fetchFilters(document) } + + val entries = document.select(popularMangaSelector()) + .map(::popularMangaFromElement) + val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null + + return MangasPage(entries, hasNextPage) + } + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) + title = element.selectFirst("h2.text-sm")!!.text() + thumbnail_url = element.selectFirst("img")?.imgAttr() + } + + // latest + protected open val latestFromHomePage = false + + override fun latestUpdatesRequest(page: Int) = + if (latestFromHomePage) { + latestHomePageRequest(page) + } else { + latestPageRequest(page) + } + + protected open fun latestHomePageRequest(page: Int) = + GET("$baseUrl/?page=$page", headers) + + protected open fun latestPageRequest(page: Int) = + GET("$baseUrl/latest?page=$page", headers) + + override fun latestUpdatesSelector() = + if (latestFromHomePage) { + "section:has(h2:containsOwn(Recent Chapters)) div#card-real," + + " section:has(h2:containsOwn(Chapitres récents)) div#card-real" + } else { + popularMangaSelector() + } + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) + + override fun latestUpdatesParse(response: Response): MangasPage { + launchIO { fetchFilters() } + + return super.latestUpdatesParse(response) + } + + // search + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/manga".toHttpUrl().newBuilder().apply { + addQueryParameter("title", query.trim()) + filters.filterIsInstance().forEach { + it.addUrlParameter(this) + } + if (page > 1) { + addQueryParameter("page", page.toString()) + } + }.build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + override fun searchMangaSelector() = popularMangaSelector() + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + // filters + protected var typeList = listOf>() + protected var statusList = listOf>() + protected var genreList = listOf>() + private var fetchFilterAttempts = 0 + + protected suspend fun fetchFilters(document: Document? = null) { + if (fetchFilterAttempts < 3 && (typeList.isEmpty() || statusList.isEmpty() || genreList.isEmpty())) { + try { + val doc = document ?: client.newCall(filtersRequest()) + .await() + .asJsoup() + + parseFilters(doc) + } catch (e: Exception) { + Log.e("$name: Filters", e.stackTraceToString()) + } + fetchFilterAttempts++ + } + } + + protected open fun filtersRequest() = GET("$baseUrl/manga", headers) + + protected open fun parseFilters(document: Document) { + typeList = document.select("select[name=type] > option").map { + it.ownText() to it.attr("value") + } + statusList = document.select("select[name=status] > option").map { + it.ownText() to it.attr("value") + } + genreList = document.select("div.grid > div.flex:has(> input[name=genre[]])").mapNotNull { + val label = it.selectFirst("label")?.ownText() + ?: return@mapNotNull null + val value = it.selectFirst("input")?.attr("value") + ?: return@mapNotNull null + + label to value + } + } + + override fun getFilterList(): FilterList { + val filters = mutableListOf>() + + if (typeList.isNotEmpty()) { + filters.add(TypeFilter(typeList)) + } + if (statusList.isNotEmpty()) { + filters.add(StatusFilter(statusList)) + } + if (genreList.isNotEmpty()) { + filters.add(GenreFilter(genreList)) + } + if (filters.size < 3) { + filters.add(0, Filter.Header("Press 'reset' to load more filters")) + } + + return FilterList(filters) + } + + private val scope = CoroutineScope(Dispatchers.IO) + + protected fun launchIO(block: suspend () -> Unit) = scope.launch { block() } + + // details + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + val genres = mutableListOf() + with(document.selectFirst("main > section > div")!!) { + thumbnail_url = selectFirst("div.relative img")?.imgAttr() + title = selectFirst("div.flex > h1, div.flex > h2")!!.ownText() + genres.addAll(select("div.flex > a.inline-block").eachText()) + description = buildString { + selectFirst("div:has(> p#description)")?.let { + it.selectFirst("span.font-semibold")?.remove() + it.select("#show-more").remove() + append(it.text()) + } + selectFirst("div.flex > h1 + div > span.text-sm, div.flex > h2 + div > span.text-sm")?.text()?.let { + if (it.isNotEmpty()) { + append("\n\n") + append("Alternative Title: ") + append(it.trim()) + } + } + }.trim() + } + document.selectFirst("div#buttons + div.hidden, div:has(> div#buttons) + div.flex")?.run { + status = (getInfo("Status") ?: getInfo("Statut")).parseStatus() + artist = (getInfo("Artist") ?: getInfo("المؤلف") ?: getInfo("Artiste")).removePlaceHolder() + author = (getInfo("Author") ?: getInfo("الرسام") ?: getInfo("Auteur")).removePlaceHolder() + (getInfo("Type") ?: getInfo("النوع"))?.also { genres.add(0, it) } + } + genre = genres.joinToString() + } + + protected open fun String?.parseStatus(): Int { + this ?: return SManga.UNKNOWN + + return when { + listOf("ongoing", "مستمر", "en cours").any { contains(it, true) } -> SManga.ONGOING + listOf("dropped", "cancelled", "متوقف").any { contains(it, true) } -> SManga.CANCELLED + listOf("completed", "مكتمل", "terminé").any { contains(it, true) } -> SManga.COMPLETED + listOf("hiatus").any { contains(it, true) } -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } + } + + protected fun Element.getInfo(text: String): String? = + selectFirst("p:has(span:containsOwn($text)) span.capitalize") + ?.ownText() + ?.trim() + + protected fun String?.removePlaceHolder(): String? = + takeUnless { it == "-" } + + // chapters + override fun chapterListParse(response: Response): List { + val originalUrl = response.request.url.toString() + + val chapterList = buildList { + var page = 1 + do { + val doc = when { + isEmpty() -> response // First page + else -> { + page++ + client.newCall(GET("$originalUrl?page=$page", headers)).execute() + } + }.asJsoup() + + addAll(doc.select(chapterListSelector()).map(::chapterFromElement)) + } while (doc.selectFirst(chapterListNextPageSelector()) != null) + } + + return chapterList + } + + override fun chapterListSelector() = "div#chapters-list > a[href]" + protected fun chapterListNextPageSelector() = latestUpdatesNextPageSelector() + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + setUrlWithoutDomain(element.attr("href")) + name = element.selectFirst("#item-title, span")!!.ownText() + date_upload = element.selectFirst("span.text-gray-500")?.text().parseRelativeDate() + } + + // from madara + protected open fun String?.parseRelativeDate(): Long { + this ?: return 0L + + val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: return 0L + val cal = Calendar.getInstance() + + return when { + listOf("detik", "segundo", "second", "วินาที").any { contains(it, true) } -> { + cal.apply { add(Calendar.SECOND, -number) }.timeInMillis + } + listOf("menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق").any { contains(it, true) } -> { + cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis + } + listOf("jam", "saat", "heure", "hora", "hour", "ชั่วโมง", "giờ", "ore", "ساعة", "小时").any { contains(it, true) } -> { + cal.apply { add(Calendar.HOUR, -number) }.timeInMillis + } + listOf("hari", "gün", "jour", "día", "dia", "day", "วัน", "ngày", "giorni", "أيام", "天").any { contains(it, true) } -> { + cal.apply { add(Calendar.DAY_OF_YEAR, -number) }.timeInMillis + } + listOf("week", "sema").any { contains(it, true) } -> { + cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis + } + listOf("month", "mes").any { it in this } -> { + cal.apply { add(Calendar.MONTH, -number) }.timeInMillis + } + listOf("year", "año").any { it in this } -> { + cal.apply { add(Calendar.YEAR, -number) }.timeInMillis + } + else -> 0L + } + } + + // pages + override fun pageListParse(document: Document): List { + return document.select("div#chapter-container > img").mapIndexed { idx, img -> + Page(idx, imageUrl = img.imgAttr()) + } + } + + private fun Element.imgAttr(): String { + return when { + hasAttr("srcset") -> attr("srcset").substringBefore(" ") + hasAttr("data-cfsrc") -> absUrl("data-cfsrc") + hasAttr("data-src") -> absUrl("data-src") + hasAttr("data-lazy-src") -> absUrl("data-lazy-src") + else -> absUrl("src") + } + } + + override fun imageUrlParse(document: Document): String { + throw UnsupportedOperationException() + } +} diff --git a/src/ar/hentaislayer/build.gradle b/src/ar/hentaislayer/build.gradle index d7314b400..44980fecf 100644 --- a/src/ar/hentaislayer/build.gradle +++ b/src/ar/hentaislayer/build.gradle @@ -1,7 +1,8 @@ ext { extName = 'Hentai Slayer' extClass = '.HentaiSlayer' - extVersionCode = 2 + themePkg = 'fuzzydoodle' + overrideVersionCode = 2 isNsfw = true } diff --git a/src/ar/hentaislayer/src/eu/kanade/tachiyomi/extension/ar/hentaislayer/HentaiSlayer.kt b/src/ar/hentaislayer/src/eu/kanade/tachiyomi/extension/ar/hentaislayer/HentaiSlayer.kt index 1a7dafbc4..67ed88b60 100644 --- a/src/ar/hentaislayer/src/eu/kanade/tachiyomi/extension/ar/hentaislayer/HentaiSlayer.kt +++ b/src/ar/hentaislayer/src/eu/kanade/tachiyomi/extension/ar/hentaislayer/HentaiSlayer.kt @@ -4,199 +4,28 @@ import android.app.Application import android.widget.Toast import androidx.preference.ListPreference import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle 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.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 uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.Calendar -class HentaiSlayer : ParsedHttpSource(), ConfigurableSource { +class HentaiSlayer : FuzzyDoodle("هنتاي سلاير", "https://hentaislayer.net", "ar"), ConfigurableSource { - override val name = "هنتاي سلاير" - - override val baseUrl = "https://hentaislayer.net" - - override val lang = "ar" - - override val supportsLatest = true - - override val client = network.cloudflareClient.newBuilder() + override val client = super.client.newBuilder() .rateLimit(2) .build() override fun headersBuilder() = super.headersBuilder() - .set("Referer", "$baseUrl/") .set("Origin", baseUrl) private val preferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } - // ============================== Popular =============================== - override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga?page=$page", headers) + override fun latestPageRequest(page: Int) = GET("$baseUrl/latest-${getLatestTypes()}?page=$page", headers) - override fun popularMangaSelector() = "div > div:has(div#card-real)" - - override fun popularMangaFromElement(element: Element) = SManga.create().apply { - with(element.selectFirst("div#card-real a")!!) { - setUrlWithoutDomain(absUrl("href")) - with(selectFirst("figure")!!) { - with(selectFirst("img.object-cover")!!) { - thumbnail_url = imgAttr() - title = attr("alt") - } - genre = select("span p.drop-shadow-sm").text() - } - } - } - - override fun popularMangaNextPageSelector() = "ul.pagination > li:last-child:not(.pagination-disabled)" - - // =============================== Latest =============================== - override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/latest-${getLatestTypes()}?page=$page", headers) - - override fun latestUpdatesSelector() = popularMangaSelector() - - override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) - - override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() - - // =============================== Search =============================== - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = "$baseUrl/manga?title=$query".toHttpUrl().newBuilder() - filters.forEach { filter -> - when (filter) { - is TypeFilter -> url.addQueryParameter("type", filter.toUriPart()) - is StatusFilter -> url.addQueryParameter("status", filter.toUriPart()) - is GenresFilter -> - filter.state - .filter { it.state } - .forEach { url.addQueryParameter("genre[]", it.uriPart) } - else -> {} - } - } - - url.addQueryParameter("page", page.toString()) - return GET(url.build(), headers) - } - - override fun searchMangaSelector() = popularMangaSelector() - - override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) - - override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() - - // =========================== Manga Details ============================ - override fun mangaDetailsParse(document: Document) = SManga.create().apply { - with(document.selectFirst("main section")!!) { - thumbnail_url = selectFirst("img#manga-cover")!!.imgAttr() - with(selectFirst("section > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2)")!!) { - status = parseStatus(select("a[href*='?status=']").text()) - genre = select("a[href*='?type=']").text() - author = select("p:has(span:contains(المؤلف)) span:nth-child(2)").text() - artist = select("p:has(span:contains(الرسام)) span:nth-child(2)").text() - } - var desc = "\u061C" - with(selectFirst("section > div:nth-child(1) > div:nth-child(2)")!!) { - title = selectFirst("h1")!!.text() - genre = select("a[href*='?genre=']") - .map { it.text() } - .let { - listOf(genre) + it - } - .joinToString() - select("h2").text().takeIf { it.isNotEmpty() }?.let { - desc += "أسماء أُخرى: $it\n" - } - } - description = desc + select("#description").text() - } - } - - private fun parseStatus(status: String) = when { - status.contains("مستمر") -> SManga.ONGOING - status.contains("متوقف") -> SManga.CANCELLED - status.contains("مكتمل") -> SManga.COMPLETED - else -> SManga.UNKNOWN - } - - // ============================== Chapters ============================== - override fun chapterListSelector() = "main section #chapters-list a#chapter-item" - - override fun chapterFromElement(element: Element) = SChapter.create().apply { - setUrlWithoutDomain(element.attr("href")) - name = "\u061C" + element.select("#item-title").text() // Add unicode ARABIC LETTER MARK to ensure all titles are right to left - - date_upload = parseRelativeDate(element.select("#item-title + span").text()) ?: 0L - } - - /** - * Parses dates in this form: - * `11 days ago` - */ - private fun parseRelativeDate(date: String): Long? { - val trimmedDate = date.split(" ") - - if (trimmedDate[2] != "ago") return null - - val number = trimmedDate[0].toIntOrNull() ?: return null - val unit = trimmedDate[1].removeSuffix("s") // Remove 's' suffix - - val now = Calendar.getInstance() - - // Map English unit to Java unit - val javaUnit = when (unit) { - "year", "yr" -> Calendar.YEAR - "month" -> Calendar.MONTH - "week", "wk" -> Calendar.WEEK_OF_MONTH - "day" -> Calendar.DAY_OF_MONTH - "hour", "hr" -> Calendar.HOUR - "minute", "min" -> Calendar.MINUTE - "second", "sec" -> Calendar.SECOND - else -> return null - } - - now.add(javaUnit, -number) - - return now.timeInMillis - } - - // =============================== Pages ================================ - override fun pageListParse(document: Document): List { - return document.select("img.chapter-image").mapIndexed { index, item -> - Page(index = index, imageUrl = item.imgAttr()) - } - } - - override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() - - private fun Element.imgAttr(): String? { - return when { - hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ") - hasAttr("data-cfsrc") -> attr("abs:data-cfsrc") - hasAttr("data-src") -> attr("abs:data-src") - hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") - else -> attr("abs:src") - } - } - - override fun getFilterList() = FilterList( - GenresFilter(), - TypeFilter(), - StatusFilter(), - ) - - // ============================== Settings ============================== companion object { private const val LATEST_PREF = "LatestType" private val LATEST_PREF_ENTRIES get() = arrayOf( diff --git a/src/ar/hentaislayer/src/eu/kanade/tachiyomi/extension/ar/hentaislayer/HentaiSlayerFilters.kt b/src/ar/hentaislayer/src/eu/kanade/tachiyomi/extension/ar/hentaislayer/HentaiSlayerFilters.kt deleted file mode 100644 index 4025d028b..000000000 --- a/src/ar/hentaislayer/src/eu/kanade/tachiyomi/extension/ar/hentaislayer/HentaiSlayerFilters.kt +++ /dev/null @@ -1,81 +0,0 @@ -package eu.kanade.tachiyomi.extension.ar.hentaislayer - -import eu.kanade.tachiyomi.source.model.Filter - -class StatusFilter : UriPartFilter( - "الحالة", - arrayOf( - Pair("الكل", ""), - Pair("مستمر", "مستمر"), - Pair("متوقف", "متوقف"), - Pair("مكتمل", "مكتمل"), - ), -) - -class TypeFilter : UriPartFilter( - "النوع", - arrayOf( - Pair("الكل", ""), - Pair("مانجا", "مانجا"), - Pair("مانهوا", "مانهوا"), - Pair("كوميكس", "كوميكس"), - ), -) - -private val genres = listOf( - Genre("أكشن", "أكشن"), - Genre("ألعاب جنسية", "ألعاب جنسية"), - Genre("إذلال", "إذلال"), - Genre("إيلف", "إيلف"), - Genre("ابتزاز", "ابتزاز"), - Genre("استعباد", "استعباد"), - Genre("اغتصاب", "اغتصاب"), - Genre("بدون حجب", "بدون حجب"), - Genre("بشرة سمراء", "بشرة سمراء"), - Genre("تاريخي", "تاريخي"), - Genre("تحكم بالعقل", "تحكم بالعقل"), - Genre("تراب", "تراب"), - Genre("تسوندري", "تسوندري"), - Genre("تصوير", "تصوير"), - Genre("جنس بالقدم", "جنس بالقدم"), - Genre("جنس جماعي", "جنس جماعي"), - Genre("جنس شرجي", "جنس شرجي"), - Genre("حريم", "حريم"), - Genre("حمل", "حمل"), - Genre("خادمة", "خادمة"), - Genre("خيال", "خيال"), - Genre("خيانة", "خيانة"), - Genre("دراغون بول", "دراغون بول"), - Genre("دراما", "دراما"), - Genre("رومانسي", "رومانسي"), - Genre("سحر", "سحر"), - Genre("شوتا", "شوتا"), - Genre("شيطانة", "شيطانة"), - Genre("شيميل", "شيميل"), - Genre("طالبة مدرسة", "طالبة مدرسة"), - Genre("عمة", "عمة"), - Genre("فوتا", "فوتا"), - Genre("لولي", "لولي"), - Genre("محارم", "محارم"), - Genre("مدرسي", "مدرسي"), - Genre("مكان عام", "مكان عام"), - Genre("ملون", "ملون"), - Genre("ميلف", "ميلف"), - Genre("ناروتو", "ناروتو"), - Genre("هجوم العمالقة", "هجوم العمالقة"), - Genre("ون بيس", "ون بيس"), - Genre("ياوي", "ياوي"), - Genre("يوري", "يوري"), -) - -class Genre(val name: String, val uriPart: String) - -class GenreCheckBox(name: String, val uriPart: String) : Filter.CheckBox(name) - -class GenresFilter : - Filter.Group("التصنيفات", genres.map { GenreCheckBox(it.name, it.uriPart) }) - -open class UriPartFilter(displayName: String, private val pairs: Array>) : - Filter.Select(displayName, pairs.map { it.first }.toTypedArray()) { - fun toUriPart() = pairs[state].second -} diff --git a/src/en/cloudrecess/AndroidManifest.xml b/src/en/cloudrecess/AndroidManifest.xml deleted file mode 100644 index a4e695313..000000000 --- a/src/en/cloudrecess/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/en/cloudrecess/build.gradle b/src/en/cloudrecess/build.gradle deleted file mode 100644 index 2e032ca49..000000000 --- a/src/en/cloudrecess/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -ext { - extName = 'CloudRecess' - extClass = '.CloudRecess' - extVersionCode = 2 - isNsfw = true -} - -apply from: "$rootDir/common.gradle" diff --git a/src/en/cloudrecess/res/mipmap-hdpi/ic_launcher.png b/src/en/cloudrecess/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 7c811df20..000000000 Binary files a/src/en/cloudrecess/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/cloudrecess/res/mipmap-mdpi/ic_launcher.png b/src/en/cloudrecess/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index a0a18641d..000000000 Binary files a/src/en/cloudrecess/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/cloudrecess/res/mipmap-xhdpi/ic_launcher.png b/src/en/cloudrecess/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index d969e3b55..000000000 Binary files a/src/en/cloudrecess/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/cloudrecess/res/mipmap-xxhdpi/ic_launcher.png b/src/en/cloudrecess/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index ef0fe43f3..000000000 Binary files a/src/en/cloudrecess/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/cloudrecess/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/cloudrecess/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 32fa3ef6c..000000000 Binary files a/src/en/cloudrecess/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/cloudrecess/src/eu/kanade/tachiyomi/extension/en/cloudrecess/CloudRecess.kt b/src/en/cloudrecess/src/eu/kanade/tachiyomi/extension/en/cloudrecess/CloudRecess.kt deleted file mode 100644 index 7a3a3d558..000000000 --- a/src/en/cloudrecess/src/eu/kanade/tachiyomi/extension/en/cloudrecess/CloudRecess.kt +++ /dev/null @@ -1,175 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.cloudrecess - -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.network.interceptor.rateLimitHost -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.ParsedHttpSource -import eu.kanade.tachiyomi.util.asJsoup -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request -import okhttp3.Response -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import rx.Observable - -class CloudRecess : ParsedHttpSource() { - - override val name = "CloudRecess" - - override val baseUrl = "https://cloudrecess.io" - - override val lang = "en" - - override val supportsLatest = true - - override val client by lazy { - network.cloudflareClient.newBuilder() - .rateLimitHost(baseUrl.toHttpUrl(), 2) - .build() - } - - // To load images - override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/") - - // ============================== Popular =============================== - override fun popularMangaRequest(page: Int) = GET(baseUrl, headers) - - override fun popularMangaSelector() = "swiper-container#popular-cards div#card-real > a" - - override fun popularMangaFromElement(element: Element) = SManga.create().apply { - setUrlWithoutDomain(element.attr("href")) - title = element.selectFirst("h2.text-sm")?.text() ?: "Manga" - thumbnail_url = element.selectFirst("img")?.run { - absUrl("data-src").ifEmpty { absUrl("src") } - } - } - - override fun popularMangaNextPageSelector() = null - - // =============================== Latest =============================== - override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers) - - override fun latestUpdatesSelector() = "section:has(h2:containsOwn(Recent Chapters)) div#card-real > a" - - override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) - - override fun latestUpdatesNextPageSelector() = "ul.pagination > li:last-child:not(.pagination-disabled)" - - // =============================== Search =============================== - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler - val id = query.removePrefix(PREFIX_SEARCH) - client.newCall(GET("$baseUrl/manga/$id")) - .asObservableSuccess() - .map(::searchMangaByIdParse) - } else { - super.fetchSearchManga(page, query, filters) - } - } - - private fun searchMangaByIdParse(response: Response): MangasPage { - val details = mangaDetailsParse(response.use { it.asJsoup() }) - return MangasPage(listOf(details), false) - } - - override fun getFilterList() = CloudRecessFilters.FILTER_LIST - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = "$baseUrl/manga?title=$query&page=$page".toHttpUrl().newBuilder().apply { - val params = CloudRecessFilters.getSearchParameters(filters) - if (params.type.isNotEmpty()) addQueryParameter("type", params.type) - if (params.status.isNotEmpty()) addQueryParameter("status", params.status) - params.genres.forEach { addQueryParameter("genre[]", it) } - }.build() - return GET(url, headers) - } - - override fun searchMangaSelector() = "main div#card-real > a" - - override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) - - override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() - - // =========================== Manga Details ============================ - override fun mangaDetailsParse(document: Document) = SManga.create().apply { - // Absolutely required element, so throwing a NPE when it's not present - // seems reasonable. - with(document.selectFirst("main > section > div")!!) { - thumbnail_url = selectFirst("div.relative img")?.absUrl("src") - title = selectFirst("div.flex > h2")?.ownText() ?: "No name" - genre = select("div.flex > a.inline-block").eachText().joinToString() - description = selectFirst("div.comicInfoExtend__synopsis")?.text() - } - - document.selectFirst("div#buttons + div.hidden")?.run { - status = when (getInfo("Status").orEmpty()) { - "Cancelled" -> SManga.CANCELLED - "Completed" -> SManga.COMPLETED - "Hiatus" -> SManga.ON_HIATUS - "Ongoing" -> SManga.ONGOING - else -> SManga.UNKNOWN - } - - artist = getInfo("Artist") - author = getInfo("Author") - } - } - - private fun Element.getInfo(text: String): String? = - selectFirst("p:has(span:containsOwn($text)) span.capitalize") - ?.ownText() - ?.trim() - - // ============================== Chapters ============================== - override fun chapterListSelector() = "div#chapters-list > a[href]" - - override fun chapterListParse(response: Response): List { - val originalUrl = response.request.url.toString() - - val chapterList = buildList { - var page = 1 - do { - val doc = when { - isEmpty() -> response // First page - else -> { - page++ - client.newCall(GET("$originalUrl?page=$page", headers)).execute() - } - }.use { it.asJsoup() } - - addAll(doc.select(chapterListSelector()).map(::chapterFromElement)) - } while (doc.selectFirst(latestUpdatesNextPageSelector()) != null) - } - - return chapterList - } - - override fun chapterFromElement(element: Element) = SChapter.create().apply { - setUrlWithoutDomain(element.attr("href")) - name = element.selectFirst("span")?.ownText() ?: "Chapter" - } - - // =============================== Pages ================================ - override fun pageListParse(document: Document): List { - return document.select("div#chapter-container > img").map { element -> - val id = element.attr("data-id").toIntOrNull() ?: 0 - val url = element.run { - absUrl("data-src").ifEmpty { absUrl("src") } - } - Page(id, "", url) - } - } - - override fun imageUrlParse(document: Document): String { - throw UnsupportedOperationException() - } - - companion object { - const val PREFIX_SEARCH = "id:" - } -} diff --git a/src/en/cloudrecess/src/eu/kanade/tachiyomi/extension/en/cloudrecess/CloudRecessFilters.kt b/src/en/cloudrecess/src/eu/kanade/tachiyomi/extension/en/cloudrecess/CloudRecessFilters.kt deleted file mode 100644 index d0c78dcbf..000000000 --- a/src/en/cloudrecess/src/eu/kanade/tachiyomi/extension/en/cloudrecess/CloudRecessFilters.kt +++ /dev/null @@ -1,163 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.cloudrecess - -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.source.model.FilterList - -object CloudRecessFilters { - open class QueryPartFilter( - displayName: String, - val vals: Array>, - ) : Filter.Select( - displayName, - vals.map { it.first }.toTypedArray(), - ) { - fun toQueryPart() = vals[state].second - } - - private inline fun FilterList.asQueryPart(): String { - return (first { it is R } as QueryPartFilter).toQueryPart() - } - - open class CheckBoxFilterList(name: String, val items: List) : - Filter.Group(name, items.map(::CheckBoxVal)) - - private class CheckBoxVal(name: String) : Filter.CheckBox(name, false) - - private inline fun FilterList.checkedItems(): List { - return (first { it is R } as CheckBoxFilterList).state - .filter { it.state } - .map { it.name } - } - - internal class TypeFilter : QueryPartFilter("Type", FiltersData.TYPE_LIST) - internal class StatusFilter : QueryPartFilter("Status", FiltersData.STATUS_LIST) - - internal class GenresFilter : CheckBoxFilterList("Genres", FiltersData.GENRES_LIST) - - val FILTER_LIST get() = FilterList( - TypeFilter(), - StatusFilter(), - GenresFilter(), - ) - - data class FilterSearchParams( - val type: String = "", - val status: String = "", - val genres: List = emptyList(), - ) - - internal fun getSearchParameters(filters: FilterList): FilterSearchParams { - if (filters.isEmpty()) return FilterSearchParams() - - return FilterSearchParams( - filters.asQueryPart(), - filters.asQueryPart(), - filters.checkedItems(), - ) - } - - private object FiltersData { - val TYPE_LIST = arrayOf( - Pair("All Types", ""), - Pair("Manga", "manga"), - Pair("Manhwa", "manhwa"), - Pair("OEL/Original", "oel"), - Pair("One Shot", "one-shot"), - Pair("Webtoon", "webtoon"), - ) - - val STATUS_LIST = arrayOf( - Pair("All Status", ""), - Pair("Cancelled", "cancelled"), - Pair("Completed", "completed"), - Pair("Hiatus", "hiatus"), - Pair("Ongoing", "ongoing"), - Pair("Pending", "pending"), - ) - - val GENRES_LIST = listOf( - "3P Relationship/s", - "Action", - "Adventure", - "Age Gap", - "Amnesia/Memory Loss", - "Art/s or Creative/s", - "BL", - "Bloody", - "Boss/Employee", - "Childhood Friend/s", - "Comedy", - "Coming of Age", - "Contractual Relationship", - "Crime", - "Cross Dressing", - "Crush", - "Depraved", - "Drama", - "Enemies to Lovers", - "Family Life", - "Fantasy", - "Fetish", - "First Love", - "Food", - "Friends to Lovers", - "Fxckbuddy", - "GL", - "Games", - "Guideverse", - "Hardcore", - "Harem", - "Historical", - "Horror", - "Idols/Celeb/Showbiz", - "Infidelity", - "Intense", - "Isekai", - "Josei", - "Light Hearted", - "Living Together", - "Love Triangle", - "Love/Hate", - "Manipulative", - "Master/Servant", - "Mature", - "Military", - "Music", - "Mystery", - "Nameverse", - "Obsessive", - "Omegaverse", - "On Campus/College Life", - "One Sided Love", - "Part Timer", - "Photography", - "Psychological", - "Rebirth/Reincarnation", - "Red Light", - "Retro", - "Revenge", - "Rich Kids", - "Romance", - "Royalty/Nobility/Gentry", - "SM/BDSM/SUB-DOM", - "School Life", - "Sci-Fi", - "Self-Discovery", - "Shounen Ai", - "Slice of Life", - "Smut", - "Sports", - "Step Family", - "Supernatural", - "Teacher/Student", - "Thriller", - "Tragedy", - "Tsundere", - "Uncensored", - "Violence", - "Voyeur", - "Work Place/Office Workers", - "Yakuza/Gangsters", - ) - } -} diff --git a/src/en/cloudrecess/src/eu/kanade/tachiyomi/extension/en/cloudrecess/CloudRecessUrlActivity.kt b/src/en/cloudrecess/src/eu/kanade/tachiyomi/extension/en/cloudrecess/CloudRecessUrlActivity.kt deleted file mode 100644 index ea9194f49..000000000 --- a/src/en/cloudrecess/src/eu/kanade/tachiyomi/extension/en/cloudrecess/CloudRecessUrlActivity.kt +++ /dev/null @@ -1,41 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.cloudrecess - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.util.Log -import kotlin.system.exitProcess - -/** - * Springboard that accepts https://cloudrecess.io/manga/ intents - * and redirects them to the main Tachiyomi process. - */ -class CloudRecessUrlActivity : Activity() { - - private val tag = javaClass.simpleName - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val pathSegments = intent?.data?.pathSegments - if (pathSegments != null && pathSegments.size > 1) { - val item = pathSegments[1] - val mainIntent = Intent().apply { - action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "${CloudRecess.PREFIX_SEARCH}$item") - putExtra("filter", packageName) - } - - try { - startActivity(mainIntent) - } catch (e: ActivityNotFoundException) { - Log.e(tag, e.toString()) - } - } else { - Log.e(tag, "could not parse uri from intent $intent") - } - - finish() - exitProcess(0) - } -} diff --git a/src/en/fleksyscans/build.gradle b/src/en/fleksyscans/build.gradle new file mode 100644 index 000000000..0181511fa --- /dev/null +++ b/src/en/fleksyscans/build.gradle @@ -0,0 +1,9 @@ +ext { + extName = 'FleksyScans' + extClass = '.FleksyScans' + themePkg = 'fuzzydoodle' + overrideVersionCode = 0 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/fleksyscans/res/mipmap-hdpi/ic_launcher.png b/src/en/fleksyscans/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..5ef28852a Binary files /dev/null and b/src/en/fleksyscans/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/fleksyscans/res/mipmap-mdpi/ic_launcher.png b/src/en/fleksyscans/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..002a89e2b Binary files /dev/null and b/src/en/fleksyscans/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/fleksyscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/fleksyscans/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d482da0a9 Binary files /dev/null and b/src/en/fleksyscans/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/fleksyscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/fleksyscans/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..02ef49d20 Binary files /dev/null and b/src/en/fleksyscans/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/fleksyscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/fleksyscans/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..ba3741c73 Binary files /dev/null and b/src/en/fleksyscans/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/fleksyscans/src/eu/kanade/tachiyomi/extension/en/fleksyscans/FleksyScans.kt b/src/en/fleksyscans/src/eu/kanade/tachiyomi/extension/en/fleksyscans/FleksyScans.kt new file mode 100644 index 000000000..f9dbf3890 --- /dev/null +++ b/src/en/fleksyscans/src/eu/kanade/tachiyomi/extension/en/fleksyscans/FleksyScans.kt @@ -0,0 +1,5 @@ +package eu.kanade.tachiyomi.extension.en.fleksyscans + +import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle + +class FleksyScans : FuzzyDoodle("FleksyScans", "https://flexscans.com", "en") diff --git a/src/en/scyllascans/build.gradle b/src/en/scyllascans/build.gradle index 53f908fb6..e98a6394b 100644 --- a/src/en/scyllascans/build.gradle +++ b/src/en/scyllascans/build.gradle @@ -1,9 +1,8 @@ ext { extName = 'Scylla Scans' extClass = '.ScyllaScans' - themePkg = 'readerfront' - baseUrl = 'https://scyllascans.org' - overrideVersionCode = 1 + themePkg = 'fuzzydoodle' + overrideVersionCode = 9 } apply from: "$rootDir/common.gradle" diff --git a/src/en/scyllascans/src/eu/kanade/tachiyomi/extension/en/scyllascans/ScyllaScans.kt b/src/en/scyllascans/src/eu/kanade/tachiyomi/extension/en/scyllascans/ScyllaScans.kt index afd46de7e..013067f8f 100644 --- a/src/en/scyllascans/src/eu/kanade/tachiyomi/extension/en/scyllascans/ScyllaScans.kt +++ b/src/en/scyllascans/src/eu/kanade/tachiyomi/extension/en/scyllascans/ScyllaScans.kt @@ -1,9 +1,11 @@ package eu.kanade.tachiyomi.extension.en.scyllascans -import eu.kanade.tachiyomi.multisrc.readerfront.ReaderFront +import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle -class ScyllaScans : ReaderFront("Scylla Scans", "https://scyllascans.org", "en") { - override fun getImageCDN(path: String, width: Int) = - "https://i${(0..2).random()}.wp.com/api.scyllascans.org" + - "$path?strip=all&quality=100&w=$width" +class ScyllaScans : FuzzyDoodle("Scylla Scans", "https://scyllascans.org", "en") { + + // readerfront -> fuzzydoodle + override val versionId = 2 + + override val latestFromHomePage = true } diff --git a/src/fr/lelscanvf/build.gradle b/src/fr/lelscanvf/build.gradle index ede73e46e..c72ae37e2 100644 --- a/src/fr/lelscanvf/build.gradle +++ b/src/fr/lelscanvf/build.gradle @@ -1,9 +1,8 @@ ext { extName = 'Lelscan-VF' extClass = '.LelscanVF' - themePkg = 'mmrcms' - baseUrl = 'https://lelscanvf.cc' - overrideVersionCode = 2 + themePkg = 'fuzzydoodle' + overrideVersionCode = 13 } apply from: "$rootDir/common.gradle" diff --git a/src/fr/lelscanvf/res/mipmap-hdpi/ic_launcher.png b/src/fr/lelscanvf/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..532492041 Binary files /dev/null and b/src/fr/lelscanvf/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/fr/lelscanvf/res/mipmap-mdpi/ic_launcher.png b/src/fr/lelscanvf/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..b823064af Binary files /dev/null and b/src/fr/lelscanvf/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/fr/lelscanvf/res/mipmap-xhdpi/ic_launcher.png b/src/fr/lelscanvf/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..f9e6b1b9f Binary files /dev/null and b/src/fr/lelscanvf/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/fr/lelscanvf/res/mipmap-xxhdpi/ic_launcher.png b/src/fr/lelscanvf/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..5ad99b812 Binary files /dev/null and b/src/fr/lelscanvf/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/fr/lelscanvf/res/mipmap-xxxhdpi/ic_launcher.png b/src/fr/lelscanvf/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..e20635a28 Binary files /dev/null and b/src/fr/lelscanvf/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/fr/lelscanvf/src/eu/kanade/tachiyomi/extension/fr/lelscanvf/LelscanVF.kt b/src/fr/lelscanvf/src/eu/kanade/tachiyomi/extension/fr/lelscanvf/LelscanVF.kt index 07d51af0c..1e13199e6 100644 --- a/src/fr/lelscanvf/src/eu/kanade/tachiyomi/extension/fr/lelscanvf/LelscanVF.kt +++ b/src/fr/lelscanvf/src/eu/kanade/tachiyomi/extension/fr/lelscanvf/LelscanVF.kt @@ -1,10 +1,11 @@ package eu.kanade.tachiyomi.extension.fr.lelscanvf -import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS +import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle -class LelscanVF : MMRCMS( - "Lelscan-VF", - "https://lelscanvf.cc", - "fr", - supportsAdvancedSearch = false, -) +class LelscanVF : FuzzyDoodle("Lelscan-VF", "https://lelscanfr.com", "fr") { + + // mmrcms -> FuzzyDoodle + override val versionId = 2 + + override val latestFromHomePage = true +}