Commit graph

3326 commits

Author SHA1 Message Date
Broque Thomas
449a26e56b Extract Auto-Sync into webui/static/auto-sync.js
Cin review: stats-automations.js had ~600 lines of new Auto-Sync code
piled into an already-large shared file. Moved into its own module:

- New webui/static/auto-sync.js holds:
  - Schedule board state (`AUTO_SYNC_BUCKETS`, `_autoSyncScheduleState`,
    `_autoSyncActiveTab`, `mirroredPipelinePollers`)
  - All `autoSync*` functions (trigger conversion, render panels,
    drag/drop, save/unschedule, schedule modal lifecycle)
  - Mirrored-playlist pipeline helpers (`runMirroredPlaylistPipeline`,
    `pollMirroredPipelineStatus`, `applyMirroredPipelineState`,
    `parseMirroredPipelineResponse`, `editMirroredSourceRef`,
    `getMirroredSourceRef`)
- index.html loads auto-sync.js immediately after stats-automations.js
  so the older `renderMirroredCard` path can keep reaching these
  globals through the window namespace.
- stats-automations.js drops 567 lines and gains a one-line breadcrumb
  pointing at the new file.

No behavior changes — every function moved verbatim. Globals stay in
the same window namespace, so the still-resident `renderMirroredCard`
keeps calling `runMirroredPlaylistPipeline` / `editMirroredSourceRef`
/ `mirroredPipelinePollers` exactly as before.

Both files pass `node --check`. Full Python suite still green.
2026-05-24 23:46:08 -07:00
Broque Thomas
9b086c5a65 Add owned_by column for Auto-Sync schedule ownership
The Auto-Sync schedule board was detecting its own automations by
checking `group_name === 'Playlist Auto-Sync' || name.startsWith('Auto-Sync:')`.
That's fragile — renaming the row from the Automations page silently
hands ownership back to the read-only Automation Pipelines tab and the
board stops managing it.

This commit replaces the string convention with an explicit
`automations.owned_by` TEXT column:

- Migration `_add_automation_owned_by_column` adds the column and
  backfills `'auto_sync'` for existing rows that match the legacy
  `group_name`/`name`-prefix pattern, so users running the migration
  don't lose their schedules.
- `database.create_automation` and `database.update_automation` accept
  `owned_by` (the latter via its `allowed` kwarg set).
- `core/automation/api.py` forwards `owned_by` on both POST and PUT.
  Missing field is left as None, preserving today's behavior for every
  caller that doesn't opt in.
- The Auto-Sync schedule board posts `owned_by: 'auto_sync'` and the
  detection helper now prefers that signal, falling back to the legacy
  name/group convention so any hand-rolled rows still show up.

Tests: three new cases in `tests/automation/test_automation_api.py`
covering create-with-owned-by, create-without (defaults to None), and
update set/clear. The fake DB grew the matching kwarg.
2026-05-24 23:40:22 -07:00
Broque Thomas
feb6778af4 Address Cin review: extract helpers, indexed pool fetch, tidy nits
Three changes folded into one perf+cleanup pass:

1. Indexed fast path for the per-artist pool fetch. The previous
   `search_tracks(artist=name)` call hit
   `unidecode_lower(artists.name) LIKE ?`, a function-in-WHERE that
   can't use `idx_artists_name`. New `MusicDatabase.get_artist_tracks_indexed`
   does a two-step lookup: exact-name match (indexed) plus a
   case-insensitive fallback, then `tracks WHERE artist_id IN (...)`
   via `idx_tracks_artist_id`. Drops per-artist fetch from seconds to
   milliseconds for the common case. The sync helper falls back to
   the old LIKE-based `search_tracks` only when the indexed lookup
   finds nothing, preserving diacritic recall and `tracks.track_artist`
   feature-artist matches with zero regression.

2. Public text-normalization helper. Lifted the body of
   `MusicDatabase._normalize_for_comparison` into
   `core/text/normalize.py:normalize_for_comparison` so callers outside
   the database layer (matching engine, sync pool, future import-side
   comparisons) don't reach across the module boundary into a
   leading-underscore "private" method. The DB method now delegates,
   so existing internal call sites stay untouched. Sync's lazy pool
   now imports the public helper.

3. Artist-name walker extracted. `_artist_name` at module level in
   `services/sync_service.py` replaces two near-identical inline
   str-or-dict-or-fallback walkers (one in `sync_playlist`, one in
   `_find_track_in_media_server`). Returns `''` for None instead of
   the literal string `'None'`.

