Repository navigation
Expand file tree
/
Copy pathmap.html
More file actions
282 lines (241 loc) · 22.5 KB
/
Copy pathmap.html
File metadata and controls
282 lines (241 loc) · 22.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Map — music library server</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--paper:#f5f5f5; --ink:#2d3142; --muted:#4f5d75;
--soft:#7a8399; --rule:rgba(45,49,66,0.12); --accent:#fa243c;
}
*{box-sizing:border-box}
body{
margin:0; padding:3rem 2rem 2rem; background:var(--paper); color:var(--ink);
font-family:'Geist',system-ui,sans-serif; -webkit-font-smoothing:antialiased;
}
.wrap{max-width:1240px;margin:0 auto}
.eyebrow{
font-family:'Geist Mono',monospace; font-size:8px; letter-spacing:0.16em;
text-transform:uppercase; color:var(--muted); margin:0 0 .5rem;
}
h1{font-family:'Instrument Serif',serif; font-weight:400; font-size:2rem; margin:0 0 .5rem; letter-spacing:-0.01em}
.sub{color:var(--muted); font-size:13px; margin:0 0 2rem; max-width:64ch; line-height:1.55}
svg{display:block;width:100%;height:auto}
.cards{display:grid; grid-template-columns:1.15fr 1fr 0.95fr; gap:1rem; margin-top:2.5rem}
.card{background:#fff; border:1px solid var(--rule); border-radius:6px; padding:1.25rem}
.card h3{font-size:13px; margin:0; font-weight:600}
.card-header{display:flex; align-items:center; gap:.5rem; margin:.25rem 0 .75rem}
.card-dot{width:7px;height:7px;border-radius:50%;flex:none}
.dot-accent{background:var(--accent)} .dot-ink{background:var(--ink)} .dot-muted{background:var(--soft)}
.card ul{margin:0; padding-left:1.1rem; color:var(--muted); font-size:12px; line-height:1.65}
.card .eyebrow{margin-bottom:.25rem}
footer{
margin-top:2.5rem; padding-top:1rem; border-top:1px solid var(--rule);
font-family:'Geist Mono',monospace; font-size:9px; color:var(--soft); letter-spacing:0.08em;
}
</style>
</head>
<body>
<div class="wrap">
<p class="eyebrow">Architecture · v1</p>
<h1>A library surrounded by players</h1>
<p class="sub">Volumio <em>is</em> a player, tied to its own sound card. Here the server needs none: it indexes, converts and serves URLs — other things do the playing. Three distinct outbound directions: a <strong>renderer</strong> plays now, a <strong>device</strong> holds a copy, an <strong>emitter</strong> lets others read.</p>
<svg viewBox="0 0 1200 664" role="img" aria-labelledby="map-title map-desc" xmlns="http://www.w3.org/2000/svg">
<title id="map-title">Map of the music library server</title>
<desc id="map-desc">Server topology: four families of sources feed a SQLite index, one job queue carries every long-running operation, and a single streaming endpoint serves clients, network renderers, devices and emitters. A scheduler and side channels cut across the whole thing.</desc>
<defs>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#fa243c"/></marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/></marker>
<marker id="arrow-sm" markerWidth="6" markerHeight="5" refX="5" refY="2.5" orient="auto"><polygon points="0 0, 6 2.5, 0 5" fill="#4f5d75"/></marker>
<marker id="arrow-dim" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="rgba(45,49,66,0.45)"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f5f5"/>
<!-- ============ server zone ============ -->
<rect x="260" y="72" width="696" height="336" rx="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
<rect x="288" y="64" width="152" height="16" fill="#f5f5f5"/>
<text x="296" y="76" fill="#4f5d75" font-size="8" font-family="'Geist Mono',monospace" letter-spacing="0.16em">MEDIA SERVER · NODE</text>
<!-- ============ connectors, before the boxes ============ -->
<!-- sources -> library: corridors 268 / - / 236 / 220 -->
<path d="M 200 124 H 260 Q 268 124 268 132 V 180 Q 268 188 276 188 H 288" fill="none" stroke="#4f5d75" stroke-width="1.2" marker-end="url(#arrow)"/>
<rect x="204" y="104" width="48" height="12" rx="2" fill="#f5f5f5"/>
<text x="228" y="113" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">SCAN</text>
<line x1="200" y1="212" x2="288" y2="212" stroke="#4f5d75" stroke-width="1.2" marker-end="url(#arrow)"/>
<rect x="216" y="192" width="56" height="12" rx="2" fill="#f5f5f5"/>
<text x="244" y="201" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">RCLONE</text>
<path d="M 200 300 H 228 Q 236 300 236 292 V 228 Q 236 220 244 220 H 288" fill="none" stroke="#4f5d75" stroke-width="1.2" marker-end="url(#arrow)"/>
<rect x="244" y="258" width="48" height="12" rx="2" fill="#f5f5f5"/>
<text x="268" y="267" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">FETCH</text>
<path d="M 200 388 H 212 Q 220 388 220 380 V 244 Q 220 236 228 236 H 288" fill="none" stroke="#4f5d75" stroke-width="1.2" marker-end="url(#arrow)"/>
<rect x="228" y="318" width="52" height="12" rx="2" fill="#f5f5f5"/>
<text x="254" y="327" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">MATCH</text>
<!-- job queue -> the three components (trigger, unlabelled) -->
<line x1="368" y1="160" x2="368" y2="176" stroke="#fa243c" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-accent)"/>
<line x1="560" y1="160" x2="560" y2="176" stroke="#fa243c" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-accent)"/>
<line x1="752" y1="160" x2="752" y2="176" stroke="#fa243c" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-accent)"/>
<!-- library and conversion -> endpoint (16px: the arrow says it) -->
<line x1="368" y1="248" x2="368" y2="264" stroke="#fa243c" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<line x1="560" y1="248" x2="560" y2="264" stroke="#fa243c" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- endpoint -> consumers: corridors 976 / 964 / - / 988 -->
<path d="M 940 272 H 968 Q 976 272 976 264 V 132 Q 976 124 984 124 H 1000" fill="none" stroke="#fa243c" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<rect x="912" y="176" width="52" height="12" rx="2" fill="#f5f5f5"/>
<text x="938" y="185" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">STREAM</text>
<path d="M 940 288 H 956 Q 964 288 964 280 V 220 Q 964 212 972 212 H 1000" fill="none" stroke="#fa243c" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<rect x="904" y="244" width="48" height="12" rx="2" fill="#f5f5f5"/>
<text x="928" y="253" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">URL</text>
<line x1="940" y1="300" x2="1000" y2="300" stroke="#fa243c" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<rect x="944" y="276" width="48" height="12" rx="2" fill="#f5f5f5"/>
<text x="968" y="285" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">PULL</text>
<path d="M 940 316 H 980 Q 988 316 988 324 V 380 Q 988 388 996 388 H 1000" fill="none" stroke="#fa243c" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<rect x="908" y="344" width="64" height="12" rx="2" fill="#f5f5f5"/>
<text x="940" y="353" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">SUBSONIC</text>
<!-- cross-cutting bars -> zone -->
<line x1="592" y1="460" x2="592" y2="408" stroke="#fa243c" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
<rect x="512" y="436" width="72" height="12" rx="2" fill="#f5f5f5"/>
<text x="548" y="445" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">TRIGGERS</text>
<line x1="624" y1="524" x2="624" y2="408" stroke="#fa243c" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
<rect x="632" y="500" width="80" height="12" rx="2" fill="#f5f5f5"/>
<text x="672" y="509" fill="#7a8399" font-size="8" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.06em">EVENTS</text>
<!-- ============ sources ============ -->
<rect x="40" y="92" width="160" height="64" rx="6" fill="#f5f5f5"/>
<rect x="40" y="92" width="160" height="64" rx="6" fill="rgba(79,93,117,0.06)" stroke="#7a8399" stroke-width="1"/>
<ellipse cx="58" cy="104" rx="7" ry="2.5" fill="none" stroke="#4f5d75" stroke-width="1"/>
<path d="M 51 104 v 7 a 7 2.5 0 0 0 14 0 v -7" fill="none" stroke="#4f5d75" stroke-width="1"/>
<text x="120" y="134" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Local storage</text>
<text x="120" y="148" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">disk · USB · mounted</text>
<rect x="40" y="180" width="160" height="64" rx="6" fill="#f5f5f5"/>
<rect x="40" y="180" width="160" height="64" rx="6" fill="rgba(79,93,117,0.06)" stroke="#7a8399" stroke-width="1"/>
<path d="M 50 201 v -10 h 6 l 2 -3 h 8 v 13 z" fill="none" stroke="#4f5d75" stroke-width="1"/>
<text x="120" y="222" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Remote sources</text>
<text x="120" y="236" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">SMB · S3 · Drive</text>
<rect x="40" y="268" width="160" height="64" rx="6" fill="#f5f5f5"/>
<rect x="40" y="268" width="160" height="64" rx="6" fill="rgba(79,93,117,0.06)" stroke="#7a8399" stroke-width="1"/>
<circle cx="58" cy="284" r="7" fill="none" stroke="#4f5d75" stroke-width="1"/>
<path d="M 51 284 h 14 M 58 277 a 10 7 0 0 1 0 14 a 10 7 0 0 1 0 -14" fill="none" stroke="#4f5d75" stroke-width="1"/>
<text x="120" y="310" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Streams</text>
<text x="120" y="324" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">radio · RSS · live</text>
<rect x="40" y="356" width="160" height="64" rx="6" fill="#f5f5f5"/>
<rect x="40" y="356" width="160" height="64" rx="6" fill="rgba(79,93,117,0.06)" stroke="#7a8399" stroke-width="1"/>
<text x="58" y="377" fill="#4f5d75" font-size="12" font-family="'Geist Mono',monospace" text-anchor="middle">{ }</text>
<text x="120" y="398" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Enrichment</text>
<text x="120" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">MusicBrainz · LB</text>
<!-- ============ focal bar: job queue ============ -->
<rect x="276" y="104" width="664" height="56" rx="6" fill="#f5f5f5"/>
<rect x="276" y="104" width="664" height="56" rx="6" fill="rgba(250,36,60,0.08)" stroke="#fa243c" stroke-width="1"/>
<rect x="288" y="112" width="36" height="12" rx="2" fill="transparent" stroke="rgba(250,36,60,0.40)" stroke-width="0.8"/>
<text x="306" y="121" fill="rgba(250,36,60,0.85)" font-size="7" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.08em">JOBS</text>
<text x="608" y="128" fill="#fa243c" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Job queue</text>
<text x="608" y="146" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">scan · transcode · fingerprint · podcasts · sync · relay — persisted, resumable</text>
<!-- ============ row: library / conversion / plugins ============ -->
<rect x="288" y="176" width="160" height="72" rx="6" fill="#f5f5f5"/>
<rect x="288" y="176" width="160" height="72" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
<rect x="296" y="184" width="40" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
<text x="316" y="193" fill="rgba(45,49,66,0.8)" font-size="7" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.08em">INDEX</text>
<text x="368" y="220" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Library</text>
<text x="368" y="236" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">node:sqlite</text>
<rect x="480" y="176" width="160" height="72" rx="6" fill="#f5f5f5"/>
<rect x="480" y="176" width="160" height="72" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
<rect x="488" y="184" width="40" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
<text x="508" y="193" fill="rgba(45,49,66,0.8)" font-size="7" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.08em">MEDIA</text>
<text x="560" y="220" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Conversion</text>
<text x="560" y="236" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">ffmpeg · chromaprint</text>
<rect x="672" y="176" width="160" height="72" rx="6" fill="#f5f5f5"/>
<rect x="672" y="176" width="160" height="72" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
<rect x="680" y="184" width="36" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
<text x="698" y="193" fill="rgba(45,49,66,0.8)" font-size="7" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.08em">HOST</text>
<text x="752" y="220" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Plugins</text>
<text x="752" y="236" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">manifest · UI zones</text>
<!-- ============ focal bar: streaming endpoint ============ -->
<rect x="276" y="264" width="664" height="56" rx="6" fill="#f5f5f5"/>
<rect x="276" y="264" width="664" height="56" rx="6" fill="rgba(250,36,60,0.08)" stroke="#fa243c" stroke-width="1"/>
<rect x="288" y="272" width="36" height="12" rx="2" fill="transparent" stroke="rgba(250,36,60,0.40)" stroke-width="0.8"/>
<text x="306" y="281" fill="rgba(250,36,60,0.85)" font-size="7" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.08em">HTTP</text>
<text x="608" y="288" fill="#fa243c" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Streaming endpoint</text>
<text x="608" y="306" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">Range · token · per-renderer profile — the server never pushes audio, it hands out a URL</text>
<!-- ============ consumers ============ -->
<rect x="1000" y="92" width="160" height="64" rx="6" fill="#f5f5f5"/>
<rect x="1000" y="92" width="160" height="64" rx="6" fill="rgba(79,93,117,0.06)" stroke="#7a8399" stroke-width="1"/>
<rect x="1010" y="99" width="16" height="11" rx="2" fill="none" stroke="#4f5d75" stroke-width="1"/>
<line x1="1014" y1="113" x2="1022" y2="113" stroke="#4f5d75" stroke-width="1"/>
<text x="1080" y="134" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Clients</text>
<text x="1080" y="148" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">web · tablet · Pi+DAC</text>
<rect x="1000" y="180" width="160" height="64" rx="6" fill="#f5f5f5"/>
<rect x="1000" y="180" width="160" height="64" rx="6" fill="rgba(79,93,117,0.06)" stroke="#7a8399" stroke-width="1"/>
<circle cx="1018" cy="196" r="7" fill="none" stroke="#4f5d75" stroke-width="1"/>
<path d="M 1011 196 h 14 M 1018 189 a 10 7 0 0 1 0 14 a 10 7 0 0 1 0 -14" fill="none" stroke="#4f5d75" stroke-width="1"/>
<text x="1080" y="222" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Network renderers</text>
<text x="1080" y="236" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">UPnP · Sonos · AirPlay</text>
<rect x="1000" y="268" width="160" height="64" rx="6" fill="#f5f5f5"/>
<rect x="1000" y="268" width="160" height="64" rx="6" fill="rgba(79,93,117,0.06)" stroke="#7a8399" stroke-width="1"/>
<path d="M 1010 289 v -10 h 6 l 2 -3 h 8 v 13 z" fill="none" stroke="#4f5d75" stroke-width="1"/>
<text x="1080" y="310" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Devices</text>
<text x="1080" y="324" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">iPod · via satellite</text>
<rect x="1000" y="356" width="160" height="64" rx="6" fill="#f5f5f5"/>
<rect x="1000" y="356" width="160" height="64" rx="6" fill="rgba(79,93,117,0.06)" stroke="#7a8399" stroke-width="1"/>
<text x="1018" y="377" fill="#4f5d75" font-size="12" font-family="'Geist Mono',monospace" text-anchor="middle">{ }</text>
<text x="1080" y="398" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Emitters</text>
<text x="1080" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">OpenSubsonic · DLNA</text>
<!-- ============ cross-cutting bars ============ -->
<rect x="40" y="460" width="1120" height="56" rx="6" fill="#f5f5f5"/>
<rect x="40" y="460" width="1120" height="56" rx="6" fill="rgba(122,140,71,0.06)" stroke="rgba(122,140,71,0.35)" stroke-width="0.8"/>
<rect x="52" y="470" width="36" height="12" rx="2" fill="transparent" stroke="rgba(122,140,71,0.40)" stroke-width="0.8"/>
<text x="70" y="479" fill="rgba(122,140,71,0.85)" font-size="7" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.08em">CRON</text>
<text x="600" y="484" fill="#7a8c47" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Scheduler</text>
<text x="600" y="502" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">named workflows — iPod sync at 3am, podcast feeds every 6h, nightly rescan</text>
<rect x="40" y="524" width="1120" height="56" rx="6" fill="#f5f5f5"/>
<rect x="40" y="524" width="1120" height="56" rx="6" fill="rgba(90,125,154,0.06)" stroke="rgba(90,125,154,0.35)" stroke-width="0.8"/>
<rect x="52" y="534" width="36" height="12" rx="2" fill="transparent" stroke="rgba(90,125,154,0.40)" stroke-width="0.8"/>
<text x="70" y="543" fill="rgba(90,125,154,0.85)" font-size="7" font-family="'Geist Mono',monospace" text-anchor="middle" letter-spacing="0.08em">HORS</text>
<text x="600" y="548" fill="#5a7d9a" font-size="12" font-weight="600" font-family="'Geist',sans-serif" text-anchor="middle">Side channels</text>
<text x="600" y="566" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace" text-anchor="middle">scrobble to Last.fm and ListenBrainz · Home Assistant · MQTT</text>
<!-- ============ legend ============ -->
<line x1="40" y1="608" x2="1160" y2="608" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
<text x="40" y="628" fill="#4f5d75" font-size="8" font-family="'Geist Mono',monospace" letter-spacing="0.14em">LEGEND</text>
<rect x="128" y="620" width="12" height="10" rx="2" fill="rgba(250,36,60,0.08)" stroke="#fa243c" stroke-width="1"/>
<text x="148" y="628" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace">focal piece</text>
<rect x="268" y="620" width="12" height="10" rx="2" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
<text x="288" y="628" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace">core component</text>
<rect x="452" y="620" width="12" height="10" rx="2" fill="rgba(79,93,117,0.06)" stroke="#7a8399" stroke-width="1"/>
<text x="472" y="628" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace">input / output</text>
<line x1="616" y1="625" x2="644" y2="625" stroke="#fa243c" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="656" y="628" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace">audio flow</text>
<line x1="760" y1="625" x2="788" y2="625" stroke="#4f5d75" stroke-width="1.2" marker-end="url(#arrow)"/>
<text x="800" y="628" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace">indexing</text>
<line x1="920" y1="625" x2="948" y2="625" stroke="#fa243c" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-accent)"/>
<text x="960" y="628" fill="#4f5d75" font-size="9" font-family="'Geist Mono',monospace">trigger</text>
</svg>
<div class="cards">
<div class="card">
<p class="eyebrow">The central mechanism</p>
<div class="card-header"><span class="card-dot dot-accent"></span><h3>One queue</h3></div>
<ul>
<li>Scanning, transcoding, fingerprinting, podcasts, sync, acquisition, analysis, stream relaying — same shape, same implementation.</li>
<li>Persisted in SQLite, resumable, pausable, concurrency capped per kind.</li>
<li>The satellite protocol <em>is</em> this contract seen remotely: same schema, same display component.</li>
</ul>
</div>
<div class="card">
<p class="eyebrow">Why a single outlet</p>
<div class="card-header"><span class="card-dot dot-ink"></span><h3>We hand out a URL</h3></div>
<ul>
<li>Sonos, UPnP and AirPlay all work this way: "here is a URL, go fetch it".</li>
<li>No audio byte passes through memory — it is HTTP with <code>Range</code>.</li>
<li>The device declares its formats, the server converts only what will not play. Same rule for the iPod.</li>
</ul>
</div>
<div class="card">
<p class="eyebrow">What is not on this map</p>
<div class="card-header"><span class="card-dot dot-muted"></span><h3>The satellites</h3></div>
<ul>
<li>The iPod dock runs elsewhere, often on an armv6 Pi 1 — outside Node's reach.</li>
<li>It pulls files at its own pace and resumes after an interruption.</li>
<li>Transcoding and fingerprinting stay on the server; the satellite only writes the device database.</li>
</ul>
</div>
</div>
<footer>JUKEBOX · MAP · SEE DOCS/ARCHITECTURE.MD</footer>
</div>
</body>
</html>