Allow books to be merged

This commit is contained in:
Tiberiu Ichim 2026-02-12 19:57:04 +02:00
parent fc97b10f58
commit 56eca37304
9 changed files with 615 additions and 25 deletions

View file

@ -17,64 +17,54 @@ The source database was created in a Docker container environment with hardcoded
### 1. libraryFolders Table
**Table**: `libraryFolders`
**Columns with paths**: `path`
**Table**: `libraryFolders` **Columns with paths**: `path`
**Sample data**:
| id | path | libraryId |
|----|------|-----------|
| 9f980819-1371-4c8f-9e7d-a6cbe9ae1ba7 | /audiobooks | a04cbf28-7eb6-4c87-b3e3-421ad8b35923 |
| 43bf8c8d-07b6-4828-848f-8bb1e3dcca04 | /libraries/romance | dad4448d-77c2-481e-9212-1ffcb4272932 |
**Sample data**: | id | path | libraryId | |----|------|-----------| | 9f980819-1371-4c8f-9e7d-a6cbe9ae1ba7 | /audiobooks | a04cbf28-7eb6-4c87-b3e3-421ad8b35923 | | 43bf8c8d-07b6-4828-848f-8bb1e3dcca04 | /libraries/books | dad4448d-77c2-481e-9212-1ffcb4272932 |
**Migration strategy**:
- Map each unique library folder path to a corresponding local path
- Preserve the folder structure within each library
### 2. libraryItems Table
**Table**: `libraryItems`
**Columns with paths**: `path`, `relPath`
**Table**: `libraryItems` **Columns with paths**: `path`, `relPath`
**Sample data**:
| id | path | relPath | title |
|----|------|---------|-------|
| 6ec745f9-608e-4556-8f78-b36e2682069b | /audiobooks/A Beginner's Guide to Forever.m4b | A Beginner's Guide to Forever.m4b | A Beginner's Guide to Forever |
**Sample data**: | id | path | relPath | title | |----|------|---------|-------| | 6ec745f9-608e-4556-8f78-b36e2682069b | /audiobooks/A Beginner's Guide to Forever.m4b | A Beginner's Guide to Forever.m4b | A Beginner's Guide to Forever |
**Migration strategy**:
- The `path` column contains full absolute paths from Docker root
- The `relPath` column contains paths relative to library folder (less likely to need migration)
- Update `path` to use local library folder mappings
### 3. books Table
**Table**: `books`
**Columns with paths**: `coverPath`
**Table**: `books` **Columns with paths**: `coverPath`
**Sample data**:
| id | coverPath |
|----|-----------|
| 68f4e9ca-c8e9-46a1-b667-7b0a409dd72d | /metadata/items/6ec745f9-608e-4556-8f78-b36e2682069b/cover.jpg |
**Sample data**: | id | coverPath | |----|-----------| | 68f4e9ca-c8e9-46a1-b667-7b0a409dd72d | /metadata/items/6ec745f9-608e-4556-8f78-b36e2682069b/cover.jpg |
**Migration strategy**:
- `coverPath` points to `/metadata/items/{libraryItemId}/cover.jpg`
- May need remapping if local `metadata` directory differs from Docker
### 4. feeds Table
**Table**: `feeds`
**Columns with paths**: `serverAddress`, `feedURL`, `imageURL`, `siteURL`, `coverPath`
**Table**: `feeds` **Columns with paths**: `serverAddress`, `feedURL`, `imageURL`, `siteURL`, `coverPath`
**Migration strategy**:
- `serverAddress`: The Docker container's server URL (e.g., `http://audiobookshelf:8080`)
- `feedURL`, `imageURL`, `siteURL`: URLs containing the server address
- `coverPath`: Local file path to feed cover images
### 5. settings Table
**Table**: `settings`
**Key with paths**: `server-settings` (JSON value)
**Table**: `settings` **Key with paths**: `server-settings` (JSON value)
**Path settings in JSON**:
- `backupPath`: Docker path (e.g., `/metadata/backups`)
- Potentially others in nested JSON structure
@ -86,7 +76,7 @@ The source database was created in a Docker container environment with hardcoded
# path-mapping.yaml
libraries:
/audiobooks: /home/user/audiobooks
/libraries/romance: /home/user/libraries/romance
/libraries/books: /home/user/libraries/books
metadata:
source: /metadata
target: /home/user/audiobookshelf/metadata
@ -111,23 +101,27 @@ server:
## Implementation Phases
### Phase 1: Path Discovery
- [ ] Scan all tables for path-like values
- [ ] Identify all unique paths requiring migration
- [ ] Categorize paths by type (library folders, metadata, URLs)
### Phase 2: Mapping Configuration
- [ ] Create mapping configuration file
- [ ] Define library folder path mappings
- [ ] Define metadata path mappings
- [ ] Define server URL mappings
### Phase 3: Migration Script
- [ ] Implement path update logic for each table
- [ ] Implement URL update logic for feeds
- [ ] Implement settings path updates
- [ ] Add transaction safety with rollback capability
### Phase 4: Validation
- [ ] Run validation checks on migrated database
- [ ] Generate migration report
- [ ] Test database with local Audiobookshelf instance

