mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-01 05:29:41 +00:00
feat: Add reset metadata capability
This commit is contained in:
parent
98ce898f41
commit
feb87e20d1
5 changed files with 167 additions and 0 deletions
37
artifacts/2026-02-14/implementation_plan.md
Normal file
37
artifacts/2026-02-14/implementation_plan.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Reset Metadata Feature Implementation Plan
|
||||
|
||||
## Objective
|
||||
Implement a "Reset Metadata" feature that allows users to reset a library item's metadata to its original state derived from the file system (tags, folder structure, OPF files), effectively ignoring or removing any manual edits stored in the database or `metadata.json` files.
|
||||
|
||||
## Rationale
|
||||
Users may encounter situations where a library item is matched to the wrong book, or the underlying files have changed (e.g., replaced with a different audiobook version). The existing "ReScan" functionality often preserves existing metadata (especially if `metadata.json` exists) to prevent data loss, which makes it difficult to force a full refresh from the files. A dedicated "Reset" action is needed.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Backend Implementation
|
||||
**File:** `server/controllers/LibraryItemController.js`
|
||||
- **Method:** `resetMetadata(req, res)`
|
||||
- **Logic:**
|
||||
1. Check for update permissions (`req.user.canUpdate`).
|
||||
2. Identify and delete `metadata.json` from the server's metadata directory (`/metadata/items/<id>/metadata.json`).
|
||||
3. Identify and delete `metadata.json` from the item's local folder (if `storeMetadataWithItem` is enabled and it exists).
|
||||
4. Set `media.coverPath` to `null` in the database to force a re-evaluation of the cover image (checking embedded art or `cover.jpg` in folder).
|
||||
5. Trigger `LibraryItemScanner.scanLibraryItem(id)` to re-process the item from scratch using the remaining sources (Audio Tags, OPF, NFO, Folder Structure).
|
||||
6. Return the updated library item.
|
||||
|
||||
**File:** `server/routers/ApiRouter.js`
|
||||
- **Route:** `POST /api/items/:id/reset-metadata`
|
||||
- **Middleware:** Authenticated, Item Access, Update Permission.
|
||||
|
||||
### 2. Frontend Implementation
|
||||
**File:** `client/components/modals/item/tabs/Details.vue`
|
||||
- **UI:** Add a "Reset" button to the "Details" tab in the edit modal, located next to the "ReScan" button.
|
||||
- **Style:** Use `bg-error` (red) to indicate a destructive action.
|
||||
- **Logic:**
|
||||
1. On click, show a confirmation dialog explaining the action.
|
||||
2. Call the `resetMetadata` API endpoint.
|
||||
3. On success, show a toast notification and update the view.
|
||||
|
||||
## Verification
|
||||
- **Test Case:** Open an audiobook with manually edited metadata (e.g., changed title). Click "Reset". The title should revert to what is defined in the audio file tags or folder name.
|
||||
- **Test Case:** Open an audiobook with `metadata.json` present. Click "Reset". The `metadata.json` file should be deleted and metadata refreshed.
|
||||
46
artifacts/2026-02-14/reset_metadata_specification.md
Normal file
46
artifacts/2026-02-14/reset_metadata_specification.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Reset Metadata Feature Specification
|
||||
|
||||
## Overview
|
||||
This document outlines the implementation of the "Reset Metadata" feature, designed to allow users to reset a library item's metadata to its original state derived from the file system (tags, folder structure, OPF files), effectively ignoring or removing any manual edits stored in the database or `metadata.json` files.
|
||||
|
||||
## Date
|
||||
2026-02-14
|
||||
|
||||
## Rationale
|
||||
Users may encounter situations where a library item is matched to the wrong book, or the underlying files have changed (e.g., replaced with a different audiobook version). The existing "ReScan" functionality often preserves existing metadata (especially if `metadata.json` exists) to prevent data loss, which makes it difficult to force a full refresh from the files. A dedicated "Reset" action is needed.
|
||||
|
||||
## Detailed Implementation
|
||||
|
||||
### 1. Backend Implementation
|
||||
|
||||
#### Server Controller: `LibraryItemController.js`
|
||||
- **Method:** `resetMetadata(req, res)`
|
||||
- **Logic:**
|
||||
1. **Permission Check:** Verifies if the user has update permissions (`req.user.canUpdate`). Returns 403 if not.
|
||||
2. **Server Metadata Removal:** Identifies and deletes the `metadata.json` file from the server's metadata directory (`/metadata/items/<id>/metadata.json`) if it exists.
|
||||
3. **Local Metadata Removal:** Identifies and deletes the `metadata.json` file from the item's local folder path (if `storeMetadataWithItem` is enabled and the file exists).
|
||||
4. **Database Update (Cover):** Sets `media.coverPath` to `null` in the database. This forces a re-evaluation of the cover image (checking embedded art or `cover.jpg` in folder) during the subsequent scan.
|
||||
5. **Re-Scan:** Triggers `LibraryItemScanner.scanLibraryItem(id)` to re-process the item from scratch using the remaining sources (Audio Tags, OPF, NFO, Folder Structure).
|
||||
6. **Response:** Returns the updated library item in JSON format.
|
||||
|
||||
#### API Router: `ApiRouter.js`
|
||||
- **Route:** `POST /api/items/:id/reset-metadata`
|
||||
- **Middleware:** Applies authentication, item access checks, and update permission verification (`LibraryItemController.middleware`).
|
||||
- **Handler:** Maps to the `LibraryItemController.resetMetadata` method.
|
||||
|
||||
### 2. Frontend Implementation
|
||||
|
||||
#### Vue Component: `Details.vue`
|
||||
- **Location:** `client/components/modals/item/tabs/Details.vue`
|
||||
- **UI Element:** Added a "Reset" button to the "Details" tab in the edit modal, located next to the existing "ReScan" button.
|
||||
- **Styling:** Used `bg-error` (red) for the button to indicate a destructive action.
|
||||
- **Interactivity:**
|
||||
1. **Click Handler:** `resetMetadata()` triggers a confirmation dialog (`globals/setConfirmPrompt`) explaining the action: "Are you sure you want to reset metadata? This will remove the metadata file and re-scan the item from files."
|
||||
2. **Action Handler:** `runResetMetadata()` calls the `POST /api/items/:id/reset-metadata` endpoint.
|
||||
3. **Feedback:** On success, a toast notification "Metadata reset successfully" is displayed. On failure, an error toast is shown.
|
||||
4. **State Management:** Uses a `resetting` flag to show a loading state on the button while the request is processing.
|
||||
|
||||
## Verification Scenarios
|
||||
1. **Manual Edit Reversion:** Open an audiobook where the title was manually changed. Click "Reset". Verify that the title reverts to the value found in the audio file tags or folder name.
|
||||
2. **Metadata File Deletion:** Open an audiobook that has a `metadata.json` file present. Click "Reset". Verify that the `metadata.json` file is deleted from the filesystem and the metadata is refreshed.
|
||||
3. **Cover Reset:** Ensure that triggering a reset also clears the cover path, causing the scanner to look for a cover again.
|
||||
|
|
@ -13,6 +13,10 @@
|
|||
|
||||
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="isLibraryScanning" color="bg-bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn>
|
||||
|
||||
<ui-tooltip v-if="userIsAdminOrUp && !isFile" text="Reset metadata to file tags" direction="bottom" class="ml-2">
|
||||
<ui-btn :loading="resetting" :disabled="isLibraryScanning" color="bg-error" type="button" class="h-full" small @click.stop.prevent="resetMetadata">Reset</ui-btn>
|
||||
</ui-tooltip>
|
||||
|
||||
<div class="grow" />
|
||||
|
||||
<!-- desktop -->
|
||||
|
|
@ -37,6 +41,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
resettingProgress: false,
|
||||
resetting: false,
|
||||
isScrollable: false,
|
||||
rescanning: false,
|
||||
quickMatching: false
|
||||
|
|
@ -141,6 +146,39 @@ export default {
|
|||
this.rescanning = false
|
||||
})
|
||||
},
|
||||
resetMetadata() {
|
||||
const payload = {
|
||||
message: 'Are you sure you want to reset metadata? This will remove the metadata file and re-scan the item from files.',
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.runResetMetadata()
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
runResetMetadata() {
|
||||
this.resetting = true
|
||||
this.$axios
|
||||
.$post(`/api/items/${this.libraryItemId}/reset-metadata`)
|
||||
.then((data) => {
|
||||
this.resetting = false
|
||||
this.$toast.success('Metadata reset successfully')
|
||||
// Update the library item in the parent component
|
||||
if (data) {
|
||||
this.$emit('submit', { updatePayload: {}, hasChanges: false }) // Just to close or maybe I should trigger an update event
|
||||
// Currently saveAndClose emits 'close'.
|
||||
// We probably want to just update the local view or close.
|
||||
// If I close, the user sees the updated card in the library.
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to reset metadata', error)
|
||||
this.$toast.error('Failed to reset metadata')
|
||||
this.resetting = false
|
||||
})
|
||||
},
|
||||
async saveAndClose() {
|
||||
const wasUpdated = await this.save()
|
||||
if (wasUpdated !== null) this.$emit('close')
|
||||
|
|
|
|||
|
|
@ -755,6 +755,51 @@ class LibraryItemController {
|
|||
res.json(matchResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/items/:id/reset-metadata
|
||||
*
|
||||
* @param {LibraryItemControllerRequest} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async resetMetadata(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to reset metadata without permission`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (global.MetadataPath) {
|
||||
const metadataPath = Path.join(global.MetadataPath, 'items', req.libraryItem.id, 'metadata.json')
|
||||
if (await fs.pathExists(metadataPath)) {
|
||||
Logger.info(`[LibraryItemController] Removing metadata file at "${metadataPath}"`)
|
||||
await fs.remove(metadataPath)
|
||||
}
|
||||
}
|
||||
|
||||
if (req.libraryItem.path && !req.libraryItem.isFile) {
|
||||
const localMetadataPath = Path.join(req.libraryItem.path, 'metadata.json')
|
||||
if (await fs.pathExists(localMetadataPath)) {
|
||||
Logger.info(`[LibraryItemController] Removing local metadata file at "${localMetadataPath}"`)
|
||||
await fs.remove(localMetadataPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear cover path to force re-scan of cover
|
||||
if (req.libraryItem.media.coverPath) {
|
||||
req.libraryItem.media.coverPath = null
|
||||
await req.libraryItem.media.save()
|
||||
}
|
||||
|
||||
// Trigger a scan ensuring we don't rely on cache/timestamps if possible
|
||||
// scanLibraryItem checks mtime but since we deleted metadata.json which might have been the source,
|
||||
// the "comparison" logic in BookScanner should now fallback to other sources (tags/folder).
|
||||
// If those sources yield different data than DB, it updates.
|
||||
const result = await LibraryItemScanner.scanLibraryItem(req.libraryItem.id)
|
||||
|
||||
// Respond with updated item
|
||||
await req.libraryItem.reload()
|
||||
res.json(req.libraryItem.toOldJSONExpanded())
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/items/batch/delete
|
||||
* Batch delete library items. Will delete from database and file system if hard delete is requested.
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ class ApiRouter {
|
|||
this.router.patch('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.updateCover.bind(this))
|
||||
this.router.delete('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.removeCover.bind(this))
|
||||
this.router.post('/items/:id/match', LibraryItemController.middleware.bind(this), LibraryItemController.match.bind(this))
|
||||
this.router.post('/items/:id/reset-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.resetMetadata.bind(this))
|
||||
this.router.post('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
|
||||
this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this))
|
||||
this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue