@naxodev/opencode-music-player 0.3.0 → 0.3.1

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 +5 -4
  2. package/dist/index.js +16 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ It supports browsers, Spotify, Apple Music, Kaset, and other apps exposed throug
8
8
 
9
9
  One reconnecting music-session client supplies replayed and live state, provider status, transport, and daemon-owned native artwork bytes. The shared same-user daemon owns provider discovery, provider events and polling, the playback clock, and global transport ordering.
10
10
 
11
- OpenCode keeps plugin/controller lifecycle, the Solid compact and sidebar UI, optimistic transport presentation, seek coalescing, notifications, waveform projection, iTunes catalog fallback and downloads, conversion, bounded presentation cache/jobs, and Kitty or half-block rendering. Plugin disposal removes local listeners and presentation work, then disposes only its session client. Other clients keep the shared daemon alive.
11
+ OpenCode keeps plugin/controller lifecycle, the Solid compact and sidebar UI, transport loading feedback, seek coalescing, notifications, waveform projection, iTunes catalog fallback and downloads, conversion, bounded presentation cache/jobs, and Kitty or half-block rendering. Plugin disposal removes local listeners and presentation work, then disposes only its session client. Other clients keep the shared daemon alive.
12
12
 
13
13
  Read the [music session architecture field guide](../../docs/music-session-architecture.html) for the daemon protocol, replay, reconnect, and cleanup model.
14
14
 
@@ -38,13 +38,12 @@ set -g allow-passthrough on
38
38
  ```
39
39
 
40
40
  > [!IMPORTANT]
41
- > This source targets stable `opencode v2.0.3`. The previous beta host is unsupported. See the [compatibility contract](../../docs/opencode-compatibility.md) for the exact dependency set and release requirements.
41
+ > This source targets the exact stable host in the [validated compatibility metadata](../../docs/package-compatibility.md). The previous beta host is unsupported. See the [compatibility contract](../../docs/opencode-compatibility.md) for runtime ownership and release requirements.
42
42
 
43
43
  ## Requirements
44
44
 
45
45
  - macOS
46
- - OpenCode `v2.0.3`
47
- - The Bun-based OpenCode runtime; Bun 1.3.7 for workspace development
46
+ - The exact tested OpenCode host and workspace Bun pin listed in the compatibility metadata
48
47
  - [`media-control`](https://github.com/ungive/media-control), recommended:
49
48
 
50
49
  ```sh
