@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,50 @@
|
|
|
1
|
+
import { cp, lstat, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { legacyAppleMusicImportsRoot, resolveAppleMusicImportsRoot } from "./apple-library-source-status.mjs";
|
|
5
|
+
import { resolveAppleMusicProjectionPath } from "./apple-projection-domain-services.mjs";
|
|
6
|
+
import { legacyAppleMusicProjectionPath } from "../importers/apple-music-library/index.mjs";
|
|
7
|
+
|
|
8
|
+
async function kind(target) {
|
|
9
|
+
try {
|
|
10
|
+
const metadata = await lstat(target);
|
|
11
|
+
return metadata.isSymbolicLink() ? "link" : metadata.isDirectory() ? "directory" : "file";
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error?.code === "ENOENT") return null;
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function copyOnce(from, to, expected) {
|
|
19
|
+
if ((await kind(from)) !== expected || (await kind(to)) !== null) return false;
|
|
20
|
+
await mkdir(path.dirname(to), { recursive: true, mode: 0o700 });
|
|
21
|
+
await cp(from, to, { recursive: true, errorOnExist: true, force: false, preserveTimestamps: true });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Earlier versions kept Apple Music data inside the source checkout. Copy it
|
|
27
|
+
* into the default private state directory once, leaving the original in
|
|
28
|
+
* place. Runs with an explicit state or Apple location are never touched, so
|
|
29
|
+
* isolated runs such as tests cannot pull private data into their directories.
|
|
30
|
+
*/
|
|
31
|
+
export async function migrateLegacyAppleMusicData(environment = process.env) {
|
|
32
|
+
const explicit = [
|
|
33
|
+
"MOONDOG_STATE_HOME",
|
|
34
|
+
"MOONDOG_CONFIG_HOME",
|
|
35
|
+
"MOONDOG_APPLE_IMPORTS_ROOT",
|
|
36
|
+
"MOONDOG_APPLE_PROJECTION_PATH",
|
|
37
|
+
].some((name) => environment[name]?.trim());
|
|
38
|
+
if (explicit) return { copied: [] };
|
|
39
|
+
const copied = [];
|
|
40
|
+
if (await copyOnce(legacyAppleMusicImportsRoot, resolveAppleMusicImportsRoot(environment), "directory")) {
|
|
41
|
+
copied.push("imports");
|
|
42
|
+
}
|
|
43
|
+
if (await copyOnce(legacyAppleMusicProjectionPath, resolveAppleMusicProjectionPath(environment), "file")) {
|
|
44
|
+
copied.push("projection");
|
|
45
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
46
|
+
await copyOnce(`${legacyAppleMusicProjectionPath}${suffix}`, `${resolveAppleMusicProjectionPath(environment)}${suffix}`, "file");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { copied };
|
|
50
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { resolveMoondogStateDirectory } from "./state-directory.mjs";
|
|
6
|
+
|
|
7
|
+
const repositoryRoot = path.resolve(
|
|
8
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
9
|
+
"../..",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
// Earlier versions kept Apple Music data inside the source checkout.
|
|
13
|
+
export const legacyAppleMusicImportsRoot = path.join(
|
|
14
|
+
repositoryRoot,
|
|
15
|
+
"data",
|
|
16
|
+
"imports",
|
|
17
|
+
"apple-music-library",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export function defaultAppleMusicImportsRootFor(environment = process.env) {
|
|
21
|
+
return path.join(resolveMoondogStateDirectory(environment), "apple-music-library", "imports");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const defaultAppleMusicImportsRoot = defaultAppleMusicImportsRootFor();
|
|
25
|
+
|
|
26
|
+
export function resolveAppleMusicImportsRoot(environment = process.env) {
|
|
27
|
+
const configured = environment.MOONDOG_APPLE_IMPORTS_ROOT?.trim();
|
|
28
|
+
if (!configured) return defaultAppleMusicImportsRootFor(environment);
|
|
29
|
+
if (!path.isAbsolute(configured)) {
|
|
30
|
+
throw new TypeError("MOONDOG_APPLE_IMPORTS_ROOT must be an absolute path");
|
|
31
|
+
}
|
|
32
|
+
return path.resolve(configured);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const uuidDirectoryPattern =
|
|
36
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
37
|
+
const maximumManifestBytes = 256 * 1024;
|
|
38
|
+
|
|
39
|
+
function missingStatus() {
|
|
40
|
+
return {
|
|
41
|
+
source: "apple_music_library_xml",
|
|
42
|
+
state: "missing",
|
|
43
|
+
valid_batches: 0,
|
|
44
|
+
invalid_batches: 0,
|
|
45
|
+
latest: null,
|
|
46
|
+
semantics: {
|
|
47
|
+
library_snapshot_available: false,
|
|
48
|
+
complete_listening_history_available: false,
|
|
49
|
+
profile_materialization_ready: false,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isNonNegativeInteger(value) {
|
|
55
|
+
return Number.isInteger(value) && value >= 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requireCount(record, key) {
|
|
59
|
+
const value = record?.[key];
|
|
60
|
+
if (!isNonNegativeInteger(value)) {
|
|
61
|
+
throw new Error("manifest_count_invalid");
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sanitizeManifest(manifest) {
|
|
67
|
+
if (
|
|
68
|
+
!manifest ||
|
|
69
|
+
manifest.schema_version !== "apple-music-library-import-manifest/1"
|
|
70
|
+
) {
|
|
71
|
+
throw new Error("manifest_schema_invalid");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const capturedAt = manifest.source?.captured_at;
|
|
75
|
+
if (
|
|
76
|
+
typeof capturedAt !== "string" ||
|
|
77
|
+
!Number.isFinite(Date.parse(capturedAt))
|
|
78
|
+
) {
|
|
79
|
+
throw new Error("manifest_timestamp_invalid");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
typeof manifest.subject_id !== "string" ||
|
|
84
|
+
!uuidDirectoryPattern.test(manifest.subject_id) ||
|
|
85
|
+
manifest.semantics?.source_is_library_snapshot !== true ||
|
|
86
|
+
manifest.semantics?.source_is_complete_listening_history !== false ||
|
|
87
|
+
manifest.semantics?.aggregate_counts_expanded_into_events !== false ||
|
|
88
|
+
manifest.semantics?.explicit_states_projected_into_taste_events !== false
|
|
89
|
+
) {
|
|
90
|
+
throw new Error("manifest_semantics_invalid");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
subject_id: manifest.subject_id.toLowerCase(),
|
|
95
|
+
captured_at: new Date(capturedAt).toISOString(),
|
|
96
|
+
tracks: requireCount(manifest.counts, "source_tracks"),
|
|
97
|
+
playlists: requireCount(manifest.counts, "source_playlists"),
|
|
98
|
+
playlist_item_references: requireCount(
|
|
99
|
+
manifest.counts,
|
|
100
|
+
"source_playlist_item_references",
|
|
101
|
+
),
|
|
102
|
+
aggregate_track_snapshots: requireCount(
|
|
103
|
+
manifest.counts,
|
|
104
|
+
"aggregate_track_snapshots",
|
|
105
|
+
),
|
|
106
|
+
listening_events: requireCount(
|
|
107
|
+
manifest.counts,
|
|
108
|
+
"core_listening_events",
|
|
109
|
+
),
|
|
110
|
+
taste_events: requireCount(manifest.counts, "core_taste_events"),
|
|
111
|
+
loved_or_favorited_coverage: requireCount(
|
|
112
|
+
manifest.coverage,
|
|
113
|
+
"loved_or_favorited",
|
|
114
|
+
),
|
|
115
|
+
aggregate_play_count_coverage: requireCount(
|
|
116
|
+
manifest.coverage,
|
|
117
|
+
"aggregate_play_count",
|
|
118
|
+
),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function readPrivateManifest(batchDirectory) {
|
|
123
|
+
const directoryMetadata = await lstat(batchDirectory);
|
|
124
|
+
if (!directoryMetadata.isDirectory() || directoryMetadata.isSymbolicLink()) {
|
|
125
|
+
throw new Error("batch_directory_invalid");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const manifestPath = path.join(batchDirectory, "manifest.json");
|
|
129
|
+
const manifestMetadata = await lstat(manifestPath);
|
|
130
|
+
if (
|
|
131
|
+
!manifestMetadata.isFile() ||
|
|
132
|
+
manifestMetadata.isSymbolicLink() ||
|
|
133
|
+
manifestMetadata.size > maximumManifestBytes
|
|
134
|
+
) {
|
|
135
|
+
throw new Error("manifest_file_invalid");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return sanitizeManifest(JSON.parse(await readFile(manifestPath, "utf8")));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function readAppleMusicSourceStatus(
|
|
142
|
+
importsRoot = resolveAppleMusicImportsRoot(),
|
|
143
|
+
operations = { lstat, readdir },
|
|
144
|
+
) {
|
|
145
|
+
let rootMetadata;
|
|
146
|
+
try {
|
|
147
|
+
rootMetadata = await operations.lstat(importsRoot);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error?.code === "ENOENT") return missingStatus();
|
|
150
|
+
return { ...missingStatus(), state: "invalid", error: "source_unreadable" };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) {
|
|
154
|
+
return { ...missingStatus(), state: "invalid", error: "source_root_invalid" };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let entries;
|
|
158
|
+
try {
|
|
159
|
+
entries = await operations.readdir(importsRoot, { withFileTypes: true });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error?.code === "ENOENT") return missingStatus();
|
|
162
|
+
return { ...missingStatus(), state: "invalid", error: "source_unreadable" };
|
|
163
|
+
}
|
|
164
|
+
const candidateNames = entries
|
|
165
|
+
.filter((entry) => uuidDirectoryPattern.test(entry.name))
|
|
166
|
+
.map((entry) => entry.name)
|
|
167
|
+
.sort();
|
|
168
|
+
|
|
169
|
+
const valid = [];
|
|
170
|
+
let invalidBatches = 0;
|
|
171
|
+
|
|
172
|
+
for (const candidateName of candidateNames) {
|
|
173
|
+
try {
|
|
174
|
+
valid.push(
|
|
175
|
+
await readPrivateManifest(path.join(importsRoot, candidateName)),
|
|
176
|
+
);
|
|
177
|
+
} catch {
|
|
178
|
+
invalidBatches += 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
valid.sort((left, right) =>
|
|
183
|
+
left.captured_at.localeCompare(right.captured_at),
|
|
184
|
+
);
|
|
185
|
+
const latest = valid.at(-1) ?? null;
|
|
186
|
+
|
|
187
|
+
if (new Set(valid.map((manifest) => manifest.subject_id)).size > 1) {
|
|
188
|
+
return {
|
|
189
|
+
...missingStatus(),
|
|
190
|
+
state: "invalid",
|
|
191
|
+
valid_batches: valid.length,
|
|
192
|
+
invalid_batches: invalidBatches,
|
|
193
|
+
error: "multiple_subjects",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!latest) {
|
|
198
|
+
return {
|
|
199
|
+
...missingStatus(),
|
|
200
|
+
state: invalidBatches > 0 ? "invalid" : "missing",
|
|
201
|
+
invalid_batches: invalidBatches,
|
|
202
|
+
...(invalidBatches > 0 ? { error: "no_valid_batches" } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { subject_id: ignoredSubjectId, ...safeLatest } = latest;
|
|
207
|
+
void ignoredSubjectId;
|
|
208
|
+
return {
|
|
209
|
+
source: "apple_music_library_xml",
|
|
210
|
+
state: invalidBatches > 0 ? "degraded" : "ready",
|
|
211
|
+
valid_batches: valid.length,
|
|
212
|
+
invalid_batches: invalidBatches,
|
|
213
|
+
latest: safeLatest,
|
|
214
|
+
semantics: {
|
|
215
|
+
library_snapshot_available: true,
|
|
216
|
+
complete_listening_history_available: false,
|
|
217
|
+
profile_materialization_ready: false,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|