Skip to content

Commit

Permalink
Merge pull request #3800 from advplyr/migrate-library-item-in-scanner
Browse files Browse the repository at this point in the history
Migrate to new library item in scanner
  • Loading branch information
advplyr authored Jan 5, 2025
2 parents fdbca4f + 108eaba commit 57d742b
Show file tree
Hide file tree
Showing 35 changed files with 560 additions and 2,179 deletions.
17 changes: 0 additions & 17 deletions server/Database.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,23 +401,6 @@ class Database {
return this.models.setting.updateSettingObj(settings.toJSON())
}

/**
* Save metadata file and update library item
*
* @param {import('./objects/LibraryItem')} oldLibraryItem
* @returns {Promise<boolean>}
*/
async updateLibraryItem(oldLibraryItem) {
if (!this.sequelize) return false
await oldLibraryItem.saveMetadata()
const updated = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
// Clear library filter data cache
if (updated) {
delete this.libraryFilterData[oldLibraryItem.libraryId]
}
return updated
}

getPlaybackSessions(where = null) {
if (!this.sequelize) return false
return this.models.playbackSession.getOldPlaybackSessions(where)
Expand Down
10 changes: 10 additions & 0 deletions server/controllers/AuthorController.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,18 @@ class AuthorController {
await CacheManager.purgeImageCache(req.author.id) // Purge cache
}

// Load library items so that metadata file can be updated
const allItemsWithAuthor = await Database.authorModel.getAllLibraryItemsForAuthor(req.author.id)
allItemsWithAuthor.forEach((libraryItem) => {
libraryItem.media.authors = libraryItem.media.authors.filter((au) => au.id !== req.author.id)
})

await req.author.destroy()

for (const libraryItem of allItemsWithAuthor) {
await libraryItem.saveMetadataFile()
}

SocketAuthority.emitter('author_removed', req.author.toOldJSON())

// Update filter data
Expand Down
135 changes: 69 additions & 66 deletions server/controllers/LibraryItemController.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,31 +81,6 @@ class LibraryItemController {
res.json(req.libraryItem.toOldJSON())
}

/**
* PATCH: /api/items/:id
*
* @deprecated
* Use the updateMedia /api/items/:id/media endpoint instead or updateCover /api/items/:id/cover
*
* @param {LibraryItemControllerRequest} req
* @param {Response} res
*/
async update(req, res) {
// Item has cover and update is removing cover so purge it from cache
if (req.libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) {
await CacheManager.purgeCoverCache(req.libraryItem.id)
}

const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(req.libraryItem)
const hasUpdates = oldLibraryItem.update(req.body)
if (hasUpdates) {
Logger.debug(`[LibraryItemController] Updated now saving`)
await Database.updateLibraryItem(oldLibraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
}
res.json(oldLibraryItem.toJSON())
}

/**
* DELETE: /api/items/:id
* Delete library item. Will delete from database and file system if hard delete is requested.
Expand Down Expand Up @@ -219,11 +194,6 @@ class LibraryItemController {
if (res.writableEnded || res.headersSent) return
}

// Book specific
if (req.libraryItem.isBook) {
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, req.libraryItem.libraryId)
}

// Podcast specific
let isPodcastAutoDownloadUpdated = false
if (req.libraryItem.isPodcast) {
Expand All @@ -234,41 +204,56 @@ class LibraryItemController {
}
}

// Book specific - Get all series being removed from this item
let seriesRemoved = []
if (req.libraryItem.isBook && mediaPayload.metadata?.series) {
const seriesIdsInUpdate = mediaPayload.metadata.series?.map((se) => se.id) || []
seriesRemoved = req.libraryItem.media.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
let hasUpdates = (await req.libraryItem.media.updateFromRequest(mediaPayload)) || mediaPayload.url

if (req.libraryItem.isBook && Array.isArray(mediaPayload.metadata?.series)) {
const seriesUpdateData = await req.libraryItem.media.updateSeriesFromRequest(mediaPayload.metadata.series, req.libraryItem.libraryId)
if (seriesUpdateData?.seriesRemoved.length) {
// Check remove empty series
Logger.debug(`[LibraryItemController] Series were removed from book. Check if series are now empty.`)
await this.checkRemoveEmptySeries(seriesUpdateData.seriesRemoved.map((se) => se.id))
}
if (seriesUpdateData?.seriesAdded.length) {
// Add series to filter data
seriesUpdateData.seriesAdded.forEach((se) => {
Database.addSeriesToFilterData(req.libraryItem.libraryId, se.name, se.id)
})
}
if (seriesUpdateData?.hasUpdates) {
hasUpdates = true
}
}

let authorsRemoved = []
if (req.libraryItem.isBook && mediaPayload.metadata?.authors) {
const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id)
authorsRemoved = req.libraryItem.media.authors.filter((au) => !authorIdsInUpdate.includes(au.id))
if (req.libraryItem.isBook && Array.isArray(mediaPayload.metadata?.authors)) {
const authorNames = mediaPayload.metadata.authors.map((au) => (typeof au.name === 'string' ? au.name.trim() : null)).filter((au) => au)
const authorUpdateData = await req.libraryItem.media.updateAuthorsFromRequest(authorNames, req.libraryItem.libraryId)
if (authorUpdateData?.authorsRemoved.length) {
// Check remove empty authors
Logger.debug(`[LibraryItemController] Authors were removed from book. Check if authors are now empty.`)
await this.checkRemoveAuthorsWithNoBooks(authorUpdateData.authorsRemoved.map((au) => au.id))
hasUpdates = true
}
if (authorUpdateData?.authorsAdded.length) {
// Add authors to filter data
authorUpdateData.authorsAdded.forEach((au) => {
Database.addAuthorToFilterData(req.libraryItem.libraryId, au.name, au.id)
})
hasUpdates = true
}
}

const hasUpdates = (await req.libraryItem.media.updateFromRequest(mediaPayload)) || mediaPayload.url
if (hasUpdates) {
req.libraryItem.changed('updatedAt', true)
await req.libraryItem.save()

await req.libraryItem.saveMetadataFile()

if (isPodcastAutoDownloadUpdated) {
this.cronManager.checkUpdatePodcastCron(req.libraryItem)
}

Logger.debug(`[LibraryItemController] Updated library item media ${req.libraryItem.media.title}`)
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())

if (authorsRemoved.length) {
// Check remove empty authors
Logger.debug(`[LibraryItemController] Authors were removed from book. Check if authors are now empty.`)
await this.checkRemoveAuthorsWithNoBooks(authorsRemoved.map((au) => au.id))
}
if (seriesRemoved.length) {
// Check remove empty series
Logger.debug(`[LibraryItemController] Series were removed from book. Check if series are now empty.`)
await this.checkRemoveEmptySeries(seriesRemoved.map((se) => se.id))
}
}
res.json({
updated: hasUpdates,
Expand Down Expand Up @@ -527,8 +512,7 @@ class LibraryItemController {
options.overrideDetails = !!reqBody.overrideDetails
}

const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(req.libraryItem)
var matchResult = await Scanner.quickMatchLibraryItem(this, oldLibraryItem, options)
const matchResult = await Scanner.quickMatchLibraryItem(this, req.libraryItem, options)
res.json(matchResult)
}

Expand Down Expand Up @@ -640,26 +624,44 @@ class LibraryItemController {
const mediaPayload = updatePayload.mediaPayload
const libraryItem = libraryItems.find((li) => li.id === updatePayload.id)

await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
let hasUpdates = await libraryItem.media.updateFromRequest(mediaPayload)

if (libraryItem.isBook) {
if (Array.isArray(mediaPayload.metadata?.series)) {
const seriesIdsInUpdate = mediaPayload.metadata.series.map((se) => se.id)
const seriesRemoved = libraryItem.media.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
seriesIdsRemoved.push(...seriesRemoved.map((se) => se.id))
if (libraryItem.isBook && Array.isArray(mediaPayload.metadata?.series)) {
const seriesUpdateData = await libraryItem.media.updateSeriesFromRequest(mediaPayload.metadata.series, libraryItem.libraryId)
if (seriesUpdateData?.seriesRemoved.length) {
seriesIdsRemoved.push(...seriesUpdateData.seriesRemoved.map((se) => se.id))
}
if (seriesUpdateData?.seriesAdded.length) {
seriesUpdateData.seriesAdded.forEach((se) => {
Database.addSeriesToFilterData(libraryItem.libraryId, se.name, se.id)
})
}
if (Array.isArray(mediaPayload.metadata?.authors)) {
const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id)
const authorsRemoved = libraryItem.media.authors.filter((au) => !authorIdsInUpdate.includes(au.id))
authorIdsRemoved.push(...authorsRemoved.map((au) => au.id))
if (seriesUpdateData?.hasUpdates) {
hasUpdates = true
}
}

if (libraryItem.isBook && Array.isArray(mediaPayload.metadata?.authors)) {
const authorNames = mediaPayload.metadata.authors.map((au) => (typeof au.name === 'string' ? au.name.trim() : null)).filter((au) => au)
const authorUpdateData = await libraryItem.media.updateAuthorsFromRequest(authorNames, libraryItem.libraryId)
if (authorUpdateData?.authorsRemoved.length) {
authorIdsRemoved.push(...authorUpdateData.authorsRemoved.map((au) => au.id))
hasUpdates = true
}
if (authorUpdateData?.authorsAdded.length) {
authorUpdateData.authorsAdded.forEach((au) => {
Database.addAuthorToFilterData(libraryItem.libraryId, au.name, au.id)
})
hasUpdates = true
}
}

const hasUpdates = await libraryItem.media.updateFromRequest(mediaPayload)
if (hasUpdates) {
libraryItem.changed('updatedAt', true)
await libraryItem.save()

await libraryItem.saveMetadataFile()

Logger.debug(`[LibraryItemController] Updated library item media "${libraryItem.media.title}"`)
SocketAuthority.emitter('item_updated', libraryItem.toOldJSONExpanded())
itemsUpdated++
Expand Down Expand Up @@ -739,8 +741,7 @@ class LibraryItemController {
}

for (const libraryItem of libraryItems) {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
const matchResult = await Scanner.quickMatchLibraryItem(this, oldLibraryItem, options)
const matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, options)
if (matchResult.updated) {
itemsUpdated++
} else if (matchResult.warning) {
Expand Down Expand Up @@ -891,6 +892,8 @@ class LibraryItemController {
req.libraryItem.media.changed('chapters', true)
await req.libraryItem.media.save()

await req.libraryItem.saveMetadataFile()

SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
}

Expand Down
6 changes: 2 additions & 4 deletions server/controllers/PodcastController.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,9 @@ class PodcastController {
}

const overrideDetails = req.query.override === '1'
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(req.libraryItem)
const episodesUpdated = await Scanner.quickMatchPodcastEpisodes(oldLibraryItem, { overrideDetails })
const episodesUpdated = await Scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
if (episodesUpdated) {
await Database.updateLibraryItem(oldLibraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
}

res.json({
Expand Down
2 changes: 1 addition & 1 deletion server/controllers/SearchController.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class SearchController {
*/
async findBooks(req, res) {
const id = req.query.id
const libraryItem = await Database.libraryItemModel.getOldById(id)
const libraryItem = await Database.libraryItemModel.getExpandedById(id)
const provider = req.query.provider || 'google'
const title = req.query.title || ''
const author = req.query.author || ''
Expand Down
21 changes: 13 additions & 8 deletions server/controllers/ToolsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ const Database = require('../Database')
* @property {import('../models/User')} user
*
* @typedef {Request & RequestUserObject} RequestWithUser
*
* @typedef RequestEntityObject
* @property {import('../models/LibraryItem')} libraryItem
*
* @typedef {RequestWithUser & RequestEntityObject} RequestWithLibraryItem
*/

class ToolsController {
Expand All @@ -18,7 +23,7 @@ class ToolsController {
*
* @this import('../routers/ApiRouter')
*
* @param {RequestWithUser} req
* @param {RequestWithLibraryItem} req
* @param {Response} res
*/
async encodeM4b(req, res) {
Expand All @@ -27,12 +32,12 @@ class ToolsController {
return res.status(404).send('Audiobook not found')
}

if (req.libraryItem.mediaType !== 'book') {
if (!req.libraryItem.isBook) {
Logger.error(`[MiscController] encodeM4b: Invalid library item ${req.params.id}: not a book`)
return res.status(400).send('Invalid library item: not a book')
}

if (req.libraryItem.media.tracks.length <= 0) {
if (!req.libraryItem.hasAudioTracks) {
Logger.error(`[MiscController] encodeM4b: Invalid audiobook ${req.params.id}: no audio tracks`)
return res.status(400).send('Invalid audiobook: no audio tracks')
}
Expand Down Expand Up @@ -72,11 +77,11 @@ class ToolsController {
*
* @this import('../routers/ApiRouter')
*
* @param {RequestWithUser} req
* @param {RequestWithLibraryItem} req
* @param {Response} res
*/
async embedAudioFileMetadata(req, res) {
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioTracks || !req.libraryItem.isBook) {
Logger.error(`[ToolsController] Invalid library item`)
return res.sendStatus(400)
}
Expand Down Expand Up @@ -111,7 +116,7 @@ class ToolsController {

const libraryItems = []
for (const libraryItemId of libraryItemIds) {
const libraryItem = await Database.libraryItemModel.getOldById(libraryItemId)
const libraryItem = await Database.libraryItemModel.getExpandedById(libraryItemId)
if (!libraryItem) {
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`)
return res.sendStatus(404)
Expand All @@ -123,7 +128,7 @@ class ToolsController {
return res.sendStatus(403)
}

if (libraryItem.isMissing || !libraryItem.hasAudioFiles || !libraryItem.isBook) {
if (libraryItem.isMissing || !libraryItem.hasAudioTracks || !libraryItem.isBook) {
Logger.error(`[ToolsController] Batch embed invalid library item (${libraryItemId})`)
return res.sendStatus(400)
}
Expand Down Expand Up @@ -157,7 +162,7 @@ class ToolsController {
}

if (req.params.id) {
const item = await Database.libraryItemModel.getOldById(req.params.id)
const item = await Database.libraryItemModel.getExpandedById(req.params.id)
if (!item?.media) return res.sendStatus(404)

// Check user can access this library item
Expand Down
2 changes: 1 addition & 1 deletion server/finders/BookFinder.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ class BookFinder {
/**
* Search for books including fuzzy searches
*
* @param {Object} libraryItem
* @param {import('../models/LibraryItem')} libraryItem
* @param {string} provider
* @param {string} title
* @param {string} author
Expand Down
Loading

0 comments on commit 57d742b

Please sign in to comment.