@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,222 @@
|
|
|
1
|
+
const uuidPattern =
|
|
2
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
3
|
+
const recoverableCatalogStates = new Set(["ambiguous_artist", "not_found"]);
|
|
4
|
+
|
|
5
|
+
function fail(message) {
|
|
6
|
+
throw new Error(message);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cleanText(value, maximum, field) {
|
|
14
|
+
if (typeof value !== "string") fail(`The public ${field} is invalid.`);
|
|
15
|
+
const cleaned = value
|
|
16
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
|
|
17
|
+
.replace(/[\u202a-\u202e\u2066-\u2069]/gu, "")
|
|
18
|
+
.replace(/\s+/gu, " ")
|
|
19
|
+
.trim();
|
|
20
|
+
if (!cleaned || Array.from(cleaned).length > maximum) {
|
|
21
|
+
fail(`The public ${field} is invalid.`);
|
|
22
|
+
}
|
|
23
|
+
return cleaned;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeForMatch(value) {
|
|
27
|
+
return String(value)
|
|
28
|
+
.normalize("NFKC")
|
|
29
|
+
.toLocaleLowerCase("und")
|
|
30
|
+
.replace(/["'‘’“”`]/gu, "")
|
|
31
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function boundedDistinctIds(
|
|
36
|
+
value,
|
|
37
|
+
{ field, itemField, maximum, pattern, minimum = 0, numeric = false },
|
|
38
|
+
) {
|
|
39
|
+
if (!Array.isArray(value) || value.length < minimum || value.length > maximum) {
|
|
40
|
+
fail(`The public ${field} are invalid.`);
|
|
41
|
+
}
|
|
42
|
+
const ids = value.map((id) => cleanText(id, 36, itemField));
|
|
43
|
+
if (
|
|
44
|
+
new Set(ids).size !== ids.length ||
|
|
45
|
+
ids.some((id) => !pattern.test(id))
|
|
46
|
+
) {
|
|
47
|
+
fail(`The public ${field} are invalid.`);
|
|
48
|
+
}
|
|
49
|
+
return ids.sort((left, right) =>
|
|
50
|
+
left.localeCompare(right, "en", numeric ? { numeric: true } : undefined),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function identityBase(state, candidateCount) {
|
|
55
|
+
return {
|
|
56
|
+
provider: "wikidata",
|
|
57
|
+
method: "exact_label_or_alias",
|
|
58
|
+
state,
|
|
59
|
+
candidate_count: candidateCount,
|
|
60
|
+
properties: ["P434", "P2850"],
|
|
61
|
+
license: "CC0",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function projectCrossCatalogArtistIdentity(value, artistName) {
|
|
66
|
+
if (!isPlainObject(value)) {
|
|
67
|
+
fail("The public cross-catalog artist identity is invalid.");
|
|
68
|
+
}
|
|
69
|
+
const requestedName = cleanText(artistName, 256, "artist name");
|
|
70
|
+
const returnedName = cleanText(
|
|
71
|
+
value.artist_name,
|
|
72
|
+
256,
|
|
73
|
+
"cross-catalog artist name",
|
|
74
|
+
);
|
|
75
|
+
if (normalizeForMatch(returnedName) !== normalizeForMatch(requestedName)) {
|
|
76
|
+
fail("The public cross-catalog artist identity is invalid.");
|
|
77
|
+
}
|
|
78
|
+
const state = cleanText(value.state, 64, "cross-catalog identity state");
|
|
79
|
+
if (!Array.isArray(value.candidates) || value.candidates.length > 8) {
|
|
80
|
+
fail("The public cross-catalog artist candidates are invalid.");
|
|
81
|
+
}
|
|
82
|
+
const base = identityBase(state, value.candidates.length);
|
|
83
|
+
if (state === "not_found" || state === "ambiguous") return base;
|
|
84
|
+
if (state !== "resolved" || value.candidates.length === 0) {
|
|
85
|
+
fail("The public cross-catalog identity state is invalid.");
|
|
86
|
+
}
|
|
87
|
+
const artistMbid = cleanText(
|
|
88
|
+
value.artist_mbid,
|
|
89
|
+
36,
|
|
90
|
+
"MusicBrainz artist identity",
|
|
91
|
+
).toLowerCase();
|
|
92
|
+
if (!uuidPattern.test(artistMbid)) {
|
|
93
|
+
fail("The public MusicBrainz artist identity is invalid.");
|
|
94
|
+
}
|
|
95
|
+
const wikidataIds = boundedDistinctIds(value.wikidata_ids, {
|
|
96
|
+
field: "Wikidata artist identities",
|
|
97
|
+
itemField: "Wikidata artist identity",
|
|
98
|
+
maximum: 8,
|
|
99
|
+
minimum: 1,
|
|
100
|
+
pattern: /^Q[1-9]\d{0,19}$/u,
|
|
101
|
+
numeric: true,
|
|
102
|
+
});
|
|
103
|
+
const appleMusicArtistIds = boundedDistinctIds(
|
|
104
|
+
value.apple_music_artist_ids,
|
|
105
|
+
{
|
|
106
|
+
field: "Apple Music artist identities",
|
|
107
|
+
itemField: "Apple Music artist identity",
|
|
108
|
+
maximum: 4,
|
|
109
|
+
pattern: /^[1-9]\d{0,19}$/u,
|
|
110
|
+
numeric: true,
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
const resolved = {
|
|
114
|
+
...base,
|
|
115
|
+
canonical_name: cleanText(
|
|
116
|
+
value.canonical_name,
|
|
117
|
+
256,
|
|
118
|
+
"cross-catalog canonical artist name",
|
|
119
|
+
),
|
|
120
|
+
musicbrainz_artist_id: artistMbid,
|
|
121
|
+
wikidata_ids: wikidataIds,
|
|
122
|
+
};
|
|
123
|
+
if (appleMusicArtistIds.length === 0) {
|
|
124
|
+
return { ...resolved, state: "apple_music_identity_missing" };
|
|
125
|
+
}
|
|
126
|
+
if (appleMusicArtistIds.length > 1) {
|
|
127
|
+
return {
|
|
128
|
+
...resolved,
|
|
129
|
+
state: "apple_music_identity_ambiguous",
|
|
130
|
+
apple_music_artist_ids: appleMusicArtistIds,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...resolved,
|
|
135
|
+
state: "resolved",
|
|
136
|
+
apple_music_artist_id: appleMusicArtistIds[0],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isWikidataProviderError(error) {
|
|
141
|
+
return (
|
|
142
|
+
error?.name === "OpenMusicSimilarityError" &&
|
|
143
|
+
error?.provider === "wikidata" &&
|
|
144
|
+
typeof error?.code === "string" &&
|
|
145
|
+
/^wikidata_[a-z0-9_]+$/u.test(error.code)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function withIdentity(result, identity) {
|
|
150
|
+
return {
|
|
151
|
+
...result,
|
|
152
|
+
cross_catalog_identity: identity,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function recoverArtistReleasesWithCrossCatalogIdentity({
|
|
157
|
+
artistName,
|
|
158
|
+
currentResult,
|
|
159
|
+
catalog,
|
|
160
|
+
identityResolver,
|
|
161
|
+
} = {}) {
|
|
162
|
+
if (!isPlainObject(currentResult)) {
|
|
163
|
+
fail("The public catalog result is invalid.");
|
|
164
|
+
}
|
|
165
|
+
if (
|
|
166
|
+
!recoverableCatalogStates.has(currentResult.state) ||
|
|
167
|
+
identityResolver === undefined ||
|
|
168
|
+
identityResolver === null
|
|
169
|
+
) {
|
|
170
|
+
return currentResult;
|
|
171
|
+
}
|
|
172
|
+
if (typeof identityResolver.resolveArtist !== "function") {
|
|
173
|
+
fail("The public cross-catalog artist resolver is unavailable.");
|
|
174
|
+
}
|
|
175
|
+
let identity;
|
|
176
|
+
try {
|
|
177
|
+
identity = projectCrossCatalogArtistIdentity(
|
|
178
|
+
await identityResolver.resolveArtist(artistName),
|
|
179
|
+
artistName,
|
|
180
|
+
);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (!isWikidataProviderError(error)) throw error;
|
|
183
|
+
return withIdentity(currentResult, identityBase("unavailable", 0));
|
|
184
|
+
}
|
|
185
|
+
if (identity.state !== "resolved") {
|
|
186
|
+
return withIdentity(currentResult, identity);
|
|
187
|
+
}
|
|
188
|
+
if (typeof catalog?.findArtistReleasesByCatalogId !== "function") {
|
|
189
|
+
fail("The public catalog identity lookup is unavailable.");
|
|
190
|
+
}
|
|
191
|
+
const linkedResult = await catalog.findArtistReleasesByCatalogId({
|
|
192
|
+
artistCatalogId: identity.apple_music_artist_id,
|
|
193
|
+
limit: 8,
|
|
194
|
+
});
|
|
195
|
+
if (!isPlainObject(linkedResult)) {
|
|
196
|
+
fail("The public catalog identity result is invalid.");
|
|
197
|
+
}
|
|
198
|
+
if (linkedResult.state === "not_found") {
|
|
199
|
+
return withIdentity(currentResult, {
|
|
200
|
+
...identity,
|
|
201
|
+
state: "apple_music_identity_not_found",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (
|
|
205
|
+
linkedResult.state !== "resolved" ||
|
|
206
|
+
!isPlainObject(linkedResult.artist) ||
|
|
207
|
+
String(linkedResult.artist.catalog_id) !== identity.apple_music_artist_id
|
|
208
|
+
) {
|
|
209
|
+
return withIdentity(currentResult, {
|
|
210
|
+
...identity,
|
|
211
|
+
state: "apple_music_identity_mismatch",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
...linkedResult,
|
|
216
|
+
query: {
|
|
217
|
+
artist_name: cleanText(artistName, 256, "artist name"),
|
|
218
|
+
},
|
|
219
|
+
selection_basis: "wikidata_exact_label_or_alias_cross_catalog",
|
|
220
|
+
cross_catalog_identity: identity,
|
|
221
|
+
};
|
|
222
|
+
}
|