From c52a2eec66af3f09e5e3e601c1656836b40e2198 Mon Sep 17 00:00:00 2001 From: Darren Labithiotis Date: Sat, 8 Aug 2026 08:41:52 +0100 Subject: [PATCH] fix(cli): key duplicate playlist tracks by position --- bun.lock | 24 +------ packages/cli/package.json | 2 +- packages/cli/src/ui/DoneScreen.tsx | 2 +- packages/cli/src/ui/SyncScreen.tsx | 2 +- .../cli/src/ui/components/ListPanel.test.tsx | 2 +- packages/cli/src/ui/utils/workerSlots.test.ts | 63 +++++++++++++++++++ packages/cli/src/ui/utils/workerSlots.ts | 5 +- 7 files changed, 74 insertions(+), 26 deletions(-) create mode 100644 packages/cli/src/ui/utils/workerSlots.test.ts diff --git a/bun.lock b/bun.lock index 9c40581..e6e2d73 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ }, "packages/cli": { "name": "@mdlx/cli", - "version": "0.0.24", + "version": "0.0.25", "bin": { "mdl": "release/cli.js", }, @@ -23,7 +23,7 @@ "effect": "3.21.0", "ink": "7.0.0", "ink-link": "5.0.0", - "ink-scroll-view": "0.3.6", + "ink-scroll-view": "0.3.7", "ink-spinner": "5.0.0", "react": "19.2.0", "sanitize-filename": "1.6.4", @@ -588,7 +588,7 @@ "ink-link": ["ink-link@5.0.0", "", { "dependencies": { "terminal-link": "^5.0.0" }, "peerDependencies": { "ink": ">=6" } }, "sha512-TFDXc/0mwUW7LMjsr0/LeLxPVV5BnHDuDQff9RCgP4rb3R+V/4dIwGBZbCevcJZtQnVcW+Iz1LUrUbpq+UDwYA=="], - "ink-scroll-view": ["ink-scroll-view@0.3.6", "", { "peerDependencies": { "ink": "6.8.0", "react": "19.2.0" } }, "sha512-XqLkmFFCA5Ow2lVyvv7q0aVHzB78XojIM8gZMQ3//lvy8ZDpJsXWtTRwsCEsdQHnwvMCDCnKCzgF4xkkzoN8mQ=="], + "ink-scroll-view": ["ink-scroll-view@0.3.7", "", { "peerDependencies": { "ink": "^5 || ^6 || ^7", "react": "^18 || ^19" } }, "sha512-lUBLxSbVry/+UtJhuvRu6wumP43+ScVp69J7e+hmYwz1kTkahWfkVwWOu7Mn1DMPb8AU8bnsmEsr6kvSJvnaRw=="], "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "2.9.2" }, "peerDependencies": { "ink": "6.8.0", "react": "19.2.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], @@ -856,8 +856,6 @@ "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], - "ink-scroll-view/ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "0.2.5", "ansi-escapes": "7.3.0", "ansi-styles": "6.2.3", "auto-bind": "5.0.1", "chalk": "5.6.2", "cli-boxes": "3.0.0", "cli-cursor": "4.0.0", "cli-truncate": "5.2.0", "code-excerpt": "4.0.0", "es-toolkit": "1.45.1", "indent-string": "5.0.0", "is-in-ci": "2.0.0", "patch-console": "2.0.0", "react-reconciler": "0.33.0", "scheduler": "0.27.0", "signal-exit": "3.0.7", "slice-ansi": "8.0.0", "stack-utils": "2.0.6", "string-width": "8.2.0", "terminal-size": "4.0.1", "type-fest": "5.5.0", "widest-line": "6.0.0", "wrap-ansi": "9.0.2", "ws": "8.19.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@types/react": "19.2.2" }, "peerDependencies": { "react": "19.2.0" } }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="], - "ink-spinner/ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "0.2.5", "ansi-escapes": "7.3.0", "ansi-styles": "6.2.3", "auto-bind": "5.0.1", "chalk": "5.6.2", "cli-boxes": "3.0.0", "cli-cursor": "4.0.0", "cli-truncate": "5.2.0", "code-excerpt": "4.0.0", "es-toolkit": "1.45.1", "indent-string": "5.0.0", "is-in-ci": "2.0.0", "patch-console": "2.0.0", "react-reconciler": "0.33.0", "scheduler": "0.27.0", "signal-exit": "3.0.7", "slice-ansi": "8.0.0", "stack-utils": "2.0.6", "string-width": "8.2.0", "terminal-size": "4.0.1", "type-fest": "5.5.0", "widest-line": "6.0.0", "wrap-ansi": "9.0.2", "ws": "8.19.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@types/react": "19.2.2" }, "peerDependencies": { "react": "19.2.0" } }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="], "miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], @@ -910,18 +908,6 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "ink-scroll-view/ink/@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "6.2.3", "is-fullwidth-code-point": "5.1.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], - - "ink-scroll-view/ink/cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - - "ink-scroll-view/ink/cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "8.0.0", "string-width": "8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], - - "ink-scroll-view/ink/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "6.2.3", "is-fullwidth-code-point": "5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], - - "ink-scroll-view/ink/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "6.2.3", "string-width": "7.2.0", "strip-ansi": "7.2.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - - "ink-scroll-view/ink/ws": ["ws@8.19.0", "", {}, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "ink-spinner/ink/@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "6.2.3", "is-fullwidth-code-point": "5.1.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], "ink-spinner/ink/cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], @@ -1058,8 +1044,6 @@ "@tailwindcss/vite/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "ink-scroll-view/ink/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "10.6.0", "get-east-asian-width": "1.5.0", "strip-ansi": "7.2.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "ink-spinner/ink/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "10.6.0", "get-east-asian-width": "1.5.0", "strip-ansi": "7.2.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1104,8 +1088,6 @@ "@mdl/youtube-token/wrangler/miniflare/youch/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - "ink-scroll-view/ink/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "ink-spinner/ink/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 4b0a8c5..c061f8b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -55,7 +55,7 @@ "effect": "3.21.0", "ink": "7.0.0", "ink-link": "5.0.0", - "ink-scroll-view": "0.3.6", + "ink-scroll-view": "0.3.7", "ink-spinner": "5.0.0", "react": "19.2.0", "sanitize-filename": "1.6.4", diff --git a/packages/cli/src/ui/DoneScreen.tsx b/packages/cli/src/ui/DoneScreen.tsx index 9b8bb58..86f2d34 100644 --- a/packages/cli/src/ui/DoneScreen.tsx +++ b/packages/cli/src/ui/DoneScreen.tsx @@ -39,7 +39,7 @@ export function DoneScreen(props: DoneScreenProps) { {props.workerSlots.map((slot) => ( - + ))} diff --git a/packages/cli/src/ui/SyncScreen.tsx b/packages/cli/src/ui/SyncScreen.tsx index b326626..690d536 100644 --- a/packages/cli/src/ui/SyncScreen.tsx +++ b/packages/cli/src/ui/SyncScreen.tsx @@ -43,7 +43,7 @@ export function SyncScreen(props: SyncScreenProps) { borderColor="magenta" minRows={20} items={visibleSlots} - renderItem={(slot) => } + renderItem={(slot) => } /> ); diff --git a/packages/cli/src/ui/components/ListPanel.test.tsx b/packages/cli/src/ui/components/ListPanel.test.tsx index ae87033..423b4cb 100644 --- a/packages/cli/src/ui/components/ListPanel.test.tsx +++ b/packages/cli/src/ui/components/ListPanel.test.tsx @@ -16,7 +16,7 @@ describe('ListPanel', () => { } + renderItem={(slot) => } title="Tracks" viewportHeight={4} /> diff --git a/packages/cli/src/ui/utils/workerSlots.test.ts b/packages/cli/src/ui/utils/workerSlots.test.ts new file mode 100644 index 0000000..430ccc7 --- /dev/null +++ b/packages/cli/src/ui/utils/workerSlots.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from 'bun:test'; +import type { PlaylistMetadata, SyncProgress } from '../../lib/types'; +import { createWorkerSlots, updateWorkerSlots } from './workerSlots'; + +describe('updateWorkerSlots', () => { + test('updates the indexed duplicate track instead of the first matching track ID', () => { + const playlist: PlaylistMetadata = { + id: 'playlist-1', + provider: 'spotify', + sourceUrl: 'https://open.spotify.com/playlist/playlist-1', + title: 'Duplicate tracks', + tracks: [ + { artists: ['Artist'], id: 'duplicate-track', title: 'First copy' }, + { artists: ['Artist'], id: 'duplicate-track', title: 'Second copy' }, + ], + }; + const progress: SyncProgress = { + completed: 1, + current: 2, + downloaded: 1, + failed: 0, + message: 'Finished', + playlistDir: '/music/Duplicate tracks', + skipped: 0, + stage: 'completed', + total: 2, + track: playlist.tracks[1], + trackIndex: 2, + }; + + const slots = updateWorkerSlots(createWorkerSlots(playlist, 1), playlist, progress); + + expect(slots.map((slot) => slot.stage)).toEqual(['queued', 'completed']); + expect(slots.map((slot) => slot.message)).toEqual(['Queued', 'Finished']); + }); + + test('ignores progress with an unknown track index', () => { + const playlist: PlaylistMetadata = { + id: 'playlist-1', + provider: 'spotify', + sourceUrl: 'https://open.spotify.com/playlist/playlist-1', + title: 'One track', + tracks: [{ artists: ['Artist'], id: 'track-1', title: 'Track' }], + }; + const progress: SyncProgress = { + completed: 0, + current: 2, + downloaded: 0, + failed: 0, + message: 'Preparing track', + playlistDir: '/music/One track', + skipped: 0, + stage: 'initializing', + total: 1, + track: playlist.tracks[0], + trackIndex: 2, + }; + + expect(updateWorkerSlots(createWorkerSlots(playlist, 1), playlist, progress)).toEqual( + createWorkerSlots(playlist, 1) + ); + }); +}); diff --git a/packages/cli/src/ui/utils/workerSlots.ts b/packages/cli/src/ui/utils/workerSlots.ts index 94f128b..03daea4 100644 --- a/packages/cli/src/ui/utils/workerSlots.ts +++ b/packages/cli/src/ui/utils/workerSlots.ts @@ -34,7 +34,10 @@ export function updateWorkerSlots( return slots; } - const slotIndex = slots.findIndex((slot) => slot.trackId === trackId); + const slotIndex = + progress.trackIndex === undefined + ? slots.findIndex((slot) => slot.trackId === trackId) + : slots.findIndex((slot) => slot.trackIndex === progress.trackIndex); if (slotIndex < 0) { return slots; }