From f7849d2956d3d1d324b270b37a2c044b863676c4 Mon Sep 17 00:00:00 2001 From: Selfhost Alt Date: Thu, 14 Sep 2023 22:12:22 -0700 Subject: [PATCH 01/14] Fix typo in fixParsedNameCase --- server/utils/parsers/parseFullName.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/utils/parsers/parseFullName.js b/server/utils/parsers/parseFullName.js index daf2de27..c6d82e40 100644 --- a/server/utils/parsers/parseFullName.js +++ b/server/utils/parsers/parseFullName.js @@ -70,8 +70,8 @@ module.exports = (nameToParse, partToReturn, fixCase, stopOnError, useLongLists) namePartWords[j].slice(3).toLowerCase(); } else if ( namePartLabels[j] === 'suffix' && - nameParts[j].slice(-1) !== '.' && - !suffixList.indexOf(nameParts[j].toLowerCase()) + namePartWords[j].slice(-1) !== '.' && + !suffixList.indexOf(namePartWords[j].toLowerCase()) ) { // Convert suffix abbreviations to UPPER CASE if (namePartWords[j] === namePartWords[j].toLowerCase()) { namePartWords[j] = namePartWords[j].toUpperCase(); @@ -343,4 +343,4 @@ module.exports = (nameToParse, partToReturn, fixCase, stopOnError, useLongLists) parsedName = fixParsedNameCase(parsedName, fixCase); return partToReturn === 'all' ? parsedName : parsedName[partToReturn]; -}; \ No newline at end of file +}; From 8b39b01269dd29b0fcb19976ceaa0ce2bac9e7ca Mon Sep 17 00:00:00 2001 From: Selfhost Alt Date: Thu, 14 Sep 2023 22:35:33 -0700 Subject: [PATCH 02/14] Scan for empty book series more efficiently --- server/Database.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/Database.js b/server/Database.js index a959d0a9..fe4362f4 100644 --- a/server/Database.js +++ b/server/Database.js @@ -684,7 +684,15 @@ class Database { // Remove empty series const emptySeries = await this.seriesModel.findAll({ - where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)`), 0) + attributes: ['series.id', 'series.name', [this.sequelize.fn('COUNT', '*'), 'book_count']], + include: [ + { + model: this.bookSeriesModel, + required: false + } + ], + group:["series.id", 'series.name'], + having: { 'book_count': 0 } }) for (const series of emptySeries) { Logger.warn(`Found series "${series.name}" with no books - removing it`) From 19cf3bfb9f6c22df0095c65eacdd2d2f2d9f4778 Mon Sep 17 00:00:00 2001 From: Selfhost Alt Date: Fri, 15 Sep 2023 13:32:21 -0700 Subject: [PATCH 03/14] Fix query to actually return empty series --- server/Database.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/Database.js b/server/Database.js index fe4362f4..64a67a1b 100644 --- a/server/Database.js +++ b/server/Database.js @@ -684,15 +684,13 @@ class Database { // Remove empty series const emptySeries = await this.seriesModel.findAll({ - attributes: ['series.id', 'series.name', [this.sequelize.fn('COUNT', '*'), 'book_count']], include: [ { model: this.bookSeriesModel, required: false } ], - group:["series.id", 'series.name'], - having: { 'book_count': 0 } + where:{ '$bookSeries.id$': null } }) for (const series of emptySeries) { Logger.warn(`Found series "${series.name}" with no books - removing it`) From cfd9a01da7a2e654511899c921461f6781e10914 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 17 Sep 2023 12:40:13 -0500 Subject: [PATCH 04/14] Fix:Server crash when removing item from playlist #2115 --- server/controllers/PlaylistController.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index 16514ef8..c501f287 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -206,7 +206,7 @@ class PlaylistController { await Database.createPlaylistMediaItem(playlistMediaItem) const jsonExpanded = await req.playlist.getOldJsonExpanded() - SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) + SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded) res.json(jsonExpanded) } @@ -376,9 +376,9 @@ class PlaylistController { if (!numMediaItems) { Logger.info(`[PlaylistController] Playlist "${req.playlist.name}" has no more items - removing it`) await req.playlist.destroy() - SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded) + SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded) } else { - SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) + SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded) } } res.json(jsonExpanded) From 0aae672e197d785e10110978b8b1061def8c5514 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 17 Sep 2023 14:53:25 -0500 Subject: [PATCH 05/14] Fix:Scanner purge cover cache when extracting from audio file --- server/managers/CoverManager.js | 35 ++------------------------------ server/scanner/BookScanner.js | 4 ++-- server/scanner/PodcastScanner.js | 4 ++-- 3 files changed, 6 insertions(+), 37 deletions(-) diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index 1a83b7cc..cd792428 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -229,38 +229,6 @@ class CoverManager { } } - async saveEmbeddedCoverArt(libraryItem) { - let audioFileWithCover = null - if (libraryItem.mediaType === 'book') { - audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt) - } else if (libraryItem.mediaType == 'podcast') { - const episodeWithCover = libraryItem.media.episodes.find(ep => ep.audioFile.embeddedCoverArt) - if (episodeWithCover) audioFileWithCover = episodeWithCover.audioFile - } else if (libraryItem.mediaType === 'music') { - audioFileWithCover = libraryItem.media.audioFile - } - if (!audioFileWithCover) return false - - const coverDirPath = this.getCoverDirectory(libraryItem) - await fs.ensureDir(coverDirPath) - - const coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg' - const coverFilePath = Path.join(coverDirPath, coverFilename) - - const coverAlreadyExists = await fs.pathExists(coverFilePath) - if (coverAlreadyExists) { - Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`) - return false - } - - const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath) - if (success) { - libraryItem.updateMediaCover(coverFilePath) - return coverFilePath - } - return false - } - /** * Extract cover art from audio file and save for library item * @param {import('../models/Book').AudioFileObject[]} audioFiles @@ -268,7 +236,7 @@ class CoverManager { * @param {string} [libraryItemPath] null for isFile library items * @returns {Promise} returns cover path */ - async saveEmbeddedCoverArtNew(audioFiles, libraryItemId, libraryItemPath) { + async saveEmbeddedCoverArt(audioFiles, libraryItemId, libraryItemPath) { let audioFileWithCover = audioFiles.find(af => af.embeddedCoverArt) if (!audioFileWithCover) return null @@ -291,6 +259,7 @@ class CoverManager { const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath) if (success) { + await CacheManager.purgeCoverCache(libraryItemId) return coverFilePath } return null diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 190bd69f..c5916b25 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -313,7 +313,7 @@ class BookScanner { // If no cover then extract cover from audio file if available OR search for cover if enabled in server settings if (!media.coverPath) { const libraryItemDir = existingLibraryItem.isFile ? null : existingLibraryItem.path - const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(media.audioFiles, existingLibraryItem.id, libraryItemDir) + const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(media.audioFiles, existingLibraryItem.id, libraryItemDir) if (extractedCoverPath) { libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`) media.coverPath = extractedCoverPath @@ -461,7 +461,7 @@ class BookScanner { if (!bookObject.coverPath) { const libraryItemDir = libraryItemObj.isFile ? null : libraryItemObj.path // Extract and save embedded cover art - const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(scannedAudioFiles, libraryItemObj.id, libraryItemDir) + const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(scannedAudioFiles, libraryItemObj.id, libraryItemDir) if (extractedCoverPath) { bookObject.coverPath = extractedCoverPath } else if (Database.serverSettings.scannerFindCovers) { diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index 4a80d40a..d5a799cf 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -178,7 +178,7 @@ class PodcastScanner { // If no cover then extract cover from audio file if available if (!media.coverPath && existingPodcastEpisodes.length) { const audioFiles = existingPodcastEpisodes.map(ep => ep.audioFile) - const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(audioFiles, existingLibraryItem.id, existingLibraryItem.path) + const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(audioFiles, existingLibraryItem.id, existingLibraryItem.path) if (extractedCoverPath) { libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`) media.coverPath = extractedCoverPath @@ -279,7 +279,7 @@ class PodcastScanner { // If cover was not found in folder then check embedded covers in audio files if (!podcastObject.coverPath && scannedAudioFiles.length) { // Extract and save embedded cover art - podcastObject.coverPath = await CoverManager.saveEmbeddedCoverArtNew(scannedAudioFiles, libraryItemObj.id, libraryItemObj.path) + podcastObject.coverPath = await CoverManager.saveEmbeddedCoverArt(scannedAudioFiles, libraryItemObj.id, libraryItemObj.path) } libraryItemObj.podcast = podcastObject From d18592eaeb436097729edea3ef3f1491b2ff40a1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 17 Sep 2023 15:29:39 -0500 Subject: [PATCH 06/14] Fix:Duplicate series and authors being added on matches and scans #2106 --- server/models/Author.js | 9 +++------ server/models/Series.js | 9 +++------ server/routers/ApiRouter.js | 12 ++++++++++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/server/models/Author.js b/server/models/Author.js index 220abeb8..c6537ec1 100644 --- a/server/models/Author.js +++ b/server/models/Author.js @@ -1,4 +1,4 @@ -const { DataTypes, Model, literal } = require('sequelize') +const { DataTypes, Model, where, fn, col } = require('sequelize') const oldAuthor = require('../objects/entities/Author') @@ -114,14 +114,11 @@ class Author extends Model { static async getOldByNameAndLibrary(authorName, libraryId) { const author = (await this.findOne({ where: [ - literal(`name = ':authorName' COLLATE NOCASE`), + where(fn('lower', col('name')), authorName.toLowerCase()), { libraryId } - ], - replacements: { - authorName - } + ] }))?.getOldAuthor() return author } diff --git a/server/models/Series.js b/server/models/Series.js index 2b37002d..81c27a8b 100644 --- a/server/models/Series.js +++ b/server/models/Series.js @@ -1,4 +1,4 @@ -const { DataTypes, Model, literal } = require('sequelize') +const { DataTypes, Model, where, fn, col } = require('sequelize') const oldSeries = require('../objects/entities/Series') @@ -105,14 +105,11 @@ class Series extends Model { static async getOldByNameAndLibrary(seriesName, libraryId) { const series = (await this.findOne({ where: [ - literal(`name = ':seriesName' COLLATE NOCASE`), + where(fn('lower', col('name')), seriesName.toLowerCase()), { libraryId } - ], - replacements: { - seriesName - } + ] }))?.getOldSeries() return series } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index a0bb2544..71d9429e 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -553,13 +553,17 @@ class ApiRouter { continue } + if (mediaMetadata.authors[i].id?.startsWith('new')) { + mediaMetadata.authors[i].id = null + } + // Ensure the ID for the author exists if (mediaMetadata.authors[i].id && !(await Database.checkAuthorExists(libraryId, mediaMetadata.authors[i].id))) { Logger.warn(`[ApiRouter] Author id "${mediaMetadata.authors[i].id}" does not exist`) mediaMetadata.authors[i].id = null } - if (!mediaMetadata.authors[i].id || mediaMetadata.authors[i].id.startsWith('new')) { + if (!mediaMetadata.authors[i].id) { let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryId) if (!author) { author = new Author() @@ -590,13 +594,17 @@ class ApiRouter { continue } + if (mediaMetadata.series[i].id?.startsWith('new')) { + mediaMetadata.series[i].id = null + } + // Ensure the ID for the series exists if (mediaMetadata.series[i].id && !(await Database.checkSeriesExists(libraryId, mediaMetadata.series[i].id))) { Logger.warn(`[ApiRouter] Series id "${mediaMetadata.series[i].id}" does not exist`) mediaMetadata.series[i].id = null } - if (!mediaMetadata.series[i].id || mediaMetadata.series[i].id.startsWith('new')) { + if (!mediaMetadata.series[i].id) { let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesName, libraryId) if (!seriesItem) { seriesItem = new Series() From 87eaacea220e43df03cf02d0a5b8a870e509a7ae Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 17 Sep 2023 15:53:25 -0500 Subject: [PATCH 07/14] Fix empty podcast and empty book queries when cleaning db on init --- server/Database.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/server/Database.js b/server/Database.js index 64a67a1b..5b8a71e2 100644 --- a/server/Database.js +++ b/server/Database.js @@ -666,7 +666,11 @@ class Database { async cleanDatabase() { // Remove invalid Podcast records const podcastsWithNoLibraryItem = await this.podcastModel.findAll({ - where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM libraryItems li WHERE li.mediaId = podcast.id)`), 0) + include: { + model: this.libraryItemModel, + required: false + }, + where: { '$libraryItem.id$': null } }) for (const podcast of podcastsWithNoLibraryItem) { Logger.warn(`Found podcast "${podcast.title}" with no libraryItem - removing it`) @@ -675,7 +679,11 @@ class Database { // Remove invalid Book records const booksWithNoLibraryItem = await this.bookModel.findAll({ - where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM libraryItems li WHERE li.mediaId = book.id)`), 0) + include: { + model: this.libraryItemModel, + required: false + }, + where: { '$libraryItem.id$': null } }) for (const book of booksWithNoLibraryItem) { Logger.warn(`Found book "${book.title}" with no libraryItem - removing it`) @@ -684,13 +692,11 @@ class Database { // Remove empty series const emptySeries = await this.seriesModel.findAll({ - include: [ - { - model: this.bookSeriesModel, - required: false - } - ], - where:{ '$bookSeries.id$': null } + include: { + model: this.bookSeriesModel, + required: false + }, + where: { '$bookSeries.id$': null } }) for (const series of emptySeries) { Logger.warn(`Found series "${series.name}" with no books - removing it`) From 8ab0a0a14ddea54ac0704f67d1aeca0a035d755a Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 17 Sep 2023 16:09:21 -0500 Subject: [PATCH 08/14] Update personalized shelves logs to dev logs --- server/models/LibraryItem.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index d7670e8f..4ca599dd 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -536,7 +536,7 @@ class LibraryItem extends Model { }) } } - Logger.debug(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) + Logger.dev(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) let start = Date.now() if (library.isBook) { @@ -553,7 +553,7 @@ class LibraryItem extends Model { total: continueSeriesPayload.count }) } - Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.dev(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } else if (library.isPodcast) { // "Newest Episodes" shelf const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit) @@ -567,7 +567,7 @@ class LibraryItem extends Model { total: newestEpisodesPayload.count }) } - Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.dev(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } start = Date.now() @@ -583,7 +583,7 @@ class LibraryItem extends Model { total: mostRecentPayload.count }) } - Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.dev(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`) if (library.isBook) { start = Date.now() @@ -599,7 +599,7 @@ class LibraryItem extends Model { total: seriesMostRecentPayload.count }) } - Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.dev(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) start = Date.now() // "Discover" shelf @@ -614,7 +614,7 @@ class LibraryItem extends Model { total: discoverLibraryItemsPayload.count }) } - Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.dev(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } start = Date.now() @@ -645,7 +645,7 @@ class LibraryItem extends Model { }) } } - Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.dev(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`) if (library.isBook) { start = Date.now() @@ -661,7 +661,7 @@ class LibraryItem extends Model { total: newestAuthorsPayload.count }) } - Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.dev(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } Logger.debug(`Loaded ${shelves.length} personalized shelves in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) From e56b8edc0a455af24e75a28aefd2e1d27a36c59d Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 17 Sep 2023 16:13:19 -0500 Subject: [PATCH 09/14] Version bump 2.4.3 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 497a1030..cf9491cc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.4.2", + "version": "2.4.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.4.2", + "version": "2.4.3", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index c6d1e812..3c90c31f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.4.2", + "version": "2.4.3", "description": "Self-hosted audiobook and podcast client", "main": "index.js", "scripts": { diff --git a/package-lock.json b/package-lock.json index a22b954c..9870d246 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.4.2", + "version": "2.4.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.4.2", + "version": "2.4.3", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index ff008aed..88f02c2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.4.2", + "version": "2.4.3", "description": "Self-hosted audiobook and podcast server", "main": "index.js", "scripts": { From 207ba7ec8e6b665cc318aa20cd50ebabbc1024af Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 18 Sep 2023 13:08:19 -0700 Subject: [PATCH 10/14] x-accel: encode all paths to URIs updates util function encodeUriPath to use node:url with a file:// path prefix, and updates all instances x-accel redirection to use this helper util instead of sending unencoded paths into the header. --- server/controllers/BackupController.js | 6 ++++-- server/controllers/LibraryItemController.js | 22 ++++++++++++--------- server/managers/CacheManager.js | 11 +++++++---- server/utils/fileUtils.js | 3 ++- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/server/controllers/BackupController.js b/server/controllers/BackupController.js index 207b3c74..f33624ac 100644 --- a/server/controllers/BackupController.js +++ b/server/controllers/BackupController.js @@ -1,4 +1,5 @@ const Logger = require('../Logger') +const { encodeUriPath } = require('../utils/fileUtils') class BackupController { constructor() { } @@ -37,8 +38,9 @@ class BackupController { */ download(req, res) { if (global.XAccel) { - Logger.debug(`Use X-Accel to serve static file ${req.backup.fullPath}`) - return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + req.backup.fullPath }).send() + const encodedURI = encodeUriPath(global.XAccel + req.backup.fullPath) + Logger.debug(`Use X-Accel to serve static file ${encodedURI}`) + return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send() } res.sendFile(req.backup.fullPath) } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 4def9c04..fa6d5f2f 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -7,7 +7,7 @@ const Database = require('../Database') const zipHelpers = require('../utils/zipHelpers') const { reqSupportsWebp } = require('../utils/index') const { ScanResult } = require('../utils/constants') -const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') +const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils') const LibraryItemScanner = require('../scanner/LibraryItemScanner') const AudioFileScanner = require('../scanner/AudioFileScanner') const Scanner = require('../scanner/Scanner') @@ -235,8 +235,9 @@ class LibraryItemController { } if (global.XAccel) { - Logger.debug(`Use X-Accel to serve static file ${libraryItem.media.coverPath}`) - return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryItem.media.coverPath }).send() + const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath) + Logger.debug(`Use X-Accel to serve static file ${encodedURI}`) + return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send() } return res.sendFile(libraryItem.media.coverPath) } @@ -575,8 +576,9 @@ class LibraryItemController { const libraryFile = req.libraryFile if (global.XAccel) { - Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`) - return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send() + const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path) + Logger.debug(`Use X-Accel to serve static file ${encodedURI}`) + return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send() } // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available @@ -632,8 +634,9 @@ class LibraryItemController { Logger.info(`[LibraryItemController] User "${req.user.username}" requested file download at "${libraryFile.metadata.path}"`) if (global.XAccel) { - Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`) - return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send() + const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path) + Logger.debug(`Use X-Accel to serve static file ${encodedURI}`) + return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send() } // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available @@ -673,8 +676,9 @@ class LibraryItemController { const ebookFilePath = ebookFile.metadata.path if (global.XAccel) { - Logger.debug(`Use X-Accel to serve static file ${ebookFilePath}`) - return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + ebookFilePath }).send() + const encodedURI = encodeUriPath(global.XAccel + ebookFilePath) + Logger.debug(`Use X-Accel to serve static file ${encodedURI}`) + return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send() } res.sendFile(ebookFilePath) diff --git a/server/managers/CacheManager.js b/server/managers/CacheManager.js index fb0cd26c..d7baee3b 100644 --- a/server/managers/CacheManager.js +++ b/server/managers/CacheManager.js @@ -3,6 +3,7 @@ const fs = require('../libs/fsExtra') const stream = require('stream') const Logger = require('../Logger') const { resizeImage } = require('../utils/ffmpegHelpers') +const { encodeUriPath } = require('../utils/fileUtils') class CacheManager { constructor() { @@ -50,8 +51,9 @@ class CacheManager { // Cache exists if (await fs.pathExists(path)) { if (global.XAccel) { - Logger.debug(`Use X-Accel to serve static file ${path}`) - return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + path }).send() + const encodedURI = encodeUriPath(global.XAccel + path) + Logger.debug(`Use X-Accel to serve static file ${encodedURI}`) + return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send() } const r = fs.createReadStream(path) @@ -73,8 +75,9 @@ class CacheManager { if (!writtenFile) return res.sendStatus(500) if (global.XAccel) { - Logger.debug(`Use X-Accel to serve static file ${writtenFile}`) - return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + writtenFile }).send() + const encodedURI = encodeUriPath(global.XAccel + writtenFile) + Logger.debug(`Use X-Accel to serve static file ${encodedURI}`) + return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send() } var readStream = fs.createReadStream(writtenFile) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 16aae18c..966c7a93 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -293,5 +293,6 @@ module.exports.removeFile = (path) => { } module.exports.encodeUriPath = (path) => { - return filePathToPOSIX(path).replace(/%/g, '%25').replace(/#/g, '%23') + const uri = new URL(path, "file://") + return uri.pathname } From 2c7132438158808aae9f1b9e43039356a2591c11 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 18 Sep 2023 16:43:43 -0500 Subject: [PATCH 11/14] Fix:Book re-scan properly checking if existing coverPath exists #2110 --- server/scanner/BookScanner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index c5916b25..ed905a03 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -136,7 +136,7 @@ class BookScanner { } // Check if cover was removed - if (media.coverPath && !libraryItemData.imageLibraryFiles.some(lf => lf.metadata.path === media.coverPath)) { + if (media.coverPath && !libraryItemData.imageLibraryFiles.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { media.coverPath = null hasMediaChanges = true } From b5a27226cc31e20c6cb7a199f143ad756089e07a Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 18 Sep 2023 16:45:30 -0500 Subject: [PATCH 12/14] Fix:Misleading log on cover manager --- server/managers/CoverManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index cd792428..f30c9c6d 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -253,7 +253,7 @@ class CoverManager { const coverAlreadyExists = await fs.pathExists(coverFilePath) if (coverAlreadyExists) { - Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${libraryItemPath}" - bail`) + Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${coverFilePath}" - bail`) return null } From ae88a4d20a30cbaf15af56e66e0eac36b1b49fb1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 18 Sep 2023 17:38:45 -0500 Subject: [PATCH 13/14] Fix:Matching a library with no items not removing library scan #2118 --- client/layouts/default.vue | 12 ++++++++---- server/scanner/LibraryScan.js | 10 +++++++++- server/scanner/Scanner.js | 14 ++++++++++---- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 805fb8a1..2817f23f 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -231,8 +231,12 @@ export default { scanComplete(data) { console.log('Scan complete received', data) - var message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" complete!` - if (data.results) { + let message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" complete!` + let toastType = 'success' + if (data.error) { + message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" finished with error:\n${data.error}` + toastType = 'error' + } else if (data.results) { var scanResultMsgs = [] var results = data.results if (results.added) scanResultMsgs.push(`${results.added} added`) @@ -247,9 +251,9 @@ export default { var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id) if (existingScan && !isNaN(existingScan.toastId)) { - this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: 'success', closeButton: false, onClose: () => null } }, true) + this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: toastType, closeButton: false, onClose: () => null } }, true) } else { - this.$toast.success(message, { timeout: 5000 }) + this.$toast[toastType](message, { timeout: 5000 }) } this.$store.commit('scanners/remove', data) diff --git a/server/scanner/LibraryScan.js b/server/scanner/LibraryScan.js index 98174a5a..88562e2c 100644 --- a/server/scanner/LibraryScan.js +++ b/server/scanner/LibraryScan.js @@ -19,6 +19,7 @@ class LibraryScan { this.startedAt = null this.finishedAt = null this.elapsed = null + this.error = null this.resultsMissing = 0 this.resultsAdded = 0 @@ -52,6 +53,7 @@ class LibraryScan { id: this.libraryId, type: this.type, name: this.libraryName, + error: this.error, results: { added: this.resultsAdded, updated: this.resultsUpdated, @@ -74,6 +76,7 @@ class LibraryScan { startedAt: this.startedAt, finishedAt: this.finishedAt, elapsed: this.elapsed, + error: this.error, resultsAdded: this.resultsAdded, resultsUpdated: this.resultsUpdated, resultsMissing: this.resultsMissing @@ -88,9 +91,14 @@ class LibraryScan { this.startedAt = Date.now() } - setComplete() { + /** + * + * @param {string} error + */ + setComplete(error = null) { this.finishedAt = Date.now() this.elapsed = this.finishedAt - this.startedAt + this.error = error } getLogLevelString(level) { diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index ee1fcc37..266ce8d8 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -328,7 +328,7 @@ class Scanner { let offset = 0 const libraryScan = new LibraryScan() - libraryScan.setData(library, null, 'match') + libraryScan.setData(library, 'match') LibraryScanner.librariesScanning.push(libraryScan.getScanEmitData) SocketAuthority.emitter('scan_start', libraryScan.getScanEmitData) @@ -338,10 +338,9 @@ class Scanner { while (hasMoreChunks) { const libraryItems = await Database.libraryItemModel.getLibraryItemsIncrement(offset, limit, { libraryId: library.id }) if (!libraryItems.length) { - Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) - SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData) - return + break } + offset += limit hasMoreChunks = libraryItems.length < limit let oldLibraryItems = libraryItems.map(li => Database.libraryItemModel.getOldLibraryItem(li)) @@ -352,6 +351,13 @@ class Scanner { } } + if (offset === 0) { + Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) + libraryScan.setComplete('Library has no items') + } else { + libraryScan.setComplete() + } + delete LibraryScanner.cancelLibraryScan[libraryScan.libraryId] LibraryScanner.librariesScanning = LibraryScanner.librariesScanning.filter(ls => ls.id !== library.id) SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData) From 9967a5dc6646331b0a4b37dafb3ca4f9bc2282dc Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 19 Sep 2023 15:42:38 -0500 Subject: [PATCH 14/14] Fix:Set ebookFormat on scans #2126 --- server/objects/files/EBookFile.js | 2 +- server/objects/mediaTypes/Book.js | 4 ++-- server/scanner/BookScanner.js | 2 ++ server/scanner/LibraryItemScanData.js | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/server/objects/files/EBookFile.js b/server/objects/files/EBookFile.js index a3558a88..ae40dc88 100644 --- a/server/objects/files/EBookFile.js +++ b/server/objects/files/EBookFile.js @@ -16,7 +16,7 @@ class EBookFile { construct(file) { this.ino = file.ino this.metadata = new FileMetadata(file.metadata) - this.ebookFormat = file.ebookFormat + this.ebookFormat = file.ebookFormat || this.metadata.format this.addedAt = file.addedAt this.updatedAt = file.updatedAt } diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js index e9eec451..598c2265 100644 --- a/server/objects/mediaTypes/Book.js +++ b/server/objects/mediaTypes/Book.js @@ -73,7 +73,7 @@ class Book { numInvalidAudioFiles: this.invalidAudioFiles.length, duration: this.duration, size: this.size, - ebookFormat: this.ebookFile ? this.ebookFile.ebookFormat : null + ebookFormat: this.ebookFile?.ebookFormat } } @@ -90,7 +90,7 @@ class Book { size: this.size, tracks: this.tracks.map(t => t.toJSON()), missingParts: [...this.missingParts], - ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null + ebookFile: this.ebookFile?.toJSON() || null } } diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index ed905a03..1e51cf52 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -160,6 +160,7 @@ class BookScanner { // Prefer to use an epub ebook then fallback to the first ebook found let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0] + ebookLibraryFile = ebookLibraryFile.toJSON() // Ebook file is the same as library file except for additional `ebookFormat` ebookLibraryFile.ebookFormat = ebookLibraryFile.metadata.ext.slice(1).toLowerCase() media.ebookFile = ebookLibraryFile @@ -386,6 +387,7 @@ class BookScanner { } if (ebookLibraryFile) { + ebookLibraryFile = ebookLibraryFile.toJSON() ebookLibraryFile.ebookFormat = ebookLibraryFile.metadata.ext.slice(1).toLowerCase() } diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index cae2c70e..7bfcdefc 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -102,7 +102,7 @@ class LibraryItemScanData { return this.libraryFiles.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } - /** @type {LibraryItem.LibraryFileObject[]} */ + /** @type {import('../objects/files/LibraryFile')[]} */ get ebookLibraryFiles() { return this.libraryFiles.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) }