@hashsome/integration.music-assistant 0.1.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/LICENSE +674 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/browse.d.ts +35 -0
- package/dist/browse.js +50 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +423 -0
- package/dist/mapper.d.ts +26 -0
- package/dist/mapper.js +50 -0
- package/dist/mock.d.ts +11 -0
- package/dist/mock.js +67 -0
- package/dist/position.d.ts +38 -0
- package/dist/position.js +94 -0
- package/package.json +40 -0
package/dist/position.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where playback is, as the dashboard shows it, for one Music Assistant player and its queue.
|
|
3
|
+
*
|
|
4
|
+
* Music Assistant does not make this easy. It sends no queue updates while a track plays, only on
|
|
5
|
+
* changes, so a position has to be carried forward from the last one. A pause on a player that
|
|
6
|
+
* cannot hold a stream open is really a stop: the queue's counter goes back to the start and only
|
|
7
|
+
* its `resume_pos` says where playback will pick up, and that number is cleared as playback starts.
|
|
8
|
+
* Reading each message as the truth makes the counter jump back and forth around a pause.
|
|
9
|
+
*
|
|
10
|
+
* So this keeps a position of its own, and follows two rules. While stopped, show where it was
|
|
11
|
+
* stopped. Once playing, the queue's own counter is the truth, from its first message on.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is pure: a state in, a message in, a state out.
|
|
14
|
+
*/
|
|
15
|
+
/** Only `idle` is a stop. Playing, buffering, paused and a state not yet known all follow the counter. */
|
|
16
|
+
const following = (playback) => playback !== 'idle';
|
|
17
|
+
/** How far before where it was a resumed stream may start, which is not shown as going back. */
|
|
18
|
+
const REWIND_SECONDS = 2;
|
|
19
|
+
export function onQueue(position, message, playback, now) {
|
|
20
|
+
// A different track has nothing to do with where the last one was, and starts out carrying its
|
|
21
|
+
// resume spot, which is not its own.
|
|
22
|
+
const same = message.item === undefined || position.item === undefined || message.item === position.item;
|
|
23
|
+
const base = same
|
|
24
|
+
? position
|
|
25
|
+
: position.resume !== undefined
|
|
26
|
+
? { inherited: position.resume }
|
|
27
|
+
: {};
|
|
28
|
+
// What the message says about the resume spot, less the old track's number.
|
|
29
|
+
const resume = message.resume !== base.inherited ? message.resume : undefined;
|
|
30
|
+
const { inherited, ...own } = base;
|
|
31
|
+
const stopped = !following(playback);
|
|
32
|
+
// Stopped, a resume spot that goes down is Music Assistant clearing it as playback is about to
|
|
33
|
+
// start, and not where it will resume.
|
|
34
|
+
const spot = stopped && resume !== undefined && own.resume !== undefined
|
|
35
|
+
? Math.max(own.resume, resume)
|
|
36
|
+
: resume;
|
|
37
|
+
const next = {
|
|
38
|
+
...own,
|
|
39
|
+
...(message.item !== undefined ? { item: message.item } : {}),
|
|
40
|
+
...(spot !== undefined ? { resume: spot } : {}),
|
|
41
|
+
// Until the track has a resume spot of its own, the old one is still to be ignored.
|
|
42
|
+
...(resume === undefined && inherited !== undefined ? { inherited } : {}),
|
|
43
|
+
};
|
|
44
|
+
if (message.elapsed === undefined) {
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
if (stopped) {
|
|
48
|
+
// Its counter is back at the start: only the first number is of any use.
|
|
49
|
+
return next.elapsed === undefined && next.held === undefined
|
|
50
|
+
? { ...next, elapsed: message.elapsed, elapsedAt: now }
|
|
51
|
+
: next;
|
|
52
|
+
}
|
|
53
|
+
// Playing: the queue's counter is the truth, apart from the stream starting a moment before the
|
|
54
|
+
// spot it was stopped at, which is not shown as a step back.
|
|
55
|
+
const { held, ...rest } = next;
|
|
56
|
+
const elapsed = held !== undefined && message.elapsed >= held - REWIND_SECONDS
|
|
57
|
+
? Math.max(message.elapsed, held)
|
|
58
|
+
: message.elapsed;
|
|
59
|
+
return { ...rest, elapsed, elapsedAt: now };
|
|
60
|
+
}
|
|
61
|
+
/** The player's state changed: a stop remembers where it got to, and a start from a stop begins there. */
|
|
62
|
+
export function onPlayback(position, from, to, now) {
|
|
63
|
+
if (from === to) {
|
|
64
|
+
return position;
|
|
65
|
+
}
|
|
66
|
+
if (following(from) && !following(to)) {
|
|
67
|
+
if (position.elapsed === undefined) {
|
|
68
|
+
return position;
|
|
69
|
+
}
|
|
70
|
+
const since = from === 'playing' && position.elapsedAt !== undefined
|
|
71
|
+
? Math.max(0, now - position.elapsedAt)
|
|
72
|
+
: 0;
|
|
73
|
+
// The resume spot is of the last stop, and the next one brings its own.
|
|
74
|
+
const { resume: _resume, ...rest } = position;
|
|
75
|
+
return { ...rest, held: position.elapsed + since };
|
|
76
|
+
}
|
|
77
|
+
if (!following(from) && following(to)) {
|
|
78
|
+
// Starting from a stop: from where it was held or, if that stop was not seen, the spot Music
|
|
79
|
+
// Assistant says it will resume at. Its first queue message says where it really starts.
|
|
80
|
+
const origin = position.held ?? position.resume ?? position.elapsed;
|
|
81
|
+
return origin === undefined
|
|
82
|
+
? position
|
|
83
|
+
: { ...position, held: origin, elapsed: origin, elapsedAt: now };
|
|
84
|
+
}
|
|
85
|
+
return position;
|
|
86
|
+
}
|
|
87
|
+
/** What to show: playing, the counter; stopped, the later of where it was held and the resume spot. */
|
|
88
|
+
export function positionOf(position, playback) {
|
|
89
|
+
if (following(playback)) {
|
|
90
|
+
return position.elapsed;
|
|
91
|
+
}
|
|
92
|
+
const spots = [position.held, position.resume].filter((spot) => spot !== undefined);
|
|
93
|
+
return spots.length > 0 ? Math.max(...spots) : position.elapsed;
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hashsome/integration.music-assistant",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Music Assistant integration: direct WebSocket client, not via Home Assistant",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@hashsome/core": "0.1.0"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./mock": {
|
|
15
|
+
"types": "./dist/mock.d.ts",
|
|
16
|
+
"default": "./dist/mock.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"license": "GPL-3.0-only",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/dstoyanoff/hashsome.git",
|
|
24
|
+
"directory": "packages/integrations/music-assistant"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/dstoyanoff/hashsome#readme",
|
|
27
|
+
"bugs": "https://github.com/dstoyanoff/hashsome/issues",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"provenance": true
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"players": "node scripts/print-players.ts",
|
|
38
|
+
"build": "tsc -b tsconfig.build.json"
|
|
39
|
+
}
|
|
40
|
+
}
|