@@ -97,6 +96,8 @@ Start `opencode` and run `/plugins` in the TUI. The list should include `music-p
97
96
 
98
97
  ## Controls
99
98
 
99
+ Play/pause uses the shared [playback toggle contract](../music-core/README.md#playback-toggle-contract). Each activation toggles the daemon's accepted state in queue order, even when this view's icon is stale. Rapid clicks remain separate commands.
100
+
100
101
  The compact bar appears below the active route whenever a current track exists, including while playback is paused. It remains visible when the session sidebar is collapsed. Wide terminals show the playback marker, title, and artist. Medium terminals omit the artist. Narrow terminals truncate the title, then keep only the playback marker when metadata cannot fit safely. The bar always stays on one row.
101
102
 
102
103
  | Input | Action |
package/dist/index.js CHANGED
@@ -561,6 +561,9 @@ function createSessionSystemMedia(overrides = {}) {
561
561
  async play() {
562
562
  await (await activeClient()).play();
563
563
  },
564
+ async toggle() {
565
+ await (await activeClient()).toggle();
566
+ },
564
567
  async pause() {
565
568
  await (await activeClient()).pause();
566
569
  },
@@ -1037,6 +1040,10 @@ function AlbumArtwork(props) {
1037
1040
  paintPending = false;
1038
1041
  if (!disposed && ownership.isCurrent())
1039
1042
  paintNativeImage();
1043
+ }).catch((error) => {
1044
+ paintPending = false;
1045
+ if (!disposed && ownership.isCurrent())
1046
+ console.error("Failed to paint music artwork", error);
1040
1047
  });
1041
1048
  };
1042
1049
  onMount(() => props.context.renderer.on("frame", scheduleNativeImage));
@@ -1899,8 +1906,6 @@ function createController(context, dependencies = controllerDependencies) {
1899
1906
  };
1900
1907
  let transportError = null;
1901
1908
  let receivedSnapshot = false;
1902
- let snapshotEpoch = 0;
1903
- let playbackIntent;
1904
1909
  let activeSeek = null;
1905
1910
  let latestSeek = null;
1906
1911
  const pending = new Set;
@@ -1958,9 +1963,7 @@ function createController(context, dependencies = controllerDependencies) {
1958
1963
  } catch (error) {
1959
1964
  result = Promise.reject(error);
1960
1965
  }
1961
- let succeeded = false;
1962
1966
  Promise.resolve(result).then(() => {
1963
- succeeded = true;
1964
1967
  if (!isActive() || generation !== lifecycleGeneration)
1965
1968
  return;
1966
1969
  if (transportError !== null)
@@ -1977,10 +1980,12 @@ function createController(context, dependencies = controllerDependencies) {
1977
1980
  });
1978
1981
  }).finally(() => {
1979
1982
  try {
1980
- onSettled?.(succeeded);
1983
+ onSettled?.();
1981
1984
  } finally {
1982
1985
  settle(resolve);
1983
1986
  }
1987
+ }).catch((error) => {
1988
+ console.error("Failed to settle music command", error);
1984
1989
  });
1985
1990
  });
1986
1991
  };
@@ -1999,6 +2004,8 @@ function createController(context, dependencies = controllerDependencies) {
1999
2004
  runSeek(next);
2000
2005
  else
2001
2006
  next?.resolves.splice(0).forEach((resolve) => resolve());
2007
+ }).catch((error) => {
2008
+ console.error("Failed to start music seek", error);
2002
2009
  });
2003
2010
  };
2004
2011
  const refreshAll = async () => {
@@ -2017,26 +2024,7 @@ function createController(context, dependencies = controllerDependencies) {
2017
2024
  setTransportError(errMsg(error));
2018
2025
  }
2019
2026
  };
2020
- const playPause = () => {
2021
- const nextPlaying = !(playbackIntent?.target ?? !!session.player?.is_playing);
2022
- const intent = {
2023
- target: nextPlaying,
2024
- startedAtEpoch: snapshotEpoch,
2025
- settled: false
2026
- };
2027
- playbackIntent = intent;
2028
- return runCommand(() => nextPlaying ? media.play() : media.pause(), (succeeded) => {
2029
- if (playbackIntent !== intent)
2030
- return;
2031
- if (!succeeded) {
2032
- playbackIntent = undefined;
2033
- return;
2034
- }
2035
- intent.settled = true;
2036
- if (snapshotEpoch > intent.startedAtEpoch && session.player?.is_playing === intent.target)
2037
- playbackIntent = undefined;
2038
- });
2039
- };
2027
+ const playPause = () => runCommand(() => media.toggle());
2040
2028
  const seek = (positionMs) => {
2041
2029
  const target = seekTarget(positionMs, session.player?.track?.duration_ms);
2042
2030
  if (!session.player?.track || target === null || !isActive())
@@ -2065,18 +2053,13 @@ function createController(context, dependencies = controllerDependencies) {
2065
2053
  return;
2066
2054
  if (event?.type === "snapshot") {
2067
2055
  receivedSnapshot = true;
2068
- snapshotEpoch++;
2069
2056
  setSession((draft) => {
2070
2057
  draft.player = mergePlayerSnapshot(draft.player, event.state);
2071
2058
  });
2072
- if (playbackIntent?.settled && snapshotEpoch > playbackIntent.startedAtEpoch && event.state.is_playing === playbackIntent.target)
2073
- playbackIntent = undefined;
2074
2059
  return;
2075
2060
  }
2076
2061
  if (event?.type === "lifecycle") {
2077
2062
  setLifecycleError(event.message, event.source);
2078
- if (event.source === "connection" && event.message !== null)
2079
- playbackIntent = undefined;
2080
2063
  if (event.message !== null && latestSeek) {
2081
2064
  latestSeek.resolves.splice(0).forEach((resolve) => resolve());
2082
2065
  latestSeek = null;
@@ -2090,7 +2073,9 @@ function createController(context, dependencies = controllerDependencies) {
2090
2073
  draft.player = mergeArtworkCompletion(draft.player, event);
2091
2074
  });
2092
2075
  });
2093
- refreshAll();
2076
+ refreshAll().catch((error) => {
2077
+ console.error("Failed to publish initial music state", error);
2078
+ });
2094
2079
  return {
2095
2080
  session,
2096
2081
  subscribe(listener) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naxodev/opencode-music-player",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "macOS system Now Playing controls for the OpenCode 2 TUI",
5
5
  "license": "MIT",
6
6
  "author": {