From 972f6efb1babfe120eb25b44839ec316fb32a00e Mon Sep 17 00:00:00 2001 From: duasmisha-hub <300955612+duasmisha-hub@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:22:28 -0300 Subject: [PATCH 1/4] Add MangaLix --- src/en/mangalix/build.gradle.kts | 17 + .../mangalix/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 5326 bytes .../mangalix/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2829 bytes .../mangalix/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 8249 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 16119 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 27259 bytes .../tachiyomi/extension/en/mangalix/Dto.kt | 74 ++++ .../extension/en/mangalix/Filters.kt | 138 +++++++ .../extension/en/mangalix/JsLiteralParser.kt | 189 ++++++++++ .../extension/en/mangalix/Mangalix.kt | 352 ++++++++++++++++++ 10 files changed, 770 insertions(+) create mode 100644 src/en/mangalix/build.gradle.kts create mode 100644 src/en/mangalix/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/en/mangalix/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/en/mangalix/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/en/mangalix/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/en/mangalix/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Dto.kt create mode 100644 src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Filters.kt create mode 100644 src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/JsLiteralParser.kt create mode 100644 src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt diff --git a/src/en/mangalix/build.gradle.kts b/src/en/mangalix/build.gradle.kts new file mode 100644 index 00000000000..d094a0b8c7e --- /dev/null +++ b/src/en/mangalix/build.gradle.kts @@ -0,0 +1,17 @@ +import io.github.keiyoushi.gradle.api.ContentWarning + +plugins { + alias(kei.plugins.extension) +} + +keiyoushi { + name = "MangaLix" + versionCode = 1 + contentWarning = ContentWarning.MIXED + libVersion = "1.4" + + source { + lang = "en" + baseUrl = "https://mangalix.com" + } +} diff --git a/src/en/mangalix/res/mipmap-hdpi/ic_launcher.png b/src/en/mangalix/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4e9c70ba4f866fbc1edd0ca1a0a61ed6f8322a1c GIT binary patch literal 5326 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⩔v*F6&+2Q$pUmbAGlA>a8!;!r@-J|@5ntM|T;RRa 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 literal 0 HcmV?d00001 diff --git a/src/en/mangalix/res/mipmap-xhdpi/ic_launcher.png b/src/en/mangalix/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3f562c7acb3744a64dd8b10088979beeb2e25269 GIT binary patch literal 8249 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 literal 0 HcmV?d00001 diff --git a/src/en/mangalix/res/mipmap-xxhdpi/ic_launcher.png b/src/en/mangalix/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..84bb68abd3ff20d3732d22174a6b41e7418d01eb GIT binary patch literal 16119 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 literal 0 HcmV?d00001 diff --git a/src/en/mangalix/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/mangalix/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..115d322ad4c1f61868520752f9bc4a4f3791c94e GIT binary patch literal 27259 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 SV?+Y~pef3!zAk%Z8uUMJ?9y}q literal 0 HcmV?d00001 diff --git a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Dto.kt b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Dto.kt new file mode 100644 index 00000000000..50c1bcd359e --- /dev/null +++ b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Dto.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.extension.en.mangalix + +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.Serializable +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneOffset +import java.util.Locale + +@Serializable +internal class MangaDto( + val slug: String, + val title: String, + val description: String, + val coverImage: String, + val author: String, + val status: String, + val rating: Double, + val releaseYear: Double, + val genres: List, + val latestChapter: LatestChapterDto?, +) { + val latestTimestamp: Long + get() = latestChapter?.releaseDate.toMangaTimestamp() + + fun toSManga(baseUrl: String): SManga = SManga.create().apply { + url = "/manga/$slug" + title = this@MangaDto.title + thumbnail_url = coverImage.takeIf { it.isNotBlank() }?.let { + when { + it.startsWith("http://", ignoreCase = true) || it.startsWith("https://", ignoreCase = true) -> it + it.startsWith("//") -> "https:$it" + else -> "${baseUrl.trimEnd('/')}/${it.trimStart('/')}" + } + } + author = this@MangaDto.author.takeIf { it.isNotBlank() } + description = this@MangaDto.description.takeIf { it.isNotBlank() } + genre = genres.takeIf { it.isNotEmpty() }?.joinToString() + status = this@MangaDto.status.toMangaStatus() + initialized = true + } +} + +@Serializable +internal class LatestChapterDto( + val releaseDate: String?, +) + +internal class ChapterDto( + val id: String, + val number: Double, + val title: String, + val pages: List, + val releaseDate: String? = null, +) + +internal fun String?.toMangaTimestamp(): Long = this?.let { value -> + runCatching { Instant.parse(value).toEpochMilli() } + .recoverCatching { LocalDate.parse(value).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() } + .getOrDefault(0L) +} ?: 0L + +private fun String.toMangaStatus(): Int = when ( + lowercase(Locale.ROOT) + .trim() + .replace("-", " ") + .replace(Regex("""\s+"""), " ") +) { + "ongoing", "publishing", "releasing", "active" -> SManga.ONGOING + "completed", "complete", "finished" -> SManga.COMPLETED + "hiatus", "on hiatus", "paused" -> SManga.ON_HIATUS + "cancelled", "canceled", "dropped", "axed", "discontinued" -> SManga.CANCELLED + else -> SManga.UNKNOWN +} diff --git a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Filters.kt b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Filters.kt new file mode 100644 index 00000000000..6683ae4e431 --- /dev/null +++ b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Filters.kt @@ -0,0 +1,138 @@ +package eu.kanade.tachiyomi.extension.en.mangalix + +import eu.kanade.tachiyomi.source.model.Filter +import java.util.Locale + +internal class StatusFilter : + Filter.Select( + "Status", + STATUS_OPTIONS.map { it.first }.toTypedArray(), + ) { + val selected: String? + get() = STATUS_OPTIONS[state].second.takeIf { it.isNotEmpty() } + + fun matches(status: String): Boolean = selected?.let { + status.normalizedStatus() == it + } ?: true + + companion object { + private val STATUS_OPTIONS = listOf( + "All" to "", + "Ongoing" to "ongoing", + "Completed" to "completed", + "Hiatus" to "hiatus", + ) + } +} + +internal class SortFilter( + selection: Selection = Selection(0, false), +) : Filter.Sort( + "Sort By", + SORT_OPTIONS.map { it.first }.toTypedArray(), + selection, +) { + val selected: String + get() = SORT_OPTIONS[state?.index ?: 0].second + + val ascending: Boolean + get() = state?.ascending ?: false + + companion object { + const val DEFAULT = "default" + const val LATEST = "latest" + const val RATING = "rating" + const val TITLE = "title" + const val RELEASE_YEAR = "release_year" + + private val SORT_OPTIONS = listOf( + "Default" to DEFAULT, + "Latest Update" to LATEST, + "Release Year" to RELEASE_YEAR, + "Rating" to RATING, + "Title" to TITLE, + ) + } +} + +internal class GenreFilter : + Filter.Group( + "Genres", + GENRES.map(::GenreOption), + ) { + val selectedGenres: Set + get() = state.filter { it.state }.mapTo(linkedSetOf()) { it.name } + + fun matches(genres: List): Boolean { + val selected = selectedGenres.mapTo(hashSetOf()) { it.normalizedGenre() } + if (selected.isEmpty()) return true + + val available = genres.mapTo(hashSetOf()) { it.normalizedGenre() } + return available.containsAll(selected) + } + + companion object { + private val GENRES = listOf( + "4-Koma", + "Action", + "Adventure", + "Comedy", + "Dark Fantasy", + "Drama", + "Ecchi", + "Family", + "Fantasy", + "Harem", + "Historical", + "Horror", + "Isekai", + "Magic", + "Manhwa", + "Martial Arts", + "Mature", + "Mecha", + "Military", + "Murim", + "Mystery", + "Parody", + "Psychological", + "Regression", + "Reincarnation", + "Romance", + "School Life", + "Sci-Fi", + "Seinen", + "Shoujo", + "Shounen", + "Slice of Life", + "Sports", + "Supernatural", + "Survival", + "System", + "Thriller", + "Tragedy", + "Vampire", + "Webtoon", + ) + } +} + +internal class GenreOption(name: String) : Filter.CheckBox(name) + +private fun String.normalizedStatus(): String = when (normalizedKey()) { + "ongoing", "publishing", "releasing", "active" -> "ongoing" + "completed", "complete", "finished" -> "completed" + "hiatus", "onhiatus", "paused" -> "hiatus" + else -> normalizedKey() +} + +private fun String.normalizedGenre(): String = when (val value = normalizedKey()) { + "school" -> "schoollife" + "shojo" -> "shoujo" + "shonen" -> "shounen" + "webtoons" -> "webtoon" + else -> value +} + +private fun String.normalizedKey(): String = lowercase(Locale.ROOT) + .filter(Char::isLetterOrDigit) diff --git a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/JsLiteralParser.kt b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/JsLiteralParser.kt new file mode 100644 index 00000000000..d13b40a36ce --- /dev/null +++ b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/JsLiteralParser.kt @@ -0,0 +1,189 @@ +package eu.kanade.tachiyomi.extension.en.mangalix + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import java.io.IOException + +internal class JsLiteralParser( + private val source: String, + startIndex: Int, +) { + private var position = startIndex + + fun parse(): JsonElement { + skipWhitespace() + return parseValue() + } + + private fun parseValue(): JsonElement { + skipWhitespace() + return when (peek()) { + '{' -> parseObject() + '[' -> parseArray() + '\'', '"' -> JsonPrimitive(parseString()) + '-', '+', in '0'..'9' -> parseNumber() + else -> parseKeyword() + } + } + + private fun parseObject(): JsonObject { + expect('{') + skipWhitespace() + + val values = linkedMapOf() + if (consume('}')) return JsonObject(values) + + while (true) { + skipWhitespace() + val key = when (peek()) { + '\'', '"' -> parseString() + else -> parseIdentifier() + } + + skipWhitespace() + expect(':') + values[key] = parseValue() + + skipWhitespace() + if (consume('}')) break + expect(',') + skipWhitespace() + if (consume('}')) break + } + + return JsonObject(values) + } + + private fun parseArray(): JsonArray { + expect('[') + skipWhitespace() + + val values = mutableListOf() + if (consume(']')) return JsonArray(values) + + while (true) { + values += parseValue() + + skipWhitespace() + if (consume(']')) break + expect(',') + skipWhitespace() + if (consume(']')) break + } + + return JsonArray(values) + } + + private fun parseString(): String { + val quote = next() + val result = StringBuilder() + + while (position < source.length) { + val char = next() + if (char == quote) return result.toString() + if (char != '\\') { + result.append(char) + continue + } + + if (position >= source.length) fail("Unterminated escape sequence") + when (val escaped = next()) { + '\'', '"', '\\', '/' -> result.append(escaped) + 'b' -> result.append('\b') + 'f' -> result.append('\u000C') + 'n' -> result.append('\n') + 'r' -> result.append('\r') + 't' -> result.append('\t') + 'v' -> result.append('\u000B') + '0' -> result.append('\u0000') + 'x' -> result.append(readHex(2).toChar()) + 'u' -> { + if (consume('{')) { + val end = source.indexOf('}', position) + if (end == -1) fail("Unterminated Unicode escape") + val codePoint = source.substring(position, end).toIntOrNull(16) + ?: fail("Invalid Unicode escape") + result.appendCodePoint(codePoint) + position = end + 1 + } else { + result.append(readHex(4).toChar()) + } + } + '\n' -> Unit + '\r' -> consume('\n') + else -> result.append(escaped) + } + } + + fail("Unterminated string") + } + + private fun parseNumber(): JsonPrimitive { + val start = position + if (peek() == '+' || peek() == '-') position++ + while (position < source.length && source[position].isDigit()) position++ + if (position < source.length && source[position] == '.') { + position++ + while (position < source.length && source[position].isDigit()) position++ + } + if (position < source.length && source[position] in "eE") { + position++ + if (position < source.length && source[position] in "+-") position++ + while (position < source.length && source[position].isDigit()) position++ + } + + val number = source.substring(start, position) + return number.toLongOrNull()?.let(::JsonPrimitive) + ?: number.toDoubleOrNull()?.let(::JsonPrimitive) + ?: fail("Invalid number") + } + + private fun parseKeyword(): JsonElement = when (val value = parseIdentifier()) { + "true" -> JsonPrimitive(true) + "false" -> JsonPrimitive(false) + "null", "undefined" -> JsonNull + else -> fail("Unsupported value '$value'") + } + + private fun parseIdentifier(): String { + val start = position + while (position < source.length) { + val char = source[position] + if (!char.isLetterOrDigit() && char != '_' && char != '$') break + position++ + } + if (position == start) fail("Expected identifier") + return source.substring(start, position) + } + + private fun readHex(length: Int): Int { + if (position + length > source.length) fail("Incomplete hexadecimal escape") + val value = source.substring(position, position + length).toIntOrNull(16) + ?: fail("Invalid hexadecimal escape") + position += length + return value + } + + private fun skipWhitespace() { + while (position < source.length && source[position].isWhitespace()) position++ + } + + private fun peek(): Char = source.getOrNull(position) ?: fail("Unexpected end of input") + + private fun next(): Char = peek().also { position++ } + + private fun expect(expected: Char) { + if (!consume(expected)) fail("Expected '$expected'") + } + + private fun consume(expected: Char): Boolean { + if (source.getOrNull(position) != expected) return false + position++ + return true + } + + private fun fail(message: String): Nothing = throw IOException("$message at position $position") +} diff --git a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt new file mode 100644 index 00000000000..457144598c7 --- /dev/null +++ b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt @@ -0,0 +1,352 @@ +package eu.kanade.tachiyomi.extension.en.mangalix + +import android.util.JsonReader +import android.util.JsonToken +import eu.kanade.tachiyomi.network.GET +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.HttpSource +import eu.kanade.tachiyomi.util.asJsoup +import keiyoushi.annotation.Source +import keiyoushi.network.rateLimit +import keiyoushi.utils.firstInstanceOrNull +import keiyoushi.utils.parseAs +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStreamReader +import java.util.Locale +import java.util.zip.GZIPInputStream + +@Source +abstract class Mangalix : HttpSource() { + + override val supportsLatest = true + + private val baseUrlHost by lazy { baseUrl.toHttpUrl().host } + + override val client = network.client.newBuilder() + .rateLimit(2) { it.host == baseUrlHost } + .build() + + private val archiveHeaders by lazy { + headersBuilder() + .set("Accept", "application/gzip") + .set("Accept-Encoding", "identity") + .build() + } + + private val imageHeaders by lazy { + headers.newBuilder() + .set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8") + .build() + } + + @Volatile + private var catalogCache: CatalogCache? = null + + @Volatile + private var archiveCache: ArchiveCache? = null + + @Volatile + private var chapterCache: ChapterCache? = null + + override fun fetchPopularManga(page: Int): Observable = Observable.fromCallable { + getCatalog().toPage(page) + } + + override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException() + + override fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() + + override fun fetchLatestUpdates(page: Int): Observable = Observable.fromCallable { + getCatalog().sortedByDescending(MangaDto::latestTimestamp).toPage(page) + } + + override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() + + override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { + val statusFilter = filters.firstInstanceOrNull() + val sortFilter = filters.firstInstanceOrNull() + val genreFilter = filters.firstInstanceOrNull() + + var result = getCatalog().asSequence() + .filter { manga -> query.isBlank() || manga.matchesQuery(query) } + .filter { manga -> statusFilter?.matches(manga.status) != false } + .filter { manga -> genreFilter?.matches(manga.genres) != false } + .toList() + + result = when (sortFilter?.selected) { + SortFilter.LATEST -> result.sortedByDescending(MangaDto::latestTimestamp) + SortFilter.RATING -> result.sortedByDescending(MangaDto::rating) + SortFilter.TITLE -> result.sortedByDescending { it.title.lowercase(Locale.ROOT) } + SortFilter.RELEASE_YEAR -> result.sortedByDescending(MangaDto::releaseYear) + else -> result + } + + if (sortFilter?.ascending == true) result = result.reversed() + result.toPage(page) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException() + + override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() + + override fun getFilterList(): FilterList = FilterList( + StatusFilter(), + SortFilter(), + GenreFilter(), + ) + + override fun fetchMangaDetails(manga: SManga): Observable = Observable.fromCallable { + getCatalog().firstOrNull { it.slug == manga.slug() }?.toSManga(baseUrl) ?: manga + } + + override fun mangaDetailsRequest(manga: SManga): Request = throw UnsupportedOperationException() + + override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException() + + override fun getMangaUrl(manga: SManga): String = "$baseUrl/manga/${manga.slug()}" + + override fun fetchChapterList(manga: SManga): Observable> = Observable.fromCallable { + val slug = manga.slug() + val archive = getArchive() + getChapters(archive, slug).distinctBy { it.id to it.number }.map { chapter -> + SChapter.create().apply { + val number = chapter.number.toString().removeSuffix(".0") + url = "$baseUrl/manga/$slug/chapter/$number".toHttpUrl().newBuilder() + .addQueryParameter("id", chapter.id) + .build() + .toString() + .removePrefix(baseUrl) + name = chapter.title.ifBlank { "Chapter $number" } + chapter_number = chapter.number.toFloat() + date_upload = chapter.releaseDate.toMangaTimestamp() + } + } + } + + override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException() + + override fun chapterListParse(response: Response): List = throw UnsupportedOperationException() + + override fun getChapterUrl(chapter: SChapter): String = (baseUrl + chapter.url).toHttpUrl().newBuilder() + .query(null) + .build() + .toString() + + override fun fetchPageList(chapter: SChapter): Observable> = Observable.fromCallable { + val chapterUrl = (baseUrl + chapter.url).toHttpUrl() + val slug = chapterUrl.pathSegments.getOrNull(1) + ?: throw IOException("Missing manga slug") + val chapterNumber = chapterUrl.pathSegments.getOrNull(3)?.toDoubleOrNull() + ?: throw IOException("Missing chapter number") + val chapterId = chapterUrl.queryParameter("id") + ?: throw IOException("Missing chapter id") + + getChapters(getArchive(), slug) + .firstOrNull { it.id == chapterId && it.number == chapterNumber } + ?.pages + .orEmpty() + .mapIndexed { index, imageUrl -> + Page(index, imageUrl = imageUrl.resolveImageUrl()) + } + } + + override fun pageListRequest(chapter: SChapter): Request = throw UnsupportedOperationException() + + override fun pageListParse(response: Response): List = throw UnsupportedOperationException() + + override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, imageHeaders) + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + + @Synchronized + private fun getCatalog(): List { + val cached = catalogCache + if (cached != null && System.currentTimeMillis() - cached.savedAt < CATALOG_CACHE_LIFETIME) { + return cached.mangas + } + + return loadCatalog().also { + catalogCache = CatalogCache(it, System.currentTimeMillis()) + } + } + + private fun loadCatalog(): List { + val document = client.newCall(GET(baseUrl, headers)).execute().use { response -> + response.requireSuccess() + response.asJsoup() + } + val scriptUrl = document.selectFirst("script[type=module][src*=assets/main-], script[src*=assets/main-]") + ?.absUrl("src") + ?.takeIf(String::isNotBlank) + ?: throw IOException("Main script not found") + + val script = client.newCall(GET(scriptUrl, headers)).execute().use { response -> + response.requireSuccess() + response.body.string() + } + + CATALOG_START_REGEX.findAll(script).forEach { match -> + val element = runCatching { + JsLiteralParser(script, match.range.first).parse() + }.getOrNull() ?: return@forEach + val candidate = runCatching { + element.parseAs>() + }.getOrNull() ?: return@forEach + + if (candidate.isNotEmpty() && candidate.all { it.slug.isNotBlank() && it.title.isNotBlank() }) { + return candidate.distinctBy(MangaDto::slug) + } + } + + throw IOException("Manga catalog not found") + } + + private fun MangaDto.matchesQuery(query: String): Boolean { + val terms = query.trim().lowercase(Locale.ROOT).split(WHITESPACE_REGEX) + val searchable = "$title $author $slug".lowercase(Locale.ROOT) + return terms.all(searchable::contains) + } + + private fun List.toPage(page: Int): MangasPage { + val start = ((page - 1).coerceAtLeast(0)) * PAGE_SIZE + if (start >= size) return MangasPage(emptyList(), false) + val end = (start + PAGE_SIZE).coerceAtMost(size) + return MangasPage(subList(start, end).map { it.toSManga(baseUrl) }, end < size) + } + + @Synchronized + private fun getArchive(): ByteArray { + val cached = archiveCache + if (cached != null && System.currentTimeMillis() - cached.savedAt < ARCHIVE_CACHE_LIFETIME) { + return cached.bytes + } + + return client.newCall(GET("$baseUrl/chapters.json.gz", archiveHeaders)).execute().use { response -> + response.requireSuccess() + response.body.bytes().also(::cacheArchive) + } + } + + private fun cacheArchive(bytes: ByteArray) { + if (bytes.size < 2 || bytes[0] != GZIP_MAGIC_FIRST || bytes[1] != GZIP_MAGIC_SECOND) { + throw IOException("Invalid chapter archive") + } + archiveCache = ArchiveCache(bytes, System.currentTimeMillis()) + chapterCache = null + } + + @Synchronized + private fun getChapters(archive: ByteArray, slug: String): List { + chapterCache?.takeIf { it.archive === archive && it.slug == slug }?.let { return it.chapters } + + return readChapters(archive, slug).also { + chapterCache = ChapterCache(archive, slug, it) + } + } + + private fun readChapters(archive: ByteArray, slug: String): List = withArchiveReader(archive) { reader -> + reader.beginObject() + while (reader.hasNext()) { + if (reader.nextName() == slug) { + return@withArchiveReader reader.readChapterArray() + } + reader.skipValue() + } + emptyList() + } + + private inline fun withArchiveReader(archive: ByteArray, block: (JsonReader) -> T): T = GZIPInputStream(ByteArrayInputStream(archive)).use { gzip -> + JsonReader(InputStreamReader(gzip, Charsets.UTF_8)).use(block) + } + + private fun JsonReader.readChapterArray(): List { + val chapters = mutableListOf() + beginArray() + while (hasNext()) { + beginObject() + var id = "" + var number = 0.0 + var title = "" + var releaseDate: String? = null + var pages = emptyList() + + while (hasNext()) { + when (nextName()) { + "id" -> id = nextString() + "number" -> number = nextDouble() + "title" -> title = nextString() + "releaseDate" -> releaseDate = nextNullableString() + "pages" -> pages = readStringArray() + else -> skipValue() + } + } + endObject() + if (id.isNotBlank()) { + chapters += ChapterDto(id, number, title, pages, releaseDate) + } + } + endArray() + return chapters + } + + private fun JsonReader.readStringArray(): List { + val values = mutableListOf() + beginArray() + while (hasNext()) values += nextString() + endArray() + return values + } + + private fun JsonReader.nextNullableString(): String? = if (peek() == JsonToken.NULL) { + nextNull() + null + } else { + nextString() + } + + private fun SManga.slug(): String = (baseUrl + url).toHttpUrl().pathSegments.getOrNull(1) + ?: throw IOException("Missing manga slug") + + private fun String.resolveImageUrl(): String = when { + startsWith("\$TEMP") -> replaceFirst("\$TEMP", "https://temp.compsci88.com") + startsWith("\$HOT") -> replaceFirst("\$HOT", "https://scans-hot.planeptune.us") + startsWith("\$LST") -> replaceFirst("\$LST", "https://scans.lastation.us") + startsWith("\$LOW") -> replaceFirst("\$LOW", "https://official.lowee.us") + startsWith("\$MFK") -> replaceFirst("\$MFK", "https://images.mangafreak.me") + startsWith("/cdn-readmanga/") -> "https://cdn.readmanga.cc${this.removePrefix("/cdn-readmanga")}" + startsWith("//") -> "https:$this" + startsWith("/") -> "$baseUrl$this" + else -> this + } + + private fun Response.requireSuccess() { + if (!isSuccessful) throw IOException("HTTP $code for ${request.url}") + } + + private data class CatalogCache(val mangas: List, val savedAt: Long) + + private data class ArchiveCache(val bytes: ByteArray, val savedAt: Long) + + private data class ChapterCache(val archive: ByteArray, val slug: String, val chapters: List) + + companion object { + private const val PAGE_SIZE = 16 + private const val CATALOG_CACHE_LIFETIME = 30 * 60 * 1000L + private const val ARCHIVE_CACHE_LIFETIME = 30 * 60 * 1000L + private const val GZIP_MAGIC_FIRST: Byte = 0x1F + private const val GZIP_MAGIC_SECOND: Byte = -117 + private val CATALOG_START_REGEX = Regex("""\[\{id\s*:""") + private val WHITESPACE_REGEX = Regex("""\s+""") + } +} From 7f8bb9c743fa6e4b883d1619a68e3411f9a77be4 Mon Sep 17 00:00:00 2001 From: duasmisha-hub <300955612+duasmisha-hub@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:31:54 -0300 Subject: [PATCH 2/4] MangaLix: implement KeiSource, store slug only, drop in-memory caches --- src/en/mangalix/build.gradle.kts | 2 +- .../tachiyomi/extension/en/mangalix/Dto.kt | 2 +- .../extension/en/mangalix/Mangalix.kt | 262 ++++++------------ 3 files changed, 91 insertions(+), 175 deletions(-) diff --git a/src/en/mangalix/build.gradle.kts b/src/en/mangalix/build.gradle.kts index d094a0b8c7e..d52f4202d9f 100644 --- a/src/en/mangalix/build.gradle.kts +++ b/src/en/mangalix/build.gradle.kts @@ -8,7 +8,7 @@ keiyoushi { name = "MangaLix" versionCode = 1 contentWarning = ContentWarning.MIXED - libVersion = "1.4" + libVersion = "1.6" source { lang = "en" diff --git a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Dto.kt b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Dto.kt index 50c1bcd359e..4b04fa4c96f 100644 --- a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Dto.kt +++ b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Dto.kt @@ -24,7 +24,7 @@ internal class MangaDto( get() = latestChapter?.releaseDate.toMangaTimestamp() fun toSManga(baseUrl: String): SManga = SManga.create().apply { - url = "/manga/$slug" + url = slug title = this@MangaDto.title thumbnail_url = coverImage.takeIf { it.isNotBlank() }?.let { when { diff --git a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt index 457144598c7..98712c24210 100644 --- a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt +++ b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt @@ -8,77 +8,56 @@ 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.HttpSource +import eu.kanade.tachiyomi.source.model.SMangaUpdate import eu.kanade.tachiyomi.util.asJsoup import keiyoushi.annotation.Source +import keiyoushi.network.get import keiyoushi.network.rateLimit +import keiyoushi.source.KeiSource import keiyoushi.utils.firstInstanceOrNull import keiyoushi.utils.parseAs +import kotlinx.serialization.json.JsonElement +import okhttp3.CacheControl +import okhttp3.Headers +import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.Response -import rx.Observable -import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStreamReader import java.util.Locale import java.util.zip.GZIPInputStream +import kotlin.time.Duration.Companion.minutes @Source -abstract class Mangalix : HttpSource() { +abstract class Mangalix : KeiSource() { - override val supportsLatest = true - - private val baseUrlHost by lazy { baseUrl.toHttpUrl().host } - - override val client = network.client.newBuilder() - .rateLimit(2) { it.host == baseUrlHost } - .build() + override fun OkHttpClient.Builder.configureClient(): OkHttpClient.Builder = apply { + val host = baseUrl.toHttpUrl().host + rateLimit(2) { it.host == host } + } - private val archiveHeaders by lazy { - headersBuilder() + private val archiveHeaders: Headers + get() = headersBuilder() .set("Accept", "application/gzip") .set("Accept-Encoding", "identity") .build() - } - private val imageHeaders by lazy { - headers.newBuilder() + private val imageHeaders: Headers + get() = headersBuilder() .set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8") .build() - } - - @Volatile - private var catalogCache: CatalogCache? = null - - @Volatile - private var archiveCache: ArchiveCache? = null - - @Volatile - private var chapterCache: ChapterCache? = null - - override fun fetchPopularManga(page: Int): Observable = Observable.fromCallable { - getCatalog().toPage(page) - } - - override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException() - - override fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() - - override fun fetchLatestUpdates(page: Int): Observable = Observable.fromCallable { - getCatalog().sortedByDescending(MangaDto::latestTimestamp).toPage(page) - } - override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() + override suspend fun getPopularManga(page: Int): MangasPage = loadCatalog().toPage(page) - override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() + override suspend fun getLatestUpdates(page: Int): MangasPage = loadCatalog().sortedByDescending(MangaDto::latestTimestamp).toPage(page) - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { + override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage { val statusFilter = filters.firstInstanceOrNull() val sortFilter = filters.firstInstanceOrNull() val genreFilter = filters.firstInstanceOrNull() - var result = getCatalog().asSequence() + var result = loadCatalog().asSequence() .filter { manga -> query.isBlank() || manga.matchesQuery(query) } .filter { manga -> statusFilter?.matches(manga.status) != false } .filter { manga -> genreFilter?.matches(manga.genres) != false } @@ -93,67 +72,74 @@ abstract class Mangalix : HttpSource() { } if (sortFilter?.ascending == true) result = result.reversed() - result.toPage(page) + return result.toPage(page) } - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException() - - override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() - - override fun getFilterList(): FilterList = FilterList( + override fun getFilterList(data: JsonElement?): FilterList = FilterList( StatusFilter(), SortFilter(), GenreFilter(), ) - override fun fetchMangaDetails(manga: SManga): Observable = Observable.fromCallable { - getCatalog().firstOrNull { it.slug == manga.slug() }?.toSManga(baseUrl) ?: manga + override suspend fun getMangaByUrl(url: HttpUrl): SManga? { + if (url.host != baseUrl.toHttpUrl().host) return null + if (url.pathSegments.getOrNull(0) != "manga") return null + val slug = url.pathSegments.getOrNull(1) ?: return null + + return loadCatalog().firstOrNull { it.slug == slug }?.toSManga(baseUrl) } - override fun mangaDetailsRequest(manga: SManga): Request = throw UnsupportedOperationException() - - override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException() - - override fun getMangaUrl(manga: SManga): String = "$baseUrl/manga/${manga.slug()}" - - override fun fetchChapterList(manga: SManga): Observable> = Observable.fromCallable { - val slug = manga.slug() - val archive = getArchive() - getChapters(archive, slug).distinctBy { it.id to it.number }.map { chapter -> - SChapter.create().apply { - val number = chapter.number.toString().removeSuffix(".0") - url = "$baseUrl/manga/$slug/chapter/$number".toHttpUrl().newBuilder() - .addQueryParameter("id", chapter.id) - .build() - .toString() - .removePrefix(baseUrl) - name = chapter.title.ifBlank { "Chapter $number" } - chapter_number = chapter.number.toFloat() - date_upload = chapter.releaseDate.toMangaTimestamp() - } - } + override fun getMangaUrl(manga: SManga): String = "$baseUrl/manga/${manga.url}" + + override suspend fun fetchMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate { + val slug = manga.url + + return SMangaUpdate( + manga = if (fetchDetails) { + loadCatalog().firstOrNull { it.slug == slug }?.toSManga(baseUrl) ?: manga + } else { + manga + }, + chapters = if (fetchChapters) { + readChapters(slug).distinctBy { it.id to it.number }.map { chapter -> + SChapter.create().apply { + val number = chapter.number.toString().removeSuffix(".0") + url = "$slug/$number/${chapter.id}" + name = chapter.title.ifBlank { "Chapter $number" } + chapter_number = chapter.number.toFloat() + date_upload = chapter.releaseDate.toMangaTimestamp() + } + } + } else { + chapters + }, + ) } - override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException() + override val supportsRelatedMangas = false - override fun chapterListParse(response: Response): List = throw UnsupportedOperationException() + override suspend fun fetchRelatedMangaList(manga: SManga): List = throw UnsupportedOperationException() - override fun getChapterUrl(chapter: SChapter): String = (baseUrl + chapter.url).toHttpUrl().newBuilder() - .query(null) - .build() - .toString() + override fun getChapterUrl(chapter: SChapter): String { + val (slug, number) = chapter.url.split('/') + return "$baseUrl/manga/$slug/chapter/$number" + } - override fun fetchPageList(chapter: SChapter): Observable> = Observable.fromCallable { - val chapterUrl = (baseUrl + chapter.url).toHttpUrl() - val slug = chapterUrl.pathSegments.getOrNull(1) - ?: throw IOException("Missing manga slug") - val chapterNumber = chapterUrl.pathSegments.getOrNull(3)?.toDoubleOrNull() + override suspend fun getPageList(chapter: SChapter): List { + val parts = chapter.url.split('/') + if (parts.size < 3) throw IOException("Invalid chapter url") + val slug = parts[0] + val number = parts[1].toDoubleOrNull() ?: throw IOException("Missing chapter number") - val chapterId = chapterUrl.queryParameter("id") - ?: throw IOException("Missing chapter id") + val chapterId = parts[2] - getChapters(getArchive(), slug) - .firstOrNull { it.id == chapterId && it.number == chapterNumber } + return readChapters(slug) + .firstOrNull { it.id == chapterId && it.number == number } ?.pages .orEmpty() .mapIndexed { index, imageUrl -> @@ -161,40 +147,16 @@ abstract class Mangalix : HttpSource() { } } - override fun pageListRequest(chapter: SChapter): Request = throw UnsupportedOperationException() - - override fun pageListParse(response: Response): List = throw UnsupportedOperationException() - override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, imageHeaders) - override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() - - @Synchronized - private fun getCatalog(): List { - val cached = catalogCache - if (cached != null && System.currentTimeMillis() - cached.savedAt < CATALOG_CACHE_LIFETIME) { - return cached.mangas - } - - return loadCatalog().also { - catalogCache = CatalogCache(it, System.currentTimeMillis()) - } - } - - private fun loadCatalog(): List { - val document = client.newCall(GET(baseUrl, headers)).execute().use { response -> - response.requireSuccess() - response.asJsoup() - } + private suspend fun loadCatalog(): List { + val document = client.get(baseUrl).asJsoup() val scriptUrl = document.selectFirst("script[type=module][src*=assets/main-], script[src*=assets/main-]") ?.absUrl("src") ?.takeIf(String::isNotBlank) ?: throw IOException("Main script not found") - val script = client.newCall(GET(scriptUrl, headers)).execute().use { response -> - response.requireSuccess() - response.body.string() - } + val script = client.get(scriptUrl).body.string() CATALOG_START_REGEX.findAll(script).forEach { match -> val element = runCatching { @@ -225,49 +187,19 @@ abstract class Mangalix : HttpSource() { return MangasPage(subList(start, end).map { it.toSManga(baseUrl) }, end < size) } - @Synchronized - private fun getArchive(): ByteArray { - val cached = archiveCache - if (cached != null && System.currentTimeMillis() - cached.savedAt < ARCHIVE_CACHE_LIFETIME) { - return cached.bytes - } - - return client.newCall(GET("$baseUrl/chapters.json.gz", archiveHeaders)).execute().use { response -> - response.requireSuccess() - response.body.bytes().also(::cacheArchive) - } - } - - private fun cacheArchive(bytes: ByteArray) { - if (bytes.size < 2 || bytes[0] != GZIP_MAGIC_FIRST || bytes[1] != GZIP_MAGIC_SECOND) { - throw IOException("Invalid chapter archive") - } - archiveCache = ArchiveCache(bytes, System.currentTimeMillis()) - chapterCache = null - } - - @Synchronized - private fun getChapters(archive: ByteArray, slug: String): List { - chapterCache?.takeIf { it.archive === archive && it.slug == slug }?.let { return it.chapters } - - return readChapters(archive, slug).also { - chapterCache = ChapterCache(archive, slug, it) - } - } - - private fun readChapters(archive: ByteArray, slug: String): List = withArchiveReader(archive) { reader -> - reader.beginObject() - while (reader.hasNext()) { - if (reader.nextName() == slug) { - return@withArchiveReader reader.readChapterArray() + private suspend fun readChapters(slug: String): List = client.get("$baseUrl/chapters.json.gz", archiveHeaders, ARCHIVE_CACHE_CONTROL).use { response -> + JsonReader(InputStreamReader(GZIPInputStream(response.body.byteStream()), Charsets.UTF_8)).use { reader -> + var chapters: List = emptyList() + reader.beginObject() + while (reader.hasNext()) { + if (reader.nextName() == slug) { + chapters = reader.readChapterArray() + break + } + reader.skipValue() } - reader.skipValue() + chapters } - emptyList() - } - - private inline fun withArchiveReader(archive: ByteArray, block: (JsonReader) -> T): T = GZIPInputStream(ByteArrayInputStream(archive)).use { gzip -> - JsonReader(InputStreamReader(gzip, Charsets.UTF_8)).use(block) } private fun JsonReader.readChapterArray(): List { @@ -315,9 +247,6 @@ abstract class Mangalix : HttpSource() { nextString() } - private fun SManga.slug(): String = (baseUrl + url).toHttpUrl().pathSegments.getOrNull(1) - ?: throw IOException("Missing manga slug") - private fun String.resolveImageUrl(): String = when { startsWith("\$TEMP") -> replaceFirst("\$TEMP", "https://temp.compsci88.com") startsWith("\$HOT") -> replaceFirst("\$HOT", "https://scans-hot.planeptune.us") @@ -330,22 +259,9 @@ abstract class Mangalix : HttpSource() { else -> this } - private fun Response.requireSuccess() { - if (!isSuccessful) throw IOException("HTTP $code for ${request.url}") - } - - private data class CatalogCache(val mangas: List, val savedAt: Long) - - private data class ArchiveCache(val bytes: ByteArray, val savedAt: Long) - - private data class ChapterCache(val archive: ByteArray, val slug: String, val chapters: List) - companion object { private const val PAGE_SIZE = 16 - private const val CATALOG_CACHE_LIFETIME = 30 * 60 * 1000L - private const val ARCHIVE_CACHE_LIFETIME = 30 * 60 * 1000L - private const val GZIP_MAGIC_FIRST: Byte = 0x1F - private const val GZIP_MAGIC_SECOND: Byte = -117 + private val ARCHIVE_CACHE_CONTROL = CacheControl.Builder().maxAge(30.minutes).build() private val CATALOG_START_REGEX = Regex("""\[\{id\s*:""") private val WHITESPACE_REGEX = Regex("""\s+""") } From d119d314224b4cb52236beaeaa3ccd1e48cd7db5 Mon Sep 17 00:00:00 2001 From: duasmisha-hub <300955612+duasmisha-hub@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:08:46 -0300 Subject: [PATCH 3/4] MangaLix: store chapter id in url and rest in memo, parallel update fetch --- .../extension/en/mangalix/Mangalix.kt | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt index 98712c24210..8eefcf408d3 100644 --- a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt +++ b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt @@ -16,7 +16,12 @@ import keiyoushi.network.rateLimit import keiyoushi.source.KeiSource import keiyoushi.utils.firstInstanceOrNull import keiyoushi.utils.parseAs +import keiyoushi.utils.string +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import okhttp3.CacheControl import okhttp3.Headers import okhttp3.HttpUrl @@ -96,28 +101,40 @@ abstract class Mangalix : KeiSource() { chapters: List, fetchDetails: Boolean, fetchChapters: Boolean, - ): SMangaUpdate { + ): SMangaUpdate = coroutineScope { val slug = manga.url - return SMangaUpdate( - manga = if (fetchDetails) { + val updatedManga = if (fetchDetails) { + async { loadCatalog().firstOrNull { it.slug == slug }?.toSManga(baseUrl) ?: manga - } else { - manga - }, - chapters = if (fetchChapters) { + } + } else { + null + } + + val updatedChapters = if (fetchChapters) { + async { readChapters(slug).distinctBy { it.id to it.number }.map { chapter -> SChapter.create().apply { val number = chapter.number.toString().removeSuffix(".0") - url = "$slug/$number/${chapter.id}" + url = chapter.id + memo = buildJsonObject { + put("slug", slug) + put("number", number) + } name = chapter.title.ifBlank { "Chapter $number" } chapter_number = chapter.number.toFloat() date_upload = chapter.releaseDate.toMangaTimestamp() } } - } else { - chapters - }, + } + } else { + null + } + + SMangaUpdate( + manga = updatedManga?.await() ?: manga, + chapters = updatedChapters?.await() ?: chapters, ) } @@ -126,17 +143,15 @@ abstract class Mangalix : KeiSource() { override suspend fun fetchRelatedMangaList(manga: SManga): List = throw UnsupportedOperationException() override fun getChapterUrl(chapter: SChapter): String { - val (slug, number) = chapter.url.split('/') + val slug = chapter.memo["slug"]!!.string + val number = chapter.memo["number"]!!.string return "$baseUrl/manga/$slug/chapter/$number" } override suspend fun getPageList(chapter: SChapter): List { - val parts = chapter.url.split('/') - if (parts.size < 3) throw IOException("Invalid chapter url") - val slug = parts[0] - val number = parts[1].toDoubleOrNull() - ?: throw IOException("Missing chapter number") - val chapterId = parts[2] + val slug = chapter.memo["slug"]?.string ?: throw Exception("Refresh chapter list") + val number = chapter.memo["number"]!!.string.toDouble() + val chapterId = chapter.url return readChapters(slug) .firstOrNull { it.id == chapterId && it.number == number } From 0da99ff8cdf770c98e550ec3e1c808dc4aebaddf Mon Sep 17 00:00:00 2001 From: duasmisha-hub <300955612+duasmisha-hub@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:48:57 -0300 Subject: [PATCH 4/4] MangaLix: drop client-side pagination, add deeplink --- src/en/mangalix/build.gradle.kts | 4 +++ .../extension/en/mangalix/Mangalix.kt | 34 ++++++++++--------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/en/mangalix/build.gradle.kts b/src/en/mangalix/build.gradle.kts index d52f4202d9f..39ddc2fc849 100644 --- a/src/en/mangalix/build.gradle.kts +++ b/src/en/mangalix/build.gradle.kts @@ -14,4 +14,8 @@ keiyoushi { lang = "en" baseUrl = "https://mangalix.com" } + + deeplink { + path("/manga/..*") + } } diff --git a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt index 8eefcf408d3..bbec5900ff4 100644 --- a/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt +++ b/src/en/mangalix/src/eu/kanade/tachiyomi/extension/en/mangalix/Mangalix.kt @@ -53,20 +53,30 @@ abstract class Mangalix : KeiSource() { .set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8") .build() - override suspend fun getPopularManga(page: Int): MangasPage = loadCatalog().toPage(page) + override suspend fun getPopularManga(page: Int): MangasPage = MangasPage( + loadCatalog().map { it.toSManga(baseUrl) }, + hasNextPage = false, + ) - override suspend fun getLatestUpdates(page: Int): MangasPage = loadCatalog().sortedByDescending(MangaDto::latestTimestamp).toPage(page) + override suspend fun getLatestUpdates(page: Int): MangasPage = MangasPage( + loadCatalog() + .sortedByDescending(MangaDto::latestTimestamp) + .map { it.toSManga(baseUrl) }, + hasNextPage = false, + ) override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage { val statusFilter = filters.firstInstanceOrNull() val sortFilter = filters.firstInstanceOrNull() val genreFilter = filters.firstInstanceOrNull() + val terms = query.trim().lowercase(Locale.ROOT) + .split(WHITESPACE_REGEX) + .filter(String::isNotEmpty) - var result = loadCatalog().asSequence() - .filter { manga -> query.isBlank() || manga.matchesQuery(query) } + var result = loadCatalog() + .filter { manga -> terms.isEmpty() || manga.matchesTerms(terms) } .filter { manga -> statusFilter?.matches(manga.status) != false } .filter { manga -> genreFilter?.matches(manga.genres) != false } - .toList() result = when (sortFilter?.selected) { SortFilter.LATEST -> result.sortedByDescending(MangaDto::latestTimestamp) @@ -77,7 +87,8 @@ abstract class Mangalix : KeiSource() { } if (sortFilter?.ascending == true) result = result.reversed() - return result.toPage(page) + + return MangasPage(result.map { it.toSManga(baseUrl) }, hasNextPage = false) } override fun getFilterList(data: JsonElement?): FilterList = FilterList( @@ -189,19 +200,11 @@ abstract class Mangalix : KeiSource() { throw IOException("Manga catalog not found") } - private fun MangaDto.matchesQuery(query: String): Boolean { - val terms = query.trim().lowercase(Locale.ROOT).split(WHITESPACE_REGEX) + private fun MangaDto.matchesTerms(terms: List): Boolean { val searchable = "$title $author $slug".lowercase(Locale.ROOT) return terms.all(searchable::contains) } - private fun List.toPage(page: Int): MangasPage { - val start = ((page - 1).coerceAtLeast(0)) * PAGE_SIZE - if (start >= size) return MangasPage(emptyList(), false) - val end = (start + PAGE_SIZE).coerceAtMost(size) - return MangasPage(subList(start, end).map { it.toSManga(baseUrl) }, end < size) - } - private suspend fun readChapters(slug: String): List = client.get("$baseUrl/chapters.json.gz", archiveHeaders, ARCHIVE_CACHE_CONTROL).use { response -> JsonReader(InputStreamReader(GZIPInputStream(response.body.byteStream()), Charsets.UTF_8)).use { reader -> var chapters: List = emptyList() @@ -275,7 +278,6 @@ abstract class Mangalix : KeiSource() { } companion object { - private const val PAGE_SIZE = 16 private val ARCHIVE_CACHE_CONTROL = CacheControl.Builder().maxAge(30.minutes).build() private val CATALOG_START_REGEX = Regex("""\[\{id\s*:""") private val WHITESPACE_REGEX = Regex("""\s+""")