mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-09 12:59:34 +02:00
Improve handling of downloads for chapters with same metadata and optionally for OSes that don't support Unicode in filename (#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>
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -11,13 +11,17 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- `Other` - for technical stuff.
|
- `Other` - for technical stuff.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- Advanced setting to limit download filenames to ASCII characters. This is provided only as a workaround for OSes that do not properly handle standard Unicode filenames. This setting is generally not recommended and should only be used as a last resort ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Delegate Suwayomi tracker authentication to extension ([@cpiber](https://github.com/cpiber)) ([#2476](https://github.com/mihonapp/mihon/pull/2476))
|
- Delegate Suwayomi tracker authentication to extension ([@cpiber](https://github.com/cpiber)) ([#2476](https://github.com/mihonapp/mihon/pull/2476))
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
- Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491))
|
- Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491))
|
||||||
|
- Download support for chapters with the same metadata. Now a hash based on chapter's url is appended to download filename to tell them apart, letting you download both. Existing downloaded chapters will continue to work normally ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
|
||||||
|
|
||||||
### Fixes
|
### Fixed
|
||||||
- Fix height of description not being calculated correctly if images are present ([@Secozzi](https://github.com/Secozzi)) ([#2382](https://github.com/mihonapp/mihon/pull/2382))
|
- Fix height of description not being calculated correctly if images are present ([@Secozzi](https://github.com/Secozzi)) ([#2382](https://github.com/mihonapp/mihon/pull/2382))
|
||||||
- Fix migration progress not updating after manual search ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
|
- Fix migration progress not updating after manual search ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
|
||||||
- Fix category migration flag being ignored due to incorrect check against chapter flag ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
|
- Fix category migration flag being ignored due to incorrect check against chapter flag ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
|
||||||
@@ -30,7 +34,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
### Removed
|
### Removed
|
||||||
- Predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2362](https://github.com/mihonapp/mihon/pull/2362))
|
- Predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2362](https://github.com/mihonapp/mihon/pull/2362))
|
||||||
|
|
||||||
### Fixes
|
### Fixed
|
||||||
- Fix scrollbar sometimes not showing during scroll or not reaching the bottom with few items ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2304](https://github.com/mihonapp/mihon/pull/2304))
|
- Fix scrollbar sometimes not showing during scroll or not reaching the bottom with few items ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2304](https://github.com/mihonapp/mihon/pull/2304))
|
||||||
- Fix local source EPUB files not loading ([@AntsyLich](https://github.com/AntsyLich)) ([#2369](https://github.com/mihonapp/mihon/pull/2369))
|
- Fix local source EPUB files not loading ([@AntsyLich](https://github.com/AntsyLich)) ([#2369](https://github.com/mihonapp/mihon/pull/2369))
|
||||||
- Fix title text color in light mode on mass migration list ([@AntsyLich](https://github.com/AntsyLich)) ([#2370](https://github.com/mihonapp/mihon/pull/2370))
|
- Fix title text color in light mode on mass migration list ([@AntsyLich](https://github.com/AntsyLich)) ([#2370](https://github.com/mihonapp/mihon/pull/2370))
|
||||||
@@ -76,7 +80,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- Make local source default chapter sorting match file explorer behavior ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224))
|
- Make local source default chapter sorting match file explorer behavior ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224))
|
||||||
- Include Manga `initialized` status in backup ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2285](https://github.com/mihonapp/mihon/pull/2285))
|
- Include Manga `initialized` status in backup ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2285](https://github.com/mihonapp/mihon/pull/2285))
|
||||||
|
|
||||||
### Fixes
|
### Fixed
|
||||||
- Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885))
|
- Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885))
|
||||||
- Fix next chapter button occasionally jumping to the last page of the current chapter ([@perokhe](https://github.com/perokhe)) ([#1920](https://github.com/mihonapp/mihon/pull/1920))
|
- Fix next chapter button occasionally jumping to the last page of the current chapter ([@perokhe](https://github.com/perokhe)) ([#1920](https://github.com/mihonapp/mihon/pull/1920))
|
||||||
- Fix page number not appearing when opening chapter ([@perokhe](https://github.com/perokhe)) ([#1936](https://github.com/mihonapp/mihon/pull/1936))
|
- Fix page number not appearing when opening chapter ([@perokhe](https://github.com/perokhe)) ([#1936](https://github.com/mihonapp/mihon/pull/1936))
|
||||||
|
@@ -114,6 +114,7 @@ class SyncChaptersWithSource(
|
|||||||
downloadManager.isChapterDownloaded(
|
downloadManager.isChapterDownloaded(
|
||||||
dbChapter.name,
|
dbChapter.name,
|
||||||
dbChapter.scanlator,
|
dbChapter.scanlator,
|
||||||
|
dbChapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
@@ -121,12 +122,14 @@ class SyncChaptersWithSource(
|
|||||||
if (shouldRenameChapter) {
|
if (shouldRenameChapter) {
|
||||||
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
var toChangeChapter = dbChapter.copy(
|
var toChangeChapter = dbChapter.copy(
|
||||||
name = chapter.name,
|
name = chapter.name,
|
||||||
chapterNumber = chapter.chapterNumber,
|
chapterNumber = chapter.chapterNumber,
|
||||||
scanlator = chapter.scanlator,
|
scanlator = chapter.scanlator,
|
||||||
sourceOrder = chapter.sourceOrder,
|
sourceOrder = chapter.sourceOrder,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (chapter.dateUpload != 0L) {
|
if (chapter.dateUpload != 0L) {
|
||||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
|
|||||||
val downloaded = downloadManager.isChapterDownloaded(
|
val downloaded = downloadManager.isChapterDownloaded(
|
||||||
chapter.name,
|
chapter.name,
|
||||||
chapter.scanlator,
|
chapter.scanlator,
|
||||||
|
chapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
|
@@ -323,6 +323,11 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
title = stringResource(MR.strings.pref_update_library_manga_titles),
|
title = stringResource(MR.strings.pref_update_library_manga_titles),
|
||||||
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
|
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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -128,6 +128,7 @@ class DownloadCache(
|
|||||||
*
|
*
|
||||||
* @param chapterName the name of the chapter to query.
|
* @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 the url of the chapter to query
|
||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
* @param sourceId the id of the source of the chapter.
|
* @param sourceId the id of the source of the chapter.
|
||||||
* @param skipCache whether to skip the directory cache and check in the filesystem.
|
* @param skipCache whether to skip the directory cache and check in the filesystem.
|
||||||
@@ -135,13 +136,14 @@ class DownloadCache(
|
|||||||
fun isChapterDownloaded(
|
fun isChapterDownloaded(
|
||||||
chapterName: String,
|
chapterName: String,
|
||||||
chapterScanlator: String?,
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
mangaTitle: String,
|
mangaTitle: String,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
skipCache: Boolean,
|
skipCache: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (skipCache) {
|
if (skipCache) {
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
val source = sourceManager.getOrStub(sourceId)
|
||||||
return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null
|
return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
renewCache()
|
renewCache()
|
||||||
@@ -153,6 +155,7 @@ class DownloadCache(
|
|||||||
return provider.getValidChapterDirNames(
|
return provider.getValidChapterDirNames(
|
||||||
chapterName,
|
chapterName,
|
||||||
chapterScanlator,
|
chapterScanlator,
|
||||||
|
chapterUrl,
|
||||||
).any { it in mangaDir.chapterDirs }
|
).any { it in mangaDir.chapterDirs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +236,7 @@ class DownloadCache(
|
|||||||
rootDownloadsDirMutex.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
||||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
|
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
|
||||||
if (it in mangaDir.chapterDirs) {
|
if (it in mangaDir.chapterDirs) {
|
||||||
mangaDir.chapterDirs -= it
|
mangaDir.chapterDirs -= it
|
||||||
}
|
}
|
||||||
@@ -254,7 +257,7 @@ class DownloadCache(
|
|||||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
|
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
|
||||||
if (it in mangaDir.chapterDirs) {
|
if (it in mangaDir.chapterDirs) {
|
||||||
mangaDir.chapterDirs -= it
|
mangaDir.chapterDirs -= it
|
||||||
}
|
}
|
||||||
|
@@ -159,7 +159,7 @@ class DownloadManager(
|
|||||||
* @return the list of pages from the chapter.
|
* @return the list of pages from the chapter.
|
||||||
*/
|
*/
|
||||||
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List<Page> {
|
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List<Page> {
|
||||||
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, manga.title, source)
|
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, chapter.url, manga.title, source)
|
||||||
val files = chapterDir?.listFiles().orEmpty()
|
val files = chapterDir?.listFiles().orEmpty()
|
||||||
.filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } }
|
.filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } }
|
||||||
|
|
||||||
@@ -185,11 +185,12 @@ class DownloadManager(
|
|||||||
fun isChapterDownloaded(
|
fun isChapterDownloaded(
|
||||||
chapterName: String,
|
chapterName: String,
|
||||||
chapterScanlator: String?,
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
mangaTitle: String,
|
mangaTitle: String,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
skipCache: Boolean = false,
|
skipCache: Boolean = false,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache)
|
return cache.isChapterDownloaded(chapterName, chapterScanlator, chapterUrl, mangaTitle, sourceId, skipCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -368,7 +369,7 @@ class DownloadManager(
|
|||||||
* @param newChapter the target chapter with the new name.
|
* @param newChapter the target chapter with the new name.
|
||||||
*/
|
*/
|
||||||
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
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(manga.title, source).getOrElse { e ->
|
val mangaDir = provider.getMangaDir(manga.title, source).getOrElse { e ->
|
||||||
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
|
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
|
||||||
return
|
return
|
||||||
@@ -379,7 +380,7 @@ class DownloadManager(
|
|||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull() ?: return
|
.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") {
|
if (oldDownload.isFile && oldDownload.extension == "cbz") {
|
||||||
newName += ".cbz"
|
newName += ".cbz"
|
||||||
}
|
}
|
||||||
|
@@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.lang.Hash.md5
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.storage.displayablePath
|
import tachiyomi.core.common.storage.displayablePath
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.storage.service.StorageManager
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -25,6 +27,7 @@ import java.io.IOException
|
|||||||
class DownloadProvider(
|
class DownloadProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storageManager: StorageManager = Injekt.get(),
|
private val storageManager: StorageManager = Injekt.get(),
|
||||||
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val downloadsDir: UniFile?
|
private val downloadsDir: UniFile?
|
||||||
@@ -96,9 +99,15 @@ class DownloadProvider(
|
|||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
* @param source the source of the chapter.
|
* @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)
|
val mangaDir = findMangaDir(mangaTitle, source)
|
||||||
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
|
return getValidChapterDirNames(chapterName, chapterScanlator, chapterUrl).asSequence()
|
||||||
.mapNotNull { mangaDir?.findFile(it) }
|
.mapNotNull { mangaDir?.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
@@ -113,7 +122,7 @@ class DownloadProvider(
|
|||||||
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): Pair<UniFile?, List<UniFile>> {
|
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): Pair<UniFile?, List<UniFile>> {
|
||||||
val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList()
|
val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList()
|
||||||
return mangaDir to chapters.mapNotNull { chapter ->
|
return mangaDir to chapters.mapNotNull { chapter ->
|
||||||
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
|
getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
@@ -125,7 +134,10 @@ class DownloadProvider(
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun getSourceDirName(source: Source): String {
|
fun getSourceDirName(source: Source): String {
|
||||||
return DiskUtil.buildValidFilename(source.toString())
|
return DiskUtil.buildValidFilename(
|
||||||
|
source.toString(),
|
||||||
|
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,23 +146,75 @@ class DownloadProvider(
|
|||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
*/
|
*/
|
||||||
fun getMangaDirName(mangaTitle: String): String {
|
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.
|
* Returns the chapter directory name for a chapter.
|
||||||
*
|
*
|
||||||
* @param chapterName the name of the chapter to query.
|
* @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 {
|
fun getChapterDirName(
|
||||||
val newChapterName = sanitizeChapterName(chapterName)
|
chapterName: String,
|
||||||
return DiskUtil.buildValidFilename(
|
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 {
|
when {
|
||||||
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$newChapterName"
|
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$sanitizedChapterName"
|
||||||
else -> newChapterName
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,24 +229,30 @@ class DownloadProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
|
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
|
||||||
return oldChapter.name != newChapter.name ||
|
return getChapterDirName(oldChapter.name, oldChapter.scanlator, oldChapter.url) !=
|
||||||
oldChapter.scanlator?.takeIf { it.isNotBlank() } != newChapter.scanlator?.takeIf { it.isNotBlank() }
|
getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns valid downloaded chapter directory names.
|
* Returns valid downloaded chapter directory names.
|
||||||
*
|
*
|
||||||
* @param chapterName the name of the chapter to query.
|
* @param chapter the domain chapter object.
|
||||||
* @param chapterScanlator scanlator of the chapter to query
|
|
||||||
*/
|
*/
|
||||||
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?): List<String> {
|
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?, chapterUrl: String): List<String> {
|
||||||
val chapterDirName = getChapterDirName(chapterName, chapterScanlator)
|
val chapterDirName = getChapterDirName(chapterName, chapterScanlator, chapterUrl)
|
||||||
return buildList(2) {
|
val legacyChapterDirNames = getLegacyChapterDirNames(chapterName, chapterScanlator, chapterUrl)
|
||||||
|
|
||||||
|
return buildList {
|
||||||
// Folder of images
|
// Folder of images
|
||||||
add(chapterDirName)
|
add(chapterDirName)
|
||||||
|
|
||||||
// Archived chapters
|
// Archived chapters
|
||||||
add("$chapterDirName.cbz")
|
add("$chapterDirName.cbz")
|
||||||
|
|
||||||
|
// any legacy names
|
||||||
|
legacyChapterDirNames.forEach {
|
||||||
|
add(it)
|
||||||
|
add("$it.cbz")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -274,7 +274,7 @@ class Downloader(
|
|||||||
val wasEmpty = queueState.value.isEmpty()
|
val wasEmpty = queueState.value.isEmpty()
|
||||||
val chaptersToQueue = chapters.asSequence()
|
val chaptersToQueue = chapters.asSequence()
|
||||||
// Filter out those already downloaded.
|
// Filter out those already downloaded.
|
||||||
.filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null }
|
.filter { provider.findChapterDir(it.name, it.scanlator, it.url, manga.title, source) == null }
|
||||||
// Add chapters to queue from the start.
|
// Add chapters to queue from the start.
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
// Filter out those already enqueued.
|
// Filter out those already enqueued.
|
||||||
@@ -336,7 +336,11 @@ class Downloader(
|
|||||||
return
|
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)!!
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -484,6 +484,7 @@ class LibraryScreenModel(
|
|||||||
downloadManager.isChapterDownloaded(
|
downloadManager.isChapterDownloaded(
|
||||||
chapter.name,
|
chapter.name,
|
||||||
chapter.scanlator,
|
chapter.scanlator,
|
||||||
|
chapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
|
@@ -527,7 +527,13 @@ class MangaScreenModel(
|
|||||||
val downloaded = if (isLocal) {
|
val downloaded = if (isLocal) {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
|
downloadManager.isChapterDownloaded(
|
||||||
|
chapter.name,
|
||||||
|
chapter.scanlator,
|
||||||
|
chapter.url,
|
||||||
|
manga.title,
|
||||||
|
manga.source,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val downloadState = when {
|
val downloadState = when {
|
||||||
activeDownload != null -> activeDownload.status
|
activeDownload != null -> activeDownload.status
|
||||||
|
@@ -38,7 +38,6 @@ import eu.kanade.tachiyomi.util.chapter.filterDownloaded
|
|||||||
import eu.kanade.tachiyomi.util.chapter.removeDuplicates
|
import eu.kanade.tachiyomi.util.chapter.removeDuplicates
|
||||||
import eu.kanade.tachiyomi.util.editCover
|
import eu.kanade.tachiyomi.util.editCover
|
||||||
import eu.kanade.tachiyomi.util.lang.byteSize
|
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
|
||||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -175,6 +174,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
!downloadManager.isChapterDownloaded(
|
!downloadManager.isChapterDownloaded(
|
||||||
it.name,
|
it.name,
|
||||||
it.scanlator,
|
it.scanlator,
|
||||||
|
it.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
@@ -184,6 +184,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
downloadManager.isChapterDownloaded(
|
downloadManager.isChapterDownloaded(
|
||||||
it.name,
|
it.name,
|
||||||
it.scanlator,
|
it.scanlator,
|
||||||
|
it.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
@@ -397,6 +398,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
val isDownloaded = downloadManager.isChapterDownloaded(
|
val isDownloaded = downloadManager.isChapterDownloaded(
|
||||||
dbChapter.name,
|
dbChapter.name,
|
||||||
dbChapter.scanlator,
|
dbChapter.scanlator,
|
||||||
|
dbChapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
skipCache = true,
|
skipCache = true,
|
||||||
@@ -473,6 +475,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
val isNextChapterDownloaded = downloadManager.isChapterDownloaded(
|
val isNextChapterDownloaded = downloadManager.isChapterDownloaded(
|
||||||
nextChapter.name,
|
nextChapter.name,
|
||||||
nextChapter.scanlator,
|
nextChapter.scanlator,
|
||||||
|
nextChapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
@@ -757,7 +760,8 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
val chapter = page.chapter.chapter
|
val chapter = page.chapter.chapter
|
||||||
val filenameSuffix = " - ${page.number}"
|
val filenameSuffix = " - ${page.number}"
|
||||||
return DiskUtil.buildValidFilename(
|
return DiskUtil.buildValidFilename(
|
||||||
"${manga.title} - ${chapter.name}".takeBytes(DiskUtil.MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()),
|
"${manga.title} - ${chapter.name}",
|
||||||
|
DiskUtil.MAX_FILE_NAME_BYTES - filenameSuffix.byteSize(),
|
||||||
) + filenameSuffix
|
) + filenameSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -80,6 +80,7 @@ class ChapterLoader(
|
|||||||
val isDownloaded = downloadManager.isChapterDownloaded(
|
val isDownloaded = downloadManager.isChapterDownloaded(
|
||||||
dbChapter.name,
|
dbChapter.name,
|
||||||
dbChapter.scanlator,
|
dbChapter.scanlator,
|
||||||
|
dbChapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
skipCache = true,
|
skipCache = true,
|
||||||
|
@@ -33,7 +33,13 @@ internal class DownloadPageLoader(
|
|||||||
|
|
||||||
override suspend fun getPages(): List<ReaderPage> {
|
override suspend fun getPages(): List<ReaderPage> {
|
||||||
val dbChapter = chapter.chapter
|
val dbChapter = chapter.chapter
|
||||||
val chapterPath = downloadProvider.findChapterDir(dbChapter.name, dbChapter.scanlator, manga.title, source)
|
val chapterPath = downloadProvider.findChapterDir(
|
||||||
|
dbChapter.name,
|
||||||
|
dbChapter.scanlator,
|
||||||
|
dbChapter.url,
|
||||||
|
manga.title,
|
||||||
|
source,
|
||||||
|
)
|
||||||
return if (chapterPath?.isFile == true) {
|
return if (chapterPath?.isFile == true) {
|
||||||
getPagesFromArchive(chapterPath)
|
getPagesFromArchive(chapterPath)
|
||||||
} else {
|
} else {
|
||||||
|
@@ -37,6 +37,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
downloadManager.isChapterDownloaded(
|
downloadManager.isChapterDownloaded(
|
||||||
chapterName = goingToChapter.name,
|
chapterName = goingToChapter.name,
|
||||||
chapterScanlator = goingToChapter.scanlator,
|
chapterScanlator = goingToChapter.scanlator,
|
||||||
|
chapterUrl = goingToChapter.url,
|
||||||
mangaTitle = manga.title,
|
mangaTitle = manga.title,
|
||||||
sourceId = manga.source,
|
sourceId = manga.source,
|
||||||
skipCache = true,
|
skipCache = true,
|
||||||
|
@@ -107,6 +107,7 @@ class UpdatesScreenModel(
|
|||||||
val downloaded = downloadManager.isChapterDownloaded(
|
val downloaded = downloadManager.isChapterDownloaded(
|
||||||
update.chapterName,
|
update.chapterName,
|
||||||
update.scanlator,
|
update.scanlator,
|
||||||
|
update.chapterUrl,
|
||||||
update.mangaTitle,
|
update.mangaTitle,
|
||||||
update.sourceId,
|
update.sourceId,
|
||||||
)
|
)
|
||||||
|
@@ -15,5 +15,5 @@ fun List<Chapter>.filterDownloaded(manga: Manga): List<Chapter> {
|
|||||||
|
|
||||||
val downloadCache: DownloadCache = Injekt.get()
|
val downloadCache: DownloadCache = Injekt.get()
|
||||||
|
|
||||||
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source, false) }
|
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, it.url, manga.title, manga.source, false) }
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,9 @@ import androidx.core.content.ContextCompat
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.CharBuffer
|
||||||
|
import java.nio.charset.CodingErrorAction
|
||||||
|
|
||||||
object DiskUtil {
|
object DiskUtil {
|
||||||
|
|
||||||
@@ -102,26 +105,84 @@ object DiskUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mutate the given filename to make it valid for a FAT filesystem,
|
* Transform a filename fragment to make it safe to use on almost
|
||||||
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
|
* all commonly used filesystems. You can pass an entire filename,
|
||||||
* with a dot), but you can manually add it later.
|
* 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('.', ' ')
|
val name = origName.trim('.', ' ')
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
return "(invalid)"
|
return "(invalid)"
|
||||||
}
|
}
|
||||||
val sb = StringBuilder(name.length)
|
val sb = StringBuilder(name.length)
|
||||||
name.forEach { c ->
|
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)
|
sb.append(c)
|
||||||
} else {
|
} else {
|
||||||
sb.append('_')
|
sb.append('_')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
|
return truncateToLength(sb.toString(), maxBytes)
|
||||||
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
|
}
|
||||||
return sb.toString().take(240)
|
|
||||||
|
/**
|
||||||
|
* 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"
|
const val NOMEDIA_FILE = ".nomedia"
|
||||||
|
|
||||||
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8)
|
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8).
|
||||||
const val MAX_FILE_NAME_BYTES = 250
|
// 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
|
||||||
}
|
}
|
||||||
|
@@ -52,6 +52,7 @@ class UpdatesRepositoryImpl(
|
|||||||
chapterId: Long,
|
chapterId: Long,
|
||||||
chapterName: String,
|
chapterName: String,
|
||||||
scanlator: String?,
|
scanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
read: Boolean,
|
read: Boolean,
|
||||||
bookmark: Boolean,
|
bookmark: Boolean,
|
||||||
lastPageRead: Long,
|
lastPageRead: Long,
|
||||||
@@ -67,6 +68,7 @@ class UpdatesRepositoryImpl(
|
|||||||
chapterId = chapterId,
|
chapterId = chapterId,
|
||||||
chapterName = chapterName,
|
chapterName = chapterName,
|
||||||
scanlator = scanlator,
|
scanlator = scanlator,
|
||||||
|
chapterUrl = chapterUrl,
|
||||||
read = read,
|
read = read,
|
||||||
bookmark = bookmark,
|
bookmark = bookmark,
|
||||||
lastPageRead = lastPageRead,
|
lastPageRead = lastPageRead,
|
||||||
|
24
data/src/main/sqldelight/tachiyomi/migrations/7.sqm
Normal file
24
data/src/main/sqldelight/tachiyomi/migrations/7.sqm
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-- 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
|
||||||
|
WHERE favorite = 1
|
||||||
|
AND date_fetch > date_added
|
||||||
|
ORDER BY date_fetch DESC;
|
@@ -5,6 +5,7 @@ SELECT
|
|||||||
chapters._id AS chapterId,
|
chapters._id AS chapterId,
|
||||||
chapters.name AS chapterName,
|
chapters.name AS chapterName,
|
||||||
chapters.scanlator,
|
chapters.scanlator,
|
||||||
|
chapters.url AS chapterUrl,
|
||||||
chapters.read,
|
chapters.read,
|
||||||
chapters.bookmark,
|
chapters.bookmark,
|
||||||
chapters.last_page_read,
|
chapters.last_page_read,
|
||||||
|
@@ -192,6 +192,8 @@ class LibraryPreferences(
|
|||||||
|
|
||||||
fun updateMangaTitles() = preferenceStore.getBoolean("pref_update_library_manga_titles", false)
|
fun updateMangaTitles() = preferenceStore.getBoolean("pref_update_library_manga_titles", false)
|
||||||
|
|
||||||
|
fun disallowNonAsciiFilenames() = preferenceStore.getBoolean("disallow_non_ascii_filenames", false)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
enum class ChapterSwipeAction {
|
enum class ChapterSwipeAction {
|
||||||
|
@@ -8,6 +8,7 @@ data class UpdatesWithRelations(
|
|||||||
val chapterId: Long,
|
val chapterId: Long,
|
||||||
val chapterName: String,
|
val chapterName: String,
|
||||||
val scanlator: String?,
|
val scanlator: String?,
|
||||||
|
val chapterUrl: String,
|
||||||
val read: Boolean,
|
val read: Boolean,
|
||||||
val bookmark: Boolean,
|
val bookmark: Boolean,
|
||||||
val lastPageRead: Long,
|
val lastPageRead: Long,
|
||||||
|
@@ -320,6 +320,8 @@
|
|||||||
<string name="pref_mark_duplicate_read_chapter_read_new">After fetching new chapter</string>
|
<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_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 -->
|
<!-- Extension section -->
|
||||||
<string name="multi_lang">Multi</string>
|
<string name="multi_lang">Multi</string>
|
||||||
|
Reference in New Issue
Block a user