@groovenet/player 0.1.3 → 0.1.5
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/MPRIS.md +97 -0
- package/OWNTONE.md +145 -0
- package/README.md +17 -0
- package/SERVER_PLAYERS.md +348 -0
- package/build/daemon.d.ts.map +1 -1
- package/build/daemon.js +3 -0
- package/build/daemon.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/mpris.d.ts +11 -0
- package/build/mpris.d.ts.map +1 -0
- package/build/mpris.js +145 -0
- package/build/mpris.js.map +1 -0
- package/build/mprisBin.d.ts +3 -0
- package/build/mprisBin.d.ts.map +1 -0
- package/build/mprisBin.js +26 -0
- package/build/mprisBin.js.map +1 -0
- package/build/mprisMapping.d.ts +12 -0
- package/build/mprisMapping.d.ts.map +1 -0
- package/build/mprisMapping.js +35 -0
- package/build/mprisMapping.js.map +1 -0
- package/build/mpvBackend.d.ts.map +1 -1
- package/build/mpvBackend.js +2 -1
- package/build/mpvBackend.js.map +1 -1
- package/build/ownToneBackend.d.ts +62 -0
- package/build/ownToneBackend.d.ts.map +1 -0
- package/build/ownToneBackend.js +349 -0
- package/build/ownToneBackend.js.map +1 -0
- package/build/player.d.ts +1 -0
- package/build/player.d.ts.map +1 -1
- package/build/player.js +11 -0
- package/build/player.js.map +1 -1
- package/build/protocol.d.ts.map +1 -1
- package/build/protocol.js +1 -0
- package/build/protocol.js.map +1 -1
- package/build/resolver.d.ts +3 -1
- package/build/resolver.d.ts.map +1 -1
- package/build/resolver.js +30 -1
- package/build/resolver.js.map +1 -1
- package/build/server/bin.d.ts +3 -0
- package/build/server/bin.d.ts.map +1 -0
- package/build/server/bin.js +7 -0
- package/build/server/bin.js.map +1 -0
- package/build/server/http.d.ts +5 -0
- package/build/server/http.d.ts.map +1 -0
- package/build/server/http.js +129 -0
- package/build/server/http.js.map +1 -0
- package/build/server/manager.d.ts +61 -0
- package/build/server/manager.d.ts.map +1 -0
- package/build/server/manager.js +555 -0
- package/build/server/manager.js.map +1 -0
- package/build/server/observer.d.ts +10 -0
- package/build/server/observer.d.ts.map +1 -0
- package/build/server/observer.js +47 -0
- package/build/server/observer.js.map +1 -0
- package/build/server/runtime.d.ts +18 -0
- package/build/server/runtime.d.ts.map +1 -0
- package/build/server/runtime.js +101 -0
- package/build/server/runtime.js.map +1 -0
- package/build/server/store.d.ts +47 -0
- package/build/server/store.d.ts.map +1 -0
- package/build/server/store.js +120 -0
- package/build/server/store.js.map +1 -0
- package/build/types.d.ts +3 -1
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/owntone/Dockerfile +27 -0
- package/owntone/smoke.mjs +135 -0
- package/owntone/terminal-v1.patch +96 -0
- package/package.json +24 -4
- package/scripts/mpris-smoke.mjs +54 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- a/src/player.h
|
|
2
|
+
+++ b/src/player.h
|
|
3
|
+
@@ -44,0 +45 @@
|
|
4
|
+
+ bool gn_connected;
|
|
5
|
+
@@ -72,0 +74,6 @@
|
|
6
|
+
+
|
|
7
|
+
+ /* Groovenet terminal signal v1; snapshotted on the player thread. */
|
|
8
|
+
+ char gn_instance[33];
|
|
9
|
+
+ uint32_t gn_sequence;
|
|
10
|
+
+ uint32_t gn_item_id;
|
|
11
|
+
+ const char *gn_reason;
|
|
12
|
+
--- a/src/player.c
|
|
13
|
+
+++ b/src/player.c
|
|
14
|
+
@@ -201,0 +202,3 @@
|
|
15
|
+
+ // Never report an input/output failure as natural completion.
|
|
16
|
+
+ bool gn_failed;
|
|
17
|
+
+
|
|
18
|
+
@@ -341,0 +345,14 @@
|
|
19
|
+
+
|
|
20
|
+
+// Groovenet terminal signal. All access is confined to the player thread.
|
|
21
|
+
+static char gn_instance[33];
|
|
22
|
+
+static uint32_t gn_sequence;
|
|
23
|
+
+static uint32_t gn_item_id;
|
|
24
|
+
+static const char *gn_reason = "none";
|
|
25
|
+
+
|
|
26
|
+
+static void
|
|
27
|
+
+gn_terminal(uint32_t item_id, const char *reason)
|
|
28
|
+
+{
|
|
29
|
+
+ gn_sequence++;
|
|
30
|
+
+ gn_item_id = item_id;
|
|
31
|
+
+ gn_reason = reason;
|
|
32
|
+
+}
|
|
33
|
+
@@ -1023,0 +1041,2 @@
|
|
34
|
+
+ pb_session.reading_now->gn_failed = true;
|
|
35
|
+
+ gn_terminal(pb_session.reading_now->item_id, "input_error");
|
|
36
|
+
@@ -1090,0 +1110,3 @@
|
|
37
|
+
+
|
|
38
|
+
+ gn_terminal(pb_session.playing_now->item_id,
|
|
39
|
+
+ pb_session.playing_now->gn_failed ? "failed" : "eof");
|
|
40
|
+
@@ -1499,0 +1522,9 @@
|
|
41
|
+
+ if ((!device || status == OUTPUT_STATE_FAILED || (status == OUTPUT_STATE_STOPPED && player_state == PLAY_PLAYING))
|
|
42
|
+
+ && pb_session.playing_now)
|
|
43
|
+
+ {
|
|
44
|
+
+ pb_session.playing_now->gn_failed = true;
|
|
45
|
+
+ if (pb_session.reading_now)
|
|
46
|
+
+ pb_session.reading_now->gn_failed = true;
|
|
47
|
+
+ gn_terminal(pb_session.playing_now->item_id, "output_lost");
|
|
48
|
+
+ }
|
|
49
|
+
+
|
|
50
|
+
@@ -1516,7 +1547,3 @@
|
|
51
|
+
- if (!device->resurrect)
|
|
52
|
+
- goto out;
|
|
53
|
+
-
|
|
54
|
+
- DPRINTF(E_LOG, L_PLAYER, "Attempting reconnection in %d sec to the %s device '%s'\n", PLAYER_SPEAKER_RESURRECT_TIME, device->type_name, device->name);
|
|
55
|
+
-
|
|
56
|
+
- // TODO do this internally instead of through the worker
|
|
57
|
+
- worker_execute(player_speaker_resurrect, &(device->id), sizeof(device->id), PLAYER_SPEAKER_RESURRECT_TIME);
|
|
58
|
+
+ // Groovenet never automatically reclaims a failed/taken-over receiver.
|
|
59
|
+
+ // A new explicit play/output-selection action is required.
|
|
60
|
+
+ goto out;
|
|
61
|
+
@@ -1791,0 +1819,3 @@
|
|
62
|
+
+ if (pb_session.playing_now)
|
|
63
|
+
+ gn_terminal(pb_session.playing_now->item_id, "interrupted");
|
|
64
|
+
+
|
|
65
|
+
@@ -1910,0 +1941,4 @@
|
|
66
|
+
+ memcpy(status->gn_instance, gn_instance, sizeof(status->gn_instance));
|
|
67
|
+
+ status->gn_sequence = gn_sequence;
|
|
68
|
+
+ status->gn_item_id = gn_item_id;
|
|
69
|
+
+ status->gn_reason = gn_reason;
|
|
70
|
+
@@ -2564,0 +2599 @@
|
|
71
|
+
+ spk->gn_connected = (device->session != NULL);
|
|
72
|
+
@@ -3943 +3978,7 @@
|
|
73
|
+
- int ret;
|
|
74
|
+
+ unsigned char nonce[16];
|
|
75
|
+
+ unsigned int i;
|
|
76
|
+
+ int ret;
|
|
77
|
+
+
|
|
78
|
+
+ gcry_create_nonce(nonce, sizeof(nonce));
|
|
79
|
+
+ for (i = 0; i < sizeof(nonce); i++)
|
|
80
|
+
+ snprintf(gn_instance + i * 2, 3, "%02x", nonce[i]);
|
|
81
|
+
--- a/src/httpd_jsonapi.c
|
|
82
|
+
+++ b/src/httpd_jsonapi.c
|
|
83
|
+
@@ -1631,0 +1632,3 @@
|
|
84
|
+
+ json_object_object_add(output, "busy", json_object_new_boolean(spk->busy));
|
|
85
|
+
+ json_object_object_add(output, "connected", json_object_new_boolean(spk->gn_connected));
|
|
86
|
+
+ json_object_object_add(output, "prevent_playback", json_object_new_boolean(spk->prevent_playback));
|
|
87
|
+
@@ -2198,0 +2202,9 @@
|
|
88
|
+
+
|
|
89
|
+
+ /* Additive, versioned extension: stock OwnTone clients ignore these fields. */
|
|
90
|
+
+ json_object *terminal = json_object_new_object();
|
|
91
|
+
+ json_object_object_add(terminal, "version", json_object_new_int(1));
|
|
92
|
+
+ json_object_object_add(terminal, "instance", json_object_new_string(status.gn_instance));
|
|
93
|
+
+ json_object_object_add(terminal, "sequence", json_object_new_int64(status.gn_sequence));
|
|
94
|
+
+ json_object_object_add(terminal, "item_id", json_object_new_int64(status.gn_item_id));
|
|
95
|
+
+ json_object_object_add(terminal, "reason", json_object_new_string(status.gn_reason));
|
|
96
|
+
+ json_object_object_add(reply, "groovenet_terminal", terminal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groovenet/player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Headless playback core and mpv backend for Groovenet",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
},
|
|
9
9
|
"main": "build/index.js",
|
|
10
10
|
"types": "build/index.d.ts",
|
|
11
|
+
"bin": {
|
|
12
|
+
"groovenet-mpris": "build/mprisBin.js",
|
|
13
|
+
"groovenet-player-server": "build/server/bin.js"
|
|
14
|
+
},
|
|
11
15
|
"exports": {
|
|
12
16
|
".": {
|
|
13
17
|
"import": "./build/index.js",
|
|
@@ -20,6 +24,10 @@
|
|
|
20
24
|
"./testing": {
|
|
21
25
|
"import": "./build/testing.js",
|
|
22
26
|
"types": "./build/testing.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./server": {
|
|
29
|
+
"types": "./build/server/manager.d.ts",
|
|
30
|
+
"import": "./build/server/manager.js"
|
|
23
31
|
}
|
|
24
32
|
},
|
|
25
33
|
"scripts": {
|
|
@@ -29,16 +37,25 @@
|
|
|
29
37
|
"test:coverage": "vitest run --coverage"
|
|
30
38
|
},
|
|
31
39
|
"files": [
|
|
32
|
-
"build/"
|
|
40
|
+
"build/",
|
|
41
|
+
"MPRIS.md",
|
|
42
|
+
"OWNTONE.md",
|
|
43
|
+
"SERVER_PLAYERS.md",
|
|
44
|
+
"owntone/",
|
|
45
|
+
"scripts/mpris-smoke.mjs"
|
|
33
46
|
],
|
|
34
47
|
"publishConfig": {
|
|
35
48
|
"access": "public"
|
|
36
49
|
},
|
|
37
50
|
"dependencies": {
|
|
38
|
-
"@groovenet/client": "^3.
|
|
51
|
+
"@groovenet/client": "^3.6.0",
|
|
52
|
+
"@groovenet/contracts": "1.3.0",
|
|
53
|
+
"pg": "8.23.1",
|
|
54
|
+
"zod": "4.6.7"
|
|
39
55
|
},
|
|
40
56
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^26.6.
|
|
57
|
+
"@types/node": "^26.6.5",
|
|
58
|
+
"@types/pg": "8.23.1",
|
|
42
59
|
"@vitest/coverage-v8": "^5.0.3",
|
|
43
60
|
"typescript": "^7.0",
|
|
44
61
|
"vitest": "^5.0.0"
|
|
@@ -48,5 +65,8 @@
|
|
|
48
65
|
"type": "git",
|
|
49
66
|
"url": "git+https://github.com/Public-Vinyl-Radio/groovenet.git",
|
|
50
67
|
"directory": "packages/groovenet-player"
|
|
68
|
+
},
|
|
69
|
+
"optionalDependencies": {
|
|
70
|
+
"dbus-next": "0.10.2"
|
|
51
71
|
}
|
|
52
72
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Run on Linux: dbus-run-session -- node packages/groovenet-player/scripts/mpris-smoke.mjs
|
|
2
|
+
// Requires built workspaces, optional dbus-next dependency, dbus and playerctl.
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { PlayerDaemon, PlayerCore, TrackResolver } from "../build/index.js";
|
|
10
|
+
import { TestPlaybackBackend } from "../build/testing.js";
|
|
11
|
+
import { startMpris } from "../build/mpris.js";
|
|
12
|
+
|
|
13
|
+
const dir = await mkdtemp(join(tmpdir(), "groovenet-mpris-"));
|
|
14
|
+
const backend = new TestPlaybackBackend();
|
|
15
|
+
const player = new PlayerCore(backend, new TrackResolver({}, { apiBase: "http://unused/api" }));
|
|
16
|
+
const paths = { socketPath: join(dir, "player.sock"), pidPath: join(dir, "player.pid") };
|
|
17
|
+
const daemon = new PlayerDaemon({ player, paths });
|
|
18
|
+
const ctl = async (...args) => (await promisify(execFile)("playerctl", ["--player=groovenet", ...args])).stdout.trim();
|
|
19
|
+
let close;
|
|
20
|
+
try {
|
|
21
|
+
player.add(["First", "Second"].map((title) => ({ title, track_id: title, artist: "Artist", album: "Album", friend_id: 1, local_audio_url: `${title}.mp3` })));
|
|
22
|
+
await daemon.start();
|
|
23
|
+
close = await startMpris(paths.socketPath);
|
|
24
|
+
assert.ok(close, "MPRIS must be enabled in this session");
|
|
25
|
+
assert.equal(await ctl("status"), "Stopped");
|
|
26
|
+
await ctl("play");
|
|
27
|
+
assert.equal(await ctl("status"), "Playing");
|
|
28
|
+
assert.equal(await ctl("metadata", "xesam:title"), "First");
|
|
29
|
+
assert.equal(await ctl("metadata", "xesam:artist"), "Artist");
|
|
30
|
+
assert.equal(await ctl("metadata", "xesam:album"), "Album");
|
|
31
|
+
await ctl("play-pause");
|
|
32
|
+
assert.equal(await ctl("status"), "Paused");
|
|
33
|
+
await ctl("position", "12.5");
|
|
34
|
+
assert.equal(Number(await ctl("position")), 12.5);
|
|
35
|
+
await ctl("position", "2+");
|
|
36
|
+
assert.equal(Number(await ctl("position")), 14.5);
|
|
37
|
+
await ctl("play-pause");
|
|
38
|
+
assert.equal(await ctl("status"), "Playing");
|
|
39
|
+
await ctl("next");
|
|
40
|
+
assert.equal(await ctl("metadata", "xesam:title"), "Second");
|
|
41
|
+
await ctl("previous");
|
|
42
|
+
assert.equal(await ctl("metadata", "xesam:title"), "First");
|
|
43
|
+
await ctl("stop");
|
|
44
|
+
assert.equal(await ctl("status"), "Stopped");
|
|
45
|
+
assert.equal(player.snapshot.queue.length, 2);
|
|
46
|
+
await close();
|
|
47
|
+
await player.play();
|
|
48
|
+
assert.equal(player.snapshot.status, "playing");
|
|
49
|
+
console.log("MPRIS playerctl smoke test passed.");
|
|
50
|
+
} finally {
|
|
51
|
+
await close?.();
|
|
52
|
+
await daemon.stop();
|
|
53
|
+
await rm(dir, { recursive: true, force: true });
|
|
54
|
+
}
|