Merge branch 'advplyr:master' into Fuzzy-Matching

This commit is contained in:
mikiher 2023-09-20 13:12:18 +03:00 committed by GitHub
commit 81a9b8d158
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 118 additions and 105 deletions

View File

@ -231,8 +231,12 @@ export default {
scanComplete(data) { scanComplete(data) {
console.log('Scan complete received', data) console.log('Scan complete received', data)
var message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" complete!` let message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" complete!`
if (data.results) { 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 scanResultMsgs = []
var results = data.results var results = data.results
if (results.added) scanResultMsgs.push(`${results.added} added`) if (results.added) scanResultMsgs.push(`${results.added} added`)
@ -247,9 +251,9 @@ export default {
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id) var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
if (existingScan && !isNaN(existingScan.toastId)) { 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 { } else {
this.$toast.success(message, { timeout: 5000 }) this.$toast[toastType](message, { timeout: 5000 })
} }
this.$store.commit('scanners/remove', data) this.$store.commit('scanners/remove', data)

View File

@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.4.2", "version": "2.4.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.4.2", "version": "2.4.3",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.4.2", "version": "2.4.3",
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.4.2", "version": "2.4.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.4.2", "version": "2.4.3",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.4.2", "version": "2.4.3",
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -666,7 +666,11 @@ class Database {
async cleanDatabase() { async cleanDatabase() {
// Remove invalid Podcast records // Remove invalid Podcast records
const podcastsWithNoLibraryItem = await this.podcastModel.findAll({ 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) { for (const podcast of podcastsWithNoLibraryItem) {
Logger.warn(`Found podcast "${podcast.title}" with no libraryItem - removing it`) Logger.warn(`Found podcast "${podcast.title}" with no libraryItem - removing it`)
@ -675,7 +679,11 @@ class Database {
// Remove invalid Book records // Remove invalid Book records
const booksWithNoLibraryItem = await this.bookModel.findAll({ 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) { for (const book of booksWithNoLibraryItem) {
Logger.warn(`Found book "${book.title}" with no libraryItem - removing it`) Logger.warn(`Found book "${book.title}" with no libraryItem - removing it`)
@ -684,7 +692,11 @@ class Database {
// Remove empty series // Remove empty series
const emptySeries = await this.seriesModel.findAll({ const emptySeries = await this.seriesModel.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)`), 0) include: {
model: this.bookSeriesModel,
required: false
},
where: { '$bookSeries.id$': null }
}) })
for (const series of emptySeries) { for (const series of emptySeries) {
Logger.warn(`Found series "${series.name}" with no books - removing it`) Logger.warn(`Found series "${series.name}" with no books - removing it`)

View File

@ -1,4 +1,5 @@
const Logger = require('../Logger') const Logger = require('../Logger')
const { encodeUriPath } = require('../utils/fileUtils')
class BackupController { class BackupController {
constructor() { } constructor() { }
@ -37,8 +38,9 @@ class BackupController {
*/ */
download(req, res) { download(req, res) {
if (global.XAccel) { if (global.XAccel) {
Logger.debug(`Use X-Accel to serve static file ${req.backup.fullPath}`) const encodedURI = encodeUriPath(global.XAccel + req.backup.fullPath)
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + req.backup.fullPath }).send() 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) res.sendFile(req.backup.fullPath)
} }

View File

@ -7,7 +7,7 @@ const Database = require('../Database')
const zipHelpers = require('../utils/zipHelpers') const zipHelpers = require('../utils/zipHelpers')
const { reqSupportsWebp } = require('../utils/index') const { reqSupportsWebp } = require('../utils/index')
const { ScanResult } = require('../utils/constants') const { ScanResult } = require('../utils/constants')
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
const LibraryItemScanner = require('../scanner/LibraryItemScanner') const LibraryItemScanner = require('../scanner/LibraryItemScanner')
const AudioFileScanner = require('../scanner/AudioFileScanner') const AudioFileScanner = require('../scanner/AudioFileScanner')
const Scanner = require('../scanner/Scanner') const Scanner = require('../scanner/Scanner')
@ -235,8 +235,9 @@ class LibraryItemController {
} }
if (global.XAccel) { if (global.XAccel) {
Logger.debug(`Use X-Accel to serve static file ${libraryItem.media.coverPath}`) const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath)
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryItem.media.coverPath }).send() 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) return res.sendFile(libraryItem.media.coverPath)
} }
@ -575,8 +576,9 @@ class LibraryItemController {
const libraryFile = req.libraryFile const libraryFile = req.libraryFile
if (global.XAccel) { if (global.XAccel) {
Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`) const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path)
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send() 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 // 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}"`) Logger.info(`[LibraryItemController] User "${req.user.username}" requested file download at "${libraryFile.metadata.path}"`)
if (global.XAccel) { if (global.XAccel) {
Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`) const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path)
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send() 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 // 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 const ebookFilePath = ebookFile.metadata.path
if (global.XAccel) { if (global.XAccel) {
Logger.debug(`Use X-Accel to serve static file ${ebookFilePath}`) const encodedURI = encodeUriPath(global.XAccel + ebookFilePath)
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + ebookFilePath }).send() Logger.debug(`Use X-Accel to serve static file ${encodedURI}`)
return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send()
} }
res.sendFile(ebookFilePath) res.sendFile(ebookFilePath)

View File

@ -206,7 +206,7 @@ class PlaylistController {
await Database.createPlaylistMediaItem(playlistMediaItem) await Database.createPlaylistMediaItem(playlistMediaItem)
const jsonExpanded = await req.playlist.getOldJsonExpanded() const jsonExpanded = await req.playlist.getOldJsonExpanded()
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
res.json(jsonExpanded) res.json(jsonExpanded)
} }
@ -376,9 +376,9 @@ class PlaylistController {
if (!numMediaItems) { if (!numMediaItems) {
Logger.info(`[PlaylistController] Playlist "${req.playlist.name}" has no more items - removing it`) Logger.info(`[PlaylistController] Playlist "${req.playlist.name}" has no more items - removing it`)
await req.playlist.destroy() await req.playlist.destroy()
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded) SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
} else { } else {
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
} }
} }
res.json(jsonExpanded) res.json(jsonExpanded)

View File

@ -3,6 +3,7 @@ const fs = require('../libs/fsExtra')
const stream = require('stream') const stream = require('stream')
const Logger = require('../Logger') const Logger = require('../Logger')
const { resizeImage } = require('../utils/ffmpegHelpers') const { resizeImage } = require('../utils/ffmpegHelpers')
const { encodeUriPath } = require('../utils/fileUtils')
class CacheManager { class CacheManager {
constructor() { constructor() {
@ -50,8 +51,9 @@ class CacheManager {
// Cache exists // Cache exists
if (await fs.pathExists(path)) { if (await fs.pathExists(path)) {
if (global.XAccel) { if (global.XAccel) {
Logger.debug(`Use X-Accel to serve static file ${path}`) const encodedURI = encodeUriPath(global.XAccel + path)
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + path }).send() 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) const r = fs.createReadStream(path)
@ -73,8 +75,9 @@ class CacheManager {
if (!writtenFile) return res.sendStatus(500) if (!writtenFile) return res.sendStatus(500)
if (global.XAccel) { if (global.XAccel) {
Logger.debug(`Use X-Accel to serve static file ${writtenFile}`) const encodedURI = encodeUriPath(global.XAccel + writtenFile)
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + writtenFile }).send() 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) var readStream = fs.createReadStream(writtenFile)

View File

@ -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 * Extract cover art from audio file and save for library item
* @param {import('../models/Book').AudioFileObject[]} audioFiles * @param {import('../models/Book').AudioFileObject[]} audioFiles
@ -268,7 +236,7 @@ class CoverManager {
* @param {string} [libraryItemPath] null for isFile library items * @param {string} [libraryItemPath] null for isFile library items
* @returns {Promise<string>} returns cover path * @returns {Promise<string>} returns cover path
*/ */
async saveEmbeddedCoverArtNew(audioFiles, libraryItemId, libraryItemPath) { async saveEmbeddedCoverArt(audioFiles, libraryItemId, libraryItemPath) {
let audioFileWithCover = audioFiles.find(af => af.embeddedCoverArt) let audioFileWithCover = audioFiles.find(af => af.embeddedCoverArt)
if (!audioFileWithCover) return null if (!audioFileWithCover) return null
@ -285,12 +253,13 @@ class CoverManager {
const coverAlreadyExists = await fs.pathExists(coverFilePath) const coverAlreadyExists = await fs.pathExists(coverFilePath)
if (coverAlreadyExists) { 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 return null
} }
const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath) const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
if (success) { if (success) {
await CacheManager.purgeCoverCache(libraryItemId)
return coverFilePath return coverFilePath
} }
return null return null

View File

@ -1,4 +1,4 @@
const { DataTypes, Model, literal } = require('sequelize') const { DataTypes, Model, where, fn, col } = require('sequelize')
const oldAuthor = require('../objects/entities/Author') const oldAuthor = require('../objects/entities/Author')
@ -114,14 +114,11 @@ class Author extends Model {
static async getOldByNameAndLibrary(authorName, libraryId) { static async getOldByNameAndLibrary(authorName, libraryId) {
const author = (await this.findOne({ const author = (await this.findOne({
where: [ where: [
literal(`name = ':authorName' COLLATE NOCASE`), where(fn('lower', col('name')), authorName.toLowerCase()),
{ {
libraryId libraryId
} }
], ]
replacements: {
authorName
}
}))?.getOldAuthor() }))?.getOldAuthor()
return author return author
} }

View File

@ -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() let start = Date.now()
if (library.isBook) { if (library.isBook) {
@ -553,7 +553,7 @@ class LibraryItem extends Model {
total: continueSeriesPayload.count 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) { } else if (library.isPodcast) {
// "Newest Episodes" shelf // "Newest Episodes" shelf
const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit) const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit)
@ -567,7 +567,7 @@ class LibraryItem extends Model {
total: newestEpisodesPayload.count 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() start = Date.now()
@ -583,7 +583,7 @@ class LibraryItem extends Model {
total: mostRecentPayload.count 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) { if (library.isBook) {
start = Date.now() start = Date.now()
@ -599,7 +599,7 @@ class LibraryItem extends Model {
total: seriesMostRecentPayload.count 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() start = Date.now()
// "Discover" shelf // "Discover" shelf
@ -614,7 +614,7 @@ class LibraryItem extends Model {
total: discoverLibraryItemsPayload.count 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() 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) { if (library.isBook) {
start = Date.now() start = Date.now()
@ -661,7 +661,7 @@ class LibraryItem extends Model {
total: newestAuthorsPayload.count 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`) Logger.debug(`Loaded ${shelves.length} personalized shelves in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`)

View File

@ -1,4 +1,4 @@
const { DataTypes, Model, literal } = require('sequelize') const { DataTypes, Model, where, fn, col } = require('sequelize')
const oldSeries = require('../objects/entities/Series') const oldSeries = require('../objects/entities/Series')
@ -105,14 +105,11 @@ class Series extends Model {
static async getOldByNameAndLibrary(seriesName, libraryId) { static async getOldByNameAndLibrary(seriesName, libraryId) {
const series = (await this.findOne({ const series = (await this.findOne({
where: [ where: [
literal(`name = ':seriesName' COLLATE NOCASE`), where(fn('lower', col('name')), seriesName.toLowerCase()),
{ {
libraryId libraryId
} }
], ]
replacements: {
seriesName
}
}))?.getOldSeries() }))?.getOldSeries()
return series return series
} }

View File

@ -16,7 +16,7 @@ class EBookFile {
construct(file) { construct(file) {
this.ino = file.ino this.ino = file.ino
this.metadata = new FileMetadata(file.metadata) this.metadata = new FileMetadata(file.metadata)
this.ebookFormat = file.ebookFormat this.ebookFormat = file.ebookFormat || this.metadata.format
this.addedAt = file.addedAt this.addedAt = file.addedAt
this.updatedAt = file.updatedAt this.updatedAt = file.updatedAt
} }

View File

@ -73,7 +73,7 @@ class Book {
numInvalidAudioFiles: this.invalidAudioFiles.length, numInvalidAudioFiles: this.invalidAudioFiles.length,
duration: this.duration, duration: this.duration,
size: this.size, size: this.size,
ebookFormat: this.ebookFile ? this.ebookFile.ebookFormat : null ebookFormat: this.ebookFile?.ebookFormat
} }
} }
@ -90,7 +90,7 @@ class Book {
size: this.size, size: this.size,
tracks: this.tracks.map(t => t.toJSON()), tracks: this.tracks.map(t => t.toJSON()),
missingParts: [...this.missingParts], missingParts: [...this.missingParts],
ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null ebookFile: this.ebookFile?.toJSON() || null
} }
} }

