From fb2b22403093750bba7f48420fcdbf45cf3238bc Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:53:32 +0600 Subject: [PATCH] Fix some downloads not being recognized (cherry picked from commit f0f1e4de5a13c1e66ef8c49158731bd2f808b0a0) --- CHANGELOG.md | 1 + .../data/download/DownloadProvider.kt | 34 +++++++-- .../data/download/DownloadProviderTest.kt | 73 +++++++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 app/src/test/java/eu/kanade/tachiyomi/data/download/DownloadProviderTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index e3f2f61ae..2a20d5938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Fixed a crash when opening the filters of a source with an invalid filter ([@AntsyLich](https://github.com/AntsyLich)) - Fixed a crash when installing an extension on a device where the system installer can't ask for confirmation ([@AntsyLich](https://github.com/AntsyLich)) - Fixed a crash when tapping in the long strip reader as it closed ([@AntsyLich](https://github.com/AntsyLich)) +- Fixed chapters downloaded before v0.21.0 showing as not downloaded when their name started with a space or dot, was long, or had a non-ASCII scanlator ([@AntsyLich](https://github.com/AntsyLich)) ## [v0.21.1] - 2026-10-12 ### Fixed diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index 201212a5b..3e12e3289 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -231,7 +231,7 @@ class DownloadProvider( enableChapterNameHash: Boolean = libraryPreferences.enableChapterNameHash.get(), ): String { return ChapterDirNameParts(chapterName, chapterScanlator, chapterUrl) - .dirName(disallowNonAsciiFilenames, enableChapterNameHash, sanitizeScanlator = true) + .dirName(disallowNonAsciiFilenames, enableChapterNameHash) } /** @@ -254,14 +254,18 @@ class DownloadProvider( val booleanPairPermutation = listOf(false to false, false to true, true to false, true to true) val othersChapterDirNames = booleanPairPermutation .map { (disallowNonAsciiFilenames, enableChapterNameHash) -> - parts.dirName(disallowNonAsciiFilenames, enableChapterNameHash, sanitizeScanlator = true) + parts.dirName(disallowNonAsciiFilenames, enableChapterNameHash) + } + val splitChapterDirNames = booleanPairPermutation + .map { (disallowNonAsciiFilenames, enableChapterNameHash) -> + parts.splitDirName(disallowNonAsciiFilenames, enableChapterNameHash, sanitizeScanlator = true) } // Scanlators weren't sanitized for a while. One with a "/" nested its folder, so it was never found anyway. val scanlator = parts.chapterScanlator val unsanitizedScanlatorChapterDirNames = if (scanlator != null && '/' !in scanlator) { booleanPairPermutation.map { (disallowNonAsciiFilenames, enableChapterNameHash) -> - parts.dirName(disallowNonAsciiFilenames, enableChapterNameHash, sanitizeScanlator = false) + parts.splitDirName(disallowNonAsciiFilenames, enableChapterNameHash, sanitizeScanlator = false) } } else { emptyList() @@ -272,6 +276,7 @@ class DownloadProvider( // chapter names) add(chapterNameV1) addAll(othersChapterDirNames) + addAll(splitChapterDirNames) addAll(unsanitizedScanlatorChapterDirNames) } } @@ -291,7 +296,6 @@ class DownloadProvider( val chapterDirName = parts.dirName( disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames.get(), enableHash = libraryPreferences.enableChapterNameHash.get(), - sanitizeScanlator = true, ) val legacyChapterDirNames = getLegacyChapterDirNames(parts) @@ -330,9 +334,29 @@ private class ChapterDirNameParts(chapterName: String, val chapterScanlator: Str chapterScanlator?.let { DiskUtil.buildValidFilename(it) } } + private val validDirNames = arrayOfNulls(2) + private val validChapterNames = arrayOfNulls(2) - fun dirName(disallowNonAscii: Boolean, enableHash: Boolean, sanitizeScanlator: Boolean): String { + fun dirName(disallowNonAscii: Boolean, enableHash: Boolean): String { + val index = if (disallowNonAscii) 1 else 0 + val dirName = validDirNames[index] + // The scanlator and the name are made valid together, as every version before 0.21.0 did + ?: DiskUtil.buildValidFilename( + if (chapterScanlator.isNullOrBlank()) chapterName else "${chapterScanlator}_$chapterName", + // Subtract 7 bytes for hash and underscore, 4 bytes for .cbz + DiskUtil.MAX_FILE_NAME_BYTES - 11, + disallowNonAscii, + ) + .also { validDirNames[index] = it } + return if (enableHash) dirName + hashSuffix else dirName + } + + /** + * The name 0.21.0 and 0.21.1 gave a chapter, which made only the chapter name valid and so differs for names + * starting with a space or dot, long names, and non-ASCII scanlators. + */ + fun splitDirName(disallowNonAscii: Boolean, enableHash: Boolean, sanitizeScanlator: Boolean): String { return buildString { if (!chapterScanlator.isNullOrBlank()) { // A "/" in the scanlator would otherwise nest the chapter in a folder of its own diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/download/DownloadProviderTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/download/DownloadProviderTest.kt new file mode 100644 index 000000000..a15aa2a7b --- /dev/null +++ b/app/src/test/java/eu/kanade/tachiyomi/data/download/DownloadProviderTest.kt @@ -0,0 +1,73 @@ +package eu.kanade.tachiyomi.data.download + +import eu.kanade.tachiyomi.util.lang.Hash.md5 +import eu.kanade.tachiyomi.util.storage.DiskUtil +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import tachiyomi.core.common.preference.InMemoryPreferenceStore +import tachiyomi.domain.library.service.LibraryPreferences + +class DownloadProviderTest { + + private fun provider(disallowNonAscii: Boolean): DownloadProvider { + val preferences = LibraryPreferences(InMemoryPreferenceStore()) + preferences.disallowNonAsciiFilenames.set(disallowNonAscii) + preferences.enableChapterNameHash.set(true) + return DownloadProvider(mockk(relaxed = true), mockk(relaxed = true), preferences) + } + + private fun dirNameBefore0210(name: String, scanlator: String?, url: String, disallowNonAscii: Boolean): String { + val dirName = if (scanlator.isNullOrBlank()) name else "${scanlator}_$name" + return DiskUtil.buildValidFilename(dirName, DiskUtil.MAX_FILE_NAME_BYTES - 11, disallowNonAscii) + + "_" + md5(url).take(6) + } + + private fun dirName0210(name: String, scanlator: String?, url: String, disallowNonAscii: Boolean): String { + return DiskUtil.buildValidFilename(scanlator!!) + "_" + + DiskUtil.buildValidFilename(name, DiskUtil.MAX_FILE_NAME_BYTES - 11, disallowNonAscii) + + "_" + md5(url).take(6) + } + + @ParameterizedTest + @MethodSource("chapters") + fun `finds chapters downloaded before 0_21_0 and names new ones the same`( + name: String, + scanlator: String?, + disallowNonAscii: Boolean, + ) { + val provider = provider(disallowNonAscii) + val expected = dirNameBefore0210(name, scanlator, URL, disallowNonAscii) + + provider.getValidChapterDirNames(name, scanlator, URL) shouldContain expected + provider.getChapterDirName(name, scanlator, URL) shouldBe expected + } + + @ParameterizedTest + @MethodSource("chapters") + fun `finds chapters downloaded by 0_21_0 and 0_21_1`(name: String, scanlator: String?, disallowNonAscii: Boolean) { + if (scanlator.isNullOrBlank()) return + + provider(disallowNonAscii).getValidChapterDirNames(name, scanlator, URL) shouldContain + dirName0210(name, scanlator, URL, disallowNonAscii) + } + + companion object { + private const val URL = "/chapter/1" + + @JvmStatic + fun chapters() = listOf( + Arguments.of("Chapter 1", "Group", false), + Arguments.of(" Chapter 1", "Group", false), + Arguments.of(".Chapter 1", "Group", false), + Arguments.of("Chapter 1", "Group ", false), + Arguments.of("Chapter 1", "A/B ", false), + Arguments.of("Chapter ".repeat(40), "Group", false), + Arguments.of("Chapter 1", "翻訳グループ", true), + Arguments.of(" Chapter 1", null, false), + ) + } +}