diff --git a/CHANGELOG.md b/CHANGELOG.md index 9debc1f..9871940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to Library Manager will be documented in this file. +## [0.9.0-beta.144] - 2026-04-07 + +### Fixed + +- **Issue #201: SAFETY BLOCK error for files in library root** - Fixed path normalization + mismatch where Windows mapped drives (e.g. `R:\`) resolve to UNC paths but config paths + were compared without `.resolve()`, causing library matching to fail. Fixed fallback logic + that assumed 2-level directory structure (`parent.parent`), which went above the library + root for loose files. Applied fix across all 4 path-matching locations in `app.py`, + `layer_ai_queue.py`, and `layer_audio_credits.py`. + +--- + ## [0.9.0-beta.143] - 2026-03-21 ### Changed diff --git a/README.md b/README.md index 62617a4..bd1e126 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Smart Audiobook Library Organizer with Multi-Source Metadata & AI Verification** -[![Version](https://img.shields.io/badge/version-0.9.0--beta.143-blue.svg)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-0.9.0--beta.144-blue.svg)](CHANGELOG.md) [![Docker](https://img.shields.io/badge/docker-ghcr.io-blue.svg)](https://ghcr.io/deucebucket/library-manager) [![License](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](LICENSE) diff --git a/app.py b/app.py index 2c2462d..548fc42 100644 --- a/app.py +++ b/app.py @@ -11,7 +11,7 @@ - Multi-provider AI (Gemini, OpenRouter, Ollama) """ -APP_VERSION = "0.9.0-beta.143" +APP_VERSION = "0.9.0-beta.144" GITHUB_REPO = "deucebucket/library-manager" # Your GitHub repo # Versioning Guide: @@ -11725,17 +11725,22 @@ def api_manual_match(): # If not from watch folder, find which library it belongs to if lib_path is None: + old_path_resolved = Path(old_path).resolve() for lp in config.get('library_paths', []): - lp_path = Path(lp) + lp_path = Path(lp).resolve() try: - Path(old_path).relative_to(lp_path) + old_path_resolved.relative_to(lp_path) lib_path = lp_path break except ValueError: continue if lib_path is None: - lib_path = Path(old_path).parent.parent + old_p = Path(old_path) + if old_p.is_file(): + lib_path = old_p.parent + else: + lib_path = old_p.parent.parent # Detect language from title for multi-language naming lang_code = detect_title_language(new_title) if new_title else None @@ -11894,17 +11899,22 @@ def api_edit_book(): return jsonify({'success': False, 'error': 'No output folder configured for watch folder items'}) else: # Normal library item - find which library it belongs to + old_path_resolved = Path(old_path).resolve() for lp in config.get('library_paths', []): - lp_path = Path(lp) + lp_path = Path(lp).resolve() try: - Path(old_path).relative_to(lp_path) + old_path_resolved.relative_to(lp_path) lib_path = lp_path break except ValueError: continue if lib_path is None: - lib_path = Path(old_path).parent.parent + old_p = Path(old_path) + if old_p.is_file(): + lib_path = old_p.parent + else: + lib_path = old_p.parent.parent # Detect language from title for multi-language naming lang_code = detect_title_language(new_title) if new_title else None diff --git a/library_manager/pipeline/layer_ai_queue.py b/library_manager/pipeline/layer_ai_queue.py index 06a7987..aa294d3 100644 --- a/library_manager/pipeline/layer_ai_queue.py +++ b/library_manager/pipeline/layer_ai_queue.py @@ -450,10 +450,11 @@ def process_queue( # (Don't assume 2-level structure - series_grouping uses 3 levels) lib_path = None is_from_watch_folder = False + old_path_resolved = old_path.resolve() for lp in config.get('library_paths', []): - lp_path = Path(lp) + lp_path = Path(lp).resolve() try: - old_path.relative_to(lp_path) + old_path_resolved.relative_to(lp_path) lib_path = lp_path break except ValueError: @@ -479,7 +480,12 @@ def process_queue( # Fallback if not found in configured libraries if lib_path is None: - lib_path = old_path.parent.parent + # For loose files in library root, parent is the library itself + # Only go up 2 levels for normal Author/Title structure + if old_path.is_file(): + lib_path = old_path.parent + else: + lib_path = old_path.parent.parent logger.warning(f"Book path {old_path} not under any configured library, guessing lib_path={lib_path}") # Detect language for multi-language naming diff --git a/library_manager/pipeline/layer_audio_credits.py b/library_manager/pipeline/layer_audio_credits.py index 50bf93c..9d6008f 100644 --- a/library_manager/pipeline/layer_audio_credits.py +++ b/library_manager/pipeline/layer_audio_credits.py @@ -219,10 +219,11 @@ def process_layer_3_audio( else: # Audio suggests different values - build new path lib_path = None + book_path_resolved = book_path.resolve() for lp in config.get('library_paths', []): - lp_path = Path(lp) + lp_path = Path(lp).resolve() try: - book_path.relative_to(lp_path) + book_path_resolved.relative_to(lp_path) lib_path = lp_path break except ValueError: @@ -240,7 +241,10 @@ def process_layer_3_audio( logger.debug(f"Watch folder path check failed: {e}") if lib_path is None: - lib_path = book_path.parent.parent + if book_path.is_file(): + lib_path = book_path.parent + else: + lib_path = book_path.parent.parent logger.warning(f"[LAYER 3] Book path {book_path} not under any configured library, guessing lib_path={lib_path}") # Detect language for multi-language naming diff --git a/library_manager/pipeline/layer_content.py b/library_manager/pipeline/layer_content.py index b408a92..210f8fe 100644 --- a/library_manager/pipeline/layer_content.py +++ b/library_manager/pipeline/layer_content.py @@ -113,16 +113,21 @@ def process_layer_4_content( # Build new path lib_path = None + book_path_resolved = book_path.resolve() for lp in config.get('library_paths', []): + lp_path = Path(lp).resolve() try: - book_path.relative_to(Path(lp)) - lib_path = Path(lp) + book_path_resolved.relative_to(lp_path) + lib_path = lp_path break except ValueError: continue if not lib_path: - lib_path = book_path.parent.parent + if book_path.is_file(): + lib_path = book_path.parent + else: + lib_path = book_path.parent.parent # Build target path with series grouping if applicable if new_series and config.get('series_grouping', True):