View file

@ -0,0 +1,63 @@
# Recursive Library Structure Fixer Specification
**Date:** 2026-02-11
**Status:** Implemented
## Overview
This document specifies the behavior of the Python utility (`scripts/reorganize_library.py`) designed to crawl and reorganize deeply nested audiobook library structures into a flat, Audiobookshelf (ABS) compatible format.
## Problem Statement
The ABS scanner performs optimally with shallow hierarchies. Deeply nested structures (e.g., `Author / Series / Book / files`) cause metadata misclassification (Author/Series shifting) and inefficiency. Additionally, "Collection" folders often contain single intro files that cause the scanner to swallow all sub-books into one item.
## Migration Strategy: Top-Level Flattening
The script reorganizes the library so that every book occupies a single folder directly under the library root.
### 1. Primary Naming Pattern
The target structure is:
`LibraryRoot / {CleanAuthor} - {BookPathSegments} / {Files}`
**Refined Naming Logic:**
1. **Author Cleaning**: The first folder segment is treated as the Author. Common suffixes are stripped to avoid clutter:
* `" Collection"`, `" Anthology"`, `" Series"`, `" Books"`, `" Works"`, `" Complete"`
2. **Redundancy Check**: If the rest of the path (the "Book" part) already starts with the Author's name (case-insensitive), the Author prefix is **not** added again.
3. **Deduplication**: Adjacent identical segments in the final name are merged (e.g., `Book - Book` becomes `Book`).
### 2. Detection Logic (Leaf Node Identification)
A directory is identified as a "Book Folder" if:
1. It contains audio files (`.mp3`, `.m4b`, etc.) AND has **no subdirectories**.
2. It contains audio files AND subdirectories, but **has more than 1 audio file**.
* *Reason*: Prevents "Collection" folders with a single `intro.mp3` from being treated as books, allowing the script to traverse deeper to find the actual books.
* *Exception*: If subdirectories are named `CD 1`, `Disc 1`, etc., it is treated as a book regardless of file count.
3. It contains **only** `CD`/`Disc` subdirectories (even if no audio files are in the root).
### 3. Transformation Examples
| Source Path (Relative to Root) | Target Folder Name | Reason |
| :--- | :--- | :--- |
| `Stephen Baxter Collection / Manifold / Origin` | `Stephen Baxter - Manifold - Origin` | "Collection" stripped; "Manifold" preserved. |
| `Abbie Rushton / Unspeakable` | `Abbie Rushton - Unspeakable` | Standard Author - Title. |
| `Dungeon Crawler Carl / Book 1 Dungeon Crawler Carl` | `Dungeon Crawler Carl - Book 1` | Deduplication of "Dungeon Crawler Carl". |
| `Fiction / Author / Book` | `Fiction - Author - Book` | "Fiction" treated as Author context if deeper than 2 levels. |
## Python Script Interface
### Location
`scripts/reorganize_library.py`
### Usage
```bash
python3 scripts/reorganize_library.py /path/to/library [options]
```
### Arguments
- `path`: Root directory of the library to scan.
- `--dry-run`: **(Recommended)** Print all planned moves without executing them.
- `--verbose`: Enable debug logging (shows every folder checked and why it was accepted/rejected).
### Technical Constraints
- **Atomic Moves**: Uses `shutil.move` for safety.
- **Conflict Handling**: Skips the move if a folder with the target name already exists.
- **Cleanup**: Automatically removes empty parent directories after moving their contents.

View file

