The InboxState class manages state tracking and change detection for audiobooks in the inbox folder.
src.lib.inbox_state
A singleton state manager that tracks which books are in the inbox, their processing status, and detects changes via file hashing.
from src.lib.inbox_state import InboxState
# Get singleton instance
inbox = InboxState()
# Scan for books
inbox.scan()
# Get all pending books
pending = inbox.pendingScan the inbox directory for new or changed books.
inbox = InboxState()
inbox.scan()
# Scan without syncing failed books
inbox.scan(skip_failed_sync=True)Parameters:
skip_failed_sync: bool = False- Skip syncing failed books
Side Effects:
- Updates internal book tracking
- Detects new books
- Detects file changes via hashing
- Updates
_last_scantimestamp
Get an InboxItem by key, path, or Audiobook object.
# Get by key
item = inbox.get("book-key-12345")
# Get by path
item = inbox.get(Path("/inbox/MyBook"))
# Get by audiobook
item = inbox.get(audiobook)Parameters:
key_path_hash_or_book: str | Path | Audiobook | None
Returns: InboxItem | None
Set or update an InboxItem in state.
from src.lib.inbox_item import InboxItem
item = InboxItem(Path("/inbox/MyBook"))
inbox.set(item, status="processing")
# Update with timestamp
inbox.set(item, last_updated=time.time())Parameters:
key_path_or_book: str | Path | Audiobook | InboxItemstatus: InboxItemStatus | None- Item statuslast_updated: float | None- Last update timestamp
Check if an item exists in inbox state.
if inbox.has("book-key-12345"):
print("Book is tracked")
if inbox.has(Path("/inbox/MyBook")):
print("Book exists")Parameters:
key_path_or_book: str | Path | Audiobook
Returns: bool
Remove an item from inbox state.
inbox.remove("book-key-12345")
inbox.remove(Path("/inbox/MyBook"))
inbox.remove(audiobook)Parameters:
key_path_or_book: str | Path | Audiobook
Mark a book as currently being processed.
inbox.mark_as_processing(audiobook)Parameters:
book: Audiobook | InboxItem
Mark a book as failed with optional error message.
inbox.mark_as_failed(audiobook, error="Corrupted audio file")Parameters:
book: Audiobook | InboxItemerror: str | None- Error message
| Property | Type | Description |
|---|---|---|
pending |
list[InboxItem] |
Books waiting to be processed |
processing |
list[InboxItem] |
Books currently being processed |
failed |
list[InboxItem] |
Books that failed processing |
completed |
list[InboxItem] |
Successfully processed books |
| Property | Type | Description |
|---|---|---|
pending_count |
int |
Number of pending books |
processing_count |
int |
Number of books being processed |
failed_count |
int |
Number of failed books |
completed_count |
int |
Number of completed books |
| Property | Type | Description |
|---|---|---|
ready |
bool |
Whether state is ready for processing |
loop_counter |
int |
Number of processing loops completed |
banner_printed |
bool |
Whether startup banner was printed |
last_run_start |
float |
Timestamp of last scan |
Represents a single audiobook item in the inbox.
| Property | Type | Description |
|---|---|---|
path |
Path |
Path to audiobook |
key |
str |
Unique identifier (hash) |
status |
InboxItemStatus |
Current status |
error |
str | None |
Error message (if failed) |
last_updated |
float |
Last update timestamp |
is_maybe_series_parent |
bool |
Possibly a series parent directory |
Convert InboxItem to Audiobook object.
item = inbox.get("book-key")
book = item.to_audiobook()Returns: Audiobook
Reload item state from filesystem.
item.reload()Valid status values:
InboxItemStatus = Literal["pending", "processing", "completed", "failed"]from src.lib.inbox_state import InboxState
# Get state manager
inbox = InboxState()
# Scan inbox
inbox.scan()
# Check what's pending
print(f"Found {inbox.pending_count} books to process")
for item in inbox.pending:
print(f" - {item.path.name}")from src.lib.inbox_state import InboxState
inbox = InboxState()
inbox.scan()
for item in inbox.pending:
# Mark as processing
inbox.mark_as_processing(item)
try:
# Convert book
book = item.to_audiobook()
success = convert_book(book)
if success:
# Remove from tracking (completed)
inbox.remove(item)
else:
# Mark as failed
inbox.mark_as_failed(item, error="Conversion failed")
except Exception as e:
inbox.mark_as_failed(item, error=str(e))inbox = InboxState()
inbox.scan()
if inbox.failed_count > 0:
print(f"Found {inbox.failed_count} failed books:")
for item in inbox.failed:
print(f" - {item.path.name}: {item.error}")from src.lib.config import cfg
inbox = InboxState()
inbox.scan()
# Filter by pattern
if cfg.MATCH_FILTER:
import re
pattern = re.compile(cfg.MATCH_FILTER)
matching = [item for item in inbox.pending
if pattern.search(item.path.name)]
print(f"Found {len(matching)} matching books")from pathlib import Path
from src.lib.inbox_state import InboxState
from src.lib.inbox_item import InboxItem
inbox = InboxState()
# Add a book manually
item = InboxItem(Path("/inbox/MyBook"))
inbox.set(item, status="pending")
# Update status
inbox.set(item, status="processing")
# Check if exists
if inbox.has(item):
print("Book is tracked")
# Remove
inbox.remove(item)State persists in memory during application runtime but is rebuilt from filesystem on each scan.
# State is rebuilt on scan
inbox.scan()
# Previous processing state is maintained
assert inbox.loop_counter > 0InboxState extends Hasher to detect file changes.
- Initial Scan: Hash all audio files in each book directory
- Subsequent Scans: Re-hash and compare
- Changes Detected: If hash differs, files have changed
- Stability Wait: Wait
WAIT_TIMEbefore processing
| Property | Type | Description |
|---|---|---|
hash |
str |
Current hash of directory |
hash_age |
float |
Seconds since last hash update |
is_stable |
bool |
Whether files haven't changed recently |
item = inbox.get("book-key")
# Check if files are stable
if item.is_stable:
print("Files haven't changed, safe to process")
else:
print(f"Files changed {item.hash_age}s ago, waiting...")from src.lib.inbox_state import InboxState
def process_inbox():
inbox = InboxState()
# Scan for books
inbox.scan()
# Get pending books
for item in inbox.pending:
# Skip if not stable
if not item.is_stable:
continue
# Convert to audiobook
book = item.to_audiobook()
# Mark as processing
inbox.mark_as_processing(item)
try:
# Process
convert_book(book)
inbox.remove(item) # Success
except Exception as e:
inbox.mark_as_failed(item, error=str(e))Decorator that triggers a scan if hash is stale.
from src.lib.inbox_state import scanner
@scanner
def my_method(self):
# Scan will run if hash_age > SLEEP_TIME
...Decorator that ensures a scan has been performed.
from src.lib.inbox_state import requires_scan
@requires_scan
def my_method(self):
# Guaranteed to have scanned inbox
...- Book list is cached until next
scan() - Hashes are cached until files change
- Use
@cached_propertyfor expensive operations
- Controlled by
SLEEP_TIME(default 10s) - Full filesystem scan on each
scan() - Consider increasing for large libraries
- Keeps all InboxItems in memory
- Minimal per-book overhead (~1KB)
- Scales to thousands of books
- No persistence: State lost on restart
- No retry logic: Failed books stay failed (Phase 1.2)
- Sequential processing: One book at a time (Phase 3.1)
- State persistence (JSON/SQLite)
- Retry with backoff (Phase 1.2)
- Parallel processing (Phase 3.1)
- Metrics tracking (Phase 1.5)
- Audiobook API - Audiobook data model
- Config API - Configuration management
- Architecture Overview - System design