View File

@ -553,13 +553,17 @@ class ApiRouter {
continue continue
} }
if (mediaMetadata.authors[i].id?.startsWith('new')) {
mediaMetadata.authors[i].id = null
}
// Ensure the ID for the author exists // Ensure the ID for the author exists
if (mediaMetadata.authors[i].id && !(await Database.checkAuthorExists(libraryId, mediaMetadata.authors[i].id))) { 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`) Logger.warn(`[ApiRouter] Author id "${mediaMetadata.authors[i].id}" does not exist`)
mediaMetadata.authors[i].id = null 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) let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryId)
if (!author) { if (!author) {
author = new Author() author = new Author()
@ -590,13 +594,17 @@ class ApiRouter {
continue continue
} }
if (mediaMetadata.series[i].id?.startsWith('new')) {
mediaMetadata.series[i].id = null
}
// Ensure the ID for the series exists // Ensure the ID for the series exists
if (mediaMetadata.series[i].id && !(await Database.checkSeriesExists(libraryId, mediaMetadata.series[i].id))) { 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`) Logger.warn(`[ApiRouter] Series id "${mediaMetadata.series[i].id}" does not exist`)
mediaMetadata.series[i].id = null 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) let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesName, libraryId)
if (!seriesItem) { if (!seriesItem) {
seriesItem = new Series() seriesItem = new Series()

View File

@ -136,7 +136,7 @@ class BookScanner {
} }
// Check if cover was removed // 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 media.coverPath = null
hasMediaChanges = true hasMediaChanges = true
} }
@ -160,6 +160,7 @@ class BookScanner {
// Prefer to use an epub ebook then fallback to the first ebook found // 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') let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub')
if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0] if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0]
ebookLibraryFile = ebookLibraryFile.toJSON()
// Ebook file is the same as library file except for additional `ebookFormat` // Ebook file is the same as library file except for additional `ebookFormat`
ebookLibraryFile.ebookFormat = ebookLibraryFile.metadata.ext.slice(1).toLowerCase() ebookLibraryFile.ebookFormat = ebookLibraryFile.metadata.ext.slice(1).toLowerCase()
media.ebookFile = ebookLibraryFile media.ebookFile = ebookLibraryFile
@ -313,7 +314,7 @@ class BookScanner {
// If no cover then extract cover from audio file if available OR search for cover if enabled in server settings // If no cover then extract cover from audio file if available OR search for cover if enabled in server settings
if (!media.coverPath) { if (!media.coverPath) {
const libraryItemDir = existingLibraryItem.isFile ? null : existingLibraryItem.path 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) { if (extractedCoverPath) {
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`) libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
media.coverPath = extractedCoverPath media.coverPath = extractedCoverPath
@ -386,6 +387,7 @@ class BookScanner {
} }
if (ebookLibraryFile) { if (ebookLibraryFile) {
ebookLibraryFile = ebookLibraryFile.toJSON()
ebookLibraryFile.ebookFormat = ebookLibraryFile.metadata.ext.slice(1).toLowerCase() ebookLibraryFile.ebookFormat = ebookLibraryFile.metadata.ext.slice(1).toLowerCase()
} }
@ -461,7 +463,7 @@ class BookScanner {
if (!bookObject.coverPath) { if (!bookObject.coverPath) {
const libraryItemDir = libraryItemObj.isFile ? null : libraryItemObj.path const libraryItemDir = libraryItemObj.isFile ? null : libraryItemObj.path
// Extract and save embedded cover art // 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) { if (extractedCoverPath) {
bookObject.coverPath = extractedCoverPath bookObject.coverPath = extractedCoverPath
} else if (Database.serverSettings.scannerFindCovers) { } else if (Database.serverSettings.scannerFindCovers) {

View File

@ -102,7 +102,7 @@ class LibraryItemScanData {
return this.libraryFiles.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) return this.libraryFiles.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
} }
/** @type {LibraryItem.LibraryFileObject[]} */ /** @type {import('../objects/files/LibraryFile')[]} */
get ebookLibraryFiles() { get ebookLibraryFiles() {
return this.libraryFiles.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) return this.libraryFiles.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
} }

View File

@ -19,6 +19,7 @@ class LibraryScan {
this.startedAt = null this.startedAt = null
this.finishedAt = null this.finishedAt = null
this.elapsed = null this.elapsed = null
this.error = null
this.resultsMissing = 0 this.resultsMissing = 0
this.resultsAdded = 0 this.resultsAdded = 0
@ -52,6 +53,7 @@ class LibraryScan {
id: this.libraryId, id: this.libraryId,
type: this.type, type: this.type,
name: this.libraryName, name: this.libraryName,
error: this.error,
results: { results: {
added: this.resultsAdded, added: this.resultsAdded,
updated: this.resultsUpdated, updated: this.resultsUpdated,
@ -74,6 +76,7 @@ class LibraryScan {
startedAt: this.startedAt, startedAt: this.startedAt,
finishedAt: this.finishedAt, finishedAt: this.finishedAt,
elapsed: this.elapsed, elapsed: this.elapsed,
error: this.error,
resultsAdded: this.resultsAdded, resultsAdded: this.resultsAdded,
resultsUpdated: this.resultsUpdated, resultsUpdated: this.resultsUpdated,
resultsMissing: this.resultsMissing resultsMissing: this.resultsMissing
@ -88,9 +91,14 @@ class LibraryScan {
this.startedAt = Date.now() this.startedAt = Date.now()
} }
setComplete() { /**
*
* @param {string} error
*/
setComplete(error = null) {
this.finishedAt = Date.now() this.finishedAt = Date.now()
this.elapsed = this.finishedAt - this.startedAt this.elapsed = this.finishedAt - this.startedAt
this.error = error
} }
getLogLevelString(level) { getLogLevelString(level) {

View File

@ -178,7 +178,7 @@ class PodcastScanner {
// If no cover then extract cover from audio file if available // If no cover then extract cover from audio file if available
if (!media.coverPath && existingPodcastEpisodes.length) { if (!media.coverPath && existingPodcastEpisodes.length) {
const audioFiles = existingPodcastEpisodes.map(ep => ep.audioFile) 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) { if (extractedCoverPath) {
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`) libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
media.coverPath = 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 cover was not found in folder then check embedded covers in audio files
if (!podcastObject.coverPath && scannedAudioFiles.length) { if (!podcastObject.coverPath && scannedAudioFiles.length) {
// Extract and save embedded cover art // 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 libraryItemObj.podcast = podcastObject

View File

@ -328,7 +328,7 @@ class Scanner {
let offset = 0 let offset = 0
const libraryScan = new LibraryScan() const libraryScan = new LibraryScan()
libraryScan.setData(library, null, 'match') libraryScan.setData(library, 'match')
LibraryScanner.librariesScanning.push(libraryScan.getScanEmitData) LibraryScanner.librariesScanning.push(libraryScan.getScanEmitData)
SocketAuthority.emitter('scan_start', libraryScan.getScanEmitData) SocketAuthority.emitter('scan_start', libraryScan.getScanEmitData)
@ -338,10 +338,9 @@ class Scanner {
while (hasMoreChunks) { while (hasMoreChunks) {
const libraryItems = await Database.libraryItemModel.getLibraryItemsIncrement(offset, limit, { libraryId: library.id }) const libraryItems = await Database.libraryItemModel.getLibraryItemsIncrement(offset, limit, { libraryId: library.id })
if (!libraryItems.length) { if (!libraryItems.length) {
Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) break
SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData)
return
} }
offset += limit offset += limit
hasMoreChunks = libraryItems.length < limit hasMoreChunks = libraryItems.length < limit
let oldLibraryItems = libraryItems.map(li => Database.libraryItemModel.getOldLibraryItem(li)) 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] delete LibraryScanner.cancelLibraryScan[libraryScan.libraryId]
LibraryScanner.librariesScanning = LibraryScanner.librariesScanning.filter(ls => ls.id !== library.id) LibraryScanner.librariesScanning = LibraryScanner.librariesScanning.filter(ls => ls.id !== library.id)
SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData) SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData)

View File

@ -293,5 +293,6 @@ module.exports.removeFile = (path) => {
} }
module.exports.encodeUriPath = (path) => { module.exports.encodeUriPath = (path) => {
return filePathToPOSIX(path).replace(/%/g, '%25').replace(/#/g, '%23') const uri = new URL(path, "file://")
return uri.pathname
} }

View File

@ -70,8 +70,8 @@ module.exports = (nameToParse, partToReturn, fixCase, stopOnError, useLongLists)
namePartWords[j].slice(3).toLowerCase(); namePartWords[j].slice(3).toLowerCase();
} else if ( } else if (
namePartLabels[j] === 'suffix' && namePartLabels[j] === 'suffix' &&
nameParts[j].slice(-1) !== '.' && namePartWords[j].slice(-1) !== '.' &&
!suffixList.indexOf(nameParts[j].toLowerCase()) !suffixList.indexOf(namePartWords[j].toLowerCase())
) { // Convert suffix abbreviations to UPPER CASE ) { // Convert suffix abbreviations to UPPER CASE
if (namePartWords[j] === namePartWords[j].toLowerCase()) { if (namePartWords[j] === namePartWords[j].toLowerCase()) {
namePartWords[j] = namePartWords[j].toUpperCase(); namePartWords[j] = namePartWords[j].toUpperCase();