Merge branch 'master' into watcher-update-api

This commit is contained in:
advplyr 2023-10-26 16:33:48 -05:00
commit 3bccd52196
21 changed files with 156 additions and 45 deletions

View File

@ -42,6 +42,7 @@ export default {
rendition: null, rendition: null,
ereaderSettings: { ereaderSettings: {
theme: 'dark', theme: 'dark',
font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto' spread: 'auto'
@ -130,6 +131,7 @@ export default {
const fontScale = settings.fontScale || 100 const fontScale = settings.fontScale || 100
this.rendition.themes.fontSize(`${fontScale}%`) this.rendition.themes.fontSize(`${fontScale}%`)
this.rendition.themes.font(settings.font)
this.rendition.spread(settings.spread || 'auto') this.rendition.spread(settings.spread || 'auto')
}, },
prev() { prev() {

View File

@ -63,7 +63,13 @@
<div class="w-40"> <div class="w-40">
<p class="text-lg">{{ $strings.LabelTheme }}:</p> <p class="text-lg">{{ $strings.LabelTheme }}:</p>
</div> </div>
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" /> <ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems.theme" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-4">
<div class="w-40">
<p class="text-lg">{{ $strings.LabelFontFamily }}:</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.font" :items="themeItems.font" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<div class="w-40"> <div class="w-40">
@ -103,6 +109,7 @@ export default {
showSettings: false, showSettings: false,
ereaderSettings: { ereaderSettings: {
theme: 'dark', theme: 'dark',
font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto' spread: 'auto'
@ -142,16 +149,28 @@ export default {
] ]
}, },
themeItems() { themeItems() {
return [ return {
{ theme: [
text: this.$strings.LabelThemeDark, {
value: 'dark' text: this.$strings.LabelThemeDark,
}, value: 'dark'
{ },
text: this.$strings.LabelThemeLight, {
value: 'light' text: this.$strings.LabelThemeLight,
} value: 'light'
] }
],
font: [
{
text: 'Sans',
value: 'sans-serif',
},
{
text: 'Serif',
value: 'serif',
}
]
}
}, },
componentName() { componentName() {
if (this.ebookType === 'epub') return 'readers-epub-reader' if (this.ebookType === 'epub') return 'readers-epub-reader'

View File

@ -260,6 +260,7 @@
"LabelFinished": "Færdig", "LabelFinished": "Færdig",
"LabelFolder": "Mappe", "LabelFolder": "Mappe",
"LabelFolders": "Mapper", "LabelFolders": "Mapper",
"LabelFontFamily": "Fontfamilie",
"LabelFontScale": "Skriftstørrelse", "LabelFontScale": "Skriftstørrelse",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",

View File

@ -260,6 +260,7 @@
"LabelFinished": "beendet", "LabelFinished": "beendet",
"LabelFolder": "Ordner", "LabelFolder": "Ordner",
"LabelFolders": "Verzeichnisse", "LabelFolders": "Verzeichnisse",
"LabelFontFamily": "Schriftfamilie",
"LabelFontScale": "Schriftgröße", "LabelFontScale": "Schriftgröße",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Kategorie", "LabelGenre": "Kategorie",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folders", "LabelFolders": "Folders",
"LabelFontFamily": "Font family",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Terminado", "LabelFinished": "Terminado",
"LabelFolder": "Carpeta", "LabelFolder": "Carpeta",
"LabelFolders": "Carpetas", "LabelFolders": "Carpetas",
"LabelFontFamily": "Familia tipográfica",
"LabelFontScale": "Tamaño de Fuente", "LabelFontScale": "Tamaño de Fuente",
"LabelFormat": "Formato", "LabelFormat": "Formato",
"LabelGenre": "Genero", "LabelGenre": "Genero",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Fini(e)", "LabelFinished": "Fini(e)",
"LabelFolder": "Dossier", "LabelFolder": "Dossier",
"LabelFolders": "Dossiers", "LabelFolders": "Dossiers",
"LabelFontFamily": "Famille de polices",
"LabelFontScale": "Taille de la police de caractère", "LabelFontScale": "Taille de la police de caractère",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folders", "LabelFolders": "Folders",
"LabelFontFamily": "ફોન્ટ કુટુંબ",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folders", "LabelFolders": "Folders",
"LabelFontFamily": "फुहारा परिवार",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folderi", "LabelFolders": "Folderi",
"LabelFontFamily": "Font family",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Finita", "LabelFinished": "Finita",
"LabelFolder": "Cartella", "LabelFolder": "Cartella",
"LabelFolders": "Cartelle", "LabelFolders": "Cartelle",
"LabelFontFamily": "Font family",
"LabelFontScale": "Dimensione Font", "LabelFontScale": "Dimensione Font",
"LabelFormat": "Formato", "LabelFormat": "Formato",
"LabelGenre": "Genere", "LabelGenre": "Genere",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Baigta", "LabelFinished": "Baigta",
"LabelFolder": "Aplankas", "LabelFolder": "Aplankas",
"LabelFolders": "Aplankai", "LabelFolders": "Aplankai",
"LabelFontFamily": "Famiglia di font",
"LabelFontScale": "Šrifto mastelis", "LabelFontScale": "Šrifto mastelis",
"LabelFormat": "Formatas", "LabelFormat": "Formatas",
"LabelGenre": "Žanras", "LabelGenre": "Žanras",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Voltooid", "LabelFinished": "Voltooid",
"LabelFolder": "Map", "LabelFolder": "Map",
"LabelFolders": "Mappen", "LabelFolders": "Mappen",
"LabelFontFamily": "Lettertypefamilie",
"LabelFontScale": "Lettertype schaal", "LabelFontScale": "Lettertype schaal",
"LabelFormat": "Formaat", "LabelFormat": "Formaat",
"LabelGenre": "Genre", "LabelGenre": "Genre",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Fullført", "LabelFinished": "Fullført",
"LabelFolder": "Mappe", "LabelFolder": "Mappe",
"LabelFolders": "Mapper", "LabelFolders": "Mapper",
"LabelFontFamily": "Fontfamilie",
"LabelFontScale": "Font størrelse", "LabelFontScale": "Font størrelse",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Sjanger", "LabelGenre": "Sjanger",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Zakończone", "LabelFinished": "Zakończone",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Foldery", "LabelFolders": "Foldery",
"LabelFontFamily": "Rodzina czcionek",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Gatunek", "LabelGenre": "Gatunek",

View File

@ -260,6 +260,7 @@
"LabelFinished": "Закончен", "LabelFinished": "Закончен",
"LabelFolder": "Папка", "LabelFolder": "Папка",
"LabelFolders": "Папки", "LabelFolders": "Папки",
"LabelFontFamily": "Семейство шрифтов",
"LabelFontScale": "Масштаб шрифта", "LabelFontScale": "Масштаб шрифта",
"LabelFormat": "Формат", "LabelFormat": "Формат",
"LabelGenre": "Жанр", "LabelGenre": "Жанр",

View File

@ -260,6 +260,7 @@
"LabelFinished": "已听完", "LabelFinished": "已听完",
"LabelFolder": "文件夹", "LabelFolder": "文件夹",
"LabelFolders": "文件夹", "LabelFolders": "文件夹",
"LabelFontFamily": "字体系列",
"LabelFontScale": "字体比例", "LabelFontScale": "字体比例",
"LabelFormat": "编码格式", "LabelFormat": "编码格式",
"LabelGenre": "流派", "LabelGenre": "流派",

View File

@ -6,7 +6,7 @@ const LibraryScanner = require('./scanner/LibraryScanner')
const Task = require('./objects/Task') const Task = require('./objects/Task')
const TaskManager = require('./managers/TaskManager') const TaskManager = require('./managers/TaskManager')
const { filePathToPOSIX, isSameOrSubPath } = require('./utils/fileUtils') const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs } = require('./utils/fileUtils')
/** /**
* @typedef PendingFileUpdate * @typedef PendingFileUpdate
@ -29,6 +29,8 @@ class FolderWatcher extends EventEmitter {
/** @type {Task} */ /** @type {Task} */
this.pendingTask = null this.pendingTask = null
this.filesBeingAdded = new Set()
/** @type {string[]} */ /** @type {string[]} */
this.ignoreDirs = [] this.ignoreDirs = []
/** @type {string[]} */ /** @type {string[]} */
@ -64,14 +66,13 @@ class FolderWatcher extends EventEmitter {
}) })
watcher watcher
.on('add', (path) => { .on('add', (path) => {
this.onNewFile(library.id, path) this.onFileAdded(library.id, filePathToPOSIX(path))
}).on('change', (path) => { }).on('change', (path) => {
// This is triggered from metadata changes, not what we want // This is triggered from metadata changes, not what we want
// this.onFileUpdated(path)
}).on('unlink', path => { }).on('unlink', path => {
this.onFileRemoved(library.id, path) this.onFileRemoved(library.id, filePathToPOSIX(path))
}).on('rename', (path, pathNext) => { }).on('rename', (path, pathNext) => {
this.onRename(library.id, path, pathNext) this.onFileRename(library.id, filePathToPOSIX(path), filePathToPOSIX(pathNext))
}).on('error', (error) => { }).on('error', (error) => {
Logger.error(`[Watcher] ${error}`) Logger.error(`[Watcher] ${error}`)
}).on('ready', () => { }).on('ready', () => {
@ -137,14 +138,31 @@ class FolderWatcher extends EventEmitter {
return this.libraryWatchers.map(lib => lib.watcher.close()) 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)) { if (this.checkShouldIgnorePath(path)) {
return return
} }
Logger.debug('[Watcher] File Added', path) Logger.debug('[Watcher] File Added', path)
this.addFileUpdate(libraryId, path, 'added') 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) { onFileRemoved(libraryId, path) {
if (this.checkShouldIgnorePath(path)) { if (this.checkShouldIgnorePath(path)) {
return return
@ -153,11 +171,13 @@ class FolderWatcher extends EventEmitter {
this.addFileUpdate(libraryId, path, 'deleted') this.addFileUpdate(libraryId, path, 'deleted')
} }
onFileUpdated(path) { /**
Logger.debug('[Watcher] Updated File', path) * Watcher detected file renamed
} *
* @param {string} libraryId
onRename(libraryId, pathFrom, pathTo) { * @param {string} path
*/
onFileRename(libraryId, pathFrom, pathTo) {
if (this.checkShouldIgnorePath(pathTo)) { if (this.checkShouldIgnorePath(pathTo)) {
return 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} libraryId
* @param {string} path * @param {string} path
* @param {string} type * @param {string} type
*/ */
addFileUpdate(libraryId, path, type) { addFileUpdate(libraryId, path, type) {
path = filePathToPOSIX(path)
if (this.pendingFilePaths.includes(path)) return if (this.pendingFilePaths.includes(path)) return
// Get file library // Get file library
@ -222,12 +270,26 @@ class FolderWatcher extends EventEmitter {
type 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) clearTimeout(this.pendingTimeout)
this.pendingTimeout = setTimeout(() => { 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) LibraryScanner.scanFilesChanged(this.pendingFileUpdates, this.pendingTask)
this.pendingTask = null this.pendingTask = null
this.pendingFileUpdates = [] this.pendingFileUpdates = []
this.filesBeingAdded.clear()
}, this.pendingDelay) }, this.pendingDelay)
} }

View File

@ -621,7 +621,7 @@ class LibraryController {
model: Database.bookModel, model: Database.bookModel,
attributes: ['id', 'tags', 'explicit'], attributes: ['id', 'tags', 'explicit'],
where: bookWhere, where: bookWhere,
required: false, required: !req.user.isAdminOrUp, // Only show authors with 0 books for admin users or up
through: { through: {
attributes: [] attributes: []
} }

View File

@ -38,22 +38,14 @@ function isSameOrSubPath(parentPath, childPath) {
} }
module.exports.isSameOrSubPath = isSameOrSubPath module.exports.isSameOrSubPath = isSameOrSubPath
async function getFileStat(path) { function getFileStat(path) {
try { try {
var stat = await fs.stat(path) return fs.stat(path)
return {
size: stat.size,
atime: stat.atime,
mtime: stat.mtime,
ctime: stat.ctime,
birthtime: stat.birthtime
}
} catch (err) { } catch (err) {
Logger.error('[fileUtils] Failed to stat', err) Logger.error('[fileUtils] Failed to stat', err)
return false return null
} }
} }
module.exports.getFileStat = getFileStat
async function getFileTimestampsWithIno(path) { async function getFileTimestampsWithIno(path) {
try { try {
@ -72,12 +64,25 @@ async function getFileTimestampsWithIno(path) {
} }
module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno
async function getFileSize(path) { /**
var stat = await getFileStat(path) * Get file size
if (!stat) return 0 *
return stat.size || 0 * @param {string} path
* @returns {Promise<number>}
*/
module.exports.getFileSize = async (path) => {
return (await getFileStat(path))?.size || 0
}
/**
* Get file mtimeMs
*
* @param {string} path
* @returns {Promise<number>} epoch timestamp
*/
module.exports.getFileMTimeMs = async (path) => {
return (await getFileStat(path))?.mtimeMs || 0
} }
module.exports.getFileSize = getFileSize
/** /**
* *

View File

@ -308,6 +308,8 @@ module.exports = {
async getNewestAuthors(library, user, limit) { async getNewestAuthors(library, user, limit) {
if (library.mediaType !== 'book') return { authors: [], count: 0 } if (library.mediaType !== 'book') return { authors: [], count: 0 }
const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(user)
const { rows: authors, count } = await Database.authorModel.findAndCountAll({ const { rows: authors, count } = await Database.authorModel.findAndCountAll({
where: { where: {
libraryId: library.id, libraryId: library.id,
@ -315,9 +317,15 @@ module.exports = {
[Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago [Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago
} }
}, },
replacements,
include: { include: {
model: Database.bookAuthorModel, model: Database.bookModel,
required: true // Must belong to a book attributes: ['id', 'tags', 'explicit'],
where: bookWhere,
required: true, // Must belong to a book
through: {
attributes: []
}
}, },
limit, limit,
distinct: true, distinct: true,
@ -328,7 +336,7 @@ module.exports = {
return { return {
authors: authors.map((au) => { authors: authors.map((au) => {
const numBooks = au.bookAuthors?.length || 0 const numBooks = au.books.length || 0
return au.getOldAuthor().toJSONExpanded(numBooks) return au.getOldAuthor().toJSONExpanded(numBooks)
}), }),
count count