Skip to content

feat: Auto-Playlists — unified management UI with master automation - #1019

Open
shkarlsson wants to merge 11 commits into
Nezreka:devfrom
shkarlsson:feat/auto-playlists
Open

feat: Auto-Playlists — unified management UI with master automation#1019
shkarlsson wants to merge 11 commits into
Nezreka:devfrom
shkarlsson:feat/auto-playlists

Conversation

@shkarlsson

Copy link
Copy Markdown
Contributor

Summary

Adds a new React page at /playlists for managing all personalized playlists from one place, with a single master automation that auto-refreshes them on configurable per-playlist schedules.

Backend Changes

  • Auto-Playlist Refresh system automation: runs hourly, discovers playlists with auto_refresh enabled in config.extra, checks their refresh_interval_hours vs last_generated_at
  • POST /api/personalized/playlist/<kind>/activate — creates playlist, enables auto-refresh, runs first generation
  • PUT /api/personalized/playlist/<kind>/auto-refresh — toggles auto-refresh or changes interval
  • Extended API responses with auto_refresh and refresh_interval_hours fields
  • unplayed_tracks generator (fixed JOIN on artists table)
  • Migration logic for legacy per-playlist personalized_pipeline automations

Frontend Changes

  • Full React page with TanStack Router + React Query
  • Active Auto-Playlists section: auto-refresh toggle + frequency selector (6h/12h/1d/2d/1w), manual refresh, editable names
  • Create New section: kind cards grouped by tag (library/discovery/other), one-click activate
  • Expanded card view: config fields (limit, max_days_since_added), track listing
  • Dark glassmorphism styling matching existing pages
  • Mobile responsive, keyboard accessible, focus-visible states
  • Loading/error states, useReactPageShell integration

Design Decisions

  • One master automation replaces per-playlist automations — cleaner, avoids N automation rows
  • config_json.extra dict used for auto_refresh and refresh_interval_hours — no schema migration needed
  • discover_due flag in pipeline action_config — clean separation between hardcoded-kind runs and dynamic discovery

Closes #N/A

SoulSync added 2 commits July 12, 2026 23:11
Add a new React page at /playlists for managing all personalized
playlists from one place:

Backend:
- Add 'Auto-Playlist Refresh' system automation that runs hourly and
  discovers playlists with auto_refresh enabled (discover_due mode)
- Add POST /api/personalized/playlist/<kind>/activate endpoint
- Add PUT /api/personalized/playlist/<kind>/auto-refresh endpoint
- Extend playlist API responses with auto_refresh and
  refresh_interval_hours fields
- Add unplayed_tracks generator (JOIN artists table fix)
- Add migration logic for legacy per-playlist personalized_pipeline
  automations

Frontend:
- Full React page with TanStack Router + React Query
- Active Auto-Playlists section with auto-refresh toggle + frequency
  selector
- Create New section with kind cards grouped by tag
- Editable playlist names, config fields, track listing
- Dark glassmorphism styling matching existing pages
- Mobile responsive, keyboard accessible, focus-visible states
- useReactPageShell integration, error/loading states
Backend:
- Add manager.delete_playlist() to remove playlist + tracks from DB
- Add api.delete_playlist() handler
- Add DELETE /api/personalized/playlist/<kind> endpoint
- Add recent_unheard generator: sort by created_at DESC, top N
  never-played tracks (replaces unplayed_tracks)
- Keep unplayed_tracks registered for backward compat

Frontend:
- Replace auto-refresh toggle with Deactivate button (with confirm)
- Add deletePlaylist API client function
- Keep refresh interval selector as secondary control
- Add btnDanger CSS style for deactivate button
- Remove unused toggle/slider CSS classes

Deactivation removes the playlist entirely (not just disabling
auto-refresh). The new Recent Unheard generator sorts by recently
added and takes the top N tracks with play_count = 0.
@shkarlsson

Copy link
Copy Markdown
Contributor Author

Updated with two changes:

