Improve handling of downloads for chapters with same metadata and optionally for OSes that don't support Unicode in filename (mihonapp/mihon#2305)

Co-authored-by: jkim <jhskim@hotmail.com>
Co-authored-by: fatotak <111342761+fatotak@users.noreply.github.com>
Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 58b25d697f7987e9888344e815d5646ec010a663)
This commit is contained in:
Radon Rosborough 2025-10-07 16:07:09 -07:00 committed by Cuong-Tran
parent 4affedcf5a
commit f12e997fb8
24 changed files with 290 additions and 48 deletions

View file

@ -25,8 +25,8 @@ android {
defaultConfig {
applicationId = "app.komikku"
versionCode = 77
versionName = "1.13.5"
versionCode = 78
versionName = "1.13.6"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")

View file

@ -119,6 +119,7 @@ class SyncChaptersWithSource(
downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
// SY -->
// manga.title,
manga.ogTitle,
@ -129,12 +130,14 @@ class SyncChaptersWithSource(
if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter)
}
var toChangeChapter = dbChapter.copy(
name = chapter.name,
chapterNumber = chapter.chapterNumber,
scanlator = chapter.scanlator,
sourceOrder = chapter.sourceOrder,
)
if (chapter.dateUpload != 0L) {
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
}

View file

@ -36,6 +36,7 @@ fun List<Chapter>.applyFilters(
val downloaded = downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
// SY -->
manga.ogTitle,
// SY <--

View file

@ -391,6 +391,11 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.pref_update_library_manga_titles),
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
),
Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.disallowNonAsciiFilenames(),
title = stringResource(MR.strings.pref_disallow_non_ascii_filenames),
subtitle = stringResource(MR.strings.pref_disallow_non_ascii_filenames_details),
),
),
)
}

View file