Plus three small tidies from the same review:

- `_POOL_FETCH_LIMIT = 10000` constant in place of the literal at the
  pool-fetch call site.
- Trimmed the verbose docstring + comment block on the pool helper.
- Set-intersection predicate for the trigger-shape reset in
  `core/automation/api.py` instead of a two-line `or` chain.

Also removed the duplicate `_get_active_media_client()` call at
sync_service.py:212/214 — pre-existing wart that was sitting in the
same block I was editing.

Tests: 21 new tests across `tests/database/`, `tests/sync/`, and
`tests/text/`, plus updates to the existing pool tests to cover the
new fast/fallback split. Full suite stays green (3953 passing).
2026-05-24 23:33:55 -07:00
Broque Thomas
687bb0ca2c Add tests for next_run reset and lazy candidate pool
`tests/automation/test_automation_api.py` gains three update_automation
tests covering the schedule-shape reset:
- trigger_config change blanks next_run
- trigger_type change blanks next_run
- non-trigger field (name) leaves next_run alone

`tests/sync/test_sync_candidate_pool.py` is new — nine tests for the
lazy artist track pool in PlaylistSyncService:
- candidate_pool=None disables pooling and skips the DB call
- first lookup for an artist fetches and caches
- second lookup for the same artist reuses the cache (zero DB calls)
- empty result still cached so the next call short-circuits without SQL
- defensive None return coerced to []
- search_tracks exception returns None and does NOT poison the cache
- pool key is normalized so casing variants share a single fetch
- different artists get separate pool entries
- server_source plumbing survives the trip to search_tracks

All assertions go through fakes / MagicMock — no real DB, no
web_server.py import, no AST-parsing.
2026-05-24 22:58:48 -07:00
Broque Thomas
871feb3997 Speed up playlist sync with a lazy per-artist track pool
Syncing a playlist where most tracks weren't in the library was burning
~30 SQL queries per missed track. `services/sync_service.py` walked
each Spotify track through `check_track_exists` with no
`candidate_tracks`, hitting the legacy title-variation × artist-variation
grid in `database/music_database.py:6041-6069` for every miss. The
`sync_match_cache` only covered matches, so misses re-paid the full
lookup cost every sync. A 30-track playlist with a 30% match rate
(Discover Weekly was 9/30 in the test run) was taking ~4m14s, almost
entirely in the matching phase.

`check_track_exists` already accepts a `candidate_tracks` kwarg that
skips the SQL widening and scores against an in-memory list (the
batched path at `music_database.py:6031`, originally added for artist
discography iteration). The sync service just wasn't using it.

This commit wires that path in via a lazy per-artist pool:

- `sync_playlist` creates an empty `candidate_pool` dict and passes it
  to each `_find_track_in_media_server` call.
- `_get_or_fetch_artist_candidates` runs SQL for an artist only on the
  first track that needs them — playlists where every track is already
  in `sync_match_cache` pay zero pool cost (no upfront delay).
- Subsequent misses for the same artist hit the memoized list and skip
  the per-variation SQL grid.
- Artists with no library tracks still get a cached empty list, which
  triggers the batched path's instant short-circuit instead of falling
  into the SQL widening.
- Any pool fetch failure returns None so the caller falls through to
  the original per-track SQL loop, so the worst case is the old
  behavior, never a regression.

On a 30-track / 25-unique-artist playlist with a cold cache the SQL
fan-out drops from ~900 queries to ~25; with a warm cache it drops to
zero (no pool fetches at all). Applies to every entry point that goes
through `sync_playlist`: manual sync, auto-sync schedules, the
`playlist_pipeline` automation action, and the Sync All button.
2026-05-24 22:43:32 -07:00
Broque Thomas
dc4d157944 Fix Auto-Sync next-run countdown and theme its modal
The Playlist Auto-Sync schedule board was showing "next in 8h" on every
card regardless of the configured interval. Root cause: backend stores
next_run as a naive UTC string ("2026-05-25 05:00:00") and the new
auto-sync renderer was parsing it with plain `new Date(...)`, which
treats unmarked timestamps as local time. On Pacific time that offsets
the displayed countdown by ~8 hours. Auto-Sync now routes through the
existing `_autoParseUTC` helper that the rest of the Automations page
already uses, so countdowns line up with the wall clock.

