This commit is contained in:
Michael Marcucci 2026-02-23 11:59:34 +01:00 committed by GitHub
commit c8946d563f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 100 additions and 2 deletions

View file

@ -150,6 +150,29 @@ class ToolsController {
res.sendStatus(200)
}
/**
* POST: /api/tools/batch/update-metadata-files
* Start batch request to update all metadata files
*
* @this import('../routers/ApiRouter')
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async updateAllItemMetadata(req, res) {
if (!req.user.isAdminOrUp) {
Logger.warn(`Non-admin user "${req.user.username}" other than admin attempted to batch scan library items`)
return res.sendStatus(403)
}
const libraryItems = await Database.libraryItemModel.findAll()
for (const libraryItem of libraryItems) {
await libraryItem.saveMetadataFile()
}
res.sendStatus(200)
}
/**
*
* @param {RequestWithUser} req

View file

@ -16,3 +16,4 @@ Please add a record of every database migration that you create to this file. Th
| v2.19.1 | v2.19.1-copy-title-to-library-items | Copies title and titleIgnorePrefix to the libraryItems table, creates update triggers and indices |
| v2.19.4 | v2.19.4-improve-podcast-queries | Adds numEpisodes to podcasts, adds podcastId to mediaProgresses, copies podcast title to libraryItems |
| v2.20.0 | v2.20.0-improve-author-sort-queries | Adds AuthorNames(FirstLast\|LastFirst) to libraryItems to improve author sort queries |
| v2.31.1 | v2.31.1-update-metadata-json-with-id | Adds ids to the locally stored metadata.json to help file moves keep track of the items |

View file

@ -0,0 +1,48 @@
/**
* @typedef MigrationContext
* @property {import('sequelize').QueryInterface} queryInterface - a sequelize QueryInterface object.
* @property {import('../Logger')} logger - a Logger object.
*
* @typedef MigrationOptions
* @property {MigrationContext} context - an object containing the migration context.
*/
const migrationVersion = '2.31.1'
const migrationName = `${migrationVersion}-update-metadata-json-with-id`
const loggerPrefix = `[${migrationVersion} migration]`
/**
* This upward migration creates a sessions table and apiKeys table.
*
* @param {MigrationOptions} options - an object containing the migration context.
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
*/
async function up({ context: { logger } }) {
// Upwards migration script
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
// Re-save all metadata json files, the id field will be added
const libraryItems = await Database.libraryItemModel.findAll()
for (const libraryItem of libraryItems) {
await libraryItem.saveMetadataFile()
}
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
}
/**
* This downward migration script removes the sessions table and apiKeys table.
*
* @param {MigrationOptions} options - an object containing the migration context.
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
*/
async function down({ context: { logger } }) {
// Downward migration script
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
// Re-save all metadata json files, the id field will be removed
const libraryItems = await Database.libraryItemModel.findAll()
for (const libraryItem of libraryItems) {
await libraryItem.saveMetadataFile()
}
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
}
module.exports = { up, down }

View file

@ -574,6 +574,7 @@ class LibraryItem extends Model {
let jsonObject = {}
if (this.mediaType === 'book') {
jsonObject = {
id: this.id,
tags: mediaExpanded.tags || [],
chapters: mediaExpanded.chapters?.map((c) => ({ ...c })) || [],
title: mediaExpanded.title,
@ -598,6 +599,7 @@ class LibraryItem extends Model {
}
} else {
jsonObject = {
id: this.id,
tags: mediaExpanded.tags || [],
title: mediaExpanded.title,
author: mediaExpanded.author,
@ -647,7 +649,7 @@ class LibraryItem extends Model {
}
}
Logger.debug(`[LibraryItem] Saved metadata for "${this.media.title}" file to "${metadataFilePath}"`)
Logger.debug(`[LibraryItem] Saved metadata for "${mediaExpanded.title}" file to "${metadataFilePath}"`)
return metadataLibraryFile
})

View file

@ -298,6 +298,7 @@ class ApiRouter {
this.router.delete('/tools/item/:id/encode-m4b', ToolsController.middleware.bind(this), ToolsController.cancelM4bEncode.bind(this))
this.router.post('/tools/item/:id/embed-metadata', ToolsController.middleware.bind(this), ToolsController.embedAudioFileMetadata.bind(this))
this.router.post('/tools/batch/embed-metadata', ToolsController.middleware.bind(this), ToolsController.batchEmbedMetadata.bind(this))
this.router.post('/tools/batch/update-metadata-files', ToolsController.middleware.bind(this), ToolsController.updateAllItemMetadata.bind(this))
//
// RSS Feed Routes (Admin and up)

View file

@ -821,6 +821,7 @@ class BookScanner {
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
const jsonObject = {
id: libraryItem.id,
tags: libraryItem.media.tags || [],
chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [],
title: libraryItem.media.title,

View file

@ -14,6 +14,7 @@ const LibraryItemScanner = require('./LibraryItemScanner')
const LibraryScan = require('./LibraryScan')
const LibraryItemScanData = require('./LibraryItemScanData')
const Task = require('../objects/Task')
const abmetadataGenerator = require('../utils/generators/abmetadataGenerator')
class LibraryScanner {
constructor() {
@ -574,7 +575,7 @@ class LibraryScanner {
let updatedLibraryItemDetails = {}
if (!existingLibraryItem) {
const isSingleMedia = isSingleMediaFile(fileUpdateGroup, itemDir)
existingLibraryItem = (await findLibraryItemByItemToItemInoMatch(library.id, fullPath)) || (await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia)) || (await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir]))
existingLibraryItem = (await findLibraryItemByItemToItemInoMatch(library.id, fullPath)) || (await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia)) || (await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir])) || (await findLibraryItemByItemToMetadata(fullPath, isSingleMedia))
if (existingLibraryItem) {
// Update library item paths for scan
existingLibraryItem.path = fullPath
@ -709,3 +710,23 @@ async function findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingle
if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with inode matching one of "${itemFileInos.join(',')}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
}
async function findLibraryItemByItemToMetadata(fullPath, isSingleMedia) {
if (isSingleMedia) return null
const metadataText = await fileUtils.readTextFile(Path.join(fullPath, 'metadata.json'))
if (!metadataText) return null
const abMetadata = abmetadataGenerator.parseJson(metadataText) || {}
// check if metadata id exists in the database
const existingLibraryItem = await Database.libraryItemModel.getExpandedById(abMetadata.id)
if (existingLibraryItem) {
Logger.debug(`[LibraryScanner] Found library item with metadata id matching one of "${abMetadata.id}" at path "${existingLibraryItem.path}"`)
for (const { metadata } of existingLibraryItem.getLibraryFiles())
if (await fs.pathExists(metadata.path)) {
Logger.debug(`[LibraryScanner] Conflicting library files exist "${metadata.path}"`)
return null
}
}
return existingLibraryItem
}

View file

@ -421,6 +421,7 @@ class PodcastScanner {
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
const jsonObject = {
id: libraryItem.id,
tags: libraryItem.media.tags || [],
title: libraryItem.media.title,
author: libraryItem.media.author,