@audiofool/moondog 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.
Files changed (109) hide show
  1. package/README.md +125 -0
  2. package/assets/brand/moondog-lunar-record/moondog-logo-1x1.png +0 -0
  3. package/contracts/v1/capability-policy.schema.json +572 -0
  4. package/contracts/v1/definitions.schema.json +365 -0
  5. package/contracts/v1/library-track-observation.schema.json +138 -0
  6. package/contracts/v1/listening-event.schema.json +111 -0
  7. package/contracts/v1/profile-evidence.schema.json +237 -0
  8. package/contracts/v1/taste-event.schema.json +258 -0
  9. package/contracts/v1/tool-invocation.schema.json +318 -0
  10. package/contracts/v1/tool-receipt.schema.json +1066 -0
  11. package/contracts/v1/track-ref.schema.json +275 -0
  12. package/contracts/v2/profile-evidence.schema.json +284 -0
  13. package/package.json +105 -0
  14. package/scripts/contract-lib.mjs +361 -0
  15. package/scripts/contract-semantics.mjs +2630 -0
  16. package/scripts/demo-studio.mjs +58 -0
  17. package/scripts/moondog +52 -0
  18. package/scripts/moondog.mjs +954 -0
  19. package/src/core/apple-data-migration.mjs +50 -0
  20. package/src/core/apple-library-source-status.mjs +220 -0
  21. package/src/core/apple-projection-domain-services.mjs +2356 -0
  22. package/src/core/capability-catalog.mjs +577 -0
  23. package/src/core/listening-profile-domain-services.mjs +1010 -0
  24. package/src/core/lyric-library.mjs +121 -0
  25. package/src/core/lyric-profile.mjs +50 -0
  26. package/src/core/lyric-service.mjs +41 -0
  27. package/src/core/moondog-application.mjs +1827 -0
  28. package/src/core/state-directory.mjs +19 -0
  29. package/src/core/synthetic-domain-services.mjs +1485 -0
  30. package/src/core/uuid-v5.mjs +42 -0
  31. package/src/demo/fictional-spotify-history.mjs +400 -0
  32. package/src/demo/moondog-demo.mjs +355 -0
  33. package/src/demo/moondog-interactive-tasteprint-demo.mjs +293 -0
  34. package/src/demo/moondog-tasteprint-demo.mjs +631 -0
  35. package/src/importers/apple-music-library/import-batch.mjs +290 -0
  36. package/src/importers/apple-music-library/index.mjs +76 -0
  37. package/src/importers/apple-music-library/normalize.mjs +1126 -0
  38. package/src/importers/apple-music-library/parse-plist.mjs +357 -0
  39. package/src/importers/apple-music-library/projection-records.mjs +557 -0
  40. package/src/importers/apple-music-library/read-batch.mjs +276 -0
  41. package/src/importers/apple-music-library/sqlite-projection.mjs +1307 -0
  42. package/src/importers/apple-music-library/stable-ids.mjs +19 -0
  43. package/src/importers/apple-music-library/track-snapshot.schema.json +136 -0
  44. package/src/integrations/apple-music/catalog.mjs +770 -0
  45. package/src/integrations/cross-catalog-artist-identity.mjs +222 -0
  46. package/src/integrations/listenbrainz/history-file.mjs +560 -0
  47. package/src/integrations/lyrics/lrclib.mjs +55 -0
  48. package/src/integrations/open-music-similarity/artist-radio.mjs +806 -0
  49. package/src/integrations/public-playlists.mjs +104 -0
  50. package/src/integrations/spotify/account-data-history.mjs +545 -0
  51. package/src/integrations/spotify/account-data-profile.mjs +918 -0
  52. package/src/integrations/spotify/authentication.mjs +791 -0
  53. package/src/integrations/spotify/catalog-resolver.mjs +657 -0
  54. package/src/integrations/spotify/connection.mjs +125 -0
  55. package/src/integrations/spotify/credential-store.mjs +26 -0
  56. package/src/integrations/spotify/extended-streaming-history.mjs +525 -0
  57. package/src/integrations/spotify/history-archive.mjs +59 -0
  58. package/src/integrations/spotify/index.mjs +80 -0
  59. package/src/integrations/spotify/recent-activity.mjs +198 -0
  60. package/src/integrations/spotify/resolution-cache.mjs +260 -0
  61. package/src/integrations/spotify/service.mjs +713 -0
  62. package/src/integrations/spotify/settings.mjs +89 -0
  63. package/src/integrations/spotify/web-api-client.mjs +761 -0
  64. package/src/integrations/web/codex-web.mjs +263 -0
  65. package/src/integrations/youtube-music/takeout.mjs +143 -0
  66. package/src/memory/local-memory-store.mjs +1705 -0
  67. package/src/memory/reflection-worker.mjs +87 -0
  68. package/src/profile/apple-library-import.mjs +28 -0
  69. package/src/profile/history-import.mjs +163 -0
  70. package/src/profile/listener-corrections.mjs +217 -0
  71. package/src/profile/listening-history-store.mjs +2055 -0
  72. package/src/profile/listening-profile-projection.mjs +2520 -0
  73. package/src/profile/local-music-data.mjs +812 -0
  74. package/src/profile/music-import-bundle.mjs +91 -0
  75. package/src/profile/music-providers.mjs +25 -0
  76. package/src/profile/spotify-archive-catalog-hints.mjs +210 -0
  77. package/src/profile/spotify-archive-taste.mjs +482 -0
  78. package/src/runtime/pi/agent-runtime.mjs +6444 -0
  79. package/src/runtime/pi/authentication.mjs +104 -0
  80. package/src/runtime/pi/configured-runtime.mjs +98 -0
  81. package/src/runtime/pi/memory-agent-runtime.mjs +337 -0
  82. package/src/runtime/pi/model-catalog.mjs +62 -0
  83. package/src/runtime/pi/model-transport.mjs +117 -0
  84. package/src/runtime/pi/persistent-credential-store.mjs +670 -0
  85. package/src/runtime/pi/provider-registry.mjs +74 -0
  86. package/src/runtime/pi/runtime-settings.mjs +81 -0
  87. package/src/surfaces/cli/auth-command.mjs +355 -0
  88. package/src/surfaces/cli/brand-components.mjs +390 -0
  89. package/src/surfaces/cli/brand-theme.mjs +231 -0
  90. package/src/surfaces/cli/catalog-command.mjs +509 -0
  91. package/src/surfaces/cli/command-completions.mjs +113 -0
  92. package/src/surfaces/cli/data-command.mjs +214 -0
  93. package/src/surfaces/cli/format-output.mjs +750 -0
  94. package/src/surfaces/cli/history-import-view.mjs +445 -0
  95. package/src/surfaces/cli/import-guides.mjs +9 -0
  96. package/src/surfaces/cli/listenbrainz-command.mjs +129 -0
  97. package/src/surfaces/cli/listening-menu.mjs +171 -0
  98. package/src/surfaces/cli/profile-command.mjs +290 -0
  99. package/src/surfaces/cli/spotify-command.mjs +849 -0
  100. package/src/surfaces/cli/taste-profile-model.mjs +306 -0
  101. package/src/surfaces/cli/taste-profile-view.mjs +272 -0
  102. package/src/surfaces/cli/terminal-art.mjs +295 -0
  103. package/src/surfaces/cli/tui.mjs +1714 -0
  104. package/src/surfaces/cli/web-command.mjs +52 -0
  105. package/src/surfaces/html/tasteprint-artifact.mjs +96 -0
  106. package/src/surfaces/html/tasteprint.mjs +3575 -0
  107. package/src/surfaces/web/showcase.mjs +272 -0
  108. package/src/surfaces/web/studio-page.mjs +4148 -0
  109. package/src/surfaces/web/studio.mjs +2288 -0
