mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-06-05 01:19:34 +02:00
Merge pull request #3779 from advplyr/refactor-library-item
Refactor LibraryItem to use new model
This commit is contained in:
commit
d205c6f734
@ -24,6 +24,16 @@ const ShareManager = require('../managers/ShareManager')
|
|||||||
* @property {import('../models/User')} user
|
* @property {import('../models/User')} user
|
||||||
*
|
*
|
||||||
* @typedef {Request & RequestUserObject} RequestWithUser
|
* @typedef {Request & RequestUserObject} RequestWithUser
|
||||||
|
*
|
||||||
|
* @typedef RequestEntityObject
|
||||||
|
* @property {import('../models/LibraryItem')} libraryItem
|
||||||
|
*
|
||||||
|
* @typedef {RequestWithUser & RequestEntityObject} LibraryItemControllerRequest
|
||||||
|
*
|
||||||
|
* @typedef RequestLibraryFileObject
|
||||||
|
* @property {import('../objects/files/LibraryFile')} libraryFile
|
||||||
|
*
|
||||||
|
* @typedef {RequestWithUser & RequestEntityObject & RequestLibraryFileObject} LibraryItemControllerRequestWithFile
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class LibraryItemController {
|
class LibraryItemController {
|
||||||
@ -35,17 +45,17 @@ class LibraryItemController {
|
|||||||
* ?include=progress,rssfeed,downloads,share
|
* ?include=progress,rssfeed,downloads,share
|
||||||
* ?expanded=1
|
* ?expanded=1
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
const includeEntities = (req.query.include || '').split(',')
|
const includeEntities = (req.query.include || '').split(',')
|
||||||
if (req.query.expanded == 1) {
|
if (req.query.expanded == 1) {
|
||||||
var item = req.libraryItem.toJSONExpanded()
|
const item = req.libraryItem.toOldJSONExpanded()
|
||||||
|
|
||||||
// Include users media progress
|
// Include users media progress
|
||||||
if (includeEntities.includes('progress')) {
|
if (includeEntities.includes('progress')) {
|
||||||
var episodeId = req.query.episode || null
|
const episodeId = req.query.episode || null
|
||||||
item.userMediaProgress = req.user.getOldMediaProgress(item.id, episodeId)
|
item.userMediaProgress = req.user.getOldMediaProgress(item.id, episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,28 +78,32 @@ class LibraryItemController {
|
|||||||
|
|
||||||
return res.json(item)
|
return res.json(item)
|
||||||
}
|
}
|
||||||
res.json(req.libraryItem)
|
res.json(req.libraryItem.toOldJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* PATCH: /api/items/:id
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @deprecated
|
||||||
|
* Use the updateMedia /api/items/:id/media endpoint instead or updateCover /api/items/:id/cover
|
||||||
|
*
|
||||||
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
var libraryItem = req.libraryItem
|
|
||||||
// Item has cover and update is removing cover so purge it from cache
|
// Item has cover and update is removing cover so purge it from cache
|
||||||
if (libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) {
|
if (req.libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) {
|
||||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
await CacheManager.purgeCoverCache(req.libraryItem.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUpdates = libraryItem.update(req.body)
|
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(req.libraryItem)
|
||||||
|
const hasUpdates = oldLibraryItem.update(req.body)
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
Logger.debug(`[LibraryItemController] Updated now saving`)
|
Logger.debug(`[LibraryItemController] Updated now saving`)
|
||||||
await Database.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(oldLibraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
res.json(libraryItem.toJSON())
|
res.json(oldLibraryItem.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,7 +114,7 @@ class LibraryItemController {
|
|||||||
*
|
*
|
||||||
* @this {import('../routers/ApiRouter')}
|
* @this {import('../routers/ApiRouter')}
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
@ -111,14 +125,14 @@ class LibraryItemController {
|
|||||||
const authorIds = []
|
const authorIds = []
|
||||||
const seriesIds = []
|
const seriesIds = []
|
||||||
if (req.libraryItem.isPodcast) {
|
if (req.libraryItem.isPodcast) {
|
||||||
mediaItemIds.push(...req.libraryItem.media.episodes.map((ep) => ep.id))
|
mediaItemIds.push(...req.libraryItem.media.podcastEpisodes.map((ep) => ep.id))
|
||||||
} else {
|
} else {
|
||||||
mediaItemIds.push(req.libraryItem.media.id)
|
mediaItemIds.push(req.libraryItem.media.id)
|
||||||
if (req.libraryItem.media.metadata.authors?.length) {
|
if (req.libraryItem.media.authors?.length) {
|
||||||
authorIds.push(...req.libraryItem.media.metadata.authors.map((au) => au.id))
|
authorIds.push(...req.libraryItem.media.authors.map((au) => au.id))
|
||||||
}
|
}
|
||||||
if (req.libraryItem.media.metadata.series?.length) {
|
if (req.libraryItem.media.series?.length) {
|
||||||
seriesIds.push(...req.libraryItem.media.metadata.series.map((se) => se.id))
|
seriesIds.push(...req.libraryItem.media.series.map((se) => se.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +169,7 @@ class LibraryItemController {
|
|||||||
* GET: /api/items/:id/download
|
* GET: /api/items/:id/download
|
||||||
* Download library item. Zip file if multiple files.
|
* Download library item. Zip file if multiple files.
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async download(req, res) {
|
async download(req, res) {
|
||||||
@ -164,7 +178,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
const libraryItemPath = req.libraryItem.path
|
const libraryItemPath = req.libraryItem.path
|
||||||
const itemTitle = req.libraryItem.media.metadata.title
|
const itemTitle = req.libraryItem.media.title
|
||||||
|
|
||||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
||||||
|
|
||||||
@ -194,11 +208,10 @@ class LibraryItemController {
|
|||||||
*
|
*
|
||||||
* @this {import('../routers/ApiRouter')}
|
* @this {import('../routers/ApiRouter')}
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async updateMedia(req, res) {
|
async updateMedia(req, res) {
|
||||||
const libraryItem = req.libraryItem
|
|
||||||
const mediaPayload = req.body
|
const mediaPayload = req.body
|
||||||
|
|
||||||
if (mediaPayload.url) {
|
if (mediaPayload.url) {
|
||||||
@ -207,44 +220,41 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Book specific
|
// Book specific
|
||||||
if (libraryItem.isBook) {
|
if (req.libraryItem.isBook) {
|
||||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
|
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, req.libraryItem.libraryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Podcast specific
|
// Podcast specific
|
||||||
let isPodcastAutoDownloadUpdated = false
|
let isPodcastAutoDownloadUpdated = false
|
||||||
if (libraryItem.isPodcast) {
|
if (req.libraryItem.isPodcast) {
|
||||||
if (mediaPayload.autoDownloadEpisodes !== undefined && libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) {
|
if (mediaPayload.autoDownloadEpisodes !== undefined && req.libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) {
|
||||||
isPodcastAutoDownloadUpdated = true
|
isPodcastAutoDownloadUpdated = true
|
||||||
} else if (mediaPayload.autoDownloadSchedule !== undefined && libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) {
|
} else if (mediaPayload.autoDownloadSchedule !== undefined && req.libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) {
|
||||||
isPodcastAutoDownloadUpdated = true
|
isPodcastAutoDownloadUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Book specific - Get all series being removed from this item
|
// Book specific - Get all series being removed from this item
|
||||||
let seriesRemoved = []
|
let seriesRemoved = []
|
||||||
if (libraryItem.isBook && mediaPayload.metadata?.series) {
|
if (req.libraryItem.isBook && mediaPayload.metadata?.series) {
|
||||||
const seriesIdsInUpdate = mediaPayload.metadata.series?.map((se) => se.id) || []
|
const seriesIdsInUpdate = mediaPayload.metadata.series?.map((se) => se.id) || []
|
||||||
seriesRemoved = libraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
|
seriesRemoved = req.libraryItem.media.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
let authorsRemoved = []
|
let authorsRemoved = []
|
||||||
if (libraryItem.isBook && mediaPayload.metadata?.authors) {
|
if (req.libraryItem.isBook && mediaPayload.metadata?.authors) {
|
||||||
const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id)
|
const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id)
|
||||||
authorsRemoved = libraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id))
|
authorsRemoved = req.libraryItem.media.authors.filter((au) => !authorIdsInUpdate.includes(au.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url
|
const hasUpdates = (await req.libraryItem.media.updateFromRequest(mediaPayload)) || mediaPayload.url
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
libraryItem.updatedAt = Date.now()
|
|
||||||
|
|
||||||
if (isPodcastAutoDownloadUpdated) {
|
if (isPodcastAutoDownloadUpdated) {
|
||||||
this.cronManager.checkUpdatePodcastCron(libraryItem)
|
this.cronManager.checkUpdatePodcastCron(req.libraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
Logger.debug(`[LibraryItemController] Updated library item media ${req.libraryItem.media.title}`)
|
||||||
await Database.updateLibraryItem(libraryItem)
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
|
||||||
|
|
||||||
if (authorsRemoved.length) {
|
if (authorsRemoved.length) {
|
||||||
// Check remove empty authors
|
// Check remove empty authors
|
||||||
@ -259,14 +269,14 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
updated: hasUpdates,
|
updated: hasUpdates,
|
||||||
libraryItem
|
libraryItem: req.libraryItem.toOldJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST: /api/items/:id/cover
|
* POST: /api/items/:id/cover
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {boolean} [updateAndReturnJson=true]
|
* @param {boolean} [updateAndReturnJson=true]
|
||||||
*/
|
*/
|
||||||
@ -276,15 +286,13 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
let libraryItem = req.libraryItem
|
|
||||||
|
|
||||||
let result = null
|
let result = null
|
||||||
if (req.body?.url) {
|
if (req.body?.url) {
|
||||||
Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`)
|
Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`)
|
||||||
result = await CoverManager.downloadCoverFromUrl(libraryItem, req.body.url)
|
result = await CoverManager.downloadCoverFromUrlNew(req.body.url, req.libraryItem.id, req.libraryItem.isFile ? null : req.libraryItem.path)
|
||||||
} else if (req.files?.cover) {
|
} else if (req.files?.cover) {
|
||||||
Logger.debug(`[LibraryItemController] Handling uploaded cover`)
|
Logger.debug(`[LibraryItemController] Handling uploaded cover`)
|
||||||
result = await CoverManager.uploadCover(libraryItem, req.files.cover)
|
result = await CoverManager.uploadCover(req.libraryItem, req.files.cover)
|
||||||
} else {
|
} else {
|
||||||
return res.status(400).send('Invalid request no file or url')
|
return res.status(400).send('Invalid request no file or url')
|
||||||
}
|
}
|
||||||
@ -296,8 +304,15 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateAndReturnJson) {
|
if (updateAndReturnJson) {
|
||||||
await Database.updateLibraryItem(libraryItem)
|
req.libraryItem.media.coverPath = result.cover
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
req.libraryItem.media.changed('coverPath', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
// client uses updatedAt timestamp in URL to force refresh cover
|
||||||
|
req.libraryItem.changed('updatedAt', true)
|
||||||
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
cover: result.cover
|
cover: result.cover
|
||||||
@ -308,22 +323,28 @@ class LibraryItemController {
|
|||||||
/**
|
/**
|
||||||
* PATCH: /api/items/:id/cover
|
* PATCH: /api/items/:id/cover
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async updateCover(req, res) {
|
async updateCover(req, res) {
|
||||||
const libraryItem = req.libraryItem
|
|
||||||
if (!req.body.cover) {
|
if (!req.body.cover) {
|
||||||
return res.status(400).send('Invalid request no cover path')
|
return res.status(400).send('Invalid request no cover path')
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationResult = await CoverManager.validateCoverPath(req.body.cover, libraryItem)
|
const validationResult = await CoverManager.validateCoverPath(req.body.cover, req.libraryItem)
|
||||||
if (validationResult.error) {
|
if (validationResult.error) {
|
||||||
return res.status(500).send(validationResult.error)
|
return res.status(500).send(validationResult.error)
|
||||||
}
|
}
|
||||||
if (validationResult.updated) {
|
if (validationResult.updated) {
|
||||||
await Database.updateLibraryItem(libraryItem)
|
req.libraryItem.media.coverPath = validationResult.cover
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
req.libraryItem.media.changed('coverPath', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
// client uses updatedAt timestamp in URL to force refresh cover
|
||||||
|
req.libraryItem.changed('updatedAt', true)
|
||||||
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -334,17 +355,22 @@ class LibraryItemController {
|
|||||||
/**
|
/**
|
||||||
* DELETE: /api/items/:id/cover
|
* DELETE: /api/items/:id/cover
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async removeCover(req, res) {
|
async removeCover(req, res) {
|
||||||
var libraryItem = req.libraryItem
|
if (req.libraryItem.media.coverPath) {
|
||||||
|
req.libraryItem.media.coverPath = null
|
||||||
|
req.libraryItem.media.changed('coverPath', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
if (libraryItem.media.coverPath) {
|
// client uses updatedAt timestamp in URL to force refresh cover
|
||||||
libraryItem.updateMediaCover('')
|
req.libraryItem.changed('updatedAt', true)
|
||||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
await req.libraryItem.save()
|
||||||
await Database.updateLibraryItem(libraryItem)
|
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
await CacheManager.purgeCoverCache(req.libraryItem.id)
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
@ -353,7 +379,7 @@ class LibraryItemController {
|
|||||||
/**
|
/**
|
||||||
* GET: /api/items/:id/cover
|
* GET: /api/items/:id/cover
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async getCover(req, res) {
|
async getCover(req, res) {
|
||||||
@ -395,11 +421,11 @@ class LibraryItemController {
|
|||||||
*
|
*
|
||||||
* @this {import('../routers/ApiRouter')}
|
* @this {import('../routers/ApiRouter')}
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
startPlaybackSession(req, res) {
|
startPlaybackSession(req, res) {
|
||||||
if (!req.libraryItem.media.numTracks) {
|
if (!req.libraryItem.hasAudioTracks) {
|
||||||
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
|
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
@ -412,18 +438,18 @@ class LibraryItemController {
|
|||||||
*
|
*
|
||||||
* @this {import('../routers/ApiRouter')}
|
* @this {import('../routers/ApiRouter')}
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
startEpisodePlaybackSession(req, res) {
|
startEpisodePlaybackSession(req, res) {
|
||||||
var libraryItem = req.libraryItem
|
if (!req.libraryItem.isPodcast) {
|
||||||
if (!libraryItem.media.numTracks) {
|
Logger.error(`[LibraryItemController] startEpisodePlaybackSession invalid media type ${req.libraryItem.id}`)
|
||||||
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${libraryItem.id}`)
|
return res.sendStatus(400)
|
||||||
return res.sendStatus(404)
|
|
||||||
}
|
}
|
||||||
var episodeId = req.params.episodeId
|
|
||||||
if (!libraryItem.media.episodes.find((ep) => ep.id === episodeId)) {
|
const episodeId = req.params.episodeId
|
||||||
Logger.error(`[LibraryItemController] startPlaybackSession episode ${episodeId} not found for item ${libraryItem.id}`)
|
if (!req.libraryItem.media.podcastEpisodes.some((ep) => ep.id === episodeId)) {
|
||||||
|
Logger.error(`[LibraryItemController] startPlaybackSession episode ${episodeId} not found for item ${req.libraryItem.id}`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,30 +459,55 @@ class LibraryItemController {
|
|||||||
/**
|
/**
|
||||||
* PATCH: /api/items/:id/tracks
|
* PATCH: /api/items/:id/tracks
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async updateTracks(req, res) {
|
async updateTracks(req, res) {
|
||||||
var libraryItem = req.libraryItem
|
const orderedFileData = req.body?.orderedFileData
|
||||||
var orderedFileData = req.body.orderedFileData
|
|
||||||
if (!libraryItem.media.updateAudioTracks) {
|
if (!req.libraryItem.isBook) {
|
||||||
Logger.error(`[LibraryItemController] updateTracks invalid media type ${libraryItem.id}`)
|
Logger.error(`[LibraryItemController] updateTracks invalid media type ${req.libraryItem.id}`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
libraryItem.media.updateAudioTracks(orderedFileData)
|
if (!Array.isArray(orderedFileData) || !orderedFileData.length) {
|
||||||
await Database.updateLibraryItem(libraryItem)
|
Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
return res.sendStatus(400)
|
||||||
res.json(libraryItem.toJSON())
|
}
|
||||||
|
// Ensure that each orderedFileData has a valid ino and is in the book audioFiles
|
||||||
|
if (orderedFileData.some((fileData) => !fileData?.ino || !req.libraryItem.media.audioFiles.some((af) => af.ino === fileData.ino))) {
|
||||||
|
Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 1
|
||||||
|
const updatedAudioFiles = orderedFileData.map((fileData) => {
|
||||||
|
const audioFile = req.libraryItem.media.audioFiles.find((af) => af.ino === fileData.ino)
|
||||||
|
audioFile.manuallyVerified = true
|
||||||
|
audioFile.exclude = !!fileData.exclude
|
||||||
|
if (audioFile.exclude) {
|
||||||
|
audioFile.index = -1
|
||||||
|
} else {
|
||||||
|
audioFile.index = index++
|
||||||
|
}
|
||||||
|
return audioFile
|
||||||
|
})
|
||||||
|
updatedAudioFiles.sort((a, b) => a.index - b.index)
|
||||||
|
|
||||||
|
req.libraryItem.media.audioFiles = updatedAudioFiles
|
||||||
|
req.libraryItem.media.changed('audioFiles', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
|
res.json(req.libraryItem.toOldJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/items/:id/match
|
* POST /api/items/:id/match
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async match(req, res) {
|
async match(req, res) {
|
||||||
const libraryItem = req.libraryItem
|
|
||||||
const reqBody = req.body || {}
|
const reqBody = req.body || {}
|
||||||
|
|
||||||
const options = {}
|
const options = {}
|
||||||
@ -473,7 +524,8 @@ class LibraryItemController {
|
|||||||
options.overrideDetails = !!reqBody.overrideDetails
|
options.overrideDetails = !!reqBody.overrideDetails
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, options)
|
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(req.libraryItem)
|
||||||
|
var matchResult = await Scanner.quickMatchLibraryItem(this, oldLibraryItem, options)
|
||||||
res.json(matchResult)
|
res.json(matchResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -741,7 +793,7 @@ class LibraryItemController {
|
|||||||
/**
|
/**
|
||||||
* POST: /api/items/:id/scan
|
* POST: /api/items/:id/scan
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async scan(req, res) {
|
async scan(req, res) {
|
||||||
@ -765,7 +817,7 @@ class LibraryItemController {
|
|||||||
/**
|
/**
|
||||||
* GET: /api/items/:id/metadata-object
|
* GET: /api/items/:id/metadata-object
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
getMetadataObject(req, res) {
|
getMetadataObject(req, res) {
|
||||||
@ -774,7 +826,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
|
if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.includedAudioFiles.length) {
|
||||||
Logger.error(`[LibraryItemController] Invalid library item`)
|
Logger.error(`[LibraryItemController] Invalid library item`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
@ -785,7 +837,7 @@ class LibraryItemController {
|
|||||||
/**
|
/**
|
||||||
* POST: /api/items/:id/chapters
|
* POST: /api/items/:id/chapters
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async updateMediaChapters(req, res) {
|
async updateMediaChapters(req, res) {
|
||||||
@ -794,26 +846,51 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
|
if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.hasAudioTracks) {
|
||||||
Logger.error(`[LibraryItemController] Invalid library item`)
|
Logger.error(`[LibraryItemController] Invalid library item`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.body.chapters) {
|
if (!Array.isArray(req.body.chapters) || req.body.chapters.some((c) => !c.title || typeof c.title !== 'string' || c.start === undefined || typeof c.start !== 'number' || c.end === undefined || typeof c.end !== 'number')) {
|
||||||
Logger.error(`[LibraryItemController] Invalid payload`)
|
Logger.error(`[LibraryItemController] Invalid payload`)
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const chapters = req.body.chapters || []
|
const chapters = req.body.chapters || []
|
||||||
const wasUpdated = req.libraryItem.media.updateChapters(chapters)
|
|
||||||
if (wasUpdated) {
|
let hasUpdates = false
|
||||||
await Database.updateLibraryItem(req.libraryItem)
|
if (chapters.length !== req.libraryItem.media.chapters.length) {
|
||||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
req.libraryItem.media.chapters = chapters.map((c, index) => {
|
||||||
|
return {
|
||||||
|
id: index,
|
||||||
|
title: c.title,
|
||||||
|
start: c.start,
|
||||||
|
end: c.end
|
||||||
|
}
|
||||||
|
})
|
||||||
|
hasUpdates = true
|
||||||
|
} else {
|
||||||
|
for (const [index, chapter] of chapters.entries()) {
|
||||||
|
const currentChapter = req.libraryItem.media.chapters[index]
|
||||||
|
if (currentChapter.title !== chapter.title || currentChapter.start !== chapter.start || currentChapter.end !== chapter.end) {
|
||||||
|
currentChapter.title = chapter.title
|
||||||
|
currentChapter.start = chapter.start
|
||||||
|
currentChapter.end = chapter.end
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUpdates) {
|
||||||
|
req.libraryItem.media.changed('chapters', true)
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
updated: wasUpdated
|
updated: hasUpdates
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -821,7 +898,7 @@ class LibraryItemController {
|
|||||||
* GET: /api/items/:id/ffprobe/:fileid
|
* GET: /api/items/:id/ffprobe/:fileid
|
||||||
* FFProbe JSON result from audio file
|
* FFProbe JSON result from audio file
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async getFFprobeData(req, res) {
|
async getFFprobeData(req, res) {
|
||||||
@ -829,25 +906,21 @@ class LibraryItemController {
|
|||||||
Logger.error(`[LibraryItemController] Non-admin user "${req.user.username}" attempted to get ffprobe data`)
|
Logger.error(`[LibraryItemController] Non-admin user "${req.user.username}" attempted to get ffprobe data`)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
if (req.libraryFile.fileType !== 'audio') {
|
|
||||||
Logger.error(`[LibraryItemController] Invalid filetype "${req.libraryFile.fileType}" for fileid "${req.params.fileid}". Expected audio file`)
|
|
||||||
return res.sendStatus(400)
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioFile = req.libraryItem.media.findFileWithInode(req.params.fileid)
|
const audioFile = req.libraryItem.getAudioFileWithIno(req.params.fileid)
|
||||||
if (!audioFile) {
|
if (!audioFile) {
|
||||||
Logger.error(`[LibraryItemController] Audio file not found with inode value ${req.params.fileid}`)
|
Logger.error(`[LibraryItemController] Audio file not found with inode value ${req.params.fileid}`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ffprobeData = await AudioFileScanner.probeAudioFile(audioFile)
|
const ffprobeData = await AudioFileScanner.probeAudioFile(audioFile.metadata.path)
|
||||||
res.json(ffprobeData)
|
res.json(ffprobeData)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET api/items/:id/file/:fileid
|
* GET api/items/:id/file/:fileid
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequestWithFile} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async getLibraryFile(req, res) {
|
async getLibraryFile(req, res) {
|
||||||
@ -870,7 +943,7 @@ class LibraryItemController {
|
|||||||
/**
|
/**
|
||||||
* DELETE api/items/:id/file/:fileid
|
* DELETE api/items/:id/file/:fileid
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequestWithFile} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async deleteLibraryFile(req, res) {
|
async deleteLibraryFile(req, res) {
|
||||||
@ -881,17 +954,35 @@ class LibraryItemController {
|
|||||||
await fs.remove(libraryFile.metadata.path).catch((error) => {
|
await fs.remove(libraryFile.metadata.path).catch((error) => {
|
||||||
Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error)
|
Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error)
|
||||||
})
|
})
|
||||||
req.libraryItem.removeLibraryFile(req.params.fileid)
|
|
||||||
|
|
||||||
if (req.libraryItem.media.removeFileWithInode(req.params.fileid)) {
|
req.libraryItem.libraryFiles = req.libraryItem.libraryFiles.filter((lf) => lf.ino !== req.params.fileid)
|
||||||
// If book has no more media files then mark it as missing
|
req.libraryItem.changed('libraryFiles', true)
|
||||||
if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaEntities) {
|
|
||||||
req.libraryItem.setMissing()
|
if (req.libraryItem.isBook) {
|
||||||
|
if (req.libraryItem.media.audioFiles.some((af) => af.ino === req.params.fileid)) {
|
||||||
|
req.libraryItem.media.audioFiles = req.libraryItem.media.audioFiles.filter((af) => af.ino !== req.params.fileid)
|
||||||
|
req.libraryItem.media.changed('audioFiles', true)
|
||||||
|
} else if (req.libraryItem.media.ebookFile?.ino === req.params.fileid) {
|
||||||
|
req.libraryItem.media.ebookFile = null
|
||||||
|
req.libraryItem.media.changed('ebookFile', true)
|
||||||
}
|
}
|
||||||
|
if (!req.libraryItem.media.hasMediaFiles) {
|
||||||
|
req.libraryItem.isMissing = true
|
||||||
|
}
|
||||||
|
} else if (req.libraryItem.media.podcastEpisodes.some((ep) => ep.audioFile.ino === req.params.fileid)) {
|
||||||
|
const episodeToRemove = req.libraryItem.media.podcastEpisodes.find((ep) => ep.audioFile.ino === req.params.fileid)
|
||||||
|
await episodeToRemove.destroy()
|
||||||
|
|
||||||
|
req.libraryItem.media.podcastEpisodes = req.libraryItem.media.podcastEpisodes.filter((ep) => ep.audioFile.ino !== req.params.fileid)
|
||||||
}
|
}
|
||||||
req.libraryItem.updatedAt = Date.now()
|
|
||||||
await Database.updateLibraryItem(req.libraryItem)
|
if (req.libraryItem.media.changed()) {
|
||||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
await req.libraryItem.media.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -899,7 +990,7 @@ class LibraryItemController {
|
|||||||
* GET api/items/:id/file/:fileid/download
|
* GET api/items/:id/file/:fileid/download
|
||||||
* Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads
|
* Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequestWithFile} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async downloadLibraryFile(req, res) {
|
async downloadLibraryFile(req, res) {
|
||||||
@ -911,7 +1002,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" file at "${libraryFile.metadata.path}"`)
|
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.title}" file at "${libraryFile.metadata.path}"`)
|
||||||
|
|
||||||
if (global.XAccel) {
|
if (global.XAccel) {
|
||||||
const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path)
|
const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path)
|
||||||
@ -947,13 +1038,13 @@ class LibraryItemController {
|
|||||||
* fileid is only required when reading a supplementary ebook
|
* fileid is only required when reading a supplementary ebook
|
||||||
* when no fileid is passed in the primary ebook will be returned
|
* when no fileid is passed in the primary ebook will be returned
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async getEBookFile(req, res) {
|
async getEBookFile(req, res) {
|
||||||
let ebookFile = null
|
let ebookFile = null
|
||||||
if (req.params.fileid) {
|
if (req.params.fileid) {
|
||||||
ebookFile = req.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
|
ebookFile = req.libraryItem.getLibraryFileWithIno(req.params.fileid)
|
||||||
if (!ebookFile?.isEBookFile) {
|
if (!ebookFile?.isEBookFile) {
|
||||||
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
||||||
return res.status(400).send('Invalid ebook file id')
|
return res.status(400).send('Invalid ebook file id')
|
||||||
@ -963,12 +1054,12 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ebookFile) {
|
if (!ebookFile) {
|
||||||
Logger.error(`[LibraryItemController] No ebookFile for library item "${req.libraryItem.media.metadata.title}"`)
|
Logger.error(`[LibraryItemController] No ebookFile for library item "${req.libraryItem.media.title}"`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
const ebookFilePath = ebookFile.metadata.path
|
const ebookFilePath = ebookFile.metadata.path
|
||||||
|
|
||||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" ebook at "${ebookFilePath}"`)
|
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.title}" ebook at "${ebookFilePath}"`)
|
||||||
|
|
||||||
if (global.XAccel) {
|
if (global.XAccel) {
|
||||||
const encodedURI = encodeUriPath(global.XAccel + ebookFilePath)
|
const encodedURI = encodeUriPath(global.XAccel + ebookFilePath)
|
||||||
@ -991,28 +1082,55 @@ class LibraryItemController {
|
|||||||
* if an ebook file is the primary ebook, then it will be changed to supplementary
|
* if an ebook file is the primary ebook, then it will be changed to supplementary
|
||||||
* if an ebook file is supplementary, then it will be changed to primary
|
* if an ebook file is supplementary, then it will be changed to primary
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {LibraryItemControllerRequestWithFile} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async updateEbookFileStatus(req, res) {
|
async updateEbookFileStatus(req, res) {
|
||||||
const ebookLibraryFile = req.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
|
if (!req.libraryItem.isBook) {
|
||||||
if (!ebookLibraryFile?.isEBookFile) {
|
Logger.error(`[LibraryItemController] Invalid media type for ebook file status update`)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
if (!req.libraryFile?.isEBookFile) {
|
||||||
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
||||||
return res.status(400).send('Invalid ebook file id')
|
return res.status(400).send('Invalid ebook file id')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ebookLibraryFile = req.libraryFile
|
||||||
|
let primaryEbookFile = null
|
||||||
|
|
||||||
|
const ebookLibraryFileInos = req.libraryItem
|
||||||
|
.getLibraryFiles()
|
||||||
|
.filter((lf) => lf.isEBookFile)
|
||||||
|
.map((lf) => lf.ino)
|
||||||
|
|
||||||
if (ebookLibraryFile.isSupplementary) {
|
if (ebookLibraryFile.isSupplementary) {
|
||||||
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to primary`)
|
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to primary`)
|
||||||
req.libraryItem.setPrimaryEbook(ebookLibraryFile)
|
|
||||||
|
primaryEbookFile = ebookLibraryFile.toJSON()
|
||||||
|
delete primaryEbookFile.isSupplementary
|
||||||
|
delete primaryEbookFile.fileType
|
||||||
|
primaryEbookFile.ebookFormat = ebookLibraryFile.metadata.format
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to supplementary`)
|
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to supplementary`)
|
||||||
ebookLibraryFile.isSupplementary = true
|
|
||||||
req.libraryItem.setPrimaryEbook(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req.libraryItem.updatedAt = Date.now()
|
req.libraryItem.media.ebookFile = primaryEbookFile
|
||||||
await Database.updateLibraryItem(req.libraryItem)
|
req.libraryItem.media.changed('ebookFile', true)
|
||||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
|
req.libraryItem.libraryFiles = req.libraryItem.libraryFiles.map((lf) => {
|
||||||
|
if (ebookLibraryFileInos.includes(lf.ino)) {
|
||||||
|
lf.isSupplementary = lf.ino !== primaryEbookFile?.ino
|
||||||
|
}
|
||||||
|
return lf
|
||||||
|
})
|
||||||
|
req.libraryItem.changed('libraryFiles', true)
|
||||||
|
|
||||||
|
req.libraryItem.isMissing = !req.libraryItem.media.hasMediaFiles
|
||||||
|
|
||||||
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1023,7 +1141,7 @@ class LibraryItemController {
|
|||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
async middleware(req, res, next) {
|
async middleware(req, res, next) {
|
||||||
req.libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
|
req.libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id)
|
||||||
if (!req.libraryItem?.media) return res.sendStatus(404)
|
if (!req.libraryItem?.media) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check user can access this library item
|
// Check user can access this library item
|
||||||
@ -1033,7 +1151,7 @@ class LibraryItemController {
|
|||||||
|
|
||||||
// For library file routes, get the library file
|
// For library file routes, get the library file
|
||||||
if (req.params.fileid) {
|
if (req.params.fileid) {
|
||||||
req.libraryFile = req.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid)
|
req.libraryFile = req.libraryItem.getLibraryFileWithIno(req.params.fileid)
|
||||||
if (!req.libraryFile) {
|
if (!req.libraryFile) {
|
||||||
Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`)
|
Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
|
@ -34,8 +34,13 @@ class AudioMetadataMangaer {
|
|||||||
return this.tasksQueued.some((t) => t.data.libraryItemId === libraryItemId) || this.tasksRunning.some((t) => t.data.libraryItemId === libraryItemId)
|
return this.tasksQueued.some((t) => t.data.libraryItemId === libraryItemId) || this.tasksRunning.some((t) => t.data.libraryItemId === libraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('../models/LibraryItem')} libraryItem
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
getMetadataObjectForApi(libraryItem) {
|
getMetadataObjectForApi(libraryItem) {
|
||||||
return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length)
|
return ffmpegHelpers.getFFMetadataObject(libraryItem.toOldJSONExpanded(), libraryItem.media.includedAudioFiles.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,6 +79,12 @@ class CoverManager {
|
|||||||
return imgType
|
return imgType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('../models/LibraryItem')} libraryItem
|
||||||
|
* @param {*} coverFile - file object from req.files
|
||||||
|
* @returns {Promise<{error:string}|{cover:string}>}
|
||||||
|
*/
|
||||||
async uploadCover(libraryItem, coverFile) {
|
async uploadCover(libraryItem, coverFile) {
|
||||||
const extname = Path.extname(coverFile.name.toLowerCase())
|
const extname = Path.extname(coverFile.name.toLowerCase())
|
||||||
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
|
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
|
||||||
@ -110,14 +116,20 @@ class CoverManager {
|
|||||||
await this.removeOldCovers(coverDirPath, extname)
|
await this.removeOldCovers(coverDirPath, extname)
|
||||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
await CacheManager.purgeCoverCache(libraryItem.id)
|
||||||
|
|
||||||
Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.metadata.title}"`)
|
Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.title}"`)
|
||||||
|
|
||||||
libraryItem.updateMediaCover(coverFullPath)
|
|
||||||
return {
|
return {
|
||||||
cover: coverFullPath
|
cover: coverFullPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} libraryItem - old library item
|
||||||
|
* @param {string} url
|
||||||
|
* @param {boolean} [forceLibraryItemFolder=false]
|
||||||
|
* @returns {Promise<{error:string}|{cover:string}>}
|
||||||
|
*/
|
||||||
async downloadCoverFromUrl(libraryItem, url, forceLibraryItemFolder = false) {
|
async downloadCoverFromUrl(libraryItem, url, forceLibraryItemFolder = false) {
|
||||||
try {
|
try {
|
||||||
// Force save cover with library item is used for adding new podcasts
|
// Force save cover with library item is used for adding new podcasts
|
||||||
@ -166,6 +178,12 @@ class CoverManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} coverPath
|
||||||
|
* @param {import('../models/LibraryItem')} libraryItem
|
||||||
|
* @returns {Promise<{error:string}|{cover:string,updated:boolean}>}
|
||||||
|
*/
|
||||||
async validateCoverPath(coverPath, libraryItem) {
|
async validateCoverPath(coverPath, libraryItem) {
|
||||||
// Invalid cover path
|
// Invalid cover path
|
||||||
if (!coverPath || coverPath.startsWith('http:') || coverPath.startsWith('https:')) {
|
if (!coverPath || coverPath.startsWith('http:') || coverPath.startsWith('https:')) {
|
||||||
@ -235,7 +253,6 @@ class CoverManager {
|
|||||||
|
|
||||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
await CacheManager.purgeCoverCache(libraryItem.id)
|
||||||
|
|
||||||
libraryItem.updateMediaCover(coverPath)
|
|
||||||
return {
|
return {
|
||||||
cover: coverPath,
|
cover: coverPath,
|
||||||
updated: true
|
updated: true
|
||||||
|
@ -215,6 +215,10 @@ class CronManager {
|
|||||||
this.podcastCrons = this.podcastCrons.filter((pc) => pc.expression !== podcastCron.expression)
|
this.podcastCrons = this.podcastCrons.filter((pc) => pc.expression !== podcastCron.expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('../models/LibraryItem')} libraryItem - this can be the old model
|
||||||
|
*/
|
||||||
checkUpdatePodcastCron(libraryItem) {
|
checkUpdatePodcastCron(libraryItem) {
|
||||||
// Remove from old cron by library item id
|
// Remove from old cron by library item id
|
||||||
const existingCron = this.podcastCrons.find((pc) => pc.libraryItemIds.includes(libraryItem.id))
|
const existingCron = this.podcastCrons.find((pc) => pc.libraryItemIds.includes(libraryItem.id))
|
||||||
@ -230,7 +234,10 @@ class CronManager {
|
|||||||
const cronMatchingExpression = this.podcastCrons.find((pc) => pc.expression === libraryItem.media.autoDownloadSchedule)
|
const cronMatchingExpression = this.podcastCrons.find((pc) => pc.expression === libraryItem.media.autoDownloadSchedule)
|
||||||
if (cronMatchingExpression) {
|
if (cronMatchingExpression) {
|
||||||
cronMatchingExpression.libraryItemIds.push(libraryItem.id)
|
cronMatchingExpression.libraryItemIds.push(libraryItem.id)
|
||||||
Logger.info(`[CronManager] Added podcast "${libraryItem.media.metadata.title}" to auto dl episode cron "${cronMatchingExpression.expression}"`)
|
|
||||||
|
// TODO: Update after old model removed
|
||||||
|
const podcastTitle = libraryItem.media.title || libraryItem.media.metadata?.title
|
||||||
|
Logger.info(`[CronManager] Added podcast "${podcastTitle}" to auto dl episode cron "${cronMatchingExpression.expression}"`)
|
||||||
} else {
|
} else {
|
||||||
this.startPodcastCron(libraryItem.media.autoDownloadSchedule, [libraryItem.id])
|
this.startPodcastCron(libraryItem.media.autoDownloadSchedule, [libraryItem.id])
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ class PlaybackSessionManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import('../controllers/SessionController').RequestWithUser} req
|
* @param {import('../controllers/LibraryItemController').LibraryItemControllerRequest} req
|
||||||
* @param {Object} [clientDeviceInfo]
|
* @param {Object} [clientDeviceInfo]
|
||||||
* @returns {Promise<DeviceInfo>}
|
* @returns {Promise<DeviceInfo>}
|
||||||
*/
|
*/
|
||||||
@ -67,14 +67,14 @@ class PlaybackSessionManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import('../controllers/SessionController').RequestWithUser} req
|
* @param {import('../controllers/LibraryItemController').LibraryItemControllerRequest} req
|
||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
* @param {string} [episodeId]
|
* @param {string} [episodeId]
|
||||||
*/
|
*/
|
||||||
async startSessionRequest(req, res, episodeId) {
|
async startSessionRequest(req, res, episodeId) {
|
||||||
const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo)
|
const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo)
|
||||||
Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`)
|
Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`)
|
||||||
const { libraryItem, body: options } = req
|
const { oldLibraryItem: libraryItem, body: options } = req
|
||||||
const session = await this.startSession(req.user, deviceInfo, libraryItem, episodeId, options)
|
const session = await this.startSession(req.user, deviceInfo, libraryItem, episodeId, options)
|
||||||
res.json(session.toJSONForClient(libraryItem))
|
res.json(session.toJSONForClient(libraryItem))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model } = require('sequelize')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
||||||
|
const parseNameString = require('../utils/parsers/parseNameString')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef EBookFileObject
|
* @typedef EBookFileObject
|
||||||
@ -113,8 +115,12 @@ class Book extends Model {
|
|||||||
/** @type {Date} */
|
/** @type {Date} */
|
||||||
this.createdAt
|
this.createdAt
|
||||||
|
|
||||||
|
// Expanded properties
|
||||||
|
|
||||||
/** @type {import('./Author')[]} - optional if expanded */
|
/** @type {import('./Author')[]} - optional if expanded */
|
||||||
this.authors
|
this.authors
|
||||||
|
/** @type {import('./Series')[]} - optional if expanded */
|
||||||
|
this.series
|
||||||
}
|
}
|
||||||
|
|
||||||
static getOldBook(libraryItemExpanded) {
|
static getOldBook(libraryItemExpanded) {
|
||||||
@ -241,32 +247,6 @@ class Book extends Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAbsMetadataJson() {
|
|
||||||
return {
|
|
||||||
tags: this.tags || [],
|
|
||||||
chapters: this.chapters?.map((c) => ({ ...c })) || [],
|
|
||||||
title: this.title,
|
|
||||||
subtitle: this.subtitle,
|
|
||||||
authors: this.authors.map((a) => a.name),
|
|
||||||
narrators: this.narrators,
|
|
||||||
series: this.series.map((se) => {
|
|
||||||
const sequence = se.bookSeries?.sequence || ''
|
|
||||||
if (!sequence) return se.name
|
|
||||||
return `${se.name} #${sequence}`
|
|
||||||
}),
|
|
||||||
genres: this.genres || [],
|
|
||||||
publishedYear: this.publishedYear,
|
|
||||||
publishedDate: this.publishedDate,
|
|
||||||
publisher: this.publisher,
|
|
||||||
description: this.description,
|
|
||||||
isbn: this.isbn,
|
|
||||||
asin: this.asin,
|
|
||||||
language: this.language,
|
|
||||||
explicit: !!this.explicit,
|
|
||||||
abridged: !!this.abridged
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize model
|
* Initialize model
|
||||||
* @param {import('../Database').sequelize} sequelize
|
* @param {import('../Database').sequelize} sequelize
|
||||||
@ -343,9 +323,50 @@ class Book extends Model {
|
|||||||
}
|
}
|
||||||
return this.authors.map((au) => au.name).join(', ')
|
return this.authors.map((au) => au.name).join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comma separated array of author names in Last, First format
|
||||||
|
* Requires authors to be loaded
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
get authorNameLF() {
|
||||||
|
if (this.authors === undefined) {
|
||||||
|
Logger.error(`[Book] authorNameLF: Cannot get authorNameLF because authors are not loaded`)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last, First
|
||||||
|
if (!this.authors.length) return ''
|
||||||
|
return this.authors.map((au) => parseNameString.nameToLastFirst(au.name)).join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comma separated array of series with sequence
|
||||||
|
* Requires series to be loaded
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
get seriesName() {
|
||||||
|
if (this.series === undefined) {
|
||||||
|
Logger.error(`[Book] seriesName: Cannot get seriesName because series are not loaded`)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.series.length) return ''
|
||||||
|
return this.series
|
||||||
|
.map((se) => {
|
||||||
|
const sequence = se.bookSeries?.sequence || ''
|
||||||
|
if (!sequence) return se.name
|
||||||
|
return `${se.name} #${sequence}`
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
get includedAudioFiles() {
|
get includedAudioFiles() {
|
||||||
return this.audioFiles.filter((af) => !af.exclude)
|
return this.audioFiles.filter((af) => !af.exclude)
|
||||||
}
|
}
|
||||||
|
|
||||||
get trackList() {
|
get trackList() {
|
||||||
let startOffset = 0
|
let startOffset = 0
|
||||||
return this.includedAudioFiles.map((af) => {
|
return this.includedAudioFiles.map((af) => {
|
||||||
@ -355,6 +376,299 @@ class Book extends Model {
|
|||||||
return track
|
return track
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasMediaFiles() {
|
||||||
|
return !!this.hasAudioTracks || !!this.ebookFile
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasAudioTracks() {
|
||||||
|
return !!this.includedAudioFiles.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total file size of all audio files and ebook file
|
||||||
|
*
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get size() {
|
||||||
|
let total = 0
|
||||||
|
this.audioFiles.forEach((af) => (total += af.metadata.size))
|
||||||
|
if (this.ebookFile) {
|
||||||
|
total += this.ebookFile.metadata.size
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
getAbsMetadataJson() {
|
||||||
|
return {
|
||||||
|
tags: this.tags || [],
|
||||||
|
chapters: this.chapters?.map((c) => ({ ...c })) || [],
|
||||||
|
title: this.title,
|
||||||
|
subtitle: this.subtitle,
|
||||||
|
authors: this.authors.map((a) => a.name),
|
||||||
|
narrators: this.narrators,
|
||||||
|
series: this.series.map((se) => {
|
||||||
|
const sequence = se.bookSeries?.sequence || ''
|
||||||
|
if (!sequence) return se.name
|
||||||
|
return `${se.name} #${sequence}`
|
||||||
|
}),
|
||||||
|
genres: this.genres || [],
|
||||||
|
publishedYear: this.publishedYear,
|
||||||
|
publishedDate: this.publishedDate,
|
||||||
|
publisher: this.publisher,
|
||||||
|
description: this.description,
|
||||||
|
isbn: this.isbn,
|
||||||
|
asin: this.asin,
|
||||||
|
language: this.language,
|
||||||
|
explicit: !!this.explicit,
|
||||||
|
abridged: !!this.abridged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} payload - old book object
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async updateFromRequest(payload) {
|
||||||
|
if (!payload) return false
|
||||||
|
|
||||||
|
let hasUpdates = false
|
||||||
|
|
||||||
|
if (payload.metadata) {
|
||||||
|
const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language']
|
||||||
|
metadataStringKeys.forEach((key) => {
|
||||||
|
if (typeof payload.metadata[key] === 'string' && this[key] !== payload.metadata[key]) {
|
||||||
|
this[key] = payload.metadata[key] || null
|
||||||
|
|
||||||
|
if (key === 'title') {
|
||||||
|
this.titleIgnorePrefix = getTitleIgnorePrefix(this.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (payload.metadata.explicit !== undefined && this.explicit !== !!payload.metadata.explicit) {
|
||||||
|
this.explicit = !!payload.metadata.explicit
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
if (payload.metadata.abridged !== undefined && this.abridged !== !!payload.metadata.abridged) {
|
||||||
|
this.abridged = !!payload.metadata.abridged
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
const arrayOfStringsKeys = ['narrators', 'genres']
|
||||||
|
arrayOfStringsKeys.forEach((key) => {
|
||||||
|
if (Array.isArray(payload.metadata[key]) && !payload.metadata[key].some((item) => typeof item !== 'string') && JSON.stringify(this[key]) !== JSON.stringify(payload.metadata[key])) {
|
||||||
|
this[key] = payload.metadata[key]
|
||||||
|
this.changed(key, true)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(payload.tags) && !payload.tags.some((tag) => typeof tag !== 'string') && JSON.stringify(this.tags) !== JSON.stringify(payload.tags)) {
|
||||||
|
this.tags = payload.tags
|
||||||
|
this.changed('tags', true)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove support for updating audioFiles, chapters and ebookFile here
|
||||||
|
const arrayOfObjectsKeys = ['audioFiles', 'chapters']
|
||||||
|
arrayOfObjectsKeys.forEach((key) => {
|
||||||
|
if (Array.isArray(payload[key]) && !payload[key].some((item) => typeof item !== 'object') && JSON.stringify(this[key]) !== JSON.stringify(payload[key])) {
|
||||||
|
this[key] = payload[key]
|
||||||
|
this.changed(key, true)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (payload.ebookFile && JSON.stringify(this.ebookFile) !== JSON.stringify(payload.ebookFile)) {
|
||||||
|
this.ebookFile = payload.ebookFile
|
||||||
|
this.changed('ebookFile', true)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUpdates) {
|
||||||
|
Logger.debug(`[Book] "${this.title}" changed keys:`, this.changed())
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(payload.metadata?.authors)) {
|
||||||
|
const authorsRemoved = this.authors.filter((au) => !payload.metadata.authors.some((a) => a.id === au.id))
|
||||||
|
const newAuthors = payload.metadata.authors.filter((a) => !this.authors.some((au) => au.id === a.id))
|
||||||
|
|
||||||
|
for (const author of authorsRemoved) {
|
||||||
|
await this.sequelize.models.bookAuthor.removeByIds(author.id, this.id)
|
||||||
|
Logger.debug(`[Book] "${this.title}" Removed author ${author.id}`)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
for (const author of newAuthors) {
|
||||||
|
await this.sequelize.models.bookAuthor.create({ bookId: this.id, authorId: author.id })
|
||||||
|
Logger.debug(`[Book] "${this.title}" Added author ${author.id}`)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(payload.metadata?.series)) {
|
||||||
|
const seriesRemoved = this.series.filter((se) => !payload.metadata.series.some((s) => s.id === se.id))
|
||||||
|
const newSeries = payload.metadata.series.filter((s) => !this.series.some((se) => se.id === s.id))
|
||||||
|
|
||||||
|
for (const series of seriesRemoved) {
|
||||||
|
await this.sequelize.models.bookSeries.removeByIds(series.id, this.id)
|
||||||
|
Logger.debug(`[Book] "${this.title}" Removed series ${series.id}`)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
for (const series of newSeries) {
|
||||||
|
await this.sequelize.models.bookSeries.create({ bookId: this.id, seriesId: series.id, sequence: series.sequence })
|
||||||
|
Logger.debug(`[Book] "${this.title}" Added series ${series.id}`)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
for (const series of payload.metadata.series) {
|
||||||
|
const existingSeries = this.series.find((se) => se.id === series.id)
|
||||||
|
if (existingSeries && existingSeries.bookSeries.sequence !== series.sequence) {
|
||||||
|
await existingSeries.bookSeries.update({ sequence: series.sequence })
|
||||||
|
Logger.debug(`[Book] "${this.title}" Updated series ${series.id} sequence ${series.sequence}`)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old model kept metadata in a separate object
|
||||||
|
*/
|
||||||
|
oldMetadataToJSON() {
|
||||||
|
const authors = this.authors.map((au) => ({ id: au.id, name: au.name }))
|
||||||
|
const series = this.series.map((se) => ({ id: se.id, name: se.name, sequence: se.bookSeries.sequence }))
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
subtitle: this.subtitle,
|
||||||
|
authors,
|
||||||
|
narrators: [...(this.narrators || [])],
|
||||||
|
series,
|
||||||
|
genres: [...(this.genres || [])],
|
||||||
|
publishedYear: this.publishedYear,
|
||||||
|
publishedDate: this.publishedDate,
|
||||||
|
publisher: this.publisher,
|
||||||
|
description: this.description,
|
||||||
|
isbn: this.isbn,
|
||||||
|
asin: this.asin,
|
||||||
|
language: this.language,
|
||||||
|
explicit: this.explicit,
|
||||||
|
abridged: this.abridged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldMetadataToJSONMinified() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
titleIgnorePrefix: getTitlePrefixAtEnd(this.title),
|
||||||
|
subtitle: this.subtitle,
|
||||||
|
authorName: this.authorName,
|
||||||
|
authorNameLF: this.authorNameLF,
|
||||||
|
narratorName: (this.narrators || []).join(', '),
|
||||||
|
seriesName: this.seriesName,
|
||||||
|
genres: [...(this.genres || [])],
|
||||||
|
publishedYear: this.publishedYear,
|
||||||
|
publishedDate: this.publishedDate,
|
||||||
|
publisher: this.publisher,
|
||||||
|
description: this.description,
|
||||||
|
isbn: this.isbn,
|
||||||
|
asin: this.asin,
|
||||||
|
language: this.language,
|
||||||
|
explicit: this.explicit,
|
||||||
|
abridged: this.abridged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldMetadataToJSONExpanded() {
|
||||||
|
const oldMetadataJSON = this.oldMetadataToJSON()
|
||||||
|
oldMetadataJSON.titleIgnorePrefix = getTitlePrefixAtEnd(this.title)
|
||||||
|
oldMetadataJSON.authorName = this.authorName
|
||||||
|
oldMetadataJSON.authorNameLF = this.authorNameLF
|
||||||
|
oldMetadataJSON.narratorName = (this.narrators || []).join(', ')
|
||||||
|
oldMetadataJSON.seriesName = this.seriesName
|
||||||
|
return oldMetadataJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The old model stored a minified series and authors array with the book object.
|
||||||
|
* Minified series is { id, name, sequence }
|
||||||
|
* Minified author is { id, name }
|
||||||
|
*
|
||||||
|
* @param {string} libraryItemId
|
||||||
|
*/
|
||||||
|
toOldJSON(libraryItemId) {
|
||||||
|
if (!libraryItemId) {
|
||||||
|
throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`)
|
||||||
|
}
|
||||||
|
if (!this.authors) {
|
||||||
|
throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`)
|
||||||
|
}
|
||||||
|
if (!this.series) {
|
||||||
|
throw new Error(`[Book] Cannot convert to old JSON because series are not loaded`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
libraryItemId: libraryItemId,
|
||||||
|
metadata: this.oldMetadataToJSON(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...(this.tags || [])],
|
||||||
|
audioFiles: structuredClone(this.audioFiles),
|
||||||
|
chapters: structuredClone(this.chapters),
|
||||||
|
ebookFile: structuredClone(this.ebookFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toOldJSONMinified() {
|
||||||
|
if (!this.authors) {
|
||||||
|
throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`)
|
||||||
|
}
|
||||||
|
if (!this.series) {
|
||||||
|
throw new Error(`[Book] Cannot convert to old JSON because series are not loaded`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
metadata: this.oldMetadataToJSONMinified(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...(this.tags || [])],
|
||||||
|
numTracks: this.trackList.length,
|
||||||
|
numAudioFiles: this.audioFiles?.length || 0,
|
||||||
|
numChapters: this.chapters?.length || 0,
|
||||||
|
duration: this.duration,
|
||||||
|
size: this.size,
|
||||||
|
ebookFormat: this.ebookFile?.ebookFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toOldJSONExpanded(libraryItemId) {
|
||||||
|
if (!libraryItemId) {
|
||||||
|
throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`)
|
||||||
|
}
|
||||||
|
if (!this.authors) {
|
||||||
|
throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`)
|
||||||
|
}
|
||||||
|
if (!this.series) {
|
||||||
|
throw new Error(`[Book] Cannot convert to old JSON because series are not loaded`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
libraryItemId: libraryItemId,
|
||||||
|
metadata: this.oldMetadataToJSONExpanded(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...(this.tags || [])],
|
||||||
|
audioFiles: structuredClone(this.audioFiles),
|
||||||
|
chapters: structuredClone(this.chapters),
|
||||||
|
ebookFile: structuredClone(this.ebookFile),
|
||||||
|
duration: this.duration,
|
||||||
|
size: this.size,
|
||||||
|
tracks: structuredClone(this.trackList)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Book
|
module.exports = Book
|
||||||
|
@ -865,54 +865,6 @@ class LibraryItem extends Model {
|
|||||||
return libraryItem.media.coverPath
|
return libraryItem.media.coverPath
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {import('sequelize').FindOptions} options
|
|
||||||
* @returns {Promise<Book|Podcast>}
|
|
||||||
*/
|
|
||||||
getMedia(options) {
|
|
||||||
if (!this.mediaType) return Promise.resolve(null)
|
|
||||||
const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}`
|
|
||||||
return this[mixinMethodName](options)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @returns {Promise<Book|Podcast>}
|
|
||||||
*/
|
|
||||||
getMediaExpanded() {
|
|
||||||
if (this.mediaType === 'podcast') {
|
|
||||||
return this.getMedia({
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: this.sequelize.models.podcastEpisode
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return this.getMedia({
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: this.sequelize.models.author,
|
|
||||||
through: {
|
|
||||||
attributes: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: this.sequelize.models.series,
|
|
||||||
through: {
|
|
||||||
attributes: ['sequence']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
order: [
|
|
||||||
[this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
|
|
||||||
[this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
@ -1131,6 +1083,64 @@ class LibraryItem extends Model {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isBook() {
|
||||||
|
return this.mediaType === 'book'
|
||||||
|
}
|
||||||
|
get isPodcast() {
|
||||||
|
return this.mediaType === 'podcast'
|
||||||
|
}
|
||||||
|
get hasAudioTracks() {
|
||||||
|
return this.media.hasAudioTracks()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').FindOptions} options
|
||||||
|
* @returns {Promise<Book|Podcast>}
|
||||||
|
*/
|
||||||
|
getMedia(options) {
|
||||||
|
if (!this.mediaType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Promise<Book|Podcast>}
|
||||||
|
*/
|
||||||
|
getMediaExpanded() {
|
||||||
|
if (this.mediaType === 'podcast') {
|
||||||
|
return this.getMedia({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: this.sequelize.models.podcastEpisode
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return this.getMedia({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: this.sequelize.models.author,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: this.sequelize.models.series,
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [
|
||||||
|
[this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
|
||||||
|
[this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if book or podcast library item has audio tracks
|
* Check if book or podcast library item has audio tracks
|
||||||
* Requires expanded library item
|
* Requires expanded library item
|
||||||
@ -1142,12 +1152,133 @@ class LibraryItem extends Model {
|
|||||||
Logger.error(`[LibraryItem] hasAudioTracks: Library item "${this.id}" does not have media`)
|
Logger.error(`[LibraryItem] hasAudioTracks: Library item "${this.id}" does not have media`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (this.mediaType === 'book') {
|
if (this.isBook) {
|
||||||
return this.media.audioFiles?.length > 0
|
return this.media.audioFiles?.length > 0
|
||||||
} else {
|
} else {
|
||||||
return this.media.podcastEpisodes?.length > 0
|
return this.media.podcastEpisodes?.length > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} ino
|
||||||
|
* @returns {import('./Book').AudioFileObject}
|
||||||
|
*/
|
||||||
|
getAudioFileWithIno(ino) {
|
||||||
|
if (!this.media) {
|
||||||
|
Logger.error(`[LibraryItem] getAudioFileWithIno: Library item "${this.id}" does not have media`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (this.isBook) {
|
||||||
|
return this.media.audioFiles.find((af) => af.ino === ino)
|
||||||
|
} else {
|
||||||
|
return this.media.podcastEpisodes.find((pe) => pe.audioFile?.ino === ino)?.audioFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} ino
|
||||||
|
* @returns {LibraryFile}
|
||||||
|
*/
|
||||||
|
getLibraryFileWithIno(ino) {
|
||||||
|
const libraryFile = this.libraryFiles.find((lf) => lf.ino === ino)
|
||||||
|
if (!libraryFile) return null
|
||||||
|
return new LibraryFile(libraryFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
getLibraryFiles() {
|
||||||
|
return this.libraryFiles.map((lf) => new LibraryFile(lf))
|
||||||
|
}
|
||||||
|
|
||||||
|
getLibraryFilesJson() {
|
||||||
|
return this.libraryFiles.map((lf) => new LibraryFile(lf).toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
toOldJSON() {
|
||||||
|
if (!this.media) {
|
||||||
|
throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
ino: this.ino,
|
||||||
|
oldLibraryItemId: this.extraData?.oldLibraryItemId || null,
|
||||||
|
libraryId: this.libraryId,
|
||||||
|
folderId: this.libraryFolderId,
|
||||||
|
path: this.path,
|
||||||
|
relPath: this.relPath,
|
||||||
|
isFile: this.isFile,
|
||||||
|
mtimeMs: this.mtime?.valueOf(),
|
||||||
|
ctimeMs: this.ctime?.valueOf(),
|
||||||
|
birthtimeMs: this.birthtime?.valueOf(),
|
||||||
|
addedAt: this.createdAt.valueOf(),
|
||||||
|
updatedAt: this.updatedAt.valueOf(),
|
||||||
|
lastScan: this.lastScan?.valueOf(),
|
||||||
|
scanVersion: this.lastScanVersion,
|
||||||
|
isMissing: !!this.isMissing,
|
||||||
|
isInvalid: !!this.isInvalid,
|
||||||
|
mediaType: this.mediaType,
|
||||||
|
media: this.media.toOldJSON(this.id),
|
||||||
|
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
||||||
|
libraryFiles: this.getLibraryFilesJson()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toOldJSONMinified() {
|
||||||
|
if (!this.media) {
|
||||||
|
throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
ino: this.ino,
|
||||||
|
oldLibraryItemId: this.extraData?.oldLibraryItemId || null,
|
||||||
|
libraryId: this.libraryId,
|
||||||
|
folderId: this.libraryFolderId,
|
||||||
|
path: this.path,
|
||||||
|
relPath: this.relPath,
|
||||||
|
isFile: this.isFile,
|
||||||
|
mtimeMs: this.mtime?.valueOf(),
|
||||||
|
ctimeMs: this.ctime?.valueOf(),
|
||||||
|
birthtimeMs: this.birthtime?.valueOf(),
|
||||||
|
addedAt: this.createdAt.valueOf(),
|
||||||
|
updatedAt: this.updatedAt.valueOf(),
|
||||||
|
isMissing: !!this.isMissing,
|
||||||
|
isInvalid: !!this.isInvalid,
|
||||||
|
mediaType: this.mediaType,
|
||||||
|
media: this.media.toOldJSONMinified(),
|
||||||
|
numFiles: this.libraryFiles.length,
|
||||||
|
size: this.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toOldJSONExpanded() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
ino: this.ino,
|
||||||
|
oldLibraryItemId: this.extraData?.oldLibraryItemId || null,
|
||||||
|
libraryId: this.libraryId,
|
||||||
|
folderId: this.libraryFolderId,
|
||||||
|
path: this.path,
|
||||||
|
relPath: this.relPath,
|
||||||
|
isFile: this.isFile,
|
||||||
|
mtimeMs: this.mtime?.valueOf(),
|
||||||
|
ctimeMs: this.ctime?.valueOf(),
|
||||||
|
birthtimeMs: this.birthtime?.valueOf(),
|
||||||
|
addedAt: this.createdAt.valueOf(),
|
||||||
|
updatedAt: this.updatedAt.valueOf(),
|
||||||
|
lastScan: this.lastScan?.valueOf(),
|
||||||
|
scanVersion: this.lastScanVersion,
|
||||||
|
isMissing: !!this.isMissing,
|
||||||
|
isInvalid: !!this.isInvalid,
|
||||||
|
mediaType: this.mediaType,
|
||||||
|
media: this.media.toOldJSONExpanded(this.id),
|
||||||
|
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
||||||
|
libraryFiles: this.getLibraryFilesJson(),
|
||||||
|
size: this.size
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = LibraryItem
|
module.exports = LibraryItem
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef PodcastExpandedProperties
|
* @typedef PodcastExpandedProperties
|
||||||
@ -47,6 +49,8 @@ class Podcast extends Model {
|
|||||||
this.lastEpisodeCheck
|
this.lastEpisodeCheck
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
this.maxEpisodesToKeep
|
this.maxEpisodesToKeep
|
||||||
|
/** @type {number} */
|
||||||
|
this.maxNewEpisodesToDownload
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
this.coverPath
|
this.coverPath
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
@ -57,6 +61,9 @@ class Podcast extends Model {
|
|||||||
this.createdAt
|
this.createdAt
|
||||||
/** @type {Date} */
|
/** @type {Date} */
|
||||||
this.updatedAt
|
this.updatedAt
|
||||||
|
|
||||||
|
/** @type {import('./PodcastEpisode')[]} */
|
||||||
|
this.podcastEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
static getOldPodcast(libraryItemExpanded) {
|
static getOldPodcast(libraryItemExpanded) {
|
||||||
@ -119,25 +126,6 @@ class Podcast extends Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAbsMetadataJson() {
|
|
||||||
return {
|
|
||||||
tags: this.tags || [],
|
|
||||||
title: this.title,
|
|
||||||
author: this.author,
|
|
||||||
description: this.description,
|
|
||||||
releaseDate: this.releaseDate,
|
|
||||||
genres: this.genres || [],
|
|
||||||
feedURL: this.feedURL,
|
|
||||||
imageURL: this.imageURL,
|
|
||||||
itunesPageURL: this.itunesPageURL,
|
|
||||||
itunesId: this.itunesId,
|
|
||||||
itunesArtistId: this.itunesArtistId,
|
|
||||||
language: this.language,
|
|
||||||
explicit: !!this.explicit,
|
|
||||||
podcastType: this.podcastType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize model
|
* Initialize model
|
||||||
* @param {import('../Database').sequelize} sequelize
|
* @param {import('../Database').sequelize} sequelize
|
||||||
@ -179,6 +167,210 @@ class Podcast extends Model {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasMediaFiles() {
|
||||||
|
return !!this.podcastEpisodes?.length
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasAudioTracks() {
|
||||||
|
return this.hasMediaFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
if (!this.podcastEpisodes?.length) return 0
|
||||||
|
return this.podcastEpisodes.reduce((total, episode) => total + episode.size, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAbsMetadataJson() {
|
||||||
|
return {
|
||||||
|
tags: this.tags || [],
|
||||||
|
title: this.title,
|
||||||
|
author: this.author,
|
||||||
|
description: this.description,
|
||||||
|
releaseDate: this.releaseDate,
|
||||||
|
genres: this.genres || [],
|
||||||
|
feedURL: this.feedURL,
|
||||||
|
imageURL: this.imageURL,
|
||||||
|
itunesPageURL: this.itunesPageURL,
|
||||||
|
itunesId: this.itunesId,
|
||||||
|
itunesArtistId: this.itunesArtistId,
|
||||||
|
language: this.language,
|
||||||
|
explicit: !!this.explicit,
|
||||||
|
podcastType: this.podcastType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} payload - Old podcast object
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async updateFromRequest(payload) {
|
||||||
|
if (!payload) return false
|
||||||
|
|
||||||
|
let hasUpdates = false
|
||||||
|
|
||||||
|
if (payload.metadata) {
|
||||||
|
const stringKeys = ['title', 'author', 'releaseDate', 'feedUrl', 'imageUrl', 'description', 'itunesPageUrl', 'itunesId', 'itunesArtistId', 'language', 'type']
|
||||||
|
stringKeys.forEach((key) => {
|
||||||
|
let newKey = key
|
||||||
|
if (key === 'type') {
|
||||||
|
newKey = 'podcastType'
|
||||||
|
} else if (key === 'feedUrl') {
|
||||||
|
newKey = 'feedURL'
|
||||||
|
} else if (key === 'imageUrl') {
|
||||||
|
newKey = 'imageURL'
|
||||||
|
} else if (key === 'itunesPageUrl') {
|
||||||
|
newKey = 'itunesPageURL'
|
||||||
|
}
|
||||||
|
if (typeof payload.metadata[key] === 'string' && payload.metadata[key] !== this[newKey]) {
|
||||||
|
this[newKey] = payload.metadata[key]
|
||||||
|
if (key === 'title') {
|
||||||
|
this.titleIgnorePrefix = getTitleIgnorePrefix(this.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (payload.metadata.explicit !== undefined && payload.metadata.explicit !== this.explicit) {
|
||||||
|
this.explicit = !!payload.metadata.explicit
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(payload.metadata.genres) && !payload.metadata.genres.some((item) => typeof item !== 'string') && JSON.stringify(this.genres) !== JSON.stringify(payload.metadata.genres)) {
|
||||||
|
this.genres = payload.metadata.genres
|
||||||
|
this.changed('genres', true)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(payload.tags) && !payload.tags.some((item) => typeof item !== 'string') && JSON.stringify(this.tags) !== JSON.stringify(payload.tags)) {
|
||||||
|
this.tags = payload.tags
|
||||||
|
this.changed('tags', true)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.autoDownloadEpisodes !== undefined && payload.autoDownloadEpisodes !== this.autoDownloadEpisodes) {
|
||||||
|
this.autoDownloadEpisodes = !!payload.autoDownloadEpisodes
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
if (typeof payload.autoDownloadSchedule === 'string' && payload.autoDownloadSchedule !== this.autoDownloadSchedule) {
|
||||||
|
this.autoDownloadSchedule = payload.autoDownloadSchedule
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberKeys = ['maxEpisodesToKeep', 'maxNewEpisodesToDownload']
|
||||||
|
numberKeys.forEach((key) => {
|
||||||
|
if (typeof payload[key] === 'number' && payload[key] !== this[key]) {
|
||||||
|
this[key] = payload[key]
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasUpdates) {
|
||||||
|
Logger.debug(`[Podcast] changed keys:`, this.changed())
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old model kept metadata in a separate object
|
||||||
|
*/
|
||||||
|
oldMetadataToJSON() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
author: this.author,
|
||||||
|
description: this.description,
|
||||||
|
releaseDate: this.releaseDate,
|
||||||
|
genres: [...(this.genres || [])],
|
||||||
|
feedUrl: this.feedURL,
|
||||||
|
imageUrl: this.imageURL,
|
||||||
|
itunesPageUrl: this.itunesPageURL,
|
||||||
|
itunesId: this.itunesId,
|
||||||
|
itunesArtistId: this.itunesArtistId,
|
||||||
|
explicit: this.explicit,
|
||||||
|
language: this.language,
|
||||||
|
type: this.podcastType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldMetadataToJSONExpanded() {
|
||||||
|
const oldMetadataJSON = this.oldMetadataToJSON()
|
||||||
|
oldMetadataJSON.titleIgnorePrefix = getTitlePrefixAtEnd(this.title)
|
||||||
|
return oldMetadataJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The old model stored episodes with the podcast object
|
||||||
|
*
|
||||||
|
* @param {string} libraryItemId
|
||||||
|
*/
|
||||||
|
toOldJSON(libraryItemId) {
|
||||||
|
if (!libraryItemId) {
|
||||||
|
throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`)
|
||||||
|
}
|
||||||
|
if (!this.podcastEpisodes) {
|
||||||
|
throw new Error(`[Podcast] Cannot convert to old JSON because episodes are not provided`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
libraryItemId: libraryItemId,
|
||||||
|
metadata: this.oldMetadataToJSON(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...(this.tags || [])],
|
||||||
|
episodes: this.podcastEpisodes.map((episode) => episode.toOldJSON(libraryItemId)),
|
||||||
|
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||||
|
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||||
|
lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null,
|
||||||
|
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||||
|
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toOldJSONMinified() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
// Minified metadata and expanded metadata are the same
|
||||||
|
metadata: this.oldMetadataToJSONExpanded(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...(this.tags || [])],
|
||||||
|
numEpisodes: this.podcastEpisodes?.length || 0,
|
||||||
|
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||||
|
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||||
|
lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null,
|
||||||
|
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||||
|
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
|
||||||
|
size: this.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toOldJSONExpanded(libraryItemId) {
|
||||||
|
if (!libraryItemId) {
|
||||||
|
throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`)
|
||||||
|
}
|
||||||
|
if (!this.podcastEpisodes) {
|
||||||
|
throw new Error(`[Podcast] Cannot convert to old JSON because episodes are not provided`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
libraryItemId: libraryItemId,
|
||||||
|
metadata: this.oldMetadataToJSONExpanded(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...(this.tags || [])],
|
||||||
|
episodes: this.podcastEpisodes.map((e) => e.toOldJSONExpanded(libraryItemId)),
|
||||||
|
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||||
|
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||||
|
lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null,
|
||||||
|
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||||
|
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
|
||||||
|
size: this.size
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Podcast
|
module.exports = Podcast
|
||||||
|
@ -53,42 +53,6 @@ class PodcastEpisode extends Model {
|
|||||||
this.updatedAt
|
this.updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} libraryItemId
|
|
||||||
* @returns {oldPodcastEpisode}
|
|
||||||
*/
|
|
||||||
getOldPodcastEpisode(libraryItemId = null) {
|
|
||||||
let enclosure = null
|
|
||||||
if (this.enclosureURL) {
|
|
||||||
enclosure = {
|
|
||||||
url: this.enclosureURL,
|
|
||||||
type: this.enclosureType,
|
|
||||||
length: this.enclosureSize !== null ? String(this.enclosureSize) : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new oldPodcastEpisode({
|
|
||||||
libraryItemId: libraryItemId || null,
|
|
||||||
podcastId: this.podcastId,
|
|
||||||
id: this.id,
|
|
||||||
oldEpisodeId: this.extraData?.oldEpisodeId || null,
|
|
||||||
index: this.index,
|
|
||||||
season: this.season,
|
|
||||||
episode: this.episode,
|
|
||||||
episodeType: this.episodeType,
|
|
||||||
title: this.title,
|
|
||||||
subtitle: this.subtitle,
|
|
||||||
description: this.description,
|
|
||||||
enclosure,
|
|
||||||
guid: this.extraData?.guid || null,
|
|
||||||
pubDate: this.pubDate,
|
|
||||||
chapters: this.chapters,
|
|
||||||
audioFile: this.audioFile,
|
|
||||||
publishedAt: this.publishedAt?.valueOf() || null,
|
|
||||||
addedAt: this.createdAt.valueOf(),
|
|
||||||
updatedAt: this.updatedAt.valueOf()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static createFromOld(oldEpisode) {
|
static createFromOld(oldEpisode) {
|
||||||
const podcastEpisode = this.getFromOld(oldEpisode)
|
const podcastEpisode = this.getFromOld(oldEpisode)
|
||||||
return this.create(podcastEpisode)
|
return this.create(podcastEpisode)
|
||||||
@ -184,7 +148,51 @@ class PodcastEpisode extends Model {
|
|||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.audioFile?.metadata.size || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} libraryItemId
|
||||||
|
* @returns {oldPodcastEpisode}
|
||||||
|
*/
|
||||||
|
getOldPodcastEpisode(libraryItemId = null) {
|
||||||
|
let enclosure = null
|
||||||
|
if (this.enclosureURL) {
|
||||||
|
enclosure = {
|
||||||
|
url: this.enclosureURL,
|
||||||
|
type: this.enclosureType,
|
||||||
|
length: this.enclosureSize !== null ? String(this.enclosureSize) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new oldPodcastEpisode({
|
||||||
|
libraryItemId: libraryItemId || null,
|
||||||
|
podcastId: this.podcastId,
|
||||||
|
id: this.id,
|
||||||
|
oldEpisodeId: this.extraData?.oldEpisodeId || null,
|
||||||
|
index: this.index,
|
||||||
|
season: this.season,
|
||||||
|
episode: this.episode,
|
||||||
|
episodeType: this.episodeType,
|
||||||
|
title: this.title,
|
||||||
|
subtitle: this.subtitle,
|
||||||
|
description: this.description,
|
||||||
|
enclosure,
|
||||||
|
guid: this.extraData?.guid || null,
|
||||||
|
pubDate: this.pubDate,
|
||||||
|
chapters: this.chapters,
|
||||||
|
audioFile: this.audioFile,
|
||||||
|
publishedAt: this.publishedAt?.valueOf() || null,
|
||||||
|
addedAt: this.createdAt.valueOf(),
|
||||||
|
updatedAt: this.updatedAt.valueOf()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
toOldJSON(libraryItemId) {
|
toOldJSON(libraryItemId) {
|
||||||
|
if (!libraryItemId) {
|
||||||
|
throw new Error(`[PodcastEpisode] Cannot convert to old JSON because libraryItemId is not provided`)
|
||||||
|
}
|
||||||
|
|
||||||
let enclosure = null
|
let enclosure = null
|
||||||
if (this.enclosureURL) {
|
if (this.enclosureURL) {
|
||||||
enclosure = {
|
enclosure = {
|
||||||
@ -209,8 +217,8 @@ class PodcastEpisode extends Model {
|
|||||||
enclosure,
|
enclosure,
|
||||||
guid: this.extraData?.guid || null,
|
guid: this.extraData?.guid || null,
|
||||||
pubDate: this.pubDate,
|
pubDate: this.pubDate,
|
||||||
chapters: this.chapters?.map((ch) => ({ ...ch })) || [],
|
chapters: structuredClone(this.chapters),
|
||||||
audioFile: this.audioFile || null,
|
audioFile: structuredClone(this.audioFile),
|
||||||
publishedAt: this.publishedAt?.valueOf() || null,
|
publishedAt: this.publishedAt?.valueOf() || null,
|
||||||
addedAt: this.createdAt.valueOf(),
|
addedAt: this.createdAt.valueOf(),
|
||||||
updatedAt: this.updatedAt.valueOf()
|
updatedAt: this.updatedAt.valueOf()
|
||||||
@ -221,7 +229,7 @@ class PodcastEpisode extends Model {
|
|||||||
const json = this.toOldJSON(libraryItemId)
|
const json = this.toOldJSON(libraryItemId)
|
||||||
|
|
||||||
json.audioTrack = this.track
|
json.audioTrack = this.track
|
||||||
json.size = this.audioFile?.metadata.size || 0
|
json.size = this.size
|
||||||
json.duration = this.audioFile?.duration || 0
|
json.duration = this.audioFile?.duration || 0
|
||||||
|
|
||||||
return json
|
return json
|
||||||
|
@ -177,9 +177,6 @@ class LibraryItem {
|
|||||||
get hasAudioFiles() {
|
get hasAudioFiles() {
|
||||||
return this.libraryFiles.some((lf) => lf.fileType === 'audio')
|
return this.libraryFiles.some((lf) => lf.fileType === 'audio')
|
||||||
}
|
}
|
||||||
get hasMediaEntities() {
|
|
||||||
return this.media.hasMediaEntities
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data comes from scandir library item data
|
// Data comes from scandir library item data
|
||||||
// TODO: Remove this function. Only used when creating a new podcast now
|
// TODO: Remove this function. Only used when creating a new podcast now
|
||||||
@ -327,20 +324,5 @@ class LibraryItem {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the EBookFile from a LibraryFile
|
|
||||||
* If null then ebookFile will be removed from the book
|
|
||||||
* all ebook library files that are not primary are marked as supplementary
|
|
||||||
*
|
|
||||||
* @param {LibraryFile} [libraryFile]
|
|
||||||
*/
|
|
||||||
setPrimaryEbook(ebookLibraryFile = null) {
|
|
||||||
const ebookLibraryFiles = this.libraryFiles.filter((lf) => lf.isEBookFile)
|
|
||||||
for (const libraryFile of ebookLibraryFiles) {
|
|
||||||
libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino
|
|
||||||
}
|
|
||||||
this.media.setEbookFile(ebookLibraryFile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
module.exports = LibraryItem
|
module.exports = LibraryItem
|
||||||
|
@ -33,8 +33,8 @@ class Book {
|
|||||||
this.metadata = new BookMetadata(book.metadata)
|
this.metadata = new BookMetadata(book.metadata)
|
||||||
this.coverPath = book.coverPath
|
this.coverPath = book.coverPath
|
||||||
this.tags = [...book.tags]
|
this.tags = [...book.tags]
|
||||||
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
|
this.audioFiles = book.audioFiles.map((f) => new AudioFile(f))
|
||||||
this.chapters = book.chapters.map(c => ({ ...c }))
|
this.chapters = book.chapters.map((c) => ({ ...c }))
|
||||||
this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null
|
this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null
|
||||||
this.lastCoverSearch = book.lastCoverSearch || null
|
this.lastCoverSearch = book.lastCoverSearch || null
|
||||||
this.lastCoverSearchQuery = book.lastCoverSearchQuery || null
|
this.lastCoverSearchQuery = book.lastCoverSearchQuery || null
|
||||||
@ -47,8 +47,8 @@ class Book {
|
|||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
audioFiles: this.audioFiles.map((f) => f.toJSON()),
|
||||||
chapters: this.chapters.map(c => ({ ...c })),
|
chapters: this.chapters.map((c) => ({ ...c })),
|
||||||
ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null
|
ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,11 +75,11 @@ class Book {
|
|||||||
metadata: this.metadata.toJSONExpanded(),
|
metadata: this.metadata.toJSONExpanded(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
audioFiles: this.audioFiles.map((f) => f.toJSON()),
|
||||||
chapters: this.chapters.map(c => ({ ...c })),
|
chapters: this.chapters.map((c) => ({ ...c })),
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
size: this.size,
|
size: this.size,
|
||||||
tracks: this.tracks.map(t => t.toJSON()),
|
tracks: this.tracks.map((t) => t.toJSON()),
|
||||||
ebookFile: this.ebookFile?.toJSON() || null
|
ebookFile: this.ebookFile?.toJSON() || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,24 +87,21 @@ class Book {
|
|||||||
toJSONForMetadataFile() {
|
toJSONForMetadataFile() {
|
||||||
return {
|
return {
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
chapters: this.chapters.map(c => ({ ...c })),
|
chapters: this.chapters.map((c) => ({ ...c })),
|
||||||
...this.metadata.toJSONForMetadataFile()
|
...this.metadata.toJSONForMetadataFile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
var total = 0
|
var total = 0
|
||||||
this.audioFiles.forEach((af) => total += af.metadata.size)
|
this.audioFiles.forEach((af) => (total += af.metadata.size))
|
||||||
if (this.ebookFile) {
|
if (this.ebookFile) {
|
||||||
total += this.ebookFile.metadata.size
|
total += this.ebookFile.metadata.size
|
||||||
}
|
}
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
get hasMediaEntities() {
|
|
||||||
return !!this.tracks.length || this.ebookFile
|
|
||||||
}
|
|
||||||
get includedAudioFiles() {
|
get includedAudioFiles() {
|
||||||
return this.audioFiles.filter(af => !af.exclude)
|
return this.audioFiles.filter((af) => !af.exclude)
|
||||||
}
|
}
|
||||||
get tracks() {
|
get tracks() {
|
||||||
let startOffset = 0
|
let startOffset = 0
|
||||||
@ -117,7 +114,7 @@ class Book {
|
|||||||
}
|
}
|
||||||
get duration() {
|
get duration() {
|
||||||
let total = 0
|
let total = 0
|
||||||
this.tracks.forEach((track) => total += track.duration)
|
this.tracks.forEach((track) => (total += track.duration))
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
get numTracks() {
|
get numTracks() {
|
||||||
@ -129,8 +126,6 @@ class Book {
|
|||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
const json = this.toJSON()
|
const json = this.toJSON()
|
||||||
delete json.audiobooks // do not update media entities here
|
|
||||||
delete json.ebooks
|
|
||||||
|
|
||||||
let hasUpdates = false
|
let hasUpdates = false
|
||||||
for (const key in json) {
|
for (const key in json) {
|
||||||
@ -149,30 +144,6 @@ class Book {
|
|||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChapters(chapters) {
|
|
||||||
var hasUpdates = this.chapters.length !== chapters.length
|
|
||||||
if (hasUpdates) {
|
|
||||||
this.chapters = chapters.map(ch => ({
|
|
||||||
id: ch.id,
|
|
||||||
start: ch.start,
|
|
||||||
end: ch.end,
|
|
||||||
title: ch.title
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < this.chapters.length; i++) {
|
|
||||||
const currChapter = this.chapters[i]
|
|
||||||
const newChapter = chapters[i]
|
|
||||||
if (!hasUpdates && (currChapter.title !== newChapter.title || currChapter.start !== newChapter.start || currChapter.end !== newChapter.end)) {
|
|
||||||
hasUpdates = true
|
|
||||||
}
|
|
||||||
this.chapters[i].title = newChapter.title
|
|
||||||
this.chapters[i].start = newChapter.start
|
|
||||||
this.chapters[i].end = newChapter.end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasUpdates
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCover(coverPath) {
|
updateCover(coverPath) {
|
||||||
coverPath = filePathToPOSIX(coverPath)
|
coverPath = filePathToPOSIX(coverPath)
|
||||||
if (this.coverPath === coverPath) return false
|
if (this.coverPath === coverPath) return false
|
||||||
@ -180,75 +151,6 @@ class Book {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFileWithInode(inode) {
|
|
||||||
if (this.audioFiles.some(af => af.ino === inode)) {
|
|
||||||
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (this.ebookFile && this.ebookFile.ino === inode) {
|
|
||||||
this.ebookFile = null
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get audio file or ebook file from inode
|
|
||||||
* @param {string} inode
|
|
||||||
* @returns {(AudioFile|EBookFile|null)}
|
|
||||||
*/
|
|
||||||
findFileWithInode(inode) {
|
|
||||||
const audioFile = this.audioFiles.find(af => af.ino === inode)
|
|
||||||
if (audioFile) return audioFile
|
|
||||||
if (this.ebookFile && this.ebookFile.ino === inode) return this.ebookFile
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the EBookFile from a LibraryFile
|
|
||||||
* If null then ebookFile will be removed from the book
|
|
||||||
*
|
|
||||||
* @param {LibraryFile} [libraryFile]
|
|
||||||
*/
|
|
||||||
setEbookFile(libraryFile = null) {
|
|
||||||
if (!libraryFile) {
|
|
||||||
this.ebookFile = null
|
|
||||||
} else {
|
|
||||||
const ebookFile = new EBookFile()
|
|
||||||
ebookFile.setData(libraryFile)
|
|
||||||
this.ebookFile = ebookFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addAudioFile(audioFile) {
|
|
||||||
this.audioFiles.push(audioFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAudioTracks(orderedFileData) {
|
|
||||||
let index = 1
|
|
||||||
this.audioFiles = orderedFileData.map((fileData) => {
|
|
||||||
const audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
|
|
||||||
audioFile.manuallyVerified = true
|
|
||||||
audioFile.error = null
|
|
||||||
if (fileData.exclude !== undefined) {
|
|
||||||
audioFile.exclude = !!fileData.exclude
|
|
||||||
}
|
|
||||||
if (audioFile.exclude) {
|
|
||||||
audioFile.index = -1
|
|
||||||
} else {
|
|
||||||
audioFile.index = index++
|
|
||||||
}
|
|
||||||
return audioFile
|
|
||||||
})
|
|
||||||
|
|
||||||
this.rebuildTracks()
|
|
||||||
}
|
|
||||||
|
|
||||||
rebuildTracks() {
|
|
||||||
Logger.debug(`[Book] Tracks being rebuilt...!`)
|
|
||||||
this.audioFiles.sort((a, b) => a.index - b.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only checks container format
|
// Only checks container format
|
||||||
checkCanDirectPlay(payload) {
|
checkCanDirectPlay(payload) {
|
||||||
var supportedMimeTypes = payload.supportedMimeTypes || []
|
var supportedMimeTypes = payload.supportedMimeTypes || []
|
||||||
@ -268,7 +170,7 @@ class Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getChapters() {
|
getChapters() {
|
||||||
return this.chapters?.map(ch => ({ ...ch })) || []
|
return this.chapters?.map((ch) => ({ ...ch })) || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = Book
|
module.exports = Book
|
||||||
|
@ -124,9 +124,6 @@ class Podcast {
|
|||||||
this.episodes.forEach((ep) => (total += ep.size))
|
this.episodes.forEach((ep) => (total += ep.size))
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
get hasMediaEntities() {
|
|
||||||
return !!this.episodes.length
|
|
||||||
}
|
|
||||||
get duration() {
|
get duration() {
|
||||||
let total = 0
|
let total = 0
|
||||||
this.episodes.forEach((ep) => (total += ep.duration))
|
this.episodes.forEach((ep) => (total += ep.duration))
|
||||||
@ -181,20 +178,6 @@ class Podcast {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFileWithInode(inode) {
|
|
||||||
const hasEpisode = this.episodes.some((ep) => ep.audioFile.ino === inode)
|
|
||||||
if (hasEpisode) {
|
|
||||||
this.episodes = this.episodes.filter((ep) => ep.audioFile.ino !== inode)
|
|
||||||
}
|
|
||||||
return hasEpisode
|
|
||||||
}
|
|
||||||
|
|
||||||
findFileWithInode(inode) {
|
|
||||||
var episode = this.episodes.find((ep) => ep.audioFile.ino === inode)
|
|
||||||
if (episode) return episode.audioFile
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(mediaData) {
|
setData(mediaData) {
|
||||||
this.metadata = new PodcastMetadata()
|
this.metadata = new PodcastMetadata()
|
||||||
if (mediaData.metadata) {
|
if (mediaData.metadata) {
|
||||||
|
@ -202,12 +202,12 @@ class AudioFileScanner {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AudioFile} audioFile
|
* @param {string} audioFilePath
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
probeAudioFile(audioFile) {
|
probeAudioFile(audioFilePath) {
|
||||||
Logger.debug(`[AudioFileScanner] Running ffprobe for audio file at "${audioFile.metadata.path}"`)
|
Logger.debug(`[AudioFileScanner] Running ffprobe for audio file at "${audioFilePath}"`)
|
||||||
return prober.rawProbe(audioFile.metadata.path)
|
return prober.rawProbe(audioFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,11 +82,11 @@ describe('LibraryItemController', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should remove authors and series with no books on library item delete', async () => {
|
it('should remove authors and series with no books on library item delete', async () => {
|
||||||
const oldLibraryItem = await Database.libraryItemModel.getOldById(libraryItem1Id)
|
const libraryItem = await Database.libraryItemModel.getExpandedById(libraryItem1Id)
|
||||||
|
|
||||||
const fakeReq = {
|
const fakeReq = {
|
||||||
query: {},
|
query: {},
|
||||||
libraryItem: oldLibraryItem
|
libraryItem
|
||||||
}
|
}
|
||||||
const fakeRes = {
|
const fakeRes = {
|
||||||
sendStatus: sinon.spy()
|
sendStatus: sinon.spy()
|
||||||
@ -156,7 +156,7 @@ describe('LibraryItemController', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should remove authors and series with no books on library item update media', async () => {
|
it('should remove authors and series with no books on library item update media', async () => {
|
||||||
const oldLibraryItem = await Database.libraryItemModel.getOldById(libraryItem1Id)
|
const libraryItem = await Database.libraryItemModel.getExpandedById(libraryItem1Id)
|
||||||
|
|
||||||
// Update library item 1 remove all authors and series
|
// Update library item 1 remove all authors and series
|
||||||
const fakeReq = {
|
const fakeReq = {
|
||||||
@ -167,7 +167,7 @@ describe('LibraryItemController', () => {
|
|||||||
series: []
|
series: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryItem: oldLibraryItem
|
libraryItem
|
||||||
}
|
}
|
||||||
const fakeRes = {
|
const fakeRes = {
|
||||||
json: sinon.spy()
|
json: sinon.spy()
|
||||||
|
Loading…
Reference in New Issue
Block a user