From 47bccaa374f52b03feebefc860ae8bbd176e2d70 Mon Sep 17 00:00:00 2001 From: deucebucket Date: Tue, 7 Apr 2026 00:32:12 -0500 Subject: [PATCH 1/3] Fix #201: Resolve path normalization mismatch causing SAFETY BLOCK for library root files Windows mapped drives (e.g. R:\) resolve to UNC paths (\\server\share) but library path comparisons in the processing layers used raw config paths without .resolve(), causing the match to fail. The fallback then used parent.parent which went above the library root for loose files. Fixed all 4 locations to resolve both sides before comparison, and added smart fallback that checks if the path is a file before deciding depth. --- app.py | 22 ++++++++++++++----- library_manager/pipeline/layer_ai_queue.py | 12 +++++++--- .../pipeline/layer_audio_credits.py | 10 ++++++--- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/app.py b/app.py index 2c2462d..04d2635 100644 --- a/app.py +++ b/app.py @@ -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 From 737d523180195bf5ef18232fc8ce5cd0eaee7b7b Mon Sep 17 00:00:00 2001 From: deucebucket Date: Tue, 7 Apr 2026 00:34:54 -0500 Subject: [PATCH 2/3] Bump APP_VERSION to beta.144, update README badge and CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ README.md | 2 +- app.py | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) 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 04d2635..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: From 07aa651e7a059401d54ef4bba00afb8cf24cdf24 Mon Sep 17 00:00:00 2001 From: deucebucket Date: Tue, 7 Apr 2026 00:40:38 -0500 Subject: [PATCH 3/3] Fix same path resolution bug in layer_content.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Missed 5th location with identical unresolved relative_to() and parent.parent fallback — caught by vibe-check review. --- library_manager/pipeline/layer_content.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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):