MediaDatabase¶
Overview¶
The package-native media_db layer provides a robust content database tailored for media content and related metadata. It is designed with a focus on multi-instance database management, where each MediaDatabase object corresponds to a distinct database (SQLite by default; PostgreSQL is supported via a backend abstraction). A key feature is its internal handling of synchronization metadata and Full-Text Search (FTS) updates, simplifying client-side logic.
Each MediaDatabase instance requires a client_id upon initialization, which is used to attribute all changes made through that instance. The library automatically logs create, update, delete, link, and unlink operations to an internal sync_log table. This log is intended for consumption by external synchronization mechanisms to keep multiple database instances consistent.
Key Features¶
- Instance-Based: Each
MediaDatabaseobject encapsulates a connection and operations for a specific database file (or in-memory SQLite). - Client ID Tracking: All data modifications are attributed to a
client_idprovided duringDatabaseinitialization. - Internal Sync Logging: Automatically records changes to a
sync_logtable, facilitating external synchronization processes. This includes changes to main entities and relationships (e.g.,MediaKeywordslinks). - Internal FTS Management: Transparently manages updates to associated FTS5 tables (
media_fts,keyword_fts) and maintains aclaims_ftsindex via triggers. - Schema Versioning: The database bootstraps and migrates to the current schema version automatically (current version: 5).
- Backend Abstraction: Uses a backend factory to support SQLite (default) and PostgreSQL. Selection is driven by
[Database]settings inConfig_Files/config.txtorTLDW_CONTENT_*environment variables (see Backend Selection Examples). - Thread-Safety: Utilizes
threading.localto provide thread-local database connections, ensuring safe concurrent access from multiple threads. - Soft Deletes: Implements a soft-delete pattern (
deleted=1) for most entities, allowing for data recovery and synchronization of deletions. - Optimistic Concurrency Control: Employs a
versioncolumn in key tables. Update operations typically require the current version of the record, and increment it, failing if a concurrent modification has occurred (ConflictError). - Transaction Management: Provides a
transaction()context manager for atomic database operations. - Comprehensive CRUD Operations: Offers methods for creating, reading, updating, and deleting media, keywords, transcripts, document versions (with
safe_metadata), claims, chunking templates, and media chunks. - Standalone Utility Functions: Includes functions that operate on a
MediaDatabaseinstance for tasks like searching, fetching related data, and database maintenance.
Dependencies¶
- Python Standard Library:
sqlite3,threading,uuid,datetime,pathlib,json,hashlib,logging,math,contextlib,typing. - Third-Party:
PyYAML(optional, used byimport_obsidian_note_to_dbfor parsing frontmatter). Ensure it's installed if this functionality is used.- PostgreSQL backend (optional): if enabled, requires the corresponding driver per backend implementation.
Custom Exceptions¶
The library defines several custom exceptions to provide more specific error information:
DatabaseError(Exception): Base exception for all database-related errors within this library.SchemaError(DatabaseError): Raised for schema version mismatches or errors during schema migration/initialization.InputError(ValueError): Custom exception for input validation errors (e.g., missing required parameters, invalid formats).ConflictError(DatabaseError): Indicates a conflict due to concurrent modification, typically when an entity'sversionnumber does not match the expected value during an update or delete operation.entity(Optional[str]): The name of the entity/table where the conflict occurred.identifier(Optional[Any]): The ID or UUID of the record that caused the conflict.
MediaDatabase Class¶
The core of the library, managing all interactions with a specific content database (SQLite by default; PostgreSQL supported via backend).
class MediaDatabase:
_CURRENT_SCHEMA_VERSION = 5
# ... schema SQL definitions and FTS/trigger setup
Initialization¶
def __init__(self, db_path: Union[str, Path], client_id: str, *, backend: Optional[DatabaseBackend] = None, config: Optional[ConfigParser] = None)
- Purpose: Initializes a
MediaDatabaseinstance, connecting to the specified database (SQLite file path or':memory:'). It sets up thread-local connection management and ensures the database schema is initialized/migrated to the current version. If a backend is not provided, a backend is resolved from configuration (defaults to SQLite; can select PostgreSQL when enabled). - Args:
db_path (Union[str, Path]): Path to the database (SQLite file path or':memory:').client_id (str): A unique identifier for the client application or process instance making changes to this database. This ID is recorded in thesync_logand on modified records.backend (Optional[DatabaseBackend]): Explicit backend instance (primarily for tests/DI).config (Optional[ConfigParser]): Optional configuration to help resolve backend when not provided.
- Raises:
ValueError: Ifclient_idis empty orNone.DatabaseError: If initialization or schema setup fails.SchemaError: If the existing database schema version is newer than what the code supports, or if migration fails.
Backend selection:
- Default is SQLite.
- To select a backend via environment for content DBs, prefer the TLDW_CONTENT_* variables (handled by content_backend.py). Examples below.
Backend selection examples:
1) Configure PostgreSQL via DSN
export TLDW_CONTENT_DB_BACKEND=postgresql
export TLDW_CONTENT_PG_DSN="postgresql://tldw_user:secret@localhost:5432/tldw_content?sslmode=prefer"
2) Configure PostgreSQL via discrete parameters
export TLDW_CONTENT_DB_BACKEND=postgresql
export TLDW_CONTENT_PG_HOST=localhost
export TLDW_CONTENT_PG_PORT=5432
export TLDW_CONTENT_PG_DATABASE=tldw_content
export TLDW_CONTENT_PG_USER=tldw_user
export TLDW_CONTENT_PG_PASSWORD=secret
export TLDW_CONTENT_PG_SSLMODE=prefer
Then instantiate normally (backend is auto-resolved):
from tldw_Server_API.app.core.DB_Management.media_db.native_class import MediaDatabase
from tldw_Server_API.app.core.DB_Management.db_path_utils import DatabasePaths
db = MediaDatabase(db_path=str(DatabasePaths.get_media_db_path(DatabasePaths.get_single_user_id())), client_id="pg_client")
3) Programmatic backend (explicit override)
from tldw_Server_API.app.core.DB_Management.backends.base import BackendType, DatabaseConfig
from tldw_Server_API.app.core.DB_Management.backends.factory import DatabaseBackendFactory
from tldw_Server_API.app.core.DB_Management.media_db.native_class import MediaDatabase
cfg = DatabaseConfig(
backend_type=BackendType.POSTGRESQL,
pg_host="localhost", pg_port=5432,
pg_database="tldw_content", pg_user="tldw_user",
pg_password="secret", pg_sslmode="prefer",
)
backend = DatabaseBackendFactory.create_backend(cfg)
from tldw_Server_API.app.core.DB_Management.db_path_utils import DatabasePaths
db = MediaDatabase(db_path=str(DatabasePaths.get_media_db_path(DatabasePaths.get_single_user_id())), client_id="pg_client", backend=backend)
4) Configure via config.txt ([Database] section)
[Database]
# Select backend: sqlite or postgresql
type = postgresql
# Option A: Single DSN
pg_connection_string = postgresql://tldw_user:secret@localhost:5432/tldw_content?sslmode=prefer
# Option B: Discrete parameters (used when no pg_connection_string is provided)
pg_host = localhost
pg_port = 5432
pg_database = tldw_content
pg_user = tldw_user
pg_password = secret
pg_sslmode = prefer
# Pooling
pg_pool_size = 20
pg_max_overflow = 40
pg_pool_timeout = 30.0
# If using SQLite instead
# type = sqlite
# sqlite_path = <USER_DB_BASE_DIR>/<user_id>/Media_DB_v2.db
# sqlite_wal_mode = true
# sqlite_foreign_keys = true
# backup_path = ./tldw_DB_Backups/
SchemaError: If the existing database schema version is newer than what the code supports, or if migration fails.
Connection Management¶
These methods handle the SQLite connection lifecycle.
get_connection(self) -> sqlite3.Connection: Returns the active, thread-local SQLite connection. EnablesPRAGMA foreign_keys = ONandPRAGMA journal_mode=WAL(for file-based DBs).close_connection(self): Closes the current thread's database connection, if open.
Query Execution¶
Methods for executing SQL queries.
execute_query(self, query: str, params: tuple = None, *, commit: bool = False) -> sqlite3.Cursor: Executes a single SQL query.commit: IfTrue, commits the transaction. Usually managed by thetransaction()context manager.- Raises:
DatabaseErrorfor general SQLite errors. Re-raisessqlite3.IntegrityErrorif a sync validation trigger (defined in the schema) fails, allowing specific handling of these optimistic locking failures.
execute_many(self, query: str, params_list: List[tuple], *, commit: bool = False) -> Optional[sqlite3.Cursor]: Executes a SQL query for multiple sets of parameters (e.g., batch inserts).- Raises:
TypeErrorfor invalidparams_list,DatabaseErrorfor SQLite errors.
- Raises:
Transaction Management¶
transaction(self)(Context Manager): Provides an atomic block for database operations. Commits on successful completion, rolls back on any exception. Handles nested transactions gracefully (outermost transaction controls commit/rollback).- Yields:
sqlite3.Connection.
- Yields:
Schema Management (Internal)¶
These methods are primarily for internal use during initialization.
_initialize_schema(self): Checks DB schema version and applies_apply_schema_v1if necessary._apply_schema_v1(self, conn: sqlite3.Connection): Applies Version 1 of the schema (tables, indices, FTS tables, validation triggers) and sets the schema version in theschema_versiontable.
Internal Helpers (Internal)¶
These methods support the public API:
_get_current_utc_timestamp_str(self) -> str: Generates an ISO 8601 UTC timestamp string._generate_uuid(self) -> str: Generates a UUID v4 string._get_next_version(self, conn: sqlite3.Connection, table: str, id_col: str, id_val: Any) -> Optional[Tuple[int, int]]: Fetches current and next sync version for a record._log_sync_event(self, conn: sqlite3.Connection, entity: str, entity_uuid: str, operation: str, version: int, payload: Optional[Dict] = None): Logs an operation to thesync_logtable._update_fts_media(self, conn: sqlite3.Connection, media_id: int, title: str, content: Optional[str]): Updates/inserts intomedia_fts._delete_fts_media(self, conn: sqlite3.Connection, media_id: int): Deletes frommedia_fts._update_fts_keyword(self, conn: sqlite3.Connection, keyword_id: int, keyword: str): Updates/inserts intokeyword_fts._delete_fts_keyword(self, conn:sqlite3.Connection, keyword_id: int): Deletes fromkeyword_fts.
Search Operations¶
search_media_db(self, search_query: Optional[str], search_fields: Optional[List[str]] = None, keywords: Optional[List[str]] = None, media_ids_filter: Optional[List[Union[int, str]]] = None, page: int = 1, results_per_page: int = 20, include_trash: bool = False, include_deleted: bool = False) -> Tuple[List[Dict[str, Any]], int]: Searches media items.- Supports FTS on
title,contentviamedia_fts. - Supports LIKE search on
author,type. - Filters by a list of required
keywords(all must match). - Filters by a list of
media_ids_filterif provided. - Applies
is_trashanddeletedfilters. - Implements pagination.
- Returns: A tuple:
(results_list, total_matches). - Raises:
ValueErrorfor invalid pagination ormedia_ids_filtertypes.DatabaseErrorifmedia_ftsis missing or other DB errors.
- Supports FTS on
Media and Metadata Management (Mutators)¶
These methods modify the database content. They typically operate within a transaction, update last_modified and version columns, log events to sync_log, and update FTS tables where appropriate.
-
add_keyword(self, keyword: str) -> Tuple[Optional[int], Optional[str]]: Adds a new keyword (case-insensitive, stored lowercase) or undeletes an existing one. Updateskeyword_fts. Logs 'create' or 'update' tosync_log.- Returns:
(keyword_id, keyword_uuid). - Raises:
InputError,ConflictError,DatabaseError.
- Returns:
-
add_media_with_keywords(self, *, url: Optional[str] = None, title: Optional[str] = None, media_type: Optional[str] = None, content: Optional[str] = None, keywords: Optional[List[str]] = None, prompt: Optional[str] = None, analysis_content: Optional[str] = None, safe_metadata: Optional[str] = None, transcription_model: Optional[str] = None, author: Optional[str] = None, ingestion_date: Optional[str] = None, overwrite: bool = False, chunk_options: Optional[Dict] = None, chunks: Optional[List[Dict[str, Any]]] = None) -> Tuple[Optional[int], Optional[str], str]: Adds a new media item or updates an existing one (ifoverwrite=True, based on URL or content hash).- Generates a content hash.
- Associates keywords (via
update_keywords_for_media). - Creates an initial
DocumentVersion(viacreate_document_version). - If
chunksare provided, they are saved asUnvectorizedMediaChunks(old ones deleted on overwrite). - Updates
media_fts. Logs 'create'/'update' for Media, and relies on called methods for other sync events. ingestion_date: Defaults to current time ifNone.- Returns:
(media_id, media_uuid, status_message). - Raises:
InputError,ConflictError,DatabaseError.
-
create_document_version(self, media_id: int, content: str, prompt: Optional[str] = None, analysis_content: Optional[str] = None, safe_metadata: Optional[str] = None) -> Dict[str, Any]: Creates a new entry inDocumentVersions. Assigns the nextversion_numberfor themedia_id. Logs 'create' tosync_log.- Returns: Dict with new version's
id,uuid,media_id,version_number. - Raises:
InputError,DatabaseError.
- Returns: Dict with new version's
-
update_keywords_for_media(self, media_id: int, keywords: List[str]): Synchronizes keywords for a media item. Adds new keywords/links and removes outdated ones. Logs 'link'/'unlink' tosync_logforMediaKeywordschanges. Callsadd_keywordinternally.- Raises:
InputError,DatabaseError,ConflictError(fromadd_keyword).
- Raises:
-
soft_delete_media(self, media_id: int, cascade: bool = True) -> bool: Soft-deletes a Media item (deleted=1). Deletes itsmedia_ftsentry. Ifcascade=True:- Deletes
MediaKeywordslinks (logs 'unlink'). - Soft-deletes associated
Transcripts,MediaChunks,UnvectorizedMediaChunks,DocumentVersions(logs 'delete' for each). - Returns:
Trueon success. - Raises:
ConflictError,DatabaseError.
- Deletes
-
soft_delete_keyword(self, keyword: str) -> bool: Soft-deletes a keyword (deleted=1). Deletes itskeyword_ftsentry. Removes allMediaKeywordslinks (logs 'unlink').- Returns:
Trueon success. - Raises:
InputError,ConflictError,DatabaseError.
- Returns:
-
soft_delete_document_version(self, version_uuid: str) -> bool: Soft-deletes aDocumentVersion. Prevents deletion if it's the last active version for the media.- Returns:
Trueon success. - Raises:
InputError,ConflictError,DatabaseError.
- Returns:
-
mark_as_trash(self, media_id: int) -> bool: Marks a media item as trash (is_trash=1, setstrash_date). Logs 'update' tosync_log. Does not affect FTS.- Returns:
Trueon success. - Raises:
ConflictError,DatabaseError.
- Returns:
-
restore_from_trash(self, media_id: int) -> bool: Restores a media item from trash (is_trash=0,trash_date=NULL). Logs 'update' tosync_log.- Returns:
Trueon success. - Raises:
ConflictError,DatabaseError.
- Returns:
-
rollback_to_version(self, media_id: int, target_version_number: int) -> Dict[str, Any]: Rolls back Media content to a previousDocumentVersionstate.- Creates a new
DocumentVersionwith the old content. - Updates the main
Mediarecord's content andmedia_fts. - Logs 'create' for new
DocumentVersion, 'update' forMedia. - Prevents rollback to the current latest version number.
- Returns: Dict with success/error message and new version details.
- Raises:
ValueError,InputError,ConflictError,DatabaseError.
- Creates a new
-
process_unvectorized_chunks(self, media_id: int, chunks: List[Dict[str, Any]], batch_size: int = 100): AddsUnvectorizedMediaChunksin batches. Logs 'create' for each chunk.chunks: List of dicts, keys:chunk_text(ortext),chunk_index, optional:start_char,end_char,chunk_type,metadata.- Raises:
InputError,DatabaseError,TypeError(for metadata JSON).
-
add_media_chunk(self, media_id: int, chunk_text: str, start_index: int, end_index: int, chunk_id: str) -> Optional[Dict]: Adds a singleMediaChunkrecord. Logs 'create' tosync_log.- Returns: Dict with new chunk's
idanduuid. - Raises:
InputError,DatabaseError.
- Returns: Dict with new chunk's
-
add_media_chunks_in_batches(self, media_id: int, chunks_to_add: List[Dict[str, Any]], batch_size: int = 100) -> int: Wrapper to addMediaChunkrecords in batches, adapting input format forbatch_insert_chunks.chunks_to_add: List of dicts, keys:text,start_index,end_index.- Returns: Total number of chunks attempted for insertion.
- Raises: Propagates errors from
batch_insert_chunks.
-
batch_insert_chunks(self, media_id: int, chunks: List[Dict]) -> int: Inserts a batch ofMediaChunkrecords. Logs 'create' for each.chunks: List of dicts, keys:text(orchunk_text),metadata(dict withstart_index,end_index).- Returns: Number of chunks prepared for insertion.
- Raises:
InputError,DatabaseError,KeyError.
Media and Metadata Retrieval (Readers)¶
These methods fetch data. They generally filter for active records (deleted=0) unless specified.
-
fetch_all_keywords(self) -> List[str]: Fetches all active (non-deleted) keywords, sorted.- Returns:
List[str]of keywords.
- Returns:
-
fetch_media_for_keywords(self, keywords: List[str], include_trash: bool = False) -> Dict[str, List[Dict[str, Any]]]: Fetches active, non-deleted media items associated with each provided active keyword.- Media items filtered by
deleted=0andis_trash(ifinclude_trash=False). - Returns: Dict mapping found keywords to lists of their associated media items (basic fields).
- Raises:
TypeError,DatabaseError.
- Media items filtered by
-
get_paginated_media_list(self, page: int = 1, results_per_page: int = 10) -> Tuple[List[Dict[str, Any]], int, int, int]: Fetches a paginated list of active, non-trashed media items (id,title,type,uuid).- Returns:
(results_list, total_pages, current_page, total_items). - Raises:
ValueError,DatabaseError.
- Returns:
-
get_media_by_id(self, media_id: int, include_deleted=False, include_trash=False) -> Optional[Dict]: Retrieves a media item by ID. get_media_by_uuid(self, media_uuid: str, include_deleted=False, include_trash=False) -> Optional[Dict]: Retrieves a media item by UUID.get_media_by_url(self, url: str, include_deleted=False, include_trash=False) -> Optional[Dict]: Retrieves a media item by URL.get_media_by_hash(self, content_hash: str, include_deleted=False, include_trash=False) -> Optional[Dict]: Retrieves a media item by content hash.-
get_media_by_title(self, title: str, include_deleted=False, include_trash=False) -> Optional[Dict]: Retrieves the first media item (bylast_modifiedDESC) matching a title. -
get_all_document_versions(self, media_id: int, include_content: bool = False, include_deleted: bool = False, limit: Optional[int] = None, offset: Optional[int] = 0) -> List[Dict[str, Any]]: Retrieves document versions for an active media item, with pagination.- Raises:
TypeError,ValueError,DatabaseError.
- Raises:
-
get_paginated_files(self, page: int = 1, results_per_page: int = 50) -> Tuple[List[sqlite3.Row], int, int, int]: Similar toget_paginated_media_listbut returnssqlite3.Rowobjects forid,title,type.- Returns:
(results_list_of_rows, total_pages, current_page, total_items).
- Returns:
Metadata Search¶
search_by_safe_metadata(self, filters: Dict[str, Any], *, logical_op: str = 'AND', case_sensitive: bool = False, limit: Optional[int] = None, offset: int = 0) -> List[Dict[str, Any]]: SearchesDocumentVersions.safe_metadata(JSON) and related identifiers using simple equality/prefix filters with optional case sensitivity. Returns matching rows with basic fields.
Sync Log Management¶
get_sync_log_entries(self, since_change_id: int = 0, limit: Optional[int] = None) -> List[Dict]: Retrievessync_logentries newer thansince_change_id. Payload is JSON-decoded.delete_sync_log_entries(self, change_ids: List[int]) -> int: Deletes specificsync_logentries bychange_id. Returns count of deleted entries.delete_sync_log_entries_before(self, change_id_threshold: int) -> int: Deletessync_logentries withchange_id <= change_id_threshold. Returns count.
Claims API¶
The Claims subsystem stores ingestion-time factual statements associated with media chunks and maintains an FTS index (claims_fts) via triggers.
upsert_claims(self, claims: List[Dict[str, Any]]) -> int:- Inserts claims in batch. Each item should include:
media_id,chunk_index,span_start,span_end,claim_text,confidence,extractor,extractor_version,chunk_hash. -
Returns number of claims written. Triggers keep
claims_ftsin sync. -
get_claims_by_media(self, media_id: int, *, limit: int = 100) -> List[Dict[str, Any]]: -
Fetches non-deleted claims for a media item (limited count).
-
soft_delete_claims_for_media(self, media_id: int) -> int: -
Soft-deletes all claims for a media item. Returns number affected.
-
rebuild_claims_fts(self) -> int: - Rebuilds
claims_ftsfromClaimstable; returns row count in the rebuilt FTS.
Example (Claims):
from tldw_Server_API.app.core.DB_Management.media_db.native_class import MediaDatabase
from tldw_Server_API.app.core.DB_Management.db_path_utils import DatabasePaths
db = MediaDatabase(db_path=str(DatabasePaths.get_media_db_path(DatabasePaths.get_single_user_id())), client_id="example")
# Upsert a single claim
claims_written = db.upsert_claims([
{
"media_id": 123,
"chunk_index": 0,
"span_start": 0,
"span_end": 42,
"claim_text": "The video states the launch date is May 2025.",
"confidence": 0.85,
"extractor": "simple_rule",
"extractor_version": "1.0",
"chunk_hash": "abc123"
}
])
# Fetch recent claims for the media item
claims = db.get_claims_by_media(123, limit=10)
# Soft-delete all claims for a media item
deleted_count = db.soft_delete_claims_for_media(123)
# After bulk ingest or when triggers were disabled, rebuild FTS
fts_rows = db.rebuild_claims_fts()
print(f"claims_fts rows: {fts_rows}")
Chunking Templates API¶
Manage reusable chunking templates stored in the ChunkingTemplates table. Built-in templates cannot be modified or deleted.
create_chunking_template(self, name: str, template_json: str, description: Optional[str] = None, is_builtin: bool = False, tags: Optional[List[str]] = None, user_id: Optional[str] = None) -> Dict[str, Any]:-
Creates a template;
namemust be unique among non-deleted templates. Validatestemplate_json. -
get_chunking_template(self, template_id: Optional[int] = None, name: Optional[str] = None, uuid: Optional[str] = None, include_deleted: bool = False) -> Optional[Dict[str, Any]]: -
Fetches a template by ID, name, or UUID.
-
list_chunking_templates(self, include_builtin: bool = True, include_custom: bool = True, tags: Optional[List[str]] = None, user_id: Optional[str] = None, include_deleted: bool = False) -> List[Dict[str, Any]]: -
Lists templates with optional filters; sorts built-ins first then by name.
-
update_chunking_template(self, template_id: Optional[int] = None, name: Optional[str] = None, uuid: Optional[str] = None, template_json: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None) -> bool: -
Updates a custom template’s JSON/description/tags. Validates JSON. Returns
Trueif updated. -
delete_chunking_template(self, template_id: Optional[int] = None, name: Optional[str] = None, uuid: Optional[str] = None, hard_delete: bool = False) -> bool: -
Soft-deletes by default;
hard_delete=Truepermanently removes. Not allowed for built-ins. -
seed_builtin_templates(self, templates: List[Dict[str, Any]]) -> int: - Seeds built-in templates or restores deleted ones. Returns number created/restored.
Example (Chunking Templates):
from tldw_Server_API.app.core.DB_Management.media_db.native_class import MediaDatabase
from tldw_Server_API.app.core.DB_Management.db_path_utils import DatabasePaths
db = MediaDatabase(db_path=str(DatabasePaths.get_media_db_path(DatabasePaths.get_single_user_id())), client_id="example")
# Create a custom template
tmpl = db.create_chunking_template(
name="Sentences-512",
template_json='{"method":"sentences","max_tokens":512}',
description="Sentence chunking up to 512 tokens",
tags=["default"],
)
# List templates
all_templates = db.list_chunking_templates()
# Update template metadata
db.update_chunking_template(uuid=tmpl["uuid"], description="Refined")
# Soft-delete the template
db.delete_chunking_template(uuid=tmpl["uuid"]) # soft delete
Standalone Functions¶
These functions require a MediaDatabase instance to be passed as the db_instance argument. They generally call instance methods or perform read operations.
-
get_document_version(db_instance: MediaDatabase, media_id: int, version_number: Optional[int] = None, include_content: bool = True) -> Optional[Dict[str, Any]]: Gets a specific active document version or the latest active one for an active media item.- Raises:
TypeError,ValueError,DatabaseError.
- Raises:
-
create_incremental_backup(db_path, backup_dir)/create_automated_backup(db_path, backup_dir)/rotate_backups(backup_dir, max_backups=10): Placeholder functions, currently not implemented (log warnings). -
check_database_integrity(db_path: str) -> bool: PerformsPRAGMA integrity_check;on the DB file (read-only).- Returns:
Trueif 'ok',Falseotherwise.
- Returns:
-
is_valid_date(date_string: str) -> bool: Checks if a string is a valid 'YYYY-MM-DD' date. -
check_media_exists(db_instance: MediaDatabase, media_id: Optional[int] = None, url: Optional[str] = None, content_hash: Optional[str] = None) -> Optional[int]: Checks if an active media item exists by ID, URL, or hash.- Returns: Media ID if found, else
None. - Raises:
TypeError,ValueError,DatabaseError.
- Returns: Media ID if found, else
-
empty_trash(db_instance: MediaDatabase, days_threshold: int) -> Tuple[int, int]: Permanently removes items from trash older thandays_thresholdby callingdb_instance.soft_delete_media()for each.- Returns:
(processed_count, remaining_in_trash_count). - Raises:
TypeError,ValueError,DatabaseError(can be fromsoft_delete_media).
- Returns:
-
check_media_and_whisper_model(*args, **kwargs): Deprecated function, logs a warning. -
get_unprocessed_media(db_instance: MediaDatabase) -> List[Dict]: Retrieves active, non-trashed media items withvector_processing = 0.- Returns: List of dicts (
id,uuid,content,type,title). - Raises:
TypeError,DatabaseError.
- Returns: List of dicts (
-
mark_media_as_processed(db_instance: MediaDatabase, media_id: int): Setsvector_processing = 1for a media item. Does not updatelast_modified,version, or log tosync_log.- Raises:
TypeError,DatabaseError.
- Raises:
-
ingest_article_to_db_new(db_instance: MediaDatabase, *, url: str, title: str, content: str, author: Optional[str] = None, keywords: Optional[List[str]] = None, summary: Optional[str] = None, ingestion_date: Optional[str] = None, custom_prompt: Optional[str] = None, overwrite: bool = False) -> Tuple[Optional[int], Optional[str], str]: Wrapper to add/update an article usingdb_instance.add_media_with_keywords. Setsmedia_type='article'.- Raises:
TypeError,InputError,ConflictError,DatabaseError.
- Raises:
-
import_obsidian_note_to_db(db_instance: MediaDatabase, note_data: Dict[str, Any]) -> Tuple[Optional[int], Optional[str], str]: Wrapper to add/update an Obsidian note usingdb_instance.add_media_with_keywords. Setsmedia_type='obsidian_note'. Usespyyamlfor frontmatter.note_data: Dict withtitle,content, optionaltags,frontmatter,file_created_date,overwrite.- Raises:
TypeError,InputError,ConflictError,DatabaseError,ImportError(ifyamlmissing).
-
get_media_transcripts(db_instance: MediaDatabase, media_id: int) -> List[Dict]: Retrieves all active transcripts for an active media item.- Raises:
TypeError,DatabaseError.
- Raises:
-
get_latest_transcription(db_instance: MediaDatabase, media_id: int) -> Optional[str]: Retrieves text of the latest active transcript for an active media item.- Raises:
TypeError,DatabaseError.
- Raises:
-
get_specific_transcript(db_instance: MediaDatabase, transcript_uuid: str) -> Optional[Dict]: Retrieves a specific active transcript by UUID (ensuring parent media is active).- Raises:
TypeError,InputError,DatabaseError.
- Raises:
-
get_specific_analysis(db_instance: MediaDatabase, version_uuid: str) -> Optional[str]: Retrievesanalysis_contentfrom a specific activeDocumentVersion(ensuring parent media is active).- Raises:
TypeError,InputError,DatabaseError.
- Raises:
-
get_media_prompts(db_instance: MediaDatabase, media_id: int) -> List[Dict]: Retrieves non-empty prompts from activeDocumentVersionsfor an active media item.- Returns: List of dicts (
id,uuid,content(prompt),created_at,version_number). - Raises:
TypeError,DatabaseError.
- Returns: List of dicts (
-
get_specific_prompt(db_instance: MediaDatabase, version_uuid: str) -> Optional[str]: Retrievespromptfrom a specific activeDocumentVersion(ensuring parent media is active).- Raises:
TypeError,InputError,DatabaseError.
- Raises:
-
soft_delete_transcript(db_instance: MediaDatabase, transcript_uuid: str) -> bool: Soft-deletes a specific transcript. Calls instance methods for logging.- Returns:
Trueon success. - Raises:
TypeError,InputError,ConflictError,DatabaseError.
- Returns:
-
clear_specific_analysis(db_instance: MediaDatabase, version_uuid: str) -> bool: Clearsanalysis_content(sets to NULL) for an activeDocumentVersion. Calls instance methods for logging.- Returns:
Trueon success. - Raises:
TypeError,InputError,ConflictError,DatabaseError.
- Returns:
-
clear_specific_prompt(db_instance: MediaDatabase, version_uuid: str) -> bool: Clearsprompt(sets to NULL) for an activeDocumentVersion. Calls instance methods for logging.- Returns:
Trueon success. - Raises:
TypeError,InputError,ConflictError,DatabaseError.
- Returns:
-
get_chunk_text(db_instance: MediaDatabase, chunk_uuid: str) -> Optional[str]: Retrieveschunk_textfrom an activeUnvectorizedMediaChunks(ensuring parent media is active).- Raises:
TypeError,InputError,DatabaseError.
- Raises:
-
get_all_content_from_database(db_instance: MediaDatabase) -> List[Dict[str, Any]]: Retrieves various fields for all active, non-trashed media items.- Raises:
TypeError,DatabaseError.
- Raises:
-
permanently_delete_item(db_instance: MediaDatabase, media_id: int) -> bool: DANGER: Hard deletes a media item and its related data via DB cascades. Bypasses soft delete and sync log. Deletes FTS entry.- Returns:
Trueif deleted. - Raises:
TypeError,DatabaseError.
- Returns:
-
fetch_keywords_for_media(media_id: int, db_instance: MediaDatabase) -> List[str]: Fetches active keywords for a specific active media item.- Raises:
TypeError,DatabaseError.
- Raises:
-
fetch_keywords_for_media_batch(media_ids: List[int], db_instance: MediaDatabase) -> Dict[int, List[str]]: Fetches active keywords for multiple active media items.- Returns: Dict mapping
media_idto list of keywords. - Raises:
TypeError,InputError,DatabaseError.
- Returns: Dict mapping
Schema Overview (v5)¶
The current database schema consists of the following main tables:
schema_version: Stores the current schema version of the database.Media: Core table for media items (articles, videos, notes, etc.). Includes metadata like URL, title, content, author, timestamps, content hash, UUID, and flags foris_trash,deleted. Also trackschunking_statusandvector_processingstates.Keywords: Stores unique keywords (normalized to lowercase). Includes UUID and sync metadata.MediaKeywords: Junction table linkingMediaandKeywords(many-to-many).Transcripts: Stores transcriptions for media items, potentially from different models. Includes UUID and sync metadata.MediaChunks: Stores processed, smaller segments (chunks) of media content, often used for vectorization or focused analysis. Includes UUID and sync metadata.UnvectorizedMediaChunks: Stores raw chunks of media content before vectorization or further processing. Includes UUID and sync metadata,chunk_index, character offsets, and processing status.DocumentVersions: Stores historical versions of a media item's content, with associatedprompt,analysis_content, andsafe_metadata(JSON). Includes UUID and sync metadata.Claims: Stores factual claims extracted at ingestion time, attached to a media chunk (with indices/spans, extractor metadata, confidence). Maintained with FTS via triggers.sync_log: Records all CUD (Create, Update, Delete) operations, as well as link/unlink operations on entities, along withclient_id,timestamp,version, and an optionalpayload(JSON of changed data).ChunkingTemplates: Stores reusable chunking templates (JSON) with metadata, tags, ownership, and soft-delete/version metadata.- FTS Tables:
media_fts: FTS5 virtual table for full-text searchingMedia.titleandMedia.content.keyword_fts: FTS5 virtual table for full-text searchingKeywords.keyword.claims_fts: FTS5 virtual table for full-text search overClaims.claim_textwith triggers to maintain sync.
All primary entities (Media, Keywords, Transcripts, MediaChunks, UnvectorizedMediaChunks, DocumentVersions) include standard columns for synchronization and optimistic locking:
* uuid: A universally unique identifier for the record.
* last_modified: UTC timestamp of the last modification.
* version: An integer incremented on each modification, used for optimistic concurrency.
* client_id: Identifier of the client instance that made the last change.
* deleted: Boolean flag (0 or 1) for soft deletes.
* prev_version: Placeholder for previous version tracking (usage may vary).
* merge_parent_uuid: Placeholder for tracking merge history (usage may vary).
Database triggers validate updates on major tables (version must increment by 1, client_id must be non-empty, uuid cannot change). Additional triggers maintain the claims_fts index.
Example Usage (Conceptual)¶
from pathlib import Path
from tldw_Server_API.app.core.DB_Management.media_db.errors import ConflictError, InputError
from tldw_Server_API.app.core.DB_Management.media_db.native_class import MediaDatabase
# Initialize a MediaDatabase instance for a specific user/DB file
from tldw_Server_API.app.core.DB_Management.db_path_utils import DatabasePaths
db_file = DatabasePaths.get_media_db_path(DatabasePaths.get_single_user_id())
client_1 = "my_desktop_app_instance_123"
try:
db = MediaDatabase(db_path=db_file, client_id=client_1)
except Exception as e:
print(f"Failed to initialize database: {e}")
raise
# Add a new article
try:
media_id, media_uuid, msg = db.add_media_with_keywords(
url="http://example.com/article1",
title="My First Article",
media_type="article",
content="This is the full content of the article...",
keywords=["tech", "python", "database"],
safe_metadata='{"source": "web", "lang": "en"}',
author="John Doe",
overwrite=False # Don't overwrite if it exists
)
print(f"{msg} - ID: {media_id}, UUID: {media_uuid}")
# Search for it
results, total = db.search_media_db(search_query="article", keywords=["tech"])
for item in results:
print(f"Found: {item['title']}")
except InputError as ie:
print(f"Input Error: {ie}")
except ConflictError as ce:
print(f"Conflict Error: {ce} - Record {ce.entity} ID {ce.identifier} was modified by another client.")
except Exception as e:
print(f"An error occurred: {e}")
finally:
db.close_connection() # Close connection for the current thread
# Another client instance (e.g., on a different device or process)
# client_2 = "my_mobile_app_instance_456"
# db_sync_client = MediaDatabase(db_path=db_file, client_id=client_2)
#
# # Fetch sync log entries to process changes made by client_1
# changes = db_sync_client.get_sync_log_entries(since_change_id=0) # Get all changes
# for change in changes:
# print(f"Sync Event: {change['entity']} {change['operation']} by {change['client_id']}")
# # ... (logic to apply these changes to another system/DB) ...
#
# db_sync_client.close_connection()
Troubleshooting¶
- Missing FTS tables (e.g.,
media_fts,keyword_fts,claims_fts) - Symptom:
DatabaseErrormentioning "no such table" or failed search. -
Fix: Ensure schema initialization completed. Instantiate
MediaDatabaseonce with valid config to bootstrap schema. For claims, you can runrebuild_claims_fts()after enabling the table. -
Permissions when creating DB directories
- Symptom: Initialization fails creating the database directory.
-
Fix: Verify the
db_pathparent exists and has write permission, or choose a writable path. In containerized deployments, mount a writable volume. -
Chunking template JSON errors
- Symptom:
InputError: Invalid template JSONwhen creating/updating templates. -
Fix: Provide valid JSON; prefer double quotes and escape inner quotes.
-
Sync validation/optimistic concurrency errors
- Symptom:
sqlite3.IntegrityErrorwith trigger messages (e.g., "Version must increment by exactly 1", "UUID cannot be changed"). -
Fix: Use the documented mutators that manage version increments and
last_modified. Avoid direct SQL updates on versioned tables. -
PostgreSQL connection/config issues
- Symptom: Connection refused/timeout or invalid DSN.
-
Fix: Use the
TLDW_CONTENT_*env vars or[Database]config as shown in examples. Verify credentials, host reachability, andpg_sslmode. -
Upgrades and
safe_metadata - Symptom: Older DBs may lack
safe_metadatacolumn; insert paths catch and adapt, but queries may fail. - Fix: Initialize via current code path to let migrations add new columns, or recreate the DB and re-ingest.
Common Migration Tasks¶
- Verify schema version and bootstrap
- Back up the DB file first.
-
Instantiate
MediaDatabaseonce to run schema checks/migrations. Verify with:- SQLite:
sqlite3 <USER_DB_BASE_DIR>/<user_id>/Media_DB_v2.db "SELECT version FROM schema_version;"→ should be5.
- SQLite:
-
Add
safe_metadatato older databases (manual) -
If you cannot run the app to migrate, add the column manually:
- SQLite:
sqlite3 <db> "ALTER TABLE DocumentVersions ADD COLUMN safe_metadata TEXT;"
- SQLite:
-
Enable Claims and rebuild FTS (SQLite)
-
Initialization creates
Claimsand triggers; if claims_fts is empty or missing, rebuild:- Python:
db.rebuild_claims_fts() - Or SQLite:
INSERT INTO claims_fts(rowid, claim_text) SELECT id, claim_text FROM Claims WHERE deleted = 0;
- Python:
-
Rebuild FTS indices after bulk changes (SQLite)
media_fts: delete and reinsert rows fromMedia.keyword_fts: delete and reinsert rows fromKeywords.-
The library updates FTS on mutators; for manual maintenance, script the rebuilds.
-
Move to per-user layout
-
Move file to
<USER_DB_BASE_DIR>/<user_id>/Media_DB_v2.dband update config (USER_DB_BASE_DIRintldw_Server_API.app.core.config, override via environment variable orConfig_Files/config.txt, or content DB settings). -
Migrate SQLite → PostgreSQL
- Recommended: re-ingest via the library (mutators handle sync/versioning/metadata) into a Postgres-backed
MediaDatabase. - Programmatic: read from SQLite and call
add_media_with_keywords,create_document_version,process_unvectorized_chunks,upsert_claimson the PG instance. - Note: FTS virtual tables are SQLite-specific; Postgres backend uses LIKE/ILIKE fallbacks in search.