@@ -0,0 +1,42 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const uuidPattern =
4
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
5
+
6
+ function uuidBytes(uuid) {
7
+ if (!uuidPattern.test(uuid)) {
8
+ throw new TypeError("UUID namespace is invalid");
9
+ }
10
+ return Buffer.from(uuid.replaceAll("-", ""), "hex");
11
+ }
12
+
13
+ function formatUuid(bytes) {
14
+ const hex = bytes.toString("hex");
15
+ return [
16
+ hex.slice(0, 8),
17
+ hex.slice(8, 12),
18
+ hex.slice(12, 16),
19
+ hex.slice(16, 20),
20
+ hex.slice(20, 32),
21
+ ].join("-");
22
+ }
23
+
24
+ export function uuidV5(name, namespace) {
25
+ if (typeof name !== "string") {
26
+ throw new TypeError("UUID name must be a string");
27
+ }
28
+
29
+ const digest = createHash("sha1")
30
+ .update(uuidBytes(namespace))
31
+ .update(name, "utf8")
32
+ .digest()
33
+ .subarray(0, 16);
34
+
35
+ digest[6] = (digest[6] & 0x0f) | 0x50;
36
+ digest[8] = (digest[8] & 0x3f) | 0x80;
37
+ return formatUuid(digest);
38
+ }
39
+
40
+ export function isUuid(value) {
41
+ return typeof value === "string" && uuidPattern.test(value);
42
+ }
@@ -0,0 +1,400 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ chmod,
4
+ link,
5
+ lstat,
6
+ mkdtemp,
7
+ rm,
8
+ writeFile,
9
+ } from "node:fs/promises";
10
+ import path from "node:path";
11
+
12
+ export const FICTIONAL_SPOTIFY_HISTORY_VERSION =
13
+ "moondog-fictional-spotify-history/2";
14
+
15
+ const archiveDirectory = "Spotify Extended Streaming History";
16
+ const fixedDosTime = 12 << 11;
17
+ const fixedDosDate = ((2026 - 1980) << 9) | (8 << 5) | 25;
18
+ const utf8Flag = 0x0800;
19
+
20
+ const tracks = Object.freeze({
21
+ midnightLines: Object.freeze({
22
+ id: "MoondogDemoTrack000001",
23
+ title: "Midnight Lines",
24
+ artist: "Mara Vale",
25
+ album: "Night Transit",
26
+ }),
27
+ quietCoordinates: Object.freeze({
28
+ id: "MoondogDemoTrack000002",
29
+ title: "Quiet Coordinates",
30
+ artist: "Sable Arcade",
31
+ album: "Night Survey",
32
+ }),
33
+ weatherMemory: Object.freeze({
34
+ id: "MoondogDemoTrack000003",
35
+ title: "Weather Memory",
36
+ artist: "Cinder Lake",
37
+ album: "Shoreline Code",
38
+ }),
39
+ glassHighway: Object.freeze({
40
+ id: "MoondogDemoTrack000004",
41
+ title: "Glass Highway",
42
+ artist: "North Window",
43
+ album: "Pale Signals",
44
+ }),
45
+ signalGarden: Object.freeze({
46
+ id: "MoondogDemoTrack000005",
47
+ title: "Signal Garden",
48
+ artist: "Glass Atlas",
49
+ album: "Unfolding Maps",
50
+ }),
51
+ blueExit: Object.freeze({
52
+ id: "MoondogDemoTrack000006",
53
+ title: "Blue Exit",
54
+ artist: "Ash Meridian",
55
+ album: "Afterimage",
56
+ }),
57
+ parallelRooms: Object.freeze({
58
+ id: "MoondogDemoTrack000007",
59
+ title: "Parallel Rooms",
60
+ artist: "Elsewhere Signal",
61
+ album: "Long Distance",
62
+ }),
63
+ northernRelay: Object.freeze({
64
+ id: "MoondogDemoTrack000008",
65
+ title: "Northern Relay",
66
+ artist: "Drift Assembly",
67
+ album: "Moving Weather",
68
+ }),
69
+ blueHourIndex: Object.freeze({
70
+ id: "MoondogDemoTrack000009",
71
+ title: "Blue Hour Index",
72
+ artist: "New Coast Archive",
73
+ album: "Tide Tables",
74
+ }),
75
+ lowVoltageMoon: Object.freeze({
76
+ id: "MoondogDemoTrack000010",
77
+ title: "Low Voltage Moon",
78
+ artist: "Low Lanterns",
79
+ album: "Street Astronomy",
80
+ }),
81
+ lastTramHome: Object.freeze({
82
+ id: "MoondogDemoTrack000011",
83
+ title: "Last Tram Home",
84
+ artist: "Mara Vale",
85
+ album: "Night Transit",
86
+ }),
87
+ signalBlue: Object.freeze({
88
+ id: "MoondogDemoTrack000012",
89
+ title: "Signal Blue",
90
+ artist: "Mara Vale",
91
+ album: "Night Transit",
92
+ }),
93
+ paleStatic: Object.freeze({
94
+ id: "MoondogDemoTrack000013",
95
+ title: "Pale Static",
96
+ artist: "North Window",
97
+ album: "Pale Signals",
98
+ }),
99
+ exitSigns: Object.freeze({
100
+ id: "MoondogDemoTrack000014",
101
+ title: "Exit Signs",
102
+ artist: "North Window",
103
+ album: "Pale Signals",
104
+ }),
105
+ });
106
+
107
+ const playSchedule = Object.freeze({
108
+ 2023: Object.freeze([
109
+ ["2023-09-01T21:12:00Z", "midnightLines", 304_000],
110
+ ["2023-09-01T21:17:00Z", "midnightLines", 311_000],
111
+ ["2023-09-01T21:22:00Z", "midnightLines", 306_000],
112
+ ["2023-09-01T21:27:00Z", "lastTramHome", 296_000],
113
+ ["2023-09-01T21:32:00Z", "signalBlue", 289_000],
114
+ ["2023-11-18T21:14:00Z", "quietCoordinates", 322_000],
115
+ ["2023-11-18T21:20:00Z", "midnightLines", 309_000],
116
+ ["2023-11-18T21:25:00Z", "weatherMemory", 284_000],
117
+ ["2023-11-18T21:31:00Z", "quietCoordinates", 315_000],
118
+ ["2023-12-28T20:11:00Z", "midnightLines", 307_000, { shuffle: true }],
119
+ ]),
120
+ 2024: Object.freeze([
121
+ ["2024-01-11T21:05:00Z", "glassHighway", 314_000],
122
+ ["2024-01-11T21:10:00Z", "glassHighway", 319_000],
123
+ ["2024-01-11T21:15:00Z", "glassHighway", 316_000],
124
+ ["2024-01-11T21:20:00Z", "paleStatic", 287_000],
125
+ ["2024-01-11T21:25:00Z", "exitSigns", 292_000],
126
+ ["2024-06-01T21:48:00Z", "glassHighway", 321_000],
127
+ ["2024-06-01T21:53:00Z", "signalGarden", 298_000],
128
+ ["2024-06-01T21:58:00Z", "glassHighway", 317_000],
129
+ ["2024-06-01T22:03:00Z", "midnightLines", 305_000],
130
+ ["2024-12-28T21:37:00Z", "glassHighway", 323_000, { offline: true }],
131
+ [
132
+ "2024-12-28T21:43:00Z",
133
+ "lowVoltageMoon",
134
+ 21_000,
135
+ { reason_end: "fwdbtn", skipped: true },
136
+ ],
137
+ ]),
138
+ 2025: Object.freeze([
139
+ ["2025-01-13T20:42:00Z", "blueExit", 307_000],
140
+ ["2025-01-13T20:47:00Z", "blueExit", 312_000],
141
+ ["2025-01-13T20:52:00Z", "blueExit", 309_000],
142
+ ["2025-01-13T20:57:00Z", "parallelRooms", 281_000],
143
+ ["2025-01-13T21:02:00Z", "quietCoordinates", 319_000],
144
+ ["2025-04-25T21:23:00Z", "blueExit", 315_000],
145
+ ["2025-04-25T21:28:00Z", "parallelRooms", 286_000],
146
+ ["2025-04-25T21:33:00Z", "blueExit", 311_000],
147
+ ["2025-04-25T21:38:00Z", "glassHighway", 318_000],
148
+ ["2025-09-09T23:06:00Z", "blueExit", 314_000, { shuffle: true }],
149
+ ["2025-09-09T23:11:00Z", "parallelRooms", 283_000],
150
+ ["2025-09-09T23:16:00Z", "blueExit", 316_000],
151
+ ["2025-11-26T19:42:00Z", "quietCoordinates", 321_000],
152
+ [
153
+ "2025-12-29T20:02:00Z",
154
+ "lowVoltageMoon",
155
+ 18_000,
156
+ { reason_end: "fwdbtn", skipped: true },
157
+ ],
158
+ ]),
159
+ 2026: Object.freeze([
160
+ ["2026-01-08T14:20:00Z", "signalGarden", 299_000],
161
+ ["2026-01-08T14:25:00Z", "northernRelay", 321_000],
162
+ ["2026-01-08T14:30:00Z", "blueHourIndex", 292_000],
163
+ ["2026-01-08T14:35:00Z", "northernRelay", 324_000],
164
+ ["2026-03-09T22:41:00Z", "northernRelay", 319_000],
165
+ ["2026-03-09T22:46:00Z", "blueExit", 310_000],
166
+ ["2026-03-09T22:51:00Z", "northernRelay", 326_000],
167
+ ["2026-03-09T22:56:00Z", "glassHighway", 315_000],
168
+ ["2026-06-04T19:12:00Z", "blueHourIndex", 296_000],
169
+ ["2026-06-04T19:17:00Z", "northernRelay", 323_000],
170
+ ["2026-06-04T19:22:00Z", "midnightLines", 308_000],
171
+ ["2026-06-04T19:27:00Z", "northernRelay", 327_000],
172
+ ["2026-08-24T22:06:00Z", "blueHourIndex", 294_000],
173
+ ["2026-08-24T22:12:00Z", "northernRelay", 325_000],
174
+ ["2026-08-24T22:18:00Z", "blueHourIndex", 297_000],
175
+ [
176
+ "2026-08-24T22:24:00Z",
177
+ "lowVoltageMoon",
178
+ 16_000,
179
+ { reason_end: "fwdbtn", skipped: true },
180
+ ],
181
+ ["2026-08-24T22:30:00Z", "northernRelay", 328_000, { offline: true }],
182
+ ]),
183
+ });
184
+
185
+ function fail(message) {
186
+ throw new Error(`Fictional Spotify history generation failed: ${message}`);
187
+ }
188
+
189
+ function spotifyRecord([ts, trackKey, playedMs, overrides = {}], index, plays) {
190
+ const track = tracks[trackKey];
191
+ if (!track) fail(`unknown fictional track key: ${trackKey}`);
192
+ const previousTimestamp = index > 0 ? plays[index - 1][0] : null;
193
+ const gapMilliseconds = previousTimestamp
194
+ ? Date.parse(ts) - Date.parse(previousTimestamp)
195
+ : Number.POSITIVE_INFINITY;
196
+ const continuedSession =
197
+ gapMilliseconds >= 0 && gapMilliseconds <= 30 * 60_000;
198
+ return {
199
+ ts,
200
+ ms_played: playedMs,
201
+ master_metadata_track_name: track.title,
202
+ master_metadata_album_artist_name: track.artist,
203
+ master_metadata_album_album_name: track.album,
204
+ spotify_track_uri: `spotify:track:${track.id}`,
205
+ reason_start: continuedSession ? "trackdone" : "clickrow",
206
+ reason_end: "trackdone",
207
+ shuffle: false,
208
+ skipped: false,
209
+ offline: false,
210
+ incognito_mode: false,
211
+ ...overrides,
212
+ };
213
+ }
214
+
215
+ export function fictionalSpotifyHistoryEntries() {
216
+ return Object.entries(playSchedule).map(([year, plays]) => ({
217
+ name: `${archiveDirectory}/Streaming_History_Audio_${year}.json`,
218
+ data: Buffer.from(
219
+ `${JSON.stringify(plays.map(spotifyRecord), null, 2)}\n`,
220
+ "utf8",
221
+ ),
222
+ year: Number(year),
223
+ records: plays.length,
224
+ }));
225
+ }
226
+
227
+ const crcTable = Array.from({ length: 256 }, (_, index) => {
228
+ let value = index;
229
+ for (let bit = 0; bit < 8; bit += 1) {
230
+ value = (value & 1) === 1
231
+ ? (value >>> 1) ^ 0xedb88320
232
+ : value >>> 1;
233
+ }
234
+ return value >>> 0;
235
+ });
236
+
237
+ function crc32(buffer) {
238
+ let value = 0xffffffff;
239
+ for (const byte of buffer) {
240
+ value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
241
+ }
242
+ return (value ^ 0xffffffff) >>> 0;
243
+ }
244
+
245
+ function localHeader(name, data) {
246
+ const header = Buffer.alloc(30);
247
+ header.writeUInt32LE(0x04034b50, 0);
248
+ header.writeUInt16LE(20, 4);
249
+ header.writeUInt16LE(utf8Flag, 6);
250
+ header.writeUInt16LE(0, 8);
251
+ header.writeUInt16LE(fixedDosTime, 10);
252
+ header.writeUInt16LE(fixedDosDate, 12);
253
+ header.writeUInt32LE(crc32(data), 14);
254
+ header.writeUInt32LE(data.length, 18);
255
+ header.writeUInt32LE(data.length, 22);
256
+ header.writeUInt16LE(name.length, 26);
257
+ header.writeUInt16LE(0, 28);
258
+ return header;
259
+ }
260
+
261
+ function centralHeader(name, data, offset) {
262
+ const header = Buffer.alloc(46);
263
+ header.writeUInt32LE(0x02014b50, 0);
264
+ header.writeUInt16LE(20, 4);
265
+ header.writeUInt16LE(20, 6);
266
+ header.writeUInt16LE(utf8Flag, 8);
267
+ header.writeUInt16LE(0, 10);
268
+ header.writeUInt16LE(fixedDosTime, 12);
269
+ header.writeUInt16LE(fixedDosDate, 14);
270
+ header.writeUInt32LE(crc32(data), 16);
271
+ header.writeUInt32LE(data.length, 20);
272
+ header.writeUInt32LE(data.length, 24);
273
+ header.writeUInt16LE(name.length, 28);
274
+ header.writeUInt16LE(0, 30);
275
+ header.writeUInt16LE(0, 32);
276
+ header.writeUInt16LE(0, 34);
277
+ header.writeUInt16LE(0, 36);
278
+ header.writeUInt32LE(0, 38);
279
+ header.writeUInt32LE(offset, 42);
280
+ return header;
281
+ }
282
+
283
+ function endOfCentralDirectory(entryCount, size, offset) {
284
+ const header = Buffer.alloc(22);
285
+ header.writeUInt32LE(0x06054b50, 0);
286
+ header.writeUInt16LE(0, 4);
287
+ header.writeUInt16LE(0, 6);
288
+ header.writeUInt16LE(entryCount, 8);
289
+ header.writeUInt16LE(entryCount, 10);
290
+ header.writeUInt32LE(size, 12);
291
+ header.writeUInt32LE(offset, 16);
292
+ header.writeUInt16LE(0, 20);
293
+ return header;
294
+ }
295
+
296
+ export function createFictionalSpotifyHistoryArchive() {
297
+ const entries = fictionalSpotifyHistoryEntries();
298
+ const localParts = [];
299
+ const centralParts = [];
300
+ let localOffset = 0;
301
+ for (const entry of entries) {
302
+ const name = Buffer.from(entry.name, "utf8");
303
+ const local = localHeader(name, entry.data);
304
+ localParts.push(local, name, entry.data);
305
+ centralParts.push(centralHeader(name, entry.data, localOffset), name);
306
+ localOffset += local.length + name.length + entry.data.length;
307
+ }
308
+ const centralDirectory = Buffer.concat(centralParts);
309
+ return Buffer.concat([
310
+ ...localParts,
311
+ centralDirectory,
312
+ endOfCentralDirectory(entries.length, centralDirectory.length, localOffset),
313
+ ]);
314
+ }
315
+
316
+ function archiveManifest(outputPath, archive) {
317
+ const entries = fictionalSpotifyHistoryEntries();
318
+ const records = entries.flatMap((entry) =>
319
+ JSON.parse(entry.data.toString("utf8")),
320
+ );
321
+ const timestamps = records.map((record) => record.ts).sort();
322
+ return {
323
+ schema: FICTIONAL_SPOTIFY_HISTORY_VERSION,
324
+ archive_path: outputPath,
325
+ archive_bytes: archive.length,
326
+ archive_sha256: createHash("sha256").update(archive).digest("hex"),
327
+ record_count: records.length,
328
+ years: entries.map((entry) => entry.year),
329
+ members: entries.map((entry) => entry.name),
330
+ listening_range: {
331
+ earliest: timestamps[0],
332
+ latest: timestamps.at(-1),
333
+ },
334
+ boundaries: {
335
+ fictional_demonstration_data_only: true,
336
+ private_listener_data_read: false,
337
+ network_requests: false,
338
+ provider_actions: false,
339
+ persistent_profile_writes: false,
340
+ },
341
+ };
342
+ }
343
+
344
+ async function pathExists(filePath) {
345
+ try {
346
+ await lstat(filePath);
347
+ return true;
348
+ } catch (error) {
349
+ if (error?.code === "ENOENT") return false;
350
+ throw error;
351
+ }
352
+ }
353
+
354
+ export async function writeFictionalSpotifyHistoryArchive({ outputPath } = {}) {
355
+ if (typeof outputPath !== "string" || !outputPath.trim()) {
356
+ fail("an explicit output path is required");
357
+ }
358
+ if (!path.isAbsolute(outputPath)) {
359
+ fail("the output path must be absolute");
360
+ }
361
+ const resolvedPath = path.normalize(outputPath);
362
+ if (path.extname(resolvedPath).toLocaleLowerCase("en-US") !== ".zip") {
363
+ fail("the output file must use the .zip extension");
364
+ }
365
+ if (await pathExists(resolvedPath)) {
366
+ fail(`the destination already exists: ${resolvedPath}`);
367
+ }
368
+ const parentPath = path.dirname(resolvedPath);
369
+ const parent = await lstat(parentPath).catch((error) => {
370
+ if (error?.code === "ENOENT") {
371
+ fail(`the destination parent does not exist: ${parentPath}`);
372
+ }
373
+ throw error;
374
+ });
375
+ if (!parent.isDirectory() || parent.isSymbolicLink()) {
376
+ fail(`the destination parent is not a regular directory: ${parentPath}`);
377
+ }
378
+
379
+ const archive = createFictionalSpotifyHistoryArchive();
380
+ const stagingRoot = await mkdtemp(
381
+ path.join(parentPath, ".moondog-fictional-history-"),
382
+ );
383
+ if (process.platform !== "win32") await chmod(stagingRoot, 0o700);
384
+ const stagedPath = path.join(stagingRoot, "history.zip");
385
+ try {
386
+ await writeFile(stagedPath, archive, { flag: "wx", mode: 0o600 });
387
+ if (process.platform !== "win32") await chmod(stagedPath, 0o600);
388
+ try {
389
+ await link(stagedPath, resolvedPath);
390
+ } catch (error) {
391
+ if (error?.code === "EEXIST") {
392
+ fail(`the destination appeared during generation: ${resolvedPath}`);
393
+ }
394
+ throw error;
395
+ }
396
+ return archiveManifest(resolvedPath, archive);
397
+ } finally {
398
+ await rm(stagingRoot, { recursive: true, force: true });
399
+ }
400
+ }