@ -78,6 +78,7 @@ fun ChapterListDialog(
downloadManager.isChapterDownloaded(
chapterItem.chapter.name,
chapterItem.chapter.scanlator,
chapterItem.chapter.url,
chapterItem.manga.ogTitle,
chapterItem.manga.source,
)

View file

@ -137,6 +137,7 @@ class DownloadCache(
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query
* @param chapterUrl the url of the chapter to query
* @param mangaTitle the title of the manga to query.
* @param sourceId the id of the source of the chapter.
* @param skipCache whether to skip the directory cache and check in the filesystem.
@ -144,13 +145,14 @@ class DownloadCache(
fun isChapterDownloaded(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
sourceId: Long,
skipCache: Boolean,
): Boolean {
if (skipCache) {
val source = sourceManager.getOrStub(sourceId)
return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null
return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null
}
renewCache()
@ -162,6 +164,7 @@ class DownloadCache(
return provider.getValidChapterDirNames(
chapterName,
chapterScanlator,
chapterUrl,
).any { it in mangaDir.chapterDirs }
}
}
@ -252,7 +255,7 @@ class DownloadCache(
// SY <--
),
] ?: return
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
if (it in mangaDir.chapterDirs) {
mangaDir.chapterDirs -= it
}
@ -294,7 +297,7 @@ class DownloadCache(
),
] ?: return
chapters.forEach { chapter ->
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
if (it in mangaDir.chapterDirs) {
mangaDir.chapterDirs -= it
}

View file

@ -165,6 +165,7 @@ class DownloadManager(
val chapterDir = provider.findChapterDir(
chapter.name,
chapter.scanlator,
chapter.url,
// SY -->
manga.ogTitle,
// SY <--
@ -195,11 +196,12 @@ class DownloadManager(
fun isChapterDownloaded(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
sourceId: Long,
skipCache: Boolean = false,
): Boolean {
return cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache)
return cache.isChapterDownloaded(chapterName, chapterScanlator, chapterUrl, mangaTitle, sourceId, skipCache)
}
/**
@ -460,7 +462,7 @@ class DownloadManager(
* @param newChapter the target chapter with the new name.
*/
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator, oldChapter.url)
val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrElse { e ->
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
return
@ -471,7 +473,7 @@ class DownloadManager(
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull() ?: return
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
if (oldDownload.isFile && oldDownload.extension == "cbz") {
newName += ".cbz"
}

View file

@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.Hash.md5
import eu.kanade.tachiyomi.util.storage.DiskUtil
import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR
@ -26,6 +28,7 @@ import java.io.IOException
class DownloadProvider(
private val context: Context,
private val storageManager: StorageManager = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
) {
private val downloadsDir: UniFile?
@ -103,9 +106,15 @@ class DownloadProvider(
* @param mangaTitle the title of the manga to query.
* @param source the source of the chapter.
*/
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
fun findChapterDir(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
source: Source,
): UniFile? {
val mangaDir = findMangaDir(mangaTitle, source)
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
return getValidChapterDirNames(chapterName, chapterScanlator, chapterUrl).asSequence()
.mapNotNull { mangaDir?.findFile(it) }
.firstOrNull()
}
@ -132,7 +141,7 @@ class DownloadProvider(
}
} else {
// KMK <--
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull()
}
@ -155,7 +164,7 @@ class DownloadProvider(
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return emptyList()
return mangaDir.listFiles().orEmpty().asList().filter {
chapters.find { chp ->
getValidChapterDirNames(chp.name, chp.scanlator).any { dir ->
getValidChapterDirNames(chp.name, chp.scanlator, chp.url).any { dir ->
mangaDir.findFile(dir) != null
}
} == null ||
@ -170,7 +179,10 @@ class DownloadProvider(
* @param source the source to query.
*/
fun getSourceDirName(source: Source): String {
return DiskUtil.buildValidFilename(source.toString())
return DiskUtil.buildValidFilename(
source.toString(),
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
)
}
/**
@ -179,23 +191,75 @@ class DownloadProvider(
* @param mangaTitle the title of the manga to query.
*/
fun getMangaDirName(mangaTitle: String): String {
return DiskUtil.buildValidFilename(mangaTitle)
return DiskUtil.buildValidFilename(
mangaTitle,
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
)
}
/**
* Returns the chapter directory name for a chapter.
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query
* @param chapterScanlator scanlator of the chapter to query.
* @param chapterUrl url of the chapter to query.
*/
fun getChapterDirName(chapterName: String, chapterScanlator: String?): String {
val newChapterName = sanitizeChapterName(chapterName)
return DiskUtil.buildValidFilename(
fun getChapterDirName(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
disallowNonAsciiFilenames: Boolean = libraryPreferences.disallowNonAsciiFilenames().get(),
): String {
var dirName = sanitizeChapterName(chapterName)
if (!chapterScanlator.isNullOrBlank()) {
dirName = chapterScanlator + "_" + dirName
}
// Subtract 7 bytes for hash and underscore, 4 bytes for .cbz
dirName = DiskUtil.buildValidFilename(dirName, DiskUtil.MAX_FILE_NAME_BYTES - 11, disallowNonAsciiFilenames)
dirName += "_" + md5(chapterUrl).take(6)
return dirName
}
/**
* Returns list of names that might have been previously used as
* the directory name for a chapter.
* Add to this list if naming pattern ever changes.
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query.
* @param chapterUrl url of the chapter to query.
*/
private fun getLegacyChapterDirNames(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
): List<String> {
val sanitizedChapterName = sanitizeChapterName(chapterName)
val chapterNameV1 = DiskUtil.buildValidFilename(
when {
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$newChapterName"
else -> newChapterName
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$sanitizedChapterName"
else -> sanitizedChapterName
},
)
// Get the filename that would be generated if the user were
// using the other value for the disallow non-ASCII
// filenames setting. This ensures that chapters downloaded
// before the user changed the setting can still be found.
val otherChapterDirName =
getChapterDirName(
chapterName,
chapterScanlator,
chapterUrl,
!libraryPreferences.disallowNonAsciiFilenames().get(),
)
return buildList(2) {
// Chapter name without hash (unable to handle duplicate
// chapter names)
add(chapterNameV1)
add(otherChapterDirName)
}
}
/**
@ -210,24 +274,32 @@ class DownloadProvider(
}
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
return oldChapter.name != newChapter.name ||
oldChapter.scanlator?.takeIf { it.isNotBlank() } != newChapter.scanlator?.takeIf { it.isNotBlank() }
return getChapterDirName(oldChapter.name, oldChapter.scanlator, oldChapter.url) !=
getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
}
/**
* Returns valid downloaded chapter directory names.
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query
* @param chapterName the name of the chapter.
* @param chapterScanlator the scanlator group for the chapter, if any.
* @param chapterUrl the unique URL of the chapter.
*/
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?): List<String> {
val chapterDirName = getChapterDirName(chapterName, chapterScanlator)
return buildList(2) {
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?, chapterUrl: String): List<String> {
val chapterDirName = getChapterDirName(chapterName, chapterScanlator, chapterUrl)
val legacyChapterDirNames = getLegacyChapterDirNames(chapterName, chapterScanlator, chapterUrl)
return buildList {
// Folder of images
add(chapterDirName)
// Archived chapters
add("$chapterDirName.cbz")
// any legacy names
legacyChapterDirNames.forEach {
add(it)
add("$it.cbz")
}
}
}
}

View file

@ -294,7 +294,7 @@ class Downloader(
val chaptersToQueue = chapters.asSequence()
// Filter out those already downloaded.
.filter {
provider.findChapterDir(it.name, it.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, source) == null
provider.findChapterDir(it.name, it.scanlator, it.url, /* SY --> */ manga.ogTitle /* SY <-- */, source) == null
}
// Add chapters to queue from the start.
.sortedByDescending { it.sourceOrder }
@ -361,7 +361,11 @@ class Downloader(
return
}
val chapterDirname = provider.getChapterDirName(download.chapter.name, download.chapter.scanlator)
val chapterDirname = provider.getChapterDirName(
download.chapter.name,
download.chapter.scanlator,
download.chapter.url,
)
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
try {

View file

@ -928,6 +928,7 @@ class LibraryScreenModel(
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
mergedManga.ogTitle,
mergedManga.source,
)
@ -946,6 +947,7 @@ class LibraryScreenModel(
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
// SY -->
manga.ogTitle,
// SY <--

View file

@ -1091,12 +1091,13 @@ class MangaScreenModel(
true
} else {
downloadManager.isChapterDownloaded(
// SY -->
chapter.name,
chapter.scanlator,
chapter.url,
// SY -->
manga.ogTitle,
manga.source,
// SY <--
manga.source,
)
}
val downloadState = when {

View file

@ -48,7 +48,6 @@ import eu.kanade.tachiyomi.util.chapter.filterDownloaded
import eu.kanade.tachiyomi.util.chapter.removeDuplicates
import eu.kanade.tachiyomi.util.editCover
import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil.MAX_FILE_NAME_BYTES
import eu.kanade.tachiyomi.util.storage.cacheImageDir
@ -282,6 +281,7 @@ class ReaderViewModel @JvmOverloads constructor(
return downloadManager.isChapterDownloaded(
chapterName = chapter.name,
chapterScanlator = chapter.scanlator,
chapterUrl = chapter.url,
mangaTitle = chapterManga.ogTitle,
sourceId = chapterManga.source,
)
@ -633,6 +633,7 @@ class ReaderViewModel @JvmOverloads constructor(
val isDownloaded = downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
// SY -->
manga.ogTitle,
// SY <--
@ -720,6 +721,7 @@ class ReaderViewModel @JvmOverloads constructor(
val isNextChapterDownloaded = downloadManager.isChapterDownloaded(
nextChapter.name,
nextChapter.scanlator,
nextChapter.url,
// KMK -->
nextChapterManga.ogTitle,
nextChapterManga.source,
@ -1081,7 +1083,8 @@ class ReaderViewModel @JvmOverloads constructor(
val chapter = page.chapter.chapter
val filenameSuffix = " - ${page.number}"
return DiskUtil.buildValidFilename(
"${manga.title} - ${chapter.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()),
"${manga.title} - ${chapter.name}",
MAX_FILE_NAME_BYTES - filenameSuffix.byteSize(),
) + filenameSuffix
}
@ -1263,7 +1266,8 @@ class ReaderViewModel @JvmOverloads constructor(
// Build destination file.
val filenameSuffix = " - ${page1.number}-${page2.number}.jpg"
val filename = DiskUtil.buildValidFilename(
"${manga.title} - ${chapter.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()),
"${manga.title} - ${chapter.name}",
MAX_FILE_NAME_BYTES - filenameSuffix.byteSize(),
) + filenameSuffix
return imageSaver.save(

View file

@ -95,6 +95,7 @@ class ChapterLoader(
val isDownloaded = downloadManager.isChapterDownloaded(
chapterName = dbChapter.name,
chapterScanlator = dbChapter.scanlator,
chapterUrl = dbChapter.url,
// SY -->
mangaTitle = manga.ogTitle,
// SY <--
@ -113,6 +114,7 @@ class ChapterLoader(
val isMergedMangaDownloaded = downloadManager.isChapterDownloaded(
chapterName = chapter.chapter.name,
chapterScanlator = chapter.chapter.scanlator,
chapterUrl = chapter.chapter.url,
mangaTitle = manga.ogTitle,
sourceId = manga.source,
skipCache = true,

View file

@ -33,7 +33,15 @@ internal class DownloadPageLoader(
override suspend fun getPages(): List<ReaderPage> {
val dbChapter = chapter.chapter
val chapterPath = downloadProvider.findChapterDir(dbChapter.name, dbChapter.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, source)
val chapterPath = downloadProvider.findChapterDir(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
// SY -->
manga.ogTitle,
// SY <--
source,
)
return if (chapterPath?.isFile == true) {
getPagesFromArchive(chapterPath)
} else {

View file

@ -48,6 +48,7 @@ class ReaderTransitionView @JvmOverloads constructor(
downloadManager.isChapterDownloaded(
chapterName = goingToChapter.name,
chapterScanlator = goingToChapter.scanlator,
chapterUrl = goingToChapter.url,
mangaTitle = /* SY --> */ manga.ogTitle, /* SY <-- */
sourceId = manga.source,
skipCache = true,

View file

@ -130,6 +130,7 @@ class UpdatesScreenModel(
val downloaded = downloadManager.isChapterDownloaded(
update.chapterName,
update.scanlator,
update.chapterUrl,
// SY -->
update.ogMangaTitle,
// SY <--

View file

@ -18,7 +18,7 @@ fun List<Chapter>.filterDownloaded(manga: Manga/* SY --> */, mangaMap: Map<Long,
// SY -->
return filter {
val chapterManga = mangaMap?.get(it.mangaId) ?: manga
downloadCache.isChapterDownloaded(it.name, it.scanlator, chapterManga.ogTitle, chapterManga.source, false)
downloadCache.isChapterDownloaded(it.name, it.scanlator, it.url, chapterManga.ogTitle, chapterManga.source, false)
}
// SY <--
}

View file

@ -9,6 +9,9 @@ import androidx.core.content.ContextCompat
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.Hash
import java.io.File
import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.nio.charset.CodingErrorAction
object DiskUtil {
@ -102,26 +105,84 @@ object DiskUtil {
}
/**
* Mutate the given filename to make it valid for a FAT filesystem,
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
* with a dot), but you can manually add it later.
* Transform a filename fragment to make it safe to use on almost
* all commonly used filesystems. You can pass an entire filename,
* or just part of one, in case you want a specific part of a long
* filename to be truncated, rather than the end of it.
*
* Characters that are potentially unsafe for some filesystems are
* replaced with underscores. This includes the standard ones from
* https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
* but does allow any other valid Unicode code point.
*
* Excessively long filenames are truncated, by default to 240
* bytes. Note that the truncation is based on bytes rather than
* characters (code points), because this is what is relevant to
* filesystem restrictions in most cases.
*
* Leading periods are stripped, to avoid the creation of hidden
* files by default. If a hidden file is desired, a period can be
* prepended to the return value from this function.
*
* If the optional argument disallowNonAscii is set to true,
* then ANYTHING outside the ASCII range is replaced not with underscores,
* but with its hexadecimal encoding. This is to make it so that distinct
* non-English titles of things remain distinct, since not all
* places where this function is used also take care of
* disambiguation.
*
* We could instead replace only non-ASCII characters known to
* be problematic, but so far nobody with a non-Unicode-compliant
* device has been able to provide either directions to reproduce
* their issue nor any documentation or tests that would allow us
* to determine which characters are problems and which are not.
*/
fun buildValidFilename(origName: String): String {
fun buildValidFilename(
origName: String,
maxBytes: Int = MAX_FILE_NAME_BYTES,
disallowNonAscii: Boolean = false,
): String {
val name = origName.trim('.', ' ')
if (name.isEmpty()) {
return "(invalid)"
}
val sb = StringBuilder(name.length)
name.forEach { c ->
if (isValidFatFilenameChar(c)) {
if (disallowNonAscii && c >= 0x80.toChar()) {
sb.append(
c.toString().toByteArray(Charsets.UTF_8).toHexString(
HexFormat {
upperCase = false
},
),
)
} else if (isValidFatFilenameChar(c)) {
sb.append(c)
} else {
sb.append('_')
}
}
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
return sb.toString().take(240)
return truncateToLength(sb.toString(), maxBytes)
}
/**
* Truncate a string to a maximum length, while maintaining valid Unicode encoding.
*/
fun truncateToLength(s: String, maxBytes: Int): String {
val charset = Charsets.UTF_8
val decoder = charset.newDecoder()
val sba = s.toByteArray(charset)
if (sba.size <= maxBytes) {
return s
}
// Ensure truncation by having byte buffer = maxBytes
val bb = ByteBuffer.wrap(sba, 0, maxBytes)
val cb = CharBuffer.allocate(maxBytes)
// Ignore an incomplete character
decoder.onMalformedInput(CodingErrorAction.IGNORE)
decoder.decode(bb, cb, true)
decoder.flush(cb)
return String(cb.array(), 0, cb.position())
}
/**
@ -139,6 +200,8 @@ object DiskUtil {
const val NOMEDIA_FILE = ".nomedia"
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8)
const val MAX_FILE_NAME_BYTES = 250
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8).
// To allow for writing to ext4 through a FUSE layer in the future, also subtract 15
// reserved characters.
const val MAX_FILE_NAME_BYTES = 240
}

View file

@ -52,6 +52,7 @@ class UpdatesRepositoryImpl(
chapterId: Long,
chapterName: String,
scanlator: String?,
chapterUrl: String,
read: Boolean,
bookmark: Boolean,
lastPageRead: Long,
@ -69,6 +70,7 @@ class UpdatesRepositoryImpl(
chapterId = chapterId,
chapterName = chapterName,
scanlator = scanlator,
chapterUrl = chapterUrl,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,

View file

@ -0,0 +1,60 @@
-- Add chapter urls to updates view
DROP VIEW IF EXISTS updatesView;
CREATE VIEW updatesView AS
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.url AS chapterUrl,
chapters.read,
chapters.bookmark,
chapters.last_page_read,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch
FROM mangas JOIN chapters
ON mangas._id = chapters.manga_id
LEFT JOIN excluded_scanlators
ON mangas._id = excluded_scanlators.manga_id
AND chapters.scanlator = excluded_scanlators.scanlator
WHERE favorite = 1 AND source <> 6969
AND excluded_scanlators.scanlator IS NULL
AND date_fetch > date_added
UNION
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.url AS chapterUrl,
chapters.read,
chapters.bookmark,
chapters.last_page_read,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch
FROM mangas
LEFT JOIN (
SELECT merged.manga_id,merged.merge_id
FROM merged
GROUP BY merged.merge_id
) AS ME
ON ME.merge_id = mangas._id
JOIN chapters
ON ME.manga_id = chapters.manga_id
LEFT JOIN excluded_scanlators
ON mangas._id = excluded_scanlators.manga_id
AND chapters.scanlator = excluded_scanlators.scanlator
WHERE favorite = 1 AND source = 6969
AND excluded_scanlators.scanlator IS NULL
AND date_fetch > date_added
ORDER BY datefetch DESC;

View file

@ -6,6 +6,7 @@ SELECT
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.url AS chapterUrl,
chapters.read,
chapters.bookmark,
chapters.last_page_read,
@ -30,6 +31,7 @@ SELECT
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.url AS chapterUrl,
chapters.read,
chapters.bookmark,
chapters.last_page_read,

View file

@ -244,6 +244,8 @@ class LibraryPreferences(
fun updateMangaTitles() = preferenceStore.getBoolean("pref_update_library_manga_titles", false)
fun disallowNonAsciiFilenames() = preferenceStore.getBoolean("disallow_non_ascii_filenames", false)
// endregion
enum class ChapterSwipeAction {

View file

@ -12,6 +12,7 @@ data class UpdatesWithRelations(
val chapterId: Long,
val chapterName: String,
val scanlator: String?,
val chapterUrl: String,
val read: Boolean,
val bookmark: Boolean,
val lastPageRead: Long,

View file

@ -322,6 +322,8 @@
<string name="pref_mark_duplicate_read_chapter_read_new">After fetching new chapter</string>
<string name="pref_hide_missing_chapter_indicators">Hide missing chapter indicators</string>
<string name="pref_disallow_non_ascii_filenames">Disallow non-ASCII filenames</string>
<string name="pref_disallow_non_ascii_filenames_details">Ensures compatibility with certain storage media that don't support Unicode. When this is enabled, you'll need to manually rename source and manga folders by replacing non-ASCII characters with their lowercase UTF-8 hexadecimal representations. Chapter files don't need to be renamed.</string>
<!-- Extension section -->
<string name="multi_lang">Multi</string>