A separate correctness fix in the automation update API: when a PUT
changes `trigger_type` or `trigger_config`, the stored `next_run` is
now blanked before the engine reschedules. Previously the scheduler's
restart-survival path would preserve a stale future timestamp from the
prior interval, so dragging a playlist from the 8h column to the 1h
column kept firing at the old 8h mark. Boot-time restart behavior is
unchanged — only user-driven schedule changes reset the clock.

Modal restyle: the Auto-Sync manager's hardcoded sky-blue palette is
replaced with `var(--accent-rgb)` everywhere so the modal honors the
user's chosen accent color. Tinted glow on the modal border, tabbed
header active state, scheduled-playlist chips, scrollbars, and a new
drag-over highlight on columns all follow the accent theme. The
column drag-over state is wired through new ondragleave handling so
the highlight clears reliably when leaving a column.
2026-05-24 22:01:21 -07:00
Broque Thomas
9a8e7d02a7 Make auto-sync schedule modal responsive
Constrain Auto-Sync columns inside the modal with per-column vertical scrolling, add responsive layouts for narrower and shorter viewports, and separate schedule interval labels from next-run timing.

Also prevents unsupported mirrored sources from being scheduled into the playlist pipeline while still showing them as unavailable in the sidebar.
2026-05-24 21:01:46 -07:00
Broque Thomas
5421f3800e Polish auto-sync manager modal
Upgrade the Auto-Sync modal into a tabbed manager with a richer schedule board and a separate read-only Automation Pipelines tab for existing playlist_pipeline automations.
2026-05-24 20:39:48 -07:00
Broque Thomas
854141f903 Add playlist auto-sync schedule board
Add a Sync-page Auto-Sync manager with source-grouped mirrored playlists, interval columns, and drag/drop scheduling backed by playlist_pipeline automations.

Schedules created by the board are editable there, while existing custom pipeline automations are shown as locked automation-managed entries.
2026-05-24 20:31:03 -07:00
Broque Thomas
46a0999ca2 Clarify mirrored playlist auto-sync action
Rename the manual pipeline button to Auto-Sync and make non-JSON endpoint failures show an actionable restart message instead of a raw JSON parse error.
2026-05-24 20:23:37 -07:00
Broque Thomas
a1409576f4 Move mirrored pipeline action into card controls
Place the Run Pipeline action alongside the existing mirrored playlist card controls so users can find it next to clear, edit source, and delete.
2026-05-24 20:09:06 -07:00
Broque Thomas
f83c671570 Add direct mirrored playlist pipeline runs
Expose playlist-native run and status endpoints that reuse the shared mirrored playlist pipeline engine while routing progress into playlist UI state.

Add a Run Pipeline action to mirrored playlist cards and modals with live status polling, and make the shared pipeline lock atomic for manual and scheduled callers.
2026-05-24 19:54:04 -07:00
Broque Thomas
bc6bacb7da Move mirrored playlist pipeline into playlist domain
Extract the all-in-one mirrored playlist lifecycle into core/playlists/pipeline.py so automation becomes a thin adapter. Preserve the existing automation action and behavior while making the pipeline reusable by future direct playlist UI controls.
2026-05-24 19:44:13 -07:00
Broque Thomas
547e499121 Expose mirrored playlist source-ref health
Return normalized source_ref metadata from mirrored playlist APIs so the UI no longer has to infer editable refresh links from description fields. Accept Spotify embed URLs during source-ref repair and add coverage for source-ref health reporting.
2026-05-24 19:32:39 -07:00
Broque Thomas
73bd2db547 Harden playlist pipeline source refresh
Centralize mirrored playlist source reference normalization so edited links and IDs are stored consistently. Preserve URL-backed refresh refs, surface missing-source refresh failures, count background sync failures in pipeline summaries, and retry guarded automation skips after a short delay instead of losing a scheduled run. Add focused coverage for source refs, mirrored playlist source updates, refresh failures, and guarded retry behavior.
2026-05-24 19:31:00 -07:00
Broque Thomas
a7ca7ddfad Harden album bundle fallback flow
Delay torrent and usenet album-bundle dispatch until missing-track analysis confirms there is work to do, matching the Soulseek album flow and avoiding release downloads for already-owned albums.

Clear private album-bundle staging state when a release-level source intentionally falls back to per-track mode so workers can use the normal staging/search path instead of an empty private bundle directory.

