@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.
- package/README.md +125 -0
- package/assets/brand/moondog-lunar-record/moondog-logo-1x1.png +0 -0
- package/contracts/v1/capability-policy.schema.json +572 -0
- package/contracts/v1/definitions.schema.json +365 -0
- package/contracts/v1/library-track-observation.schema.json +138 -0
- package/contracts/v1/listening-event.schema.json +111 -0
- package/contracts/v1/profile-evidence.schema.json +237 -0
- package/contracts/v1/taste-event.schema.json +258 -0
- package/contracts/v1/tool-invocation.schema.json +318 -0
- package/contracts/v1/tool-receipt.schema.json +1066 -0
- package/contracts/v1/track-ref.schema.json +275 -0
- package/contracts/v2/profile-evidence.schema.json +284 -0
- package/package.json +105 -0
- package/scripts/contract-lib.mjs +361 -0
- package/scripts/contract-semantics.mjs +2630 -0
- package/scripts/demo-studio.mjs +58 -0
- package/scripts/moondog +52 -0
- package/scripts/moondog.mjs +954 -0
- package/src/core/apple-data-migration.mjs +50 -0
- package/src/core/apple-library-source-status.mjs +220 -0
- package/src/core/apple-projection-domain-services.mjs +2356 -0
- package/src/core/capability-catalog.mjs +577 -0
- package/src/core/listening-profile-domain-services.mjs +1010 -0
- package/src/core/lyric-library.mjs +121 -0
- package/src/core/lyric-profile.mjs +50 -0
- package/src/core/lyric-service.mjs +41 -0
- package/src/core/moondog-application.mjs +1827 -0
- package/src/core/state-directory.mjs +19 -0
- package/src/core/synthetic-domain-services.mjs +1485 -0
- package/src/core/uuid-v5.mjs +42 -0
- package/src/demo/fictional-spotify-history.mjs +400 -0
- package/src/demo/moondog-demo.mjs +355 -0
- package/src/demo/moondog-interactive-tasteprint-demo.mjs +293 -0
- package/src/demo/moondog-tasteprint-demo.mjs +631 -0
- package/src/importers/apple-music-library/import-batch.mjs +290 -0
- package/src/importers/apple-music-library/index.mjs +76 -0
- package/src/importers/apple-music-library/normalize.mjs +1126 -0
- package/src/importers/apple-music-library/parse-plist.mjs +357 -0
- package/src/importers/apple-music-library/projection-records.mjs +557 -0
- package/src/importers/apple-music-library/read-batch.mjs +276 -0
- package/src/importers/apple-music-library/sqlite-projection.mjs +1307 -0
- package/src/importers/apple-music-library/stable-ids.mjs +19 -0
- package/src/importers/apple-music-library/track-snapshot.schema.json +136 -0
- package/src/integrations/apple-music/catalog.mjs +770 -0
- package/src/integrations/cross-catalog-artist-identity.mjs +222 -0
- package/src/integrations/listenbrainz/history-file.mjs +560 -0
- package/src/integrations/lyrics/lrclib.mjs +55 -0
- package/src/integrations/open-music-similarity/artist-radio.mjs +806 -0
- package/src/integrations/public-playlists.mjs +104 -0
- package/src/integrations/spotify/account-data-history.mjs +545 -0
- package/src/integrations/spotify/account-data-profile.mjs +918 -0
- package/src/integrations/spotify/authentication.mjs +791 -0
- package/src/integrations/spotify/catalog-resolver.mjs +657 -0
- package/src/integrations/spotify/connection.mjs +125 -0
- package/src/integrations/spotify/credential-store.mjs +26 -0
- package/src/integrations/spotify/extended-streaming-history.mjs +525 -0
- package/src/integrations/spotify/history-archive.mjs +59 -0
- package/src/integrations/spotify/index.mjs +80 -0
- package/src/integrations/spotify/recent-activity.mjs +198 -0
- package/src/integrations/spotify/resolution-cache.mjs +260 -0
- package/src/integrations/spotify/service.mjs +713 -0
- package/src/integrations/spotify/settings.mjs +89 -0
- package/src/integrations/spotify/web-api-client.mjs +761 -0
- package/src/integrations/web/codex-web.mjs +263 -0
- package/src/integrations/youtube-music/takeout.mjs +143 -0
- package/src/memory/local-memory-store.mjs +1705 -0
- package/src/memory/reflection-worker.mjs +87 -0
- package/src/profile/apple-library-import.mjs +28 -0
- package/src/profile/history-import.mjs +163 -0
- package/src/profile/listener-corrections.mjs +217 -0
- package/src/profile/listening-history-store.mjs +2055 -0
- package/src/profile/listening-profile-projection.mjs +2520 -0
- package/src/profile/local-music-data.mjs +812 -0
- package/src/profile/music-import-bundle.mjs +91 -0
- package/src/profile/music-providers.mjs +25 -0
- package/src/profile/spotify-archive-catalog-hints.mjs +210 -0
- package/src/profile/spotify-archive-taste.mjs +482 -0
- package/src/runtime/pi/agent-runtime.mjs +6444 -0
- package/src/runtime/pi/authentication.mjs +104 -0
- package/src/runtime/pi/configured-runtime.mjs +98 -0
- package/src/runtime/pi/memory-agent-runtime.mjs +337 -0
- package/src/runtime/pi/model-catalog.mjs +62 -0
- package/src/runtime/pi/model-transport.mjs +117 -0
- package/src/runtime/pi/persistent-credential-store.mjs +670 -0
- package/src/runtime/pi/provider-registry.mjs +74 -0
- package/src/runtime/pi/runtime-settings.mjs +81 -0
- package/src/surfaces/cli/auth-command.mjs +355 -0
- package/src/surfaces/cli/brand-components.mjs +390 -0
- package/src/surfaces/cli/brand-theme.mjs +231 -0
- package/src/surfaces/cli/catalog-command.mjs +509 -0
- package/src/surfaces/cli/command-completions.mjs +113 -0
- package/src/surfaces/cli/data-command.mjs +214 -0
- package/src/surfaces/cli/format-output.mjs +750 -0
- package/src/surfaces/cli/history-import-view.mjs +445 -0
- package/src/surfaces/cli/import-guides.mjs +9 -0
- package/src/surfaces/cli/listenbrainz-command.mjs +129 -0
- package/src/surfaces/cli/listening-menu.mjs +171 -0
- package/src/surfaces/cli/profile-command.mjs +290 -0
- package/src/surfaces/cli/spotify-command.mjs +849 -0
- package/src/surfaces/cli/taste-profile-model.mjs +306 -0
- package/src/surfaces/cli/taste-profile-view.mjs +272 -0
- package/src/surfaces/cli/terminal-art.mjs +295 -0
- package/src/surfaces/cli/tui.mjs +1714 -0
- package/src/surfaces/cli/web-command.mjs +52 -0
- package/src/surfaces/html/tasteprint-artifact.mjs +96 -0
- package/src/surfaces/html/tasteprint.mjs +3575 -0
- package/src/surfaces/web/showcase.mjs +272 -0
- package/src/surfaces/web/studio-page.mjs +4148 -0
- 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
|
+
}
|