Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to new library item in scanner #3800

Merged
merged 3 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading