Skip to content

Commit

Permalink
Merge pull request #3670 from advplyr/fix_remove_authors_no_books
Browse files Browse the repository at this point in the history
Fix:Remove authors with no books when a books is removed #3668
  • Loading branch information
advplyr authored Dec 1, 2024
2 parents ea4d5ff + 0dedb09 commit c03f18b
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 90 deletions.
66 changes: 63 additions & 3 deletions server/controllers/LibraryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,19 +400,48 @@ class LibraryController {
model: Database.podcastEpisodeModel,
attributes: ['id']
}
},
{
model: Database.bookModel,
attributes: ['id'],
include: [
{
model: Database.bookAuthorModel,
attributes: ['authorId']
},
{
model: Database.bookSeriesModel,
attributes: ['seriesId']
}
]
}
]
})
Logger.info(`[LibraryController] Removed folder "${folder.path}" from library "${req.library.name}" with ${libraryItemsInFolder.length} library items`)
const seriesIds = []
const authorIds = []
for (const libraryItem of libraryItemsInFolder) {
let mediaItemIds = []
if (req.library.isPodcast) {
mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id)
} else {
mediaItemIds.push(libraryItem.mediaId)
if (libraryItem.media.bookAuthors.length) {
authorIds.push(...libraryItem.media.bookAuthors.map((ba) => ba.authorId))
}
if (libraryItem.media.bookSeries.length) {
seriesIds.push(...libraryItem.media.bookSeries.map((bs) => bs.seriesId))
}
}
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.path}"`)
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
}

if (authorIds.length) {
await this.checkRemoveAuthorsWithNoBooks(authorIds)
}
if (seriesIds.length) {
await this.checkRemoveEmptySeries(seriesIds)
}

// Remove folder
Expand Down Expand Up @@ -501,7 +530,7 @@ class LibraryController {
mediaItemIds.push(libraryItem.mediaId)
}
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${req.library.name}"`)
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
}

// Set PlaybackSessions libraryId to null
Expand Down Expand Up @@ -580,6 +609,8 @@ class LibraryController {
* DELETE: /api/libraries/:id/issues
* Remove all library items missing or invalid
*
* @this {import('../routers/ApiRouter')}
*
* @param {LibraryControllerRequest} req
* @param {Response} res
*/
Expand All @@ -605,6 +636,20 @@ class LibraryController {
model: Database.podcastEpisodeModel,
attributes: ['id']
}
},
{
model: Database.bookModel,
attributes: ['id'],
include: [
{
model: Database.bookAuthorModel,
attributes: ['authorId']
},
{
model: Database.bookSeriesModel,
attributes: ['seriesId']
}
]
}
]
})
Expand All @@ -615,15 +660,30 @@ class LibraryController {
}

Logger.info(`[LibraryController] Removing ${libraryItemsWithIssues.length} items with issues`)
const authorIds = []
const seriesIds = []
for (const libraryItem of libraryItemsWithIssues) {
let mediaItemIds = []
if (req.library.isPodcast) {
mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id)
} else {
mediaItemIds.push(libraryItem.mediaId)
if (libraryItem.media.bookAuthors.length) {
authorIds.push(...libraryItem.media.bookAuthors.map((ba) => ba.authorId))
}
if (libraryItem.media.bookSeries.length) {
seriesIds.push(...libraryItem.media.bookSeries.map((bs) => bs.seriesId))
}
}
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" with issue`)
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
}

if (authorIds.length) {
await this.checkRemoveAuthorsWithNoBooks(authorIds)
}
if (seriesIds.length) {
await this.checkRemoveEmptySeries(seriesIds)
}

