@naxodev/opencode-music-player 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +67 -47
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,6 +14,8 @@ Read the [music session architecture field guide](../../docs/music-session-archi
14
14
 
15
15
  ## Artwork
16
16
 
17
+ The player retains a completed cover when the same recording loses duration metadata or reports a duration within one second of the resolved value. Conflicting recording metadata starts a new acquisition. A catalog duration keeps the seek bar usable when the provider has no duration; this host-only fallback does not change daemon playback state. A valid provider duration takes priority.
18
+
17
19
  The daemon performs the bounded native `media-control get --now` read and validates the complete recording identity before and after the read. OpenCode uses those bytes when available, then keeps iTunes Search fallback, image downloads, conversion, cache/job ownership, and terminal rendering locally. Artwork failure never blocks playback state.
18
20
 
19
21
  Catalog matching, bounded downloads, cancellation, and transient retries use the [shared host-side acquisition policy](../music-core/README.md#host-side-catalog-artwork). OpenCode keeps image conversion and its shared presentation cache/jobs. Track changes remove only the changing view's interest; another view can keep the same job alive.
package/dist/index.js CHANGED
@@ -181,6 +181,53 @@ async function resolveArtworkDetails(id, target, nativeBase64, legacyId = id, fe
181
181
  }
182
182
  }
183
183
 
184
+ // types.ts
185
+ import {
186
+ emptyPlayer as emptyCorePlayer,
187
+ formatMs,
188
+ isMac
189
+ } from "@naxodev/music-core";
190
+ function presentationDuration(track) {
191
+ for (const duration of [track?.duration_ms, track?.artwork_duration_ms])
192
+ if (typeof duration === "number" && Number.isFinite(duration) && duration > 0 && duration <= 86400000)
193
+ return duration;
194
+ return 0;
195
+ }
196
+ function compatibleArtworkDuration(previous, resolved, next) {
197
+ return next === previous || next === 0 || typeof resolved === "number" && resolved > 0 && Number.isFinite(resolved) && Math.abs(next - resolved) <= 1000;
198
+ }
199
+ function mergeArtworkCompletion(player, event) {
200
+ const track = player?.track;
201
+ if (!track || track.name !== event.identity.title || track.artists !== event.identity.artist || track.album !== event.identity.album || track.duration_ms > 0 && track.duration_ms !== event.identity.duration_ms) {
202
+ return player;
203
+ }
204
+ return {
205
+ ...player,
206
+ track: {
207
+ ...track,
208
+ artwork: event.artwork,
209
+ artwork_loading: false,
210
+ artwork_duration_ms: event.duration_ms
211
+ }
212
+ };
213
+ }
214
+ function mergePlayerSnapshot(player, snapshot) {
215
+ const current = player?.track;
216
+ const next = snapshot.track;
217
+ if (!current || !next || current.name !== next.name || current.artists !== next.artists || current.album !== next.album || !compatibleArtworkDuration(current.duration_ms, current.artwork_duration_ms, next.duration_ms) || current.artwork === null && current.artwork_loading !== false) {
218
+ return snapshot;
219
+ }
220
+ return {
221
+ ...snapshot,
222
+ track: {
223
+ ...next,
224
+ artwork: next.artwork ?? current.artwork,
225
+ artwork_loading: false,
226
+ artwork_duration_ms: next.artwork_duration_ms || current.artwork_duration_ms || 0
227
+ }
228
+ };
229
+ }
230
+
184
231
  // system-media.ts
185
232
  var MAX_ARTWORK_ENTRIES = 32;
186
233
  var artworkCache = new Map;
@@ -377,6 +424,7 @@ function createSessionSystemMedia(overrides = {}) {
377
424
  let disposed = false;
378
425
  let currentArtworkIdentity = null;
379
426
  let currentArtworkKey = null;
427
+ let currentArtworkTrack = null;
380
428
  let client;
381
429
  let installed = false;
382
430
  let latest;
@@ -449,10 +497,15 @@ function createSessionSystemMedia(overrides = {}) {
449
497
  emit(event);
450
498
  };
451
499
  const project = (state) => {
452
- const nextKey = state?.state.track ? artworkCacheKey(identityFromTrack(state.state.track)) : null;
500
+ const nextIdentity = state?.state.track ? identityFromTrack(state.state.track) : null;
501
+ let nextKey = nextIdentity ? artworkCacheKey(nextIdentity) : null;
502
+ const retained = currentArtworkKey ? artworkCache.get(currentArtworkKey) : undefined;
503
+ if (nextIdentity && currentArtworkTrack && retained?.value && nextIdentity.title === currentArtworkTrack.title && nextIdentity.artist === currentArtworkTrack.artist && nextIdentity.album === currentArtworkTrack.album && compatibleArtworkDuration(currentArtworkTrack.duration_ms, retained.duration_ms, nextIdentity.duration_ms))
504
+ nextKey = currentArtworkKey;
453
505
  if (nextKey !== currentArtworkKey)
454
506
  removeArtworkInterests(host);
455
507
  currentArtworkKey = nextKey;
508
+ currentArtworkTrack = nextIdentity;
456
509
  if (!state) {
457
510
  currentArtworkIdentity = null;
458
511
  return null;
@@ -464,7 +517,7 @@ function createSessionSystemMedia(overrides = {}) {
464
517
  const track = state.state.track;
465
518
  const identity = identityFromTrack(track);
466
519
  currentArtworkIdentity = artworkIdentityKey(identity);
467
- const artworkState = artworkForTrack(artworkCacheKey(identity), artworkIdentityKey(identity), {
520
+ const artworkState = artworkForTrack(currentArtworkKey, artworkIdentityKey(identity), {
468
521
  title: track.name,
469
522
  artist: track.artists,
470
523
  album: track.album,
@@ -487,7 +540,8 @@ function createSessionSystemMedia(overrides = {}) {
487
540
  track: {
488
541
  ...track,
489
542
  artwork: artworkState.artwork,
490
- artwork_loading: artworkState.loading
543
+ artwork_loading: artworkState.loading,
544
+ artwork_duration_ms: artworkState.duration_ms
491
545
  }
492
546
  };
493
547
  };
@@ -552,10 +606,12 @@ function createSessionSystemMedia(overrides = {}) {
552
606
  const track = latest?.state.track;
553
607
  if (!track)
554
608
  return;
555
- const key = artworkCacheKey(identityFromTrack(track));
556
- if (artworkJobs.has(key))
609
+ const key = currentArtworkKey ?? artworkCacheKey(identityFromTrack(track));
610
+ const requestedKey = artworkCacheKey(identityFromTrack(track));
611
+ if (artworkJobs.has(key) || artworkJobs.has(requestedKey))
557
612
  return;
558
613
  artworkCache.delete(key);
614
+ artworkCache.delete(requestedKey);
559
615
  emit({ type: "snapshot", state: project(latest) });
560
616
  },
561
617
  async play() {
@@ -644,42 +700,6 @@ function openNowPlayingApp() {
644
700
  });
645
701
  }
646
702
 
647
- // types.ts
648
- import {
649
- emptyPlayer as emptyCorePlayer,
650
- formatMs,
651
- isMac
652
- } from "@naxodev/music-core";
653
- function mergeArtworkCompletion(player, event) {
654
- const track = player?.track;
655
- if (!track || track.name !== event.identity.title || track.artists !== event.identity.artist || track.album !== event.identity.album || track.duration_ms > 0 && track.duration_ms !== event.identity.duration_ms) {
656
- return player;
657
- }
658
- return {
659
- ...player,
660
- track: {
661
- ...track,
662
- artwork: event.artwork,
663
- artwork_loading: false
664
- }
665
- };
666
- }
667
- function mergePlayerSnapshot(player, snapshot) {
668
- const current = player?.track;
669
- const next = snapshot.track;
670
- if (!current || !next || current.name !== next.name || current.artists !== next.artists || current.album !== next.album || current.duration_ms > 0 && next.duration_ms > 0 && current.duration_ms !== next.duration_ms || current.artwork === null && current.artwork_loading !== false) {
671
- return snapshot;
672
- }
673
- return {
674
- ...snapshot,
675
- track: {
676
- ...next,
677
- artwork: next.artwork ?? current.artwork,
678
- artwork_loading: false
679
- }
680
- };
681
- }
682
-
683
703
  // ui.tsx
684
704
  import { memo as _$memo } from "@opentui/solid";
685
705
  import { effect as _$effect2 } from "@opentui/solid";
@@ -1415,7 +1435,7 @@ function CompactPlayer(props) {
1415
1435
  }
1416
1436
  if (cell < 0 || cell >= seekWidth)
1417
1437
  return;
1418
- const position = seekPositionForCell(cell, seekWidth, current.duration_ms);
1438
+ const position = seekPositionForCell(cell, seekWidth, presentationDuration(current));
1419
1439
  if (position === null)
1420
1440
  return;
1421
1441
  event.preventDefault();
@@ -1439,7 +1459,7 @@ function liveProgress(player, now = Date.now()) {
1439
1459
  return 0;
1440
1460
  if (!player.is_playing)
1441
1461
  return player.progress_ms;
1442
- return Math.min(player.track.duration_ms, player.progress_ms + (Math.max(now, player.fetched_at) - player.fetched_at));
1462
+ return Math.min(presentationDuration(player.track), player.progress_ms + (Math.max(now, player.fetched_at) - player.fetched_at));
1443
1463
  }
1444
1464
  function progressSegments(progress, duration, width) {
1445
1465
  if (duration <= 0)
@@ -1465,7 +1485,7 @@ function ProgressBar(props) {
1465
1485
  const width = () => Math.max(8, props.width ?? 28);
1466
1486
  const rendered = () => {
1467
1487
  const player = props.source.current();
1468
- return progressSegments(liveProgress(player), player?.track?.duration_ms ?? 0, width());
1488
+ return progressSegments(liveProgress(player), presentationDuration(player?.track), width());
1469
1489
  };
1470
1490
  const paint = () => {
1471
1491
  const next = rendered();
@@ -1496,7 +1516,7 @@ function ProgressBar(props) {
1496
1516
  _$setProp2(_el$3, "onMouseDown", (event) => {
1497
1517
  if (event.button !== MouseButton.LEFT || !bar)
1498
1518
  return;
1499
- const position = seekPositionForCell(event.x - bar.x, bar.width, props.source.current()?.track?.duration_ms ?? 0);
1519
+ const position = seekPositionForCell(event.x - bar.x, bar.width, presentationDuration(props.source.current()?.track));
1500
1520
  if (position === null)
1501
1521
  return;
1502
1522
  event.preventDefault();
@@ -1642,7 +1662,7 @@ function SidebarPlayer(props) {
1642
1662
  const player = createMemo(() => props.state.player);
1643
1663
  const track = createMemo(() => player()?.track);
1644
1664
  const playing = createMemo(() => !!player()?.is_playing);
1645
- const duration = createMemo(() => track()?.duration_ms ?? 0);
1665
+ const duration = createMemo(() => presentationDuration(track()));
1646
1666
  return (() => {
1647
1667
  var _el$1 = _$createElement3("box"), _el$10 = _$createElement3("box"), _el$11 = _$createElement3("box"), _el$12 = _$createElement3("box"), _el$16 = _$createElement3("box");
1648
1668
  _$insertNode2(_el$1, _el$10);
@@ -2026,7 +2046,7 @@ function createController(context, dependencies = controllerDependencies) {
2026
2046
  };
2027
2047
  const playPause = () => runCommand(() => media.toggle());
2028
2048
  const seek = (positionMs) => {
2029
- const target = seekTarget(positionMs, session.player?.track?.duration_ms);
2049
+ const target = seekTarget(positionMs, presentationDuration(session.player?.track));
2030
2050
  if (!session.player?.track || target === null || !isActive())
2031
2051
  return Promise.resolve();
2032
2052
  return new Promise((resolve) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naxodev/opencode-music-player",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "macOS system Now Playing controls for the OpenCode 2 TUI",
5
5
  "license": "MIT",
6
6
  "author": {