Verified by user: focused downloads master tests passed, 2 passed.
2026-05-24 16:15:36 -07:00
Broque Thomas
93743119d9 Bump version to 2.6.1
Update the SoulSync base version, Docker release workflow default, and What's New notes so the next release surfaces as 2.6.1.
2026-05-24 15:44:58 -07:00
Broque Thomas
28922ad2c1 Cap persistent download history response
Stop appending persistent download history once the unified downloads payload reaches the requested limit. This keeps the Downloads tab history tail bounded even if the history provider returns more rows than requested, while preserving existing live-task total behavior.
2026-05-24 14:57:32 -07:00
Broque Thomas
28641403d9 Fix Zustand shallow import
Use Zustand's public shallow selector export so the import page resolves correctly under the installed Zustand 5 package. This fixes the Vite boot overlay without changing import workflow behavior.
2026-05-24 14:47:29 -07:00
BoulderBadgeDad
29ba4e0049
Merge pull request #689 from kettui/refactor/more-native-links
Replace programmatic navigation with anchor links on issues & stats pages
2026-05-24 14:41:25 -07:00
BoulderBadgeDad
a1222d5a8f
Merge pull request #686 from kettui/feat/react-migration-import
feat(webui): migrate import page to React
2026-05-24 14:16:05 -07:00
Broque Thomas
0bea332aed Preserve album bundle track numbers
Keep album-bundle staging from replacing known per-track album numbers with the filename parser's default when staged files do not expose a real track number. Carry staging tag numbers through the cache, fall back to task metadata for private release staging, and cap hybrid album batches to one worker when Soulseek is first in the source order.
2026-05-24 13:59:44 -07:00
Antti Kettunen
caa6534ee8
feat(import): show MusicBrainz variants
- pass release metadata through album search normalization
- surface release format, country, label, and disambiguation in React import cards
- add coverage for search normalization and import route rendering
2026-05-24 21:29:22 +03:00
Antti Kettunen
400d0dd48a
docs(import): align migration docs
- describe the implemented nested /import route structure
- document the route-local workflow store and stable draft state
- update testing, risk, and cleanup notes to match the current code
2026-05-24 21:17:22 +03:00
Antti Kettunen
e2a760bd68
fix(webui): keep import pages cache-aware
- keep the /import loader from turning transient staging fetch failures into route errors
- keep cached auto-import status and results visible during refetch failures
- show inline notices only when there is no stale data to fall back to
- add regression coverage for staging, status, and results failure paths
2026-05-24 21:17:22 +03:00
Antti Kettunen
a65fbfd532
refactor(webui): move badge into primitives
- move the shared badge primitive and styling out of the form component module
- keep primary-button badge styling working via a data-slot hook instead of a form-local class
- update the import pages and primitive tests to consume the new home for Badge
2026-05-24 21:17:22 +03:00
Antti Kettunen
9da8251e43
feat(webui): clarify import source metadata
- thread primary_source through album and track search payloads while keeping per-result source on the returned rows
- reuse the shared Notice primitive for fallback and error messaging in the import pages
- update the import route tests and shell route smoke coverage for the new behavior
2026-05-24 21:17:22 +03:00
Antti Kettunen
2a7c03f398
refactor(webui): merge primitives into one module
- fold Show and Notice into a single primitives module with one shared stylesheet
- keep the primitives barrel export intact while shrinking the folder footprint
- consolidate the primitive tests into one combined suite
2026-05-24 21:17:22 +03:00
Antti Kettunen
01a0333d7a
style(import): add separate styles for mobile 2026-05-24 21:17:21 +03:00
Antti Kettunen
7bd6e0ba09
feat(form): add ghost option variant
- Keep option buttons transparent by default and subtle on hover
- Use the ghost style for inactive auto-import filters so the active one stands out
- Keep OptionButton aligned with the existing button variant API
2026-05-24 21:17:21 +03:00
Antti Kettunen
5a227e3ace
test(import): use shared shell helper 2026-05-24 21:17:21 +03:00
Antti Kettunen
b4ca5efe39
refactor(form): use nested data selectors
- keep only semantic data attributes on the form primitives
- move variant styling into nested CSS module selectors
- preserve the existing visual treatment while simplifying the component layer
2026-05-24 21:17:21 +03:00
Antti Kettunen
e65ec37c9e
test(import): use MSW route handlers
- replace direct fetch stubs with shared MSW handlers
- keep fetch spying only for request assertions
- cover the shell prefetch with an issues counts handler
2026-05-24 21:17:21 +03:00
Antti Kettunen
7a0548ac94
refactor(import): normalize controls and classes
- let the singles action buttons use the default size again
- remove redundant type="button" props from import controls
- switch import page conditional classes to clsx object notation
- drop route-test assertions that pinned compact auto-import sizes
2026-05-24 21:17:21 +03:00
Antti Kettunen
9ba54bd82d
feat(form): add compact option groups
- add a size prop to OptionButtonGroup with a denser sm layout\n- use the compact filter group on the auto-import panel\n- keep the new size variants covered in form and route tests
2026-05-24 21:17:21 +03:00
Antti Kettunen
fccc03efef
feat(import): badge auto-import status 2026-05-24 21:17:21 +03:00
Antti Kettunen
934a612df3
feat(form): add select size variants 2026-05-24 21:17:21 +03:00
Antti Kettunen
56f642aadd
test(import): remove stale frontend pytest
- delete the source-text guard for the old album lookup cache pattern\n- keep the import-page source-routing contract covered by Vitest route tests\n- avoid duplicating frontend behavior checks across pytest and the webui test suite
2026-05-24 21:17:21 +03:00
Antti Kettunen
0721a859a9
fix(import): keep count badge visible
- add a contrast override for badges inside primary buttons
- keep the singles process action aligned with the select/deselect row
- update import route tests for the new button label shape
2026-05-24 21:17:21 +03:00
Antti Kettunen
9f2c5c685b
refactor(import): simplify API failure handling
- rely on ky for transport errors across import/staging calls
- keep explicit soft-failure checks for auto-import approval endpoints
- add regression test for approval/rejection soft failures
2026-05-24 21:17:20 +03:00
Antti Kettunen
d32d88ea0e
refactor(webui): add shared badges to form kit
- add a reusable shared Badge primitive alongside the existing form controls\n- use it for the import auto-filter count pills and remove the route-local badge styles\n- tighten option button spacing so embedded badges read cleanly
2026-05-24 21:17:20 +03:00
Antti Kettunen
d066aba03d
refactor(webui): lean import buttons on shared styles
- move the import page over to shared button variants and option buttons
- strip route-local button chrome back to layout-only helpers
- keep the import route styling focused on layout, cards, and state indicators
2026-05-24 21:17:20 +03:00
Antti Kettunen
b78350a3e2
fix(webui): preserve import tab refresh URLs
- stop the legacy shell bootstrap from collapsing /import/auto and /import/singles back to the import root on reload\n- update the shell route smoke test to expect the canonical /import/album redirect for the bare import page
2026-05-24 21:17:20 +03:00
Antti Kettunen
ae0711f464
refactor(webui): theme shared import controls
- add a shared switch primitive for theme-aware toggle styling\n- keep import-page buttons leaning on shared variants instead of local color rules\n- simplify the singles and auto-import controls around the shared form layer
2026-05-24 21:17:20 +03:00
Antti Kettunen
af3d51c2ed
refactor(webui): theme import buttons
- add shared button variants and size controls backed by theme-aware styles
- move album import search controls onto shared button styling
- keep the import route layout-specific CSS limited to positioning
2026-05-24 21:17:20 +03:00
Antti Kettunen
0d7fb91d98
refactor(webui): share import form primitives
- add shared Base UI-backed checkbox and slider primitives under the form component layer
- move the singles import checkbox and auto-import confidence slider to the shared controls
- keep the import route tests aligned with the new accessible component roles
2026-05-24 21:17:20 +03:00
Antti Kettunen
aa86bedc6e
refactor(webui): key singles import state by file
- replace index-based singles selection and search state with stable staging file keys\n- keep refreshes from shifting selected rows or open search panels when files are inserted or reordered\n- add a regression test that proves selection stays attached to the intended file across refreshes
2026-05-24 21:17:20 +03:00
Antti Kettunen
89252cf6e4
fix(webui): refine import singles checkbox
- switch the singles selector to a real checkbox input for cleaner semantics\n- keep the mobile import page layout aligned while avoiding legacy button sizing\n- tune the checkbox tick so it stays visually centered and readable
2026-05-24 21:17:20 +03:00
Antti Kettunen
fe8ae8f290
fix(webui): restore import glyphs
- bring the React import page back in line with the legacy emoji/glyph treatment\n- restore album, singles, auto-import, and queue fallback icons\n- keep the visual refresh aligned with the old page while preserving the React port
2026-05-24 21:17:20 +03:00
Antti Kettunen
3fdc815794
fix(webui): show import refresh timestamp 2026-05-24 21:17:19 +03:00