// Set numIssues to 0 for library filter data
Expand Down
132 changes: 98 additions & 34 deletions server/controllers/LibraryItemController.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,45 @@ class LibraryItemController {
* Optional query params:
* ?hard=1
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async delete(req, res) {
const hardDelete = req.query.hard == 1 // Delete from file system
const libraryItemPath = req.libraryItem.path

const mediaItemIds = req.libraryItem.mediaType === 'podcast' ? req.libraryItem.media.episodes.map((ep) => ep.id) : [req.libraryItem.media.id]
await this.handleDeleteLibraryItem(req.libraryItem.mediaType, req.libraryItem.id, mediaItemIds)
const mediaItemIds = []
const authorIds = []
const seriesIds = []
if (req.libraryItem.isPodcast) {
mediaItemIds.push(...req.libraryItem.media.episodes.map((ep) => ep.id))
} else {
mediaItemIds.push(req.libraryItem.media.id)
if (req.libraryItem.media.metadata.authors?.length) {
authorIds.push(...req.libraryItem.media.metadata.authors.map((au) => au.id))
}
if (req.libraryItem.media.metadata.series?.length) {
seriesIds.push(...req.libraryItem.media.metadata.series.map((se) => se.id))
}
}

await this.handleDeleteLibraryItem(req.libraryItem.id, mediaItemIds)
if (hardDelete) {
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
await fs.remove(libraryItemPath).catch((error) => {
Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error)
})
}

if (authorIds.length) {
await this.checkRemoveAuthorsWithNoBooks(authorIds)
}
if (seriesIds.length) {
await this.checkRemoveEmptySeries(seriesIds)
}

await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
res.sendStatus(200)
}
Expand Down Expand Up @@ -212,15 +236,6 @@ class LibraryItemController {
if (hasUpdates) {
libraryItem.updatedAt = Date.now()

if (seriesRemoved.length) {
// Check remove empty series
Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
await this.checkRemoveEmptySeries(
libraryItem.media.id,
seriesRemoved.map((se) => se.id)
)
}

if (isPodcastAutoDownloadUpdated) {
this.cronManager.checkUpdatePodcastCron(libraryItem)
}
Expand All @@ -232,10 +247,12 @@ class LibraryItemController {
if (authorsRemoved.length) {
// Check remove empty authors
Logger.debug(`[LibraryItemController] Authors were removed from book. Check if authors are now empty.`)
await this.checkRemoveAuthorsWithNoBooks(
libraryItem.libraryId,
authorsRemoved.map((au) => au.id)
)
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({
Expand Down Expand Up @@ -450,6 +467,8 @@ class LibraryItemController {
* Optional query params:
* ?hard=1
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
Expand Down Expand Up @@ -477,14 +496,33 @@ class LibraryItemController {
for (const libraryItem of itemsToDelete) {
const libraryItemPath = libraryItem.path
Logger.info(`[LibraryItemController] (${hardDelete ? 'Hard' : 'Soft'}) deleting Library Item "${libraryItem.media.metadata.title}" with id "${libraryItem.id}"`)
const mediaItemIds = libraryItem.mediaType === 'podcast' ? libraryItem.media.episodes.map((ep) => ep.id) : [libraryItem.media.id]
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
const mediaItemIds = []
const seriesIds = []
const authorIds = []
if (libraryItem.isPodcast) {
mediaItemIds.push(...libraryItem.media.episodes.map((ep) => ep.id))
} else {
mediaItemIds.push(libraryItem.media.id)
if (libraryItem.media.metadata.series?.length) {
seriesIds.push(...libraryItem.media.metadata.series.map((se) => se.id))
}
if (libraryItem.media.metadata.authors?.length) {
authorIds.push(...libraryItem.media.metadata.authors.map((au) => au.id))
}
}
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
if (hardDelete) {
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
await fs.remove(libraryItemPath).catch((error) => {
Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error)
})
}
if (seriesIds.length) {
await this.checkRemoveEmptySeries(seriesIds)
}
if (authorIds.length) {
await this.checkRemoveAuthorsWithNoBooks(authorIds)
}
}

await Database.resetLibraryIssuesFilterData(libraryId)
Expand All @@ -494,48 +532,74 @@ class LibraryItemController {
/**
* POST: /api/items/batch/update
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async batchUpdate(req, res) {
const updatePayloads = req.body
if (!updatePayloads?.length) {
return res.sendStatus(500)
if (!Array.isArray(updatePayloads) || !updatePayloads.length) {
Logger.error(`[LibraryItemController] Batch update failed. Invalid payload`)
return res.sendStatus(400)
}

// Ensure that each update payload has a unique library item id
const libraryItemIds = [...new Set(updatePayloads.map((up) => up?.id).filter((id) => id))]
if (!libraryItemIds.length || libraryItemIds.length !== updatePayloads.length) {
Logger.error(`[LibraryItemController] Batch update failed. Each update payload must have a unique library item id`)
return res.sendStatus(400)
}

// Get all library items to update
const libraryItems = await Database.libraryItemModel.getAllOldLibraryItems({
id: libraryItemIds
})
if (updatePayloads.length !== libraryItems.length) {
Logger.error(`[LibraryItemController] Batch update failed. Not all library items found`)
return res.sendStatus(404)
}

let itemsUpdated = 0

const seriesIdsRemoved = []
const authorIdsRemoved = []

for (const updatePayload of updatePayloads) {
const mediaPayload = updatePayload.mediaPayload
const libraryItem = await Database.libraryItemModel.getOldById(updatePayload.id)
if (!libraryItem) return null
const libraryItem = libraryItems.find((li) => li.id === updatePayload.id)

await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)

let seriesRemoved = []
if (libraryItem.isBook && mediaPayload.metadata?.series) {
const seriesIdsInUpdate = (mediaPayload.metadata?.series || []).map((se) => se.id)
seriesRemoved = libraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
if (libraryItem.isBook) {
if (Array.isArray(mediaPayload.metadata?.series)) {
const seriesIdsInUpdate = mediaPayload.metadata.series.map((se) => se.id)
const seriesRemoved = libraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
seriesIdsRemoved.push(...seriesRemoved.map((se) => se.id))
}
if (Array.isArray(mediaPayload.metadata?.authors)) {
const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id)
const authorsRemoved = libraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id))
authorIdsRemoved.push(...authorsRemoved.map((au) => au.id))
}
}

if (libraryItem.media.update(mediaPayload)) {
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)

if (seriesRemoved.length) {
// Check remove empty series
Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
await this.checkRemoveEmptySeries(
libraryItem.media.id,
seriesRemoved.map((se) => se.id)
)
}

await Database.updateLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
itemsUpdated++
}
}

if (seriesIdsRemoved.length) {
await this.checkRemoveEmptySeries(seriesIdsRemoved)
}
if (authorIdsRemoved.length) {
await this.checkRemoveAuthorsWithNoBooks(authorIdsRemoved)
}

res.json({
success: true,
updates: itemsUpdated
Expand Down
1 change: 1 addition & 0 deletions server/managers/CacheManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class CacheManager {
}

async purgeEntityCache(entityId, cachePath) {
if (!entityId || !cachePath) return []
return Promise.all(
(await fs.readdir(cachePath)).reduce((promises, file) => {
if (file.startsWith(entityId)) {
Expand Down
2 changes: 1 addition & 1 deletion server/objects/LibraryItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ class LibraryItem {
* @returns {Promise<LibraryFile>} null if not saved
*/
async saveMetadata() {
if (this.isSavingMetadata) return null
if (this.isSavingMetadata || !global.MetadataPath) return null

this.isSavingMetadata = true

Expand Down
Loading

0 comments on commit c03f18b

Please sign in to comment.