mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master' into auth_passportjs
This commit is contained in:
		
						commit
						51b0750a3f
					
				@ -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)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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": {
 | 
				
			||||||
 | 
				
			|||||||
@ -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`)
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -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`)
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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() || ''))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user