From b4969595c6bd5f102a267f7d5bc892684da43ad8 Mon Sep 17 00:00:00 2001 From: Charlie W Iskandar Date: Tue, 14 Jul 2026 22:56:49 +0700 Subject: [PATCH 01/21] Save current work before sparse update --- gradle/gradle-daemon-jvm.properties | 12 + lib-multisrc/pawchive/build.gradle.kts | 12 + .../pawchive/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4302 bytes .../pawchive/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2420 bytes .../pawchive/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5790 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 11021 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 18525 bytes .../kanade/tachiyomi/multisrc/pawchive/Dto.kt | 109 +++++ .../tachiyomi/multisrc/pawchive/Pawchive.kt | 409 ++++++++++++++++++ 9 files changed, 542 insertions(+) create mode 100644 gradle/gradle-daemon-jvm.properties create mode 100644 lib-multisrc/pawchive/build.gradle.kts create mode 100644 lib-multisrc/pawchive/res/mipmap-hdpi/ic_launcher.png create mode 100644 lib-multisrc/pawchive/res/mipmap-mdpi/ic_launcher.png create mode 100644 lib-multisrc/pawchive/res/mipmap-xhdpi/ic_launcher.png create mode 100644 lib-multisrc/pawchive/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 lib-multisrc/pawchive/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Dto.kt create mode 100644 lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 00000000000..6c1139ec06a --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,12 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect +toolchainVersion=21 diff --git a/lib-multisrc/pawchive/build.gradle.kts b/lib-multisrc/pawchive/build.gradle.kts new file mode 100644 index 00000000000..52d3166f7df --- /dev/null +++ b/lib-multisrc/pawchive/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(kei.plugins.multisrc) +} + +dependencies { + compileOnlyApi("com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.11") +} + +keiyoushi { + baseVersionCode = 24 + libVersion = "1.4" +} diff --git a/lib-multisrc/pawchive/res/mipmap-hdpi/ic_launcher.png b/lib-multisrc/pawchive/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..eb3bc86e34f81afb9f5fe0ca7f06dcc934efeaf7 GIT binary patch literal 4302 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 literal 0 HcmV?d00001 diff --git a/lib-multisrc/pawchive/res/mipmap-mdpi/ic_launcher.png b/lib-multisrc/pawchive/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..266648947fc8e657e24c5932af5b7b24cb20ca03 GIT binary patch literal 2420 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 Y4B%99mtjAN5n6#!lvS0fmNE_fAJZWuVgLXD literal 0 HcmV?d00001 diff --git a/lib-multisrc/pawchive/res/mipmap-xxxhdpi/ic_launcher.png b/lib-multisrc/pawchive/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c144355436abc98d64e4995590e72f7d18919825 GIT binary patch literal 18525 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 dateFormat.tryParse(updated.content) + else -> (updated.double * 1000).toLong() + } + + fun toSManga(avatarUrl: String) = SManga.create().apply { + url = "/$service/user/$id" // should be /server/ for Discord but will be filtered anyway + title = name + author = service.serviceName() + thumbnail_url = "$avatarUrl/icons/$service/$id" + description = Pawchive.PROMPT + initialized = true + } + + companion object { + private val dateFormat by lazy { getApiDateFormat() } + + fun String.serviceName() = when (this) { + "fanbox" -> "Pixiv Fanbox" + else -> replaceFirstChar { it.uppercase() } + } + } +} + +@Serializable +class PawchivePostDto( + private val id: String, + private val service: String, + private val user: String, + private val title: String, + private val added: String?, + private val published: String?, + private val edited: String?, + private val file: PawchiveFileDto, + private val attachments: List, +) { + val images: List + get() = buildList(attachments.size + 1) { + if (file.path != null) add(PawchiveAttachmentDto(file.name, file.path)) + addAll(attachments) + }.filter { + when (it.path.substringAfterLast('.').lowercase()) { + "png", "jpg", "gif", "jpeg", "webp" -> true + else -> false + } + }.distinctBy { it.path }.map { it.toString() } + + fun toSChapter() = SChapter.create().apply { + val postDate = dateFormat.tryParse(edited ?: published ?: added) + + url = "/$service/user/$user/post/$id" + date_upload = postDate + name = title.ifBlank { + val postDateString = when { + postDate != 0L -> chapterNameDateFormat.format(postDate) + else -> "unknown date" + } + + "Post from $postDateString" + } + chapter_number = -2f + } + + companion object { + val dateFormat by lazy { getApiDateFormat() } + val chapterNameDateFormat by lazy { getChapterNameDateFormat() } + } +} + +@Serializable +class PawchiveFileDto(val name: String? = null, val path: String? = null) + +// name might have ".jpe" extension for JPEG, path might have ".m4v" extension for MP4 +@Serializable +class PawchiveAttachmentDto(var name: String? = null, val path: String) { + override fun toString() = path + if (name != null) "?f=$name" else "" +} + +private fun getApiDateFormat() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH) + +private fun getChapterNameDateFormat() = SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss", Locale.ENGLISH) diff --git a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt b/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt new file mode 100644 index 00000000000..2f9494c2c9d --- /dev/null +++ b/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt @@ -0,0 +1,409 @@ +package eu.kanade.tachiyomi.multisrc.pawchive + +import android.app.Application +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.multisrc.pawchive.PawchiveCreatorDto.Companion.serviceName +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.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 keiyoushi.network.rateLimit +import keiyoushi.utils.getPreferences +import keiyoushi.utils.parseAs +import okhttp3.Cache +import okhttp3.CacheControl +import okhttp3.Request +import okhttp3.Response +import okhttp3.brotli.BrotliInterceptor +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File +import java.lang.Thread.sleep +import java.util.TimeZone +import kotlin.math.min +import kotlin.time.Duration.Companion.minutes + +abstract class Pawchive : + HttpSource(), + ConfigurableSource { + override val supportsLatest = true + + override val client = network.client.newBuilder() + .addInterceptor { chain -> + val request = chain.request() + + // 1. Existing API fix + val modifiedRequest = if (request.url.pathSegments.firstOrNull() == "api") { + request.newBuilder().header("Accept", "text/css").build() + } else { + request + } + + var response = chain.proceed(modifiedRequest) + + // 2. New Fallback Logic for Images + // If the response is a 404 Not Found AND the request was aiming for the full-size file server + if (response.code == 404 && request.url.toString().startsWith(fileUrl)) { + + // Check if the user has the fallback toggle enabled + if (preferences.getBoolean(FALLBACK_LOW_RES_IMG, true)) { + response.close() // Close the failed response to prevent memory leaks + + val imageUrl = request.url.toString() + val pathIndex = imageUrl.indexOf('/', 8) + + // Build the new thumbnail URL + val fallbackUrl = buildString { + append(imgUrl) + append("/thumbnail") + append(imageUrl.substring(pathIndex)) + } + + // Retry the request seamlessly with the new fallback URL + val fallbackRequest = request.newBuilder().url(fallbackUrl).build() + response = chain.proceed(fallbackRequest) + } + } + + response + } + .apply { + val index = networkInterceptors().indexOfFirst { it is BrotliInterceptor } + if (index >= 0) interceptors().add(networkInterceptors().removeAt(index)) + } + .cache( + Cache( + directory = File(Injekt.get().externalCacheDir, "network_cache_${name.lowercase()}"), + maxSize = 50L * 1024 * 1024, // 50 MiB + ), + ) + .rateLimit(1) + .build() + + private val creatorsClient = client.newBuilder() + .readTimeout(5.minutes) + .build() + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + private val preferences = getPreferences() + + private val apiPath = "api/v1" + + private val dataPath = "data" + + private val avatarUrl = baseUrl + + private val fileUrl = baseUrl.replace("//", "//file.") + + private val imgUrl = baseUrl.replace("//", "//img.") + + private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', avatarUrl) + + override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() + + override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() + + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() + + override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() + + override fun fetchPopularManga(page: Int): Observable = Observable.fromCallable { + searchMangas(page, sortBy = "pop" to "desc") + } + + override fun fetchLatestUpdates(page: Int): Observable = Observable.fromCallable { + searchMangas(page, sortBy = "lat" to "desc") + } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { + searchMangas(page, query, filters) + } + + private fun searchMangas(page: Int = 1, title: String = "", filters: FilterList? = null, sortBy: Pair = "" to ""): MangasPage { + var sort = sortBy + val typeIncluded: MutableList = mutableListOf() + val typeExcluded: MutableList = mutableListOf() + var fav: Boolean? = null + filters?.forEach { filter -> + when (filter) { + is SortFilter -> { + sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc" + } + + is TypeFilter -> { + filter.state.filter { state -> state.isIncluded() }.forEach { tri -> + typeIncluded.add(tri.value) + } + + filter.state.filter { state -> state.isExcluded() }.forEach { tri -> + typeExcluded.add(tri.value) + } + } + + is FavoritesFilter -> { + fav = when (filter.state[0].state) { + 0 -> null + 1 -> true + else -> false + } + } + + else -> {} + } + } + + val mangas = run { + val favorites = if (fav != null) { + val response = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute() + + if (response.isSuccessful) { + response.parseAs>().filterNot { it.service.lowercase() == "discord" } + } else { + response.close() + val message = if (response.code == 401) "You are not logged in" else "HTTP error ${response.code}" + throw Exception("Failed to fetch favorites: $message") + } + } else { + emptyList() + } + + val request = GET( + "$baseUrl/$apiPath/creators", + headers, + CacheControl.Builder().maxStale(30.minutes).build(), + ) + val response = creatorsClient.newCall(request).execute() + if (!response.isSuccessful) { + response.close() + throw Exception("HTTP error ${response.code}") + } + val allCreators = response.parseAs>().filterNot { it.service.lowercase() == "discord" } + allCreators.filter { + val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase()) + val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase()) + + val regularSearch = it.name.contains(title, true) + + val isFavorited = when (fav) { + true -> favorites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } } + false -> favorites.none { f -> f.id == it.id } + else -> true + } + + includeType && !excludeType && isFavorited && + regularSearch + } + } + + val sorted = when (sort.first) { + "pop" -> { + if (sort.second == "desc") { + mangas.sortedByDescending { it.favorited } + } else { + mangas.sortedBy { it.favorited } + } + } + + "tit" -> { + if (sort.second == "desc") { + mangas.sortedByDescending { it.name } + } else { + mangas.sortedBy { it.name } + } + } + + "new" -> { + if (sort.second == "desc") { + mangas.sortedByDescending { it.id } + } else { + mangas.sortedBy { it.id } + } + } + + "fav" -> { + if (fav != true) throw Exception("Please check 'Favorites Only' Filter") + if (sort.second == "desc") { + mangas.sortedByDescending { it.fav } + } else { + mangas.sortedBy { it.fav } + } + } + + else -> { + if (sort.second == "desc") { + mangas.sortedByDescending { it.updatedDate } + } else { + mangas.sortedBy { it.updatedDate } + } + } + } + val maxIndex = mangas.size + val fromIndex = (page - 1) * PAGE_CREATORS_LIMIT + val toIndex = min(maxIndex, fromIndex + PAGE_CREATORS_LIMIT) + + val final = sorted.subList(fromIndex, toIndex).map { it.toSManga(avatarUrl) } + return MangasPage(final, toIndex != maxIndex) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() + override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() + + override fun fetchMangaDetails(manga: SManga): Observable { + manga.thumbnail_url = manga.thumbnail_url!!.formatAvatarUrl() + return Observable.just(manga) + } + + override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException() + + override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url.replace("$apiPath/", "")}" + + override fun fetchChapterList(manga: SManga): Observable> = Observable.fromCallable { + PawchivePostDto.dateFormat.timeZone = when (manga.author) { + "Pixiv Fanbox" -> TimeZone.getTimeZone("GMT+09:00") + else -> TimeZone.getTimeZone("GMT") + } + val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!! + .toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT + var offset = 0 + var hasNextPage = true + val result = ArrayList() + while (offset < prefMaxPost && hasNextPage) { + val request = GET("$baseUrl/$apiPath${manga.url}/posts?o=$offset", headers) + val page: List = retry(request).parseAs() + page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) } + offset += PAGE_POST_LIMIT + hasNextPage = page.size == PAGE_POST_LIMIT + } + result + } + + private fun retry(request: Request): Response { + var code = 0 + repeat(5) { + val response = client.newCall(request).execute() + if (response.isSuccessful) return response + response.close() + code = response.code + if (code == 429) { + sleep(10000) + } + } + throw Exception("HTTP error $code") + } + + override fun chapterListParse(response: Response) = throw UnsupportedOperationException() + + override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl/$apiPath${chapter.url}", headers) + + override fun pageListParse(response: Response): List { + val postData: PawchivePostDto = response.parseAs() + return postData.images.mapIndexed { i, path -> Page(i, imageUrl = "$fileUrl/$dataPath$path") } + } + + override fun imageRequest(page: Page): Request { + val imageUrl = page.imageUrl!! + + if (!preferences.getBoolean(USE_LOW_RES_IMG, false)) return GET(imageUrl, headers) + + val index = imageUrl.indexOf('/', 8) + val url = buildString { + append(imgUrl) + append("/thumbnail") + append(imageUrl.substring(index)) + } + return GET(url, headers) + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + key = POST_PAGES_PREF + title = "Maximum posts to load" + summary = "Loading more posts costs more time and network traffic.\nCurrently: %s" + entryValues = Array(POST_PAGES_MAX) { (it + 1).toString() } + entries = Array(POST_PAGES_MAX) { "${(it + 1)} pages (${(it + 1) * PAGE_POST_LIMIT} posts)" } + setDefaultValue(POST_PAGES_DEFAULT) + }.let { screen.addPreference(it) } + + SwitchPreferenceCompat(screen.context).apply { + key = USE_LOW_RES_IMG + title = "Use low resolution images" + summary = "Reduce load time significantly. When turning off, clear chapter cache to remove cached low resolution images." + setDefaultValue(false) + }.let(screen::addPreference) + + SwitchPreferenceCompat(screen.context).apply { + key = FALLBACK_LOW_RES_IMG + title = "Fallback to low resolution images" + summary = "If a full size image fails to load (e.g. 404 Not Found), automatically try loading the low resolution thumbnail instead." + setDefaultValue(true) // Enabled by default + }.let(screen::addPreference) + } + + // Filters + + override fun getFilterList(): FilterList = FilterList( + SortFilter( + "Sort by", + Filter.Sort.Selection(0, false), + getSortsList, + ), + TypeFilter("Types", getTypes), + FavoritesFilter(), + ) + + open val getTypes: List = emptyList() + + open val getSortsList: List> = listOf( + Pair("Popularity", "pop"), + Pair("Date Indexed", "new"), + Pair("Date Updated", "lat"), + Pair("Alphabetical Order", "tit"), + Pair("Service", "serv"), + Pair("Date Favorited", "fav"), + ) + + internal open class TypeFilter(name: String, vals: List) : + Filter.Group( + name, + vals.map { TriFilter(it, it.lowercase()) }, + ) + + internal class FavoritesFilter : + Filter.Group( + "Favorites", + listOf(TriFilter("Favorites Only", "fav")), + ) + + internal open class TriFilter(name: String, val value: String) : Filter.TriState(name) + + internal open class SortFilter(name: String, selection: Selection, private val vals: List>) : Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) { + fun getValue() = vals[state!!.index].second + } + + companion object { + private const val PAGE_POST_LIMIT = 50 + private const val PAGE_CREATORS_LIMIT = 50 + const val PROMPT = "You can change how many posts to load in the extension preferences." + + private const val POST_PAGES_PREF = "POST_PAGES" + private const val POST_PAGES_DEFAULT = "1" + private const val POST_PAGES_MAX = 75 + + // private const val BASE_URL_PREF = "BASE_URL" + private const val USE_LOW_RES_IMG = "USE_LOW_RES_IMG" + + private const val FALLBACK_LOW_RES_IMG = "FALLBACK_LOW_RES_IMG" + } +} From 172d3e68997b9c72e11e218ce7cd918779f1d9aa Mon Sep 17 00:00:00 2001 From: Charlie W Iskandar Date: Tue, 14 Jul 2026 23:01:03 +0700 Subject: [PATCH 02/21] Save current work before sparse update --- src/all/pawchive/build.gradle.kts | 18 ++++++++++++++++++ .../extension/all/pawchive/Pawchive.kt | 12 ++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/all/pawchive/build.gradle.kts create mode 100644 src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt diff --git a/src/all/pawchive/build.gradle.kts b/src/all/pawchive/build.gradle.kts new file mode 100644 index 00000000000..1bdaa5b4313 --- /dev/null +++ b/src/all/pawchive/build.gradle.kts @@ -0,0 +1,18 @@ +import io.github.keiyoushi.gradle.api.ContentWarning + +plugins { + alias(kei.plugins.extension) +} + +keiyoushi { + name = "Pawchive" + versionCode = 1 + contentWarning = ContentWarning.NSFW + libVersion = "1.4" + theme = "pawchive" + + source { + lang = "all" + baseUrl = "https://pawchive.pw" + } +} diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt new file mode 100644 index 00000000000..31b8f47c887 --- /dev/null +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -0,0 +1,12 @@ +package eu.kanade.tachiyomi.extension.all.pawchive + +import eu.kanade.tachiyomi.multisrc.pawchive.Pawchive +import keiyoushi.annotation.Source + +@Source +abstract class Pawchive : Pawchive() { + override val getTypes = listOf( + "Patreon", + "Pixiv Fanbox", + ) +} \ No newline at end of file From 523e51e3a20d4e39e7e41f6db426d392c1ed8210 Mon Sep 17 00:00:00 2001 From: Charlie W Iskandar Date: Tue, 14 Jul 2026 23:19:44 +0700 Subject: [PATCH 03/21] Revert "MangaGeko, RageScans, Mangago, FlameComics: Split/add alt titles (#17605)" This reverts commit b60dada56a7218f4877ff7d7cbe60946a1c63427. --- src/en/flamecomics/build.gradle.kts | 2 +- .../extension/en/flamecomics/FlameComics.kt | 17 +------- src/en/mangago/build.gradle.kts | 2 +- .../tachiyomi/extension/en/mangago/Mangago.kt | 37 +---------------- src/en/mangarawclub/build.gradle.kts | 2 +- .../extension/en/mangarawclub/MangaRawClub.kt | 29 +++----------- src/en/ragescans/build.gradle.kts | 2 +- .../extension/en/ragescans/RageScans.kt | 40 ------------------- 8 files changed, 13 insertions(+), 118 deletions(-) diff --git a/src/en/flamecomics/build.gradle.kts b/src/en/flamecomics/build.gradle.kts index a51f0371fe1..ac0b2cb702d 100644 --- a/src/en/flamecomics/build.gradle.kts +++ b/src/en/flamecomics/build.gradle.kts @@ -6,7 +6,7 @@ plugins { keiyoushi { name = "Flame Comics" - versionCode = 50 + versionCode = 49 contentWarning = ContentWarning.SAFE libVersion = "1.4" diff --git a/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt b/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt index 785304300b9..20829a98e41 100644 --- a/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt +++ b/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt @@ -175,22 +175,8 @@ abstract class FlameComics : HttpSource() { json.decodeFromString(response.body.string()).pageProps.series title = seriesData.title thumbnail_url = thumbnailUrl(seriesData) - - val synopsis = seriesData.description + description = seriesData.description ?.let { Jsoup.parseBodyFragment(it).wholeText() } - .orEmpty() - val altNames = seriesData.altTitles.orEmpty() - .map { it.trim() } - .filter { it.isNotEmpty() } - - description = buildString { - append(synopsis) - if (altNames.isNotEmpty()) { - if (isNotEmpty()) append("\n\n") - append(ALT_NAME) - altNames.forEach { name -> append("\n- $name") } - } - }.takeIf { it.isNotEmpty() } genre = seriesData.tags?.let { tags -> (listOf(seriesData.type) + tags).joinToString() @@ -376,6 +362,5 @@ abstract class FlameComics : HttpSource() { private const val COMPOSED_SUFFIX = "?comp" private val MEDIA_TYPE = "image/png".toMediaType() private const val THUMBNAIL_FRAGMENT = "thumbnail" - private const val ALT_NAME = "Alternative Names:" } } diff --git a/src/en/mangago/build.gradle.kts b/src/en/mangago/build.gradle.kts index aef912b99e1..06b540a54d6 100644 --- a/src/en/mangago/build.gradle.kts +++ b/src/en/mangago/build.gradle.kts @@ -6,7 +6,7 @@ plugins { keiyoushi { name = "Mangago" - versionCode = 37 + versionCode = 36 contentWarning = ContentWarning.MIXED libVersion = "1.4" diff --git a/src/en/mangago/src/eu/kanade/tachiyomi/extension/en/mangago/Mangago.kt b/src/en/mangago/src/eu/kanade/tachiyomi/extension/en/mangago/Mangago.kt index 477495f2a37..ee8b7b9366c 100644 --- a/src/en/mangago/src/eu/kanade/tachiyomi/extension/en/mangago/Mangago.kt +++ b/src/en/mangago/src/eu/kanade/tachiyomi/extension/en/mangago/Mangago.kt @@ -186,25 +186,10 @@ abstract class Mangago : description = info.selectFirst(".manga_summary")?.let { summary: Element -> summary.selectFirst("font")?.remove() summary.text() - }?.takeIf { !it.equals("not found...", ignoreCase = true) } - + } info.select(".manga_info li, .manga_right tr").forEach { el -> when (el.selectFirst("b, label")?.text()?.lowercase()) { - "alternative:" -> { - val labelText = el.selectFirst("b, label")?.text().orEmpty() - val raw = el.text().removePrefix(labelText).trim() - val altNames = parseAltNames(raw) - - if (altNames.isNotEmpty()) { - description = buildString { - append(description.orEmpty()) - if (isNotEmpty()) append("\n\n") - append(ALT_NAME_PREFIX) - append("\n") - altNames.joinTo(this, "\n") { "- $it" } - } - } - } + "alternative:" -> description = listOfNotNull(description, el.text()).joinToString("\n\n") "status:" -> status = when (el.selectFirst("span")?.text()?.lowercase()) { "ongoing" -> SManga.ONGOING @@ -221,21 +206,6 @@ abstract class Mangago : } } - private fun parseAltNames(raw: String): List { - val separator = if (ALT_NAME_SLASH_SEMICOLON_REGEX.containsMatchIn(raw)) { - ALT_NAME_SLASH_SEMICOLON_REGEX - } else { - ALT_NAME_COMMA_REGEX - } - - return raw.split(separator) - .map { it.trim() } - .filter { - it.isNotEmpty() && - !it.equals("None", ignoreCase = true) - } - } - private fun mangaFromElement(element: Element): SManga? = SManga.create().apply { val linkElement = element.selectFirst(".thm-effect") ?: return null @@ -640,8 +610,5 @@ abstract class Mangago : companion object { private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION" private const val PREF_KEY_CUSTOM_UA = "pref_key_custom_ua_" - private const val ALT_NAME_PREFIX = "Alternative Names:" - private val ALT_NAME_SLASH_SEMICOLON_REGEX = Regex("[/;]") - private val ALT_NAME_COMMA_REGEX = Regex(",") } } diff --git a/src/en/mangarawclub/build.gradle.kts b/src/en/mangarawclub/build.gradle.kts index c46ba09336b..feda06ea7c0 100644 --- a/src/en/mangarawclub/build.gradle.kts +++ b/src/en/mangarawclub/build.gradle.kts @@ -6,7 +6,7 @@ plugins { keiyoushi { name = "MangaGeko" - versionCode = 33 + versionCode = 32 contentWarning = ContentWarning.MIXED libVersion = "1.4" diff --git a/src/en/mangarawclub/src/eu/kanade/tachiyomi/extension/en/mangarawclub/MangaRawClub.kt b/src/en/mangarawclub/src/eu/kanade/tachiyomi/extension/en/mangarawclub/MangaRawClub.kt index d2d55d4fa34..95f86715647 100644 --- a/src/en/mangarawclub/src/eu/kanade/tachiyomi/extension/en/mangarawclub/MangaRawClub.kt +++ b/src/en/mangarawclub/src/eu/kanade/tachiyomi/extension/en/mangarawclub/MangaRawClub.kt @@ -165,11 +165,11 @@ abstract class MangaRawClub : document.selectFirst(".description")?.text()?.substringAfter("Summary is")?.let { append(it) } - - parseAltNames(document.selectFirst(".alternative-title")?.ownText())?.let { altNames -> - if (isNotEmpty()) append("\n\n") - append(ALT_NAME) - altNames.forEach { name -> append("\n- $name") } + document.selectFirst(".alternative-title")?.ownText()?.takeIf { it.isNotEmpty() }?.let { + append("\n\n$ALT_NAME") + it.split(",").filter { t -> t.isNotEmpty() && t.trim().lowercase() != "updating" }.forEach { name -> + append("\n- ${name.trim()}") + } } } @@ -190,21 +190,6 @@ abstract class MangaRawClub : } } - private fun parseAltNames(raw: String?): List? { - if (raw.isNullOrEmpty()) return null - - val separator = if (ALT_NAME_BULLET_SEMICOLON_REGEX.containsMatchIn(raw)) { - ALT_NAME_BULLET_SEMICOLON_REGEX - } else { - ALT_NAME_COMMA_REGEX - } - - return raw.split(separator) - .map { it.trim() } - .filter { it.isNotEmpty() && it.lowercase() != "updating" } - .takeIf { it.isNotEmpty() } - } - // ============================= Chapters ============================== override fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url + "all-chapters/", headers) @@ -249,9 +234,7 @@ abstract class MangaRawClub : } companion object { - private const val ALT_NAME = "Alternative Names:" + private const val ALT_NAME = "Alternative Name:" private const val PREF_HIDE_NSFW = "pref_hide_nsfw" - private val ALT_NAME_BULLET_SEMICOLON_REGEX = Regex("[•;]") - private val ALT_NAME_COMMA_REGEX = Regex(",") } } diff --git a/src/en/ragescans/build.gradle.kts b/src/en/ragescans/build.gradle.kts index fbb46fd23e7..fdc73e909ed 100644 --- a/src/en/ragescans/build.gradle.kts +++ b/src/en/ragescans/build.gradle.kts @@ -6,7 +6,7 @@ plugins { keiyoushi { name = "Rage Scans" - versionCode = 2 + versionCode = 1 contentWarning = ContentWarning.SAFE libVersion = "1.4" theme = "mangathemesia" diff --git a/src/en/ragescans/src/eu/kanade/tachiyomi/extension/en/ragescans/RageScans.kt b/src/en/ragescans/src/eu/kanade/tachiyomi/extension/en/ragescans/RageScans.kt index 0a78ff9534f..f363179975a 100644 --- a/src/en/ragescans/src/eu/kanade/tachiyomi/extension/en/ragescans/RageScans.kt +++ b/src/en/ragescans/src/eu/kanade/tachiyomi/extension/en/ragescans/RageScans.kt @@ -1,49 +1,9 @@ package eu.kanade.tachiyomi.extension.en.ragescans import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia -import eu.kanade.tachiyomi.source.model.SManga import keiyoushi.annotation.Source -import org.jsoup.nodes.Document @Source abstract class RageScans : MangaThemesia() { override fun chapterListSelector() = "li:has(.chbox .eph-num):not(:has([data-bs-target='#lockedChapterModal']))" - - override fun mangaDetailsParse(document: Document): SManga = super.mangaDetailsParse(document).apply { - val baseDescription = description - ?.substringBefore(altNamePrefix) - ?.trim() - .orEmpty() - - val altNames = document.selectFirst(seriesAltNameSelector)?.ownText() - ?.let(::parseAltNames) - .orEmpty() - - description = buildString { - append(baseDescription) - if (altNames.isNotEmpty()) { - if (isNotEmpty()) append("\n\n") - append(altNamePrefix.trim()) - append("\n") - altNames.joinTo(this, "\n") { "- $it" } - } - }.takeIf { it.isNotEmpty() } - } - - private fun parseAltNames(raw: String): List { - val separator = if (ALT_NAME_SLASH_SEMICOLON_REGEX.containsMatchIn(raw)) { - ALT_NAME_SLASH_SEMICOLON_REGEX - } else { - ALT_NAME_COMMA_REGEX - } - - return raw.split(separator) - .map { it.trim() } - .filter { it.isNotEmpty() && !it.equals("Unknown", ignoreCase = true) } - } - - companion object { - private val ALT_NAME_SLASH_SEMICOLON_REGEX = Regex("[/;]") - private val ALT_NAME_COMMA_REGEX = Regex(",") - } } From 32037b905bb71c2b1a5161ad6622be605f37c216 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:50:50 +0700 Subject: [PATCH 04/21] Reapply "MangaGeko, RageScans, Mangago, FlameComics: Split/add alt titles (#17605)" This reverts commit 523e51e3a20d4e39e7e41f6db426d392c1ed8210. --- src/en/flamecomics/build.gradle.kts | 2 +- .../extension/en/flamecomics/FlameComics.kt | 17 +++++++- src/en/mangago/build.gradle.kts | 2 +- .../tachiyomi/extension/en/mangago/Mangago.kt | 37 ++++++++++++++++- src/en/mangarawclub/build.gradle.kts | 2 +- .../extension/en/mangarawclub/MangaRawClub.kt | 29 +++++++++++--- src/en/ragescans/build.gradle.kts | 2 +- .../extension/en/ragescans/RageScans.kt | 40 +++++++++++++++++++ 8 files changed, 118 insertions(+), 13 deletions(-) diff --git a/src/en/flamecomics/build.gradle.kts b/src/en/flamecomics/build.gradle.kts index ac0b2cb702d..a51f0371fe1 100644 --- a/src/en/flamecomics/build.gradle.kts +++ b/src/en/flamecomics/build.gradle.kts @@ -6,7 +6,7 @@ plugins { keiyoushi { name = "Flame Comics" - versionCode = 49 + versionCode = 50 contentWarning = ContentWarning.SAFE libVersion = "1.4" diff --git a/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt b/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt index 20829a98e41..785304300b9 100644 --- a/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt +++ b/src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt @@ -175,8 +175,22 @@ abstract class FlameComics : HttpSource() { json.decodeFromString(response.body.string()).pageProps.series title = seriesData.title thumbnail_url = thumbnailUrl(seriesData) - description = seriesData.description + + val synopsis = seriesData.description ?.let { Jsoup.parseBodyFragment(it).wholeText() } + .orEmpty() + val altNames = seriesData.altTitles.orEmpty() + .map { it.trim() } + .filter { it.isNotEmpty() } + + description = buildString { + append(synopsis) + if (altNames.isNotEmpty()) { + if (isNotEmpty()) append("\n\n") + append(ALT_NAME) + altNames.forEach { name -> append("\n- $name") } + } + }.takeIf { it.isNotEmpty() } genre = seriesData.tags?.let { tags -> (listOf(seriesData.type) + tags).joinToString() @@ -362,5 +376,6 @@ abstract class FlameComics : HttpSource() { private const val COMPOSED_SUFFIX = "?comp" private val MEDIA_TYPE = "image/png".toMediaType() private const val THUMBNAIL_FRAGMENT = "thumbnail" + private const val ALT_NAME = "Alternative Names:" } } diff --git a/src/en/mangago/build.gradle.kts b/src/en/mangago/build.gradle.kts index 06b540a54d6..aef912b99e1 100644 --- a/src/en/mangago/build.gradle.kts +++ b/src/en/mangago/build.gradle.kts @@ -6,7 +6,7 @@ plugins { keiyoushi { name = "Mangago" - versionCode = 36 + versionCode = 37 contentWarning = ContentWarning.MIXED libVersion = "1.4" diff --git a/src/en/mangago/src/eu/kanade/tachiyomi/extension/en/mangago/Mangago.kt b/src/en/mangago/src/eu/kanade/tachiyomi/extension/en/mangago/Mangago.kt index ee8b7b9366c..477495f2a37 100644 --- a/src/en/mangago/src/eu/kanade/tachiyomi/extension/en/mangago/Mangago.kt +++ b/src/en/mangago/src/eu/kanade/tachiyomi/extension/en/mangago/Mangago.kt @@ -186,10 +186,25 @@ abstract class Mangago : description = info.selectFirst(".manga_summary")?.let { summary: Element -> summary.selectFirst("font")?.remove() summary.text() - } + }?.takeIf { !it.equals("not found...", ignoreCase = true) } + info.select(".manga_info li, .manga_right tr").forEach { el -> when (el.selectFirst("b, label")?.text()?.lowercase()) { - "alternative:" -> description = listOfNotNull(description, el.text()).joinToString("\n\n") + "alternative:" -> { + val labelText = el.selectFirst("b, label")?.text().orEmpty() + val raw = el.text().removePrefix(labelText).trim() + val altNames = parseAltNames(raw) + + if (altNames.isNotEmpty()) { + description = buildString { + append(description.orEmpty()) + if (isNotEmpty()) append("\n\n") + append(ALT_NAME_PREFIX) + append("\n") + altNames.joinTo(this, "\n") { "- $it" } + } + } + } "status:" -> status = when (el.selectFirst("span")?.text()?.lowercase()) { "ongoing" -> SManga.ONGOING @@ -206,6 +221,21 @@ abstract class Mangago : } } + private fun parseAltNames(raw: String): List { + val separator = if (ALT_NAME_SLASH_SEMICOLON_REGEX.containsMatchIn(raw)) { + ALT_NAME_SLASH_SEMICOLON_REGEX + } else { + ALT_NAME_COMMA_REGEX + } + + return raw.split(separator) + .map { it.trim() } + .filter { + it.isNotEmpty() && + !it.equals("None", ignoreCase = true) + } + } + private fun mangaFromElement(element: Element): SManga? = SManga.create().apply { val linkElement = element.selectFirst(".thm-effect") ?: return null @@ -610,5 +640,8 @@ abstract class Mangago : companion object { private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION" private const val PREF_KEY_CUSTOM_UA = "pref_key_custom_ua_" + private const val ALT_NAME_PREFIX = "Alternative Names:" + private val ALT_NAME_SLASH_SEMICOLON_REGEX = Regex("[/;]") + private val ALT_NAME_COMMA_REGEX = Regex(",") } } diff --git a/src/en/mangarawclub/build.gradle.kts b/src/en/mangarawclub/build.gradle.kts index feda06ea7c0..c46ba09336b 100644 --- a/src/en/mangarawclub/build.gradle.kts +++ b/src/en/mangarawclub/build.gradle.kts @@ -6,7 +6,7 @@ plugins { keiyoushi { name = "MangaGeko" - versionCode = 32 + versionCode = 33 contentWarning = ContentWarning.MIXED libVersion = "1.4" diff --git a/src/en/mangarawclub/src/eu/kanade/tachiyomi/extension/en/mangarawclub/MangaRawClub.kt b/src/en/mangarawclub/src/eu/kanade/tachiyomi/extension/en/mangarawclub/MangaRawClub.kt index 95f86715647..d2d55d4fa34 100644 --- a/src/en/mangarawclub/src/eu/kanade/tachiyomi/extension/en/mangarawclub/MangaRawClub.kt +++ b/src/en/mangarawclub/src/eu/kanade/tachiyomi/extension/en/mangarawclub/MangaRawClub.kt @@ -165,11 +165,11 @@ abstract class MangaRawClub : document.selectFirst(".description")?.text()?.substringAfter("Summary is")?.let { append(it) } - document.selectFirst(".alternative-title")?.ownText()?.takeIf { it.isNotEmpty() }?.let { - append("\n\n$ALT_NAME") - it.split(",").filter { t -> t.isNotEmpty() && t.trim().lowercase() != "updating" }.forEach { name -> - append("\n- ${name.trim()}") - } + + parseAltNames(document.selectFirst(".alternative-title")?.ownText())?.let { altNames -> + if (isNotEmpty()) append("\n\n") + append(ALT_NAME) + altNames.forEach { name -> append("\n- $name") } } } @@ -190,6 +190,21 @@ abstract class MangaRawClub : } } + private fun parseAltNames(raw: String?): List? { + if (raw.isNullOrEmpty()) return null + + val separator = if (ALT_NAME_BULLET_SEMICOLON_REGEX.containsMatchIn(raw)) { + ALT_NAME_BULLET_SEMICOLON_REGEX + } else { + ALT_NAME_COMMA_REGEX + } + + return raw.split(separator) + .map { it.trim() } + .filter { it.isNotEmpty() && it.lowercase() != "updating" } + .takeIf { it.isNotEmpty() } + } + // ============================= Chapters ============================== override fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url + "all-chapters/", headers) @@ -234,7 +249,9 @@ abstract class MangaRawClub : } companion object { - private const val ALT_NAME = "Alternative Name:" + private const val ALT_NAME = "Alternative Names:" private const val PREF_HIDE_NSFW = "pref_hide_nsfw" + private val ALT_NAME_BULLET_SEMICOLON_REGEX = Regex("[•;]") + private val ALT_NAME_COMMA_REGEX = Regex(",") } } diff --git a/src/en/ragescans/build.gradle.kts b/src/en/ragescans/build.gradle.kts index fdc73e909ed..fbb46fd23e7 100644 --- a/src/en/ragescans/build.gradle.kts +++ b/src/en/ragescans/build.gradle.kts @@ -6,7 +6,7 @@ plugins { keiyoushi { name = "Rage Scans" - versionCode = 1 + versionCode = 2 contentWarning = ContentWarning.SAFE libVersion = "1.4" theme = "mangathemesia" diff --git a/src/en/ragescans/src/eu/kanade/tachiyomi/extension/en/ragescans/RageScans.kt b/src/en/ragescans/src/eu/kanade/tachiyomi/extension/en/ragescans/RageScans.kt index f363179975a..0a78ff9534f 100644 --- a/src/en/ragescans/src/eu/kanade/tachiyomi/extension/en/ragescans/RageScans.kt +++ b/src/en/ragescans/src/eu/kanade/tachiyomi/extension/en/ragescans/RageScans.kt @@ -1,9 +1,49 @@ package eu.kanade.tachiyomi.extension.en.ragescans import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia +import eu.kanade.tachiyomi.source.model.SManga import keiyoushi.annotation.Source +import org.jsoup.nodes.Document @Source abstract class RageScans : MangaThemesia() { override fun chapterListSelector() = "li:has(.chbox .eph-num):not(:has([data-bs-target='#lockedChapterModal']))" + + override fun mangaDetailsParse(document: Document): SManga = super.mangaDetailsParse(document).apply { + val baseDescription = description + ?.substringBefore(altNamePrefix) + ?.trim() + .orEmpty() + + val altNames = document.selectFirst(seriesAltNameSelector)?.ownText() + ?.let(::parseAltNames) + .orEmpty() + + description = buildString { + append(baseDescription) + if (altNames.isNotEmpty()) { + if (isNotEmpty()) append("\n\n") + append(altNamePrefix.trim()) + append("\n") + altNames.joinTo(this, "\n") { "- $it" } + } + }.takeIf { it.isNotEmpty() } + } + + private fun parseAltNames(raw: String): List { + val separator = if (ALT_NAME_SLASH_SEMICOLON_REGEX.containsMatchIn(raw)) { + ALT_NAME_SLASH_SEMICOLON_REGEX + } else { + ALT_NAME_COMMA_REGEX + } + + return raw.split(separator) + .map { it.trim() } + .filter { it.isNotEmpty() && !it.equals("Unknown", ignoreCase = true) } + } + + companion object { + private val ALT_NAME_SLASH_SEMICOLON_REGEX = Regex("[/;]") + private val ALT_NAME_COMMA_REGEX = Regex(",") + } } From 2d43878d2ee025ef8d8581c800b2e6ac8a7b02e4 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:52:32 +0700 Subject: [PATCH 05/21] Delete gradle/gradle-daemon-jvm.properties --- gradle/gradle-daemon-jvm.properties | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 gradle/gradle-daemon-jvm.properties diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties deleted file mode 100644 index 6c1139ec06a..00000000000 --- a/gradle/gradle-daemon-jvm.properties +++ /dev/null @@ -1,12 +0,0 @@ -#This file is generated by updateDaemonJvm -toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect -toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect -toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect -toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect -toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect -toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect -toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect -toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect -toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect -toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect -toolchainVersion=21 From 716cd88fed66631bf3a51ca22ea3fbd382937abd Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:17:03 +0700 Subject: [PATCH 06/21] Update lib-multisrc/pawchive/build.gradle.kts Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- lib-multisrc/pawchive/build.gradle.kts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib-multisrc/pawchive/build.gradle.kts b/lib-multisrc/pawchive/build.gradle.kts index 52d3166f7df..571c891c913 100644 --- a/lib-multisrc/pawchive/build.gradle.kts +++ b/lib-multisrc/pawchive/build.gradle.kts @@ -2,10 +2,6 @@ plugins { alias(kei.plugins.multisrc) } -dependencies { - compileOnlyApi("com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.11") -} - keiyoushi { baseVersionCode = 24 libVersion = "1.4" From 710e95ae4b231253ea33a5fbb19a92246ec6ba2b Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:17:10 +0700 Subject: [PATCH 07/21] Update lib-multisrc/pawchive/build.gradle.kts Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- lib-multisrc/pawchive/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-multisrc/pawchive/build.gradle.kts b/lib-multisrc/pawchive/build.gradle.kts index 571c891c913..9bdd8fb9ff7 100644 --- a/lib-multisrc/pawchive/build.gradle.kts +++ b/lib-multisrc/pawchive/build.gradle.kts @@ -3,6 +3,6 @@ plugins { } keiyoushi { - baseVersionCode = 24 + baseVersionCode = 1 libVersion = "1.4" } From c1c9620eb992555306b9e137ba6f644bcfa7d3ae Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:23:20 +0700 Subject: [PATCH 08/21] Update lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- .../src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt b/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt index 2f9494c2c9d..4d94b08c670 100644 --- a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt +++ b/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt @@ -75,10 +75,6 @@ abstract class Pawchive : response } - .apply { - val index = networkInterceptors().indexOfFirst { it is BrotliInterceptor } - if (index >= 0) interceptors().add(networkInterceptors().removeAt(index)) - } .cache( Cache( directory = File(Injekt.get().externalCacheDir, "network_cache_${name.lowercase()}"), From 3a79bf880732a9a0aaaf030efbf5d5be66fc0f47 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:25:15 +0700 Subject: [PATCH 09/21] Update lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- .../src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt b/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt index 4d94b08c670..530d8d64adf 100644 --- a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt +++ b/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt @@ -77,7 +77,7 @@ abstract class Pawchive : } .cache( Cache( - directory = File(Injekt.get().externalCacheDir, "network_cache_${name.lowercase()}"), + directory = File(applicationContext.externalCacheDir, "network_cache_${name.lowercase()}"), maxSize = 50L * 1024 * 1024, // 50 MiB ), ) From 8364f2fd8ef599e21cf722ca1cb286c4ff0368db Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:56:39 +0700 Subject: [PATCH 10/21] Delete lib-multisrc/pawchive directory --- lib-multisrc/pawchive/build.gradle.kts | 8 - .../pawchive/res/mipmap-hdpi/ic_launcher.png | Bin 4302 -> 0 bytes .../pawchive/res/mipmap-mdpi/ic_launcher.png | Bin 2420 -> 0 bytes .../pawchive/res/mipmap-xhdpi/ic_launcher.png | Bin 5790 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 11021 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 18525 -> 0 bytes .../kanade/tachiyomi/multisrc/pawchive/Dto.kt | 109 ----- .../tachiyomi/multisrc/pawchive/Pawchive.kt | 405 ------------------ 8 files changed, 522 deletions(-) delete mode 100644 lib-multisrc/pawchive/build.gradle.kts delete mode 100644 lib-multisrc/pawchive/res/mipmap-hdpi/ic_launcher.png delete mode 100644 lib-multisrc/pawchive/res/mipmap-mdpi/ic_launcher.png delete mode 100644 lib-multisrc/pawchive/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 lib-multisrc/pawchive/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 lib-multisrc/pawchive/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Dto.kt delete mode 100644 lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt diff --git a/lib-multisrc/pawchive/build.gradle.kts b/lib-multisrc/pawchive/build.gradle.kts deleted file mode 100644 index 9bdd8fb9ff7..00000000000 --- a/lib-multisrc/pawchive/build.gradle.kts +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - alias(kei.plugins.multisrc) -} - -keiyoushi { - baseVersionCode = 1 - libVersion = "1.4" -} diff --git a/lib-multisrc/pawchive/res/mipmap-hdpi/ic_launcher.png b/lib-multisrc/pawchive/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index eb3bc86e34f81afb9f5fe0ca7f06dcc934efeaf7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4302 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 diff --git a/lib-multisrc/pawchive/res/mipmap-mdpi/ic_launcher.png b/lib-multisrc/pawchive/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 266648947fc8e657e24c5932af5b7b24cb20ca03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2420 zcmV-)35)iLP)_esH}1;6e`{FN(l7c&GM>otWoZEbay^E;b4W4*C;k|9Sxjbii>EP!lQ zJRZ-dLzo1{k{cQ4FF2WB=P-+5=JGUUnYOXE0umJ_lbUcOR#zNFrmlWssOs~dOs5{R z_lHk7r{Gd-T6F_%y*7-@3_cCjuu)HD?aZ9U+6Gz+-~hr5@cTjWCWh9;(O!Ak4BWf< zi@0TRH8$RSJ=U*WAwx&IO1hu9#tLfzW-x^irY#B*u5oOuwr$( z$y$J~KQSRWlic{T3kuZ!F1}(8sYGnJs1S~$?+M4zHajyD>FLSovNAGIU33j@s;&g@ z6`bcUnbDs>R$GY`tEZR8r>r}?hR~SDJ-+b^*l@#2e0Rg=uqs@c$S`ll^n_1`x#T&H zczh6!<6yPi7kg{Jif^r7i*0LGVfUSzQ8-IqF~Grie)a++gGs~5xow0}2gTD_SF;?s z*;x=h0tcV}L=AGXGSyYcl-1=p#_75pyw(vxzw2Uc%~EVxeG{hU<*IJSagdRoE?1*a z#wtpzeVggt)U&k!3O0aO2?ZJPxpH$>6owkr!XW>z!|Tt??Oa z0YIu~Jt5SOex$n_aaKsiv*b^_(Mm?$to#4|4}5R`LHw(sQE}Os8JLoj%}eNIS2Rwx zw&7QQ`I~Am2c3~D`+~3*zyyRpm~DbwZ~p+Ed8t8(VVnGOFE^p{?EmFr$X+5TATLI7 zeFHAq&UCS+ywii@O((ENwh3eX zarFJ%PwEfjc;g8iYkUPi`2AzJ=XVF?hEKhDO?=QYmkrheIGFGcV4E;pOrA6kKK^HX zdDjED@8BWy^!6eOk^)~(C z<}ab9tVG?Uj-wxo5UXNknb#~?q^^$KG>YP<#-6+H#3z?60du6wi>zqYV^D#;-!VWoE(&1IhQVGTyinKvG!A# zlA9y-cw*TjW75;!Qx2ns!8#%+nxp(2_xX<7Z$q|xGO&+O{d5du<&7)#p-Mj77<0?w zu=?bXrB>bK=+HK{yg^sGbYZG7%E?*)$1tHEz<6@R{5iA18<5>>r%WfpxZ0-A zaZC%Cb?L{DDSN$Fo?fb-ClCD4yIF`E$!P~t?8u_hxQmxJUWmOr=CW0TRYr%935TV_^q^S>-HhieU@S4hP8lT778?Idc764Xr+(N}=+W_+5D>zBU`cqQ5A zk70W}uoe(ZzzGceeP`wWo+n#darEUz9D4Sc`mWje$N_ZsM0J>~x-LU5lrV;8E@`U6 z?<-=QCu;#r>PKqMA#at3)RTyPqEk3}4qj|Nh1$Abqot#hgu;-VxyRePx+RV+A>`m3 zVkUH03mBd;DVlxWP7pD&vPnJC)^QYD`_Ml8QMOu(eCMB%n|9Y@hjC7BT#l!3g8oD? zLIGNc$dmk9VXA2MOP6<04B<6ZA-(b^BER#u{cI2Je(;xQINhSU{c(zz;Aw;c;M*Qr zEGOnxf%@s4tvOvF>9MpKH<_>&poPf#D{WdVpsns>QK}RC8IC?NTAMMG1#1EFZQwmd zScDcE6h#3+ZnQSXOaRoBTs1F|tpL}JpL;wghVh0omK+gm8;*9e5Eo67>zp9v2o}H! z`y*{_JKcf)`oY1$m_YtWk1NpkBQ8%BVi+g9pPPs;Z~K@#;MTttiR={R{QHv{#nnf! z0I~yeYVC|fcC^3Q8Wxmyw7qFXq`j>?h|Z4o^2tP`qdk1)?am$4B%n4i$-`CvD~w4< zhv2lJ`2vF)sD+xSP2?mGTLJ84pes(VfH%*D1T|0#HHn(U`@FGk%kdCXS;x zBSzyElxRdz^J$DbZUi?_6hTBq=x(|jXu5g5zB&KB?^fNqRrTt=Ud?B}ul~CCob#V^ zZ=G|iUcGv+J4Jo3Ud=#X0eUt4M12M5D?l%2pqGd5I|KR((90R<<>C9zfW89sat3;N z_>P_dg6G`_A~OfnkypM2z<}@p3VFaHM)6l1$N_Ruzlo= zPCDgVSI<4}_?5F}&)GTanAz!>AsmyQF)N7aGiRkuRLq!}4q*C>bcKiM+*g=3eR_IY zg^zOXmseDzHKtChNSipid|KMXl&R%u4@XZ;9|d?gIz4$x5Jw(0C9QG9Qa^ zU0q#NRb5To22fQM)KyhgS@xh@ZQD5};!3bhwps_7<5zj8qAIC#Xc5EUR#pamDl02v zW7#(NR&p%X7k$2I)4?CDUw2)<{sUg0Tv0Keh@6YyQUJjLQ{%^v-@Uc1{qd@*%Cfe$ zHdYa3G8x&LJ6qRTnPlxL^3|Ex`T7cEA#xTf56H=u%F42)hQ`N{E2bj1IlP#`8%W&5 zZU_KQ(Ox6>y{x9Twk(#(2K%5`mAGMm04byhSObC(d5{C1L_&1jm#mi%eyp0UTgwKO zmR`mbyf+|s+>PH73G50^abrWn>D-qa5)DWpyR&7eZC!WE7*iwPv9zB{n-sf#M?G?d zn-k>DuTDg85F*r`3eLEm6}7as?B{8_sDt2vP|1n_J(nOvC|E9fUN#|e$8K(sxxyKM z+~p*@6abvU*4EY{R30#A3mI_H*_@t~&9Ur$)cj3O*zQ}ywhSRx%)qTFa@VN>a2!Ga zR1*Cz1A+#Eg%km6w+ce!9z2&>h*;mmDYs4}BdF}gO*l?3os3;5wFwH>xW*7&))gW&xh{*1KzS7ai2&ee zBT65o;|~u3SDWJ^n;i539n|bY*VQs&b|st(U>}G%i6~oG7e1Y3jP6}va){3C!Y4r2 zPT08sQ3rt}1GX(rXHPvt!B5i4&t}=tgL3I^CA9xOd($3+2L%n?AgTHJfs z6+`4*03-#l2Zs)b(nntRtz)r$qhzfc%&Y$A=bS-bpL{qK7NiK5iBw8Z`GFJYn)&m* zDQLb{AeEwF{8{0Y@+tI- zbI+oiF1wh1eerkb{JFDf_kjbM(Y0Grv{Vwowj;3sU$AYHlwp&r-a3A%$HHgU;F1A! z&Drzl#G{UspI}Q%O6bsWV`LGK7;u>u*vj_aDF~0#G=^_mC-5ifQ>Ra*8~^zN8nOEj(}nxifv~14H|GMV1I!Sn14K#|t7XR3QYnf{F_ZY5p-Y=-{!V$$TO1ApYcQ>Vy!c80gpU z+C|N+t)8xkpd`|i}d3n;_LI^pw5Q4roXds<`+-wq_XZLqfu0zC43dxbc z{e1*U+A{tq`|(Pidcgj^!svlpSXe;k9y`m^LI5T(nKlpItZMN(ga&(*v z5EX+@a+1ew9trs`bpS!*^hpzW1*V9P)hBXK9$Hq3+s&~fhO^e(Bts@ah+s@X!X(m) zwQD60%3}{dgpTD;#zF)+uti~D|Nc~vN=2EvMVI$Ts4@}{M9oOrIrF@Q%qdR7tV1V} z5W!@?$0kpiu)mi?*mgBFbCXH*-QLhZ&%V8ixkPv{_fP!5PKfY$rUv5+p)^e`E!57F z=S{}YJ8*O#ZITR*3!Dppgaf4J85vtm8U!Fn!Gy={wI}t@p5EFDJ!KDG>(K5qY$zHJ zTk7h(zS$8ZJh45~PJej%S=zZvU4BlPIz@dTv*P<6o~!I+c)zi-ickdZ_Q>X3fVkNp zfeO(KWy7KIiX2=@LimRd8!G~iQpfZ`d?_#uYd`;j>UQq*RGOz1kY8TChMs?SwN&A8 z_3T3rqR9BWNiirmjih#Ze-%DwQ@HLpz zQ!C$8h925G7XSz81Idk^q0hnL#Bd_i@Z;=ymZlbU8!7c!?O#^BNpG$Dm}x3DY&ART z>Gw}PLpS_sDK%$(K%Y8s0`AMul>nC=Wnb^4E-RqF?9~-iS+_l_Q4RE}r}p{E$}4{F zTmX_Q4yy&A$d^4io7Bv2a4L0{g-<0rYAq+$j)ynm>lZ&n|NY1^TKxQryyBmvOK-l7 z9)E2`AfOO}1d0|rYFLXC~gF}h#OqX5y9L6S7088}<`?VFRlb;phn z1->CAE7pAW8SjG|xcHR&!n^O$k{6cq-Ojs#4_sh_Q_A0%4dCZNkOAmasv#*rr+P#U z{HA@=pO;a6<1Ve@lO-eJDY#?$u$X8ECkY)c044(@4PbKcM%via!k=RQ z{^2@wJ(zK{GD-(qTbp_mfj$s5h7d~IkZz*iJ^Uzr@YyC%dmPRMz`>fxHPc)??~Rm` zZ^zJn&+&cyHol*SUF*;{0lg`suXQ^b8)-WOG60)~meGRuAAelJAKX6DX4%3fJI2n| zCCP?s&aQI-az-(X_QVX6>^{YBq}z5j$iBIQpAbUc!!LdHb<-5;gKH~o-Lr4KD=BK7 z%dWQrcl9Tq(p7idL#sd8$oeQ2tFs&U*>0CNoh|?-XCiKfnp4Ng|ILy|sJ6aAUPGaW zj}SsWzq#R|rF3hK(-$?hgj>vC z-o2PsZNQ2&*?!-S<6FYL*0wgjwm(My&X<@6o`0F{fA$6X)qM}r4;C)sH}6&10*C#2 zTH{;*9K4BK6HVg5UR%GD9$EgHT#1+8ayu=0@@e{bOJz1OxH0rc5JJ%2{9W+0i3iBX z#rX2(h6UfHOHMk04jVr<@bC<=0eH7?5#Hp_4U3<9k^c1Ja(a9H$KGQG%>fU944|)J z%h208TLvc|I2XV*Xwrmo*-aZB+>IQ%2c0u#7X9v$3+YBq_>WKj7JYl#(KLDgG8)g1 zwU0mYF!?3Uk4`&8h=wYiXY7NFD@n_gar7ABZMG4DpiNv2c z81@!Co8YT&z{(GtKv#VGB)aAD1+@68E9rr&ujE2sKywd2RGx`uB>0&!`cXRQ^FfCS zfPpoV220|>j@6DQr9V9NWceG zE&vA9ND8lcXydrB0ps@?L08S6NB?=@`84yONmP(hQ{ub_T)_qGYuK{ZccfKE+Y!%@4_^Q{ z5lNGS9Vp>P1!wb0$E9X+*?7JN7m^TyK@t@8V!N#tS%naSMh_oGqlOQamk$=R7S+IZ zJs0`lasKzsn1`|&dpI)irf+Dwt=s7W;2<<|&C=wI<*yrmebIMl-ppxKT&NNcJC`sN z0(yBN;iQ;G=sDA-ii=o4hm08$DxeU8esbn%^5-bp0sgvpo=$giwD8Me&fCsv zNh8`k+`(LX-h3L#Z>)j@yCc0B;v8{5xMd(l8Zrb=m;+$$0sZ>PH(DWJ&EcQ5BM$sl$ww5B6rcl?)OCC30=Nu`2?9~v zk$dbx-{ZRrAvh6Of!v^LbESWj0YvzrwIBos%j%H)!^_5Hiz{f-=utF$_aVv};U6^~ zbg#Vsf%56*eWwe6$!f&SwUAfjg(nO0Hi*kQ38I} zLZpf^de7mq7sZu2Ydq+-)Yj385B(>k&`~$N@-6@-V-#QV^7j9`7;=Jb}yU@fCC1SrUQ-%@TVSl zfOmfn8Spp|A^!T0In>$rvW7+e-E_y z`IqS3>;pl`;{{;Qy1eUL0GE+5;V527_A!FC$%E%a@hPEIffjLs>GjP*2zC>`BHI(f z*Wm|Q1B!~dei{0BvwFanrK!f?LV@ub`J1s@mOeob@{?1@e&l+S000I@Nkl&lWY~Sl??vecaKTFlmPpcD`~>ucRqt`wjDpmv9emr{ z+iB(6^>p=}_t4WT-%u%R=+FYZEFp#f8xS(EBBvh3WR#Yu zGl1zlM1~%I=@t4{{tEJ<8~%qbyy0g0{;jvuZy#F9-w|v8#Y0?dkI==PE-L^g4n)nb zofC35ufS{Xd4Nyzx2ckE^CXw`vSlZ<8Z7e3A|%i^0ge#DRC#? zw2#flb@=V|8|az0-l0cceuWk;dxHM$z6a>a|GSf};O}jJdCy{cY4!Vr!Xbtb;lv}R zw4q}GZ~T@jRz!?gV>cU|3*b5r9E^!LKh5Dg@BoGHRS4e~zw_}1`mZH_rfcrHU%o+t zZxW@lS+5?~S(03V;Iv+zukc*LRYjn3swr=)H}f(ogTa zmzKY`hVWhjI&600r5?NNnX-QK7Dy7{%Z{~+(_6G|74t3tCLM*OS!y49#oK<02d$rC zT(0n2qWhnDo?2U5$+v)~06*0u`F8~R4?0Y}wxf;H!`72kbIMy@f6+0Kawe3PsY9 za3I$KfeH{I=>UaZNG1?6uwva8oB2-Z4!Ze~$LZzOYv>Jrbo|B#Yw6KfUZ)=~x{L5! zhiQQ=E>?il3xab2^Z}p+kZd1B$w^m5Qi-S77h(f(1ZiUvtJx&Syx1->B31&o9P@rSDTk{nA);=mr~oXZyH z(_4Dy0^p#0sARibTJ(YWzMW4zV(GYM3q4@!zU+PH0+4qN&%L_Icp(`YhB~sF#B;Gn z2L6&OT?>F^8-9Lk9Gp&&*&60h*W{reB4ib?0a-EBzy^d2y{OR*w2aU%5xpJn|Q5ltu5=Cn$oxKsH;7*iF3rMZe-w*EppeX0&pDpPe*V% z;U6i~b9Fb@)zvJluBtqvYRl#dMtQ!duG~`Yf~{4R<^KSv*jinE#!|5b!}{KyG%gB&4O$Qg3SDT?1*3IJ~e zLO=w@>4kx|u>phP&&|KeKn{=#WI)ccuXOj@8GH$Ut8KB9hXRs=fmBWVR#5>MKA*QcP8M_x~RN0RR6OvI_$M000I_ cL_t&o0PV@9u}CtY$3!DWgMop;R8)}Bggz7hyHJpz_XD||78n?87)2RLZEx6< z08}vPyvLc4sAv`eZ3wNWWAKus6gDhexMw~lD*T}Lt4Y_=B*6-K0EGT~0U&Q8` za44QE8(f?d4vQXMX(oJ(TwbO`Ok|Bg zYo^3JsRAz?9fNv-Zbd2<7^jqe3YI4o-i>k|ff)Q%+|;1z|0~GA{f9hm!g+5?+}YF-MY=#16H*6kim%pnRj-BJWzA<~ngy>HtHt zbVdMywUM8=`G?s1%k>buQJv*coz+-1sP4<7-x#~eMw|QcKn5kNeiLiG%opX@@H5lP z87pYPppF+WdO(@`duXlgS8JTw&%YwR6R?d6erDy4is(f)`y>0En}469gAp$p5uN86 z@%jbaP06Fixfu4C(RzJgvkyTZJZZh=mIU+JIMIJ}3%@sON(m8iGqf51dA$))zi%Y& zwht+qE&fEpf)_9FThvP`wJ9}I!j_b+K#aTH^K!?^jU)=tp6>y=b~J2qdVZ0oaLjIS zdVL~>MKpVT!m@jPx@~aEA>0b~__q+lhU^lY3yS&}JW27ga{JG=>f-Tp?qm)ArbJ~? zql4Sjo#VveO8g$lgo9yg%itntk-0{{esC7+LD&epJ8eP1IE>H1UTm$_Y-XP=OT630 zdDhY67E>Tw++o2eLxdJ*AK+Gc&E(#JnYuea>{G~|Xwv3>WL1^1K+@#24h&j6TfcR5 zbex-=wGheZdtO*vwDEswZxM;;zZ{ay5NZ*JTzdBt+j2(TF0a;~ojLVppR6@j1YW1H zT^nW@I)O#~?yPnqW=Zz3Q2_L+5&YV$mRNQ9R4^V62W&=y&LFojwPxo{F=8@V#7{5^ zFxeE(@CjmY15edr%=4fzX2*@mKS+RJCz|LmQ9w->n8v3tVWZLJTy63vm&MQiKT6?` z+9>z|(zr7B^!J5K$MY&VKo6B-zH3Z<54q%9b2$)eL8!g0@+GEErzHS$=h6yrX8}0K z2y^_|Ba608b!EZh*J|AE8Rz8a^g0;B@KaDOb2@eSA8n*L32r`EEs^YY=2kjV#o5uJ zGq1T>U{an(Y?4xjDhs^p*cB3^=d;xjHW)1A{RR&6;Xb!vK8g_PTbrMo&tZe*4Uvf> z{16kY^fegR@}RWnGHwF|0*}@8kr8lV4v@Vmqmd7q?UsLBxa3*Uz*h%hoVR3lrs)uC zdK&05r7Sm8&Lq}dr$=y;c=vp$Bopi@h$Zg-qYl`hkRD^61_ZnM`T6Di+NaZ8K)|04heA()Ky9X;c0n;VepSjN*##EK7*+mPrlTy7bFKjZp=Ua=1Xg(NcP+FEf zd;x<`1|Vu0519r|o;+AZtb(kgykaVQA^nhL&C(vDUPYYaLxH@4-0zz4>*^_aKLVtX z!eij2IkDKqYLlsG-CQ=M*Wm3cc?03=8Q9T+H>+%0=Mbc}rjVx15J|(Zt@X%fs!ObB zUjH=+hNj~s*=~$qz`k9CV$LK8iui}*edMocQ@UNO8)fX+B=qQkopMC@TKnt`p@Y$4 zP05s?<0GiB5wo!_QM5VdDfnn^YW+L%<3N(TMLa>4&{Bes*d(b@l$5S%`Budo0@`U3 zM?L&2+9PKaW_&oBzG6RaTZ9hNT-1~)Y^3V|DvBffRhT#0LtYQ5h!f=UVe77USqAl2 z7;@Q#0HTS|VQ`ubGcqcp^O%%>wie+IjOUlbtq+VWDPh_GTq~G1=;s`lL`Ifqh6U6L z9ZDJnUD(n9aOtp#(S+5PV5|?w)FCyAWc&+JM(L~0-Rm11e_#tz6dS$F`f%f;oF;5 z`j|4=a;Q|KiJq--P*Dm5G8O4dys@MZ=i$*08Ki>*QF=EJxp!=RvZ)tltANbJ$jIWN zh<=+HF&CTArZNzF8G*;LuFA*#as>9D!*PFv5ZxS%5llC8=qtVZjYt`Azp4-~EN#(i z9!)D(9FZ63cO04B+a%9y>VLEeqL9H|w$% zDo+SUO#lPAbNKD3bNsGiZXTC;j-o&d9=iINowE0nLC9{!hGg(9@05@FfJ?Co1dZ8tAOyq^7%^op8EHN*O836n>e z#F;LKpPxYJ*0YMc33B-N4Yorel4DS`8|b0}rs|YjB&tsW#E{AV7xMqxBMw4nV}Kz5 z*r_qt3>LVT-{AT$x(}zSt$EX5nQEO&D~i5PzTz3MmXrNiNVXD(-&$7g;4Tu*2z=hhCp9zI|9|JCQPr zQW3kD1QJbEnr6Zqoc>XW#XsoU8PmJ#tw%9idICqFYqqCr5ggLGq>)kn+Y?`r(hob2 zrPp_VB>)-I6nTXwUU-b7Qiv*Eg z(TI?G*r1;MyfFB@Rg@jm8r}qcC0SYA=;e`{TKIb}f(;4sny-IX!a_+^5I@3bxp+Yy zvmG$hCo=s>b7)!66gz7(Atva$<$ZGT7}2^Wql=aJYpWDvQ_l8K+;k>nid@}A_%SMp zI#QX=iIIwY`8b}%A7QV_^o@s(DPx(7MqWLDt&1p(pwko(KS-KTl4ueUPPM=FpdF&e ztf=A_3SHGV^!6*c`f>!eGr#bQO!E!`;3&7Th#&G}HbmU_{tDiGH2fs1iN~P+OSeuHqLMw2 zj_Tu#&yDAn!iXr{GH9TfJt9wGW{xCl1u)M!uElV)&0BHbi{$F?X*j#FpuEQya;+b= zOh9Xsnn2cHU@=98CrT3;q-RN|+0MZlN;B-5m-bQEqty863!w#m7W28i-CxwAP!Lxq zBy|!guEIwVfyw%RIc5vyKQQnklaa!#BMJ8GD^0T`NkCe?ka=bE0h#?F7NvNxXhU{u zOVjRu${1!tc}iwFcvMSf_q4+k7UCNperDLkt~olDj(6LYHfBs0r@1DLJnE&ZW|4ds zQs#C1JC*N#_3)MVwSUC@E@o)93~vaJefV^R>eF|0bfh8O9|+u0^cAv8-z-k<*!R78 zN6B{ad5UN6*}KEvP6{4x_I&D(SgvhmLF^!@Ade$gV89H5`2?cX#F1xx%c{sxt@QXV zT2?|K4L4r94%n?t`CBMSLd&B2w>9R6pxpM9>epG08J*nD@UBL4rU17`^C$82syvE- zOSHC^P$C5J{VhRA>RwUB?`JCqRg@pd9L?Gw*5e? zn{9)LYH9NKE%$GGQcZ&*bmICy)dCy_a19tMG>sh%+Nd3}$a>;rv~#PAeizDpjxDrr zS7mDU^7;2=aduI;b9pw`VNzGJl|GfbV(pvTWHCj+k=son2Z#yFCZVtoi(IyEuPm@o z#E(Q1q=t-X_lpJ~1g>tm@PDe9WEB{ag5~JP$Q%Ef7KZgrBU(8{f}WFJKKMSO_X1Yf z+FOu9ay)B+3CWuo5xIwnr(T3id@;L@`0s|Y(rDQSyn4bu5|SfspN~L!a^<2 zjOEHe2=2VZsH;ec3b7g}Lp&;yk5~Qe`*j(trKaKv`<}X`C?#_~HP7sa*1^_`hU7B# z9C~Yn-)V58o#2c}EW14Pey;|j-M_3RYy(W?HC4Ash)R%boAuQhxl?0f9~Kx@X>Z=E zqE^D0{=PwjfQg9V1iAf|9C{ug`r7AYhmX)a;}DGrcr&oANS7HG zMGuA%4_x$*$>_y>)Cu`%tm?i7)SIqotFxQJOGJNfea0{rO5OSrjQp||W!N;`wV<0- zEDa!bdV4terhkCPew^_GSBS75)-|>;1Rkt^%s2Wi;$MAAY=m3oN5Y}mxZNX_-WnHv&Q@&ie#O^Ld%dWgKjj11QjaVqctz6#1 zleO_E^JIV*r@rsU`G67m1|h%hZ*$aeEqv@ofj6)8Zt^#3?0W~PL5PRfMun4xs#P4X z*Rh>Y@61-keGoW|q0jZc)SK|!-YI{c!s+JXL9pwvl4tKyj%);?HT7Mng>9d7vOnB` z7gzBLjfCPx8scMUB*q~jL#+X~oD=RuSyILrW8rV5O-i(8xMXU!%qH}#vh#tL>2ubT z1T_^a9**#vR(ysfyY9C1LPc|PISU&nhhcQ#P^$VZ$B?j(pB8SRk5$PGN>}#;;!EG zR(T{cXCd1B_>_Gt+j$AZJeh+8Eg$)>vpx|B<5Cxj=~n-~npmHiQ+eBw8em~y_ z$(e(i|H}f@6DwQpucK)vhgQ8>ekx2IF_6kLJ%Z2F$QR3kd$WZBH$t<^l38A-^p!G* z30y&b{>(w?#oy9A?ND}Eip?4ssG96a*~Z3c_R5x zor2C&Cm?q5m@c$=g{sS@#B2*<<+Z5ygB%8Nuzurmj#a9i1cCuRW zkG88q*UjUMgy$ce7>|1bC{Y_qigZ-_T1QnTx=D9!1@}7ZtjX3dZ|VU*)_1M{rYPd= zm37eCDdYoL#ALmmSGMx@{^D6 z<>A7YU%LKS(Z6VAk^L)l6h4f*rO?cE#@7TX8~_t+ZzD0?cP-Rr7~1cHJwyDo7)EG5 zSbMf=;@@DFRBcxI#<#WQfS&J%LKAYOg<_+sG!;@L4qe2e3usjXMeB>h$xHrG9EW&X zl}`(rK5l=Q&v~sO#2(_#_`dc0Gbl!EsX?D!MeO$TdN9Ya<9cWMSrY2&s^R%BN58w# zOMO+`leJ{k#DvpFuLcc@M6{jMRSX8%)Pgp{^qcEuH4*9Ol@+52-O%G6lW45E5O{R~ zRHz~xc?MEw-|5we)LN!_38+)Hd4tK9*}=!;86S+GreP_~*zd3nnIgYr(>FcSa%XFi zX%U#^c}81_@a}s4>OQ2kF`KqNh`M_rUBaI{J6&QlG7Tn6TmAu14qrv17-qVS5zs^5 zYeeN@Yz{}&Ug0teEm#P5RxZv_Kq3J~boy7sx0w-h=j6=I5x>Oic7GD~oViU9QPq3j zr+-?gc?;`uTfe>8H9Sj6J@Baqn$c;342$<6Q^Cqep%r>0D1oTL?t(v#v!d|l!RF=DMB$ES8{ zb~AfzlBnIbL)hnntV^663?qT#G{~r>hQGRmSmdMTj34(`-6!2R{tk@{T6nIE5{Ob) zCYW`UxDuho;G*PUNdNB2C-sTa z-XxJXr*mh8Mfwa8j?dNm)+;Q88^`Eb$4v-WjL4nRlGPLo{A#96qc~_` z))O=zYJqP{8e=iIjP^8UfnNdEeG63$fn&NptC6x}myikJJBwCMcK4^z`RHoI+s7&_ zfwqyohHQ$2jF^l#*hRtI;YI?n<@J{bt+|;EstyahfiIAaIx6ICCkZ^p3Gb8C_JN8G zpY+i)(2$7>`CfcjfLZScLLC;s7rPgT0&0}h+p^v&tf?xt zp<`MRZZFkSRlCpMCqzk* znNn-)qc}|cb(A8utF`W|Cnhc4@z{KGCQqT2zGfF`{OX8G4z<(&N!=8!GJZB`xY2ZF zUD^@BJ$}tg?@A8!`!&;UJB@DY4xZ%bsMrL?a6HpWuYztyW8HeZm_!B z)6W9$5PiJ7^lyJ`dD~_%9KU9JU{Y6u1pxQ+yZ9ZuviRPnxZGFzFWomr7-kj;IP%Vo zyFxFl2JDg(if8~tR$Uq)Kz?lbd=XX9r>d$3r!j_bbO+ko$f{Kc1M7|cYqqb!sAAN) z8^`cG969hmz_xUB2bS$=m*J0TaGd1znG;^ySZg&VQjm+P0AJMGE*i9sl&`I)1=2b4 zE7%Ps-|yYhXS_H%!2~}&A|J~yuiW1IX`uyUr!F>NCDoy@L9x(e{PGbz!WLH>;?NKq zfd&};nGm=^osXBL6+?5Q1+(89R=SQt3`v=*s;MeMoGqRL1wjqKzFYcqE?cJjb@5MC z8&WR}GNC8z`yuXWwE7do2cG$uC^Dt7i5%6S>r@t0meeM#&%pcdXwQEu4=Gmcq3%BN z2?%|-1v<$+<(Wth6*YyVlqvB#QLx@nhBu-^G2NWs!Dc2;gc}U_rTOJe>LNQiUZDH* z2FGB=W*}E&3uY9|sX0=utCU z)B)gjz^6n*gAr5^X}0LC_S5ms1XEcr5`IjkU41_@=dZH^79pnecuBxjqukZ3TCA z{iQY#vjE14*|xO)i0U`DF3gIo%+VL*w|HDzSEC%+t}zn zp~wKnzbGH$$gg2HP(Ys)35yrn;UpIO6=FZj$6J{%iTld<)SHby&P7Ty!jfv?cn9t; z|2W%vf~p-KpN?AyH3%<@2oDD1UVq#6X09jlN94(=2uI?sYI&^a30iUZ#US7 zk0ai3(Oq|-FT{2<=yp$wKr2~kTCnVX9A6pYvAD)lJdDPh89B^^UO~CT!^3qD48jun zx~6e6&{}_(Mk3fkM_Rc4Qu$0W0-hkLt7mL;<%H+B1AGpxjd|EpK23eC`de-4>?sFN!T#>*C#bku?~x|3bbV$tZ-pdt|7AnE?!nsz1Qn z3K%CR{R_jf&UnA0Q?}bLD*?|(X2cLt{s5FtDf`D+L}Q_2MY}X74<)z-VW7QAqAc1n zL0Bv1KR!Lx$=)oZPty2{$7K`=@tYJ^Ps4fcFvQn!VLFzUphm@Gx{PW-R3W6Z;w!*34fVW0NzFSeM*(08jSsD z=7g^z{wCPkq?l#NnOfJrhGP|j{Ud1>RAn^W2O(`F=wM0EZq2Ms?yV{OQmCHwA6=6` zR37LaYsQX_|Fi55zb!uPa0-W^fezeLg*$CoQ!lvGsU?Vo#YSMngn6_+efl;bXq`kW zi}?sDtso6_{F1pwudwL-8mlJprB8XUXTwJE7Dr$biTon*Rh`igj!QVXG+^)#j6pne zMvmZHTgz0xEO#IpKmaysP5_(;i5JHm(ut$9L}sAu#~{XmJ(LV~yqcDunR- z$qDGaN3@O88sn za=@WoM7e)rb1cB@V$*;#D{$-=5}6AVhvEVMv(JRc+2=NcvkaBQ_Fg`w1LX7ufG+uM zBi#w3G;!V*ymwJ7IcIAmA%lX1c0C{e+}O|sS2&a2#t)St3J+Q`e*2U)vEca@;R^ZR z5AhBgT}7Qa7x*;Yu%}EfO>fK$N_pNnko&o9|eCPLcbUTyK`$F z;xC!bNrQ{h{q%>`(JCi$bTs^#pGlvoK7}F94f)NbF9Suh0dFjT@A{t_*VduiZ{DpPsk z7iiwq)3p2~DNTHoC-(x@j;~54$tuJmmINDU2~bL6a_CUPYFzX8DI5EzJ`LP2wgi#d zcwa^$$dFA{eL?Myr5Ync2LOh-rg^OyCJ^*Jx;Xvl^HF5VG3E2X^(q(!uxPE^^$nBx z>`Xpg?q2M@?$OV|!)#(f$2{0-85d=wfN_Xx`!al^ zm5l(a0#hA=b;sB@X|(n|0~&@D_W{UbYT3g*lT?%D=-^Y#Uk7qWcAm{y^tw~^NOrU3 z0oXs3X4nBTICKF2c`V+4Rsy~u2oSD*`y*aGF44>i{;}|}r>DoPG)JeB zc;~8WXGZx)qvyj+uRgXryPOh=GitZGQ8*g!744Jeo5$Ch;nTm8D#;@90AGQHr7W- z2PCZfDX6>an$FG?ovak?mxj_QS_YdiFlgof%>}eP+H{F9>UO|#@}tq}6&HqyZFD7{ z`JBlL`NON?{eg4zGzP@!%fRvxa7~&!#C;{W!I+Zkn-DNR9zs`n^8Yw1aSqa2h z8$AmBxxIAhHREYSiyU}*a(>=1RLSV8Cs^ekwFdr^W816N&JS>lrlHqK-709t{HN^w-jhQ$u6DF3Qvaiz}TB@ z8ik1)d=_<;2%|h>?nul;_;QR+4*WX^)o|&gHJztUPiG#BvjTx{4o_pC*T}$&+~z5c zbH8ZvWUck4PCxi=gqSB7b4+VmrMwp8!PxCIzftasf#V%s&shtWqlwhX8RFKwe!YlW zvhy+$DgvIYS!^D^awL54@78E{v9#6aRIpGPT(M2o?^aSS5 zW-rty;L{XFbt+0Tl4JXIGziXa3*VhH9h8mhZZ4qQBB}n)83VKri%&%n&$(J_KcDRu zz2JNu;0XMrXY`D9Q<&wP?hs{j^i^oQv&*xyQZsj|n5^CQc_W}fzFHuYhe!&}GxY-f zKvujQ zY{Q}p(aoUHe;{ea?mXUf&ZAiKW-<0#&ACzYT33E|orH(l3znm8J(EJcJv;tDU&P!* zY(j~5u+WV=VmrDbsVDj)+E1_p#Q$H&IISurhFU9fO@nuU>v>TGztgJfx&I9pG@fA- zy?Y{Yzs6IWo;+*K?F#B+yO}d|#$%tFJ}6~O5hR9x-zz#U8l8!6om&{yt6@d`&?tOI zDTyWh+xyMeRiFGbVt3l~^t@Fd8T_K^`wQrP`u^=z?pllUL5QFo*W8{6n78LLIfJ zM7Q6*Pp{f5y{FnSQ}+?3o9BqP)14S1k-V#ns=D_#t)88JkWpt8wp)HKrrGo zF<}SiS04!|<*j-^h_ zyv@(})xGuc@qPCC@nw%MVhVarL8Kntcj*S#Tj9CsWh-4`;YdaDtl?v}3{PJw4QY>L8{}+#{_k9+>4J`iBbc2(aYV;rVq~|B_$&1!GElpc55qfUEq)< zI51Qw)rZkRTYc~Pmug<`KXp4_$xm%pY~C6WN|@&m+x&bXO#l86f{Kt${0ElM2V`=j z?q1h*FA-dkO4Vd?6^S))9qSlr%R!agNqcOq+BUXr)uIXUR}9G zhPmFEf%NyuPCrOeyd?}6$=w?MvVX9sU4@2e8^$mI+?mL1?eMcPDVcsut~q>P04~n< z+(*jo3SRdJ*Fe;E*D2VPX%$0x=a-#t>Jt(L$k; z5^vPxUsl!)_-t^vRV@c4lZ88|mRKDi`Q?yGGg$)6hohTPfSvSdAS%UP-m3aWT5Tal znZz=ZH=DE`Y8pk4p~H~7s?z5s!`S9ldm>81BCOc8>VD5a9I^$i&+=r0@ujexI7XD?V6 zk6ztp$eNPghH{1VycF|GgC^c^hn3X@{5z7A_kHOLaMa9&1(C6|koShNbe&_ng4CVl zzdion`{=<6B~p0+#Bl6p16#BZr&TlOxzoV)(O*ji zEX|WUL!p2(-Q0p2j`X23hb%+<=I+kZy!JJE1Myszb;s5in&Dt2U+9oP*A=C>O6UvT zgdMoQ_3-q#6QLcNW9jMm7TrAXiBd-1LPAYhd5lD~QY}%qf98Ki{dz{Kmpl^3Ss~ zQ6LWUfD`-AMhqgP2Lia`GBRK6xwG^O9Y7oAo;aW8+1VYX4{$YTW^L{!mei{R#?6l5 zhx&ArAZmED0yRur{xpCJdKPAqe{pnp5A?D*Ko{-od?++m+lfwKl1@XjKH!ZSNgxSp zy=nWpdhGmOdTO@zS7myBe*RU;kN!G9@h5T-`#x5WkvVieCm3Ad_!SRDV;V-6iuc=z zBO&)}o%1A*-`z=jaaHT>ar{Jll(4XHv%8nqEn2BjVqyB{bvi0V;};u-XD;9N_O>SX zk!(wDN4;|g`>x_5D+*ommvc67J9~yWgl_+X8#)TUBa0qfJ`daa`e`n&z`YSWq2i; z9y?7UTYj3#9fNF*A)Y_e-)dENKIX{*gHr%ZQz-)GfshfvLW)#0UaC?Mq*&f)r3h%U zrnxF*4rzfTH-J)CnO80(;9DZhK7w1AP_L2M#L3@jp@A1UIVjwb8YC4gIf3D^atDd) zhm5*(h(OSP%L$1W-G_4vZT&-PR?TSx7Dn9Xg!sTJas8VOZ7|OSz6}%C{$BG8uQF=I0!b0?@ zQ&>V8@D5Wb4!!YZ;DwOxvJZ_?Wz&R1MrX?G6JKo*%26BMA@g}Bc6=d7dkrVzIpy?I zZ#;TobtTXl&y-niu<+igAzw(xt_IrxDRgXffV8%}-3#Fj83yM4{X9$SzoAy7|5PhH Y4B%99mtjAN5n6#!lvS0fmNE_fAJZWuVgLXD diff --git a/lib-multisrc/pawchive/res/mipmap-xxxhdpi/ic_launcher.png b/lib-multisrc/pawchive/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index c144355436abc98d64e4995590e72f7d18919825..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18525 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 dateFormat.tryParse(updated.content) - else -> (updated.double * 1000).toLong() - } - - fun toSManga(avatarUrl: String) = SManga.create().apply { - url = "/$service/user/$id" // should be /server/ for Discord but will be filtered anyway - title = name - author = service.serviceName() - thumbnail_url = "$avatarUrl/icons/$service/$id" - description = Pawchive.PROMPT - initialized = true - } - - companion object { - private val dateFormat by lazy { getApiDateFormat() } - - fun String.serviceName() = when (this) { - "fanbox" -> "Pixiv Fanbox" - else -> replaceFirstChar { it.uppercase() } - } - } -} - -@Serializable -class PawchivePostDto( - private val id: String, - private val service: String, - private val user: String, - private val title: String, - private val added: String?, - private val published: String?, - private val edited: String?, - private val file: PawchiveFileDto, - private val attachments: List, -) { - val images: List - get() = buildList(attachments.size + 1) { - if (file.path != null) add(PawchiveAttachmentDto(file.name, file.path)) - addAll(attachments) - }.filter { - when (it.path.substringAfterLast('.').lowercase()) { - "png", "jpg", "gif", "jpeg", "webp" -> true - else -> false - } - }.distinctBy { it.path }.map { it.toString() } - - fun toSChapter() = SChapter.create().apply { - val postDate = dateFormat.tryParse(edited ?: published ?: added) - - url = "/$service/user/$user/post/$id" - date_upload = postDate - name = title.ifBlank { - val postDateString = when { - postDate != 0L -> chapterNameDateFormat.format(postDate) - else -> "unknown date" - } - - "Post from $postDateString" - } - chapter_number = -2f - } - - companion object { - val dateFormat by lazy { getApiDateFormat() } - val chapterNameDateFormat by lazy { getChapterNameDateFormat() } - } -} - -@Serializable -class PawchiveFileDto(val name: String? = null, val path: String? = null) - -// name might have ".jpe" extension for JPEG, path might have ".m4v" extension for MP4 -@Serializable -class PawchiveAttachmentDto(var name: String? = null, val path: String) { - override fun toString() = path + if (name != null) "?f=$name" else "" -} - -private fun getApiDateFormat() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH) - -private fun getChapterNameDateFormat() = SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss", Locale.ENGLISH) diff --git a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt b/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt deleted file mode 100644 index 530d8d64adf..00000000000 --- a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt +++ /dev/null @@ -1,405 +0,0 @@ -package eu.kanade.tachiyomi.multisrc.pawchive - -import android.app.Application -import androidx.preference.ListPreference -import androidx.preference.PreferenceScreen -import androidx.preference.SwitchPreferenceCompat -import eu.kanade.tachiyomi.multisrc.pawchive.PawchiveCreatorDto.Companion.serviceName -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.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 keiyoushi.network.rateLimit -import keiyoushi.utils.getPreferences -import keiyoushi.utils.parseAs -import okhttp3.Cache -import okhttp3.CacheControl -import okhttp3.Request -import okhttp3.Response -import okhttp3.brotli.BrotliInterceptor -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.File -import java.lang.Thread.sleep -import java.util.TimeZone -import kotlin.math.min -import kotlin.time.Duration.Companion.minutes - -abstract class Pawchive : - HttpSource(), - ConfigurableSource { - override val supportsLatest = true - - override val client = network.client.newBuilder() - .addInterceptor { chain -> - val request = chain.request() - - // 1. Existing API fix - val modifiedRequest = if (request.url.pathSegments.firstOrNull() == "api") { - request.newBuilder().header("Accept", "text/css").build() - } else { - request - } - - var response = chain.proceed(modifiedRequest) - - // 2. New Fallback Logic for Images - // If the response is a 404 Not Found AND the request was aiming for the full-size file server - if (response.code == 404 && request.url.toString().startsWith(fileUrl)) { - - // Check if the user has the fallback toggle enabled - if (preferences.getBoolean(FALLBACK_LOW_RES_IMG, true)) { - response.close() // Close the failed response to prevent memory leaks - - val imageUrl = request.url.toString() - val pathIndex = imageUrl.indexOf('/', 8) - - // Build the new thumbnail URL - val fallbackUrl = buildString { - append(imgUrl) - append("/thumbnail") - append(imageUrl.substring(pathIndex)) - } - - // Retry the request seamlessly with the new fallback URL - val fallbackRequest = request.newBuilder().url(fallbackUrl).build() - response = chain.proceed(fallbackRequest) - } - } - - response - } - .cache( - Cache( - directory = File(applicationContext.externalCacheDir, "network_cache_${name.lowercase()}"), - maxSize = 50L * 1024 * 1024, // 50 MiB - ), - ) - .rateLimit(1) - .build() - - private val creatorsClient = client.newBuilder() - .readTimeout(5.minutes) - .build() - - override fun headersBuilder() = super.headersBuilder() - .add("Referer", "$baseUrl/") - - private val preferences = getPreferences() - - private val apiPath = "api/v1" - - private val dataPath = "data" - - private val avatarUrl = baseUrl - - private val fileUrl = baseUrl.replace("//", "//file.") - - private val imgUrl = baseUrl.replace("//", "//img.") - - private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', avatarUrl) - - override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() - - override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() - - override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() - - override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() - - override fun fetchPopularManga(page: Int): Observable = Observable.fromCallable { - searchMangas(page, sortBy = "pop" to "desc") - } - - override fun fetchLatestUpdates(page: Int): Observable = Observable.fromCallable { - searchMangas(page, sortBy = "lat" to "desc") - } - - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { - searchMangas(page, query, filters) - } - - private fun searchMangas(page: Int = 1, title: String = "", filters: FilterList? = null, sortBy: Pair = "" to ""): MangasPage { - var sort = sortBy - val typeIncluded: MutableList = mutableListOf() - val typeExcluded: MutableList = mutableListOf() - var fav: Boolean? = null - filters?.forEach { filter -> - when (filter) { - is SortFilter -> { - sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc" - } - - is TypeFilter -> { - filter.state.filter { state -> state.isIncluded() }.forEach { tri -> - typeIncluded.add(tri.value) - } - - filter.state.filter { state -> state.isExcluded() }.forEach { tri -> - typeExcluded.add(tri.value) - } - } - - is FavoritesFilter -> { - fav = when (filter.state[0].state) { - 0 -> null - 1 -> true - else -> false - } - } - - else -> {} - } - } - - val mangas = run { - val favorites = if (fav != null) { - val response = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute() - - if (response.isSuccessful) { - response.parseAs>().filterNot { it.service.lowercase() == "discord" } - } else { - response.close() - val message = if (response.code == 401) "You are not logged in" else "HTTP error ${response.code}" - throw Exception("Failed to fetch favorites: $message") - } - } else { - emptyList() - } - - val request = GET( - "$baseUrl/$apiPath/creators", - headers, - CacheControl.Builder().maxStale(30.minutes).build(), - ) - val response = creatorsClient.newCall(request).execute() - if (!response.isSuccessful) { - response.close() - throw Exception("HTTP error ${response.code}") - } - val allCreators = response.parseAs>().filterNot { it.service.lowercase() == "discord" } - allCreators.filter { - val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase()) - val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase()) - - val regularSearch = it.name.contains(title, true) - - val isFavorited = when (fav) { - true -> favorites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } } - false -> favorites.none { f -> f.id == it.id } - else -> true - } - - includeType && !excludeType && isFavorited && - regularSearch - } - } - - val sorted = when (sort.first) { - "pop" -> { - if (sort.second == "desc") { - mangas.sortedByDescending { it.favorited } - } else { - mangas.sortedBy { it.favorited } - } - } - - "tit" -> { - if (sort.second == "desc") { - mangas.sortedByDescending { it.name } - } else { - mangas.sortedBy { it.name } - } - } - - "new" -> { - if (sort.second == "desc") { - mangas.sortedByDescending { it.id } - } else { - mangas.sortedBy { it.id } - } - } - - "fav" -> { - if (fav != true) throw Exception("Please check 'Favorites Only' Filter") - if (sort.second == "desc") { - mangas.sortedByDescending { it.fav } - } else { - mangas.sortedBy { it.fav } - } - } - - else -> { - if (sort.second == "desc") { - mangas.sortedByDescending { it.updatedDate } - } else { - mangas.sortedBy { it.updatedDate } - } - } - } - val maxIndex = mangas.size - val fromIndex = (page - 1) * PAGE_CREATORS_LIMIT - val toIndex = min(maxIndex, fromIndex + PAGE_CREATORS_LIMIT) - - val final = sorted.subList(fromIndex, toIndex).map { it.toSManga(avatarUrl) } - return MangasPage(final, toIndex != maxIndex) - } - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() - override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() - - override fun fetchMangaDetails(manga: SManga): Observable { - manga.thumbnail_url = manga.thumbnail_url!!.formatAvatarUrl() - return Observable.just(manga) - } - - override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException() - - override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url.replace("$apiPath/", "")}" - - override fun fetchChapterList(manga: SManga): Observable> = Observable.fromCallable { - PawchivePostDto.dateFormat.timeZone = when (manga.author) { - "Pixiv Fanbox" -> TimeZone.getTimeZone("GMT+09:00") - else -> TimeZone.getTimeZone("GMT") - } - val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!! - .toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT - var offset = 0 - var hasNextPage = true - val result = ArrayList() - while (offset < prefMaxPost && hasNextPage) { - val request = GET("$baseUrl/$apiPath${manga.url}/posts?o=$offset", headers) - val page: List = retry(request).parseAs() - page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) } - offset += PAGE_POST_LIMIT - hasNextPage = page.size == PAGE_POST_LIMIT - } - result - } - - private fun retry(request: Request): Response { - var code = 0 - repeat(5) { - val response = client.newCall(request).execute() - if (response.isSuccessful) return response - response.close() - code = response.code - if (code == 429) { - sleep(10000) - } - } - throw Exception("HTTP error $code") - } - - override fun chapterListParse(response: Response) = throw UnsupportedOperationException() - - override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl/$apiPath${chapter.url}", headers) - - override fun pageListParse(response: Response): List { - val postData: PawchivePostDto = response.parseAs() - return postData.images.mapIndexed { i, path -> Page(i, imageUrl = "$fileUrl/$dataPath$path") } - } - - override fun imageRequest(page: Page): Request { - val imageUrl = page.imageUrl!! - - if (!preferences.getBoolean(USE_LOW_RES_IMG, false)) return GET(imageUrl, headers) - - val index = imageUrl.indexOf('/', 8) - val url = buildString { - append(imgUrl) - append("/thumbnail") - append(imageUrl.substring(index)) - } - return GET(url, headers) - } - - override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() - - override fun setupPreferenceScreen(screen: PreferenceScreen) { - ListPreference(screen.context).apply { - key = POST_PAGES_PREF - title = "Maximum posts to load" - summary = "Loading more posts costs more time and network traffic.\nCurrently: %s" - entryValues = Array(POST_PAGES_MAX) { (it + 1).toString() } - entries = Array(POST_PAGES_MAX) { "${(it + 1)} pages (${(it + 1) * PAGE_POST_LIMIT} posts)" } - setDefaultValue(POST_PAGES_DEFAULT) - }.let { screen.addPreference(it) } - - SwitchPreferenceCompat(screen.context).apply { - key = USE_LOW_RES_IMG - title = "Use low resolution images" - summary = "Reduce load time significantly. When turning off, clear chapter cache to remove cached low resolution images." - setDefaultValue(false) - }.let(screen::addPreference) - - SwitchPreferenceCompat(screen.context).apply { - key = FALLBACK_LOW_RES_IMG - title = "Fallback to low resolution images" - summary = "If a full size image fails to load (e.g. 404 Not Found), automatically try loading the low resolution thumbnail instead." - setDefaultValue(true) // Enabled by default - }.let(screen::addPreference) - } - - // Filters - - override fun getFilterList(): FilterList = FilterList( - SortFilter( - "Sort by", - Filter.Sort.Selection(0, false), - getSortsList, - ), - TypeFilter("Types", getTypes), - FavoritesFilter(), - ) - - open val getTypes: List = emptyList() - - open val getSortsList: List> = listOf( - Pair("Popularity", "pop"), - Pair("Date Indexed", "new"), - Pair("Date Updated", "lat"), - Pair("Alphabetical Order", "tit"), - Pair("Service", "serv"), - Pair("Date Favorited", "fav"), - ) - - internal open class TypeFilter(name: String, vals: List) : - Filter.Group( - name, - vals.map { TriFilter(it, it.lowercase()) }, - ) - - internal class FavoritesFilter : - Filter.Group( - "Favorites", - listOf(TriFilter("Favorites Only", "fav")), - ) - - internal open class TriFilter(name: String, val value: String) : Filter.TriState(name) - - internal open class SortFilter(name: String, selection: Selection, private val vals: List>) : Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) { - fun getValue() = vals[state!!.index].second - } - - companion object { - private const val PAGE_POST_LIMIT = 50 - private const val PAGE_CREATORS_LIMIT = 50 - const val PROMPT = "You can change how many posts to load in the extension preferences." - - private const val POST_PAGES_PREF = "POST_PAGES" - private const val POST_PAGES_DEFAULT = "1" - private const val POST_PAGES_MAX = 75 - - // private const val BASE_URL_PREF = "BASE_URL" - private const val USE_LOW_RES_IMG = "USE_LOW_RES_IMG" - - private const val FALLBACK_LOW_RES_IMG = "FALLBACK_LOW_RES_IMG" - } -} From fba833457db7deede150e1189ddd886f5115d86c Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:01:13 +0700 Subject: [PATCH 11/21] Moved multisrc to src and stripped some code --- lib-multisrc/pawchive/build.gradle.kts | 8 - .../kanade/tachiyomi/multisrc/pawchive/Dto.kt | 109 ----- .../tachiyomi/multisrc/pawchive/Pawchive.kt | 405 ------------------ src/all/pawchive/build.gradle.kts | 19 +- .../pawchive/res/mipmap-hdpi/ic_launcher.png | Bin .../pawchive/res/mipmap-mdpi/ic_launcher.png | Bin .../pawchive/res/mipmap-xhdpi/ic_launcher.png | Bin .../res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher.png | Bin .../tachiyomi/extension/all/pawchive/Dto.kt | 127 ++++++ .../extension/all/pawchive/Pawchive.kt | 338 ++++++++++++++- 11 files changed, 468 insertions(+), 538 deletions(-) delete mode 100644 lib-multisrc/pawchive/build.gradle.kts delete mode 100644 lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Dto.kt delete mode 100644 lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt rename {lib-multisrc => src/all}/pawchive/res/mipmap-hdpi/ic_launcher.png (100%) rename {lib-multisrc => src/all}/pawchive/res/mipmap-mdpi/ic_launcher.png (100%) rename {lib-multisrc => src/all}/pawchive/res/mipmap-xhdpi/ic_launcher.png (100%) rename {lib-multisrc => src/all}/pawchive/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {lib-multisrc => src/all}/pawchive/res/mipmap-xxxhdpi/ic_launcher.png (100%) create mode 100644 src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt diff --git a/lib-multisrc/pawchive/build.gradle.kts b/lib-multisrc/pawchive/build.gradle.kts deleted file mode 100644 index 9bdd8fb9ff7..00000000000 --- a/lib-multisrc/pawchive/build.gradle.kts +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - alias(kei.plugins.multisrc) -} - -keiyoushi { - baseVersionCode = 1 - libVersion = "1.4" -} diff --git a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Dto.kt b/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Dto.kt deleted file mode 100644 index ff50a93d0ca..00000000000 --- a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Dto.kt +++ /dev/null @@ -1,109 +0,0 @@ -package eu.kanade.tachiyomi.multisrc.pawchive - -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import keiyoushi.utils.tryParse -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.double -import java.text.SimpleDateFormat -import java.util.Locale - -@Serializable -class PawchiveFavoritesDto( - val id: String, - val name: String, - val service: String, - val faved_seq: Long, -) - -@Serializable -class PawchiveCreatorDto( - val id: String, - val name: String, - val service: String, - private val updated: JsonPrimitive, - val favorited: Int = -1, -) { - var fav: Long = 0 - val updatedDate get() = when { - updated.isString -> dateFormat.tryParse(updated.content) - else -> (updated.double * 1000).toLong() - } - - fun toSManga(avatarUrl: String) = SManga.create().apply { - url = "/$service/user/$id" // should be /server/ for Discord but will be filtered anyway - title = name - author = service.serviceName() - thumbnail_url = "$avatarUrl/icons/$service/$id" - description = Pawchive.PROMPT - initialized = true - } - - companion object { - private val dateFormat by lazy { getApiDateFormat() } - - fun String.serviceName() = when (this) { - "fanbox" -> "Pixiv Fanbox" - else -> replaceFirstChar { it.uppercase() } - } - } -} - -@Serializable -class PawchivePostDto( - private val id: String, - private val service: String, - private val user: String, - private val title: String, - private val added: String?, - private val published: String?, - private val edited: String?, - private val file: PawchiveFileDto, - private val attachments: List, -) { - val images: List - get() = buildList(attachments.size + 1) { - if (file.path != null) add(PawchiveAttachmentDto(file.name, file.path)) - addAll(attachments) - }.filter { - when (it.path.substringAfterLast('.').lowercase()) { - "png", "jpg", "gif", "jpeg", "webp" -> true - else -> false - } - }.distinctBy { it.path }.map { it.toString() } - - fun toSChapter() = SChapter.create().apply { - val postDate = dateFormat.tryParse(edited ?: published ?: added) - - url = "/$service/user/$user/post/$id" - date_upload = postDate - name = title.ifBlank { - val postDateString = when { - postDate != 0L -> chapterNameDateFormat.format(postDate) - else -> "unknown date" - } - - "Post from $postDateString" - } - chapter_number = -2f - } - - companion object { - val dateFormat by lazy { getApiDateFormat() } - val chapterNameDateFormat by lazy { getChapterNameDateFormat() } - } -} - -@Serializable -class PawchiveFileDto(val name: String? = null, val path: String? = null) - -// name might have ".jpe" extension for JPEG, path might have ".m4v" extension for MP4 -@Serializable -class PawchiveAttachmentDto(var name: String? = null, val path: String) { - override fun toString() = path + if (name != null) "?f=$name" else "" -} - -private fun getApiDateFormat() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH) - -private fun getChapterNameDateFormat() = SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss", Locale.ENGLISH) diff --git a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt b/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt deleted file mode 100644 index 530d8d64adf..00000000000 --- a/lib-multisrc/pawchive/src/eu/kanade/tachiyomi/multisrc/pawchive/Pawchive.kt +++ /dev/null @@ -1,405 +0,0 @@ -package eu.kanade.tachiyomi.multisrc.pawchive - -import android.app.Application -import androidx.preference.ListPreference -import androidx.preference.PreferenceScreen -import androidx.preference.SwitchPreferenceCompat -import eu.kanade.tachiyomi.multisrc.pawchive.PawchiveCreatorDto.Companion.serviceName -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.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 keiyoushi.network.rateLimit -import keiyoushi.utils.getPreferences -import keiyoushi.utils.parseAs -import okhttp3.Cache -import okhttp3.CacheControl -import okhttp3.Request -import okhttp3.Response -import okhttp3.brotli.BrotliInterceptor -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.File -import java.lang.Thread.sleep -import java.util.TimeZone -import kotlin.math.min -import kotlin.time.Duration.Companion.minutes - -abstract class Pawchive : - HttpSource(), - ConfigurableSource { - override val supportsLatest = true - - override val client = network.client.newBuilder() - .addInterceptor { chain -> - val request = chain.request() - - // 1. Existing API fix - val modifiedRequest = if (request.url.pathSegments.firstOrNull() == "api") { - request.newBuilder().header("Accept", "text/css").build() - } else { - request - } - - var response = chain.proceed(modifiedRequest) - - // 2. New Fallback Logic for Images - // If the response is a 404 Not Found AND the request was aiming for the full-size file server - if (response.code == 404 && request.url.toString().startsWith(fileUrl)) { - - // Check if the user has the fallback toggle enabled - if (preferences.getBoolean(FALLBACK_LOW_RES_IMG, true)) { - response.close() // Close the failed response to prevent memory leaks - - val imageUrl = request.url.toString() - val pathIndex = imageUrl.indexOf('/', 8) - - // Build the new thumbnail URL - val fallbackUrl = buildString { - append(imgUrl) - append("/thumbnail") - append(imageUrl.substring(pathIndex)) - } - - // Retry the request seamlessly with the new fallback URL - val fallbackRequest = request.newBuilder().url(fallbackUrl).build() - response = chain.proceed(fallbackRequest) - } - } - - response - } - .cache( - Cache( - directory = File(applicationContext.externalCacheDir, "network_cache_${name.lowercase()}"), - maxSize = 50L * 1024 * 1024, // 50 MiB - ), - ) - .rateLimit(1) - .build() - - private val creatorsClient = client.newBuilder() - .readTimeout(5.minutes) - .build() - - override fun headersBuilder() = super.headersBuilder() - .add("Referer", "$baseUrl/") - - private val preferences = getPreferences() - - private val apiPath = "api/v1" - - private val dataPath = "data" - - private val avatarUrl = baseUrl - - private val fileUrl = baseUrl.replace("//", "//file.") - - private val imgUrl = baseUrl.replace("//", "//img.") - - private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', avatarUrl) - - override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() - - override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() - - override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() - - override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() - - override fun fetchPopularManga(page: Int): Observable = Observable.fromCallable { - searchMangas(page, sortBy = "pop" to "desc") - } - - override fun fetchLatestUpdates(page: Int): Observable = Observable.fromCallable { - searchMangas(page, sortBy = "lat" to "desc") - } - - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { - searchMangas(page, query, filters) - } - - private fun searchMangas(page: Int = 1, title: String = "", filters: FilterList? = null, sortBy: Pair = "" to ""): MangasPage { - var sort = sortBy - val typeIncluded: MutableList = mutableListOf() - val typeExcluded: MutableList = mutableListOf() - var fav: Boolean? = null - filters?.forEach { filter -> - when (filter) { - is SortFilter -> { - sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc" - } - - is TypeFilter -> { - filter.state.filter { state -> state.isIncluded() }.forEach { tri -> - typeIncluded.add(tri.value) - } - - filter.state.filter { state -> state.isExcluded() }.forEach { tri -> - typeExcluded.add(tri.value) - } - } - - is FavoritesFilter -> { - fav = when (filter.state[0].state) { - 0 -> null - 1 -> true - else -> false - } - } - - else -> {} - } - } - - val mangas = run { - val favorites = if (fav != null) { - val response = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute() - - if (response.isSuccessful) { - response.parseAs>().filterNot { it.service.lowercase() == "discord" } - } else { - response.close() - val message = if (response.code == 401) "You are not logged in" else "HTTP error ${response.code}" - throw Exception("Failed to fetch favorites: $message") - } - } else { - emptyList() - } - - val request = GET( - "$baseUrl/$apiPath/creators", - headers, - CacheControl.Builder().maxStale(30.minutes).build(), - ) - val response = creatorsClient.newCall(request).execute() - if (!response.isSuccessful) { - response.close() - throw Exception("HTTP error ${response.code}") - } - val allCreators = response.parseAs>().filterNot { it.service.lowercase() == "discord" } - allCreators.filter { - val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase()) - val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase()) - - val regularSearch = it.name.contains(title, true) - - val isFavorited = when (fav) { - true -> favorites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } } - false -> favorites.none { f -> f.id == it.id } - else -> true - } - - includeType && !excludeType && isFavorited && - regularSearch - } - } - - val sorted = when (sort.first) { - "pop" -> { - if (sort.second == "desc") { - mangas.sortedByDescending { it.favorited } - } else { - mangas.sortedBy { it.favorited } - } - } - - "tit" -> { - if (sort.second == "desc") { - mangas.sortedByDescending { it.name } - } else { - mangas.sortedBy { it.name } - } - } - - "new" -> { - if (sort.second == "desc") { - mangas.sortedByDescending { it.id } - } else { - mangas.sortedBy { it.id } - } - } - - "fav" -> { - if (fav != true) throw Exception("Please check 'Favorites Only' Filter") - if (sort.second == "desc") { - mangas.sortedByDescending { it.fav } - } else { - mangas.sortedBy { it.fav } - } - } - - else -> { - if (sort.second == "desc") { - mangas.sortedByDescending { it.updatedDate } - } else { - mangas.sortedBy { it.updatedDate } - } - } - } - val maxIndex = mangas.size - val fromIndex = (page - 1) * PAGE_CREATORS_LIMIT - val toIndex = min(maxIndex, fromIndex + PAGE_CREATORS_LIMIT) - - val final = sorted.subList(fromIndex, toIndex).map { it.toSManga(avatarUrl) } - return MangasPage(final, toIndex != maxIndex) - } - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() - override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() - - override fun fetchMangaDetails(manga: SManga): Observable { - manga.thumbnail_url = manga.thumbnail_url!!.formatAvatarUrl() - return Observable.just(manga) - } - - override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException() - - override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url.replace("$apiPath/", "")}" - - override fun fetchChapterList(manga: SManga): Observable> = Observable.fromCallable { - PawchivePostDto.dateFormat.timeZone = when (manga.author) { - "Pixiv Fanbox" -> TimeZone.getTimeZone("GMT+09:00") - else -> TimeZone.getTimeZone("GMT") - } - val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!! - .toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT - var offset = 0 - var hasNextPage = true - val result = ArrayList() - while (offset < prefMaxPost && hasNextPage) { - val request = GET("$baseUrl/$apiPath${manga.url}/posts?o=$offset", headers) - val page: List = retry(request).parseAs() - page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) } - offset += PAGE_POST_LIMIT - hasNextPage = page.size == PAGE_POST_LIMIT - } - result - } - - private fun retry(request: Request): Response { - var code = 0 - repeat(5) { - val response = client.newCall(request).execute() - if (response.isSuccessful) return response - response.close() - code = response.code - if (code == 429) { - sleep(10000) - } - } - throw Exception("HTTP error $code") - } - - override fun chapterListParse(response: Response) = throw UnsupportedOperationException() - - override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl/$apiPath${chapter.url}", headers) - - override fun pageListParse(response: Response): List { - val postData: PawchivePostDto = response.parseAs() - return postData.images.mapIndexed { i, path -> Page(i, imageUrl = "$fileUrl/$dataPath$path") } - } - - override fun imageRequest(page: Page): Request { - val imageUrl = page.imageUrl!! - - if (!preferences.getBoolean(USE_LOW_RES_IMG, false)) return GET(imageUrl, headers) - - val index = imageUrl.indexOf('/', 8) - val url = buildString { - append(imgUrl) - append("/thumbnail") - append(imageUrl.substring(index)) - } - return GET(url, headers) - } - - override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() - - override fun setupPreferenceScreen(screen: PreferenceScreen) { - ListPreference(screen.context).apply { - key = POST_PAGES_PREF - title = "Maximum posts to load" - summary = "Loading more posts costs more time and network traffic.\nCurrently: %s" - entryValues = Array(POST_PAGES_MAX) { (it + 1).toString() } - entries = Array(POST_PAGES_MAX) { "${(it + 1)} pages (${(it + 1) * PAGE_POST_LIMIT} posts)" } - setDefaultValue(POST_PAGES_DEFAULT) - }.let { screen.addPreference(it) } - - SwitchPreferenceCompat(screen.context).apply { - key = USE_LOW_RES_IMG - title = "Use low resolution images" - summary = "Reduce load time significantly. When turning off, clear chapter cache to remove cached low resolution images." - setDefaultValue(false) - }.let(screen::addPreference) - - SwitchPreferenceCompat(screen.context).apply { - key = FALLBACK_LOW_RES_IMG - title = "Fallback to low resolution images" - summary = "If a full size image fails to load (e.g. 404 Not Found), automatically try loading the low resolution thumbnail instead." - setDefaultValue(true) // Enabled by default - }.let(screen::addPreference) - } - - // Filters - - override fun getFilterList(): FilterList = FilterList( - SortFilter( - "Sort by", - Filter.Sort.Selection(0, false), - getSortsList, - ), - TypeFilter("Types", getTypes), - FavoritesFilter(), - ) - - open val getTypes: List = emptyList() - - open val getSortsList: List> = listOf( - Pair("Popularity", "pop"), - Pair("Date Indexed", "new"), - Pair("Date Updated", "lat"), - Pair("Alphabetical Order", "tit"), - Pair("Service", "serv"), - Pair("Date Favorited", "fav"), - ) - - internal open class TypeFilter(name: String, vals: List) : - Filter.Group( - name, - vals.map { TriFilter(it, it.lowercase()) }, - ) - - internal class FavoritesFilter : - Filter.Group( - "Favorites", - listOf(TriFilter("Favorites Only", "fav")), - ) - - internal open class TriFilter(name: String, val value: String) : Filter.TriState(name) - - internal open class SortFilter(name: String, selection: Selection, private val vals: List>) : Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) { - fun getValue() = vals[state!!.index].second - } - - companion object { - private const val PAGE_POST_LIMIT = 50 - private const val PAGE_CREATORS_LIMIT = 50 - const val PROMPT = "You can change how many posts to load in the extension preferences." - - private const val POST_PAGES_PREF = "POST_PAGES" - private const val POST_PAGES_DEFAULT = "1" - private const val POST_PAGES_MAX = 75 - - // private const val BASE_URL_PREF = "BASE_URL" - private const val USE_LOW_RES_IMG = "USE_LOW_RES_IMG" - - private const val FALLBACK_LOW_RES_IMG = "FALLBACK_LOW_RES_IMG" - } -} diff --git a/src/all/pawchive/build.gradle.kts b/src/all/pawchive/build.gradle.kts index 1bdaa5b4313..0faa633e677 100644 --- a/src/all/pawchive/build.gradle.kts +++ b/src/all/pawchive/build.gradle.kts @@ -1,18 +1,17 @@ import io.github.keiyoushi.gradle.api.ContentWarning plugins { - alias(kei.plugins.extension) + alias(kei.plugins.extension) } keiyoushi { - name = "Pawchive" - versionCode = 1 - contentWarning = ContentWarning.NSFW - libVersion = "1.4" - theme = "pawchive" + name = "Pawchive" + versionCode = 1 + contentWarning = ContentWarning.NSFW + libVersion = "1.4" - source { - lang = "all" - baseUrl = "https://pawchive.pw" - } + source { + lang = "all" + baseUrl = "https://pawchive.pw" + } } diff --git a/lib-multisrc/pawchive/res/mipmap-hdpi/ic_launcher.png b/src/all/pawchive/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from lib-multisrc/pawchive/res/mipmap-hdpi/ic_launcher.png rename to src/all/pawchive/res/mipmap-hdpi/ic_launcher.png diff --git a/lib-multisrc/pawchive/res/mipmap-mdpi/ic_launcher.png b/src/all/pawchive/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from lib-multisrc/pawchive/res/mipmap-mdpi/ic_launcher.png rename to src/all/pawchive/res/mipmap-mdpi/ic_launcher.png diff --git a/lib-multisrc/pawchive/res/mipmap-xhdpi/ic_launcher.png b/src/all/pawchive/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from lib-multisrc/pawchive/res/mipmap-xhdpi/ic_launcher.png rename to src/all/pawchive/res/mipmap-xhdpi/ic_launcher.png diff --git a/lib-multisrc/pawchive/res/mipmap-xxhdpi/ic_launcher.png b/src/all/pawchive/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from lib-multisrc/pawchive/res/mipmap-xxhdpi/ic_launcher.png rename to src/all/pawchive/res/mipmap-xxhdpi/ic_launcher.png diff --git a/lib-multisrc/pawchive/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/pawchive/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from lib-multisrc/pawchive/res/mipmap-xxxhdpi/ic_launcher.png rename to src/all/pawchive/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt new file mode 100644 index 00000000000..5755e05554e --- /dev/null +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt @@ -0,0 +1,127 @@ +package eu.kanade.tachiyomi.extension.all.pawchive + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.double +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +@Serializable +class PawchiveFavoritesDto( + val id: String, + val faved_seq: Long, +) + +@Serializable +class PawchiveCreatorDto( + val id: String, + val name: String, + val service: String, + private val updated: JsonPrimitive, + val favorited: Int = -1, +) { + @Transient + var fav: Long = 0L + + val updatedDate get() = when { + updated.isString -> try { + dateFormat.get()!!.parse(updated.content)?.time ?: 0L + } catch (_: Exception) { + 0L + } + else -> (updated.double * 1000).toLong() + } + + fun toSManga(baseUrl: String) = SManga.create().apply { + url = "/$service/user/$id" + title = name + author = service.serviceName() + thumbnail_url = "$baseUrl/icons/$service/$id" + description = Pawchive.PROMPT + initialized = true + } + + companion object { + val dateFormat = object : ThreadLocal() { + override fun initialValue() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).apply { + timeZone = TimeZone.getTimeZone("GMT") + } + } + + fun String.serviceName() = when (this) { + "fanbox" -> "Pixiv Fanbox" + else -> replaceFirstChar { it.uppercase() } + } + } +} + +@Serializable +class PawchivePostDto( + private val id: String, + private val service: String, + private val user: String, + private val title: String, + private val added: String? = null, + private val published: String? = null, + private val edited: String? = null, + private val file: PawchiveFileDto? = null, + private val attachments: List = emptyList(), +) { + val images: List + get() = (listOfNotNull(file) + attachments) + .filter { it.path != null && it.path.substringAfterLast('.').lowercase() in validImageExtensions } + .distinctBy { it.path } + .map { it.toString() } + + fun toSChapter(serviceName: String?, datePref: String) = SChapter.create().apply { + val dateStr = when (datePref) { + "added" -> added ?: published ?: edited + "edited" -> edited ?: published ?: added + else -> published ?: added ?: edited // Default to published + } + + val format = dateFormat.get()!! + + // Prevents leaking SimpleDateFormat mutations across concurrently fetched mangas + format.timeZone = if (serviceName == "Pixiv Fanbox") TimeZone.getTimeZone("GMT+09:00") else TimeZone.getTimeZone("GMT") + + val postDate = try { + if (dateStr != null) format.parse(dateStr)?.time ?: 0L else 0L + } catch (_: Exception) { + 0L + } + + url = "/$service/user/$user/post/$id" + date_upload = postDate + name = title.ifBlank { + val postDateString = if (postDate != 0L) { + chapterNameDateFormat.get()!!.apply { timeZone = format.timeZone }.format(postDate) + } else { + "unknown date" + } + "Post from $postDateString" + } + chapter_number = -2f + } + + companion object { + private val validImageExtensions = setOf("png", "jpg", "gif", "jpeg", "webp") + + private val dateFormat = object : ThreadLocal() { + override fun initialValue() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH) + } + + private val chapterNameDateFormat = object : ThreadLocal() { + override fun initialValue() = SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss", Locale.ENGLISH) + } + } +} + +@Serializable +class PawchiveFileDto(val name: String? = null, val path: String? = null) { + override fun toString() = path + if (name != null) "?f=$name" else "" +} diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index 31b8f47c887..1d25b974c00 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -1,12 +1,338 @@ package eu.kanade.tachiyomi.extension.all.pawchive -import eu.kanade.tachiyomi.multisrc.pawchive.Pawchive +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.extension.all.pawchive.PawchiveCreatorDto.Companion.serviceName +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.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 keiyoushi.annotation.Source +import keiyoushi.network.rateLimit +import keiyoushi.utils.applicationContext +import keiyoushi.utils.getPreferences +import keiyoushi.utils.parseAs +import okhttp3.Cache +import okhttp3.CacheControl +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import java.io.File +import kotlin.math.min +import kotlin.time.Duration.Companion.minutes @Source -abstract class Pawchive : Pawchive() { - override val getTypes = listOf( - "Patreon", - "Pixiv Fanbox", +abstract class Pawchive : + HttpSource(), + ConfigurableSource { + override val supportsLatest = true + + private val preferences = getPreferences() + private val apiPath = "api/v1" + private val dataPath = "data" + private val fileUrl = baseUrl.replace("//", "//file.") + private val imgUrl = baseUrl.replace("//", "//img.") + + override val client = network.client.newBuilder() + .addInterceptor { chain -> + val request = chain.request() + + // 1. Existing API fix + val modifiedRequest = if (request.url.pathSegments.firstOrNull() == "api") { + request.newBuilder().header("Accept", "text/css").build() + } else { + request + } + + var response = chain.proceed(modifiedRequest) + + // 2. New Fallback Logic for Images + if (response.code == 404 && request.url.toString().startsWith(fileUrl)) { + if (preferences.getBoolean(FALLBACK_LOW_RES_IMG, true)) { + response.close() // Close the failed response to prevent memory leaks + + val fallbackRequest = request.newBuilder() + .url(request.url.toString().toThumbnailUrl()) + .build() + response = chain.proceed(fallbackRequest) + } + } + + response + } + .cache( + Cache( + directory = File(applicationContext.externalCacheDir, "network_cache_${name.lowercase()}"), + maxSize = 50L * 1024 * 1024, // 50 MiB + ), + ) + .rateLimit(1) + .build() + + private val creatorsClient = client.newBuilder() + .readTimeout(5.minutes) + .build() + + override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/") + + private fun String.toThumbnailUrl(): String { + val pathIndex = this.indexOf('/', 8) // Skips the `https://` + return buildString { + append(imgUrl) + append("/thumbnail") + append(this@toThumbnailUrl.substring(pathIndex)) + } + } + + override fun fetchPopularManga(page: Int): Observable = Observable.fromCallable { + searchMangas(page, sortBy = "pop" to "desc") + } + + override fun fetchLatestUpdates(page: Int): Observable = Observable.fromCallable { + searchMangas(page, sortBy = "lat" to "desc") + } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { + searchMangas(page, query, filters) + } + + private fun searchMangas(page: Int = 1, title: String = "", filters: FilterList? = null, sortBy: Pair = "" to ""): MangasPage { + var sort = sortBy + val typeIncluded = mutableListOf() + val typeExcluded = mutableListOf() + var fav: Boolean? = null + + filters?.forEach { filter -> + when (filter) { + is SortFilter -> sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc" + is TypeFilter -> { + filter.state.forEach { tri -> + if (tri.isIncluded()) typeIncluded.add(tri.value) + if (tri.isExcluded()) typeExcluded.add(tri.value) + } + } + is FavoritesFilter -> { + fav = when (filter.state[0].state) { + 1 -> true + 2 -> false + else -> null + } + } + else -> {} + } + } + + val mangas = run { + val favorites = if (fav != null) { + val response = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute() + if (response.isSuccessful) { + response.parseAs>() + } else { + response.close() + val message = if (response.code == 401) "You are not logged in" else "HTTP error ${response.code}" + throw Exception("Failed to fetch favorites: $message") + } + } else { + emptyList() + } + + val request = GET( + "$baseUrl/$apiPath/creators", + headers, + CacheControl.Builder().maxStale(30.minutes).build(), + ) + val response = creatorsClient.newCall(request).execute() + if (!response.isSuccessful) { + response.close() + throw Exception("HTTP error ${response.code}") + } + + val allCreators = response.parseAs>() + + allCreators.filter { creator -> + val sName = creator.service.serviceName().lowercase() + val includeType = typeIncluded.isEmpty() || typeIncluded.contains(sName) + val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(sName) + val regularSearch = creator.name.contains(title, true) + + val favEntry = favorites.find { f -> f.id == creator.id } + creator.fav = favEntry?.faved_seq ?: 0L + + val isFavorited = when (fav) { + true -> favEntry != null + false -> favEntry == null + else -> true + } + + includeType && !excludeType && isFavorited && regularSearch + } + } + + val sorted = when (sort.first) { + "pop" -> if (sort.second == "desc") mangas.sortedByDescending { it.favorited } else mangas.sortedBy { it.favorited } + "tit" -> if (sort.second == "desc") mangas.sortedByDescending { it.name.lowercase() } else mangas.sortedBy { it.name.lowercase() } + "new" -> if (sort.second == "desc") mangas.sortedByDescending { it.id.toLongOrNull() ?: 0L } else mangas.sortedBy { it.id.toLongOrNull() ?: 0L } + "fav" -> { + if (fav != true) throw Exception("Please check 'Favorites Only' Filter") + if (sort.second == "desc") mangas.sortedByDescending { it.fav } else mangas.sortedBy { it.fav } + } + else -> if (sort.second == "desc") mangas.sortedByDescending { it.updatedDate } else mangas.sortedBy { it.updatedDate } + } + + val maxIndex = sorted.size + val fromIndex = (page - 1) * PAGE_CREATORS_LIMIT + val toIndex = min(maxIndex, fromIndex + PAGE_CREATORS_LIMIT) + val final = sorted.subList(fromIndex, toIndex).map { it.toSManga(baseUrl) } + + return MangasPage(final, toIndex != maxIndex) + } + + override fun fetchMangaDetails(manga: SManga): Observable = Observable.just(manga) + + override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}" + + override fun fetchChapterList(manga: SManga): Observable> = Observable.fromCallable { + val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!! + .toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT + + val datePref = preferences.getString(POST_DATE_PREF, POST_DATE_PUBLISHED)!! + + var offset = 0 + var hasNextPage = true + val result = ArrayList() + + while (offset < prefMaxPost && hasNextPage) { + val request = GET("$baseUrl/$apiPath${manga.url}/posts?o=$offset", headers) + val page: List = retry(request).parseAs() + + page.forEach { post -> + if (post.images.isNotEmpty()) { + result.add(post.toSChapter(manga.author, datePref)) + } + } + + offset += PAGE_POST_LIMIT + hasNextPage = page.size == PAGE_POST_LIMIT + } + result + } + + private fun retry(request: Request): Response { + var code = 0 + repeat(5) { + val response = client.newCall(request).execute() + if (response.isSuccessful) return response + response.close() + code = response.code + if (code == 429) { + Thread.sleep(10000) + } + } + throw Exception("HTTP error $code") + } + + override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl/$apiPath${chapter.url}", headers) + + override fun pageListParse(response: Response): List { + val postData: PawchivePostDto = response.parseAs() + return postData.images.mapIndexed { i, path -> Page(i, imageUrl = "$fileUrl/$dataPath$path") } + } + + override fun imageRequest(page: Page): Request { + val imageUrl = page.imageUrl!! + return GET(if (preferences.getBoolean(USE_LOW_RES_IMG, false)) imageUrl.toThumbnailUrl() else imageUrl, headers) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + key = POST_PAGES_PREF + title = "Maximum posts to load" + summary = "Loading more posts costs more time and network traffic.\nCurrently: %s" + entryValues = Array(POST_PAGES_MAX) { (it + 1).toString() } + entries = Array(POST_PAGES_MAX) { "${(it + 1)} pages (${(it + 1) * PAGE_POST_LIMIT} posts)" } + setDefaultValue(POST_PAGES_DEFAULT) + }.let { screen.addPreference(it) } + + ListPreference(screen.context).apply { + key = POST_DATE_PREF + title = "Preferred Post Date" + summary = "Choose which date to use for chapters.\nCurrently: %s" + entries = arrayOf("Added Date", "Published Date", "Edited Date") + entryValues = arrayOf(POST_DATE_ADDED, POST_DATE_PUBLISHED, POST_DATE_EDITED) + setDefaultValue(POST_DATE_PUBLISHED) + }.let(screen::addPreference) + + SwitchPreferenceCompat(screen.context).apply { + key = USE_LOW_RES_IMG + title = "Use low resolution images" + summary = "Reduce load time significantly. When turning off, clear chapter cache to remove cached low resolution images." + setDefaultValue(false) + }.let(screen::addPreference) + + SwitchPreferenceCompat(screen.context).apply { + key = FALLBACK_LOW_RES_IMG + title = "Fallback to low resolution images" + summary = "If a full size image fails to load (e.g. 404 Not Found), automatically try loading the low resolution thumbnail instead." + setDefaultValue(true) + }.let(screen::addPreference) + } + + override fun getFilterList(): FilterList = FilterList( + SortFilter("Sort by", Filter.Sort.Selection(0, false), getSortsList), + TypeFilter("Types", getTypes), + FavoritesFilter(), + ) + + open val getTypes = listOf("Patreon", "Pixiv Fanbox") + + open val getSortsList: List> = listOf( + Pair("Popularity", "pop"), + Pair("Date Indexed", "new"), + Pair("Date Updated", "lat"), + Pair("Alphabetical Order", "tit"), + Pair("Service", "serv"), + Pair("Date Favorited", "fav"), ) -} \ No newline at end of file + + internal open class TypeFilter(name: String, vals: List) : Filter.Group(name, vals.map { TriFilter(it, it.lowercase()) }) + internal class FavoritesFilter : Filter.Group("Favorites", listOf(TriFilter("Favorites Only", "fav"))) + internal open class TriFilter(name: String, val value: String) : Filter.TriState(name) + internal open class SortFilter(name: String, selection: Selection, private val vals: List>) : Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) { + fun getValue() = vals[state!!.index].second + } + + // Required by HttpSource when no generic overrides are in place + override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() + override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() + override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() + override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() + override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException() + override fun chapterListParse(response: Response) = throw UnsupportedOperationException() + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + companion object { + private const val PAGE_POST_LIMIT = 50 + private const val PAGE_CREATORS_LIMIT = 50 + const val PROMPT = "You can change how many posts to load in the extension preferences." + + private const val POST_PAGES_PREF = "POST_PAGES" + private const val POST_PAGES_DEFAULT = "1" + private const val POST_PAGES_MAX = 75 + + private const val USE_LOW_RES_IMG = "USE_LOW_RES_IMG" + private const val FALLBACK_LOW_RES_IMG = "FALLBACK_LOW_RES_IMG" + + private const val POST_DATE_PREF = "POST_DATE_PREF" + private const val POST_DATE_ADDED = "added" + private const val POST_DATE_PUBLISHED = "published" + private const val POST_DATE_EDITED = "edited" + } +} From aaae4697cfbe4ff0520e4dd4bf1e2b50948f0cb5 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:05:19 +0700 Subject: [PATCH 12/21] 1.6 --- src/all/pawchive/build.gradle.kts | 2 +- .../tachiyomi/extension/all/pawchive/Dto.kt | 13 +- .../extension/all/pawchive/Pawchive.kt | 235 ++++++++++-------- 3 files changed, 139 insertions(+), 111 deletions(-) diff --git a/src/all/pawchive/build.gradle.kts b/src/all/pawchive/build.gradle.kts index 0faa633e677..f8aa1c87aa5 100644 --- a/src/all/pawchive/build.gradle.kts +++ b/src/all/pawchive/build.gradle.kts @@ -8,7 +8,7 @@ keiyoushi { name = "Pawchive" versionCode = 1 contentWarning = ContentWarning.NSFW - libVersion = "1.4" + libVersion = "1.6" source { lang = "all" diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt index 5755e05554e..24b9613e312 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.all.pawchive import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import keiyoushi.utils.tryParse import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.json.JsonPrimitive @@ -28,11 +29,7 @@ class PawchiveCreatorDto( var fav: Long = 0L val updatedDate get() = when { - updated.isString -> try { - dateFormat.get()!!.parse(updated.content)?.time ?: 0L - } catch (_: Exception) { - 0L - } + updated.isString -> dateFormat.get()!!.tryParse(updated.content) else -> (updated.double * 1000).toLong() } @@ -89,11 +86,7 @@ class PawchivePostDto( // Prevents leaking SimpleDateFormat mutations across concurrently fetched mangas format.timeZone = if (serviceName == "Pixiv Fanbox") TimeZone.getTimeZone("GMT+09:00") else TimeZone.getTimeZone("GMT") - val postDate = try { - if (dateStr != null) format.parse(dateStr)?.time ?: 0L else 0L - } catch (_: Exception) { - 0L - } + val postDate = format.tryParse(dateStr) url = "/$service/user/$user/post/$id" date_upload = postDate diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index 1d25b974c00..eb92974e418 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -5,6 +5,7 @@ import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.extension.all.pawchive.PawchiveCreatorDto.Companion.serviceName import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.await // Imported for non-blocking network calls import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList @@ -12,35 +13,43 @@ 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 keiyoushi.annotation.Source +import keiyoushi.network.get import keiyoushi.network.rateLimit +import keiyoushi.source.KeiSource import keiyoushi.utils.applicationContext import keiyoushi.utils.getPreferences import keiyoushi.utils.parseAs +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.serialization.json.JsonElement import okhttp3.Cache import okhttp3.CacheControl -import okhttp3.Request -import okhttp3.Response -import rx.Observable +import okhttp3.HttpUrl +import okhttp3.OkHttpClient import java.io.File +import kotlin.io.use import kotlin.math.min import kotlin.time.Duration.Companion.minutes @Source abstract class Pawchive : - HttpSource(), + KeiSource(), ConfigurableSource { - override val supportsLatest = true private val preferences = getPreferences() + private val apiPath = "api/v1" + private val dataPath = "data" + private val fileUrl = baseUrl.replace("//", "//file.") + private val imgUrl = baseUrl.replace("//", "//img.") - override val client = network.client.newBuilder() - .addInterceptor { chain -> + override fun OkHttpClient.Builder.configureClient() = apply { + addInterceptor { chain -> val request = chain.request() // 1. Existing API fix @@ -66,21 +75,19 @@ abstract class Pawchive : response } - .cache( - Cache( - directory = File(applicationContext.externalCacheDir, "network_cache_${name.lowercase()}"), - maxSize = 50L * 1024 * 1024, // 50 MiB - ), - ) - .rateLimit(1) - .build() + .cache( + Cache( + directory = File(applicationContext.externalCacheDir, "network_cache_${name.lowercase()}"), + maxSize = 50L * 1024 * 1024, // 50 MiB + ), + ) + .rateLimit(1) + } private val creatorsClient = client.newBuilder() .readTimeout(5.minutes) .build() - override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/") - private fun String.toThumbnailUrl(): String { val pathIndex = this.indexOf('/', 8) // Skips the `https://` return buildString { @@ -90,19 +97,13 @@ abstract class Pawchive : } } - override fun fetchPopularManga(page: Int): Observable = Observable.fromCallable { - searchMangas(page, sortBy = "pop" to "desc") - } + override suspend fun getPopularManga(page: Int): MangasPage = searchMangas(page, sortBy = "pop" to "desc") - override fun fetchLatestUpdates(page: Int): Observable = Observable.fromCallable { - searchMangas(page, sortBy = "lat" to "desc") - } + override suspend fun getLatestUpdates(page: Int): MangasPage = searchMangas(page, sortBy = "lat" to "desc") - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { - searchMangas(page, query, filters) - } + override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage = searchMangas(page, query, filters) - private fun searchMangas(page: Int = 1, title: String = "", filters: FilterList? = null, sortBy: Pair = "" to ""): MangasPage { + private suspend fun searchMangas(page: Int = 1, title: String = "", filters: FilterList? = null, sortBy: Pair = "" to ""): MangasPage { var sort = sortBy val typeIncluded = mutableListOf() val typeExcluded = mutableListOf() @@ -128,33 +129,43 @@ abstract class Pawchive : } } - val mangas = run { - val favorites = if (fav != null) { - val response = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute() - if (response.isSuccessful) { - response.parseAs>() + val mangas = coroutineScope { + // 1. Start fetching favorites in the background + val favoritesDeferred = async { + if (fav != null) { + client.get("$baseUrl/$apiPath/account/favorites", headers).use { response -> + if (response.isSuccessful) { + response.parseAs>() + } else { + val message = if (response.code == 401) "You are not logged in" else "HTTP error ${response.code}" + throw Exception("Failed to fetch favorites: $message") + } + } } else { - response.close() - val message = if (response.code == 401) "You are not logged in" else "HTTP error ${response.code}" - throw Exception("Failed to fetch favorites: $message") + emptyList() } - } else { - emptyList() } - val request = GET( - "$baseUrl/$apiPath/creators", - headers, - CacheControl.Builder().maxStale(30.minutes).build(), - ) - val response = creatorsClient.newCall(request).execute() - if (!response.isSuccessful) { - response.close() - throw Exception("HTTP error ${response.code}") + // 2. Start fetching creators in the background simultaneously + val creatorsDeferred = async { + val request = GET( + "$baseUrl/$apiPath/creators", + headers, + CacheControl.Builder().maxStale(30.minutes).build(), + ) + creatorsClient.newCall(request).await().use { response -> + if (!response.isSuccessful) { + throw Exception("HTTP error ${response.code}") + } + response.parseAs>() + } } - val allCreators = response.parseAs>() + // 3. Wait for both background tasks to finish + val favorites = favoritesDeferred.await() + val allCreators = creatorsDeferred.await() + // 4. Perform the filter mapping as before allCreators.filter { creator -> val sName = creator.service.serviceName().lowercase() val includeType = typeIncluded.isEmpty() || typeIncluded.contains(sName) @@ -193,60 +204,94 @@ abstract class Pawchive : return MangasPage(final, toIndex != maxIndex) } - override fun fetchMangaDetails(manga: SManga): Observable = Observable.just(manga) + override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}" override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}" - override fun fetchChapterList(manga: SManga): Observable> = Observable.fromCallable { - val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!! - .toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT + // RESOLVES: getMangaByUrl(url) + override suspend fun getMangaByUrl(url: HttpUrl): SManga? { + val id = url.pathSegments.lastOrNull() ?: return null + val request = GET("$baseUrl/$apiPath/creators", headers, CacheControl.Builder().maxStale(30.minutes).build()) + val response = creatorsClient.newCall(request).await() + if (!response.isSuccessful) { + response.close() + return null + } + val allCreators = response.parseAs>() + val creator = allCreators.find { it.id == id } ?: return null + return creator.toSManga(baseUrl) + } + + // RESOLVES: fetchRelatedMangaList(manga) + override val supportsRelatedMangas get() = false - val datePref = preferences.getString(POST_DATE_PREF, POST_DATE_PUBLISHED)!! + override suspend fun fetchRelatedMangaList(manga: SManga): List = emptyList() - var offset = 0 - var hasNextPage = true - val result = ArrayList() + override suspend fun fetchMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate { + // We aren't fetching new details in this snippet, so we pass the existing manga back + val updatedManga = manga - while (offset < prefMaxPost && hasNextPage) { - val request = GET("$baseUrl/$apiPath${manga.url}/posts?o=$offset", headers) - val page: List = retry(request).parseAs() + val updatedChapters = if (fetchChapters) { + val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!! + .toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT - page.forEach { post -> - if (post.images.isNotEmpty()) { - result.add(post.toSChapter(manga.author, datePref)) - } - } + val datePref = preferences.getString(POST_DATE_PREF, POST_DATE_PUBLISHED)!! - offset += PAGE_POST_LIMIT - hasNextPage = page.size == PAGE_POST_LIMIT - } - result - } + var offset = 0 + var hasNextPage = true + val result = ArrayList() - private fun retry(request: Request): Response { - var code = 0 - repeat(5) { - val response = client.newCall(request).execute() - if (response.isSuccessful) return response - response.close() - code = response.code - if (code == 429) { - Thread.sleep(10000) + while (offset < prefMaxPost && hasNextPage) { + val url = "$baseUrl/$apiPath${manga.url}/posts?o=$offset" + + // Using the client.get().use {} pattern from your example + client.get(url, headers).use { response -> + val page: List = response.parseAs() + + page.forEach { post -> + if (post.images.isNotEmpty()) { + result.add(post.toSChapter(manga.author, datePref)) + } + } + + offset += PAGE_POST_LIMIT + hasNextPage = page.size == PAGE_POST_LIMIT + } } + result + } else { + chapters } - throw Exception("HTTP error $code") + + return SMangaUpdate(updatedManga, updatedChapters) } - override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl/$apiPath${chapter.url}", headers) + // 4. Pages/Images + override suspend fun getPageList(chapter: SChapter): List { + val url = "$baseUrl/$apiPath${chapter.url}" - override fun pageListParse(response: Response): List { - val postData: PawchivePostDto = response.parseAs() - return postData.images.mapIndexed { i, path -> Page(i, imageUrl = "$fileUrl/$dataPath$path") } - } + // 1. Fetch and parse the page list + client.get(url, headers).use { response -> + val postData: PawchivePostDto = response.parseAs() + + // 2. Grab the preference ONCE before mapping to save performance + val useLowRes = preferences.getBoolean(USE_LOW_RES_IMG, false) + + // 3. Map the images and apply the low-res logic immediately + return postData.images.mapIndexed { i, path -> + val originalUrl = "$fileUrl/$dataPath$path" - override fun imageRequest(page: Page): Request { - val imageUrl = page.imageUrl!! - return GET(if (preferences.getBoolean(USE_LOW_RES_IMG, false)) imageUrl.toThumbnailUrl() else imageUrl, headers) + // This replaces your old `imageRequest` logic + val finalImageUrl = if (useLowRes) originalUrl.toThumbnailUrl() else originalUrl + + Page(i, imageUrl = finalImageUrl) + } + } } override fun setupPreferenceScreen(screen: PreferenceScreen) { @@ -278,12 +323,13 @@ abstract class Pawchive : SwitchPreferenceCompat(screen.context).apply { key = FALLBACK_LOW_RES_IMG title = "Fallback to low resolution images" - summary = "If a full size image fails to load (e.g. 404 Not Found), automatically try loading the low resolution thumbnail instead." + summary = + "If a full size image fails to load (e.g. 404 Not Found), automatically try loading the low resolution thumbnail instead." setDefaultValue(true) }.let(screen::addPreference) } - override fun getFilterList(): FilterList = FilterList( + override fun getFilterList(data: JsonElement?): FilterList = FilterList( SortFilter("Sort by", Filter.Sort.Selection(0, false), getSortsList), TypeFilter("Types", getTypes), FavoritesFilter(), @@ -307,17 +353,6 @@ abstract class Pawchive : fun getValue() = vals[state!!.index].second } - // Required by HttpSource when no generic overrides are in place - override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() - override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() - override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() - override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() - override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() - override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() - override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException() - override fun chapterListParse(response: Response) = throw UnsupportedOperationException() - override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() - companion object { private const val PAGE_POST_LIMIT = 50 private const val PAGE_CREATORS_LIMIT = 50 From 7dc3de523a17bbe724484d783628dfad272349e8 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:39:25 +0700 Subject: [PATCH 13/21] Update src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- .../eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index eb92974e418..a5de747d013 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -75,13 +75,13 @@ abstract class Pawchive : response } - .cache( + cache( Cache( directory = File(applicationContext.externalCacheDir, "network_cache_${name.lowercase()}"), maxSize = 50L * 1024 * 1024, // 50 MiB ), ) - .rateLimit(1) + rateLimit(1) } private val creatorsClient = client.newBuilder() From 49e11935c6e0754981d7a166f14bcd9f72560cb0 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:39:53 +0700 Subject: [PATCH 14/21] Update src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- .../src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index a5de747d013..9c603eeff3e 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -133,7 +133,7 @@ abstract class Pawchive : // 1. Start fetching favorites in the background val favoritesDeferred = async { if (fav != null) { - client.get("$baseUrl/$apiPath/account/favorites", headers).use { response -> + client.get("$baseUrl/$apiPath/account/favorites").use { response -> if (response.isSuccessful) { response.parseAs>() } else { From be6fa16aaad92d4fc156328409472f3576ab58d8 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:44:39 +0700 Subject: [PATCH 15/21] Update src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- .../src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index 9c603eeff3e..88aa819d032 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -250,7 +250,7 @@ abstract class Pawchive : val url = "$baseUrl/$apiPath${manga.url}/posts?o=$offset" // Using the client.get().use {} pattern from your example - client.get(url, headers).use { response -> + client.get(url).use { response -> val page: List = response.parseAs() page.forEach { post -> From 155e483208e1216c92a36c62a0f5284abff02258 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:45:05 +0700 Subject: [PATCH 16/21] Update src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- .../src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index 88aa819d032..6b8eb694737 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -276,7 +276,7 @@ abstract class Pawchive : val url = "$baseUrl/$apiPath${chapter.url}" // 1. Fetch and parse the page list - client.get(url, headers).use { response -> + client.get(url).use { response -> val postData: PawchivePostDto = response.parseAs() // 2. Grab the preference ONCE before mapping to save performance From 9178b45d0cdf571971e0adbcde06b86e7aa171a1 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Sat, 18 Jul 2026 02:01:29 +0700 Subject: [PATCH 17/21] hopefully final --- .../src/eu/kanade/tachiyomi/extension/all/pawchive/Filters.kt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Filters.kt diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Filters.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Filters.kt new file mode 100644 index 00000000000..c469617dcf3 --- /dev/null +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Filters.kt @@ -0,0 +1,2 @@ +package eu.kanade.tachiyomi.extension.all.pawchive + From a58d1bd23b8929ab98590a1525d0d3e1dea51ede Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Sat, 18 Jul 2026 02:01:47 +0700 Subject: [PATCH 18/21] hopefully final --- .../tachiyomi/extension/all/pawchive/Dto.kt | 41 ++-- .../extension/all/pawchive/Filters.kt | 34 ++++ .../extension/all/pawchive/Pawchive.kt | 187 ++++++++---------- 3 files changed, 133 insertions(+), 129 deletions(-) diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt index 24b9613e312..bf05663b378 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Dto.kt @@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.extension.all.pawchive import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import keiyoushi.utils.tryParse import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.double -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.TimeZone +import kotlin.time.Instant @Serializable class PawchiveFavoritesDto( @@ -29,7 +26,7 @@ class PawchiveCreatorDto( var fav: Long = 0L val updatedDate get() = when { - updated.isString -> dateFormat.get()!!.tryParse(updated.content) + updated.isString -> Instant.parseOrNull(updated.content)?.toEpochMilliseconds() ?: 0L else -> (updated.double * 1000).toLong() } @@ -43,12 +40,6 @@ class PawchiveCreatorDto( } companion object { - val dateFormat = object : ThreadLocal() { - override fun initialValue() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).apply { - timeZone = TimeZone.getTimeZone("GMT") - } - } - fun String.serviceName() = when (this) { "fanbox" -> "Pixiv Fanbox" else -> replaceFirstChar { it.uppercase() } @@ -81,18 +72,22 @@ class PawchivePostDto( else -> published ?: added ?: edited // Default to published } - val format = dateFormat.get()!! - - // Prevents leaking SimpleDateFormat mutations across concurrently fetched mangas - format.timeZone = if (serviceName == "Pixiv Fanbox") TimeZone.getTimeZone("GMT+09:00") else TimeZone.getTimeZone("GMT") - - val postDate = format.tryParse(dateStr) + val postDate = dateStr?.let { + val dateWithTz = if (it.endsWith("Z") || it.contains("+")) { + it + } else { + it + if (serviceName == "Pixiv Fanbox") "+09:00" else "Z" + } + Instant.parseOrNull(dateWithTz)?.toEpochMilliseconds() + } ?: 0L url = "/$service/user/$user/post/$id" date_upload = postDate + name = title.ifBlank { - val postDateString = if (postDate != 0L) { - chapterNameDateFormat.get()!!.apply { timeZone = format.timeZone }.format(postDate) + val postDateString = if (postDate != 0L && dateStr != null) { + // Strips any unexpected timezone markers and recreates "yyyy-MM-dd 'at' HH:mm:ss" natively + dateStr.substringBefore("+").substringBefore("Z").replace("T", " at ") } else { "unknown date" } @@ -103,14 +98,6 @@ class PawchivePostDto( companion object { private val validImageExtensions = setOf("png", "jpg", "gif", "jpeg", "webp") - - private val dateFormat = object : ThreadLocal() { - override fun initialValue() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH) - } - - private val chapterNameDateFormat = object : ThreadLocal() { - override fun initialValue() = SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss", Locale.ENGLISH) - } } } diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Filters.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Filters.kt index c469617dcf3..38b5ba04e4f 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Filters.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Filters.kt @@ -1,2 +1,36 @@ package eu.kanade.tachiyomi.extension.all.pawchive +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +val getTypes = listOf("Patreon", "Pixiv Fanbox") + +val getSortsList: List> = listOf( + Pair("Popularity", "pop"), + Pair("Date Indexed", "new"), + Pair("Date Updated", "lat"), + Pair("Alphabetical Order", "tit"), + Pair("Service", "serv"), + Pair("Date Favorited", "fav"), +) + +class TypeFilter(name: String, vals: List) : Filter.Group(name, vals.map { TriFilter(it, it.lowercase()) }) + +class FavoritesFilter : Filter.Group("Favorites", listOf(TriFilter("Favorites Only", "fav"))) + +class TriFilter(name: String, val value: String) : Filter.TriState(name) + +class SortFilter(name: String, selection: Selection, private val vals: List>) : Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) { + fun getValue() = vals[state!!.index].second +} + +fun getLatestUpdatesFilterList(): FilterList = FilterList( + SortFilter("Sort by", Filter.Sort.Selection(2, false), getSortsList), + TypeFilter("Types", getTypes), + FavoritesFilter(), +) + +fun getDefaultFilterList(): FilterList = FilterList( + SortFilter("Sort by", Filter.Sort.Selection(0, false), getSortsList), + TypeFilter("Types", getTypes), + FavoritesFilter(), +) diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index 6b8eb694737..690e6bbca63 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -4,10 +4,7 @@ import androidx.preference.ListPreference import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.extension.all.pawchive.PawchiveCreatorDto.Companion.serviceName -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.await // Imported for non-blocking network calls import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page @@ -21,8 +18,10 @@ import keiyoushi.source.KeiSource import keiyoushi.utils.applicationContext import keiyoushi.utils.getPreferences import keiyoushi.utils.parseAs +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonElement import okhttp3.Cache import okhttp3.CacheControl @@ -30,8 +29,8 @@ import okhttp3.HttpUrl import okhttp3.OkHttpClient import java.io.File import kotlin.io.use -import kotlin.math.min import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds @Source abstract class Pawchive : @@ -52,7 +51,6 @@ abstract class Pawchive : addInterceptor { chain -> val request = chain.request() - // 1. Existing API fix val modifiedRequest = if (request.url.pathSegments.firstOrNull() == "api") { request.newBuilder().header("Accept", "text/css").build() } else { @@ -61,10 +59,9 @@ abstract class Pawchive : var response = chain.proceed(modifiedRequest) - // 2. New Fallback Logic for Images if (response.code == 404 && request.url.toString().startsWith(fileUrl)) { if (preferences.getBoolean(FALLBACK_LOW_RES_IMG, true)) { - response.close() // Close the failed response to prevent memory leaks + response.close() val fallbackRequest = request.newBuilder() .url(request.url.toString().toThumbnailUrl()) @@ -75,41 +72,33 @@ abstract class Pawchive : response } - cache( - Cache( - directory = File(applicationContext.externalCacheDir, "network_cache_${name.lowercase()}"), - maxSize = 50L * 1024 * 1024, // 50 MiB - ), - ) - rateLimit(1) + val cacheDir = File(applicationContext.cacheDir, "network_cache_${name.lowercase()}") + val cache = customCache ?: synchronized(this@Pawchive.javaClass) { + customCache ?: Cache(cacheDir, 50L * 1024 * 1024).also { customCache = it } + } + cache(cache) + connectTimeout(15.seconds) + readTimeout(30.seconds) + writeTimeout(15.seconds) } - private val creatorsClient = client.newBuilder() - .readTimeout(5.minutes) + private val apiClient = client.newBuilder() + .rateLimit(2) .build() - private fun String.toThumbnailUrl(): String { - val pathIndex = this.indexOf('/', 8) // Skips the `https://` - return buildString { - append(imgUrl) - append("/thumbnail") - append(this@toThumbnailUrl.substring(pathIndex)) - } - } - - override suspend fun getPopularManga(page: Int): MangasPage = searchMangas(page, sortBy = "pop" to "desc") + override suspend fun getPopularManga(page: Int): MangasPage = getSearchMangaList(page, "", getFilterList(null)) - override suspend fun getLatestUpdates(page: Int): MangasPage = searchMangas(page, sortBy = "lat" to "desc") + override suspend fun getLatestUpdates(page: Int): MangasPage = getSearchMangaList(page, "", getLatestUpdatesFilterList()) - override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage = searchMangas(page, query, filters) + // ============================== Search =============================== - private suspend fun searchMangas(page: Int = 1, title: String = "", filters: FilterList? = null, sortBy: Pair = "" to ""): MangasPage { - var sort = sortBy + override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage { + var sort: Pair = "" to "" val typeIncluded = mutableListOf() val typeExcluded = mutableListOf() var fav: Boolean? = null - filters?.forEach { filter -> + filters.forEach { filter -> when (filter) { is SortFilter -> sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc" is TypeFilter -> { @@ -129,11 +118,16 @@ abstract class Pawchive : } } + if (sort.first.isEmpty()) { + sort = "pop" to "desc" + } + val mangas = coroutineScope { - // 1. Start fetching favorites in the background - val favoritesDeferred = async { + val favoritesDeferred = async(Dispatchers.IO) { if (fav != null) { - client.get("$baseUrl/$apiPath/account/favorites").use { response -> + val url = "$baseUrl/$apiPath/account/favorites" + + apiClient.get(url, headers, ensureSuccess = false).use { response -> if (response.isSuccessful) { response.parseAs>() } else { @@ -146,14 +140,11 @@ abstract class Pawchive : } } - // 2. Start fetching creators in the background simultaneously - val creatorsDeferred = async { - val request = GET( - "$baseUrl/$apiPath/creators", - headers, - CacheControl.Builder().maxStale(30.minutes).build(), - ) - creatorsClient.newCall(request).await().use { response -> + val creatorsDeferred = async(Dispatchers.IO) { + val url = "$baseUrl/$apiPath/creators" + val cacheControl = CacheControl.Builder().maxStale(30.minutes).build() + + apiClient.get(url, headers, cacheControl).use { response -> if (!response.isSuccessful) { throw Exception("HTTP error ${response.code}") } @@ -161,16 +152,14 @@ abstract class Pawchive : } } - // 3. Wait for both background tasks to finish val favorites = favoritesDeferred.await() val allCreators = creatorsDeferred.await() - // 4. Perform the filter mapping as before allCreators.filter { creator -> val sName = creator.service.serviceName().lowercase() val includeType = typeIncluded.isEmpty() || typeIncluded.contains(sName) val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(sName) - val regularSearch = creator.name.contains(title, true) + val regularSearch = creator.name.contains(query, true) val favEntry = favorites.find { f -> f.id == creator.id } creator.fav = favEntry?.faved_seq ?: 0L @@ -196,46 +185,45 @@ abstract class Pawchive : else -> if (sort.second == "desc") mangas.sortedByDescending { it.updatedDate } else mangas.sortedBy { it.updatedDate } } - val maxIndex = sorted.size - val fromIndex = (page - 1) * PAGE_CREATORS_LIMIT - val toIndex = min(maxIndex, fromIndex + PAGE_CREATORS_LIMIT) - val final = sorted.subList(fromIndex, toIndex).map { it.toSManga(baseUrl) } + val startIndex = (page - 1) * PAGE_CREATORS_LIMIT + val endIndex = minOf(startIndex + PAGE_CREATORS_LIMIT, sorted.size) - return MangasPage(final, toIndex != maxIndex) - } + if (startIndex >= sorted.size) { + return MangasPage(emptyList(), false) + } - override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}" + val pageItems = sorted.subList(startIndex, endIndex) + val final = pageItems.map { it.toSManga(baseUrl) } + val hasNextPage = endIndex < sorted.size - override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}" + return MangasPage(final, hasNextPage) + } - // RESOLVES: getMangaByUrl(url) override suspend fun getMangaByUrl(url: HttpUrl): SManga? { val id = url.pathSegments.lastOrNull() ?: return null - val request = GET("$baseUrl/$apiPath/creators", headers, CacheControl.Builder().maxStale(30.minutes).build()) - val response = creatorsClient.newCall(request).await() - if (!response.isSuccessful) { - response.close() - return null + val cache = CacheControl.Builder().maxStale(30.minutes).build() + val url = "$baseUrl/$apiPath/creators" + + return apiClient.get(url, headers, cache, ensureSuccess = false).use { response -> + if (!response.isSuccessful) { + return null + } + val allCreators = response.parseAs>() + val creator = allCreators.find { it.id == id } ?: return null + creator.toSManga(baseUrl) } - val allCreators = response.parseAs>() - val creator = allCreators.find { it.id == id } ?: return null - return creator.toSManga(baseUrl) } - // RESOLVES: fetchRelatedMangaList(manga) - override val supportsRelatedMangas get() = false + // ============================== Details ============================== - override suspend fun fetchRelatedMangaList(manga: SManga): List = emptyList() + override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}" override suspend fun fetchMangaUpdate( manga: SManga, chapters: List, fetchDetails: Boolean, fetchChapters: Boolean, - ): SMangaUpdate { - // We aren't fetching new details in this snippet, so we pass the existing manga back - val updatedManga = manga - + ): SMangaUpdate = withContext(Dispatchers.IO) { val updatedChapters = if (fetchChapters) { val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!! .toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT @@ -249,8 +237,7 @@ abstract class Pawchive : while (offset < prefMaxPost && hasNextPage) { val url = "$baseUrl/$apiPath${manga.url}/posts?o=$offset" - // Using the client.get().use {} pattern from your example - client.get(url).use { response -> + apiClient.get(url, headers).use { response -> val page: List = response.parseAs() page.forEach { post -> @@ -268,25 +255,28 @@ abstract class Pawchive : chapters } - return SMangaUpdate(updatedManga, updatedChapters) + SMangaUpdate(manga, updatedChapters) } - // 4. Pages/Images + override val supportsRelatedMangas get() = false + + override suspend fun fetchRelatedMangaList(manga: SManga): List = emptyList() + + override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}" + + // =============================== Pages =============================== + override suspend fun getPageList(chapter: SChapter): List { val url = "$baseUrl/$apiPath${chapter.url}" - // 1. Fetch and parse the page list - client.get(url).use { response -> + apiClient.get(url, headers).use { response -> val postData: PawchivePostDto = response.parseAs() - // 2. Grab the preference ONCE before mapping to save performance val useLowRes = preferences.getBoolean(USE_LOW_RES_IMG, false) - // 3. Map the images and apply the low-res logic immediately return postData.images.mapIndexed { i, path -> val originalUrl = "$fileUrl/$dataPath$path" - // This replaces your old `imageRequest` logic val finalImageUrl = if (useLowRes) originalUrl.toThumbnailUrl() else originalUrl Page(i, imageUrl = finalImageUrl) @@ -294,6 +284,21 @@ abstract class Pawchive : } } + // ============================== Filters ============================== + + override fun getFilterList(data: JsonElement?): FilterList = getDefaultFilterList() + + // ============================= Utilities ============================= + + private fun String.toThumbnailUrl(): String { + val pathIndex = this.indexOf('/', 8) // Skips the `https://` + return buildString { + append(imgUrl) + append("/thumbnail") + append(this@toThumbnailUrl.substring(pathIndex)) + } + } + override fun setupPreferenceScreen(screen: PreferenceScreen) { ListPreference(screen.context).apply { key = POST_PAGES_PREF @@ -329,31 +334,9 @@ abstract class Pawchive : }.let(screen::addPreference) } - override fun getFilterList(data: JsonElement?): FilterList = FilterList( - SortFilter("Sort by", Filter.Sort.Selection(0, false), getSortsList), - TypeFilter("Types", getTypes), - FavoritesFilter(), - ) - - open val getTypes = listOf("Patreon", "Pixiv Fanbox") - - open val getSortsList: List> = listOf( - Pair("Popularity", "pop"), - Pair("Date Indexed", "new"), - Pair("Date Updated", "lat"), - Pair("Alphabetical Order", "tit"), - Pair("Service", "serv"), - Pair("Date Favorited", "fav"), - ) - - internal open class TypeFilter(name: String, vals: List) : Filter.Group(name, vals.map { TriFilter(it, it.lowercase()) }) - internal class FavoritesFilter : Filter.Group("Favorites", listOf(TriFilter("Favorites Only", "fav"))) - internal open class TriFilter(name: String, val value: String) : Filter.TriState(name) - internal open class SortFilter(name: String, selection: Selection, private val vals: List>) : Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) { - fun getValue() = vals[state!!.index].second - } - companion object { + @Volatile + private var customCache: Cache? = null private const val PAGE_POST_LIMIT = 50 private const val PAGE_CREATORS_LIMIT = 50 const val PROMPT = "You can change how many posts to load in the extension preferences." From 4408059e615340ab3553a62f796e1385300f438d Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:01:35 +0700 Subject: [PATCH 19/21] hopefully final 2 --- .../extension/all/pawchive/Pawchive.kt | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index 690e6bbca63..7890d22a5ad 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -15,19 +15,16 @@ import keiyoushi.annotation.Source import keiyoushi.network.get import keiyoushi.network.rateLimit import keiyoushi.source.KeiSource -import keiyoushi.utils.applicationContext import keiyoushi.utils.getPreferences import keiyoushi.utils.parseAs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonElement -import okhttp3.Cache import okhttp3.CacheControl import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient -import java.io.File import kotlin.io.use import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -72,11 +69,6 @@ abstract class Pawchive : response } - val cacheDir = File(applicationContext.cacheDir, "network_cache_${name.lowercase()}") - val cache = customCache ?: synchronized(this@Pawchive.javaClass) { - customCache ?: Cache(cacheDir, 50L * 1024 * 1024).also { customCache = it } - } - cache(cache) connectTimeout(15.seconds) readTimeout(30.seconds) writeTimeout(15.seconds) @@ -127,7 +119,7 @@ abstract class Pawchive : if (fav != null) { val url = "$baseUrl/$apiPath/account/favorites" - apiClient.get(url, headers, ensureSuccess = false).use { response -> + apiClient.get(url, ensureSuccess = false).use { response -> if (response.isSuccessful) { response.parseAs>() } else { @@ -144,7 +136,7 @@ abstract class Pawchive : val url = "$baseUrl/$apiPath/creators" val cacheControl = CacheControl.Builder().maxStale(30.minutes).build() - apiClient.get(url, headers, cacheControl).use { response -> + apiClient.get(url, cacheControl).use { response -> if (!response.isSuccessful) { throw Exception("HTTP error ${response.code}") } @@ -200,11 +192,23 @@ abstract class Pawchive : } override suspend fun getMangaByUrl(url: HttpUrl): SManga? { - val id = url.pathSegments.lastOrNull() ?: return null + if (url.host != baseUrl.toHttpUrl().host) { + return null + } + + if (url.pathSegments.firstOrNull() != "creators" || url.pathSegments.size < 2) { + return null + } + + val id = url.pathSegments[1] + if (id.isBlank()) { + return null + } + val cache = CacheControl.Builder().maxStale(30.minutes).build() - val url = "$baseUrl/$apiPath/creators" + val apiUrl = "$baseUrl/$apiPath/creators" - return apiClient.get(url, headers, cache, ensureSuccess = false).use { response -> + return apiClient.get(apiUrl, cache, ensureSuccess = false).use { response -> if (!response.isSuccessful) { return null } @@ -223,7 +227,7 @@ abstract class Pawchive : chapters: List, fetchDetails: Boolean, fetchChapters: Boolean, - ): SMangaUpdate = withContext(Dispatchers.IO) { + ): SMangaUpdate { val updatedChapters = if (fetchChapters) { val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!! .toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT @@ -237,7 +241,7 @@ abstract class Pawchive : while (offset < prefMaxPost && hasNextPage) { val url = "$baseUrl/$apiPath${manga.url}/posts?o=$offset" - apiClient.get(url, headers).use { response -> + apiClient.get(url).use { response -> val page: List = response.parseAs() page.forEach { post -> @@ -255,7 +259,7 @@ abstract class Pawchive : chapters } - SMangaUpdate(manga, updatedChapters) + return SMangaUpdate(manga, updatedChapters) } override val supportsRelatedMangas get() = false @@ -269,7 +273,7 @@ abstract class Pawchive : override suspend fun getPageList(chapter: SChapter): List { val url = "$baseUrl/$apiPath${chapter.url}" - apiClient.get(url, headers).use { response -> + apiClient.get(url).use { response -> val postData: PawchivePostDto = response.parseAs() val useLowRes = preferences.getBoolean(USE_LOW_RES_IMG, false) @@ -335,8 +339,6 @@ abstract class Pawchive : } companion object { - @Volatile - private var customCache: Cache? = null private const val PAGE_POST_LIMIT = 50 private const val PAGE_CREATORS_LIMIT = 50 const val PROMPT = "You can change how many posts to load in the extension preferences." From 5e47a8ea0cbc585ce3a36b801675eb266d20b429 Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:04:32 +0700 Subject: [PATCH 20/21] fixed apiUrl to url --- .../eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index 7890d22a5ad..a6088cb2522 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -206,9 +206,9 @@ abstract class Pawchive : } val cache = CacheControl.Builder().maxStale(30.minutes).build() - val apiUrl = "$baseUrl/$apiPath/creators" + val url = "$baseUrl/$apiPath/creators" - return apiClient.get(apiUrl, cache, ensureSuccess = false).use { response -> + return apiClient.get(url, cache, ensureSuccess = false).use { response -> if (!response.isSuccessful) { return null } From 7bf825ac6c3d9787e0170d1b1f99c349a8f8a8db Mon Sep 17 00:00:00 2001 From: Charlie <70556779+towermom9@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:06:25 +0700 Subject: [PATCH 21/21] Remove redundant '.use {}' wrapper since 'parseAs()' automatically closes the response body --- .../tachiyomi/extension/all/pawchive/Pawchive.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt index a6088cb2522..1cf5255d4da 100644 --- a/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt +++ b/src/all/pawchive/src/eu/kanade/tachiyomi/extension/all/pawchive/Pawchive.kt @@ -241,18 +241,16 @@ abstract class Pawchive : while (offset < prefMaxPost && hasNextPage) { val url = "$baseUrl/$apiPath${manga.url}/posts?o=$offset" - apiClient.get(url).use { response -> - val page: List = response.parseAs() + val page: List = apiClient.get(url).parseAs() - page.forEach { post -> - if (post.images.isNotEmpty()) { - result.add(post.toSChapter(manga.author, datePref)) - } + page.forEach { post -> + if (post.images.isNotEmpty()) { + result.add(post.toSChapter(manga.author, datePref)) } - - offset += PAGE_POST_LIMIT - hasNextPage = page.size == PAGE_POST_LIMIT } + + offset += PAGE_POST_LIMIT + hasNextPage = page.size == PAGE_POST_LIMIT } result } else {