import sqlite3 from app.settings import settings # Columns actually used by the app today. beets' `items` table has many more; # add columns here as new features need them rather than SELECT *. _ITEM_COLUMNS = ["id", "path", "title", "artist", "albumartist", "genres", "grouping", "isrc", "added", "format"] def db_exists() -> bool: return settings.beets_db_path.exists() def _connect() -> sqlite3.Connection: # mode=ro: alembic never writes through this connection. Mutations (tag # edits, imports) go through the `beet` CLI or beets.library.Library # in-process -- see services/library_edit.py (manual-fix feature). conn = sqlite3.connect(f"file:{settings.beets_db_path}?mode=ro", uri=True) conn.row_factory = sqlite3.Row return conn def _decode_path(value) -> str: """beets stores `path` as a BLOB (raw filesystem bytes), not TEXT.""" if isinstance(value, bytes): return value.decode("utf-8", errors="replace") return value or "" def _row_to_dict(row: sqlite3.Row) -> dict: d = dict(row) if "path" in d: d["path"] = _decode_path(d["path"]) return d def _grouping_clause(grouping: str) -> tuple[str, list]: """grouping is a single tag OR a "; "-joined set (e.g. "techno; djstuff" for a track that's in two playlists' libraries). Matching only the exact combined string would silently exclude combo-tagged tracks from a single-tag filter -- match the token in any position instead.""" return ( "(grouping = ? OR grouping LIKE ? OR grouping LIKE ? OR grouping LIKE ?)", [grouping, f"{grouping}; %", f"%; {grouping}", f"%; {grouping}; %"], ) def _build_where(grouping: str | None, search: str | None, format: str | None) -> tuple[str, list]: where = [] params: list = [] # Truthy checks, not `is not None`: a