feat: migrate LibraryCache to SQLite, add SessionStore + splash overlay

- Rewrite LibraryCache from JSON (cache dir) to SQLite (data dir with WAL)
- Each track gets a stable UUID so play stats survive file renames/re-encodes
- Thread-safe via PRAGMA busy_timeout + RLock
- Automatic backup rotation (.backup, .bak) on every write
- One-shot migration from old ~/.cache/library_cache.json
- Add SessionStore (~/.local/share/session.json) for playback state
- Add SplashOverlay with spinning animation on first (empty-DB) launch
- _scan_library() now accepts on_progress callback for the overlay
- Fix _mtime → mtime SQLite column name lookup in recently-added filter
- PlaylistManager path migrated to ~/.local/share/ with auto-migration
- Metadata sync: remove dead consume_play_deltas(), use library API
This commit is contained in:
Maksim Totmin
2026-06-01 22:12:56 +07:00
parent 2d77b3ea3e
commit ba7144c88f
10 changed files with 778 additions and 160 deletions
+30 -14
View File
@@ -9,7 +9,7 @@ import sys
import time
import logging
import shutil
from typing import List, Dict, Optional, Tuple, Any
from typing import List, Dict, Optional, Tuple, Any, Callable
from pathlib import Path
from PyQt6.QtWidgets import (
@@ -450,7 +450,8 @@ class LibraryTab(QWidget):
return f"{size_bytes / 1024:.0f} KB"
return f"{size_bytes / (1024 * 1024):.1f} MB"
def _scan_library(self, force: bool = False):
def _scan_library(self, force: bool = False,
on_progress: Callable[[int, int], None] | None = None):
output_dir = self.get_output_dir()
if not output_dir or not os.path.isdir(output_dir):
self.library_ready.clear()
@@ -458,10 +459,23 @@ class LibraryTab(QWidget):
self._refresh_library_ui()
return
# Pre-count total scannable files so the overlay can show progress.
total = 0
if on_progress:
for root, _dirs, files in os.walk(output_dir):
for fname in files:
fpath = os.path.join(root, fname)
ext = os.path.splitext(fname)[1].lower()
if os.path.isfile(fpath) and (ext in AUDIO_EXTS or ext in SOURCE_EXTS):
total += 1
if total == 0:
total = 1
ready = []
sources = []
cache_hits = 0
cache_misses = 0
processed = 0
cache = get_library_cache()
handler = MetadataHandler()
@@ -473,6 +487,7 @@ class LibraryTab(QWidget):
if not os.path.isfile(fpath):
continue
processed += 1
cached = None if force else cache.get(fpath)
if cached is not None:
@@ -483,18 +498,19 @@ class LibraryTab(QWidget):
elif ext in SOURCE_EXTS:
cached.pop("_mtime", None)
sources.append(cached)
continue
else:
cache_misses += 1
if ext in AUDIO_EXTS:
track_data = self._extract_ready_track(fpath, fname, handler)
ready.append(track_data)
cache.put(fpath, track_data)
elif ext in SOURCE_EXTS:
track_data = self._extract_source_track(fpath, fname, handler)
sources.append(track_data)
cache.put(fpath, track_data)
cache_misses += 1
if ext in AUDIO_EXTS:
track_data = self._extract_ready_track(fpath, fname, handler)
ready.append(track_data)
cache.put(fpath, track_data)
elif ext in SOURCE_EXTS:
track_data = self._extract_source_track(fpath, fname, handler)
sources.append(track_data)
cache.put(fpath, track_data)
if on_progress:
on_progress(processed, total)
cache.save()
@@ -1384,7 +1400,7 @@ class LibraryTab(QWidget):
entry = cache.get(path) if path else None
visible = False
if entry and isinstance(entry, dict):
added = entry.get("_mtime", 0) or entry.get("added_at", 0) or 0
added = entry.get("mtime", 0) or entry.get("added_at", 0) or 0
visible = added >= cutoff
self.library_table.setRowHidden(row, not visible)