diff --git a/src/en/anchira/build.gradle b/src/en/anchira/build.gradle
index b040a87d8..20b793df3 100644
--- a/src/en/anchira/build.gradle
+++ b/src/en/anchira/build.gradle
@@ -1,7 +1,7 @@
 ext {
     extName = 'Anchira'
     extClass = '.Anchira'
-    extVersionCode = 9
+    extVersionCode = 10
     isNsfw = true
 }
 
diff --git a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/Anchira.kt b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/Anchira.kt
index ff26a17ff..180832c19 100644
--- a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/Anchira.kt
+++ b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/Anchira.kt
@@ -6,6 +6,7 @@ import android.content.SharedPreferences
 import androidx.preference.ListPreference
 import androidx.preference.PreferenceScreen
 import androidx.preference.SwitchPreferenceCompat
+import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.createChapter
 import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl
 import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags
 import eu.kanade.tachiyomi.network.GET
@@ -23,6 +24,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.json.Json
 import kotlinx.serialization.json.decodeFromStream
+import okhttp3.HttpUrl
 import okhttp3.HttpUrl.Companion.toHttpUrl
 import okhttp3.Interceptor
 import okhttp3.OkHttpClient
@@ -33,6 +35,8 @@ import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 import java.io.IOException
 import java.util.concurrent.TimeUnit
+import kotlin.math.ceil
+import kotlin.math.min
 
 class Anchira : HttpSource(), ConfigurableSource {
     override val name = "Anchira"
@@ -109,6 +113,23 @@ class Anchira : HttpSource(), ConfigurableSource {
             fetchMangaDetails(manga).map {
                 MangasPage(listOf(it), false)
             }
+        } else if (query.startsWith(SLUG_BUNDLE_PREFIX)) {
+            // bundle entries as chapters
+            val url = applyFilters(
+                page,
+                query.substringAfter(SLUG_BUNDLE_PREFIX),
+                filters,
+            ).removeAllQueryParameters("page")
+            if (
+                url.build().queryParameter("sort") == "4"
+            ) {
+                url.removeAllQueryParameters("sort")
+            }
+            val manga = SManga.create()
+                .apply { this.url = "?${url.build().query}" }
+            fetchMangaDetails(manga).map {
+                MangasPage(listOf(it), false)
+            }
         } else {
             // regular filtering without text search
             client.newCall(searchMangaRequest(page, query, filters))
@@ -116,29 +137,29 @@ class Anchira : HttpSource(), ConfigurableSource {
                 .map(::searchMangaParse)
         }
     }
-    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
+        GET(applyFilters(page, query, filters).build(), headers)
+
+    private fun applyFilters(page: Int, query: String, filters: FilterList): HttpUrl.Builder {
         val filterList = if (filters.isEmpty()) getFilterList() else filters
         val trendingFilter = filterList.findInstance<TrendingFilter>()
         val sortTrendingFilter = filters.findInstance<SortTrendingFilter>()
         var url = libraryUrl.toHttpUrl().newBuilder()
 
-        url.addQueryParameter("page", page.toString())
-
         if (trendingFilter?.state == true) {
             val interval = when (sortTrendingFilter?.state) {
                 1 -> "3"
                 else -> ""
             }
 
-            if (interval.isNotBlank()) url.addQueryParameter("interval", interval)
+            if (interval.isNotBlank()) url.setQueryParameter("interval", interval)
 
             url = url.toString().replace("library", "trending").toHttpUrl()
                 .newBuilder()
-
-            return GET(url.build(), headers)
         } else {
             if (query.isNotBlank()) {
-                url.addQueryParameter("s", query)
+                url.setQueryParameter("s", query)
             }
 
             filters.forEach { filter ->
@@ -154,7 +175,7 @@ class Anchira : HttpSource(), ConfigurableSource {
                             }
                         }
 
-                        if (sum > 0) url.addQueryParameter("cat", sum.toString())
+                        if (sum > 0) url.setQueryParameter("cat", sum.toString())
                     }
 
                     is SortFilter -> {
@@ -166,8 +187,8 @@ class Anchira : HttpSource(), ConfigurableSource {
                             else -> ""
                         }
 
-                        if (sort.isNotEmpty()) url.addQueryParameter("sort", sort)
-                        if (filter.state?.ascending == true) url.addQueryParameter("order", "1")
+                        if (sort.isNotEmpty()) url.setQueryParameter("sort", sort)
+                        if (filter.state?.ascending == true) url.setQueryParameter("order", "1")
                     }
 
                     is FavoritesFilter -> {
@@ -184,57 +205,103 @@ class Anchira : HttpSource(), ConfigurableSource {
                     else -> {}
                 }
             }
-
-            return GET(url.build(), headers)
         }
+
+        if (page > 1) {
+            url.setQueryParameter("page", page.toString())
+        }
+
+        return url
     }
 
     override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
 
     // Details
 
