fluncle 0.184.0 → 0.186.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/bin/fluncle.mjs +375 -54
- package/package.json +1 -1
package/bin/fluncle.mjs
CHANGED
|
@@ -561,7 +561,7 @@ function parseVersion(version) {
|
|
|
561
561
|
var currentVersion;
|
|
562
562
|
var init_version = __esm(() => {
|
|
563
563
|
init_output();
|
|
564
|
-
currentVersion = "0.
|
|
564
|
+
currentVersion = "0.186.0".trim() ? "0.186.0".trim() : "0.1.0";
|
|
565
565
|
});
|
|
566
566
|
|
|
567
567
|
// src/update-notifier.ts
|
|
@@ -1066,22 +1066,207 @@ var init_fresh = __esm(() => {
|
|
|
1066
1066
|
init_output();
|
|
1067
1067
|
});
|
|
1068
1068
|
|
|
1069
|
+
// src/commands/entity-browse.ts
|
|
1070
|
+
function entityRow(item) {
|
|
1071
|
+
const count = `${item.trackCount} track${item.trackCount === 1 ? "" : "s"}`;
|
|
1072
|
+
return `${item.name.padEnd(NAME_WIDTH)} ${count}`;
|
|
1073
|
+
}
|
|
1074
|
+
function printEntityIndex(items, page, noun, command) {
|
|
1075
|
+
if (items.length === 0) {
|
|
1076
|
+
if (page.total === 0) {
|
|
1077
|
+
console.log(`No ${noun.plural} in the archive yet.`);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
console.log(`Nothing on page ${page.page}. The archive holds ${page.total} ${noun.plural} across ${page.pageCount} pages.`);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
for (const item of items) {
|
|
1084
|
+
console.log(entityRow(item));
|
|
1085
|
+
}
|
|
1086
|
+
if (page.pageCount > 1) {
|
|
1087
|
+
const more = page.page < page.pageCount ? ` More with: fluncle ${command} --page ${page.page + 1}` : "";
|
|
1088
|
+
console.log("");
|
|
1089
|
+
console.log(`Page ${page.page} of ${page.pageCount}, ${page.total} ${noun.plural}.${more}`);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function entityDetailLines(name, slug, trackCount, findingCount) {
|
|
1093
|
+
return [`${name} (${slug})`, `Tracks: ${trackCount}`, `Findings: ${findingCount}`];
|
|
1094
|
+
}
|
|
1095
|
+
var NAME_WIDTH = 40;
|
|
1096
|
+
|
|
1097
|
+
// src/commands/albums.ts
|
|
1098
|
+
var exports_albums = {};
|
|
1099
|
+
__export(exports_albums, {
|
|
1100
|
+
albumsCommand: () => albumsCommand
|
|
1101
|
+
});
|
|
1102
|
+
async function albumsCommand({
|
|
1103
|
+
json,
|
|
1104
|
+
page,
|
|
1105
|
+
slug
|
|
1106
|
+
}) {
|
|
1107
|
+
if (slug) {
|
|
1108
|
+
const response2 = await publicApiGet(`/api/v1/albums/${encodeURIComponent(slug)}`);
|
|
1109
|
+
if (json) {
|
|
1110
|
+
printJson2(response2);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const { album } = response2;
|
|
1114
|
+
const lines = entityDetailLines(album.name, album.slug, album.trackCount, album.findingCount);
|
|
1115
|
+
if (album.releaseDate) {
|
|
1116
|
+
lines.push(`Released: ${album.releaseDate.slice(0, 10)}`);
|
|
1117
|
+
}
|
|
1118
|
+
console.log(lines.join(`
|
|
1119
|
+
`));
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const response = await publicApiGet(`/api/v1/albums?page=${page}`);
|
|
1123
|
+
if (json) {
|
|
1124
|
+
printJson2(response);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
printEntityIndex(response.albums, response, { plural: "albums", singular: "album" }, "albums");
|
|
1128
|
+
}
|
|
1129
|
+
var init_albums = __esm(() => {
|
|
1130
|
+
init_api();
|
|
1131
|
+
init_output();
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1069
1134
|
// src/commands/artists.ts
|
|
1070
1135
|
var exports_artists = {};
|
|
1071
1136
|
__export(exports_artists, {
|
|
1072
|
-
|
|
1073
|
-
artistsGetCommand: () => artistsGetCommand
|
|
1137
|
+
artistsCommand: () => artistsCommand
|
|
1074
1138
|
});
|
|
1075
|
-
async function
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1139
|
+
async function artistsCommand({
|
|
1140
|
+
json,
|
|
1141
|
+
page,
|
|
1142
|
+
slug
|
|
1143
|
+
}) {
|
|
1144
|
+
if (slug) {
|
|
1145
|
+
const response2 = await publicApiGet(`/api/v1/artists/${encodeURIComponent(slug)}`);
|
|
1146
|
+
if (json) {
|
|
1147
|
+
printJson2(response2);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const { artist } = response2;
|
|
1151
|
+
const lines = entityDetailLines(artist.name, artist.slug, artist.trackCount, artist.findingCount);
|
|
1152
|
+
if (artist.spotifyUrl) {
|
|
1153
|
+
lines.push(`Spotify: ${artist.spotifyUrl}`);
|
|
1154
|
+
}
|
|
1155
|
+
console.log(lines.join(`
|
|
1156
|
+
`));
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const response = await publicApiGet(`/api/v1/artists?page=${page}`);
|
|
1160
|
+
if (json) {
|
|
1161
|
+
printJson2(response);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
printEntityIndex(response.artists, response, { plural: "artists", singular: "artist" }, "artists");
|
|
1082
1165
|
}
|
|
1083
1166
|
var init_artists = __esm(() => {
|
|
1084
1167
|
init_api();
|
|
1168
|
+
init_output();
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
// src/commands/labels.ts
|
|
1172
|
+
var exports_labels = {};
|
|
1173
|
+
__export(exports_labels, {
|
|
1174
|
+
labelsCommand: () => labelsCommand
|
|
1175
|
+
});
|
|
1176
|
+
async function labelsCommand({
|
|
1177
|
+
json,
|
|
1178
|
+
page,
|
|
1179
|
+
slug
|
|
1180
|
+
}) {
|
|
1181
|
+
if (slug) {
|
|
1182
|
+
const response2 = await publicApiGet(`/api/v1/labels/${encodeURIComponent(slug)}`);
|
|
1183
|
+
if (json) {
|
|
1184
|
+
printJson2(response2);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const { label } = response2;
|
|
1188
|
+
const lines = entityDetailLines(label.name, label.slug, label.trackCount, label.findingCount);
|
|
1189
|
+
if (label.foundedLocation) {
|
|
1190
|
+
lines.push(`Based: ${label.foundedLocation}`);
|
|
1191
|
+
}
|
|
1192
|
+
if (label.parentLabel) {
|
|
1193
|
+
lines.push(`Imprint of: ${label.parentLabel.name}`);
|
|
1194
|
+
}
|
|
1195
|
+
console.log(lines.join(`
|
|
1196
|
+
`));
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const response = await publicApiGet(`/api/v1/labels?page=${page}`);
|
|
1200
|
+
if (json) {
|
|
1201
|
+
printJson2(response);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
printEntityIndex(response.labels, response, { plural: "labels", singular: "label" }, "labels");
|
|
1205
|
+
}
|
|
1206
|
+
var init_labels = __esm(() => {
|
|
1207
|
+
init_api();
|
|
1208
|
+
init_output();
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// src/links.ts
|
|
1212
|
+
var spotifyPlaylistUrl2 = "https://open.spotify.com/playlist/1m5LADqpLjiBERdtqrIiL0", telegramUrl2 = "https://t.me/fluncle", webBaseUrl = "https://www.fluncle.com";
|
|
1213
|
+
|
|
1214
|
+
// src/commands/search.ts
|
|
1215
|
+
var exports_search = {};
|
|
1216
|
+
__export(exports_search, {
|
|
1217
|
+
searchCommand: () => searchCommand
|
|
1218
|
+
});
|
|
1219
|
+
function entityLine(entity) {
|
|
1220
|
+
const kind = `${entity.kind[0]?.toUpperCase() ?? ""}${entity.kind.slice(1)}`;
|
|
1221
|
+
return `${kind} ${entity.name} ${webBaseUrl}/${entity.kind}/${entity.slug}`;
|
|
1222
|
+
}
|
|
1223
|
+
function trackTable(hits) {
|
|
1224
|
+
const coordWidth = hits.reduce((width, hit) => {
|
|
1225
|
+
return Math.max(width, (hit.logId ?? COORD_FALLBACK2).length);
|
|
1226
|
+
}, 0);
|
|
1227
|
+
return hits.map((hit) => {
|
|
1228
|
+
const coordinate2 = (hit.logId ?? COORD_FALLBACK2).padEnd(coordWidth);
|
|
1229
|
+
return `${coordinate2} ${hit.artists.join(", ")} — ${hit.title}`;
|
|
1230
|
+
}).join(`
|
|
1231
|
+
`);
|
|
1232
|
+
}
|
|
1233
|
+
async function searchCommand({
|
|
1234
|
+
json,
|
|
1235
|
+
limit,
|
|
1236
|
+
query
|
|
1237
|
+
}) {
|
|
1238
|
+
const params = new URLSearchParams({ q: query });
|
|
1239
|
+
if (limit !== undefined) {
|
|
1240
|
+
params.set("limit", String(limit));
|
|
1241
|
+
}
|
|
1242
|
+
const response = await publicApiGet(`/api/v1/search/archive?${params.toString()}`);
|
|
1243
|
+
if (json) {
|
|
1244
|
+
printJson2(response);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const blocks = [];
|
|
1248
|
+
if (response.redirect) {
|
|
1249
|
+
blocks.push(`Jump to ${webBaseUrl}${response.redirect}`);
|
|
1250
|
+
}
|
|
1251
|
+
if (response.entities.length > 0) {
|
|
1252
|
+
blocks.push(response.entities.map(entityLine).join(`
|
|
1253
|
+
`));
|
|
1254
|
+
}
|
|
1255
|
+
if (response.results.length > 0) {
|
|
1256
|
+
blocks.push(trackTable(response.results));
|
|
1257
|
+
}
|
|
1258
|
+
if (blocks.length === 0) {
|
|
1259
|
+
console.log(`Nothing in the archive matches "${query}".`);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
console.log(blocks.join(`
|
|
1263
|
+
|
|
1264
|
+
`));
|
|
1265
|
+
}
|
|
1266
|
+
var COORD_FALLBACK2 = "—";
|
|
1267
|
+
var init_search = __esm(() => {
|
|
1268
|
+
init_api();
|
|
1269
|
+
init_output();
|
|
1085
1270
|
});
|
|
1086
1271
|
|
|
1087
1272
|
// src/commands/galaxies.ts
|
|
@@ -1603,12 +1788,12 @@ var init_mixtapes = __esm(() => {
|
|
|
1603
1788
|
|
|
1604
1789
|
// src/format.ts
|
|
1605
1790
|
function coordinate2(track) {
|
|
1606
|
-
return track.logId ??
|
|
1791
|
+
return track.logId ?? COORD_FALLBACK3;
|
|
1607
1792
|
}
|
|
1608
1793
|
function artistTitle(track) {
|
|
1609
1794
|
return `${track.artists.join(", ")} — ${track.title}`;
|
|
1610
1795
|
}
|
|
1611
|
-
var
|
|
1796
|
+
var COORD_FALLBACK3 = "—";
|
|
1612
1797
|
var init_format = __esm(() => {
|
|
1613
1798
|
init_util();
|
|
1614
1799
|
});
|
|
@@ -1715,9 +1900,6 @@ var init_interactive = __esm(() => {
|
|
|
1715
1900
|
init_output();
|
|
1716
1901
|
});
|
|
1717
1902
|
|
|
1718
|
-
// src/links.ts
|
|
1719
|
-
var spotifyPlaylistUrl2 = "https://open.spotify.com/playlist/1m5LADqpLjiBERdtqrIiL0", telegramUrl2 = "https://t.me/fluncle";
|
|
1720
|
-
|
|
1721
1903
|
// src/open-external.ts
|
|
1722
1904
|
async function openExternal(target) {
|
|
1723
1905
|
const command = platformOpenCommand();
|
|
@@ -2260,7 +2442,7 @@ function parseVersion2(version) {
|
|
|
2260
2442
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2261
2443
|
var init_version2 = __esm(() => {
|
|
2262
2444
|
init_output();
|
|
2263
|
-
currentVersion2 = "0.
|
|
2445
|
+
currentVersion2 = "0.186.0".trim() ? "0.186.0".trim() : "0.1.0";
|
|
2264
2446
|
});
|
|
2265
2447
|
|
|
2266
2448
|
// ../../packages/registry/src/index.ts
|
|
@@ -2633,8 +2815,22 @@ var init_src = __esm(() => {
|
|
|
2633
2815
|
apiFormat: "application/json",
|
|
2634
2816
|
discoveryUrl: `${SITE}/api/v1/openapi.json`,
|
|
2635
2817
|
exposedContent: [
|
|
2636
|
-
"every
|
|
2637
|
-
"/api/v1/
|
|
2818
|
+
"every album in the archive, alphabetical, paginated, as JSON",
|
|
2819
|
+
"/api/v1/albums/{slug} — one album's identity, cover, and counts, as JSON"
|
|
2820
|
+
],
|
|
2821
|
+
kind: "api",
|
|
2822
|
+
name: "api.albums",
|
|
2823
|
+
probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
|
|
2824
|
+
route: "/api/v1/albums",
|
|
2825
|
+
url: `${SITE}/api/v1/albums`,
|
|
2826
|
+
weights: { web: "secondary" }
|
|
2827
|
+
},
|
|
2828
|
+
{
|
|
2829
|
+
apiFormat: "application/json",
|
|
2830
|
+
discoveryUrl: `${SITE}/api/v1/openapi.json`,
|
|
2831
|
+
exposedContent: [
|
|
2832
|
+
"every artist in the archive, alphabetical, paginated, as JSON",
|
|
2833
|
+
"/api/v1/artists/{slug} — one artist's identity, finding count, and track count, as JSON"
|
|
2638
2834
|
],
|
|
2639
2835
|
kind: "api",
|
|
2640
2836
|
name: "api.artists",
|
|
@@ -2643,6 +2839,20 @@ var init_src = __esm(() => {
|
|
|
2643
2839
|
url: `${SITE}/api/v1/artists`,
|
|
2644
2840
|
weights: { web: "secondary" }
|
|
2645
2841
|
},
|
|
2842
|
+
{
|
|
2843
|
+
apiFormat: "application/json",
|
|
2844
|
+
discoveryUrl: `${SITE}/api/v1/openapi.json`,
|
|
2845
|
+
exposedContent: [
|
|
2846
|
+
"every label in the archive, alphabetical, paginated, as JSON",
|
|
2847
|
+
"/api/v1/labels/{slug} — one label's identity, lineage, and counts, as JSON"
|
|
2848
|
+
],
|
|
2849
|
+
kind: "api",
|
|
2850
|
+
name: "api.labels",
|
|
2851
|
+
probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
|
|
2852
|
+
route: "/api/v1/labels",
|
|
2853
|
+
url: `${SITE}/api/v1/labels`,
|
|
2854
|
+
weights: { web: "secondary" }
|
|
2855
|
+
},
|
|
2646
2856
|
{
|
|
2647
2857
|
apiFormat: "application/json",
|
|
2648
2858
|
discoveryUrl: `${SITE}/api/v1/openapi.json`,
|
|
@@ -2920,12 +3130,12 @@ var init_src = __esm(() => {
|
|
|
2920
3130
|
{
|
|
2921
3131
|
apiFormat: "application/json+oembed",
|
|
2922
3132
|
exposedContent: [
|
|
2923
|
-
"the oEmbed 1.0 provider — a pasted /log, /
|
|
3133
|
+
"the oEmbed 1.0 provider — a pasted /log link unfurls as a rich finding card, and a /artist, /label, /album, or /mixtapes link as a link card (Discord/Notion/WordPress/Ghost/…)",
|
|
2924
3134
|
"html iframes the self-contained /embed/<logId> card; thumbnail is the finding's OG image"
|
|
2925
3135
|
],
|
|
2926
3136
|
kind: "discovery",
|
|
2927
3137
|
name: "discovery.oembed",
|
|
2928
|
-
operatorNotes: 'GET /oembed?url=<a fluncle.com /log|/mixtapes|/artist URL>&format=json.
|
|
3138
|
+
operatorNotes: 'GET /oembed?url=<a fluncle.com /log|/mixtapes|/artist|/label|/album URL>&format=json — the provider resolves each. The auto-discovery `<link rel="alternate" type="application/json+oembed">` rides the /log, mixtape, and /artist heads today; a consumer unfurls a /label or /album link by hitting the provider directly (those page heads can advertise it later). XML → 501 (JSON only). No probeConfig — a bare GET without a valid `url` param is a 404, so there is no fixed URL to health-probe (like web.artist). The `rich` html frames /embed/<logId> (apps/web/src/routes/embed.$logId.ts), a standalone dark card served with a permissive `frame-ancestors *` CSP scoped to that route. Source: apps/web/src/routes/oembed.ts + src/lib/oembed.ts.',
|
|
2929
3139
|
route: "/oembed",
|
|
2930
3140
|
url: `${SITE}/oembed`,
|
|
2931
3141
|
weights: { web: "tertiary" }
|
|
@@ -2994,11 +3204,32 @@ var init_src = __esm(() => {
|
|
|
2994
3204
|
},
|
|
2995
3205
|
{
|
|
2996
3206
|
command: "fluncle artists",
|
|
2997
|
-
exposedContent: ["every artist
|
|
3207
|
+
exposedContent: ["every artist Fluncle holds, A to Z (bare `slug` looks one up)"],
|
|
2998
3208
|
kind: "cli",
|
|
2999
3209
|
name: "cli.artists",
|
|
3000
3210
|
weights: { cli: "secondary" }
|
|
3001
3211
|
},
|
|
3212
|
+
{
|
|
3213
|
+
command: "fluncle albums",
|
|
3214
|
+
exposedContent: ["every album Fluncle holds, A to Z (bare `slug` looks one up)"],
|
|
3215
|
+
kind: "cli",
|
|
3216
|
+
name: "cli.albums",
|
|
3217
|
+
weights: { cli: "secondary" }
|
|
3218
|
+
},
|
|
3219
|
+
{
|
|
3220
|
+
command: "fluncle labels",
|
|
3221
|
+
exposedContent: ["every label Fluncle holds, A to Z (bare `slug` looks one up)"],
|
|
3222
|
+
kind: "cli",
|
|
3223
|
+
name: "cli.labels",
|
|
3224
|
+
weights: { cli: "secondary" }
|
|
3225
|
+
},
|
|
3226
|
+
{
|
|
3227
|
+
command: "fluncle search",
|
|
3228
|
+
exposedContent: ["search the archive by coordinate, track, artist, label, or album"],
|
|
3229
|
+
kind: "cli",
|
|
3230
|
+
name: "cli.search",
|
|
3231
|
+
weights: { cli: "secondary" }
|
|
3232
|
+
},
|
|
3002
3233
|
{
|
|
3003
3234
|
command: "fluncle open",
|
|
3004
3235
|
exposedContent: ["pick a track, open it in Spotify"],
|
|
@@ -3226,6 +3457,19 @@ var init_src = __esm(() => {
|
|
|
3226
3457
|
title: "Artist edges",
|
|
3227
3458
|
weights: { status: "hidden" }
|
|
3228
3459
|
},
|
|
3460
|
+
{
|
|
3461
|
+
command: "fluncle admin backfills artist-credits",
|
|
3462
|
+
exposedContent: [
|
|
3463
|
+
"mint identity-true artists from MusicBrainz credits for slice 0's zero-matched residual → track_artists edges"
|
|
3464
|
+
],
|
|
3465
|
+
kind: "cron",
|
|
3466
|
+
name: "cron.artist-credits",
|
|
3467
|
+
operatorNotes: "every 5m, run by a rave-02 HOST systemd timer (docs/agents/hermes/artist-credits-timer/). The MB CREDIT SWEEP (RFC artist-primary-capture, slice 1b): the sibling that completes what `backfill_artist_edges` (slice 0) could not. Slice 0's name-fold left a ~14.3k ZERO-MATCHED residual (a track it stamped but wrote no edge — no credited name folded to an existing identity), and those are exactly the tracks capture-authorization (slice 1) cannot reason about, since it matches BY IDENTITY through the `track_artists` graph. This sweep picks up that residual: for each zero-matched track carrying a MusicBrainz recording identity (`mb_recording_id`, or the `mb_<recording-mbid>` PK a crawler-born row carries), ONE paced `/recording/<mbid>?inc=artist-credits` lookup through the shared MusicBrainz client names its credited artists WITH their MB artist ids, and each resolves down a three-rung ladder: an EXACT `mbid` match, else an ADOPT (the credit name folds unambiguously onto an existing artist with no mbid — the common case, since the residual is dominated by compound credit strings like 'Sub Focus & Dimension' whose members Fluncle already holds as Spotify-keyed rows; adopting `coalesce`s the mbid onto that row instead of minting a duplicate, the split-identity guard), else a MINT of a fresh identity-true row (`mintArtistByMbid` — a real MBID is identity, the licence slice 0 lacked). Fail-closed on any ambiguity (a shared fold, or a fold whose row carries a different mbid) → mint, never a wrong merge. Then the `track_artists` edges are written. A zero-matched track with NO MB identity is TERMINALLY SKIPPED (stamped, never retried). METADATA / GRAPH IDENTITY ONLY — it certifies nothing and publishes nothing (agent tier, the `backfill_recording_mbids` precedent). Worker-paced (the box holds no MusicBrainz budget): a SMALL bounded batch per tick (each row is one ~1.1s MB call), 1 req/s, circuit-broken on a throttle, with a 60s response budget that pauses mid-page and resumes on the next request. The `tracks` row carries the durable per-row reliability state — its OWN `artist_credits_backfilled_at` stamp, DISTINCT from slice 0's `artist_edges_backfilled_at` (never disturbed). The ~14.3k residual drains at ~40/tick over ~a day and a half. Zero LLM tokens. Source: docs/agents/hermes/scripts/artist-credits-sweep.*. See docs/artist-relationship.md.",
|
|
3468
|
+
probeConfig: { cadenceMs: 5 * MINUTE_MS, cronName: "fluncle-artist-credits", kind: "cron" },
|
|
3469
|
+
statusDescription: "mints artists from MusicBrainz credits for the residual",
|
|
3470
|
+
title: "Artist credits",
|
|
3471
|
+
weights: { status: "hidden" }
|
|
3472
|
+
},
|
|
3229
3473
|
{
|
|
3230
3474
|
command: "fluncle admin backfills label-lineage",
|
|
3231
3475
|
exposedContent: [
|
|
@@ -4151,6 +4395,7 @@ __export(exports_admin_tracks, {
|
|
|
4151
4395
|
backfillLastfmCommand: () => backfillLastfmCommand,
|
|
4152
4396
|
backfillDiscogsCommand: () => backfillDiscogsCommand,
|
|
4153
4397
|
backfillArtistEdgesCommand: () => backfillArtistEdgesCommand,
|
|
4398
|
+
backfillArtistCreditsCommand: () => backfillArtistCreditsCommand,
|
|
4154
4399
|
backfillAppleMusicCommand: () => backfillAppleMusicCommand,
|
|
4155
4400
|
backfillAppleCatalogueCommand: () => backfillAppleCatalogueCommand
|
|
4156
4401
|
});
|
|
@@ -4350,6 +4595,13 @@ async function backfillArtistEdgesCommand(limit, dryRun, cursor) {
|
|
|
4350
4595
|
}
|
|
4351
4596
|
return adminApiPost(`/api/admin/backfill/artist-edges?${params.toString()}`);
|
|
4352
4597
|
}
|
|
4598
|
+
async function backfillArtistCreditsCommand(limit, dryRun, cursor) {
|
|
4599
|
+
const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
|
|
4600
|
+
if (cursor) {
|
|
4601
|
+
params.set("cursor", cursor);
|
|
4602
|
+
}
|
|
4603
|
+
return adminApiPost(`/api/admin/backfill/artist-credits?${params.toString()}`);
|
|
4604
|
+
}
|
|
4353
4605
|
async function vehiclesCommand(limit) {
|
|
4354
4606
|
const tracks = await fetchAdminTracks({ hasVideo: true, max: limit, order: "desc" });
|
|
4355
4607
|
return tracks.map((track) => ({
|
|
@@ -6781,7 +7033,7 @@ __export(exports_format, {
|
|
|
6781
7033
|
artistTitle: () => artistTitle2
|
|
6782
7034
|
});
|
|
6783
7035
|
function coordinate3(track) {
|
|
6784
|
-
return track.logId ??
|
|
7036
|
+
return track.logId ?? COORD_FALLBACK4;
|
|
6785
7037
|
}
|
|
6786
7038
|
function artistTitle2(track) {
|
|
6787
7039
|
return `${track.artists.join(", ")} — ${track.title}`;
|
|
@@ -6812,7 +7064,7 @@ function vehicleRows(rows) {
|
|
|
6812
7064
|
return Math.max(width, coordinate3(row).length);
|
|
6813
7065
|
}, 0);
|
|
6814
7066
|
return rows.map((row) => {
|
|
6815
|
-
return `${coordinate3(row).padEnd(coordWidth)} ${foundDate(row.addedAt)} ${row.vehicle ??
|
|
7067
|
+
return `${coordinate3(row).padEnd(coordWidth)} ${foundDate(row.addedAt)} ${row.vehicle ?? COORD_FALLBACK4} · ${row.register ?? COORD_FALLBACK4}`;
|
|
6816
7068
|
});
|
|
6817
7069
|
}
|
|
6818
7070
|
function trackDetailLines(track) {
|
|
@@ -6829,7 +7081,7 @@ function trackDetailLines(track) {
|
|
|
6829
7081
|
}
|
|
6830
7082
|
return lines;
|
|
6831
7083
|
}
|
|
6832
|
-
var
|
|
7084
|
+
var COORD_FALLBACK4 = "—";
|
|
6833
7085
|
var init_format2 = __esm(() => {
|
|
6834
7086
|
init_util();
|
|
6835
7087
|
});
|
|
@@ -9026,9 +9278,25 @@ function addListenCommands(program2) {
|
|
|
9026
9278
|
view: parseFreshView(options.view)
|
|
9027
9279
|
});
|
|
9028
9280
|
});
|
|
9029
|
-
program2.command("
|
|
9030
|
-
const {
|
|
9031
|
-
await
|
|
9281
|
+
program2.command("albums").description("Browse the albums in Fluncle's archive").argument("[slug]", "Album slug (omit for the A-to-Z list)").option("--page <n>", "Which page of the list (default 1)").option("--json", "Print JSON", false).action(async (slug, options) => {
|
|
9282
|
+
const { albumsCommand: albumsCommand2 } = await Promise.resolve().then(() => (init_albums(), exports_albums));
|
|
9283
|
+
await albumsCommand2({ json: options.json, page: parsePage(options.page), slug });
|
|
9284
|
+
});
|
|
9285
|
+
program2.command("artists").description("Browse the artists in Fluncle's archive").argument("[slug]", "Artist slug (omit for the A-to-Z list)").option("--page <n>", "Which page of the list (default 1)").option("--json", "Print JSON", false).action(async (slug, options) => {
|
|
9286
|
+
const { artistsCommand: artistsCommand2 } = await Promise.resolve().then(() => (init_artists(), exports_artists));
|
|
9287
|
+
await artistsCommand2({ json: options.json, page: parsePage(options.page), slug });
|
|
9288
|
+
});
|
|
9289
|
+
program2.command("labels").description("Browse the labels in Fluncle's archive").argument("[slug]", "Label slug (omit for the A-to-Z list)").option("--page <n>", "Which page of the list (default 1)").option("--json", "Print JSON", false).action(async (slug, options) => {
|
|
9290
|
+
const { labelsCommand: labelsCommand2 } = await Promise.resolve().then(() => (init_labels(), exports_labels));
|
|
9291
|
+
await labelsCommand2({ json: options.json, page: parsePage(options.page), slug });
|
|
9292
|
+
});
|
|
9293
|
+
program2.command("search").description("Search the archive for a track, artist, label, or album").argument("<query...>", "What to look for").option("--limit <n>", "Max results to return (1-50)").option("--json", "Print JSON", false).action(async (query, options) => {
|
|
9294
|
+
const { searchCommand: searchCommand2 } = await Promise.resolve().then(() => (init_search(), exports_search));
|
|
9295
|
+
await searchCommand2({
|
|
9296
|
+
json: options.json,
|
|
9297
|
+
limit: parseSearchLimit(options.limit),
|
|
9298
|
+
query: query.join(" ")
|
|
9299
|
+
});
|
|
9032
9300
|
});
|
|
9033
9301
|
program2.command("galaxies").description("Wander Fluncle's sonic galaxies").argument("[slug]", "Galaxy slug (omit for the full map)").option("--json", "Print JSON", false).action(async (slug, options) => {
|
|
9034
9302
|
const { galaxiesListCommand: galaxiesListCommand2, galaxyGetCommand: galaxyGetCommand2 } = await Promise.resolve().then(() => (init_galaxies(), exports_galaxies));
|
|
@@ -9767,6 +10035,10 @@ function addAdminCommands(program2) {
|
|
|
9767
10035
|
const { backfillArtistEdgesCommand: backfillArtistEdgesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
9768
10036
|
await runBackfillArtistEdges(options, backfillArtistEdgesCommand2);
|
|
9769
10037
|
});
|
|
10038
|
+
backfill.command("artist-credits").description("Mint identity-true artists from MusicBrainz credits for the zero-matched residual").option("--dry-run", "Report the eligible worklist without any vendor call or write", false).option("--limit <limit>", "Max worklist rows to visit", "40").option("--json", "Print JSON", false).action(async (options) => {
|
|
10039
|
+
const { backfillArtistCreditsCommand: backfillArtistCreditsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
10040
|
+
await runBackfillArtistCredits(options, backfillArtistCreditsCommand2);
|
|
10041
|
+
});
|
|
9770
10042
|
backfill.command("cover-masters").description("Resolve owned \u22641200\xB2 cover masters (album/artist) into R2").option("--kind <kind>", "Which entity to drain: album | artist", "album").option("--dry-run", "Report the eligible worklist without any fetch or write", false).option("--retry-none", "First re-queue a bounded batch of terminal none rows to pending, then run the pass", false).option("--limit <limit>", "Max entities to process", "50").option("--json", "Print JSON", false).action(async (options) => {
|
|
9771
10043
|
const { backfillCoverMastersCommand: backfillCoverMastersCommand2 } = await Promise.resolve().then(() => (init_admin_covers(), exports_admin_covers));
|
|
9772
10044
|
await runBackfillCoverMasters(options, backfillCoverMastersCommand2);
|
|
@@ -10814,6 +11086,57 @@ async function runBackfillArtistEdges(options, backfillArtistEdgesCommand2) {
|
|
|
10814
11086
|
const verb = dryRun ? "Would write" : "Wrote";
|
|
10815
11087
|
console.log(`${verb} ${edgesWritten} track_artists edge(s) over ${scanned} track(s): ${fullyMatched.length} fully matched, ${partiallyMatched.length} partially, ${zeroMatched.length} with none; ${unmatchedNames} credited name(s) left unmatched.`);
|
|
10816
11088
|
}
|
|
11089
|
+
async function runBackfillArtistCredits(options, backfillArtistCreditsCommand2) {
|
|
11090
|
+
const limit = parseListLimit(options.limit) ?? 40;
|
|
11091
|
+
let cursor;
|
|
11092
|
+
let dryRun = options.dryRun;
|
|
11093
|
+
let scanned = 0;
|
|
11094
|
+
let mintedArtists = 0;
|
|
11095
|
+
let matchedArtists = 0;
|
|
11096
|
+
let adoptedArtists = 0;
|
|
11097
|
+
let edgesWritten = 0;
|
|
11098
|
+
let skippedNoIdentity = 0;
|
|
11099
|
+
let throttled = false;
|
|
11100
|
+
while (scanned < limit) {
|
|
11101
|
+
const remaining = limit - scanned;
|
|
11102
|
+
const result = await backfillArtistCreditsCommand2(remaining, options.dryRun, cursor);
|
|
11103
|
+
dryRun = result.dryRun;
|
|
11104
|
+
scanned += result.scanned;
|
|
11105
|
+
mintedArtists += result.mintedArtists;
|
|
11106
|
+
matchedArtists += result.matchedArtists;
|
|
11107
|
+
adoptedArtists += result.adoptedArtists;
|
|
11108
|
+
edgesWritten += result.edgesWritten;
|
|
11109
|
+
skippedNoIdentity += result.skippedNoIdentity;
|
|
11110
|
+
if (!options.json) {
|
|
11111
|
+
const verb2 = result.dryRun ? "would visit" : "visited";
|
|
11112
|
+
console.log(` \u2026${verb2} ${result.scanned}; minted ${result.mintedArtists}, adopted ${result.adoptedArtists}, matched ${result.matchedArtists} artist(s); wrote ${result.edgesWritten} edge(s); ${result.skippedNoIdentity} without a MusicBrainz identity`);
|
|
11113
|
+
}
|
|
11114
|
+
if (result.rateLimited) {
|
|
11115
|
+
throttled = true;
|
|
11116
|
+
break;
|
|
11117
|
+
}
|
|
11118
|
+
if (result.nextCursor === null) {
|
|
11119
|
+
break;
|
|
11120
|
+
}
|
|
11121
|
+
cursor = result.nextCursor;
|
|
11122
|
+
}
|
|
11123
|
+
if (options.json) {
|
|
11124
|
+
printJson({
|
|
11125
|
+
adoptedArtists,
|
|
11126
|
+
dryRun,
|
|
11127
|
+
edgesWritten,
|
|
11128
|
+
matchedArtists,
|
|
11129
|
+
mintedArtists,
|
|
11130
|
+
ok: true,
|
|
11131
|
+
rateLimited: throttled,
|
|
11132
|
+
scanned,
|
|
11133
|
+
skippedNoIdentity
|
|
11134
|
+
});
|
|
11135
|
+
return;
|
|
11136
|
+
}
|
|
11137
|
+
const verb = dryRun ? "Would visit" : "Visited";
|
|
11138
|
+
console.log(`${verb} ${scanned} track(s): minted ${mintedArtists}, adopted ${adoptedArtists}, matched ${matchedArtists} artist(s); wrote ${edgesWritten} track_artists edge(s); ${skippedNoIdentity} carried no MusicBrainz identity.`);
|
|
11139
|
+
}
|
|
10817
11140
|
async function runArtistResolveQueue(options, listArtistsCommand2) {
|
|
10818
11141
|
const limit = parseListLimit(options.limit);
|
|
10819
11142
|
const result = await listArtistsCommand2(limit ?? 50);
|
|
@@ -11614,33 +11937,6 @@ async function runRecent(options, recentCommand3) {
|
|
|
11614
11937
|
console.log(trackRows2(tracks).join(`
|
|
11615
11938
|
`));
|
|
11616
11939
|
}
|
|
11617
|
-
async function runArtists(slug, options, commands) {
|
|
11618
|
-
if (slug) {
|
|
11619
|
-
const artist = await commands.artistsGetCommand(slug);
|
|
11620
|
-
if (options.json) {
|
|
11621
|
-
printJson({ artist, ok: true });
|
|
11622
|
-
return;
|
|
11623
|
-
}
|
|
11624
|
-
console.log(`${artist.name} (${artist.slug})`);
|
|
11625
|
-
console.log(`Findings: ${artist.findingCount}`);
|
|
11626
|
-
if (artist.spotifyUrl) {
|
|
11627
|
-
console.log(`Spotify: ${artist.spotifyUrl}`);
|
|
11628
|
-
}
|
|
11629
|
-
return;
|
|
11630
|
-
}
|
|
11631
|
-
const artists = await commands.artistsListCommand();
|
|
11632
|
-
if (options.json) {
|
|
11633
|
-
printJson({ artists, ok: true });
|
|
11634
|
-
return;
|
|
11635
|
-
}
|
|
11636
|
-
if (artists.length === 0) {
|
|
11637
|
-
console.log("No artists in the archive yet.");
|
|
11638
|
-
return;
|
|
11639
|
-
}
|
|
11640
|
-
for (const a of artists) {
|
|
11641
|
-
console.log(`${a.name.padEnd(40)} ${String(a.findingCount).padStart(3)} finding${a.findingCount === 1 ? "" : "s"}`);
|
|
11642
|
-
}
|
|
11643
|
-
}
|
|
11644
11940
|
async function runGalaxies(slug, options, commands) {
|
|
11645
11941
|
if (slug) {
|
|
11646
11942
|
const { findings, galaxy } = await commands.galaxyGetCommand(slug);
|
|
@@ -11860,6 +12156,23 @@ function parseListLimit(value) {
|
|
|
11860
12156
|
}
|
|
11861
12157
|
return limit;
|
|
11862
12158
|
}
|
|
12159
|
+
function parsePage(value) {
|
|
12160
|
+
const page = Number.parseInt(value ?? "1", 10);
|
|
12161
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
12162
|
+
throw new Error("Page must be a positive integer");
|
|
12163
|
+
}
|
|
12164
|
+
return page;
|
|
12165
|
+
}
|
|
12166
|
+
function parseSearchLimit(value) {
|
|
12167
|
+
if (value === undefined) {
|
|
12168
|
+
return;
|
|
12169
|
+
}
|
|
12170
|
+
const limit = Number.parseInt(value, 10);
|
|
12171
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
12172
|
+
throw new Error("Limit must be an integer between 1 and 50");
|
|
12173
|
+
}
|
|
12174
|
+
return limit;
|
|
12175
|
+
}
|
|
11863
12176
|
function parseFreshView(value) {
|
|
11864
12177
|
const view = value ?? "all";
|
|
11865
12178
|
if (view !== "all" && view !== "tracks" && view !== "albums") {
|
|
@@ -12425,6 +12738,7 @@ var stringOptions = new Set([
|
|
|
12425
12738
|
"--model",
|
|
12426
12739
|
"--note",
|
|
12427
12740
|
"--order",
|
|
12741
|
+
"--page",
|
|
12428
12742
|
"--parent-id",
|
|
12429
12743
|
"--plate",
|
|
12430
12744
|
"--plate-background",
|
|
@@ -12472,6 +12786,13 @@ Listen:
|
|
|
12472
12786
|
fluncle random [--json] The archive throws one back
|
|
12473
12787
|
fluncle subscribe [email] Fresh bangers, every Friday
|
|
12474
12788
|
|
|
12789
|
+
Browse:
|
|
12790
|
+
fluncle search <query> [--limit 20] [--json] Search the archive for a track, artist, label, or album
|
|
12791
|
+
fluncle artists [slug] [--page 1] [--json] Artists in the archive, A to Z
|
|
12792
|
+
fluncle albums [slug] [--page 1] [--json] Albums in the archive, A to Z
|
|
12793
|
+
fluncle labels [slug] [--page 1] [--json] Labels in the archive, A to Z
|
|
12794
|
+
fluncle fresh [--limit 10] [--view all] [--json] What just came out, newest release first
|
|
12795
|
+
|
|
12475
12796
|
Share:
|
|
12476
12797
|
fluncle submit [search-or-spotify-url] Send a track for review
|
|
12477
12798
|
|
package/package.json
CHANGED