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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

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

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

### Added

- **Issue #203: Plugin system documentation and discoverability** - Added Python drop-in
plugin guide with manifest.json and BasePlugin interface examples directly in the Plugins
settings tab. Added secrets management card explaining secrets.json usage for Docker and
bare metal. Added ready-to-use API configurations for Google Books and Open Library.
Shipped example-logger plugin to `examples/plugins/` with comprehensive README covering
plugin creation, manifest fields, BasePlugin interface, configuration, and behavior.
Added new hint entries for plugin-related tooltips.

---

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

### Fixed
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.144-blue.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-0.9.0--beta.145-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
2 changes: 1 addition & 1 deletion 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.144"
APP_VERSION = "0.9.0-beta.145"
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
121 changes: 121 additions & 0 deletions examples/plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Example Plugins

Drop-in Python plugins for Library Manager. Copy a plugin folder to `/data/plugins/` (Docker) or `plugins/` (bare metal) and restart.

## example-logger

A minimal template plugin that logs each book it processes. Use as a starting point for your own plugins.

**Install:**
```bash
cp -r example-logger /data/plugins/
# Restart Library Manager
```

## Creating Your Own Plugin

Each plugin needs two files in its own folder:

### manifest.json

```json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "What it does",
"type": "layer",
"entry_point": "layer.py",
"class_name": "MyPlugin",
"default_order": 35,
"requires_config": [],
"requires_secrets": ["my_api_key"],
"permissions": {
"network": ["api.example.com"],
"database": "read"
}
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `id` | Yes | Unique identifier (alphanumeric, hyphens, underscores) |
| `name` | Yes | Display name |
| `version` | No | Semver version string |
| `description` | No | What the plugin does |
| `type` | Yes | Must be `layer` |
| `entry_point` | Yes | Python file containing the plugin class |
| `class_name` | No | Class name to load (auto-detected if omitted) |
| `default_order` | No | Pipeline position 1-999 (default: 50). Lower = runs earlier |
| `requires_config` | No | Config keys your plugin reads |
| `requires_secrets` | No | Secret keys your plugin needs (stored in secrets.json) |
| `permissions.network` | No | Domains your plugin connects to |
| `permissions.database` | No | `read` or `write` |

### layer.py

```python
from library_manager.plugin_loader import BasePlugin

class MyPlugin(BasePlugin):
name = "My Plugin"
description = "What it does"
version = "1.0.0"

def setup(self, config, secrets):
"""Called once on startup. Store config/secrets you need."""
self.api_key = secrets.get('my_api_key')

def can_process(self, book_data):
"""Return True to process this book, False to skip."""
return True

def process(self, book_data):
"""Main logic. Return dict of matched fields, or empty dict for no match.

book_data contains:
- current_title: Current title (from path or prior identification)
- current_author: Current author
- current_narrator: Current narrator (if known)
- path: Full filesystem path to the book
- book_id: Database ID

Return any of: title, author, narrator, series, series_num, year, language
"""
return {}

def teardown(self):
"""Called on shutdown. Clean up resources."""
pass
```

## Plugin Behavior

- Plugins run with a **30 second timeout** per `process()` call
- Default confidence weight: **60** (configurable via `plugin_configs` in config.json)
- Auto-disabled after **5 consecutive failures** (re-enable from Plugin Health dashboard)
- Plugins never crash the app - exceptions are caught and logged
- Results feed into the book profile system alongside built-in sources

## Configuration

Per-plugin settings in `config.json`:

```json
{
"plugin_configs": {
"my-plugin": {
"timeout": 30,
"custom_setting": "value"
}
}
}
```

Secrets in `secrets.json`:

```json
{
"my_api_key": "your-key-here"
}
```
78 changes: 78 additions & 0 deletions examples/plugins/example-logger/layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Example plugin for Library Manager.

This is a minimal plugin that demonstrates the BasePlugin interface.
It logs each book it sees and returns empty results (no modifications).

To use this as a template:
1. Copy this directory to /data/plugins/your-plugin-name/
2. Edit manifest.json with your plugin's metadata
3. Implement process() with your logic
4. Restart Library Manager

The plugin loader will discover and load your plugin automatically.
"""

import logging

# Import BasePlugin from the plugin loader
from library_manager.plugin_loader import BasePlugin

logger = logging.getLogger(__name__)


class ExampleLoggerPlugin(BasePlugin):
"""A simple plugin that logs book information.

This demonstrates:
- setup() for one-time initialization
- can_process() for filtering books
- process() for the main logic
- teardown() for cleanup
"""

name = "Example Logger"
description = "Logs book data for debugging"
version = "1.0.0"

def setup(self, config, secrets):
"""Store config for later use."""
self.log_level = config.get('log_level', 'info')
self.books_seen = 0
logger.info("[ExamplePlugin] Setup complete")

def can_process(self, book_data):
"""Process all books."""
return True

def process(self, book_data):
"""Log the book data and return empty (no changes).

In a real plugin, you would:
1. Extract info from book_data (title, author, path, etc.)
2. Query your data source (API, database, file, etc.)
3. Return a dict with matched fields

Example return for a match:
return {
'title': 'The Corrected Title',
'author': 'Correct Author Name',
'narrator': 'Narrator Name',
}
"""
self.books_seen += 1
title = book_data.get('current_title', 'Unknown')
author = book_data.get('current_author', 'Unknown')

logger.info(
f"[ExamplePlugin] Book #{self.books_seen}: "
f"'{title}' by {author}"
)

# Return empty dict = no changes (this is just a logger)
return {}

def teardown(self):
"""Log summary on shutdown."""
logger.info(
f"[ExamplePlugin] Shutting down. Saw {self.books_seen} books total."
)
16 changes: 16 additions & 0 deletions examples/plugins/example-logger/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"id": "example-logger",
"name": "Example Logger Plugin",
"version": "1.0.0",
"description": "A minimal example plugin that logs book data and returns empty results. Use as a template for building your own plugins.",
"type": "layer",
"entry_point": "layer.py",
"class_name": "ExampleLoggerPlugin",
"default_order": 35,
"requires_config": [],
"requires_secrets": [],
"permissions": {
"network": [],
"database": "read"
}
}
6 changes: 6 additions & 0 deletions library_manager/hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@

# === 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.',

# === 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.',
'plugin_health': 'Monitor the health and performance of your custom API sources and Python plugins. Plugins are auto-disabled after 5 consecutive failures to protect your processing pipeline.',
'plugin_secrets': 'API keys and passwords are stored in secrets.json (not config.json) so they are never exposed in backups or logs. Add your key as a named entry, then reference the key name here.',
}


Expand Down
Loading
Loading