feat(#461): add smart playlists with rule engine, lastfm api, and criteria editor - #498
feat(#461): add smart playlists with rule engine, lastfm api, and criteria editor#498Owie6789 wants to merge 38 commits into
Conversation
|
root cause n/a, new feature. smart playlists work by storing rules as json in the criteria column. the rule engine (playlist-rules.ts) compiles those into drizzle where clauses on the songs table. refreshSmartPlaylist clears and re-inserts playlists_songs inside a transaction. saveSmartPlaylistCriteria writes the json + calls refresh so the ui updates immediately. the 3 lastfm endpoints follow the existing get-request pattern using ipcRenderer.invoke + ipcMain.handle. -edit |
|
@CodeRabbit — comprehensive review request. Please do all of the following and report findings as inline review comments on the changed files (not in this thread), grouped by severity. 1. Careful code review
2. UI audit (only if the PR touches UI)
3. Production-readiness verification
4. Code smell sniff
5. Half-baked implementation detection
6. Required output from you
Please be thorough. I will be applying fixes from your review in a follow-up pass. |
|
The self-reported post-hoc fixes (Boolean coercion, Perform a comprehensive review of every changed file in this PR as described by the requester. Cover all six areas:
✅ Action performedFull review finished. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds smart-playlist storage, criteria validation, SQL evaluation, Last.fm retrieval and synchronization, IPC and preload APIs, renderer controls, localized messages, playlist mutation guards, and a scrobble queue migration. ChangesSmart Playlists and Last.fm Integration
Scrobble Queue Migration
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Renderer
participant PreloadAPI
participant IPC
participant LastFm
participant SmartPlaylistSync
participant Database
Renderer->>PreloadAPI: request Last.fm tracks
PreloadAPI->>IPC: invoke track retrieval
IPC->>LastFm: fetch normalized track data
LastFm-->>IPC: return tracks
IPC-->>Renderer: return tracks
Renderer->>PreloadAPI: synchronize matched songs
PreloadAPI->>IPC: invoke smart playlist sync
IPC->>SmartPlaylistSync: validate source and song IDs
SmartPlaylistSync->>Database: replace memberships and save source
Database-->>SmartPlaylistSync: return synchronized count
SmartPlaylistSync-->>Renderer: return synchronization result
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (1)
src/main/db/queries/playlist-rules.ts (1)
6-116: 🏗️ Heavy liftSplit
buildConditioninto smaller field-specific helpers.
buildConditioncurrently combines parsing, operator mapping, and all field branches in one large function. Breaking it into focused helpers will make this rule engine easier to validate and extend safely.As per coding guidelines "Keep functions small, aiming for 30-50 lines maximum per function; extract complex logic into separate helper functions for single responsibility".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/db/queries/playlist-rules.ts` around lines 6 - 116, buildCondition is too large and mixes parsing, operator mapping, and field branches; extract focused helpers and delegate each case to them. Create helpers like numericCondition(rule) (replacing numeric), stringCondition(rule) (replacing str), booleanCondition(rule) (replacing bool), existsRelationCondition(rule, relationColumn, joinTable) for genre/artist/album branches, countCondition(rule, eventsTable) for playCount/skipCount, and lastPlayedCondition(rule) for the lastPlayed logic, then have buildCondition just switch on field and call the appropriate helper (e.g., buildCondition -> numericCondition, stringCondition, booleanCondition, existsRelationCondition for 'genre'/'artist'/'album', countCondition for 'playCount'/'skipCount', lastPlayedCondition for 'lastPlayed'); ensure each helper parses value once and returns SQL | undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/db/queries/playlist-rules.ts`:
- Around line 75-96: The playCount, skipCount (and the lastPlayed branch around
lines 103-109) switch statements drop SmartPlaylistRuleOperator 'neq' silently;
update each switch in the song rule builder (the playCount, skipCount cases and
the lastPlayed case) to add a case 'neq' that returns the appropriate SQL
expression using a not-equal comparison (e.g., count(...) <> ${n} for
playCount/skipCount and a <> (or IS DISTINCT FROM if nullable timestamps are
involved) comparison for lastPlayed) instead of falling through to undefined so
'neq' rules are honored.
- Around line 9-20: The numeric helper currently coerces value to Number without
guarding for NaN/Infinity, which can emit invalid SQL; modify the numeric
function (and the other numeric coercion helpers in this file) to compute const
n = Number(value) and immediately return undefined if !Number.isFinite(n) before
the switch on operator so malformed inputs are ignored and no SQL condition is
emitted for non-finite numbers.
In `@src/main/db/schema.ts`:
- Around line 303-304: The playlists table in schema.ts is missing the index
declared in src/resources/drizzle/0000_add_smart_playlist_columns.sql
(idx_playlists_is_smart), causing schema drift; open the playlists table
definition in src/main/db/schema.ts and add a Drizzle index named
idx_playlists_is_smart targeting the isSmart column (the same column declared as
isSmart: boolean('is_smart').notNull().default(false)), then run npm run
db:generate to regenerate migrations so the Drizzle schema and SQL index stay in
sync.
In `@src/main/ipc.ts`:
- Around line 340-358: The Last.fm IPC handlers ('app/lastfm/getUserTopTracks',
'app/lastfm/getUserRecentTracks', 'app/lastfm/getUserLovedTracks') accept
renderer-controlled inputs without validation; update each ipcMain.handle
callback to trim and validate the username (reject or return an error for
empty/invalid usernames), clamp/normalize limit to a safe range (e.g., min 1,
max 100) and coerce types, and for getUserTopTracks also validate the period
against the allowed set ('overall','7day','1month','3month','6month','12month')
before calling getUserTopTracks/getUserRecentTracks/getUserLovedTracks so
network helpers only receive cleaned, bounded inputs.
- Around line 481-493: Extract the orchestration inside the ipcMain.handle
handlers into a core business API (e.g., create functions like
saveAndRefreshSmartPlaylist(playlistId, criteria) and
refreshSmartPlaylistById(playlistId)) in src/main/core; move logic that calls
updatePlaylistCriteria, refreshSmartPlaylist and getPlaylistById (and the
JSON.parse of playlist.criteria / SmartPlaylistCriteria handling) into those
core functions, export them, then simplify the ipc handlers in ipc.ts to just
call these core functions (ipcMain.handle('app/saveSmartPlaylistCriteria' ->
call saveAndRefreshSmartPlaylist; ipcMain.handle('app/refreshSmartPlaylist' ->
call refreshSmartPlaylistById) and return their { songIds } results). Ensure
type usage (SmartPlaylistCriteria) is preserved and core functions are
unit-testable.
- Around line 487-492: Wrap the JSON.parse of playlist.criteria inside a
try/catch within the ipcMain.handle('app/refreshSmartPlaylist') handler: if
getPlaylistById returns a playlist but parsing fails, catch the error, log it
(use the existing logger used elsewhere in ipc.ts), and return the safe fallback
{ songIds: [] } instead of letting the IPC call reject; on successful parse
continue to call refreshSmartPlaylist(playlistId, criteria) and return its
result.
In `@src/main/other/lastFm/getUserLovedTracks.ts`:
- Around line 25-46: In getUserLovedTracks, change the URL construction to use
'https://ws.audioscrobbler.com/2.0/' instead of 'http://...', and wrap the
fetch(url) call with an AbortController-based timeout (create AbortController,
pass controller.signal to fetch, set a timer to controller.abort() after a
reasonable ms and clear it on success) to bound the request; additionally, when
res.ok is false, read and log the response status and body (or at least
status/text) and return undefined (or throw) so failures are visible—refer to
symbols url, LAST_FM_API_KEY, fetch(...), AbortController, res and
getUserLovedTracks when applying the fix.
In `@src/main/other/lastFm/getUserRecentTracks.ts`:
- Around line 26-48: Update the request in getUserRecentTracks to use HTTPS for
the Last.fm endpoint, add an AbortController-based timeout around the fetch call
(create controller, pass controller.signal to fetch, and clear/abort on
timeout), and change the non-OK response handling so it throws a descriptive
Error (including HTTP status and statusText or body) instead of returning
undefined; adjust the fetch/response logic around the existing url construction,
the fetch(url) call, and the res.ok branch so that errors always throw and the
caller/catch will log them.
In `@src/main/other/lastFm/getUserTopTracks.ts`:
- Around line 27-50: In getUserTopTracks update the URL to use HTTPS and make
the fetch call fail-fast by using an AbortController with a short timeout (e.g.,
a few seconds) and cancelling the request on timeout; if the response is not OK,
throw an Error containing the HTTP status and statusText instead of returning
undefined, and include any JSON error details when available (references:
getUserTopTracks, url, fetch call, LAST_FM_API_KEY, and the data.error
handling).
In `@src/renderer/src/components/SmartPlaylistCriteriaEditor.tsx`:
- Around line 180-181: In SmartPlaylistCriteriaEditor, the input elements
currently use the hardcoded placeholder "value" (placeholder="value" at the JSX
for the inputs around the shown diff and similarly at the other occurrence);
replace those with the localized string from your app i18n helper (e.g. call the
existing translation function/hook such as t('smartPlaylist.criteria.value') or
useI18n().t(...) and pass that result to the placeholder prop) so both input
placeholders are localized; ensure you import/use the same translation hook
already used elsewhere in the component and pick a descriptive translation key
like "smartPlaylist.criteria.value".
- Around line 125-143: The save path calling
window.api.playlistsData.saveSmartPlaylistCriteria can reject and currently
leaves an unhandled rejection; wrap the await in a try/catch inside the async
callback (the useCallback around the save logic in SmartPlaylistCriteriaEditor)
so that on error you call addNewNotifications with the 'smartPlaylistSaveFailed'
payload and avoid assuming success, and only call changePromptMenuData(false)
after a confirmed successful save (result true). Ensure you reference
saveSmartPlaylistCriteria, result, addNewNotifications and changePromptMenuData
when making the changes.
- Around line 38-47: The JSON.parse result used in useMemo for initialCriteria
may produce an object that doesn't match SmartPlaylistCriteria (missing/invalid
rules) so add a validation guard: after parsing playlist.criteria in the
initialCriteria useMemo, verify the parsed value satisfies the
SmartPlaylistCriteria shape (e.g., has a rules array and required fields) — if
it fails validation, return defaultCriteria(); implement or call a small
type-guard helper (e.g., isValidSmartPlaylistCriteria(parsed)) and use that in
the initialCriteria block to ensure rendering won't break.
---
Nitpick comments:
In `@src/main/db/queries/playlist-rules.ts`:
- Around line 6-116: buildCondition is too large and mixes parsing, operator
mapping, and field branches; extract focused helpers and delegate each case to
them. Create helpers like numericCondition(rule) (replacing numeric),
stringCondition(rule) (replacing str), booleanCondition(rule) (replacing bool),
existsRelationCondition(rule, relationColumn, joinTable) for genre/artist/album
branches, countCondition(rule, eventsTable) for playCount/skipCount, and
lastPlayedCondition(rule) for the lastPlayed logic, then have buildCondition
just switch on field and call the appropriate helper (e.g., buildCondition ->
numericCondition, stringCondition, booleanCondition, existsRelationCondition for
'genre'/'artist'/'album', countCondition for 'playCount'/'skipCount',
lastPlayedCondition for 'lastPlayed'); ensure each helper parses value once and
returns SQL | undefined.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7ed705ed-2a12-4234-96f5-45b771071ceb
📒 Files selected for processing (19)
src/main/db/queries/playlist-rules.tssrc/main/db/queries/playlists.tssrc/main/db/schema.tssrc/main/ipc.tssrc/main/other/lastFm/getUserLovedTracks.tssrc/main/other/lastFm/getUserRecentTracks.tssrc/main/other/lastFm/getUserTopTracks.tssrc/main/utils/convert.tssrc/preload/index.tssrc/renderer/src/assets/locales/en/en.jsonsrc/renderer/src/components/PlaylistsPage/Playlist.tsxsrc/renderer/src/components/SearchPage/Result_Containers/PlaylistSearchResultsContainer.tsxsrc/renderer/src/components/SmartPlaylistCriteriaEditor.tsxsrc/renderer/src/components/SongsPage/AddSongsToPlaylistsPrompt.tsxsrc/renderer/src/routes/main-player/playlists/$playlistId.tsxsrc/renderer/src/routes/main-player/playlists/favorites.tsxsrc/renderer/src/routes/main-player/playlists/history.tsxsrc/resources/drizzle/0000_add_smart_playlist_columns.sqlsrc/types/app.d.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Use module-level initialization for singleton services (AudioPlayer, PlayerQueue) accessed by intervals, timers, or event handlers, NOT useRef-based singletons, to avoid stale closure issues
Always return cleanup functions in useEffect hooks that register IPC event listeners, and ensure corresponding removal functions are called to prevent memory leaks
Use descriptive, context-rich variable and function names; avoid generic names like 'data', 'user', 'info', 'temp'. Functions should start with verbs (calculateDuration, fetchSongData, validatePlaylist)
Use guard clauses with early returns to keep function logic flat and readable, avoiding deep nesting and improving error handling clarity
Use eslint-plugin-simple-import-sort for automatic import organization: external dependencies, internal path aliases (@renderer,@main,@common), then relative imports
All data fetching errors should be handled within queryFn, returning safe defaults (empty arrays, null, default objects) instead of throwing exceptions
Keep functions small, aiming for 30-50 lines maximum per function; extract complex logic into separate helper functions for single responsibility
Files:
src/main/db/schema.tssrc/main/db/queries/playlists.tssrc/renderer/src/routes/main-player/playlists/favorites.tsxsrc/main/utils/convert.tssrc/main/other/lastFm/getUserTopTracks.tssrc/renderer/src/components/SearchPage/Result_Containers/PlaylistSearchResultsContainer.tsxsrc/main/db/queries/playlist-rules.tssrc/main/other/lastFm/getUserLovedTracks.tssrc/renderer/src/components/PlaylistsPage/Playlist.tsxsrc/types/app.d.tssrc/renderer/src/routes/main-player/playlists/history.tsxsrc/main/other/lastFm/getUserRecentTracks.tssrc/main/ipc.tssrc/preload/index.tssrc/renderer/src/routes/main-player/playlists/$playlistId.tsxsrc/renderer/src/components/SmartPlaylistCriteriaEditor.tsxsrc/renderer/src/components/SongsPage/AddSongsToPlaylistsPrompt.tsx
src/main/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All business logic should be in src/main/core/ modules with single responsibility, following the pattern of taking data as parameters and calling database queries or external APIs
Files:
src/main/db/schema.tssrc/main/db/queries/playlists.tssrc/main/utils/convert.tssrc/main/other/lastFm/getUserTopTracks.tssrc/main/db/queries/playlist-rules.tssrc/main/other/lastFm/getUserLovedTracks.tssrc/main/other/lastFm/getUserRecentTracks.tssrc/main/ipc.ts
src/main/db/schema.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All database schema changes must be made here using Drizzle table definitions, then run npm run db:generate to create migrations automatically
Files:
src/main/db/schema.ts
src/main/db/queries/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All database queries must be defined in dedicated query modules following the pattern: one file per entity (songs.ts, artists.ts, playlists.ts, etc.) using Drizzle ORM methods
Files:
src/main/db/queries/playlists.tssrc/main/db/queries/playlist-rules.ts
src/renderer/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
src/renderer/src/**/*.{ts,tsx}: Use dispatch() function from store.ts for all state updates in the renderer, never modify store state directly
Extract all data fetching logic into centralized query modules using createQueryKeys factory pattern from@lukemorales/query-key-factoryin src/renderer/src/queries/, never inline fetch logic in components
Use useSuspenseQuery() for data fetching in components with TanStack Router loaders for pre-fetching, not custom fetch hooks or useQuery without suspense
Use TanStack Router's , useNavigate(), and useRouter() for navigation, not deprecated changeCurrentActivePage() or updatePageHistoryIndex() functions
Files:
src/renderer/src/routes/main-player/playlists/favorites.tsxsrc/renderer/src/components/SearchPage/Result_Containers/PlaylistSearchResultsContainer.tsxsrc/renderer/src/components/PlaylistsPage/Playlist.tsxsrc/renderer/src/routes/main-player/playlists/history.tsxsrc/renderer/src/routes/main-player/playlists/$playlistId.tsxsrc/renderer/src/components/SmartPlaylistCriteriaEditor.tsxsrc/renderer/src/components/SongsPage/AddSongsToPlaylistsPrompt.tsx
src/renderer/src/**/*.tsx
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All custom hooks that integrate features into App.tsx must be called in App.tsx with no return value (they manage state via dispatch and event listeners internally)
Files:
src/renderer/src/routes/main-player/playlists/favorites.tsxsrc/renderer/src/components/SearchPage/Result_Containers/PlaylistSearchResultsContainer.tsxsrc/renderer/src/components/PlaylistsPage/Playlist.tsxsrc/renderer/src/routes/main-player/playlists/history.tsxsrc/renderer/src/routes/main-player/playlists/$playlistId.tsxsrc/renderer/src/components/SmartPlaylistCriteriaEditor.tsxsrc/renderer/src/components/SongsPage/AddSongsToPlaylistsPrompt.tsx
src/main/ipc.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Register all IPC handlers using ipcMain.handle() for async operations with return values and ipcMain.on() for fire-and-forget events, mapping them to core business logic modules
Files:
src/main/ipc.ts
src/preload/index.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All IPC communication to main process must be exposed through window.api with categorized namespaces (playerControls, audioLibraryControls, settingsHelpers, etc.) for type safety
Files:
src/preload/index.ts
🧠 Learnings (2)
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/main/db/queries/**/*.ts : All database queries must be defined in dedicated query modules following the pattern: one file per entity (songs.ts, artists.ts, playlists.ts, etc.) using Drizzle ORM methods
Applied to files:
src/main/db/queries/playlists.ts
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to **/*.{ts,tsx} : Use descriptive, context-rich variable and function names; avoid generic names like 'data', 'user', 'info', 'temp'. Functions should start with verbs (calculateDuration, fetchSongData, validatePlaylist)
Applied to files:
src/main/ipc.ts
🔇 Additional comments (12)
src/types/app.d.ts (1)
73-78: LGTM!Also applies to: 700-727, 738-739
src/main/utils/convert.ts (1)
105-107: LGTM!src/resources/drizzle/0000_add_smart_playlist_columns.sql (1)
1-4: LGTM!src/main/db/queries/playlists.ts (1)
259-268: LGTM!src/renderer/src/assets/locales/en/en.json (1)
76-76: LGTM!Also applies to: 269-302
src/renderer/src/components/PlaylistsPage/Playlist.tsx (1)
18-18: LGTM!Also applies to: 213-220, 425-430
src/renderer/src/components/SearchPage/Result_Containers/PlaylistSearchResultsContainer.tsx (1)
49-50: LGTM!src/renderer/src/components/SongsPage/AddSongsToPlaylistsPrompt.tsx (1)
123-125: LGTM!Also applies to: 134-135
src/renderer/src/routes/main-player/playlists/$playlistId.tsx (1)
24-26: LGTM!Also applies to: 150-177, 219-230, 279-279
src/renderer/src/routes/main-player/playlists/favorites.tsx (1)
37-39: LGTM!src/renderer/src/routes/main-player/playlists/history.tsx (1)
35-37: LGTM!src/preload/index.ts (1)
430-448: LGTM!Also applies to: 494-497, 648-648
| ipcMain.handle('app/saveSmartPlaylistCriteria', async (_, playlistId: number, criteria: SmartPlaylistCriteria) => { | ||
| await updatePlaylistCriteria(playlistId, criteria); | ||
| const songIds = await refreshSmartPlaylist(playlistId, criteria); | ||
| return { songIds }; | ||
| }); | ||
|
|
||
| ipcMain.handle('app/refreshSmartPlaylist', async (_, playlistId: number) => { | ||
| const playlist = await getPlaylistById(playlistId); | ||
| if (!playlist?.criteria) return { songIds: [] }; | ||
| const criteria: SmartPlaylistCriteria = JSON.parse(playlist.criteria); | ||
| const songIds = await refreshSmartPlaylist(playlistId, criteria); | ||
| return { songIds }; | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Move smart-playlist orchestration out of ipc.ts into a core module.
initializeIPC now contains write/parse/refresh business flow. Keep IPC handlers as transport adapters and delegate this flow to src/main/core/* for single-responsibility and testability.
As per coding guidelines src/main/ipc.ts: “Register all IPC handlers … mapping them to core business logic modules”, and src/main/**/*.ts: “All business logic should be in src/main/core/ modules…”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/ipc.ts` around lines 481 - 493, Extract the orchestration inside the
ipcMain.handle handlers into a core business API (e.g., create functions like
saveAndRefreshSmartPlaylist(playlistId, criteria) and
refreshSmartPlaylistById(playlistId)) in src/main/core; move logic that calls
updatePlaylistCriteria, refreshSmartPlaylist and getPlaylistById (and the
JSON.parse of playlist.criteria / SmartPlaylistCriteria handling) into those
core functions, export them, then simplify the ipc handlers in ipc.ts to just
call these core functions (ipcMain.handle('app/saveSmartPlaylistCriteria' ->
call saveAndRefreshSmartPlaylist; ipcMain.handle('app/refreshSmartPlaylist' ->
call refreshSmartPlaylistById) and return their { songIds } results). Ensure
type usage (SmartPlaylistCriteria) is preserved and core functions are
unit-testable.
…s + lastfm)
playlist-rules.ts
- numeric helper now returns undefined for !Number.isFinite(n), guarding
against NaN/Infinity values from malformed rule inputs
- playCount / skipCount switches now handle 'neq' operator (was silently
dropped before)
- lastPlayed switch now handles 'neq' (via not-between on the same window)
- All three branches guard against !Number.isFinite(value) before the switch
getUserLovedTracks.ts / getUserRecentTracks.ts / getUserTopTracks.ts
- URL changed http -> https
- Added AbortController-based 10s timeout around fetch
- Non-OK response now throws Error with status+statusText instead of
returning undefined (errors are now visible to callers)
- Catch block re-throws so the IPC layer can surface them
ipc.ts
- Last.fm IPC handlers now validate inputs at the boundary:
trim username, reject empty, clamp limit to [1,100], validate period
against allowed set
- app/refreshSmartPlaylist JSON.parse is now wrapped in try/catch with
a safe { songIds: [] } fallback and logger.error
schema.ts
- Added idx_playlists_is_smart Drizzle index to match the
0000_add_smart_playlist_columns.sql migration (closes schema drift)
SmartPlaylistCriteriaEditor.tsx
- Added isValidCriteriaShape type guard: parsed JSON must have
matchType ALL/ANY and rules[] with string field/operator
- placeholder='value' replaced with t('playlist.criteriaValuePlaceholder')
- saveCriteria now wraps the IPC call in try/catch and notifies on error
(previously unhandled rejection)
en.json
- Added playlist.criteriaValuePlaceholder = 'value'
Summary
Adds smart playlist support (rule-based auto-populated playlists) and Last.fm integration: fetch user top/recent/loved tracks and sync matching local songs into a smart playlist.
Motivation
Issue #461 requests automatic playlists based on listening habits via Last.fm, including late-night, workout/high-energy, recently played, and frequently played tracks. This PR delivers the rule-based playlist engine, the creation flow, the criteria editor, and the Last.fm fetch + sync pipeline.
Changes by file
Schema and Migrations
resources/drizzle/0004_mixed_adam_destine.sql— addsis_smart,criteriacolumns to playlists table with index onis_smartresources/drizzle/0005_talented_master_chief.sql— scrobble_queue migration, renumbered into the journal chainsrc/main/db/schema.ts— playlist schema additionsCore Smart Playlist Engine
src/main/db/queries/playlist-rules.ts— rule compiler for 11 fields (year, duration, bitRate, isFavorite, isBlacklisted, genre, artist, album, playCount, skipCount, lastPlayed) with operator support per field;evaluateSmartPlaylist()builds SQL with AND/OR matching inside the transaction;refreshSmartPlaylist()atomically replaces membership; case-insensitive ILIKE with wildcard escaping; correct NULL handling for nullable columns and untagged songs; zero-condition safety returns empty setsrc/main/db/queries/validateSmartPlaylistCriteria.ts— shared main-process validator for per-rule field/operator/value/limit checks, used by both save and refreshsrc/main/db/queries/playlists.ts—updatePlaylistCriteria()for persisting criteria JSONSmart Playlist Creation
src/main/core/addNewPlaylist.ts— acceptsisSmartflag, creation flow through IPCsrc/renderer/src/components/PlaylistsPage/NewPlaylistPrompt.tsx— smart playlist toggle, trim validation, pending state, duplicate-submit guardsrc/renderer/src/components/SmartPlaylistCriteriaEditor.tsx— visual rule editor with field/operator selection, accessible controls, stable rule IDs, empty numeric input preserved, Last.fm source cleared on rule saveLast.fm Integration
src/main/other/lastFm/getUserTopTracks.ts,getUserRecentTracks.ts,getUserLovedTracks.ts— fetch APIs with HTTPS, URLSearchParams, 10s timeout, HTTP/API error checkssrc/main/core/syncLastFmToSmartPlaylist.ts— atomic membership replacement + Last.fm source persistence in one transaction, input validation at IPC boundarysrc/renderer/src/hooks/useLastFmConsumer.ts— fetch-and-match pipeline with per-request abort, pending state, i18n notificationssrc/renderer/src/routes/main-player/playlists/$playlistId.tsx— Sync from Last.fm action, disabled while syncingIPC and Preload
src/main/ipc.ts— save/refresh/sync handlers with per-playlist lock serialization, discriminated results,.finally()lock cleanup, input validationsrc/preload/index.ts— typed preload bindings matching the discriminated result contractsSafety
src/main/core/addSongsToPlaylist.ts/removeSongFromPlaylist.ts— smart playlist mutation guards at main-process boundary with renderer notification, discriminated return valuesCANNOT_MODIFY_SMART_PLAYLISTwired into notification config with locale stringTests
test/src/main/db/queries/validateSmartPlaylistCriteria.test.ts— 23 tests covering valid/invalid rules, null entries, field/operator/value validation, limitsTest Plan
%,_, and\wildcardsSummary by CodeRabbit
New Features
Bug Fixes
Fixes #461