fluncle 0.138.0 → 0.140.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 +130 -11
- 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.140.0".trim() ? "0.140.0".trim() : "0.1.0";
|
|
565
565
|
});
|
|
566
566
|
|
|
567
567
|
// src/update-notifier.ts
|
|
@@ -2189,7 +2189,7 @@ function parseVersion2(version) {
|
|
|
2189
2189
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2190
2190
|
var init_version2 = __esm(() => {
|
|
2191
2191
|
init_output();
|
|
2192
|
-
currentVersion2 = "0.
|
|
2192
|
+
currentVersion2 = "0.140.0".trim() ? "0.140.0".trim() : "0.1.0";
|
|
2193
2193
|
});
|
|
2194
2194
|
|
|
2195
2195
|
// ../../packages/registry/src/index.ts
|
|
@@ -2945,6 +2945,17 @@ var init_src = __esm(() => {
|
|
|
2945
2945
|
probeConfig: { cadenceMs: 30 * MINUTE_MS, cronName: "fluncle-rank", kind: "cron" },
|
|
2946
2946
|
weights: { status: "hidden" }
|
|
2947
2947
|
},
|
|
2948
|
+
{
|
|
2949
|
+
command: "fluncle admin backfills label-images",
|
|
2950
|
+
exposedContent: [
|
|
2951
|
+
"resolve each pending label's own logo (Discogs → Wikidata → cover floor) → its own R2 image"
|
|
2952
|
+
],
|
|
2953
|
+
kind: "cron",
|
|
2954
|
+
name: "cron.label-images",
|
|
2955
|
+
operatorNotes: "every 60m, run by a rave-02 HOST systemd timer (docs/agents/hermes/label-images-timer/). The DURABLE other half of the label entity: the crawler MINTS new labels every few minutes (each `image_state='pending'`) and the one-shot operator backfill only seeded the labels that already existed, so this cron is what gives every freshly-minted label its OWN logo instead of a borrowed album cover. METADATA ONLY — a label logo is internal, reversible, nominative-use trademark (the album-art posture); it certifies nothing and publishes nothing (agent tier, the `backfill_discogs` precedent). Worker-paced (the box holds no Discogs key / MusicBrainz budget): one bounded batch per tick walks each label's MB identity → its curated Discogs/Wikidata url-rels → downloads the logo once into R2, up the ladder Discogs → Wikidata → none (the freshest-cover floor). The `labels` row carries the durable reliability state (image_state/image_attempted_at/image_failures), so a resolved/none label is terminal and a vendor throttle just circuit-breaks and resumes next tick. Zero LLM tokens. Source: docs/agents/hermes/scripts/label-images-sweep.*. See docs/label-entity.md.",
|
|
2956
|
+
probeConfig: { cadenceMs: 60 * MINUTE_MS, cronName: "fluncle-label-images", kind: "cron" },
|
|
2957
|
+
weights: { status: "hidden" }
|
|
2958
|
+
},
|
|
2948
2959
|
{
|
|
2949
2960
|
command: "fluncle admin tracks capture-audio --queue",
|
|
2950
2961
|
exposedContent: [
|
|
@@ -3655,7 +3666,8 @@ __export(exports_admin_tracks, {
|
|
|
3655
3666
|
captureQueueCommand: () => captureQueueCommand,
|
|
3656
3667
|
backfillLastfmCommand: () => backfillLastfmCommand,
|
|
3657
3668
|
backfillDiscogsCommand: () => backfillDiscogsCommand,
|
|
3658
|
-
backfillAppleMusicCommand: () => backfillAppleMusicCommand
|
|
3669
|
+
backfillAppleMusicCommand: () => backfillAppleMusicCommand,
|
|
3670
|
+
backfillAppleCatalogueCommand: () => backfillAppleCatalogueCommand
|
|
3659
3671
|
});
|
|
3660
3672
|
async function fetchAdminTracks(options) {
|
|
3661
3673
|
const {
|
|
@@ -3835,6 +3847,10 @@ async function backfillAppleMusicCommand(limit, dryRun, cursor) {
|
|
|
3835
3847
|
}
|
|
3836
3848
|
return adminApiPost(`/api/admin/backfill/apple-music?${params.toString()}`);
|
|
3837
3849
|
}
|
|
3850
|
+
async function backfillAppleCatalogueCommand(limit, dryRun) {
|
|
3851
|
+
const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
|
|
3852
|
+
return adminApiPost(`/api/admin/backfill/apple-catalogue?${params.toString()}`);
|
|
3853
|
+
}
|
|
3838
3854
|
async function vehiclesCommand(limit) {
|
|
3839
3855
|
const tracks = await fetchAdminTracks({ hasVideo: true, max: limit, order: "desc" });
|
|
3840
3856
|
return tracks.map((track) => ({
|
|
@@ -5688,6 +5704,7 @@ var exports_admin_catalogue = {};
|
|
|
5688
5704
|
__export(exports_admin_catalogue, {
|
|
5689
5705
|
crawlStatusCommand: () => crawlStatusCommand,
|
|
5690
5706
|
crawlCatalogueCommand: () => crawlCatalogueCommand,
|
|
5707
|
+
clearWrongAudioCommand: () => clearWrongAudioCommand,
|
|
5691
5708
|
catalogueRankCommand: () => catalogueRankCommand,
|
|
5692
5709
|
catalogueListCommand: () => catalogueListCommand
|
|
5693
5710
|
});
|
|
@@ -5698,12 +5715,17 @@ async function catalogueRankCommand(options) {
|
|
|
5698
5715
|
}
|
|
5699
5716
|
async function catalogueListCommand(options) {
|
|
5700
5717
|
const params = new URLSearchParams;
|
|
5701
|
-
params.set("lens", options.lens === "capture" ? "capture" : "ear");
|
|
5718
|
+
params.set("lens", options.lens === "capture" ? "capture" : options.lens === "quarantine" ? "quarantine" : "ear");
|
|
5702
5719
|
if (options.limit) {
|
|
5703
5720
|
params.set("limit", options.limit);
|
|
5704
5721
|
}
|
|
5705
5722
|
return adminApiGet(`/api/admin/catalogue?${params.toString()}`);
|
|
5706
5723
|
}
|
|
5724
|
+
async function clearWrongAudioCommand(trackId) {
|
|
5725
|
+
return adminApiPost("/api/admin/catalogue/wrong-audio/clear", {
|
|
5726
|
+
trackId
|
|
5727
|
+
});
|
|
5728
|
+
}
|
|
5707
5729
|
async function crawlCatalogueCommand(limit, maxHop, dryRun) {
|
|
5708
5730
|
const params = new URLSearchParams({ limit: String(limit), maxHop: String(maxHop) });
|
|
5709
5731
|
if (dryRun) {
|
|
@@ -8750,10 +8772,19 @@ function addAdminCommands(program2) {
|
|
|
8750
8772
|
const { catalogueRankCommand: catalogueRankCommand2 } = await Promise.resolve().then(() => (init_admin_catalogue(), exports_admin_catalogue));
|
|
8751
8773
|
await runCatalogueRank(options, catalogueRankCommand2);
|
|
8752
8774
|
});
|
|
8753
|
-
catalogue.command("list").description("The ranked catalogue: closest to a finding (ear),
|
|
8775
|
+
catalogue.command("list").description("The ranked catalogue: closest to a finding (ear), next to capture, or wrong audio").option("--lens <lens>", "ear (default), capture, or quarantine", "ear").option("--limit <limit>", "Rows (default 50, max 200)").option("--json", "Print JSON", false).action(async (options) => {
|
|
8754
8776
|
const { catalogueListCommand: catalogueListCommand2 } = await Promise.resolve().then(() => (init_admin_catalogue(), exports_admin_catalogue));
|
|
8755
8777
|
await runCatalogueList(options, catalogueListCommand2);
|
|
8756
8778
|
});
|
|
8779
|
+
catalogue.command("clear-wrong-audio").description("Overrule the wrong-audio quarantine on one catalogue row (operator)").argument("<trackId>", "The catalogue track id to keep").option("--json", "Print JSON", false).action(async (trackId, options) => {
|
|
8780
|
+
const { clearWrongAudioCommand: clearWrongAudioCommand2 } = await Promise.resolve().then(() => (init_admin_catalogue(), exports_admin_catalogue));
|
|
8781
|
+
const { cleared } = await clearWrongAudioCommand2(trackId);
|
|
8782
|
+
if (options.json) {
|
|
8783
|
+
printJson({ cleared, ok: true });
|
|
8784
|
+
return;
|
|
8785
|
+
}
|
|
8786
|
+
console.log(cleared ? `Kept ${trackId}. Its audio re-embeds and rejoins the ranking on the next sweep.` : `${trackId} was not quarantined \u2014 nothing to clear.`);
|
|
8787
|
+
});
|
|
8757
8788
|
catalogue.command("crawl").description("Run one bounded, resumable pass of the catalogue crawler").option("--dry-run", "Report the seed plan and write nothing at all", false).option("--limit <limit>", "Frontier nodes to expand this pass", "10").option("--max-hop <maxHop>", "Graph distance from a seed label (0-3)", "2").option("--json", "Print JSON", false).action(async (options) => {
|
|
8758
8789
|
const { crawlCatalogueCommand: crawlCatalogueCommand2 } = await Promise.resolve().then(() => (init_admin_catalogue(), exports_admin_catalogue));
|
|
8759
8790
|
await runCrawlCatalogue(options, crawlCatalogueCommand2);
|
|
@@ -8782,6 +8813,10 @@ function addAdminCommands(program2) {
|
|
|
8782
8813
|
const { backfillAppleMusicCommand: backfillAppleMusicCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
8783
8814
|
await runBackfillAppleMusic(options, backfillAppleMusicCommand2);
|
|
8784
8815
|
});
|
|
8816
|
+
backfill.command("apple-catalogue").description("Resolve Apple URLs + album facts for catalogue tracks (batched exact ISRC)").option("--dry-run", "Report the eligible set without resolving or writing", false).option("--limit <limit>", "Max catalogue rows to resolve", "50").option("--json", "Print JSON", false).action(async (options) => {
|
|
8817
|
+
const { backfillAppleCatalogueCommand: backfillAppleCatalogueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
8818
|
+
await runBackfillAppleCatalogue(options, backfillAppleCatalogueCommand2);
|
|
8819
|
+
});
|
|
8785
8820
|
backfill.command("artists").description("Back-fill the artist entity (artists + track_artists) for existing findings").option("--dry-run", "Report which findings would be upserted without touching the DB", false).option("--limit <limit>", "Max findings to process", "50").option("--json", "Print JSON", false).action(async (options) => {
|
|
8786
8821
|
const { backfillArtistsCommand: backfillArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
|
|
8787
8822
|
await runBackfillArtists(options, backfillArtistsCommand2);
|
|
@@ -9202,23 +9237,30 @@ async function runBackfillAppleMusic(options, backfillAppleMusicCommand2) {
|
|
|
9202
9237
|
let cursor;
|
|
9203
9238
|
let dryRun = options.dryRun;
|
|
9204
9239
|
let throttled = false;
|
|
9240
|
+
let breakerTripped = false;
|
|
9205
9241
|
let configured = true;
|
|
9242
|
+
let albumFactsWritten = 0;
|
|
9206
9243
|
while (resolved.length + unresolved.length + failed.length < limit) {
|
|
9207
9244
|
const remaining = limit - (resolved.length + unresolved.length + failed.length);
|
|
9208
9245
|
const result = await backfillAppleMusicCommand2(remaining, options.dryRun, cursor);
|
|
9209
9246
|
dryRun = result.dryRun;
|
|
9210
9247
|
configured = result.configured;
|
|
9248
|
+
albumFactsWritten += result.albumFactsWritten;
|
|
9211
9249
|
resolved.push(...result.resolved);
|
|
9212
9250
|
unresolved.push(...result.unresolved);
|
|
9213
9251
|
failed.push(...result.failed);
|
|
9214
9252
|
skipped.push(...result.skipped);
|
|
9215
9253
|
if (!options.json) {
|
|
9216
9254
|
const verb2 = result.dryRun ? "would resolve" : "resolved";
|
|
9217
|
-
console.log(` \u2026${verb2} ${result.resolvedCount}; ${result.unresolvedCount} unresolved; ${result.failedCount} failed; ${result.skippedCount} skipped`);
|
|
9255
|
+
console.log(` \u2026${verb2} ${result.resolvedCount}; ${result.unresolvedCount} unresolved; ${result.failedCount} failed; ${result.skippedCount} skipped; ${result.albumFactsWritten} album facts`);
|
|
9218
9256
|
}
|
|
9219
9257
|
if (!result.configured) {
|
|
9220
9258
|
break;
|
|
9221
9259
|
}
|
|
9260
|
+
if (result.breakerTripped) {
|
|
9261
|
+
breakerTripped = true;
|
|
9262
|
+
break;
|
|
9263
|
+
}
|
|
9222
9264
|
if (result.rateLimited) {
|
|
9223
9265
|
throttled = true;
|
|
9224
9266
|
break;
|
|
@@ -9230,6 +9272,8 @@ async function runBackfillAppleMusic(options, backfillAppleMusicCommand2) {
|
|
|
9230
9272
|
}
|
|
9231
9273
|
if (options.json) {
|
|
9232
9274
|
printSweepJson({
|
|
9275
|
+
albumFactsWritten,
|
|
9276
|
+
breakerTripped,
|
|
9233
9277
|
configured,
|
|
9234
9278
|
dryRun,
|
|
9235
9279
|
failed,
|
|
@@ -9248,7 +9292,7 @@ async function runBackfillAppleMusic(options, backfillAppleMusicCommand2) {
|
|
|
9248
9292
|
return;
|
|
9249
9293
|
}
|
|
9250
9294
|
const verb = dryRun ? "Would resolve" : "Resolved";
|
|
9251
|
-
console.log(`${verb} ${resolved.length} Apple Music URL(s); ${unresolved.length} unresolved; ${failed.length} failed; ${skipped.length} skipped.`);
|
|
9295
|
+
console.log(`${verb} ${resolved.length} Apple Music URL(s); ${unresolved.length} unresolved; ${failed.length} failed; ${skipped.length} skipped; ${albumFactsWritten} album facts.`);
|
|
9252
9296
|
for (const item of resolved) {
|
|
9253
9297
|
console.log(` ${item.logId}: ${item.url}`);
|
|
9254
9298
|
}
|
|
@@ -9259,6 +9303,72 @@ async function runBackfillAppleMusic(options, backfillAppleMusicCommand2) {
|
|
|
9259
9303
|
process.exitCode = 1;
|
|
9260
9304
|
}
|
|
9261
9305
|
}
|
|
9306
|
+
async function runBackfillAppleCatalogue(options, backfillAppleCatalogueCommand2) {
|
|
9307
|
+
const limit = parseListLimit(options.limit);
|
|
9308
|
+
const resolved = [];
|
|
9309
|
+
const unresolved = [];
|
|
9310
|
+
const failed = [];
|
|
9311
|
+
let dryRun = options.dryRun;
|
|
9312
|
+
let throttled = false;
|
|
9313
|
+
let breakerTripped = false;
|
|
9314
|
+
let configured = true;
|
|
9315
|
+
let albumFactsWritten = 0;
|
|
9316
|
+
while (resolved.length + unresolved.length + failed.length < limit) {
|
|
9317
|
+
const remaining = limit - (resolved.length + unresolved.length + failed.length);
|
|
9318
|
+
const result = await backfillAppleCatalogueCommand2(remaining, options.dryRun);
|
|
9319
|
+
dryRun = result.dryRun;
|
|
9320
|
+
configured = result.configured;
|
|
9321
|
+
albumFactsWritten += result.albumFactsWritten;
|
|
9322
|
+
resolved.push(...result.resolved);
|
|
9323
|
+
unresolved.push(...result.unresolved);
|
|
9324
|
+
failed.push(...result.failed);
|
|
9325
|
+
if (!options.json) {
|
|
9326
|
+
const verb2 = result.dryRun ? "would resolve" : "resolved";
|
|
9327
|
+
console.log(` \u2026${verb2} ${result.resolvedCount}; ${result.unresolvedCount} unresolved; ${result.failedCount} failed; ${result.albumFactsWritten} album facts`);
|
|
9328
|
+
}
|
|
9329
|
+
if (!result.configured) {
|
|
9330
|
+
break;
|
|
9331
|
+
}
|
|
9332
|
+
if (result.breakerTripped) {
|
|
9333
|
+
breakerTripped = true;
|
|
9334
|
+
break;
|
|
9335
|
+
}
|
|
9336
|
+
if (result.rateLimited) {
|
|
9337
|
+
throttled = true;
|
|
9338
|
+
break;
|
|
9339
|
+
}
|
|
9340
|
+
if (result.resolvedCount === 0 && result.unresolvedCount === 0 && result.failedCount === 0) {
|
|
9341
|
+
break;
|
|
9342
|
+
}
|
|
9343
|
+
}
|
|
9344
|
+
if (options.json) {
|
|
9345
|
+
printSweepJson({
|
|
9346
|
+
albumFactsWritten,
|
|
9347
|
+
breakerTripped,
|
|
9348
|
+
configured,
|
|
9349
|
+
dryRun,
|
|
9350
|
+
failed,
|
|
9351
|
+
rateLimited: throttled,
|
|
9352
|
+
resolved,
|
|
9353
|
+
resolvedCount: resolved.length,
|
|
9354
|
+
unresolved,
|
|
9355
|
+
unresolvedCount: unresolved.length
|
|
9356
|
+
}, failed.length);
|
|
9357
|
+
return;
|
|
9358
|
+
}
|
|
9359
|
+
if (!configured) {
|
|
9360
|
+
console.log("Apple catalogue backfill is not configured (the Worker's MusicKit secrets are unset) \u2014 nothing resolved.");
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
9363
|
+
const verb = dryRun ? "Would resolve" : "Resolved";
|
|
9364
|
+
console.log(`${verb} ${resolved.length} catalogue Apple URL(s); ${unresolved.length} unresolved; ${failed.length} failed; ${albumFactsWritten} album facts.`);
|
|
9365
|
+
for (const item of failed) {
|
|
9366
|
+
console.log(` ${item.trackId}: ${item.error}`);
|
|
9367
|
+
}
|
|
9368
|
+
if (failed.length > 0) {
|
|
9369
|
+
process.exitCode = 1;
|
|
9370
|
+
}
|
|
9371
|
+
}
|
|
9262
9372
|
async function runBackfillArtists(options, backfillArtistsCommand2) {
|
|
9263
9373
|
const limit = parseListLimit(options.limit);
|
|
9264
9374
|
const upserted = [];
|
|
@@ -10269,7 +10379,8 @@ async function runCatalogueRank(options, catalogueRankCommand2) {
|
|
|
10269
10379
|
printJson({ ok: true, summary });
|
|
10270
10380
|
return;
|
|
10271
10381
|
}
|
|
10272
|
-
|
|
10382
|
+
const quarantine = summary.quarantined > 0 ? ` Quarantined ${summary.quarantined} as wrong audio.` : "";
|
|
10383
|
+
console.log(`Ranked ${summary.scored} against ${summary.embeddedFindings} embedded findings; prioritized ${summary.prioritized} for capture.${quarantine} ${summary.remaining} still stale.`);
|
|
10273
10384
|
}
|
|
10274
10385
|
async function runCatalogueList(options, catalogueListCommand2) {
|
|
10275
10386
|
const { summary, tracks } = await catalogueListCommand2({
|
|
@@ -10281,20 +10392,28 @@ async function runCatalogueList(options, catalogueListCommand2) {
|
|
|
10281
10392
|
return;
|
|
10282
10393
|
}
|
|
10283
10394
|
if (tracks.length === 0) {
|
|
10284
|
-
console.log("Nothing out there yet.");
|
|
10395
|
+
console.log(options.lens === "quarantine" ? "No wrong-audio captures." : "Nothing out there yet.");
|
|
10285
10396
|
return;
|
|
10286
10397
|
}
|
|
10287
10398
|
for (const track of tracks) {
|
|
10288
10399
|
const identity = `${track.artists.join(", ")} \u2014 ${track.title}`;
|
|
10400
|
+
if (options.lens === "quarantine") {
|
|
10401
|
+
const collided = track.nearestFinding;
|
|
10402
|
+
const why2 = collided ? `its audio came back as ${collided.logId ? `${collided.logId} ` : ""}${collided.artists.join(", ")} \u2014 ${collided.title}` : "its audio matched a track already in the archive";
|
|
10403
|
+
console.log(`wrong-audio ${identity.padEnd(52)} ${why2}`);
|
|
10404
|
+
continue;
|
|
10405
|
+
}
|
|
10406
|
+
const dup = track.duplicateOf;
|
|
10407
|
+
const dupWhy = dup ? `already in the archive \u2014 ${dup.logId ? `${dup.logId} ` : ""}${dup.artists.join(", ")} \u2014 ${dup.title}` : null;
|
|
10289
10408
|
if (options.lens === "capture") {
|
|
10290
10409
|
const reason = track.captureReason;
|
|
10291
|
-
const why2 = reason?.kind === "artist" ? `${reason.name} is already in the archive` : reason?.kind === "label" ? `${reason.name} already carries a finding` : reason?.kind === "seed-label" ? `${reason.name} is a label the crawler digs from` : reason?.kind === "skipped-label" ? `${reason.name} is not your lane \u2014 ranked last, kept anyway` : "nothing ties it to the archive yet";
|
|
10410
|
+
const why2 = dupWhy ?? (reason?.kind === "artist" ? `${reason.name} is already in the archive` : reason?.kind === "label" ? `${reason.name} already carries a finding` : reason?.kind === "seed-label" ? `${reason.name} is a label the crawler digs from` : reason?.kind === "skipped-label" ? `${reason.name} is not your lane \u2014 ranked last, kept anyway` : "nothing ties it to the archive yet");
|
|
10292
10411
|
console.log(`${String(track.capturePriority ?? 0)} ${identity.padEnd(52)} ${why2}`);
|
|
10293
10412
|
continue;
|
|
10294
10413
|
}
|
|
10295
10414
|
const match = track.nearestFinding;
|
|
10296
10415
|
const score = track.nearestFindingScore?.toFixed(2) ?? "\u2014";
|
|
10297
|
-
const why = match ? `closest to ${match.logId ? `${match.logId} ` : ""}${match.artists.join(", ")} \u2014 ${match.title}` : "nothing to compare it to yet";
|
|
10416
|
+
const why = dupWhy ?? (match ? `closest to ${match.logId ? `${match.logId} ` : ""}${match.artists.join(", ")} \u2014 ${match.title}` : "nothing to compare it to yet");
|
|
10298
10417
|
console.log(`${score} ${identity.padEnd(52)} ${why}`);
|
|
10299
10418
|
}
|
|
10300
10419
|
}
|
package/package.json
CHANGED