-    override fun mangaDetailsRequest(manga: SManga) =
-        GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
-
-    override fun mangaDetailsParse(response: Response): SManga {
-        val data = json.decodeFromString<Entry>(response.body.string())
-
-        return SManga.create().apply {
-            url = "/g/${data.id}/${data.key}"
-            title = data.title
-            thumbnail_url =
-                "$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
-            artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
-            author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
-            genre = prepareTags(data.tags, preferences.useTagGrouping)
-            update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
-            status = SManga.COMPLETED
+    override fun mangaDetailsRequest(manga: SManga): Request {
+        return if (manga.url.startsWith("?")) {
+            GET(libraryUrl + manga.url, headers)
+        } else {
+            GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
         }
     }
 
-    override fun getMangaUrl(manga: SManga) = if (preferences.openSource) {
-        val id = manga.url.split("/").reversed()[1].toInt()
-        anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
-    } else {
-        "$baseUrl${manga.url}"
+    override fun mangaDetailsParse(response: Response): SManga {
+        return if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
+            val manga = latestUpdatesParse(response).mangas.first()
+            val query = response.request.url.queryParameter("s")
+            val cleanTitle = CHAPTER_SUFFIX_RE.replace(manga.title, "").trim()
+            manga.apply {
+                url = "?${response.request.url.query}"
+                description = "Bundled from $query"
+                title = "[Bundle] $cleanTitle"
+                update_strategy = UpdateStrategy.ALWAYS_UPDATE
+            }
+        } else {
+            val data = json.decodeFromString<Entry>(response.body.string())
+
+            SManga.create().apply {
+                url = "/g/${data.id}/${data.key}"
+                title = data.title
+                thumbnail_url =
+                    "$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
+                artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
+                author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
+                genre = prepareTags(data.tags, preferences.useTagGrouping)
+                update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
+                status = SManga.COMPLETED
+            }
+        }
     }
 
+    override fun getMangaUrl(manga: SManga) =
+        if (preferences.openSource && !manga.url.startsWith("?")) {
+            val id = manga.url.split("/").reversed()[1].toInt()
+            anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
+        } else {
+            "$baseUrl${manga.url}"
+        }
+
     // Chapter
 
-    override fun chapterListRequest(manga: SManga) =
-        GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
+    override fun chapterListRequest(manga: SManga): Request {
+        return if (manga.url.startsWith("?")) {
+            GET(libraryUrl + manga.url, headers)
+        } else {
+            GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
+        }
+    }
 
     override fun chapterListParse(response: Response): List<SChapter> {
-        val data = json.decodeFromString<Entry>(response.body.string())
-
-        return listOf(
-            SChapter.create().apply {
-                url = "/g/${data.id}/${data.key}"
-                name = "Chapter"
-                date_upload = data.publishedAt * 1000
-                chapter_number = 1f
-            },
-        )
+        val chapterList = mutableListOf<SChapter>()
+        if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
+            var results = json.decodeFromString<LibraryResponse>(response.body.string())
+            val pages = min(5, ceil((results.total.toFloat() / results.limit)).toInt())
+            for (page in 1..pages) {
+                results.entries.forEach { data ->
+                    chapterList.add(
+                        createChapter(data, response, anchiraData),
+                    )
+                }
+                if (page < pages) {
+                    results = json.decodeFromString<LibraryResponse>(
+                        client.newCall(
+                            GET(
+                                response.request.url.newBuilder()
+                                    .setQueryParameter("page", (page + 1).toString()).build(),
+                                headers,
+                            ),
+                        ).execute().body.string(),
+                    )
+                }
+            }
+        } else {
+            val data = json.decodeFromString<Entry>(response.body.string())
+            chapterList.add(
+                createChapter(data, response, anchiraData),
+            )
+        }
+        return chapterList
     }
 
     override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${getPathFromUrl(chapter.url)}"
@@ -295,14 +362,16 @@ class Anchira : HttpSource(), ConfigurableSource {
         val openSourcePref = SwitchPreferenceCompat(screen.context).apply {
             key = OPEN_SOURCE_PREF
             title = "Open source website in WebView"
-            summary = "Enable to open the original source website of the gallery (if available) instead of Anchira."
+            summary =
+                "Enable to open the original source website of the gallery (if available) instead of Anchira."
             setDefaultValue(false)
         }
 
         val useTagGrouping = SwitchPreferenceCompat(screen.context).apply {
             key = USE_TAG_GROUPING
             title = "Group tags"
-            summary = "Enable to group tags together by artist, circle, parody, magazine and general tags"
+            summary =
+                "Enable to group tags together by artist, circle, parody, magazine and general tags"
             setDefaultValue(false)
         }
 
@@ -399,6 +468,7 @@ class Anchira : HttpSource(), ConfigurableSource {
 
     companion object {
         const val SLUG_SEARCH_PREFIX = "id:"
+        const val SLUG_BUNDLE_PREFIX = "bundle:"
         private const val IMAGE_QUALITY_PREF = "image_quality"
         private const val OPEN_SOURCE_PREF = "use_manga_source"
         private const val USE_TAG_GROUPING = "use_tag_grouping"
@@ -406,3 +476,5 @@ class Anchira : HttpSource(), ConfigurableSource {
             "https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json"
     }
 }
+
+val CHAPTER_SUFFIX_RE = Regex("(?<!20\\d\\d-)\\b[\\d.]{1,4}$")
diff --git a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraDto.kt b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraDto.kt
index 80da1ea39..0929b0c1b 100644
--- a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraDto.kt
+++ b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraDto.kt
@@ -3,15 +3,6 @@ package eu.kanade.tachiyomi.extension.en.anchira
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 
-@Serializable
-data class ListEntry(
-    val id: Int,
-    val key: String,
-    val title: String,
-    @SerialName("thumb_index") val thumbnailIndex: Int,
-    val tags: List<Tag> = emptyList(),
-)
-
 @Serializable
 data class Tag(
     var name: String,
@@ -20,7 +11,7 @@ data class Tag(
 
 @Serializable
 data class LibraryResponse(
-    val entries: List<ListEntry> = emptyList(),
+    val entries: List<Entry> = emptyList(),
     val total: Int,
     val page: Int,
     val limit: Int,
@@ -30,11 +21,12 @@ data class LibraryResponse(
 data class Entry(
     val id: Int,
     val key: String,
-    @SerialName("published_at") val publishedAt: Long,
+    @SerialName("published_at") val publishedAt: Long = 0L,
     val title: String,
-    @SerialName("thumb_index") val thumbnailIndex: Int,
+    @SerialName("thumb_index") val thumbnailIndex: Int = 1,
     val tags: List<Tag> = emptyList(),
     val url: String? = null,
+    val pages: Int = 1,
 )
 
 @Serializable
diff --git a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraHelper.kt b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraHelper.kt
index 868929646..5d50f9b85 100644
--- a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraHelper.kt
+++ b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraHelper.kt
@@ -1,5 +1,9 @@
 package eu.kanade.tachiyomi.extension.en.anchira
 
+import eu.kanade.tachiyomi.source.model.SChapter
+import okhttp3.Response
+import java.util.Locale
+
 object AnchiraHelper {
     fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}"
 
@@ -25,4 +29,31 @@ object AnchiraHelper {
             }
         }
         .joinToString(", ") { it }
+
+    fun createChapter(entry: Entry, response: Response, anchiraData: List<EntryKey>) =
+        SChapter.create().apply {
+            val ch =
+                CHAPTER_SUFFIX_RE.find(entry.title)?.value?.trim('.') ?: "1"
+            val source = anchiraData.find { it.id == entry.id }?.url
+                ?: response.request.url.toString()
+            url = "/g/${entry.id}/${entry.key}"
+            name = "$ch. ${entry.title.removeSuffix(" $ch")}"
+            date_upload = entry.publishedAt * 1000
+            chapter_number = ch.toFloat()
+            scanlator = buildString {
+                append(
+                    Regex("fakku|irodori|anchira").find(source)?.value.orEmpty()
+                        .replaceFirstChar {
+                            if (it.isLowerCase()) {
+                                it.titlecase(
+                                    Locale.getDefault(),
+                                )
+                            } else {
+                                it.toString()
+                            }
+                        },
+                )
+                append(" - ${entry.pages} pages")
+            }
+        }
 }
diff --git a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/XXTEA.kt b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/XXTEA.kt
deleted file mode 100644
index 894dfbe3c..000000000
--- a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/XXTEA.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-package eu.kanade.tachiyomi.extension.en.anchira
-
-object XXTEA {
-
-    private const val DELTA = -0x61c88647
-
-    @Suppress("NOTHING_TO_INLINE", "FunctionName")
-    private inline fun MX(sum: Int, y: Int, z: Int, p: Int, e: Int, k: IntArray): Int {
-        return (z.ushr(5) xor (y shl 2)) + (y.ushr(3) xor (z shl 4)) xor (sum xor y) + (k[p and 3 xor e] xor z)
-    }
-
-    private fun decrypt(data: ByteArray, key: ByteArray): ByteArray =
-        data.takeIf { it.isNotEmpty() }
-            ?.let {
-                decrypt(data.toIntArray(false), key.fixKey().toIntArray(false))
-                    .toByteArray(true)
-            } ?: data
-
-    fun decrypt(data: ByteArray, key: String): ByteArray? =
-        kotlin.runCatching { decrypt(data, key.toByteArray(Charsets.UTF_8)) }.getOrNull()
-
-    fun decryptToString(data: ByteArray, key: String): String? =
-        kotlin.runCatching { decrypt(data, key)?.toString(Charsets.UTF_8) }.getOrNull()
-
-    private fun decrypt(v: IntArray, k: IntArray): IntArray {
-        val n = v.size - 1
-
-        if (n < 1) {
-            return v
-        }
-        var p: Int
-        val q = 6 + 52 / (n + 1)
-        var z: Int
-        var y = v[0]
-        var sum = q * DELTA
-        var e: Int
-
-        while (sum != 0) {
-            e = sum.ushr(2) and 3
-            p = n
-            while (p > 0) {
-                z = v[p - 1]
-                v[p] -= MX(sum, y, z, p, e, k)
-                y = v[p]
-                p--
-            }
-            z = v[n]
-            v[0] -= MX(sum, y, z, p, e, k)
-            y = v[0]
-            sum -= DELTA
-        }
-        return v
-    }
-
-    private fun ByteArray.fixKey(): ByteArray {
-        if (size == 16) return this
-        val fixedKey = ByteArray(16)
-
-        if (size < 16) {
-            copyInto(fixedKey)
-        } else {
-            copyInto(fixedKey, endIndex = 16)
-        }
-        return fixedKey
-    }
-
-    private fun ByteArray.toIntArray(includeLength: Boolean): IntArray {
-        var n = if (size and 3 == 0) {
-            size.ushr(2)
-        } else {
-            size.ushr(2) + 1
-        }
-        val result: IntArray
-
-        if (includeLength) {
-            result = IntArray(n + 1)
-            result[n] = size
-        } else {
-            result = IntArray(n)
-        }
-        n = size
-        for (i in 0 until n) {
-            result[i.ushr(2)] =
-                result[i.ushr(2)] or (0x000000ff and this[i].toInt() shl (i and 3 shl 3))
-        }
-        return result
-    }
-
-    private fun IntArray.toByteArray(includeLength: Boolean): ByteArray? {
-        var n = size shl 2
-
-        if (includeLength) {
-            val m = this[size - 1]
-            n -= 4
-            if (m < n - 3 || m > n) {
-                return null
-            }
-            n = m
-        }
-        val result = ByteArray(n)
-
-        for (i in 0 until n) {
-            result[i] = this[i.ushr(2)].ushr(i and 3 shl 3).toByte()
-        }
-        return result
-    }
-}