Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to Library Manager will be documented in this file.

## [0.9.0-beta.146] - 2026-04-07

### Added

- **Issue #110: Folder triage UI** - Dashboard now shows messy/garbage folder counts in an
info banner. Library view displays triage badges (Messy/Garbage) on affected books. Added
Settings toggle to enable/disable folder triage. Triage data now included in "all" library
view API responses. Split push corrections feature to #205 (blocked on Skaldleita).

---

## [0.9.0-beta.145] - 2026-04-07

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.145-blue.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-0.9.0--beta.146-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)

Expand Down
21 changes: 15 additions & 6 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- Multi-provider AI (Gemini, OpenRouter, Ollama)
"""

APP_VERSION = "0.9.0-beta.145"
APP_VERSION = "0.9.0-beta.146"
GITHUB_REPO = "deucebucket/library-manager" # Your GitHub repo

# Versioning Guide:
Expand Down Expand Up @@ -737,7 +737,7 @@
try:
with open(ERROR_REPORTS_PATH, 'r') as f:
reports = json.load(f)
except:

Check failure on line 740 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (E722)

app.py:740:13: E722 Do not use bare `except`
reports = []

# Add new report (keep last 100 reports to avoid file bloat)
Expand All @@ -761,7 +761,7 @@
try:
with open(ERROR_REPORTS_PATH, 'r') as f:
return json.load(f)
except:

Check failure on line 764 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (E722)

app.py:764:9: E722 Do not use bare `except`
return []
return []

Expand Down Expand Up @@ -1716,7 +1716,7 @@
continue
result = call_gemini(prompt, merged_config)
if result:
logger.info(f"[PROVIDER CHAIN] Success with gemini")

Check failure on line 1719 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1719:33: F541 f-string without any placeholders help: Remove extraneous `f` prefix
return result

elif provider == 'openrouter':
Expand All @@ -1725,13 +1725,13 @@
continue
result = call_openrouter(prompt, merged_config)
if result:
logger.info(f"[PROVIDER CHAIN] Success with openrouter")

Check failure on line 1728 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1728:33: F541 f-string without any placeholders help: Remove extraneous `f` prefix
return result

elif provider == 'ollama':
result = call_ollama(prompt, merged_config)
if result:
logger.info(f"[PROVIDER CHAIN] Success with ollama")

Check failure on line 1734 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1734:33: F541 f-string without any placeholders help: Remove extraneous `f` prefix
return result

else:
Expand Down Expand Up @@ -1833,7 +1833,7 @@
return result
elif result and result.get('transcript'):
# Got transcript but no match - still useful, return for potential AI fallback
logger.info(f"[AUDIO CHAIN] BookDB returned transcript only")

Check failure on line 1836 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1836:37: F541 f-string without any placeholders help: Remove extraneous `f` prefix
return result
elif result is None and attempt < max_retries - 1:
# Connection might be down, wait and retry
Expand Down Expand Up @@ -2165,11 +2165,11 @@
device = "cuda"
# int8 works on all CUDA devices including GTX 1080 (compute 6.1)
# float16 only works on newer GPUs (compute 7.0+)
logger.info(f"[WHISPER] Using CUDA GPU acceleration (10x faster)")

Check failure on line 2168 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:2168:29: F541 f-string without any placeholders help: Remove extraneous `f` prefix
else:
logger.info(f"[WHISPER] Using CPU (no CUDA GPU detected)")

Check failure on line 2170 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:2170:29: F541 f-string without any placeholders help: Remove extraneous `f` prefix
except ImportError:
logger.info(f"[WHISPER] Using CPU (ctranslate2 not available)")

Check failure on line 2172 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:2172:25: F541 f-string without any placeholders help: Remove extraneous `f` prefix

_whisper_model = WhisperModel(model_name, device=device, compute_type=compute_type)
_whisper_model_name = model_name
Expand Down Expand Up @@ -2376,7 +2376,7 @@
if sample_path and os.path.exists(sample_path):
try:
os.unlink(sample_path)
except:

Check failure on line 2379 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (E722)

app.py:2379:13: E722 Do not use bare `except`
pass

return result
Expand Down Expand Up @@ -4924,6 +4924,7 @@
validation_counts = {'valid': 0, 'invalid': 0, 'skipped': 0} # Issue #110: File validation stats
issues_found = {} # path -> list of issues
triage_counts = {'clean': 0, 'messy': 0, 'garbage': 0} # Issue #110: Folder triage stats
triage_enabled = config.get('enable_folder_triage', True) # Issue #110: Folder triage toggle

# Issue #110: Check ffmpeg availability once at scan start
ffmpeg_available, ffmpeg_msg = check_ffmpeg_available()
Expand Down Expand Up @@ -5111,7 +5112,7 @@
# Issue #132: Resolve path to prevent duplicates
flat_path = str(author_dir.resolve())
# Issue #110: Triage folder name quality
flat_triage = triage_folder(author)
flat_triage = triage_folder(author) if triage_enabled else 'clean'

checked += 1

Expand Down Expand Up @@ -5293,7 +5294,7 @@

checked += 1
# Issue #110: Triage folder name quality
series_book_triage = triage_folder(book_title)
series_book_triage = triage_folder(book_title) if triage_enabled else 'clean'

# Check if already tracked
c.execute('''SELECT id, status, profile, user_locked, attempt_count,
Expand Down Expand Up @@ -5370,7 +5371,7 @@
checked += 1

# Issue #110: Triage folder name quality
folder_triage_result = triage_folder(title)
folder_triage_result = triage_folder(title) if triage_enabled else 'clean'
triage_counts[folder_triage_result] = triage_counts.get(folder_triage_result, 0) + 1
if folder_triage_result != 'clean':
logger.info(f"Folder triage: {folder_triage_result} - {title[:60]}")
Expand Down Expand Up @@ -7032,6 +7033,11 @@
c.execute("SELECT COUNT(*) as count FROM books WHERE validation_status = 'invalid'")
validation_failed_count = c.fetchone()['count']

# Issue #110: Count folder triage categories
c.execute("SELECT folder_triage, COUNT(*) as count FROM books WHERE folder_triage != 'clean' GROUP BY folder_triage")
triage_rows = c.fetchall()
triage_counts = {row['folder_triage']: row['count'] for row in triage_rows}

# Get recent history (use LEFT JOIN in case book was deleted)
c.execute('''SELECT h.*, b.path FROM history h
LEFT JOIN books b ON h.book_id = b.id
Expand All @@ -7054,6 +7060,7 @@
verified_count=verified_count,
pending_fixes=pending_fixes,
validation_failed_count=validation_failed_count,
triage_counts=triage_counts,
recent_history=recent_history,
daily_stats=daily_stats,
config=config,
Expand Down Expand Up @@ -7314,6 +7321,7 @@
config['enable_file_validation'] = 'enable_file_validation' in request.form
config['min_audio_duration_seconds'] = int(request.form.get('min_audio_duration_seconds', 600))
config['min_audio_file_size_mb'] = int(request.form.get('min_audio_file_size_mb', 1))
config['enable_folder_triage'] = 'enable_folder_triage' in request.form

# Provider chain settings - parse comma-separated values into lists
audio_chain_str = request.form.get('audio_provider_chain', 'bookdb,gemini').strip()
Expand Down Expand Up @@ -9817,7 +9825,7 @@
else: # 'all' - show everything from books table
order = build_order_by(BOOK_SORT_COLS, 'current_author ASC, current_title ASC')
c.execute('''SELECT b.id, b.path, b.current_author, b.current_title, b.status,
b.user_locked, b.confidence, b.media_type,
b.user_locked, b.confidence, b.media_type, b.folder_triage,
h.old_author, h.old_title, h.new_author, h.new_title,
h.old_path, h.new_path, h.status as history_status,
h.fixed_at, h.error_message
Expand Down Expand Up @@ -9856,7 +9864,8 @@
'status': history_status or book_status,
'confidence': row['confidence'] or 0,
'user_locked': row['user_locked'] == 1,
'media_type': row['media_type'] or 'audiobook'
'media_type': row['media_type'] or 'audiobook',
'folder_triage': row['folder_triage'] or 'clean'
}
# Overlay history data when present
if history_status:
Expand Down
3 changes: 3 additions & 0 deletions library_manager/hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@
# === Post-Processing Hooks ===
'post_processing': 'Run external scripts or webhooks after a book is successfully renamed. Use for M4B conversion, Audiobookshelf library scans, Discord notifications, backup scripts, etc. Hook failures never undo a successful rename.',

# === Folder Triage ===
'folder_triage': 'Classifies folder names as clean, messy (scene tags, torrent markers), or garbage (hashes, generic names). Messy and garbage folders skip path-based hints and rely on audio/metadata identification only.',

# === Plugins ===
'custom_api_sources': 'Add your own book metadata APIs as processing layers. Each source queries an HTTP endpoint and maps the response into the book profile system.',
'python_plugins': 'Drop-in Python plugins for advanced users. Place a plugin folder in /data/plugins/ with a manifest.json and a Python file extending BasePlugin. Plugins are auto-discovered on startup.',
Expand Down
21 changes: 21 additions & 0 deletions templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@
</div>
{% endif %}

{% if triage_counts is defined and (triage_counts.get('messy', 0) > 0 or triage_counts.get('garbage', 0) > 0) %}
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info py-2 mb-0 d-flex align-items-center" role="alert">
<i class="bi bi-funnel me-2"></i>
<small>
{{ _('Folder triage:') }}
{% if triage_counts.get('messy', 0) > 0 %}
<strong>{{ triage_counts['messy'] }}</strong> {{ _('messy') }}
{% endif %}
{% if triage_counts.get('garbage', 0) > 0 %}
{% if triage_counts.get('messy', 0) > 0 %}, {% endif %}
<strong>{{ triage_counts['garbage'] }}</strong> {{ _('garbage') }}
{% endif %}
{{ _('folder(s) detected. Path hints skipped for these &mdash; relying on audio/metadata only.') }}
</small>
</div>
</div>
</div>
{% endif %}

<!-- Toast container for action feedback -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1080;">
<div id="action-toast" class="toast align-items-center border-0" role="alert" aria-live="assertive" aria-atomic="true">
Expand Down
8 changes: 8 additions & 0 deletions templates/library.html
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,14 @@ <h5 class="modal-title"><i class="bi bi-pencil"></i> Edit Book Metadata</h5>
if (item.user_locked) {
badge = '<span class="badge bg-info status-badge" title="User-locked - protected from automatic changes"><i class="bi bi-lock-fill"></i></span> ' + badge;
}

// Add folder triage badge for messy/garbage folders
if (item.folder_triage === 'messy') {
badge += ' <span class="badge bg-warning text-dark status-badge" title="Messy folder name (scene tags, torrent markers). Path hints skipped - using audio/metadata only."><i class="bi bi-exclamation-triangle"></i> Messy</span>';
} else if (item.folder_triage === 'garbage') {
badge += ' <span class="badge bg-danger status-badge" title="Garbage folder name (hash, numbers, placeholder). Path hints skipped - using audio/metadata only."><i class="bi bi-trash"></i> Garbage</span>';
}

return badge;
}

Expand Down
11 changes: 11 additions & 0 deletions templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,17 @@ <h5 class="mb-3"><i class="bi bi-play-circle"></i> Processing</h5>
</div>
</div>

<!-- Issue #110: Folder Triage -->
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" name="enable_folder_triage"
id="enable_folder_triage" {% if config.enable_folder_triage is not defined or config.enable_folder_triage %}checked{% endif %}>
<label class="form-check-label" for="enable_folder_triage">
<i class="bi bi-funnel text-info"></i> <strong>{{ _('Folder Triage') }}</strong>
<span class="badge bg-success">{{ _('Local') }}</span>
<br><small class="text-muted">{{ _('Classify folder names as clean/messy/garbage. Messy and garbage folders skip path parsing and rely on audio/metadata only.') }}</small>
</label>
</div>

<hr class="my-2">

<div class="form-check form-switch mb-3">
Expand Down
Loading