diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue
index fba30ec9..7cc3c33a 100644
--- a/client/components/readers/EpubReader.vue
+++ b/client/components/readers/EpubReader.vue
@@ -42,6 +42,7 @@ export default {
rendition: null,
ereaderSettings: {
theme: 'dark',
+ font: 'serif',
fontScale: 100,
lineSpacing: 115,
spread: 'auto'
@@ -130,6 +131,7 @@ export default {
const fontScale = settings.fontScale || 100
this.rendition.themes.fontSize(`${fontScale}%`)
+ this.rendition.themes.font(settings.font)
this.rendition.spread(settings.spread || 'auto')
},
prev() {
diff --git a/client/components/readers/Reader.vue b/client/components/readers/Reader.vue
index 120bb400..569ff84f 100644
--- a/client/components/readers/Reader.vue
+++ b/client/components/readers/Reader.vue
@@ -63,7 +63,13 @@
@@ -103,6 +109,7 @@ export default {
showSettings: false,
ereaderSettings: {
theme: 'dark',
+ font: 'serif',
fontScale: 100,
lineSpacing: 115,
spread: 'auto'
@@ -142,16 +149,28 @@ export default {
]
},
themeItems() {
- return [
- {
- text: this.$strings.LabelThemeDark,
- value: 'dark'
- },
- {
- text: this.$strings.LabelThemeLight,
- value: 'light'
- }
- ]
+ return {
+ theme: [
+ {
+ text: this.$strings.LabelThemeDark,
+ value: 'dark'
+ },
+ {
+ text: this.$strings.LabelThemeLight,
+ value: 'light'
+ }
+ ],
+ font: [
+ {
+ text: 'Sans',
+ value: 'sans-serif',
+ },
+ {
+ text: 'Serif',
+ value: 'serif',
+ }
+ ]
+ }
},
componentName() {
if (this.ebookType === 'epub') return 'readers-epub-reader'
diff --git a/client/strings/da.json b/client/strings/da.json
index adf138a1..3197cc3c 100644
--- a/client/strings/da.json
+++ b/client/strings/da.json
@@ -260,6 +260,7 @@
"LabelFinished": "Færdig",
"LabelFolder": "Mappe",
"LabelFolders": "Mapper",
+ "LabelFontFamily": "Fontfamilie",
"LabelFontScale": "Skriftstørrelse",
"LabelFormat": "Format",
"LabelGenre": "Genre",
diff --git a/client/strings/de.json b/client/strings/de.json
index a072a549..942cad8b 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -260,6 +260,7 @@
"LabelFinished": "beendet",
"LabelFolder": "Ordner",
"LabelFolders": "Verzeichnisse",
+ "LabelFontFamily": "Schriftfamilie",
"LabelFontScale": "Schriftgröße",
"LabelFormat": "Format",
"LabelGenre": "Kategorie",
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index 24d07726..9e69aa4e 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -260,6 +260,7 @@
"LabelFinished": "Finished",
"LabelFolder": "Folder",
"LabelFolders": "Folders",
+ "LabelFontFamily": "Font family",
"LabelFontScale": "Font scale",
"LabelFormat": "Format",
"LabelGenre": "Genre",
diff --git a/client/strings/es.json b/client/strings/es.json
index 4b37139d..b04815ab 100644
--- a/client/strings/es.json
+++ b/client/strings/es.json
@@ -260,6 +260,7 @@
"LabelFinished": "Terminado",
"LabelFolder": "Carpeta",
"LabelFolders": "Carpetas",
+ "LabelFontFamily": "Familia tipográfica",
"LabelFontScale": "Tamaño de Fuente",
"LabelFormat": "Formato",
"LabelGenre": "Genero",
diff --git a/client/strings/fr.json b/client/strings/fr.json
index 28bdf743..11fa1468 100644
--- a/client/strings/fr.json
+++ b/client/strings/fr.json
@@ -260,6 +260,7 @@
"LabelFinished": "Fini(e)",
"LabelFolder": "Dossier",
"LabelFolders": "Dossiers",
+ "LabelFontFamily": "Famille de polices",
"LabelFontScale": "Taille de la police de caractère",
"LabelFormat": "Format",
"LabelGenre": "Genre",
diff --git a/client/strings/gu.json b/client/strings/gu.json
index 8593a95d..b3de487a 100644
--- a/client/strings/gu.json
+++ b/client/strings/gu.json
@@ -260,6 +260,7 @@
"LabelFinished": "Finished",
"LabelFolder": "Folder",
"LabelFolders": "Folders",
+ "LabelFontFamily": "ફોન્ટ કુટુંબ",
"LabelFontScale": "Font scale",
"LabelFormat": "Format",
"LabelGenre": "Genre",
diff --git a/client/strings/hi.json b/client/strings/hi.json
index 82d25986..d05c1e85 100644
--- a/client/strings/hi.json
+++ b/client/strings/hi.json
@@ -260,6 +260,7 @@
"LabelFinished": "Finished",
"LabelFolder": "Folder",
"LabelFolders": "Folders",
+ "LabelFontFamily": "फुहारा परिवार",
"LabelFontScale": "Font scale",
"LabelFormat": "Format",
"LabelGenre": "Genre",
diff --git a/client/strings/hr.json b/client/strings/hr.json
index e9a323ee..32213095 100644
--- a/client/strings/hr.json
+++ b/client/strings/hr.json
@@ -260,6 +260,7 @@
"LabelFinished": "Finished",
"LabelFolder": "Folder",
"LabelFolders": "Folderi",
+ "LabelFontFamily": "Font family",
"LabelFontScale": "Font scale",
"LabelFormat": "Format",
"LabelGenre": "Genre",
diff --git a/client/strings/it.json b/client/strings/it.json
index f73b3ffc..8de1f4ec 100644
--- a/client/strings/it.json
+++ b/client/strings/it.json
@@ -260,6 +260,7 @@
"LabelFinished": "Finita",
"LabelFolder": "Cartella",
"LabelFolders": "Cartelle",
+ "LabelFontFamily": "Font family",
"LabelFontScale": "Dimensione Font",
"LabelFormat": "Formato",
"LabelGenre": "Genere",
diff --git a/client/strings/lt.json b/client/strings/lt.json
index dee54e12..0623a7ab 100644
--- a/client/strings/lt.json
+++ b/client/strings/lt.json
@@ -260,6 +260,7 @@
"LabelFinished": "Baigta",
"LabelFolder": "Aplankas",
"LabelFolders": "Aplankai",
+ "LabelFontFamily": "Famiglia di font",
"LabelFontScale": "Šrifto mastelis",
"LabelFormat": "Formatas",
"LabelGenre": "Žanras",
diff --git a/client/strings/nl.json b/client/strings/nl.json
index 62696dce..659e3ec5 100644
--- a/client/strings/nl.json
+++ b/client/strings/nl.json
@@ -260,6 +260,7 @@
"LabelFinished": "Voltooid",
"LabelFolder": "Map",
"LabelFolders": "Mappen",
+ "LabelFontFamily": "Lettertypefamilie",
"LabelFontScale": "Lettertype schaal",
"LabelFormat": "Formaat",
"LabelGenre": "Genre",
diff --git a/client/strings/no.json b/client/strings/no.json
index dc7685ee..5bf537f2 100644
--- a/client/strings/no.json
+++ b/client/strings/no.json
@@ -260,6 +260,7 @@
"LabelFinished": "Fullført",
"LabelFolder": "Mappe",
"LabelFolders": "Mapper",
+ "LabelFontFamily": "Fontfamilie",
"LabelFontScale": "Font størrelse",
"LabelFormat": "Format",
"LabelGenre": "Sjanger",
diff --git a/client/strings/pl.json b/client/strings/pl.json
index c4fb50f8..16a0970b 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -260,6 +260,7 @@
"LabelFinished": "Zakończone",
"LabelFolder": "Folder",
"LabelFolders": "Foldery",
+ "LabelFontFamily": "Rodzina czcionek",
"LabelFontScale": "Font scale",
"LabelFormat": "Format",
"LabelGenre": "Gatunek",
diff --git a/client/strings/ru.json b/client/strings/ru.json
index 69868bca..478ac33a 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.json
@@ -260,6 +260,7 @@
"LabelFinished": "Закончен",
"LabelFolder": "Папка",
"LabelFolders": "Папки",
+ "LabelFontFamily": "Семейство шрифтов",
"LabelFontScale": "Масштаб шрифта",
"LabelFormat": "Формат",
"LabelGenre": "Жанр",
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index 219e861a..ded2c9e2 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.json
@@ -260,6 +260,7 @@
"LabelFinished": "已听完",
"LabelFolder": "文件夹",
"LabelFolders": "文件夹",
+ "LabelFontFamily": "字体系列",
"LabelFontScale": "字体比例",
"LabelFormat": "编码格式",
"LabelGenre": "流派",
diff --git a/server/Watcher.js b/server/Watcher.js
index f348ce8e..99318a7e 100644
--- a/server/Watcher.js
+++ b/server/Watcher.js
@@ -6,7 +6,7 @@ const LibraryScanner = require('./scanner/LibraryScanner')
const Task = require('./objects/Task')
const TaskManager = require('./managers/TaskManager')
-const { filePathToPOSIX, isSameOrSubPath } = require('./utils/fileUtils')
+const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs } = require('./utils/fileUtils')
/**
* @typedef PendingFileUpdate
@@ -29,6 +29,8 @@ class FolderWatcher extends EventEmitter {
/** @type {Task} */
this.pendingTask = null
+ this.filesBeingAdded = new Set()
+
/** @type {string[]} */
this.ignoreDirs = []
/** @type {string[]} */
@@ -64,14 +66,13 @@ class FolderWatcher extends EventEmitter {
})
watcher
.on('add', (path) => {
- this.onNewFile(library.id, path)
+ this.onFileAdded(library.id, filePathToPOSIX(path))
}).on('change', (path) => {
// This is triggered from metadata changes, not what we want
- // this.onFileUpdated(path)
}).on('unlink', path => {
- this.onFileRemoved(library.id, path)
+ this.onFileRemoved(library.id, filePathToPOSIX(path))
}).on('rename', (path, pathNext) => {
- this.onRename(library.id, path, pathNext)
+ this.onFileRename(library.id, filePathToPOSIX(path), filePathToPOSIX(pathNext))
}).on('error', (error) => {
Logger.error(`[Watcher] ${error}`)
}).on('ready', () => {
@@ -137,14 +138,31 @@ class FolderWatcher extends EventEmitter {
return this.libraryWatchers.map(lib => lib.watcher.close())
}
- onNewFile(libraryId, path) {
+ /**
+ * Watcher detected file added
+ *
+ * @param {string} libraryId
+ * @param {string} path
+ */
+ onFileAdded(libraryId, path) {
if (this.checkShouldIgnorePath(path)) {
return
}
Logger.debug('[Watcher] File Added', path)
this.addFileUpdate(libraryId, path, 'added')
+
+ if (!this.filesBeingAdded.has(path)) {
+ this.filesBeingAdded.add(path)
+ this.waitForFileToAdd(path)
+ }
}
+ /**
+ * Watcher detected file removed
+ *
+ * @param {string} libraryId
+ * @param {string} path
+ */
onFileRemoved(libraryId, path) {
if (this.checkShouldIgnorePath(path)) {
return
@@ -153,11 +171,13 @@ class FolderWatcher extends EventEmitter {
this.addFileUpdate(libraryId, path, 'deleted')
}
- onFileUpdated(path) {
- Logger.debug('[Watcher] Updated File', path)
- }
-
- onRename(libraryId, pathFrom, pathTo) {
+ /**
+ * Watcher detected file renamed
+ *
+ * @param {string} libraryId
+ * @param {string} path
+ */
+ onFileRename(libraryId, pathFrom, pathTo) {
if (this.checkShouldIgnorePath(pathTo)) {
return
}
@@ -166,13 +186,41 @@ class FolderWatcher extends EventEmitter {
}
/**
- * File update detected from watcher
+ * Get mtimeMs from an added file every second until it is no longer changing
+ * Times out after 180s
+ *
+ * @param {string} path
+ * @param {number} [lastMTimeMs=0]
+ * @param {number} [loop=0]
+ */
+ async waitForFileToAdd(path, lastMTimeMs = 0, loop = 0) {
+ // Safety to catch infinite loop (180s)
+ if (loop >= 180) {
+ Logger.warn(`[Watcher] Waiting to add file at "${path}" timeout (loop ${loop}) - proceeding`)
+ return this.filesBeingAdded.delete(path)
+ }
+
+ const mtimeMs = await getFileMTimeMs(path)
+ if (mtimeMs === lastMTimeMs) {
+ if (lastMTimeMs) Logger.debug(`[Watcher] File finished adding at "${path}"`)
+ return this.filesBeingAdded.delete(path)
+ }
+ if (lastMTimeMs % 5 === 0) {
+ Logger.debug(`[Watcher] Waiting to add file at "${path}". mtimeMs=${mtimeMs} lastMTimeMs=${lastMTimeMs} (loop ${loop})`)
+ }
+ // Wait 1 second
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ this.waitForFileToAdd(path, mtimeMs, ++loop)
+ }
+
+ /**
+ * Queue file update
+ *
* @param {string} libraryId
* @param {string} path
* @param {string} type
*/
addFileUpdate(libraryId, path, type) {
- path = filePathToPOSIX(path)
if (this.pendingFilePaths.includes(path)) return
// Get file library
@@ -222,12 +270,26 @@ class FolderWatcher extends EventEmitter {
type
})
- // Notify server of update after "pendingDelay"
+ this.handlePendingFileUpdatesTimeout()
+ }
+
+ /**
+ * Wait X seconds before notifying scanner that files changed
+ * reset timer if files are still copying
+ */
+ handlePendingFileUpdatesTimeout() {
clearTimeout(this.pendingTimeout)
this.pendingTimeout = setTimeout(() => {
+ // Check that files are not still being added
+ if (this.pendingFileUpdates.some(pfu => this.filesBeingAdded.has(pfu.path))) {
+ Logger.debug(`[Watcher] Still waiting for pending files "${[...this.filesBeingAdded].join(', ')}"`)
+ return this.handlePendingFileUpdatesTimeout()
+ }
+
LibraryScanner.scanFilesChanged(this.pendingFileUpdates, this.pendingTask)
this.pendingTask = null
this.pendingFileUpdates = []
+ this.filesBeingAdded.clear()
}, this.pendingDelay)
}
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 10a77b2a..d2090270 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -621,7 +621,7 @@ class LibraryController {
model: Database.bookModel,
attributes: ['id', 'tags', 'explicit'],
where: bookWhere,
- required: false,
+ required: !req.user.isAdminOrUp, // Only show authors with 0 books for admin users or up
through: {
attributes: []
}
diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js
index 19735fb7..26578f57 100644
--- a/server/utils/fileUtils.js
+++ b/server/utils/fileUtils.js
@@ -38,22 +38,14 @@ function isSameOrSubPath(parentPath, childPath) {
}
module.exports.isSameOrSubPath = isSameOrSubPath
-async function getFileStat(path) {
+function getFileStat(path) {
try {
- var stat = await fs.stat(path)
- return {
- size: stat.size,
- atime: stat.atime,
- mtime: stat.mtime,
- ctime: stat.ctime,
- birthtime: stat.birthtime
- }
+ return fs.stat(path)
} catch (err) {
Logger.error('[fileUtils] Failed to stat', err)
- return false
+ return null
}
}
-module.exports.getFileStat = getFileStat
async function getFileTimestampsWithIno(path) {
try {
@@ -72,12 +64,25 @@ async function getFileTimestampsWithIno(path) {
}
module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno
-async function getFileSize(path) {
- var stat = await getFileStat(path)
- if (!stat) return 0
- return stat.size || 0
+/**
+ * Get file size
+ *
+ * @param {string} path
+ * @returns {Promise}
+ */
+module.exports.getFileSize = async (path) => {
+ return (await getFileStat(path))?.size || 0
+}
+
+/**
+ * Get file mtimeMs
+ *
+ * @param {string} path
+ * @returns {Promise} epoch timestamp
+ */
+module.exports.getFileMTimeMs = async (path) => {
+ return (await getFileStat(path))?.mtimeMs || 0
}
-module.exports.getFileSize = getFileSize
/**
*
diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js
index 6ba6ec5e..785124a9 100644
--- a/server/utils/queries/libraryFilters.js
+++ b/server/utils/queries/libraryFilters.js
@@ -308,6 +308,8 @@ module.exports = {
async getNewestAuthors(library, user, limit) {
if (library.mediaType !== 'book') return { authors: [], count: 0 }
+ const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(user)
+
const { rows: authors, count } = await Database.authorModel.findAndCountAll({
where: {
libraryId: library.id,
@@ -315,9 +317,15 @@ module.exports = {
[Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago
}
},
+ replacements,
include: {
- model: Database.bookAuthorModel,
- required: true // Must belong to a book
+ model: Database.bookModel,
+ attributes: ['id', 'tags', 'explicit'],
+ where: bookWhere,
+ required: true, // Must belong to a book
+ through: {
+ attributes: []
+ }
},
limit,
distinct: true,
@@ -328,7 +336,7 @@ module.exports = {
return {
authors: authors.map((au) => {
- const numBooks = au.bookAuthors?.length || 0
+ const numBooks = au.books.length || 0
return au.getOldAuthor().toJSONExpanded(numBooks)
}),
count