1. Deactivation = Playlist Removal

  • Added DELETE /api/personalized/playlist/<kind> endpoint
  • Added manager.delete_playlist() + api.delete_playlist()
  • Replaced auto-refresh toggle with a Deactivate button (with confirmation dialog)
  • Refresh interval selector kept as secondary control
  • Deactivation removes the playlist row + all tracks from DB

2. "Recent Unheard" replaces "Unplayed Tracks"

  • New generator: sorts by created_at DESC, takes top N tracks with play_count = 0
  • Registered as recent_unheard kind (old unplayed_tracks kept for backward compat)
  • 500 tracks generated on activation
  • Updated display name, description, and UI text

SoulSync added 5 commits July 13, 2026 17:14
Previously, tracks.created_at defaulted to CURRENT_TIMESTAMP (the
SoulSync import time), ignoring the media server's addedAt field.
This caused 'recently added' playlists to show tracks in import
order rather than the order they were added to the library.

Now:
- INSERT uses the media server's addedAt for created_at
- UPDATE corrects created_at when the new addedAt differs
- Falls back to CURRENT_TIMESTAMP when addedAt is unavailable
@Nezreka

Nezreka commented Jul 15, 2026

Copy link
Copy Markdown
Owner

hey, thanks for putting this much into it. the page genuinely looks great and you picked a real problem, managing auto-playlists deserves better than it has today.

the one big thing standing in the way: this builds its own scheduler running parallel to the automation engine (per-playlist intervals in playlist config, polled by an hourly master job). all scheduling in soulsync lives in the engine, so this needs to as well. if the page drives automations the engine owns instead of its own refresh system, i'd be really happy to take it.

two smaller things to catch in that rework: the migration disables automations users set up themselves, and the master job runs as profile 1 so auto-refresh never fires for anyone else.

on the sync page there is a discovery playlist section but it's nothing like what you built. Replacing that but built on the automation engine would be huge

@shkarlsson shkarlsson closed this Jul 19, 2026
@shkarlsson

Copy link
Copy Markdown
Contributor Author

Closing this to rework based on your feedback. The page will be rebuilt to drive automations the engine owns instead of its own parallel scheduler. Will reopen when it's ready.

Rework auto-playlists to use the automation engine directly instead of
a parallel scheduler (config_json.extra polling + discover_due master job).

Backend:
- Remove 'Auto-Playlist Refresh' system automation from SYSTEM_AUTOMATIONS
- Remove discover_due orphan cleanup that disabled user automations
- Add _migrate_auto_refresh_to_automations(): converts existing
  auto_refresh=True playlists to per-playlist automation rows
- activate_playlist creates an automation row (owned_by='auto_playlist')
- toggle_auto_refresh toggles the automation's enabled status
- update_refresh_interval updates the automation's trigger_config
- delete_playlist also deletes the associated automation row
- Each automation carries the correct profile_id (not hardcoded 1)

API:
- New endpoints: activate, auto-refresh, refresh-interval, delete
- _record_to_dict enriched with automation data from the engine
- All handlers accept optional engine= parameter

Frontend:
- Types: add automation_id field
- API: add updateRefreshInterval, fix toggleAutoRefresh signature
- Page: intervalMutation uses updateRefreshInterval
@shkarlsson shkarlsson reopened this Jul 19, 2026
@shkarlsson

Copy link
Copy Markdown
Contributor Author

Hi — reworked this based on your feedback. Here's what changed:

Core change: per-playlist automation rows replace the parallel scheduler.
Each activated playlist now creates its own row in the automations table (owned_by='auto_playlist') with the correct profile_id and the user-chosen schedule. The engine handles scheduling, running, and tracking like any other automation. No more discover_due master job, no more config_json.extra polling.

