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;
}