error handling

This commit is contained in:
Thomas 2026-03-20 19:49:27 -06:00
parent b4fded0d40
commit 5657899749
2 changed files with 167 additions and 33 deletions

View file

@ -79,8 +79,16 @@ export default {
newPath = item.media?.metadata?.title || 'Unknown Podcast' newPath = item.media?.metadata?.title || 'Unknown Podcast'
} else { } else {
const parts = [] const parts = []
if (item.media?.metadata?.authorName) parts.push(item.media.metadata.authorName) // Use only the first author
if (item.media?.metadata?.seriesName) parts.push(item.media.metadata.seriesName) const authors = item.media?.metadata?.authors
if (authors && authors.length > 0) {
parts.push(authors[0].name)
}
if (item.media?.metadata?.seriesName) {
// Remove sequence number from series name (e.g., "Series Name #1" -> "Series Name")
const seriesNameWithoutSequence = item.media.metadata.seriesName.replace(/\s*#\d+$/, '')
parts.push(seriesNameWithoutSequence)
}
if (item.media?.metadata?.title) parts.push(item.media.metadata.title) if (item.media?.metadata?.title) parts.push(item.media.metadata.title)
newPath = parts.join(' / ') || 'Unknown Item' newPath = parts.join(' / ') || 'Unknown Item'
} }
@ -140,10 +148,15 @@ export default {
this.processing = false this.processing = false
this.$store.commit('setProcessingBatch', false) this.$store.commit('setProcessingBatch', false)
if (result.successCount > 0 && result.errorCount === 0) { if (result.successCount > 0 && result.warningCount === 0 && result.errorCount === 0) {
this.$toast.success(this.$getString('MessageBatchReorganizeSuccess', [result.successCount])) this.$toast.success(this.$getString('MessageBatchReorganizeSuccess', [result.successCount]))
} else if (result.successCount > 0 && result.errorCount > 0) { } else if ((result.successCount > 0 || result.warningCount > 0) && result.errorCount === 0) {
this.$toast.warning(this.$getString('MessageBatchReorganizePartial', [result.successCount, result.errorCount])) const message = result.warningCount > 0
? this.$getString('MessageBatchReorganizePartial', [result.successCount, result.warningCount])
: this.$getString('MessageBatchReorganizeSuccess', [result.successCount])
this.$toast.warning(message)
} else if (result.errorCount > 0) {
this.$toast.error(this.$getString('MessageBatchReorganizePartial', [result.successCount + result.warningCount, result.errorCount]))
} else { } else {
this.$toast.error('Failed to reorganize files') this.$toast.error('Failed to reorganize files')
} }

View file

@ -1207,6 +1207,55 @@ class LibraryItemController {
Logger.info(`[LibraryItemController] Reorganizing item ${itemId}`) Logger.info(`[LibraryItemController] Reorganizing item ${itemId}`)
// Get library folder early for error directory path
const library = await Database.libraryModel.findByIdWithFolders(libraryId)
if (!library || !library.libraryFolders || !library.libraryFolders.length) {
return { success: false, error: 'Library not found' }
}
const libraryFolder = library.libraryFolders.find((f) => currentPath.startsWith(f.path))
if (!libraryFolder) {
return { success: false, error: 'Library folder not found' }
}
// Refresh metadata before reorganizing
let hasMatchWarning = false
Logger.info(`[LibraryItemController] Refreshing metadata for item ${itemId}`)
try {
const matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, {
overrideDetails: true
})
if (matchResult.updated) {
Logger.info(`[LibraryItemController] Metadata updated for item ${itemId}`)
} else if (matchResult.warning) {
hasMatchWarning = true
Logger.warn(`[LibraryItemController] Metadata match warning for item ${itemId}: ${matchResult.warning}`)
}
} catch (err) {
Logger.warn(`[LibraryItemController] Failed to refresh metadata for item ${itemId}: ${err.message}`)
// Continue anyway - use existing metadata
}
// If match failed, move to error directory
if (hasMatchWarning) {
try {
const errorDirPath = Path.join(libraryFolder.path, '__Reorganize_Errors')
await fs.ensureDir(errorDirPath)
const errorPath = Path.join(errorDirPath, Path.basename(currentPath))
Logger.warn(`[LibraryItemController] Moving unmatched item to error directory: ${currentPath} -> ${errorPath}`)
await fs.move(currentPath, errorPath, { overwrite: false })
// Update database
await Database.libraryItemModel.update({ path: errorPath }, { where: { id: itemId } })
Logger.info(`[LibraryItemController] Moved unmatched item ${itemId} to error directory`)
return { success: true, warning: 'Item moved to error directory - no metadata match found' }
} catch (err) {
return { success: false, error: `Failed to move unmatched item to error directory: ${err.message}` }
}
}
// Reload media with authors and series for books // Reload media with authors and series for books
let media = libraryItem.media let media = libraryItem.media
if (isBook) { if (isBook) {
@ -1236,27 +1285,19 @@ class LibraryItemController {
return { success: false, error: 'Media not found' } return { success: false, error: 'Media not found' }
} }
// Get library folder
const library = await Database.libraryModel.findByIdWithFolders(libraryId)
if (!library || !library.libraryFolders || !library.libraryFolders.length) {
return { success: false, error: 'Library not found' }
}
const libraryFolder = library.libraryFolders.find((f) => currentPath.startsWith(f.path))
if (!libraryFolder) {
return { success: false, error: 'Library folder not found' }
}
// Build new path // Build new path
const parts = [] const parts = []
if (isPodcast) { if (isPodcast) {
parts.push(media.title) parts.push(media.title)
} else if (isBook) { } else if (isBook) {
if (media.authorName) { // Use only the first author
parts.push(media.authorName) if (media.authors && media.authors.length > 0) {
parts.push(media.authors[0].name)
} }
if (media.seriesName) { if (media.seriesName) {
parts.push(media.seriesName) // Remove sequence number from series name (e.g., "Series Name #1" -> "Series Name")
const seriesNameWithoutSequence = media.seriesName.replace(/\s*#\d+$/, '')
parts.push(seriesNameWithoutSequence)
} }
parts.push(media.title) parts.push(media.title)
} }
@ -1274,30 +1315,100 @@ class LibraryItemController {
return { success: false, error: 'Current directory does not exist' } return { success: false, error: 'Current directory does not exist' }
} }
// Check if target already exists with different item // Check if target path has conflicts
const targetExists = await fs.pathExists(newPath) const targetExists = await fs.pathExists(newPath)
if (targetExists) { if (targetExists) {
// Check if this item already owns the target directory
const existingItem = await Database.libraryItemModel.findOne({ where: { path: newPath } }) const existingItem = await Database.libraryItemModel.findOne({ where: { path: newPath } })
if (existingItem && existingItem.id !== itemId) { if (existingItem) {
return { success: false, error: 'Target directory already contains a different item' } if (existingItem.id === itemId) {
// Item already reorganized to this location - skip
Logger.info(`[LibraryItemController] Item ${itemId} already at target path, skipping`)
return { success: true }
} else {
// Different item owns this directory - move it to error directory
try {
const errorDirPath = Path.join(libraryFolder.path, '__Reorganize_Errors')
await fs.ensureDir(errorDirPath)
const existingItemPath = existingItem.path
const errorPath = Path.join(errorDirPath, Path.basename(existingItemPath))
Logger.warn(`[LibraryItemController] Moving conflicting item to error directory: ${existingItemPath} -> ${errorPath}`)
await fs.move(existingItemPath, errorPath, { overwrite: false })
// Update the conflicting item's path in database
await Database.libraryItemModel.update({ path: errorPath }, { where: { id: existingItem.id } })
Logger.info(`[LibraryItemController] Moved conflicting item ${existingItem.id} to error directory`)
} catch (err) {
return { success: false, error: `Failed to move conflicting item: ${err.message}` }
}
}
}
// Target exists but isn't a registered item - check if it's empty
try {
const targetContents = await fs.readdir(newPath)
if (targetContents.length > 0) {
// Move conflicting directory to error directory
try {
const errorDirPath = Path.join(libraryFolder.path, '__Reorganize_Errors')
await fs.ensureDir(errorDirPath)
const errorPath = Path.join(errorDirPath, Path.basename(newPath) + '_conflict')
Logger.warn(`[LibraryItemController] Moving conflicting directory to error: ${newPath} -> ${errorPath}`)
await fs.move(newPath, errorPath, { overwrite: false })
Logger.info(`[LibraryItemController] Moved conflicting directory to error path`)
} catch (err) {
return { success: false, error: `Cannot handle conflicting directory: ${err.message}` }
}
}
} catch (err) {
return { success: false, error: `Cannot access target directory: ${err.message}` }
} }
} }
// Create target directory // Create target directory
try {
await fs.ensureDir(newPath) await fs.ensureDir(newPath)
} catch (err) {
return { success: false, error: `Cannot create target directory: ${err.message}` }
}
// Read and move files // Read and move files
try {
const files = await fs.readdir(currentPath) const files = await fs.readdir(currentPath)
Logger.info(`[LibraryItemController] Moving ${files.length} files for item ${itemId}`) Logger.info(`[LibraryItemController] Moving ${files.length} files for item ${itemId}`)
for (const file of files) { for (const file of files) {
const src = Path.join(currentPath, file) const src = Path.join(currentPath, file)
const dst = Path.join(newPath, file) const dst = Path.join(newPath, file)
try {
await fs.move(src, dst, { overwrite: true }) await fs.move(src, dst, { overwrite: true })
} catch (err) {
return { success: false, error: `Failed to move file ${file}: ${err.message}` }
}
}
} catch (err) {
return { success: false, error: `Failed to read source directory: ${err.message}` }
} }
// Update database // Update database
try {
await Database.libraryItemModel.update({ path: newPath }, { where: { id: itemId } }) await Database.libraryItemModel.update({ path: newPath }, { where: { id: itemId } })
} catch (err) {
// Try to rollback files
try {
const movedFiles = await fs.readdir(newPath)
for (const file of movedFiles) {
const src = Path.join(newPath, file)
const dst = Path.join(currentPath, file)
await fs.move(src, dst, { overwrite: true })
}
} catch (rollbackErr) {
Logger.error(`[LibraryItemController] Failed to rollback files: ${rollbackErr.message}`)
}
return { success: false, error: `Database update failed: ${err.message}` }
}
// Clean up old directory // Clean up old directory
try { try {
@ -1358,15 +1469,23 @@ class LibraryItemController {
// Process in background // Process in background
Logger.info(`[LibraryItemController] Starting batch reorganize for ${itemsToReorganize.length} items`) Logger.info(`[LibraryItemController] Starting batch reorganize for ${itemsToReorganize.length} items`)
let successCount = 0 let successCount = 0
let warningCount = 0
let errorCount = 0 let errorCount = 0
const errors = [] const errors = []
const warnings = []
for (const libraryItem of itemsToReorganize) { for (const libraryItem of itemsToReorganize) {
// Get the controller instance to call helper methods // Get the controller instance to call helper methods
const result = await controller.reorganizeFilesForItem(libraryItem) const result = await controller.reorganizeFilesForItem(libraryItem)
if (result.success) { if (result.success) {
if (result.warning) {
warningCount++
warnings.push({ id: libraryItem.id, title: libraryItem.media.title, warning: result.warning })
Logger.warn(`[LibraryItemController] Reorganized with warning: ${libraryItem.media.title} - ${result.warning}`)
} else {
successCount++ successCount++
Logger.info(`[LibraryItemController] Successfully reorganized: ${libraryItem.media.title}`) Logger.info(`[LibraryItemController] Successfully reorganized: ${libraryItem.media.title}`)
}
} else { } else {
errorCount++ errorCount++
errors.push({ id: libraryItem.id, title: libraryItem.media.title, error: result.error }) errors.push({ id: libraryItem.id, title: libraryItem.media.title, error: result.error })
@ -1374,12 +1493,14 @@ class LibraryItemController {
} }
} }
Logger.info(`[LibraryItemController] Batch reorganize complete: ${successCount} succeeded, ${errorCount} failed`) Logger.info(`[LibraryItemController] Batch reorganize complete: ${successCount} succeeded, ${warningCount} moved to error dir, ${errorCount} failed`)
// Notify user of completion // Notify user of completion
SocketAuthority.clientEmitter(req.user.id, 'batch_reorganize_complete', { SocketAuthority.clientEmitter(req.user.id, 'batch_reorganize_complete', {
successCount, successCount,
warningCount,
errorCount, errorCount,
warnings,
errors, errors,
total: itemsToReorganize.length total: itemsToReorganize.length
}) })