Fixed the three issues you called out:

  1. Parallel scheduler removed — scheduling lives in the engine. activate_playlist creates an automation row, toggle_auto_refresh toggles its enabled flag, update_refresh_interval changes its trigger_config, delete_playlist removes both the playlist and its automation.
  2. Migration no longer disables user automations — the old orphan cleanup that killed non-system personalized_pipeline rows is gone. The new migration (_migrate_auto_refresh_to_automations) converts legacy auto_refresh=True playlists into proper automation rows, then deletes the old system row.
  3. Profile isolation fixed — each automation carries the playlist owner's profile_id (not hardcoded 1), so auto-refresh works for all profiles.

The existing playlists page and its UI are unchanged — same card grid, same track preview, same activate/deactivate/interval controls. The difference is entirely in what happens backend: create/toggle/delete real automations instead of storing flags in extra config.

SoulSync added 2 commits July 19, 2026 20:48
Backend:
- C1: Move config_json cleanup inside 'if aid:' block (prevent data
  loss when automation creation fails)
- C2: Delete playlist before automation (prevent orphaned state)
- W2: Filter _find_playlist_automation by owned_by='auto_playlist'
  (prevent matching user-created automations)
- W4: Single automation fetch in list_playlists (eliminate N+1 query)
- W5: Add owned_by filter to migration duplicate check

Frontend:
- Remove dead toggleAutoRefresh export
- Fix deletePlaylist return type to include error field
- Remove unused kind prop from PlaylistCard
- Remove dead kindsByKind memo
…ive clamping

Backend:
- _interval_to_trigger: clamp input to >= 1
- toggle_auto_refresh/update_refresh_interval: return error instead of silently creating playlist
- delete_playlist: cancel_automation before delete_automation (timer leak)
- migration: reuse _interval_to_trigger instead of duplicating logic
- migration: hoist N+1 get_automations_by_action out of loop
- web server: clamp refresh interval input to 1-168
- remove dead _discover_due_playlists code path

Frontend:
- ConfigField: blur-to-commit pattern (no per-keystroke API calls)
- ConfigField: isPending guard on both config mutations
- getConfig() reads from query cache to prevent stale closure overwrites
- useEffect: guard name sync against overwriting in-progress edits
- onBlur: guard against double-fire with isPending check
- intervalMutation: guard against concurrent fire
- select: fallback option for non-standard refresh intervals
- refreshMutation: check result.error field
- retry button on error state
@Nezreka

Nezreka commented Jul 24, 2026

Copy link
Copy Markdown
Owner

hey, this rework is exactly what i asked for. i verified the big three myself: scheduling now lives entirely in the engine (real automation rows, schedule/cancel/toggle), the migration no longer touches anything users made (i ran it against a db with an auto-sync board row and it stayed untouched, and it's idempotent), and the rows carry the owner's profile_id so the engine runs them as that profile. backend logic is solid, all existing tests pass on a merge with current dev.

three things before i can merge it:

  1. there's no way to actually reach the page. nothing in the pr touches the sidebar nav in webui/index.html, so /playlists only exists if you type the url. it also needs a checkbox in the profile permissions list (same file, the allowed-pages section), otherwise restricted profiles can never be granted it. and add 'playlists' to _DEEPLINK_VALID_PAGES in webui/static/init.js, that list is the hard-load fallback and import/stats are in it for the same reason. look at how the import page is wired in all three spots, it's the same pattern.

  2. the deactivate button uses window.confirm. we never use native dialogs, use window.showConfirmDialog instead. the react import page already calls it (webui/src/routes/import/-ui/auto-import-tab.tsx around line 641), so you can copy that exact shape.

  3. four kinds require a variant (daily_mix, genre_playlist, seasonal_mix, time_machine) and their create cards always fail with a 400 toast since activate sends an empty variant. the kinds payload already includes requires_variant and your ts type has it, so either filter those out of the create section or give them a variant picker.

couple small ones while you're in there: the old unplayed_tracks kind is still registered so users see both it and recent unheard as create cards, hide the deprecated one from the listing. and the package-lock changes are just your npm version stripping libc fields, please drop that file from the pr since package.json didn't change.

fix those and i'm happy to take this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants