mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-15 22:01:37 +00:00
Merge branch 'rich-text-descriptions' of https://github.com/MxMarx/audiobookshelf into rich-text-descriptions
This commit is contained in:
commit
089e4b47db
22 changed files with 165 additions and 104 deletions
|
|
@ -92,7 +92,7 @@ class Logger {
|
|||
* @param {...any} args
|
||||
*/
|
||||
dev(...args) {
|
||||
if (!this.isDev) return
|
||||
if (!this.isDev || process.env.HIDE_DEV_LOGS === '1') return
|
||||
console.log(`[${this.timestamp}] DEV:`, ...args)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ class FolderWatcher extends EventEmitter {
|
|||
this.ignoreDirs = []
|
||||
/** @type {string[]} */
|
||||
this.pendingDirsToRemoveFromIgnore = []
|
||||
/** @type {NodeJS.Timeout} */
|
||||
this.removeFromIgnoreTimer = null
|
||||
|
||||
this.disabled = false
|
||||
}
|
||||
|
|
@ -240,9 +242,12 @@ class FolderWatcher extends EventEmitter {
|
|||
*/
|
||||
addIgnoreDir(path) {
|
||||
path = this.cleanDirPath(path)
|
||||
if (this.ignoreDirs.includes(path)) return
|
||||
this.pendingDirsToRemoveFromIgnore = this.pendingDirsToRemoveFromIgnore.filter(p => p !== path)
|
||||
Logger.debug(`[Watcher] Ignoring directory "${path}"`)
|
||||
if (this.ignoreDirs.includes(path)) {
|
||||
// Already ignoring dir
|
||||
return
|
||||
}
|
||||
Logger.debug(`[Watcher] addIgnoreDir: Ignoring directory "${path}"`)
|
||||
this.ignoreDirs.push(path)
|
||||
}
|
||||
|
||||
|
|
@ -255,18 +260,24 @@ class FolderWatcher extends EventEmitter {
|
|||
*/
|
||||
removeIgnoreDir(path) {
|
||||
path = this.cleanDirPath(path)
|
||||
if (!this.ignoreDirs.includes(path) || this.pendingDirsToRemoveFromIgnore.includes(path)) return
|
||||
if (!this.ignoreDirs.includes(path)) {
|
||||
Logger.debug(`[Watcher] removeIgnoreDir: Path is not being ignored "${path}"`)
|
||||
return
|
||||
}
|
||||
|
||||
// Add a 5 second delay before removing the ignore from this dir
|
||||
this.pendingDirsToRemoveFromIgnore.push(path)
|
||||
setTimeout(() => {
|
||||
if (!this.pendingDirsToRemoveFromIgnore.includes(path)) {
|
||||
this.pendingDirsToRemoveFromIgnore.push(path)
|
||||
}
|
||||
|
||||
clearTimeout(this.removeFromIgnoreTimer)
|
||||
this.removeFromIgnoreTimer = setTimeout(() => {
|
||||
if (this.pendingDirsToRemoveFromIgnore.includes(path)) {
|
||||
this.pendingDirsToRemoveFromIgnore = this.pendingDirsToRemoveFromIgnore.filter(p => p !== path)
|
||||
Logger.debug(`[Watcher] No longer ignoring directory "${path}"`)
|
||||
Logger.debug(`[Watcher] removeIgnoreDir: No longer ignoring directory "${path}"`)
|
||||
this.ignoreDirs = this.ignoreDirs.filter(p => p !== path)
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
}
|
||||
}
|
||||
module.exports = FolderWatcher
|
||||
|
|
@ -259,7 +259,6 @@ class LibraryItemController {
|
|||
|
||||
// Check if library item media has a cover path
|
||||
if (!libraryItem.media.coverPath || !await fs.pathExists(libraryItem.media.coverPath)) {
|
||||
Logger.debug(`[LibraryItemController] getCover: Library item "${req.params.id}" has no cover path`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
|
|
@ -280,12 +279,6 @@ class LibraryItemController {
|
|||
return CacheManager.handleCoverCache(res, libraryItem.id, libraryItem.media.coverPath, options)
|
||||
}
|
||||
|
||||
// GET: api/items/:id/stream
|
||||
openStream(req, res) {
|
||||
// this.streamManager.openStreamApiRequest(res, req.user, req.libraryItem)
|
||||
res.sendStatus(500)
|
||||
}
|
||||
|
||||
// POST: api/items/:id/play
|
||||
startPlaybackSession(req, res) {
|
||||
if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') {
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ class MeController {
|
|||
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(localProgress.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress)
|
||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item with id "${localProgress.libraryItemId}"`, localProgress)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class PodcastController {
|
|||
|
||||
res.json(libraryItem.toJSONExpanded())
|
||||
|
||||
if (payload.episodesToDownload && payload.episodesToDownload.length) {
|
||||
if (payload.episodesToDownload?.length) {
|
||||
Logger.info(`[PodcastController] Podcast created now starting ${payload.episodesToDownload.length} episode downloads`)
|
||||
this.podcastManager.downloadPodcastEpisodes(libraryItem, payload.episodesToDownload)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SearchController {
|
|||
|
||||
let results = null
|
||||
if (podcast) results = await PodcastFinder.findCovers(query.title)
|
||||
else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || null)
|
||||
else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || '')
|
||||
res.json({
|
||||
results
|
||||
})
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ class BookFinder {
|
|||
if (!books.length && maxFuzzySearches > 0) {
|
||||
// normalize title and author
|
||||
title = title.trim().toLowerCase()
|
||||
author = author.trim().toLowerCase()
|
||||
author = author?.trim().toLowerCase() || ''
|
||||
|
||||
// Now run up to maxFuzzySearches fuzzy searches
|
||||
let candidates = new Set()
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class BackupManager {
|
|||
let entries
|
||||
try {
|
||||
entries = await zip.entries()
|
||||
} catch(error){
|
||||
} catch (error) {
|
||||
// Not a valid zip file
|
||||
Logger.error('[BackupManager] Failed to read backup file - backup might not be a valid .zip file', tempPath, error)
|
||||
return res.status(400).send('Failed to read backup file - backup might not be a valid .zip file')
|
||||
|
|
@ -182,7 +182,6 @@ class BackupManager {
|
|||
data = await zip.entryData('details')
|
||||
} catch (error) {
|
||||
Logger.error(`[BackupManager] Failed to unzip backup "${fullFilePath}"`, error)
|
||||
await zip.close()
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -794,6 +794,9 @@ class LibraryItem extends Model {
|
|||
{
|
||||
fields: ['libraryId', 'mediaType']
|
||||
},
|
||||
{
|
||||
fields: ['libraryId', 'mediaId', 'mediaType']
|
||||
},
|
||||
{
|
||||
fields: ['birthtime']
|
||||
},
|
||||
|
|
|
|||
|
|
@ -168,7 +168,13 @@ class PlaybackSession {
|
|||
this.currentTime = session.currentTime || 0
|
||||
|
||||
this.startedAt = session.startedAt
|
||||
this.updatedAt = session.updatedAt || null
|
||||
this.updatedAt = session.updatedAt || session.startedAt
|
||||
|
||||
// Local playback sessions dont set this date field so set using updatedAt
|
||||
if (!this.date && session.updatedAt) {
|
||||
this.date = date.format(new Date(session.updatedAt), 'YYYY-MM-DD')
|
||||
this.dayOfWeek = date.format(new Date(session.updatedAt), 'dddd')
|
||||
}
|
||||
}
|
||||
|
||||
get mediaItemId() {
|
||||
|
|
|
|||
|
|
@ -339,9 +339,9 @@ class Stream extends EventEmitter {
|
|||
} else {
|
||||
Logger.error('Ffmpeg Err', '"' + err.message + '"')
|
||||
|
||||
// Temporary workaround for https://github.com/advplyr/audiobookshelf/issues/172
|
||||
const aacErrorMsg = 'ffmpeg exited with code 1: Could not write header for output file #0 (incorrect codec parameters ?)'
|
||||
if (audioCodec === 'copy' && this.isAACEncodable && err.message && err.message.startsWith(aacErrorMsg)) {
|
||||
// Temporary workaround for https://github.com/advplyr/audiobookshelf/issues/172 and https://github.com/advplyr/audiobookshelf/issues/2157
|
||||
const aacErrorMsg = 'ffmpeg exited with code 1:'
|
||||
if (audioCodec === 'copy' && this.isAACEncodable && err.message?.startsWith(aacErrorMsg)) {
|
||||
Logger.info(`[Stream] Re-attempting stream with AAC encode`)
|
||||
this.transcodeOptions.forceAAC = true
|
||||
this.reset(this.startTime)
|
||||
|
|
@ -435,4 +435,4 @@ class Stream extends EventEmitter {
|
|||
return newAudioTrack
|
||||
}
|
||||
}
|
||||
module.exports = Stream
|
||||
module.exports = Stream
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ class ServerSettings {
|
|||
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
|
||||
homeBookshelfView: this.homeBookshelfView,
|
||||
bookshelfView: this.bookshelfView,
|
||||
podcastEpisodeSchedule: this.podcastEpisodeSchedule,
|
||||
sortingIgnorePrefix: this.sortingIgnorePrefix,
|
||||
sortingPrefixes: [...this.sortingPrefixes],
|
||||
chromecastEnabled: this.chromecastEnabled,
|
||||
|
|
|
|||
|
|
@ -168,9 +168,7 @@ class BookScanner {
|
|||
hasMediaChanges = true
|
||||
}
|
||||
|
||||
// TODO: When metadata file is stored in /metadata/items/{libraryItemId}.[abs|json] we should load this
|
||||
// TODO: store an additional array of metadata keys that the user has changed manually so we know what not to override
|
||||
const bookMetadata = await this.getBookMetadataFromScanData(media.audioFiles, libraryItemData, libraryScan)
|
||||
const bookMetadata = await this.getBookMetadataFromScanData(media.audioFiles, libraryItemData, libraryScan, existingLibraryItem.id)
|
||||
let authorsUpdated = false
|
||||
const bookAuthorsRemoved = []
|
||||
let seriesUpdated = false
|
||||
|
|
@ -550,9 +548,10 @@ class BookScanner {
|
|||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {string} [existingLibraryItemId]
|
||||
* @returns {Promise<BookMetadataObject>}
|
||||
*/
|
||||
async getBookMetadataFromScanData(audioFiles, libraryItemData, libraryScan) {
|
||||
async getBookMetadataFromScanData(audioFiles, libraryItemData, libraryScan, existingLibraryItemId = null) {
|
||||
// First set book metadata from folder/file names
|
||||
const bookMetadata = {
|
||||
title: libraryItemData.mediaMetadata.title,
|
||||
|
|
@ -722,11 +721,31 @@ class BookScanner {
|
|||
|
||||
// If metadata.json or metadata.abs use this for metadata
|
||||
const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile || libraryItemData.metadataAbsLibraryFile
|
||||
const metadataText = metadataLibraryFile ? await readTextFile(metadataLibraryFile.metadata.path) : null
|
||||
let metadataText = metadataLibraryFile ? await readTextFile(metadataLibraryFile.metadata.path) : null
|
||||
let metadataFilePath = metadataLibraryFile?.metadata.path
|
||||
let metadataFileFormat = libraryItemData.metadataJsonLibraryFile ? 'json' : 'abs'
|
||||
|
||||
// When metadata file is not stored with library item then check in the /metadata/items folder for it
|
||||
if (!metadataText && existingLibraryItemId) {
|
||||
let metadataPath = Path.join(global.MetadataPath, 'items', existingLibraryItemId)
|
||||
|
||||
let altFormat = global.ServerSettings.metadataFileFormat === 'json' ? 'abs' : 'json'
|
||||
// First check the metadata format set in server settings, fallback to the alternate
|
||||
metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
|
||||
metadataFileFormat = global.ServerSettings.metadataFileFormat
|
||||
if (await fsExtra.pathExists(metadataFilePath)) {
|
||||
metadataText = await readTextFile(metadataFilePath)
|
||||
} else if (await fsExtra.pathExists(Path.join(metadataPath, `metadata.${altFormat}`))) {
|
||||
metadataFilePath = Path.join(metadataPath, `metadata.${altFormat}`)
|
||||
metadataFileFormat = altFormat
|
||||
metadataText = await readTextFile(metadataFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
if (metadataText) {
|
||||
libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataLibraryFile.metadata.path}" - preferring`)
|
||||
libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}" - preferring`)
|
||||
let abMetadata = null
|
||||
if (!!libraryItemData.metadataJsonLibraryFile) {
|
||||
if (metadataFileFormat === 'json') {
|
||||
abMetadata = abmetadataGenerator.parseJson(metadataText)
|
||||
} else {
|
||||
abMetadata = abmetadataGenerator.parse(metadataText, 'book')
|
||||
|
|
@ -1092,7 +1111,7 @@ class BookScanner {
|
|||
const result = await CoverManager.downloadCoverFromUrlNew(results[i], libraryItemId, libraryItemPath)
|
||||
|
||||
if (result.error) {
|
||||
Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)
|
||||
libraryScan.addLog(LogLevel.ERROR, `Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)
|
||||
} else if (result.cover) {
|
||||
return result.cover
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,8 +145,7 @@ class PodcastScanner {
|
|||
hasMediaChanges = true
|
||||
}
|
||||
|
||||
// TODO: When metadata file is stored in /metadata/items/{libraryItemId}.[abs|json] we should load this
|
||||
const podcastMetadata = await this.getPodcastMetadataFromScanData(existingPodcastEpisodes, libraryItemData, libraryScan)
|
||||
const podcastMetadata = await this.getPodcastMetadataFromScanData(existingPodcastEpisodes, libraryItemData, libraryScan, existingLibraryItem.id)
|
||||
|
||||
for (const key in podcastMetadata) {
|
||||
// Ignore unset metadata and empty arrays
|
||||
|
|
@ -312,9 +311,10 @@ class PodcastScanner {
|
|||
* @param {PodcastEpisode[]} podcastEpisodes Not the models for new podcasts
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {import('./LibraryScan')} libraryScan
|
||||
* @param {string} [existingLibraryItemId]
|
||||
* @returns {Promise<PodcastMetadataObject>}
|
||||
*/
|
||||
async getPodcastMetadataFromScanData(podcastEpisodes, libraryItemData, libraryScan) {
|
||||
async getPodcastMetadataFromScanData(podcastEpisodes, libraryItemData, libraryScan, existingLibraryItemId = null) {
|
||||
const podcastMetadata = {
|
||||
title: libraryItemData.mediaMetadata.title,
|
||||
titleIgnorePrefix: getTitleIgnorePrefix(libraryItemData.mediaMetadata.title),
|
||||
|
|
@ -389,11 +389,31 @@ class PodcastScanner {
|
|||
|
||||
// If metadata.json or metadata.abs use this for metadata
|
||||
const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile || libraryItemData.metadataAbsLibraryFile
|
||||
const metadataText = metadataLibraryFile ? await readTextFile(metadataLibraryFile.metadata.path) : null
|
||||
let metadataText = metadataLibraryFile ? await readTextFile(metadataLibraryFile.metadata.path) : null
|
||||
let metadataFilePath = metadataLibraryFile?.metadata.path
|
||||
let metadataFileFormat = libraryItemData.metadataJsonLibraryFile ? 'json' : 'abs'
|
||||
|
||||
// When metadata file is not stored with library item then check in the /metadata/items folder for it
|
||||
if (!metadataText && existingLibraryItemId) {
|
||||
let metadataPath = Path.join(global.MetadataPath, 'items', existingLibraryItemId)
|
||||
|
||||
let altFormat = global.ServerSettings.metadataFileFormat === 'json' ? 'abs' : 'json'
|
||||
// First check the metadata format set in server settings, fallback to the alternate
|
||||
metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
|
||||
metadataFileFormat = global.ServerSettings.metadataFileFormat
|
||||
if (await fsExtra.pathExists(metadataFilePath)) {
|
||||
metadataText = await readTextFile(metadataFilePath)
|
||||
} else if (await fsExtra.pathExists(Path.join(metadataPath, `metadata.${altFormat}`))) {
|
||||
metadataFilePath = Path.join(metadataPath, `metadata.${altFormat}`)
|
||||
metadataFileFormat = altFormat
|
||||
metadataText = await readTextFile(metadataFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
if (metadataText) {
|
||||
libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataLibraryFile.metadata.path}" - preferring`)
|
||||
libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}" - preferring`)
|
||||
let abMetadata = null
|
||||
if (!!libraryItemData.metadataJsonLibraryFile) {
|
||||
if (metadataFileFormat === 'json') {
|
||||
abMetadata = abmetadataGenerator.parseJson(metadataText)
|
||||
} else {
|
||||
abMetadata = abmetadataGenerator.parse(metadataText, 'podcast')
|
||||
|
|
|
|||
|
|
@ -205,6 +205,15 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
]
|
||||
|
||||
// Handle library setting to hide single book series
|
||||
// TODO: Merge with existing query
|
||||
if (library.settings.hideSingleBookSeries) {
|
||||
seriesWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id)`), {
|
||||
[Sequelize.Op.gt]: 1
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle user permissions to only include series with at least 1 book
|
||||
// TODO: Simplify to a single query
|
||||
if (userPermissionBookWhere.bookWhere.length) {
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ module.exports = {
|
|||
podcastEpisodeWhere['$mediaProgresses.isFinished$'] = true
|
||||
}
|
||||
} else if (filterGroup === 'recent') {
|
||||
libraryItemWhere['createdAt'] = {
|
||||
podcastEpisodeWhere['createdAt'] = {
|
||||
[Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue