Commit Graph
2 Commits
Author SHA1 Message Date
trav e69917a290 v0.7.0: startup speed — the 21k-track library stops freezing
Three reported symptoms, one shape: every operation was whole-library,
whole-file, on the Qt main thread. Measured on the real library (21,482
tracks, 506 playlists, 15 MB library.json).

The track table sorted through a QSortFilterProxyModel, which asks data()
for a value on every comparison — 580k Python round trips, 8.5 s per table
load, paid again on every reload. TrackTableModel now keeps _tracks in
canonical (playlist) order plus an _order index list and sorts a key list
computed once per track. Nothing used the proxy's filtering. Sorting by #
is the identity order, so "source row" still means "playlist position" for
drag-reorder. 8.5 s -> 0.13 s; MainWindow() 18.1 s -> 0.79 s.

Smart rules compile to closures once per evaluate() instead of being
re-dispatched per track — the operator lookup, casefolding the query and
parsing the rule's own date constants all leave the 21k-iteration loop
(_match_date was re-parsing its own constant 21,482 times per rule).
Verified identical membership against the old evaluator on all 20 real
smart playlists. 2.54 s -> 0.72 s, and it now runs after the first paint.

Also: _reconcile_track skips two to_dict() round trips per unchanged track
(MERGEABLE_FIELDS in the resolver is the single source of truth for what a
merge touches); no git subprocess on the startup path (Updater.enabled is
probed lazily on the background thread, version-button tooltip deferred);
and .resolved/ is pruned to the newest 10 snapshots — it had reached 1.7 GB
across 73 snapshots inside the Syncthing share.

Time to interactive window ~15 s -> 3.2 s. The mid-session freeze when
Syncthing delivers a change — a full reload + recompute + table rebuild
with the window already on screen, which is what GNOME was offering to
force-quit — ~16 s -> 3.8 s.

The merge rework itself (playlist ordering, per-machine play journal,
quieting the dialog) is queued in TASKS.md as Round 36.
2026-08-19 17:09:07 -04:00
trav 2917a42b75 Add smart playlists with full iTunes 12 import (Phase 1)
Auto-populating, criteria-driven playlists that import faithfully from iTunes.

- lintunes/smart.py: recursive SmartCriteria/SmartGroup/SmartRule/SmartLimit
  model (JSON round-tripping), a shared FIELD_REGISTRY used by both the
  evaluator and the editor, and evaluate() (match all/any, nested groups,
  string/int/duration/rating/date/bool ops, "in the last N", limit by
  items/time/size). Null play/skip dates are treated as the distant past,
  matching iTunes.
- iTunes import via a vendored MIT-licensed binary parser
  (lintunes/itunes_smart/, from cvzi/itunes_smartplaylist). Nested groups
  parse and evaluate; blobs we can't represent (MediaKind/iCloud/etc.) flag
  unsupported and keep the imported snapshot. "loved" is dropped per user pref.
- library_manager: create/set/recompute smart playlists (undoable), field-scoped
  coalesced live recompute hooked into the edit/play/skip/add funnels, a no-op
  equality guard to avoid Syncthing churn, and manual-edit guards. main.py
  recomputes on load; conflict_resolver keeps newest criteria for smart lists.
- GUI: ❧ glyph painted in the sidebar branch column, read-only track table for
  smart playlists, New/Edit Smart Playlist menus, and SmartPlaylistEditorDialog
  (per-field rule rows, match all/any, limits, live updating).

Tests: tests/test_round14.py (real captured blobs in tests/smart_blobs.json).
2026-07-01 16:43:39 -04:00