@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.
- package/README.md +5 -4
- package/dist/index.js +16 -31
- 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,
|
|
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
|
|
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
|
|
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?.(
|
|
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) {
|