@ -0,0 +1,129 @@
# Specification: Merge Books Feature
## Implementation Plan
# Merge Books Feature Implementation Plan
## Goal Description
Allow users to select multiple books (e.g., individual mp3 files improperly imported as separate books) and "Merge" them into a single book. This involves moving all files to a single folder and updating the database to reflect a single library item containing all files.
## Proposed Changes
### Backend
#### [NEW] `server/controllers/LibraryItemController.js`
- Implement `batchMerge(req, res)` method.
- **Validation**: Ensure user has update/delete permissions. Check all items belong to the same library and are books.
- **Primary Item Selection**: Use the first selected item as the "primary" item (the one that will act as the container).
- **Target Folder**: Determine the target folder path.
- If the primary item is already in a suitable folder (e.g. `Author/Title`), use it.
- If the items are in the root or disorganized, create a new folder based on the primary item's metadata (Author/Title).
- **File Operations**:
- Iterate through all _other_ selected items.
- Move their media files (audio, ebook, cover) to the target folder.
- Handle filename collisions (append counter if needed).
- Update `LibraryItem` entries? No, we will rescind the primary item.
- **Database Updates**:
- Delete the _other_ `LibraryItem` records from the database.
- Trigger a rescan of the primary item's folder to pick up the new files and update tracks/chapters.
- Clean up empty source folders of the moved items.
#### [MODIFY] `server/routers/ApiRouter.js`
- Add `POST /items/batch/merge` route mapped to `LibraryItemController.batchMerge`.
### Frontend
#### [MODIFY] `client/components/app/Appbar.vue`
- Update `contextMenuItems` computed property.
- Add "Merge" option when:
- User has update/delete permissions.
- Library is a "book" library.
- Multiple items are selected (`selectedMediaItems.length > 1`).
- Implement `batchMerge()` method to call the API.
- Add confirmation dialog explaining what will happen.
## Verification Plan
### Manual Verification
1. **Setup**:
- Add multiple individual mp3 files to the root of a library (or separate folders) so they show up as separate books.
- Ensure they have some metadata (Title/Author) or add it manually.
2. **Execution**:
- Go to the library in the web UI.
- Select the multiple "books" (mp3 files).
- Click the Multi-select "x items selected" bar if not already open (it opens automatically on selection).
- Click the Context Menu (3 dots) or find the "Merge" button (to be added).
- Select "Merge".
- Confirm the dialog.
3. **Result Validation**:
- Verify that the separate books disappear.
- Verify that one single book remains.
- Open the remaining book and check "Files" tab. It should contain all the mp3 files.
- Check the filesystem: Ensure all mp3 files are now in the same folder.
- Check metadata: Ensure the book plays correctly.
## Walkthrough
# Merge Books Feature Walkthrough
I have implemented the "Merge Books" feature, which allows users to combine multiple library items (specifically books) into a single library item. This is particularly useful for fixing issues where individual audio files were imported as separate books.
## Changes
### Backend
- **`server/controllers/LibraryItemController.js`**: Added `batchMerge` method.
- Validates that all items are books and from the same library.
- Identifies a "primary" item (the first one selected).
- Creates a new folder for the book if the primary item is a file in the root.
- Moves all media files from the other selected items into the primary item's folder.
- Deletes the old library items for the moved files.
- Triggers a scan of the primary item to update metadata and tracks.
- Cleans up empty authors and series.
- **`server/routers/ApiRouter.js`**: Added `POST /api/items/batch/merge` route.
### Frontend
- **`client/components/app/Appbar.vue`**: Added "Merge" option to the multi-select context menu.
- Enabled only when multiple books are selected.
- Shows a confirmation dialog before proceeding.
- **`client/strings/en-us.json`**: Added localization strings for the new feature.
## Verification
### Automated Tests
I created a new test file `test/server/controllers/LibraryItemController_merge.test.js` to verify the backend logic.
To run the verification test:
```bash
npx mocha test/server/controllers/LibraryItemController_merge.test.js --exit
```
**Test Results:**
```
LibraryItemController Merge
batchMerge
✔ should merge two file-based items into a new folder
1 passing (113ms)
```
### Manual Verification Steps
1. **Identify Split Books**: Find a set of books in your library that should be a single book (e.g., "Chapter 1", "Chapter 2" showing as separate books).
2. **Select Books**: Enable multi-select and click on the books you want to merge.
3. **Click Merge**: In the top URI bar, click the menu button (3 dots) and select **Merge**.
4. **Confirm**: Accept the confirmation dialog ("Are you sure you want to merge...").
5. **Verify**:
- The separate books should disappear.
- A single book should remain (based on the first selected book).
- Open the book and check the "Files" tab. It should contain all the audio files.
- Play the book to ensure tracks are ordered and playable.