@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,2356 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lyricSeedsFromProfile } from "./lyric-profile.mjs";
3
+ import { collectionCoverage } from "../profile/music-providers.mjs";
4
+ import { lstat } from "node:fs/promises";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ resolveAppleMusicImportsRoot,
9
+ } from "./apple-library-source-status.mjs";
10
+ import {
11
+ computeAppleProjectionInputDigest,
12
+ defaultAppleMusicProjectionPath,
13
+ defaultAppleMusicProjectionPathFor,
14
+ isUuid,
15
+ listAppleMusicImportBatches,
16
+ openAppleMusicSqliteProjection,
17
+ } from "../importers/apple-music-library/index.mjs";
18
+
19
+ export { defaultAppleMusicProjectionPath };
20
+
21
+ export function resolveAppleMusicProjectionPath(environment = process.env) {
22
+ const configured = environment.MOONDOG_APPLE_PROJECTION_PATH?.trim();
23
+ if (!configured) return defaultAppleMusicProjectionPathFor(environment);
24
+ if (!path.isAbsolute(configured)) {
25
+ throw new TypeError("MOONDOG_APPLE_PROJECTION_PATH must be an absolute path");
26
+ }
27
+ return path.resolve(configured);
28
+ }
29
+
30
+ export const APPLE_DOMAIN_LIMITS = Object.freeze({
31
+ searchDefault: 8,
32
+ searchMax: 12,
33
+ searchOffsetMax: 10_000,
34
+ profileDefault: 6,
35
+ profileMax: 10,
36
+ rediscoveryDefault: 6,
37
+ rediscoveryMax: 12,
38
+ historicalReturnDefault: 6,
39
+ historicalReturnMax: 12,
40
+ backToBackDefault: 6,
41
+ backToBackMax: 12,
42
+ timeCapsuleDefault: 6,
43
+ timeCapsuleMax: 12,
44
+ candidateSetsMax: 8,
45
+ playlistTracksMax: 12,
46
+ queryLengthMax: 256,
47
+ shortTextMax: 256,
48
+ longTextMax: 500,
49
+ });
50
+
51
+ const familiarityValues = new Set(["low", "medium", "high", "unknown"]);
52
+ const preferenceSignalValues = new Set(["loved", "favorited", "rated"]);
53
+ const externalCatalogProviders = new Set(["apple_music", "listenbrainz"]);
54
+ const listenBrainzModes = new Set(["easy", "medium", "hard"]);
55
+ const rediscoverySignals = new Set([
56
+ "explicit listener preference",
57
+ "saved-library state",
58
+ "private playlist curation",
59
+ "historical attention only",
60
+ ]);
61
+ const backToBackSignals = new Set(["adjacent retained plays"]);
62
+ const spotifyTrackIdPattern = /^[A-Za-z0-9]{22}$/u;
63
+ const englishTrackCounts = new Map([
64
+ ["one", 1],
65
+ ["two", 2],
66
+ ["three", 3],
67
+ ["four", 4],
68
+ ["five", 5],
69
+ ["six", 6],
70
+ ["seven", 7],
71
+ ["eight", 8],
72
+ ["nine", 9],
73
+ ["ten", 10],
74
+ ["eleven", 11],
75
+ ["twelve", 12],
76
+ ]);
77
+ const chineseTrackCounts = new Map([
78
+ ["一", 1],
79
+ ["二", 2],
80
+ ["两", 2],
81
+ ["三", 3],
82
+ ["四", 4],
83
+ ["五", 5],
84
+ ["六", 6],
85
+ ["七", 7],
86
+ ["八", 8],
87
+ ["九", 9],
88
+ ["十", 10],
89
+ ]);
90
+
91
+ export class AppleProjectionDomainError extends Error {
92
+ constructor(code, message, options) {
93
+ super(message, options);
94
+ this.name = "AppleProjectionDomainError";
95
+ this.code = code;
96
+ }
97
+ }
98
+
99
+ function fail(code, message, options) {
100
+ throw new AppleProjectionDomainError(code, message, options);
101
+ }
102
+
103
+ function isPlainObject(value) {
104
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
105
+ const prototype = Object.getPrototypeOf(value);
106
+ return prototype === Object.prototype || prototype === null;
107
+ }
108
+
109
+ function modelArguments(value, allowedKeys) {
110
+ const input = value ?? {};
111
+ if (
112
+ !isPlainObject(input) ||
113
+ Object.keys(input).some((key) => !allowedKeys.has(key))
114
+ ) {
115
+ fail("invalid_arguments", "Domain service arguments are invalid.");
116
+ }
117
+ return input;
118
+ }
119
+
120
+ function cleanText(value, maximum, code) {
121
+ if (typeof value !== "string" || Array.from(value).length > maximum) {
122
+ fail(code, "Domain service text is invalid.");
123
+ }
124
+ const cleaned = value
125
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
126
+ .replace(/[\u202a-\u202e\u2066-\u2069]/gu, "")
127
+ .replace(/\s+/gu, " ")
128
+ .trim();
129
+ if (!cleaned) fail(code, "Domain service text is invalid.");
130
+ return cleaned;
131
+ }
132
+
133
+ function cleanQuery(value) {
134
+ if (value === undefined) return "";
135
+ if (
136
+ typeof value !== "string" ||
137
+ Array.from(value).length > APPLE_DOMAIN_LIMITS.queryLengthMax
138
+ ) {
139
+ fail("invalid_query", "Library search query is invalid.");
140
+ }
141
+ return value
142
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
143
+ .replace(/[\u202a-\u202e\u2066-\u2069]/gu, "")
144
+ .replace(/\s+/gu, " ")
145
+ .trim();
146
+ }
147
+
148
+ function boundedLimit(value, fallback, maximum, code) {
149
+ if (value === undefined) return fallback;
150
+ if (!Number.isInteger(value) || value < 1 || value > maximum) {
151
+ fail(code, "Domain service limit is invalid.");
152
+ }
153
+ return value;
154
+ }
155
+
156
+ function boundedOffset(value, maximum, code) {
157
+ const offset = value ?? 0;
158
+ if (!Number.isInteger(offset) || offset < 0 || offset > maximum) {
159
+ fail(code, "Domain service offset is invalid.");
160
+ }
161
+ return offset;
162
+ }
163
+
164
+ function normalizeText(value) {
165
+ return value.normalize("NFKC").toLocaleLowerCase("und");
166
+ }
167
+
168
+ function normalizeFilterArray(value, allowedValues, code) {
169
+ if (value === undefined) return [];
170
+ if (!Array.isArray(value) || value.length > 4) {
171
+ fail(code, "Library search filters are invalid.");
172
+ }
173
+ const normalized = value.map((item) =>
174
+ normalizeText(cleanText(item, APPLE_DOMAIN_LIMITS.shortTextMax, code)),
175
+ );
176
+ if (allowedValues && normalized.some((item) => !allowedValues.has(item))) {
177
+ fail(code, "Library search filters are invalid.");
178
+ }
179
+ return [...new Set(normalized)];
180
+ }
181
+
182
+ function normalizeFilters(value) {
183
+ if (value === undefined) {
184
+ return { artists: [], genres: [], familiarity: [], preferenceSignals: [] };
185
+ }
186
+ if (!isPlainObject(value)) fail("invalid_filters", "Library search filters are invalid.");
187
+ const allowed = new Set([
188
+ "artists",
189
+ "genres",
190
+ "familiarity",
191
+ "preferenceSignals",
192
+ ]);
193
+ if (Object.keys(value).some((key) => !allowed.has(key))) {
194
+ fail("invalid_filters", "Library search filters are invalid.");
195
+ }
196
+ return {
197
+ artists: normalizeFilterArray(value.artists, null, "invalid_filters"),
198
+ genres: normalizeFilterArray(value.genres, null, "invalid_filters"),
199
+ familiarity: normalizeFilterArray(
200
+ value.familiarity,
201
+ familiarityValues,
202
+ "invalid_filters",
203
+ ),
204
+ preferenceSignals: normalizeFilterArray(
205
+ value.preferenceSignals,
206
+ preferenceSignalValues,
207
+ "invalid_filters",
208
+ ),
209
+ };
210
+ }
211
+
212
+ function preferenceSignals(observation = {}) {
213
+ const result = [];
214
+ if (observation.loved === true) result.push("loved");
215
+ if (observation.favorited === true) result.push("favorited");
216
+ if (
217
+ isPlainObject(observation.rating) &&
218
+ observation.rating.computed !== true &&
219
+ Number.isInteger(observation.rating.value) &&
220
+ observation.rating.value >= 60
221
+ ) {
222
+ result.push("rated");
223
+ }
224
+ return result;
225
+ }
226
+
227
+ function familiarityLevel(playCount) {
228
+ if (!Number.isInteger(playCount) || playCount < 0) return "unknown";
229
+ if (playCount >= 20) return "high";
230
+ if (playCount >= 5) return "medium";
231
+ return "low";
232
+ }
233
+
234
+ function safeTrack(raw) {
235
+ if (!isPlainObject(raw) || !isPlainObject(raw.track_ref)) {
236
+ fail("projection_result_invalid", "Projection returned an invalid track.");
237
+ }
238
+ const observed = isPlainObject(raw.observation_summary)
239
+ ? raw.observation_summary
240
+ : {};
241
+ const playCount = Number.isInteger(observed.play_count)
242
+ ? observed.play_count
243
+ : undefined;
244
+ const genres =
245
+ typeof raw.labels?.provider_genre === "string" &&
246
+ raw.labels.provider_genre.trim()
247
+ ? [raw.labels.provider_genre.trim()]
248
+ : [];
249
+ const labels = { genres };
250
+ if (typeof raw.labels?.composer === "string" && raw.labels.composer.trim()) {
251
+ labels.composer = raw.labels.composer.trim();
252
+ }
253
+ const familiarity = {
254
+ level: familiarityLevel(playCount),
255
+ basis: playCount === undefined ? "not_observed" : "aggregate_play_count",
256
+ };
257
+ if (playCount !== undefined) familiarity.play_count = playCount;
258
+ const track = {
259
+ track_ref_id: cleanText(
260
+ raw.track_ref.track_ref_id,
261
+ 128,
262
+ "projection_result_invalid",
263
+ ),
264
+ title: cleanText(raw.title, 512, "projection_result_invalid"),
265
+ artist_credit:
266
+ typeof raw.artist_credit === "string" && raw.artist_credit.trim()
267
+ ? raw.artist_credit.trim()
268
+ : "Unknown artist",
269
+ release:
270
+ typeof raw.release === "string" && raw.release.trim()
271
+ ? raw.release.trim()
272
+ : "Unknown release",
273
+ labels,
274
+ observation_summary: {
275
+ preference_signals: preferenceSignals(observed),
276
+ familiarity,
277
+ },
278
+ };
279
+ if (Number.isInteger(raw.duration_ms) && raw.duration_ms >= 0) {
280
+ track.duration_ms = raw.duration_ms;
281
+ }
282
+ return track;
283
+ }
284
+
285
+ function safeExternalCatalogUrl(value, provider) {
286
+ if (typeof value !== "string") return undefined;
287
+ try {
288
+ const url = new URL(value);
289
+ const allowedHosts =
290
+ provider === "apple_music"
291
+ ? new Set(["music.apple.com", "itunes.apple.com"])
292
+ : new Set(["musicbrainz.org", "listenbrainz.org"]);
293
+ if (
294
+ url.protocol !== "https:" ||
295
+ !allowedHosts.has(url.hostname)
296
+ ) {
297
+ return undefined;
298
+ }
299
+ return url.toString();
300
+ } catch {
301
+ return undefined;
302
+ }
303
+ }
304
+
305
+ export function safeExternalTrack(raw) {
306
+ const provider =
307
+ isPlainObject(raw) && externalCatalogProviders.has(raw.catalog_provider)
308
+ ? raw.catalog_provider
309
+ : null;
310
+ if (
311
+ !isPlainObject(raw) ||
312
+ !isUuid(raw.track_ref_id) ||
313
+ raw.candidate_scope !== "external_catalog" ||
314
+ !provider
315
+ ) {
316
+ fail("external_candidate_invalid", "An external catalog candidate is invalid.");
317
+ }
318
+ const track = {
319
+ track_ref_id: raw.track_ref_id.toLowerCase(),
320
+ title: cleanText(raw.title, 512, "external_candidate_invalid"),
321
+ artist_credit: cleanText(
322
+ raw.artist_credit,
323
+ 512,
324
+ "external_candidate_invalid",
325
+ ),
326
+ release: cleanText(raw.release, 512, "external_candidate_invalid"),
327
+ candidate_scope: "external_catalog",
328
+ catalog_provider: provider,
329
+ labels: { genres: [] },
330
+ observation_summary: {
331
+ preference_signals: [],
332
+ familiarity: {
333
+ level: "unknown",
334
+ basis: "external_catalog_not_personal_evidence",
335
+ },
336
+ },
337
+ };
338
+ if (
339
+ Number.isInteger(raw.duration_ms) &&
340
+ raw.duration_ms >= 0 &&
341
+ raw.duration_ms <= 86_400_000
342
+ ) {
343
+ track.duration_ms = raw.duration_ms;
344
+ }
345
+ if (typeof raw.primary_genre === "string" && raw.primary_genre.trim()) {
346
+ track.primary_genre = cleanText(
347
+ raw.primary_genre,
348
+ 128,
349
+ "external_candidate_invalid",
350
+ );
351
+ track.labels.genres = [track.primary_genre];
352
+ }
353
+ if (
354
+ typeof raw.release_date === "string" &&
355
+ /^\d{4}-\d{2}-\d{2}$/u.test(raw.release_date) &&
356
+ Number.isFinite(Date.parse(`${raw.release_date}T00:00:00.000Z`))
357
+ ) {
358
+ track.release_date = raw.release_date;
359
+ }
360
+ const catalogUrl = safeExternalCatalogUrl(raw.catalog_url, provider);
361
+ if (catalogUrl) track.catalog_url = catalogUrl;
362
+ if (provider === "apple_music" && Array.isArray(raw.matched_queries)) {
363
+ track.matched_queries = raw.matched_queries
364
+ .slice(0, 3)
365
+ .map((query) =>
366
+ cleanText(query, APPLE_DOMAIN_LIMITS.queryLengthMax, "external_candidate_invalid"),
367
+ );
368
+ }
369
+ if (provider === "listenbrainz") {
370
+ const basis = raw.discovery_basis;
371
+ if (
372
+ !isPlainObject(basis) ||
373
+ basis.kind !== "listenbrainz_collaborative_artist_similarity" ||
374
+ !listenBrainzModes.has(basis.mode)
375
+ ) {
376
+ fail("external_candidate_invalid", "An external catalog candidate is invalid.");
377
+ }
378
+ track.discovery_basis = {
379
+ kind: "listenbrainz_collaborative_artist_similarity",
380
+ seed_artist: cleanText(
381
+ basis.seed_artist,
382
+ 512,
383
+ "external_candidate_invalid",
384
+ ),
385
+ adjacent_artist: cleanText(
386
+ basis.adjacent_artist,
387
+ 512,
388
+ "external_candidate_invalid",
389
+ ),
390
+ mode: basis.mode,
391
+ };
392
+ }
393
+ return track;
394
+ }
395
+
396
+ function safeHistoryTimestamp(value) {
397
+ const cleaned = cleanText(value, 64, "history_candidate_invalid");
398
+ if (!Number.isFinite(Date.parse(cleaned))) {
399
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
400
+ }
401
+ return cleaned;
402
+ }
403
+
404
+ function safeHistoryCount(value) {
405
+ if (!Number.isSafeInteger(value) || value < 0) {
406
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
407
+ }
408
+ return value;
409
+ }
410
+
411
+ function safeHistoryExternalRefs(value) {
412
+ if (value === undefined) return [];
413
+ if (!Array.isArray(value)) {
414
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
415
+ }
416
+ const spotifyRefs = [];
417
+ for (const raw of value) {
418
+ if (!isPlainObject(raw) || raw.system !== "spotify") continue;
419
+ if (
420
+ raw.entity_type !== "spotify.track" ||
421
+ typeof raw.external_id !== "string" ||
422
+ !spotifyTrackIdPattern.test(raw.external_id)
423
+ ) {
424
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
425
+ }
426
+ if (!spotifyRefs.some((ref) => ref.external_id === raw.external_id)) {
427
+ spotifyRefs.push({
428
+ system: "spotify",
429
+ entity_type: "spotify.track",
430
+ external_id: raw.external_id,
431
+ });
432
+ }
433
+ }
434
+ if (spotifyRefs.length > 1) {
435
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
436
+ }
437
+ return spotifyRefs;
438
+ }
439
+
440
+ function safeHistoryTrack(raw) {
441
+ const hasRediscovery = isPlainObject(raw?.rediscovery);
442
+ const hasHistoricalReturn = isPlainObject(raw?.historical_return);
443
+ const hasTimeCapsule = isPlainObject(raw?.time_capsule);
444
+ const hasBackToBack = isPlainObject(raw?.back_to_back);
445
+ if (
446
+ !isPlainObject(raw) ||
447
+ raw.candidate_scope !== "private_history" ||
448
+ !isUuid(raw.track_ref_id) ||
449
+ [
450
+ hasRediscovery,
451
+ hasHistoricalReturn,
452
+ hasTimeCapsule,
453
+ hasBackToBack,
454
+ ].filter(Boolean).length !== 1
455
+ ) {
456
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
457
+ }
458
+ const identityStatus = raw.identity_status;
459
+ if (!new Set(["resolved", "provisional"]).has(identityStatus)) {
460
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
461
+ }
462
+ const playCount = safeHistoryCount(
463
+ raw.observation_summary?.familiarity?.play_count,
464
+ );
465
+ const externalRefs = safeHistoryExternalRefs(raw.external_refs);
466
+ const track = {
467
+ track_ref_id: raw.track_ref_id.toLowerCase(),
468
+ title: cleanText(raw.title, 512, "history_candidate_invalid"),
469
+ artist_credit: cleanText(
470
+ raw.artist_credit,
471
+ 512,
472
+ "history_candidate_invalid",
473
+ ),
474
+ release: cleanText(raw.release, 512, "history_candidate_invalid"),
475
+ candidate_scope: "private_history",
476
+ identity_status: identityStatus,
477
+ labels: { genres: [] },
478
+ observation_summary: {
479
+ preference_signals: [],
480
+ familiarity: {
481
+ level: familiarityLevel(playCount),
482
+ basis: "effective_listening_history",
483
+ play_count: playCount,
484
+ },
485
+ },
486
+ };
487
+ if (hasRediscovery) {
488
+ const rediscovery = raw.rediscovery;
489
+ const engagedPlayCount = safeHistoryCount(rediscovery.engaged_play_count);
490
+ const explicitSkips = safeHistoryCount(rediscovery.explicit_skips);
491
+ const listeningMinutes = safeHistoryCount(rediscovery.listening_minutes);
492
+ const quietDays = safeHistoryCount(rediscovery.quiet_days);
493
+ if (engagedPlayCount > playCount || explicitSkips > playCount) {
494
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
495
+ }
496
+ const rediscoverySignal = cleanText(
497
+ rediscovery.rediscovery_signal,
498
+ 128,
499
+ "history_candidate_invalid",
500
+ );
501
+ if (!rediscoverySignals.has(rediscoverySignal)) {
502
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
503
+ }
504
+ const firstPlayedAt = safeHistoryTimestamp(rediscovery.first_played_at);
505
+ const lastPlayedAt = safeHistoryTimestamp(rediscovery.last_played_at);
506
+ if (Date.parse(firstPlayedAt) > Date.parse(lastPlayedAt)) {
507
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
508
+ }
509
+ if (!isUuid(rediscovery.evidence_id)) {
510
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
511
+ }
512
+ const peakFields = [
513
+ rediscovery.peak_year,
514
+ rediscovery.peak_year_play_count,
515
+ rediscovery.peak_year_listening_minutes,
516
+ ];
517
+ if (peakFields.some((value) => value !== undefined)) {
518
+ if (
519
+ !peakFields.every((value) => Number.isSafeInteger(value) && value >= 0) ||
520
+ rediscovery.peak_year < 1900 ||
521
+ rediscovery.peak_year > 9999
522
+ ) {
523
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
524
+ }
525
+ }
526
+ track.rediscovery = {
527
+ listening_minutes: listeningMinutes,
528
+ engaged_play_count: engagedPlayCount,
529
+ explicit_skips: explicitSkips,
530
+ first_played_at: firstPlayedAt,
531
+ last_played_at: lastPlayedAt,
532
+ quiet_days: quietDays,
533
+ rediscovery_signal: rediscoverySignal,
534
+ ...(peakFields.every((value) => value !== undefined)
535
+ ? {
536
+ peak_year: rediscovery.peak_year,
537
+ peak_year_play_count: rediscovery.peak_year_play_count,
538
+ peak_year_listening_minutes:
539
+ rediscovery.peak_year_listening_minutes,
540
+ }
541
+ : {}),
542
+ evidence_id: rediscovery.evidence_id.toLowerCase(),
543
+ };
544
+ } else if (hasHistoricalReturn) {
545
+ const historicalReturn = raw.historical_return;
546
+ const engagedPlayCount = safeHistoryCount(
547
+ historicalReturn.engaged_play_count,
548
+ );
549
+ const explicitSkips = safeHistoryCount(historicalReturn.explicit_skips);
550
+ const listeningMinutes = safeHistoryCount(
551
+ historicalReturn.listening_minutes,
552
+ );
553
+ const returnCount = safeHistoryCount(historicalReturn.return_count);
554
+ const longestGapDays = safeHistoryCount(
555
+ historicalReturn.longest_gap_days,
556
+ );
557
+ const latestReturnGapDays = safeHistoryCount(
558
+ historicalReturn.latest_return_gap_days,
559
+ );
560
+ const historicalReturnSignal = cleanText(
561
+ historicalReturn.historical_return_signal,
562
+ 128,
563
+ "history_candidate_invalid",
564
+ );
565
+ const firstPlayedAt = safeHistoryTimestamp(
566
+ historicalReturn.first_played_at,
567
+ );
568
+ const lastPlayedAt = safeHistoryTimestamp(
569
+ historicalReturn.last_played_at,
570
+ );
571
+ const latestReturnAt = safeHistoryTimestamp(
572
+ historicalReturn.latest_return_at,
573
+ );
574
+ if (
575
+ returnCount < 1 ||
576
+ longestGapDays < 1 ||
577
+ latestReturnGapDays < 1 ||
578
+ latestReturnGapDays > longestGapDays ||
579
+ engagedPlayCount > playCount ||
580
+ explicitSkips > playCount ||
581
+ Date.parse(firstPlayedAt) > Date.parse(latestReturnAt) ||
582
+ Date.parse(latestReturnAt) > Date.parse(lastPlayedAt) ||
583
+ !rediscoverySignals.has(historicalReturnSignal) ||
584
+ !isUuid(historicalReturn.evidence_id)
585
+ ) {
586
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
587
+ }
588
+ track.historical_return = {
589
+ listening_minutes: listeningMinutes,
590
+ engaged_play_count: engagedPlayCount,
591
+ explicit_skips: explicitSkips,
592
+ first_played_at: firstPlayedAt,
593
+ last_played_at: lastPlayedAt,
594
+ return_count: returnCount,
595
+ longest_gap_days: longestGapDays,
596
+ latest_return_at: latestReturnAt,
597
+ latest_return_gap_days: latestReturnGapDays,
598
+ historical_return_signal: historicalReturnSignal,
599
+ evidence_id: historicalReturn.evidence_id.toLowerCase(),
600
+ };
601
+ } else if (hasTimeCapsule) {
602
+ const capsule = raw.time_capsule;
603
+ const year = safeHistoryCount(capsule.year);
604
+ const yearPlayCount = safeHistoryCount(capsule.year_play_count);
605
+ const yearEngagedPlayCount = safeHistoryCount(
606
+ capsule.year_engaged_play_count,
607
+ );
608
+ const yearListeningMinutes = safeHistoryCount(
609
+ capsule.year_listening_minutes,
610
+ );
611
+ const yearExplicitSkips = safeHistoryCount(capsule.year_explicit_skips);
612
+ const lifetimeListeningMinutes = safeHistoryCount(
613
+ capsule.lifetime_listening_minutes,
614
+ );
615
+ const representativeSignal = cleanText(
616
+ capsule.representative_signal,
617
+ 128,
618
+ "history_candidate_invalid",
619
+ );
620
+ if (
621
+ year < 1900 ||
622
+ year > 9999 ||
623
+ yearPlayCount > playCount ||
624
+ yearEngagedPlayCount > yearPlayCount ||
625
+ yearExplicitSkips > yearPlayCount ||
626
+ yearListeningMinutes > lifetimeListeningMinutes ||
627
+ !rediscoverySignals.has(representativeSignal) ||
628
+ !isUuid(capsule.evidence_id)
629
+ ) {
630
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
631
+ }
632
+ track.time_capsule = {
633
+ year,
634
+ year_play_count: yearPlayCount,
635
+ year_engaged_play_count: yearEngagedPlayCount,
636
+ year_listening_minutes: yearListeningMinutes,
637
+ year_explicit_skips: yearExplicitSkips,
638
+ lifetime_listening_minutes: lifetimeListeningMinutes,
639
+ representative_signal: representativeSignal,
640
+ evidence_id: capsule.evidence_id.toLowerCase(),
641
+ };
642
+ } else {
643
+ const context = raw.back_to_back;
644
+ const engagedPlayCount = safeHistoryCount(context.engaged_play_count);
645
+ const explicitSkips = safeHistoryCount(context.explicit_skips);
646
+ const burstCount = safeHistoryCount(context.burst_count);
647
+ const maximumConsecutivePlays = safeHistoryCount(
648
+ context.maximum_consecutive_plays,
649
+ );
650
+ const playsInBursts = safeHistoryCount(context.plays_in_bursts);
651
+ const listeningMinutesInBursts = safeHistoryCount(
652
+ context.listening_minutes_in_bursts,
653
+ );
654
+ const latestBurstAt = safeHistoryTimestamp(context.latest_burst_at);
655
+ const sequenceSignal = cleanText(
656
+ context.sequence_signal,
657
+ 128,
658
+ "history_candidate_invalid",
659
+ );
660
+ if (
661
+ burstCount < 1 ||
662
+ maximumConsecutivePlays < 2 ||
663
+ playsInBursts < maximumConsecutivePlays ||
664
+ playsInBursts < burstCount * 2 ||
665
+ playsInBursts > playCount ||
666
+ engagedPlayCount > playCount ||
667
+ explicitSkips > playCount ||
668
+ !backToBackSignals.has(sequenceSignal) ||
669
+ !isUuid(context.evidence_id)
670
+ ) {
671
+ fail("history_candidate_invalid", "A listening-history candidate is invalid.");
672
+ }
673
+ track.back_to_back = {
674
+ engaged_play_count: engagedPlayCount,
675
+ explicit_skips: explicitSkips,
676
+ burst_count: burstCount,
677
+ maximum_consecutive_plays: maximumConsecutivePlays,
678
+ plays_in_bursts: playsInBursts,
679
+ listening_minutes_in_bursts: listeningMinutesInBursts,
680
+ latest_burst_at: latestBurstAt,
681
+ sequence_signal: sequenceSignal,
682
+ evidence_id: context.evidence_id.toLowerCase(),
683
+ };
684
+ }
685
+ if (
686
+ Number.isInteger(raw.duration_ms) &&
687
+ raw.duration_ms >= 0 &&
688
+ raw.duration_ms <= 86_400_000
689
+ ) {
690
+ track.duration_ms = raw.duration_ms;
691
+ }
692
+ if (externalRefs.length > 0) track.external_refs = externalRefs;
693
+ return track;
694
+ }
695
+
696
+ function publicHistoryTrack(track) {
697
+ const projected = structuredClone(track);
698
+ delete projected.external_refs;
699
+ return projected;
700
+ }
701
+
702
+ function safeRetainedPlaylistTrack(raw) {
703
+ if (raw?.candidate_scope === "private_history") {
704
+ return safeHistoryTrack(raw);
705
+ }
706
+ if (raw?.candidate_scope === "external_catalog") {
707
+ return {
708
+ ...safeExternalTrack(raw),
709
+ knownness: {
710
+ imported_library: "not_found_by_exact_title_artist",
711
+ listening_history: "not_checked",
712
+ },
713
+ };
714
+ }
715
+ if (
716
+ !isPlainObject(raw) ||
717
+ (raw.candidate_scope !== undefined &&
718
+ raw.candidate_scope !== "private_library") ||
719
+ !isUuid(raw.track_ref_id)
720
+ ) {
721
+ fail(
722
+ "retained_candidate_invalid",
723
+ "Retained playlist candidates are invalid.",
724
+ );
725
+ }
726
+ const playCount = Number.isInteger(
727
+ raw.observation_summary?.familiarity?.play_count,
728
+ )
729
+ ? raw.observation_summary.familiarity.play_count
730
+ : undefined;
731
+ const level = familiarityValues.has(
732
+ raw.observation_summary?.familiarity?.level,
733
+ )
734
+ ? raw.observation_summary.familiarity.level
735
+ : "unknown";
736
+ const genres = Array.isArray(raw.labels?.genres)
737
+ ? raw.labels.genres
738
+ .slice(0, 4)
739
+ .map((genre) =>
740
+ cleanText(genre, 128, "retained_candidate_invalid"),
741
+ )
742
+ : [];
743
+ const labels = { genres };
744
+ if (typeof raw.labels?.composer === "string" && raw.labels.composer.trim()) {
745
+ labels.composer = cleanText(
746
+ raw.labels.composer,
747
+ APPLE_DOMAIN_LIMITS.shortTextMax,
748
+ "retained_candidate_invalid",
749
+ );
750
+ }
751
+ const track = {
752
+ track_ref_id: raw.track_ref_id.toLowerCase(),
753
+ title: cleanText(raw.title, 512, "retained_candidate_invalid"),
754
+ artist_credit: cleanText(
755
+ raw.artist_credit,
756
+ 512,
757
+ "retained_candidate_invalid",
758
+ ),
759
+ release: cleanText(raw.release, 512, "retained_candidate_invalid"),
760
+ candidate_scope: "private_library",
761
+ labels,
762
+ observation_summary: {
763
+ preference_signals: Array.isArray(
764
+ raw.observation_summary?.preference_signals,
765
+ )
766
+ ? [
767
+ ...new Set(
768
+ raw.observation_summary.preference_signals.filter((signal) =>
769
+ preferenceSignalValues.has(signal),
770
+ ),
771
+ ),
772
+ ]
773
+ : [],
774
+ familiarity: {
775
+ level,
776
+ basis:
777
+ playCount === undefined
778
+ ? "not_observed"
779
+ : "aggregate_play_count",
780
+ ...(playCount === undefined ? {} : { play_count: playCount }),
781
+ },
782
+ },
783
+ };
784
+ if (
785
+ Number.isInteger(raw.duration_ms) &&
786
+ raw.duration_ms >= 0 &&
787
+ raw.duration_ms <= 86_400_000
788
+ ) {
789
+ track.duration_ms = raw.duration_ms;
790
+ }
791
+ return track;
792
+ }
793
+
794
+ function normalizedArtistNames(value) {
795
+ return normalizeText(value)
796
+ .split(/\s*(?:,|&|;|\/|\bfeat\.?\b|\bfeaturing\b)\s*/u)
797
+ .filter(Boolean);
798
+ }
799
+
800
+ export function exactTitleArtistMatch(left, right) {
801
+ const leftTitle = normalizeText(left.title);
802
+ const rightTitle = normalizeText(right.title);
803
+ const leftArtists = normalizedArtistNames(left.artist_credit);
804
+ const rightArtists = new Set(normalizedArtistNames(right.artist_credit));
805
+ return (
806
+ leftTitle === rightTitle &&
807
+ leftArtists.some((artist) => rightArtists.has(artist))
808
+ );
809
+ }
810
+
811
+ export function assertExternalPlanDiversity(tracks, intent) {
812
+ const externalTracks = tracks.filter(
813
+ (track) => track.candidate_scope === "external_catalog",
814
+ );
815
+ if (externalTracks.length < 2) return;
816
+ const normalizedIntent = normalizeText(intent);
817
+ const releases = new Map();
818
+ const artists = new Map();
819
+ for (const track of externalTracks) {
820
+ const artistKey = normalizeText(track.artist_credit);
821
+ const releaseKey = `${artistKey}|${normalizeText(track.release)}`;
822
+ const releaseGroup = releases.get(releaseKey) ?? {
823
+ release: normalizeText(track.release),
824
+ count: 0,
825
+ };
826
+ releaseGroup.count += 1;
827
+ releases.set(releaseKey, releaseGroup);
828
+ const artistGroup = artists.get(artistKey) ?? {
829
+ names: normalizedArtistNames(track.artist_credit),
830
+ count: 0,
831
+ };
832
+ artistGroup.count += 1;
833
+ artists.set(artistKey, artistGroup);
834
+ }
835
+ for (const group of releases.values()) {
836
+ if (
837
+ group.count > 1 &&
838
+ !normalizedIntent.includes(group.release)
839
+ ) {
840
+ fail(
841
+ "playlist_external_release_concentration",
842
+ "External discovery plans may select only one track per release unless the intent names that release.",
843
+ );
844
+ }
845
+ }
846
+ for (const group of artists.values()) {
847
+ const artistRequested = group.names.some(
848
+ (name) =>
849
+ Array.from(name).length >= 2 && normalizedIntent.includes(name),
850
+ );
851
+ if (group.count > 2 && !artistRequested) {
852
+ fail(
853
+ "playlist_external_artist_concentration",
854
+ "External discovery plans may select at most two tracks per artist unless the intent names that artist.",
855
+ );
856
+ }
857
+ }
858
+ }
859
+
860
+ export function safeExternalSource(raw) {
861
+ if (!isPlainObject(raw) || !externalCatalogProviders.has(raw.provider)) {
862
+ fail("external_candidate_invalid", "External catalog source metadata is invalid.");
863
+ }
864
+ if (
865
+ typeof raw.retrieved_at !== "string" ||
866
+ !Number.isFinite(Date.parse(raw.retrieved_at))
867
+ ) {
868
+ fail("external_candidate_invalid", "External catalog source metadata is invalid.");
869
+ }
870
+ if (raw.provider === "apple_music") {
871
+ if (raw.catalog !== "itunes_search_api" || raw.storefront !== "US") {
872
+ fail("external_candidate_invalid", "External catalog source metadata is invalid.");
873
+ }
874
+ return {
875
+ provider: "apple_music",
876
+ catalog: "itunes_search_api",
877
+ storefront: "US",
878
+ retrieved_at: raw.retrieved_at,
879
+ coverage: cleanText(raw.coverage, 500, "external_candidate_invalid"),
880
+ };
881
+ }
882
+ if (
883
+ raw.catalog !== "lb_radio_artist+metadata_recording" ||
884
+ raw.identity_provider !== "wikidata" ||
885
+ raw.recommendation_basis !== "listenbrainz_collaborative_artist_similarity" ||
886
+ !listenBrainzModes.has(raw.mode) ||
887
+ !isPlainObject(raw.popularity_range) ||
888
+ !Number.isInteger(raw.popularity_range.begin) ||
889
+ !Number.isInteger(raw.popularity_range.end) ||
890
+ raw.popularity_range.begin < 0 ||
891
+ raw.popularity_range.end > 100 ||
892
+ raw.popularity_range.begin > raw.popularity_range.end
893
+ ) {
894
+ fail("external_candidate_invalid", "External catalog source metadata is invalid.");
895
+ }
896
+ return {
897
+ provider: "listenbrainz",
898
+ catalog: "lb_radio_artist+metadata_recording",
899
+ identity_provider: "wikidata",
900
+ retrieved_at: raw.retrieved_at,
901
+ license: cleanText(raw.license, 500, "external_candidate_invalid"),
902
+ recommendation_basis: "listenbrainz_collaborative_artist_similarity",
903
+ mode: raw.mode,
904
+ popularity_range: {
905
+ begin: raw.popularity_range.begin,
906
+ end: raw.popularity_range.end,
907
+ },
908
+ seed_artist: cleanText(
909
+ raw.seed_artist,
910
+ 512,
911
+ "external_candidate_invalid",
912
+ ),
913
+ coverage: cleanText(raw.coverage, 500, "external_candidate_invalid"),
914
+ };
915
+ }
916
+
917
+ function matchesFilters(track, filters) {
918
+ const artist = normalizeText(track.artist_credit);
919
+ const genres = track.labels.genres.map(normalizeText);
920
+ if (
921
+ filters.artists.length > 0 &&
922
+ !filters.artists.some((candidate) => artist.includes(candidate))
923
+ ) {
924
+ return false;
925
+ }
926
+ if (
927
+ filters.genres.length > 0 &&
928
+ !filters.genres.some((candidate) => genres.includes(candidate))
929
+ ) {
930
+ return false;
931
+ }
932
+ if (
933
+ filters.familiarity.length > 0 &&
934
+ !filters.familiarity.includes(track.observation_summary.familiarity.level)
935
+ ) {
936
+ return false;
937
+ }
938
+ if (
939
+ filters.preferenceSignals.length > 0 &&
940
+ !filters.preferenceSignals.every((candidate) =>
941
+ track.observation_summary.preference_signals.includes(candidate),
942
+ )
943
+ ) {
944
+ return false;
945
+ }
946
+ return true;
947
+ }
948
+
949
+ function rawSearchFilters(filters) {
950
+ return {
951
+ artists: filters.artists,
952
+ genres: filters.genres,
953
+ familiarity: filters.familiarity,
954
+ preferenceSignals: filters.preferenceSignals,
955
+ };
956
+ }
957
+
958
+ function preferenceSignalLabel(observation = {}) {
959
+ const labels = [];
960
+ if (observation.loved === true) labels.push("Loved");
961
+ if (observation.favorited === true) labels.push("Favorited");
962
+ if (
963
+ isPlainObject(observation.rating) &&
964
+ observation.rating.computed !== true &&
965
+ Number.isInteger(observation.rating.value) &&
966
+ observation.rating.value >= 60
967
+ ) {
968
+ labels.push(`non-computed rating ${observation.rating.value}/100`);
969
+ }
970
+ return labels.join(", ") || "explicit provider preference signal";
971
+ }
972
+
973
+ function listenerAssertionAvoids(
974
+ listening,
975
+ entityType,
976
+ label,
977
+ artistCredit = "",
978
+ ) {
979
+ const avoids = listening?.listener_assertions?.avoids ?? [];
980
+ const normalizedLabel = normalizeText(label ?? "");
981
+ const normalizedArtist = normalizeText(artistCredit ?? "");
982
+ return avoids.some((avoid) => {
983
+ if (avoid.entity_type === "artist") {
984
+ const avoidedArtist = normalizeText(avoid.label ?? "");
985
+ return entityType === "artist"
986
+ ? avoidedArtist === normalizedLabel
987
+ : avoidedArtist === normalizedArtist;
988
+ }
989
+ return entityType === "track" &&
990
+ normalizeText(avoid.label ?? "") === normalizedLabel &&
991
+ normalizeText(avoid.artist_credit ?? "") === normalizedArtist;
992
+ });
993
+ }
994
+
995
+ function normalizeAppleCorrectionLabels(listening, projection) {
996
+ if (!listening || typeof projection.resolveTrackLabel !== "function") return listening;
997
+ const resolved = new Map();
998
+ function normalize(item) {
999
+ if (item?.entity_type !== "track" || !item.correction_id) return item;
1000
+ if (!resolved.has(item.correction_id)) {
1001
+ const match = projection.resolveTrackLabel({
1002
+ label: item.label,
1003
+ artistCredit: item.artist_credit,
1004
+ });
1005
+ resolved.set(item.correction_id, match?.match === "legacy_display_label" ? match.title : null);
1006
+ }
1007
+ const title = resolved.get(item.correction_id);
1008
+ return title === null ? item : { ...item, label: title };
1009
+ }
1010
+ return {
1011
+ ...listening,
1012
+ listener_assertions: {
1013
+ ...listening.listener_assertions,
1014
+ active: (listening.listener_assertions?.active ?? []).map(normalize),
1015
+ preferences: (listening.listener_assertions?.preferences ?? []).map(normalize),
1016
+ avoids: (listening.listener_assertions?.avoids ?? []).map(normalize),
1017
+ },
1018
+ curated_preferences: {
1019
+ ...listening.curated_preferences,
1020
+ avoids: (listening.curated_preferences?.avoids ?? []).map(normalize),
1021
+ },
1022
+ };
1023
+ }
1024
+
1025
+ function facetItems(preferences, field, maximum) {
1026
+ const facets = new Map();
1027
+ for (const item of preferences) {
1028
+ const name = field(item);
1029
+ if (typeof name !== "string" || !name.trim()) continue;
1030
+ const normalized = normalizeText(name);
1031
+ const current = facets.get(normalized) ?? {
1032
+ name: name.trim(),
1033
+ evidence_id: item.evidence_id,
1034
+ strength: 0,
1035
+ };
1036
+ current.strength += Number.isFinite(item.strength) ? item.strength : 0;
1037
+ facets.set(normalized, current);
1038
+ }
1039
+ return [...facets.values()]
1040
+ .sort((left, right) => {
1041
+ if (right.strength !== left.strength) return right.strength - left.strength;
1042
+ return left.name.localeCompare(right.name);
1043
+ })
1044
+ .slice(0, maximum)
1045
+ .map(({ name, evidence_id: evidenceId }) => ({
1046
+ name,
1047
+ evidence_id: evidenceId,
1048
+ }));
1049
+ }
1050
+
1051
+ function interleaveUnique(lists, maximum, field) {
1052
+ const result = [];
1053
+ const seen = new Set();
1054
+ for (let index = 0; result.length < maximum; index += 1) {
1055
+ let added = false;
1056
+ for (const list of lists) {
1057
+ const item = list[index];
1058
+ if (!item) continue;
1059
+ added = true;
1060
+ const value = field(item);
1061
+ const key = typeof value === "string" ? normalizeText(value) : value;
1062
+ if (seen.has(key)) continue;
1063
+ seen.add(key);
1064
+ result.push(item);
1065
+ if (result.length >= maximum) break;
1066
+ }
1067
+ if (!added) break;
1068
+ }
1069
+ return result;
1070
+ }
1071
+
1072
+ function basisSummaryText(explanation) {
1073
+ const basis = explanation.basis_summary ?? {};
1074
+ const details = [];
1075
+ if (basis.loved === true) details.push("Loved is true");
1076
+ if (basis.favorited === true) details.push("Favorited is true");
1077
+ if (Number.isInteger(basis.play_count)) {
1078
+ details.push(`aggregate Play Count is ${basis.play_count}`);
1079
+ }
1080
+ if (isPlainObject(basis.rating)) {
1081
+ details.push(
1082
+ `rating is ${basis.rating.value}/100${
1083
+ basis.rating.computed === false ? " and is not computed" : ""
1084
+ }`,
1085
+ );
1086
+ }
1087
+ const observed =
1088
+ typeof basis.observed_at === "string"
1089
+ ? ` in the snapshot captured at ${basis.observed_at}`
1090
+ : "";
1091
+ return `${details.join(", ") || "A bounded library observation supports this claim"}${observed}.`;
1092
+ }
1093
+
1094
+ function claimValueLabel(claim) {
1095
+ const value = claim?.value;
1096
+ if (value?.value_type === "entity") {
1097
+ return value.entity_ref?.label ?? "Referenced track";
1098
+ }
1099
+ if (value?.value_type === "string") return value.string_value;
1100
+ if (value?.value_type === "number") return String(value.number_value);
1101
+ if (value?.value_type === "boolean") return String(value.boolean_value);
1102
+ return "Bounded profile value";
1103
+ }
1104
+
1105
+ function requestedTrackCount(intent) {
1106
+ const numeric = intent.match(
1107
+ /(?:^|[^0-9])(\d{1,2})\s*(?:首|tracks?|songs?)/iu,
1108
+ );
1109
+ if (numeric) return Number.parseInt(numeric[1], 10);
1110
+ const after = intent.match(/(?:tracks?|songs?)\s*[:=]?\s*(\d{1,2})\b/iu);
1111
+ if (after) return Number.parseInt(after[1], 10);
1112
+ const english = intent.match(
1113
+ /\b(one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s+(?:tracks?|songs?)\b/iu,
1114
+ );
1115
+ if (english) return englishTrackCounts.get(english[1].toLowerCase());
1116
+ const chinese = intent.match(/([一二两三四五六七八九十]+)\s*首/u);
1117
+ if (!chinese) return undefined;
1118
+ if (chineseTrackCounts.has(chinese[1])) {
1119
+ return chineseTrackCounts.get(chinese[1]);
1120
+ }
1121
+ const parts = chinese[1].split("十");
1122
+ if (parts.length !== 2) return undefined;
1123
+ const tens = parts[0] === "" ? 1 : chineseTrackCounts.get(parts[0]);
1124
+ const ones = parts[1] === "" ? 0 : chineseTrackCounts.get(parts[1]);
1125
+ return tens && ones !== undefined ? tens * 10 + ones : undefined;
1126
+ }
1127
+
1128
+ async function assertProjectionExists(databasePath) {
1129
+ let metadata;
1130
+ try {
1131
+ metadata = await lstat(databasePath);
1132
+ } catch (cause) {
1133
+ if (cause?.code === "ENOENT") {
1134
+ fail("projection_not_built", "The local projection has not been built.");
1135
+ }
1136
+ fail("projection_unavailable", "The local projection is unavailable.", {
1137
+ cause,
1138
+ });
1139
+ }
1140
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
1141
+ fail("projection_unavailable", "The local projection is unavailable.");
1142
+ }
1143
+ if (process.platform !== "win32" && (metadata.mode & 0o077) !== 0) {
1144
+ fail("projection_permissions_invalid", "The local projection is not private.");
1145
+ }
1146
+ }
1147
+
1148
+ async function trustedScopeFromImports(importsRoot, expectedSubjectId) {
1149
+ let batches;
1150
+ try {
1151
+ batches = await listAppleMusicImportBatches(importsRoot, {
1152
+ subjectId: expectedSubjectId,
1153
+ });
1154
+ } catch (cause) {
1155
+ fail("projection_source_invalid", "The canonical import source is invalid.", {
1156
+ cause,
1157
+ });
1158
+ }
1159
+ const subjects = new Set(batches.map((batch) => batch.manifest.subject_id));
1160
+ if (subjects.size !== 1) {
1161
+ fail(
1162
+ "projection_subject_unavailable",
1163
+ "One trusted subject scope could not be resolved from canonical imports.",
1164
+ );
1165
+ }
1166
+ const subjectId = [...subjects][0];
1167
+ if (
1168
+ expectedSubjectId !== undefined &&
1169
+ subjectId !== expectedSubjectId.toLowerCase()
1170
+ ) {
1171
+ fail(
1172
+ "projection_subject_mismatch",
1173
+ "Canonical imports do not match the trusted subject scope.",
1174
+ );
1175
+ }
1176
+ return {
1177
+ subjectId,
1178
+ inputs: batches.map((batch) => ({
1179
+ import_batch_id: batch.manifest.import_batch_id,
1180
+ captured_at: batch.manifest.source.captured_at,
1181
+ digest: computeAppleProjectionInputDigest(batch),
1182
+ })),
1183
+ };
1184
+ }
1185
+
1186
+ export async function resolveAppleMusicSubjectId({
1187
+ importsRoot = resolveAppleMusicImportsRoot(),
1188
+ subjectId,
1189
+ } = {}) {
1190
+ return (await trustedScopeFromImports(importsRoot, subjectId)).subjectId;
1191
+ }
1192
+
1193
+ export class AppleProjectionDomainServices {
1194
+ #projection;
1195
+ #listeningHistoryStore;
1196
+ #subjectId;
1197
+ #candidateSets = new Map();
1198
+
1199
+ constructor({ projection, listeningHistoryStore = null, subjectId = null }) {
1200
+ if (!projection) throw new TypeError("A trusted Apple projection is required");
1201
+ if (listeningHistoryStore && !isUuid(subjectId)) {
1202
+ throw new TypeError("A trusted listening profile subject is required");
1203
+ }
1204
+ this.#projection = projection;
1205
+ this.#listeningHistoryStore = listeningHistoryStore;
1206
+ this.#subjectId = subjectId?.toLowerCase() ?? null;
1207
+ }
1208
+
1209
+ status() {
1210
+ return {
1211
+ state: "ready",
1212
+ adapter: "apple_music_sqlite_projection",
1213
+ subject_scope: "trusted_runtime",
1214
+ candidate_scope: "prompt_local",
1215
+ external_effects: "none",
1216
+ };
1217
+ }
1218
+
1219
+ profileStatus() {
1220
+ const summary = this.#projection.getProfileSummary({ maxItems: 1 });
1221
+ const listening = this.#listeningHistoryStore?.profileSummary({
1222
+ subjectId: this.#subjectId,
1223
+ maxItems: 1,
1224
+ });
1225
+ return {
1226
+ state: "ready",
1227
+ projection_version: listening ? "profile-projection/1" : summary.schema_version,
1228
+ evidence_records:
1229
+ summary.coverage.evidence_records +
1230
+ (listening?.coverage.profile_evidence_records ?? 0) +
1231
+ (listening?.coverage.active_listener_assertions ?? 0),
1232
+ ...(listening
1233
+ ? {
1234
+ effective_listening_events:
1235
+ listening.coverage.effective_listening_events,
1236
+ listening_hours: listening.coverage.listening_hours,
1237
+ spotify_profile_evidence:
1238
+ listening.coverage.profile_evidence_records,
1239
+ }
1240
+ : {}),
1241
+ claims: listening?.coverage.active_listener_assertions ?? 0,
1242
+ reason:
1243
+ listening
1244
+ ? "ProfileProjection v1 combines the canonical Apple library snapshot with private effective Spotify evidence and current explicit, retractable listener assertions."
1245
+ : "ProfileProjection v0 is deterministically derived from provider-neutral library observations and can be rebuilt from the canonical import batch.",
1246
+ };
1247
+ }
1248
+
1249
+ rediscoveryReady() {
1250
+ return (
1251
+ this.#listeningHistoryStore !== null &&
1252
+ typeof this.#listeningHistoryStore.rediscoveryCandidates === "function"
1253
+ );
1254
+ }
1255
+
1256
+ historicalReturnReady() {
1257
+ return (
1258
+ this.#listeningHistoryStore !== null &&
1259
+ typeof this.#listeningHistoryStore.historicalReturnCandidates ===
1260
+ "function"
1261
+ );
1262
+ }
1263
+
1264
+ timeCapsuleReady() {
1265
+ return (
1266
+ this.#listeningHistoryStore !== null &&
1267
+ typeof this.#listeningHistoryStore.timeCapsuleCandidates === "function"
1268
+ );
1269
+ }
1270
+
1271
+ backToBackReady() {
1272
+ return (
1273
+ this.#listeningHistoryStore !== null &&
1274
+ typeof this.#listeningHistoryStore.backToBackCandidates === "function"
1275
+ );
1276
+ }
1277
+
1278
+ beginPrompt() {
1279
+ return this.resetCandidateSets();
1280
+ }
1281
+
1282
+ endPrompt() {
1283
+ return this.resetCandidateSets();
1284
+ }
1285
+
1286
+ resetCandidateSets() {
1287
+ const invalidated = this.#candidateSets.size;
1288
+ this.#candidateSets.clear();
1289
+ return { invalidated_candidate_sets: invalidated };
1290
+ }
1291
+
1292
+ async searchLibrary(argumentsValue) {
1293
+ const input = modelArguments(
1294
+ argumentsValue,
1295
+ new Set(["query", "limit", "offset", "filters"]),
1296
+ );
1297
+ const cleanedQuery = cleanQuery(input.query);
1298
+ const bounded = boundedLimit(
1299
+ input.limit,
1300
+ APPLE_DOMAIN_LIMITS.searchDefault,
1301
+ APPLE_DOMAIN_LIMITS.searchMax,
1302
+ "invalid_search_limit",
1303
+ );
1304
+ const offset = boundedOffset(
1305
+ input.offset,
1306
+ APPLE_DOMAIN_LIMITS.searchOffsetMax,
1307
+ "invalid_search_offset",
1308
+ );
1309
+ const normalizedFilters = normalizeFilters(input.filters);
1310
+ if (this.#candidateSets.size >= APPLE_DOMAIN_LIMITS.candidateSetsMax) {
1311
+ fail(
1312
+ "candidate_set_capacity_reached",
1313
+ "The prompt has reached its candidate set limit.",
1314
+ );
1315
+ }
1316
+
1317
+ const rawLimit = Math.min(bounded + 1, 25);
1318
+ const raw = this.#projection.searchLibrary({
1319
+ query: cleanedQuery,
1320
+ limit: rawLimit,
1321
+ offset,
1322
+ filters: rawSearchFilters(normalizedFilters),
1323
+ });
1324
+ const filtered = raw.tracks
1325
+ .map(safeTrack)
1326
+ .filter((track) => matchesFilters(track, normalizedFilters));
1327
+ const tracks = filtered.slice(0, bounded);
1328
+ const hasMore = filtered.length > bounded || raw.tracks.length === rawLimit;
1329
+ const nextOffset = hasMore
1330
+ ? Math.min(
1331
+ offset + Math.max(tracks.length, bounded),
1332
+ APPLE_DOMAIN_LIMITS.searchOffsetMax,
1333
+ )
1334
+ : null;
1335
+ const candidateSetId = randomUUID();
1336
+ this.#candidateSets.set(
1337
+ candidateSetId,
1338
+ new Map(tracks.map((track) => [track.track_ref_id, structuredClone(track)])),
1339
+ );
1340
+ return {
1341
+ candidate_set_id: candidateSetId,
1342
+ candidate_scope: "private_library",
1343
+ result_count: tracks.length,
1344
+ limit_applied: bounded,
1345
+ offset_applied: offset,
1346
+ next_offset: nextOffset === offset ? null : nextOffset,
1347
+ has_more: hasMore && nextOffset !== offset,
1348
+ expires_on: "prompt_end",
1349
+ tracks,
1350
+ };
1351
+ }
1352
+
1353
+ async getRediscoveryCandidates(argumentsValue) {
1354
+ const input = modelArguments(argumentsValue, new Set(["limit"]));
1355
+ const limit = boundedLimit(
1356
+ input.limit,
1357
+ APPLE_DOMAIN_LIMITS.rediscoveryDefault,
1358
+ APPLE_DOMAIN_LIMITS.rediscoveryMax,
1359
+ "invalid_rediscovery_limit",
1360
+ );
1361
+ if (!this.rediscoveryReady()) {
1362
+ fail(
1363
+ "rediscovery_unavailable",
1364
+ "Private listening-history rediscovery is not ready.",
1365
+ );
1366
+ }
1367
+ const raw = this.#listeningHistoryStore.rediscoveryCandidates({
1368
+ subjectId: this.#subjectId,
1369
+ limit,
1370
+ });
1371
+ if (!isPlainObject(raw) || !Array.isArray(raw.tracks)) {
1372
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1373
+ }
1374
+ const referenceDate =
1375
+ raw.reference_date === null || raw.reference_date === undefined
1376
+ ? null
1377
+ : safeHistoryTimestamp(raw.reference_date);
1378
+ const quietDays = safeHistoryCount(raw.quiet_days);
1379
+ const minimumPlays = safeHistoryCount(raw.minimum_plays);
1380
+ const minimumEngagedPlays = safeHistoryCount(raw.minimum_engaged_plays);
1381
+ const minimumListeningMinutes = safeHistoryCount(
1382
+ raw.minimum_listening_minutes,
1383
+ );
1384
+ const tracks = raw.tracks.slice(0, limit).map(safeHistoryTrack);
1385
+ if (new Set(tracks.map((track) => track.track_ref_id)).size !== tracks.length) {
1386
+ fail("history_candidate_invalid", "Listening-history candidates are duplicated.");
1387
+ }
1388
+ if (tracks.length === 0) {
1389
+ return {
1390
+ state: "empty",
1391
+ candidate_set_id: null,
1392
+ candidate_scope: "private_history",
1393
+ result_count: 0,
1394
+ limit_applied: limit,
1395
+ reference_date: referenceDate,
1396
+ quiet_days: quietDays,
1397
+ minimum_plays: minimumPlays,
1398
+ minimum_engaged_plays: minimumEngagedPlays,
1399
+ minimum_listening_minutes: minimumListeningMinutes,
1400
+ expires_on: "prompt_end",
1401
+ tracks: [],
1402
+ };
1403
+ }
1404
+ if (referenceDate === null) {
1405
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1406
+ }
1407
+ if (this.#candidateSets.size >= APPLE_DOMAIN_LIMITS.candidateSetsMax) {
1408
+ fail(
1409
+ "candidate_set_capacity_reached",
1410
+ "The prompt has reached its candidate set limit.",
1411
+ );
1412
+ }
1413
+ const candidateSetId = randomUUID();
1414
+ this.#candidateSets.set(
1415
+ candidateSetId,
1416
+ new Map(
1417
+ tracks.map((track) => [track.track_ref_id, structuredClone(track)]),
1418
+ ),
1419
+ );
1420
+ return {
1421
+ state: "ready",
1422
+ candidate_set_id: candidateSetId,
1423
+ candidate_scope: "private_history",
1424
+ result_count: tracks.length,
1425
+ limit_applied: limit,
1426
+ reference_date: referenceDate,
1427
+ quiet_days: quietDays,
1428
+ minimum_plays: minimumPlays,
1429
+ minimum_engaged_plays: minimumEngagedPlays,
1430
+ minimum_listening_minutes: minimumListeningMinutes,
1431
+ expires_on: "prompt_end",
1432
+ tracks: tracks.map(publicHistoryTrack),
1433
+ };
1434
+ }
1435
+
1436
+ async getHistoricalReturnCandidates(argumentsValue) {
1437
+ const input = modelArguments(argumentsValue, new Set(["limit"]));
1438
+ const limit = boundedLimit(
1439
+ input.limit,
1440
+ APPLE_DOMAIN_LIMITS.historicalReturnDefault,
1441
+ APPLE_DOMAIN_LIMITS.historicalReturnMax,
1442
+ "invalid_historical_return_limit",
1443
+ );
1444
+ if (!this.historicalReturnReady()) {
1445
+ fail(
1446
+ "historical_return_unavailable",
1447
+ "Private listening-history returns are not ready.",
1448
+ );
1449
+ }
1450
+ const raw = this.#listeningHistoryStore.historicalReturnCandidates({
1451
+ subjectId: this.#subjectId,
1452
+ limit,
1453
+ });
1454
+ if (!isPlainObject(raw) || !Array.isArray(raw.tracks)) {
1455
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1456
+ }
1457
+ const referenceDate =
1458
+ raw.reference_date === null || raw.reference_date === undefined
1459
+ ? null
1460
+ : safeHistoryTimestamp(raw.reference_date);
1461
+ const minimumGapDays = safeHistoryCount(raw.minimum_gap_days);
1462
+ const minimumPlays = safeHistoryCount(raw.minimum_plays);
1463
+ const minimumEngagedPlays = safeHistoryCount(raw.minimum_engaged_plays);
1464
+ const minimumListeningMinutes = safeHistoryCount(
1465
+ raw.minimum_listening_minutes,
1466
+ );
1467
+ const tracks = raw.tracks.slice(0, limit).map(safeHistoryTrack);
1468
+ if (new Set(tracks.map((track) => track.track_ref_id)).size !== tracks.length) {
1469
+ fail("history_candidate_invalid", "Listening-history candidates are duplicated.");
1470
+ }
1471
+ if (tracks.length === 0) {
1472
+ return {
1473
+ state: "empty",
1474
+ candidate_set_id: null,
1475
+ candidate_scope: "private_history",
1476
+ result_count: 0,
1477
+ limit_applied: limit,
1478
+ reference_date: referenceDate,
1479
+ minimum_gap_days: minimumGapDays,
1480
+ minimum_plays: minimumPlays,
1481
+ minimum_engaged_plays: minimumEngagedPlays,
1482
+ minimum_listening_minutes: minimumListeningMinutes,
1483
+ expires_on: "prompt_end",
1484
+ tracks: [],
1485
+ };
1486
+ }
1487
+ if (referenceDate === null) {
1488
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1489
+ }
1490
+ if (this.#candidateSets.size >= APPLE_DOMAIN_LIMITS.candidateSetsMax) {
1491
+ fail(
1492
+ "candidate_set_capacity_reached",
1493
+ "The prompt has reached its candidate set limit.",
1494
+ );
1495
+ }
1496
+ const candidateSetId = randomUUID();
1497
+ this.#candidateSets.set(
1498
+ candidateSetId,
1499
+ new Map(
1500
+ tracks.map((track) => [track.track_ref_id, structuredClone(track)]),
1501
+ ),
1502
+ );
1503
+ return {
1504
+ state: "ready",
1505
+ candidate_set_id: candidateSetId,
1506
+ candidate_scope: "private_history",
1507
+ result_count: tracks.length,
1508
+ limit_applied: limit,
1509
+ reference_date: referenceDate,
1510
+ minimum_gap_days: minimumGapDays,
1511
+ minimum_plays: minimumPlays,
1512
+ minimum_engaged_plays: minimumEngagedPlays,
1513
+ minimum_listening_minutes: minimumListeningMinutes,
1514
+ expires_on: "prompt_end",
1515
+ tracks: tracks.map(publicHistoryTrack),
1516
+ };
1517
+ }
1518
+
1519
+ async getBackToBackCandidates(argumentsValue) {
1520
+ const input = modelArguments(argumentsValue, new Set(["limit"]));
1521
+ const limit = boundedLimit(
1522
+ input.limit,
1523
+ APPLE_DOMAIN_LIMITS.backToBackDefault,
1524
+ APPLE_DOMAIN_LIMITS.backToBackMax,
1525
+ "invalid_back_to_back_limit",
1526
+ );
1527
+ if (!this.backToBackReady()) {
1528
+ fail(
1529
+ "back_to_back_unavailable",
1530
+ "Private listening-history back-to-back playback is not ready.",
1531
+ );
1532
+ }
1533
+ const raw = this.#listeningHistoryStore.backToBackCandidates({
1534
+ subjectId: this.#subjectId,
1535
+ limit,
1536
+ });
1537
+ if (!isPlainObject(raw) || !Array.isArray(raw.tracks)) {
1538
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1539
+ }
1540
+ const referenceDate =
1541
+ raw.reference_date === null || raw.reference_date === undefined
1542
+ ? null
1543
+ : safeHistoryTimestamp(raw.reference_date);
1544
+ const minimumConsecutivePlays = safeHistoryCount(
1545
+ raw.minimum_consecutive_plays,
1546
+ );
1547
+ const minimumPlayedSeconds = safeHistoryCount(raw.minimum_played_seconds);
1548
+ const maximumGapMinutes = safeHistoryCount(raw.maximum_gap_minutes);
1549
+ if (
1550
+ minimumConsecutivePlays < 2 ||
1551
+ minimumPlayedSeconds < 1 ||
1552
+ maximumGapMinutes < 1
1553
+ ) {
1554
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1555
+ }
1556
+ const tracks = raw.tracks.slice(0, limit).map(safeHistoryTrack);
1557
+ if (new Set(tracks.map((track) => track.track_ref_id)).size !== tracks.length) {
1558
+ fail("history_candidate_invalid", "Listening-history candidates are duplicated.");
1559
+ }
1560
+ if (tracks.length === 0) {
1561
+ return {
1562
+ state: "empty",
1563
+ candidate_set_id: null,
1564
+ candidate_scope: "private_history",
1565
+ result_count: 0,
1566
+ limit_applied: limit,
1567
+ reference_date: referenceDate,
1568
+ minimum_consecutive_plays: minimumConsecutivePlays,
1569
+ minimum_played_seconds: minimumPlayedSeconds,
1570
+ maximum_gap_minutes: maximumGapMinutes,
1571
+ expires_on: "prompt_end",
1572
+ tracks: [],
1573
+ };
1574
+ }
1575
+ if (referenceDate === null) {
1576
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1577
+ }
1578
+ if (this.#candidateSets.size >= APPLE_DOMAIN_LIMITS.candidateSetsMax) {
1579
+ fail(
1580
+ "candidate_set_capacity_reached",
1581
+ "The prompt has reached its candidate set limit.",
1582
+ );
1583
+ }
1584
+ const candidateSetId = randomUUID();
1585
+ this.#candidateSets.set(
1586
+ candidateSetId,
1587
+ new Map(
1588
+ tracks.map((track) => [track.track_ref_id, structuredClone(track)]),
1589
+ ),
1590
+ );
1591
+ return {
1592
+ state: "ready",
1593
+ candidate_set_id: candidateSetId,
1594
+ candidate_scope: "private_history",
1595
+ result_count: tracks.length,
1596
+ limit_applied: limit,
1597
+ reference_date: referenceDate,
1598
+ minimum_consecutive_plays: minimumConsecutivePlays,
1599
+ minimum_played_seconds: minimumPlayedSeconds,
1600
+ maximum_gap_minutes: maximumGapMinutes,
1601
+ expires_on: "prompt_end",
1602
+ tracks: tracks.map(publicHistoryTrack),
1603
+ };
1604
+ }
1605
+
1606
+ async getTimeCapsuleCandidates(argumentsValue) {
1607
+ const input = modelArguments(argumentsValue, new Set(["limit"]));
1608
+ const limit = boundedLimit(
1609
+ input.limit,
1610
+ APPLE_DOMAIN_LIMITS.timeCapsuleDefault,
1611
+ APPLE_DOMAIN_LIMITS.timeCapsuleMax,
1612
+ "invalid_time_capsule_limit",
1613
+ );
1614
+ if (limit < 2) {
1615
+ fail(
1616
+ "invalid_time_capsule_limit",
1617
+ "A listening time capsule requires at least two tracks.",
1618
+ );
1619
+ }
1620
+ if (!this.timeCapsuleReady()) {
1621
+ fail(
1622
+ "time_capsule_unavailable",
1623
+ "Private listening-history Time Machine is not ready.",
1624
+ );
1625
+ }
1626
+ const raw = this.#listeningHistoryStore.timeCapsuleCandidates({
1627
+ subjectId: this.#subjectId,
1628
+ limit,
1629
+ });
1630
+ if (
1631
+ !isPlainObject(raw) ||
1632
+ !Array.isArray(raw.tracks) ||
1633
+ !Array.isArray(raw.represented_years)
1634
+ ) {
1635
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1636
+ }
1637
+ const optionalYear = (value) => {
1638
+ if (value === null || value === undefined) return null;
1639
+ const year = safeHistoryCount(value);
1640
+ if (year < 1900 || year > 9999) {
1641
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1642
+ }
1643
+ return year;
1644
+ };
1645
+ const referenceDate =
1646
+ raw.reference_date === null || raw.reference_date === undefined
1647
+ ? null
1648
+ : safeHistoryTimestamp(raw.reference_date);
1649
+ const historyStartYear = optionalYear(raw.history_start_year);
1650
+ const historyEndYear = optionalYear(raw.history_end_year);
1651
+ const minimumYears = safeHistoryCount(raw.minimum_years);
1652
+ const minimumEngagedPlays = safeHistoryCount(raw.minimum_engaged_plays);
1653
+ const minimumListeningMinutes = safeHistoryCount(
1654
+ raw.minimum_listening_minutes,
1655
+ );
1656
+ const tracks = raw.tracks.slice(0, limit).map(safeHistoryTrack);
1657
+ const representedYears = raw.represented_years.map(optionalYear);
1658
+ const trackYears = tracks.map((track) => track.time_capsule?.year);
1659
+ if (
1660
+ representedYears.some((year) => year === null) ||
1661
+ representedYears.length !== tracks.length ||
1662
+ new Set(tracks.map((track) => track.track_ref_id)).size !== tracks.length ||
1663
+ new Set(trackYears).size !== trackYears.length ||
1664
+ representedYears.some((year, index) => year !== trackYears[index]) ||
1665
+ trackYears.some((year, index) => index > 0 && year <= trackYears[index - 1])
1666
+ ) {
1667
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1668
+ }
1669
+ if (tracks.length === 0) {
1670
+ return {
1671
+ state: "empty",
1672
+ candidate_set_id: null,
1673
+ candidate_scope: "private_history",
1674
+ result_count: 0,
1675
+ limit_applied: limit,
1676
+ reference_date: referenceDate,
1677
+ history_start_year: historyStartYear,
1678
+ history_end_year: historyEndYear,
1679
+ minimum_years: minimumYears,
1680
+ minimum_engaged_plays: minimumEngagedPlays,
1681
+ minimum_listening_minutes: minimumListeningMinutes,
1682
+ represented_years: [],
1683
+ expires_on: "prompt_end",
1684
+ tracks: [],
1685
+ };
1686
+ }
1687
+ if (
1688
+ tracks.length < minimumYears ||
1689
+ referenceDate === null ||
1690
+ historyStartYear === null ||
1691
+ historyEndYear === null ||
1692
+ historyStartYear > historyEndYear ||
1693
+ trackYears[0] < historyStartYear ||
1694
+ trackYears.at(-1) > historyEndYear
1695
+ ) {
1696
+ fail("history_candidate_invalid", "Listening-history candidates are invalid.");
1697
+ }
1698
+ if (this.#candidateSets.size >= APPLE_DOMAIN_LIMITS.candidateSetsMax) {
1699
+ fail(
1700
+ "candidate_set_capacity_reached",
1701
+ "The prompt has reached its candidate set limit.",
1702
+ );
1703
+ }
1704
+ const candidateSetId = randomUUID();
1705
+ this.#candidateSets.set(
1706
+ candidateSetId,
1707
+ new Map(
1708
+ tracks.map((track) => [track.track_ref_id, structuredClone(track)]),
1709
+ ),
1710
+ );
1711
+ return {
1712
+ state: "ready",
1713
+ candidate_set_id: candidateSetId,
1714
+ candidate_scope: "private_history",
1715
+ result_count: tracks.length,
1716
+ limit_applied: limit,
1717
+ reference_date: referenceDate,
1718
+ history_start_year: historyStartYear,
1719
+ history_end_year: historyEndYear,
1720
+ minimum_years: minimumYears,
1721
+ minimum_engaged_plays: minimumEngagedPlays,
1722
+ minimum_listening_minutes: minimumListeningMinutes,
1723
+ represented_years: representedYears,
1724
+ expires_on: "prompt_end",
1725
+ tracks: tracks.map(publicHistoryTrack),
1726
+ };
1727
+ }
1728
+
1729
+ registerExternalCandidateSet({ tracks, source } = {}) {
1730
+ if (
1731
+ !Array.isArray(tracks) ||
1732
+ tracks.length < 1 ||
1733
+ tracks.length > APPLE_DOMAIN_LIMITS.playlistTracksMax
1734
+ ) {
1735
+ fail("external_candidate_invalid", "External catalog candidates are invalid.");
1736
+ }
1737
+ if (this.#candidateSets.size >= APPLE_DOMAIN_LIMITS.candidateSetsMax) {
1738
+ fail(
1739
+ "candidate_set_capacity_reached",
1740
+ "The prompt has reached its candidate set limit.",
1741
+ );
1742
+ }
1743
+ const trustedSource = safeExternalSource(source);
1744
+ const prepared = tracks.map(safeExternalTrack);
1745
+ if (new Set(prepared.map((track) => track.track_ref_id)).size !== prepared.length) {
1746
+ fail("external_candidate_invalid", "External catalog candidates are duplicated.");
1747
+ }
1748
+ const accepted = [];
1749
+ let excludedLibraryMatches = 0;
1750
+ for (const track of prepared) {
1751
+ const query = Array.from(track.title).slice(0, 256).join("");
1752
+ const libraryResults = this.#projection.searchLibrary({
1753
+ query,
1754
+ limit: 25,
1755
+ offset: 0,
1756
+ filters: {
1757
+ artists: [],
1758
+ genres: [],
1759
+ familiarity: [],
1760
+ preferenceSignals: [],
1761
+ },
1762
+ });
1763
+ const libraryMatch = libraryResults.tracks
1764
+ .map(safeTrack)
1765
+ .some((candidate) => exactTitleArtistMatch(track, candidate));
1766
+ if (libraryMatch) {
1767
+ excludedLibraryMatches += 1;
1768
+ continue;
1769
+ }
1770
+ accepted.push({
1771
+ ...track,
1772
+ knownness: {
1773
+ imported_library: "not_found_by_exact_title_artist",
1774
+ listening_history: "not_checked",
1775
+ },
1776
+ });
1777
+ }
1778
+ if (accepted.length === 0) {
1779
+ return {
1780
+ candidate_set_id: null,
1781
+ candidate_scope: "external_catalog",
1782
+ result_count: 0,
1783
+ excluded_library_matches: excludedLibraryMatches,
1784
+ expires_on: "prompt_end",
1785
+ source: trustedSource,
1786
+ tracks: [],
1787
+ };
1788
+ }
1789
+ const candidateSetId = randomUUID();
1790
+ this.#candidateSets.set(
1791
+ candidateSetId,
1792
+ new Map(
1793
+ accepted.map((track) => [track.track_ref_id, structuredClone(track)]),
1794
+ ),
1795
+ );
1796
+ return {
1797
+ candidate_set_id: candidateSetId,
1798
+ candidate_scope: "external_catalog",
1799
+ result_count: accepted.length,
1800
+ excluded_library_matches: excludedLibraryMatches,
1801
+ expires_on: "prompt_end",
1802
+ source: trustedSource,
1803
+ tracks: accepted,
1804
+ };
1805
+ }
1806
+
1807
+ registerRetainedPlaylistCandidateSet({ tracks } = {}) {
1808
+ if (
1809
+ !Array.isArray(tracks) ||
1810
+ tracks.length < 1 ||
1811
+ tracks.length > APPLE_DOMAIN_LIMITS.playlistTracksMax
1812
+ ) {
1813
+ fail(
1814
+ "retained_candidate_invalid",
1815
+ "Retained playlist candidates are invalid.",
1816
+ );
1817
+ }
1818
+ if (this.#candidateSets.size >= APPLE_DOMAIN_LIMITS.candidateSetsMax) {
1819
+ fail(
1820
+ "candidate_set_capacity_reached",
1821
+ "The prompt has reached its candidate set limit.",
1822
+ );
1823
+ }
1824
+ const prepared = tracks.map(safeRetainedPlaylistTrack);
1825
+ if (
1826
+ new Set(prepared.map((track) => track.track_ref_id)).size !==
1827
+ prepared.length
1828
+ ) {
1829
+ fail(
1830
+ "retained_candidate_invalid",
1831
+ "Retained playlist candidates are duplicated.",
1832
+ );
1833
+ }
1834
+ const candidateSetId = randomUUID();
1835
+ this.#candidateSets.set(
1836
+ candidateSetId,
1837
+ new Map(
1838
+ prepared.map((track) => [track.track_ref_id, structuredClone(track)]),
1839
+ ),
1840
+ );
1841
+ const scopes = new Set(
1842
+ prepared.map((track) => track.candidate_scope ?? "private_library"),
1843
+ );
1844
+ return {
1845
+ candidate_set_id: candidateSetId,
1846
+ candidate_scope: scopes.size === 1 ? [...scopes][0] : "mixed",
1847
+ result_count: prepared.length,
1848
+ expires_on: "prompt_end",
1849
+ source: "retained_validated_playlist",
1850
+ tracks: structuredClone(prepared),
1851
+ };
1852
+ }
1853
+
1854
+ getTrustedTracks(trackRefIds) {
1855
+ if (
1856
+ !Array.isArray(trackRefIds) ||
1857
+ trackRefIds.length < 1 ||
1858
+ trackRefIds.length > APPLE_DOMAIN_LIMITS.playlistTracksMax
1859
+ ) {
1860
+ fail("invalid_track_refs", "Trusted track references are invalid.");
1861
+ }
1862
+ const results = [];
1863
+ for (const trackRefId of trackRefIds) {
1864
+ const cleanedId = cleanText(trackRefId, 128, "invalid_track_refs");
1865
+ let track = null;
1866
+ for (const candidateSet of this.#candidateSets.values()) {
1867
+ if (candidateSet.has(cleanedId)) {
1868
+ track = candidateSet.get(cleanedId);
1869
+ break;
1870
+ }
1871
+ }
1872
+ if (!track) {
1873
+ fail(
1874
+ "untrusted_track_ref",
1875
+ "A referenced track was not returned by the trusted candidate sets.",
1876
+ );
1877
+ }
1878
+ results.push(track);
1879
+ }
1880
+ return structuredClone(results);
1881
+ }
1882
+
1883
+ async getLyricSeeds() {
1884
+ const profile = await this.getProfileSummary({ maxItems: 10 });
1885
+ const listening = this.#listeningHistoryStore?.lyricProfile({ subjectId: this.#subjectId });
1886
+ return {
1887
+ subjectId: this.#subjectId,
1888
+ tracks: lyricSeedsFromProfile({
1889
+ ...profile, ...listening,
1890
+ strong_preferences: profile.strong_preferences,
1891
+ familiarity: profile.familiarity,
1892
+ }),
1893
+ };
1894
+ }
1895
+
1896
+ async getProfileSummary(argumentsValue) {
1897
+ const input = modelArguments(argumentsValue, new Set(["maxItems"]));
1898
+ const bounded = boundedLimit(
1899
+ input.maxItems,
1900
+ APPLE_DOMAIN_LIMITS.profileDefault,
1901
+ APPLE_DOMAIN_LIMITS.profileMax,
1902
+ "invalid_profile_limit",
1903
+ );
1904
+ const raw = this.#projection.getProfileSummary({ maxItems: 50 });
1905
+ const listening = normalizeAppleCorrectionLabels(
1906
+ this.#listeningHistoryStore?.profileSummary({
1907
+ subjectId: this.#subjectId,
1908
+ maxItems: bounded,
1909
+ }),
1910
+ this.#projection,
1911
+ );
1912
+ const preferenceItems = (raw.preference ?? [])
1913
+ .filter(
1914
+ (item) =>
1915
+ item.direction === "supports" &&
1916
+ !listenerAssertionAvoids(
1917
+ listening,
1918
+ "track",
1919
+ item.label,
1920
+ item.artist_credit,
1921
+ ),
1922
+ )
1923
+ .slice(0, bounded);
1924
+ const familiarityItems = (raw.familiarity ?? [])
1925
+ .filter((item) => item.direction === "supports")
1926
+ .slice(0, bounded);
1927
+ const appleStrongPreferences = preferenceItems.map((item) => ({
1928
+ label: item.label,
1929
+ entity_type: "track",
1930
+ artist_credit: item.artist_credit,
1931
+ track_ref_id: item.track_ref?.track_ref_id,
1932
+ source: "Apple Music library preference",
1933
+ observed_at: item.observed_at,
1934
+ confidence: item.confidence,
1935
+ signal: preferenceSignalLabel(item.observation_summary),
1936
+ evidence_id: item.evidence_id,
1937
+ }));
1938
+ const listenerStrongPreferences = (
1939
+ listening?.listener_assertions?.preferences ?? []
1940
+ ).map((item) => ({
1941
+ label: item.label,
1942
+ entity_type: item.entity_type,
1943
+ ...(item.artist_credit ? { artist_credit: item.artist_credit } : {}),
1944
+ signal: `You explicitly said you like this ${item.entity_type}`,
1945
+ evidence_id: item.evidence_id,
1946
+ }));
1947
+ const spotifyStrongPreferences = listening
1948
+ ? listening.curated_preferences.saved_tracks
1949
+ .filter(
1950
+ (item) =>
1951
+ !listenerAssertionAvoids(
1952
+ listening,
1953
+ "track",
1954
+ item.label,
1955
+ item.artist_credit,
1956
+ ),
1957
+ )
1958
+ .map((item) => ({
1959
+ label: item.label,
1960
+ entity_type: "track",
1961
+ artist_credit: item.artist_credit,
1962
+ track_ref_id: item.track_ref_id,
1963
+ signal: `Saved in ${item.source_label ?? "Spotify"} library`,
1964
+ evidence_id: item.evidence_id,
1965
+ }))
1966
+ : [];
1967
+ const appleFamiliarity = familiarityItems.map((item) => {
1968
+ const playCount = item.observation_summary?.play_count;
1969
+ const result = {
1970
+ label: item.label,
1971
+ entity_type: "track",
1972
+ artist_credit: item.artist_credit,
1973
+ track_ref_id: item.track_ref?.track_ref_id,
1974
+ source: "Apple Music aggregate play count",
1975
+ observed_at: item.observed_at,
1976
+ confidence: item.confidence,
1977
+ level: familiarityLevel(playCount),
1978
+ evidence_id: item.evidence_id,
1979
+ };
1980
+ if (Number.isInteger(playCount)) result.play_count = playCount;
1981
+ return result;
1982
+ });
1983
+ const spotifyFamiliarity =
1984
+ listening?.listening_behavior.repeat_tracks.map((item) => ({
1985
+ label: item.label,
1986
+ entity_type: "track",
1987
+ artist_credit: item.artist_credit,
1988
+ track_ref_id: item.track_ref_id,
1989
+ level: familiarityLevel(item.play_count),
1990
+ play_count: item.play_count,
1991
+ evidence_id: item.evidence_id,
1992
+ })) ?? [];
1993
+ const appleArtistFacets = facetItems(
1994
+ preferenceItems,
1995
+ (item) => item.artist_credit,
1996
+ bounded,
1997
+ ).filter(
1998
+ (item) => !listenerAssertionAvoids(listening, "artist", item.name),
1999
+ );
2000
+ const spotifyArtistFacets = [
2001
+ ...(listening?.curated_preferences.followed_artists ?? []),
2002
+ ...(listening?.listening_behavior.enduring_artists ?? []),
2003
+ ...(listening?.provider_signals.artists ?? []),
2004
+ ]
2005
+ .filter(
2006
+ (item) => !listenerAssertionAvoids(listening, "artist", item.name),
2007
+ )
2008
+ .map((item) => ({
2009
+ name: item.name,
2010
+ evidence_id: item.evidence_id,
2011
+ }));
2012
+ const appleGenreFacets = facetItems(
2013
+ preferenceItems,
2014
+ (item) => item.labels?.provider_genre,
2015
+ bounded,
2016
+ );
2017
+ const spotifyGenreFacets = (listening?.provider_signals.genres ?? []).map(
2018
+ (item) => ({ name: item.name, evidence_id: item.evidence_id }),
2019
+ );
2020
+ return {
2021
+ profile_version: listening ? "profile-projection/1" : raw.schema_version,
2022
+ max_items_applied: bounded,
2023
+ strong_preferences: interleaveUnique(
2024
+ [
2025
+ listenerStrongPreferences,
2026
+ appleStrongPreferences,
2027
+ spotifyStrongPreferences,
2028
+ ],
2029
+ bounded,
2030
+ (item) => JSON.stringify([item.entity_type, item.label, item.artist_credit ?? ""]),
2031
+ ),
2032
+ familiarity: interleaveUnique(
2033
+ [appleFamiliarity, spotifyFamiliarity],
2034
+ bounded,
2035
+ (item) => JSON.stringify([item.entity_type, item.label, item.artist_credit ?? ""]),
2036
+ ),
2037
+ artist_facets: interleaveUnique(
2038
+ [appleArtistFacets, spotifyArtistFacets],
2039
+ bounded,
2040
+ (item) => item.name,
2041
+ ),
2042
+ genre_facets: interleaveUnique(
2043
+ [appleGenreFacets, spotifyGenreFacets],
2044
+ bounded,
2045
+ (item) => item.name,
2046
+ ),
2047
+ coverage: {
2048
+ tracks_observed: raw.coverage.current_tracks,
2049
+ loved_or_favorited: raw.coverage.loved_or_favorited,
2050
+ aggregate_play_count: raw.coverage.aggregate_play_count,
2051
+ non_computed_rating: raw.coverage.non_computed_rating,
2052
+ ...(listening
2053
+ ? {
2054
+ effective_listening_events:
2055
+ listening.coverage.effective_listening_events,
2056
+ profiled_listening_events:
2057
+ listening.coverage.profiled_listening_events,
2058
+ listening_hours: listening.coverage.listening_hours,
2059
+ listening_tracks: listening.coverage.distinct_tracks,
2060
+ resolved_listening_tracks: listening.coverage.resolved_tracks,
2061
+ cross_format_track_links:
2062
+ listening.coverage.cross_format_track_links,
2063
+ cross_format_linked_events:
2064
+ listening.coverage.cross_format_linked_events,
2065
+ cross_format_ambiguous_tracks:
2066
+ listening.coverage.cross_format_ambiguous_tracks,
2067
+ cross_format_ambiguous_events:
2068
+ listening.coverage.cross_format_ambiguous_events,
2069
+ ...collectionCoverage(listening.coverage),
2070
+ verified_search_interactions:
2071
+ listening.coverage.verified_search_interactions,
2072
+ listener_assertion_events:
2073
+ listening.coverage.listener_assertion_events,
2074
+ active_listener_assertions:
2075
+ listening.coverage.active_listener_assertions,
2076
+ listener_retractions:
2077
+ listening.coverage.listener_retractions,
2078
+ }
2079
+ : {}),
2080
+ },
2081
+ source: {
2082
+ kind: "provider_library_snapshot",
2083
+ captured_at: raw.coverage.source_timestamp,
2084
+ },
2085
+ ...(listening
2086
+ ? {
2087
+ listening_behavior: listening.listening_behavior,
2088
+ curated_preferences: listening.curated_preferences,
2089
+ listener_assertions: listening.listener_assertions,
2090
+ search_intent: listening.search_intent,
2091
+ provider_signals: listening.provider_signals,
2092
+ listening_source: listening.source,
2093
+ }
2094
+ : {}),
2095
+ limitations: [
2096
+ ...(raw.limitations ?? []),
2097
+ ...(listening?.limitations ?? []),
2098
+ ].slice(0, 8),
2099
+ };
2100
+ }
2101
+
2102
+ async explainProfileEvidence(argumentsValue) {
2103
+ const input = modelArguments(argumentsValue, new Set(["evidenceId"]));
2104
+ const cleanedId = cleanText(
2105
+ input.evidenceId,
2106
+ 128,
2107
+ "invalid_evidence_id",
2108
+ );
2109
+ if (!isUuid(cleanedId)) {
2110
+ fail("invalid_evidence_id", "Profile evidence ID is invalid.");
2111
+ }
2112
+ const raw = this.#projection.explainProfileEvidence({
2113
+ evidenceId: cleanedId,
2114
+ });
2115
+ if (!raw) {
2116
+ const listening = this.#listeningHistoryStore?.explainProfileEvidence({
2117
+ subjectId: this.#subjectId,
2118
+ evidenceId: cleanedId,
2119
+ });
2120
+ if (listening) return listening;
2121
+ fail(
2122
+ "profile_evidence_unavailable",
2123
+ "Profile evidence is unavailable in the trusted subject scope.",
2124
+ );
2125
+ }
2126
+ return {
2127
+ evidence_id: raw.evidence_id,
2128
+ claim: {
2129
+ dimension: raw.claim.dimension,
2130
+ value: claimValueLabel(raw.claim),
2131
+ direction: raw.direction,
2132
+ },
2133
+ basis_summary: basisSummaryText(raw),
2134
+ derivation: {
2135
+ kind: raw.derivation.kind,
2136
+ name: raw.derivation.name,
2137
+ version: raw.derivation.version,
2138
+ },
2139
+ confidence: raw.confidence,
2140
+ interpretation_limit: raw.limitation,
2141
+ };
2142
+ }
2143
+
2144
+ async buildPlaylistPlan(argumentsValue) {
2145
+ const input = modelArguments(
2146
+ argumentsValue,
2147
+ new Set([
2148
+ "intent",
2149
+ "requestedTrackCount",
2150
+ "candidateSetIds",
2151
+ "trackRefs",
2152
+ "orderingNotes",
2153
+ ]),
2154
+ );
2155
+ const {
2156
+ intent,
2157
+ requestedTrackCount: explicitRequestedCount,
2158
+ candidateSetIds,
2159
+ trackRefs,
2160
+ orderingNotes,
2161
+ } = input;
2162
+ const cleanedIntent = cleanText(
2163
+ intent,
2164
+ APPLE_DOMAIN_LIMITS.longTextMax,
2165
+ "invalid_playlist_intent",
2166
+ );
2167
+ const orderingRationale = cleanText(
2168
+ orderingNotes,
2169
+ APPLE_DOMAIN_LIMITS.longTextMax,
2170
+ "invalid_ordering_notes",
2171
+ );
2172
+ if (
2173
+ !Array.isArray(candidateSetIds) ||
2174
+ candidateSetIds.length < 1 ||
2175
+ candidateSetIds.length > APPLE_DOMAIN_LIMITS.candidateSetsMax ||
2176
+ new Set(candidateSetIds).size !== candidateSetIds.length
2177
+ ) {
2178
+ fail("invalid_candidate_sets", "Playlist candidate sets are invalid.");
2179
+ }
2180
+ const trustedTracks = new Map();
2181
+ for (const candidateSetId of candidateSetIds) {
2182
+ const cleanedId = cleanText(
2183
+ candidateSetId,
2184
+ 128,
2185
+ "invalid_candidate_sets",
2186
+ );
2187
+ const candidateSet = this.#candidateSets.get(cleanedId);
2188
+ if (!candidateSet) {
2189
+ fail(
2190
+ "candidate_set_unavailable",
2191
+ "A playlist candidate set is unavailable or expired.",
2192
+ );
2193
+ }
2194
+ for (const [trackId, track] of candidateSet) trustedTracks.set(trackId, track);
2195
+ }
2196
+ if (
2197
+ !Array.isArray(trackRefs) ||
2198
+ trackRefs.length < 1 ||
2199
+ trackRefs.length > APPLE_DOMAIN_LIMITS.playlistTracksMax
2200
+ ) {
2201
+ fail("invalid_track_refs", "Playlist track references are invalid.");
2202
+ }
2203
+ const selections = trackRefs.map((entry) => {
2204
+ if (!isPlainObject(entry)) {
2205
+ fail("invalid_track_refs", "Playlist track references are invalid.");
2206
+ }
2207
+ const allowed = new Set(["trackRefId", "selectionReason"]);
2208
+ if (Object.keys(entry).some((key) => !allowed.has(key))) {
2209
+ fail("invalid_track_refs", "Playlist track references are invalid.");
2210
+ }
2211
+ return {
2212
+ trackRefId: cleanText(
2213
+ entry.trackRefId,
2214
+ 128,
2215
+ "invalid_track_refs",
2216
+ ),
2217
+ selectionReason: cleanText(
2218
+ entry.selectionReason,
2219
+ APPLE_DOMAIN_LIMITS.shortTextMax,
2220
+ "invalid_selection_reason",
2221
+ ),
2222
+ };
2223
+ });
2224
+ const selectedIds = selections.map((selection) => selection.trackRefId);
2225
+ if (new Set(selectedIds).size !== selectedIds.length) {
2226
+ fail("duplicate_track_ref", "A playlist plan cannot contain duplicate tracks.");
2227
+ }
2228
+ if (selectedIds.some((trackId) => !trustedTracks.has(trackId))) {
2229
+ fail(
2230
+ "untrusted_track_ref",
2231
+ "A playlist track was not returned by the trusted candidate sets.",
2232
+ );
2233
+ }
2234
+ if (
2235
+ !Number.isInteger(explicitRequestedCount) ||
2236
+ explicitRequestedCount < 1 ||
2237
+ explicitRequestedCount > APPLE_DOMAIN_LIMITS.playlistTracksMax
2238
+ ) {
2239
+ fail(
2240
+ "playlist_intent_count_unsupported",
2241
+ "The requested playlist size is outside the supported range.",
2242
+ );
2243
+ }
2244
+ const inferredRequestedCount = requestedTrackCount(cleanedIntent);
2245
+ if (
2246
+ inferredRequestedCount !== undefined &&
2247
+ inferredRequestedCount !== explicitRequestedCount
2248
+ ) {
2249
+ fail(
2250
+ "playlist_track_count_mismatch",
2251
+ "The playlist track count does not match the explicit user intent.",
2252
+ );
2253
+ }
2254
+ const requestedCount = explicitRequestedCount;
2255
+ if (requestedCount !== selections.length) {
2256
+ fail(
2257
+ "playlist_track_count_mismatch",
2258
+ "The playlist track count does not match the explicit user intent.",
2259
+ );
2260
+ }
2261
+ const tracks = selections.map((selection, index) => {
2262
+ const track = trustedTracks.get(selection.trackRefId);
2263
+ const result = {
2264
+ position: index + 1,
2265
+ track_ref_id: track.track_ref_id,
2266
+ title: track.title,
2267
+ artist_credit: track.artist_credit,
2268
+ release: track.release,
2269
+ candidate_scope: track.candidate_scope ?? "private_library",
2270
+ selection_reason: selection.selectionReason,
2271
+ };
2272
+ if (track.duration_ms !== undefined) result.duration_ms = track.duration_ms;
2273
+ if (isPlainObject(track.time_capsule)) {
2274
+ result.history_context = {
2275
+ kind: "time_capsule",
2276
+ year: track.time_capsule.year,
2277
+ };
2278
+ } else if (isPlainObject(track.historical_return)) {
2279
+ result.history_context = { kind: "historical_return" };
2280
+ } else if (isPlainObject(track.back_to_back)) {
2281
+ result.history_context = { kind: "back_to_back" };
2282
+ } else if (isPlainObject(track.rediscovery)) {
2283
+ result.history_context = { kind: "rediscovery" };
2284
+ }
2285
+ if (
2286
+ track.candidate_scope === "external_catalog" &&
2287
+ track.catalog_provider === "apple_music" &&
2288
+ track.catalog_url
2289
+ ) {
2290
+ result.public_catalog_reference = {
2291
+ provider: "apple_music",
2292
+ url: track.catalog_url,
2293
+ };
2294
+ }
2295
+ return result;
2296
+ });
2297
+ const selectedScopes = new Set(
2298
+ tracks.map((track) => track.candidate_scope),
2299
+ );
2300
+ assertExternalPlanDiversity(tracks, cleanedIntent);
2301
+ return {
2302
+ plan_version: "playlist_plan/0",
2303
+ intent: cleanedIntent,
2304
+ requested_track_count: requestedCount ?? null,
2305
+ track_count: tracks.length,
2306
+ tracks,
2307
+ candidate_scope:
2308
+ selectedScopes.size === 1 ? [...selectedScopes][0] : "mixed",
2309
+ ordering_rationale: orderingRationale,
2310
+ candidate_sets_validated: candidateSetIds.length,
2311
+ persistence: "none",
2312
+ external_effects: "none",
2313
+ };
2314
+ }
2315
+
2316
+ close() {
2317
+ this.resetCandidateSets();
2318
+ try {
2319
+ this.#projection.close();
2320
+ } finally {
2321
+ this.#listeningHistoryStore?.close?.();
2322
+ }
2323
+ }
2324
+ }
2325
+
2326
+ export function createAppleProjectionDomainServices({
2327
+ projection,
2328
+ listeningHistoryStore,
2329
+ subjectId,
2330
+ }) {
2331
+ return new AppleProjectionDomainServices({
2332
+ projection,
2333
+ listeningHistoryStore,
2334
+ subjectId,
2335
+ });
2336
+ }
2337
+
2338
+ export async function openAppleProjectionDomainServices({
2339
+ databasePath = resolveAppleMusicProjectionPath(),
2340
+ importsRoot = resolveAppleMusicImportsRoot(),
2341
+ subjectId,
2342
+ listeningHistoryStore,
2343
+ } = {}) {
2344
+ await assertProjectionExists(databasePath);
2345
+ const trustedScope = await trustedScopeFromImports(importsRoot, subjectId);
2346
+ const projection = await openAppleMusicSqliteProjection({
2347
+ databasePath,
2348
+ subjectId: trustedScope.subjectId,
2349
+ expectedInputs: trustedScope.inputs,
2350
+ });
2351
+ return createAppleProjectionDomainServices({
2352
+ projection,
2353
+ listeningHistoryStore,
2354
+ subjectId: trustedScope.subjectId,
2355
+ });
2356
+ }