@naxodev/opencode-music-player 0.3.0 → 0.4.0
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 +35 -50
- package/package.json +4 -4
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));
|
|
@@ -1186,7 +1193,7 @@ var BLUE = [
|
|
|
1186
1193
|
];
|
|
1187
1194
|
function blueFor(level, playing, theme) {
|
|
1188
1195
|
if (!playing && level < 0.05)
|
|
1189
|
-
return theme.text.
|
|
1196
|
+
return theme.text.muted;
|
|
1190
1197
|
if (level <= 0.02)
|
|
1191
1198
|
return BLUE[1];
|
|
1192
1199
|
const idx = Math.min(BLUE.length - 1, Math.floor(level * (BLUE.length - 1)));
|
|
@@ -1422,7 +1429,7 @@ function CompactPlayer(props) {
|
|
|
1422
1429
|
_$setProp2(_el$2, "wrapMode", "none");
|
|
1423
1430
|
_$setProp2(_el$2, "truncate", true);
|
|
1424
1431
|
_$insert3(_el$2, renderLine);
|
|
1425
|
-
_$effect2((_$p) => _$setProp2(_el$2, "fg", theme().text.action.primary.
|
|
1432
|
+
_$effect2((_$p) => _$setProp2(_el$2, "fg", theme().text.action.primary.base, _$p));
|
|
1426
1433
|
return _el$;
|
|
1427
1434
|
})()
|
|
1428
1435
|
});
|
|
@@ -1507,11 +1514,11 @@ function ProgressBar(props) {
|
|
|
1507
1514
|
_$insert3(_el$7, () => rendered().right);
|
|
1508
1515
|
_$effect2((_p$) => {
|
|
1509
1516
|
var _v$ = width(), _v$2 = {
|
|
1510
|
-
fg: props.accent ?? props.theme.text.action.primary.
|
|
1517
|
+
fg: props.accent ?? props.theme.text.action.primary.base
|
|
1511
1518
|
}, _v$3 = {
|
|
1512
|
-
fg: props.accent ?? props.theme.text.action.primary.
|
|
1519
|
+
fg: props.accent ?? props.theme.text.action.primary.base
|
|
1513
1520
|
}, _v$4 = {
|
|
1514
|
-
fg: props.theme.text.
|
|
1521
|
+
fg: props.theme.text.muted
|
|
1515
1522
|
};
|
|
1516
1523
|
_v$ !== _p$.e && (_p$.e = _$setProp2(_el$3, "width", _v$, _p$.e));
|
|
1517
1524
|
_v$2 !== _p$.t && (_p$.t = _$setProp2(_el$5, "style", _v$2, _p$.t));
|
|
@@ -1548,7 +1555,7 @@ function LiveElapsed(props) {
|
|
|
1548
1555
|
var _el$8 = _$createElement3("text");
|
|
1549
1556
|
_$use3((element) => text = element, _el$8);
|
|
1550
1557
|
_$insert3(_el$8, () => formatMs(liveProgress(props.source.current())));
|
|
1551
|
-
_$effect2((_$p) => _$setProp2(_el$8, "fg", props.theme.text.
|
|
1558
|
+
_$effect2((_$p) => _$setProp2(_el$8, "fg", props.theme.text.muted, _$p));
|
|
1552
1559
|
return _el$8;
|
|
1553
1560
|
})();
|
|
1554
1561
|
}
|
|
@@ -1557,17 +1564,17 @@ function IconBtn(props) {
|
|
|
1557
1564
|
const [press, setPress] = createSignal(false);
|
|
1558
1565
|
const bg = () => {
|
|
1559
1566
|
if (press() || props.active) {
|
|
1560
|
-
return props.theme.background.action.primary.
|
|
1567
|
+
return props.theme.background.action.primary.base;
|
|
1561
1568
|
}
|
|
1562
|
-
return props.theme.background.
|
|
1569
|
+
return props.theme.background.raised.base;
|
|
1563
1570
|
};
|
|
1564
1571
|
const fg2 = () => {
|
|
1565
1572
|
if (press() || props.active)
|
|
1566
|
-
return props.accent ?? props.theme.text.action.primary.
|
|
1573
|
+
return props.accent ?? props.theme.text.action.primary.base;
|
|
1567
1574
|
if (props.primary) {
|
|
1568
|
-
return hover() ? props.theme.text.action.primary.
|
|
1575
|
+
return hover() ? props.theme.text.action.primary.base : props.theme.text.base;
|
|
1569
1576
|
}
|
|
1570
|
-
return hover() ? props.theme.text.action.primary.
|
|
1577
|
+
return hover() ? props.theme.text.action.primary.base : props.theme.text.base;
|
|
1571
1578
|
};
|
|
1572
1579
|
const w = () => props.width ?? 3;
|
|
1573
1580
|
const h = () => props.height ?? 1;
|
|
@@ -1656,7 +1663,7 @@ function SidebarPlayer(props) {
|
|
|
1656
1663
|
children: (err) => (() => {
|
|
1657
1664
|
var _el$17 = _$createElement3("text");
|
|
1658
1665
|
_$insert3(_el$17, () => sanitizeTerminalText(err()));
|
|
1659
|
-
_$effect2((_$p) => _$setProp2(_el$17, "fg", theme().text.feedback.error.
|
|
1666
|
+
_$effect2((_$p) => _$setProp2(_el$17, "fg", theme().text.feedback.error.base, _$p));
|
|
1660
1667
|
return _el$17;
|
|
1661
1668
|
})()
|
|
1662
1669
|
}), _el$10);
|
|
@@ -1682,7 +1689,7 @@ function SidebarPlayer(props) {
|
|
|
1682
1689
|
var _c$ = _$memo(() => !!track());
|
|
1683
1690
|
return () => _c$() ? track()?.artwork_loading ? "Loading artwork\u2026" : "Artwork unavailable" : "";
|
|
1684
1691
|
})());
|
|
1685
|
-
_$effect2((_$p) => _$setProp2(_el$19, "fg", theme().text.
|
|
1692
|
+
_$effect2((_$p) => _$setProp2(_el$19, "fg", theme().text.muted, _$p));
|
|
1686
1693
|
return _el$18;
|
|
1687
1694
|
})();
|
|
1688
1695
|
},
|
|
@@ -1703,7 +1710,7 @@ function SidebarPlayer(props) {
|
|
|
1703
1710
|
return (() => {
|
|
1704
1711
|
var _el$20 = _$createElement3("text");
|
|
1705
1712
|
_$insert3(_el$20, () => props.state.loading ? "Syncing\u2026" : "Nothing playing");
|
|
1706
|
-
_$effect2((_$p) => _$setProp2(_el$20, "fg", theme().text.
|
|
1713
|
+
_$effect2((_$p) => _$setProp2(_el$20, "fg", theme().text.muted, _$p));
|
|
1707
1714
|
return _el$20;
|
|
1708
1715
|
})();
|
|
1709
1716
|
},
|
|
@@ -1721,7 +1728,7 @@ function SidebarPlayer(props) {
|
|
|
1721
1728
|
get children() {
|
|
1722
1729
|
var _el$24 = _$createElement3("text");
|
|
1723
1730
|
_$insert3(_el$24, () => sanitizeTerminalText(t2().artists));
|
|
1724
|
-
_$effect2((_$p) => _$setProp2(_el$24, "fg", theme().text.
|
|
1731
|
+
_$effect2((_$p) => _$setProp2(_el$24, "fg", theme().text.muted, _$p));
|
|
1725
1732
|
return _el$24;
|
|
1726
1733
|
}
|
|
1727
1734
|
}), null);
|
|
@@ -1732,11 +1739,11 @@ function SidebarPlayer(props) {
|
|
|
1732
1739
|
get children() {
|
|
1733
1740
|
var _el$25 = _$createElement3("text");
|
|
1734
1741
|
_$insert3(_el$25, () => sanitizeTerminalText(t2().album));
|
|
1735
|
-
_$effect2((_$p) => _$setProp2(_el$25, "fg", theme().text.
|
|
1742
|
+
_$effect2((_$p) => _$setProp2(_el$25, "fg", theme().text.muted, _$p));
|
|
1736
1743
|
return _el$25;
|
|
1737
1744
|
}
|
|
1738
1745
|
}), null);
|
|
1739
|
-
_$effect2((_$p) => _$setProp2(_el$22, "fg", theme().text.
|
|
1746
|
+
_$effect2((_$p) => _$setProp2(_el$22, "fg", theme().text.base, _$p));
|
|
1740
1747
|
return _el$21;
|
|
1741
1748
|
})()
|
|
1742
1749
|
}), _el$12);
|
|
@@ -1784,7 +1791,7 @@ function SidebarPlayer(props) {
|
|
|
1784
1791
|
source
|
|
1785
1792
|
}), _el$15);
|
|
1786
1793
|
_$insert3(_el$15, () => formatMs(duration()));
|
|
1787
|
-
_$effect2((_$p) => _$setProp2(_el$15, "fg", theme().text.
|
|
1794
|
+
_$effect2((_$p) => _$setProp2(_el$15, "fg", theme().text.muted, _$p));
|
|
1788
1795
|
return _el$13;
|
|
1789
1796
|
}
|
|
1790
1797
|
}), _el$16);
|
|
@@ -1843,7 +1850,7 @@ function SidebarPlayer(props) {
|
|
|
1843
1850
|
onClick: () => props.onNext()
|
|
1844
1851
|
}), null);
|
|
1845
1852
|
_$effect2((_p$) => {
|
|
1846
|
-
var _v$9 = playing() ? track()?.artwork?.accent ?? theme().text.action.primary.
|
|
1853
|
+
var _v$9 = playing() ? track()?.artwork?.accent ?? theme().text.action.primary.base : theme().border.base, _v$0 = TRANSPORT_LAYOUT.gap;
|
|
1847
1854
|
_v$9 !== _p$.e && (_p$.e = _$setProp2(_el$1, "borderColor", _v$9, _p$.e));
|
|
1848
1855
|
_v$0 !== _p$.t && (_p$.t = _$setProp2(_el$16, "gap", _v$0, _p$.t));
|
|
1849
1856
|
return _p$;
|
|
@@ -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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naxodev/opencode-music-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "macOS system Now Playing controls for the OpenCode 2 TUI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -64,16 +64,16 @@
|
|
|
64
64
|
"pngjs": "^7.0.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@opencode/plugin": "2.0.
|
|
67
|
+
"@opencode/plugin": "2.0.16",
|
|
68
68
|
"@opentui/core": "0.5.10",
|
|
69
69
|
"@opentui/solid": "0.5.10"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@opencode/plugin": "2.0.
|
|
72
|
+
"@opencode/plugin": "2.0.16",
|
|
73
73
|
"@opentui/core": "0.5.10",
|
|
74
74
|
"@opentui/solid": "0.5.10",
|
|
75
75
|
"solid-js": "1.9.15",
|
|
76
|
-
"@opencode/theme": "2.0.
|
|
76
|
+
"@opencode/theme": "2.0.16",
|
|
77
77
|
"@types/bun": "^1.3.14",
|
|
78
78
|
"@types/pngjs": "^6.0.5",
|
|
79
79
|
"prettier": "3.9.6",
|