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 - } -}