fluncle 0.137.0 → 0.139.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 +96 -5
- 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.139.0".trim() ? "0.139.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.139.0".trim() ? "0.139.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: [
|
|
@@ -3654,7 +3665,8 @@ __export(exports_admin_tracks, {
|
|
|
3654
3665
|
contextQueueCommand: () => contextQueueCommand,
|
|
3655
3666
|
captureQueueCommand: () => captureQueueCommand,
|
|
3656
3667
|
backfillLastfmCommand: () => backfillLastfmCommand,
|
|
3657
|
-
backfillDiscogsCommand: () => backfillDiscogsCommand
|
|
3668
|
+
backfillDiscogsCommand: () => backfillDiscogsCommand,
|
|
3669
|
+
backfillAppleMusicCommand: () => backfillAppleMusicCommand
|
|
3658
3670
|
});
|
|
3659
3671
|
async function fetchAdminTracks(options) {
|
|
3660
3672
|
const {
|
|
@@ -3827,6 +3839,13 @@ async function backfillDiscogsCommand(limit, dryRun, cursor) {
|
|
|
3827
3839
|
}
|
|
3828
3840
|
return adminApiPost(`/api/admin/backfill/discogs?${params.toString()}`);
|
|
3829
3841
|
}
|
|
3842
|
+
async function backfillAppleMusicCommand(limit, dryRun, cursor) {
|
|
3843
|
+
const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
|
|
3844
|
+
if (cursor) {
|
|
3845
|
+
params.set("cursor", cursor);
|
|
3846
|
+
}
|
|
3847
|
+
return adminApiPost(`/api/admin/backfill/apple-music?${params.toString()}`);
|
|
3848
|
+
}
|
|
3830
3849
|
async function vehiclesCommand(limit) {
|
|
3831
3850
|
const tracks = await fetchAdminTracks({ hasVideo: true, max: limit, order: "desc" });
|
|
3832
3851
|
return tracks.map((track) => ({
|
|
@@ -8770,6 +8789,10 @@ function addAdminCommands(program2) {
|
|
|
8770
8789
|
const { backfillDiscogsCommand: backfillDiscogsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
8771
8790
|
await runBackfillDiscogs(options, backfillDiscogsCommand2);
|
|
8772
8791
|
});
|
|
8792
|
+
backfill.command("apple-music").description("Resolve missing Apple Music URLs for published findings (exact ISRC match)").option("--dry-run", "Report the eligible set without resolving or writing", false).option("--limit <limit>", "Max findings to resolve", "50").option("--json", "Print JSON", false).action(async (options) => {
|
|
8793
|
+
const { backfillAppleMusicCommand: backfillAppleMusicCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
8794
|
+
await runBackfillAppleMusic(options, backfillAppleMusicCommand2);
|
|
8795
|
+
});
|
|
8773
8796
|
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) => {
|
|
8774
8797
|
const { backfillArtistsCommand: backfillArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
|
|
8775
8798
|
await runBackfillArtists(options, backfillArtistsCommand2);
|
|
@@ -9181,6 +9204,72 @@ async function runBackfillDiscogs(options, backfillDiscogsCommand2) {
|
|
|
9181
9204
|
console.log(` ${item.logId}: release ${item.releaseId}${master}`);
|
|
9182
9205
|
}
|
|
9183
9206
|
}
|
|
9207
|
+
async function runBackfillAppleMusic(options, backfillAppleMusicCommand2) {
|
|
9208
|
+
const limit = parseListLimit(options.limit);
|
|
9209
|
+
const resolved = [];
|
|
9210
|
+
const unresolved = [];
|
|
9211
|
+
const failed = [];
|
|
9212
|
+
const skipped = [];
|
|
9213
|
+
let cursor;
|
|
9214
|
+
let dryRun = options.dryRun;
|
|
9215
|
+
let throttled = false;
|
|
9216
|
+
let configured = true;
|
|
9217
|
+
while (resolved.length + unresolved.length + failed.length < limit) {
|
|
9218
|
+
const remaining = limit - (resolved.length + unresolved.length + failed.length);
|
|
9219
|
+
const result = await backfillAppleMusicCommand2(remaining, options.dryRun, cursor);
|
|
9220
|
+
dryRun = result.dryRun;
|
|
9221
|
+
configured = result.configured;
|
|
9222
|
+
resolved.push(...result.resolved);
|
|
9223
|
+
unresolved.push(...result.unresolved);
|
|
9224
|
+
failed.push(...result.failed);
|
|
9225
|
+
skipped.push(...result.skipped);
|
|
9226
|
+
if (!options.json) {
|
|
9227
|
+
const verb2 = result.dryRun ? "would resolve" : "resolved";
|
|
9228
|
+
console.log(` \u2026${verb2} ${result.resolvedCount}; ${result.unresolvedCount} unresolved; ${result.failedCount} failed; ${result.skippedCount} skipped`);
|
|
9229
|
+
}
|
|
9230
|
+
if (!result.configured) {
|
|
9231
|
+
break;
|
|
9232
|
+
}
|
|
9233
|
+
if (result.rateLimited) {
|
|
9234
|
+
throttled = true;
|
|
9235
|
+
break;
|
|
9236
|
+
}
|
|
9237
|
+
if (result.nextCursor === null) {
|
|
9238
|
+
break;
|
|
9239
|
+
}
|
|
9240
|
+
cursor = result.nextCursor;
|
|
9241
|
+
}
|
|
9242
|
+
if (options.json) {
|
|
9243
|
+
printSweepJson({
|
|
9244
|
+
configured,
|
|
9245
|
+
dryRun,
|
|
9246
|
+
failed,
|
|
9247
|
+
rateLimited: throttled,
|
|
9248
|
+
resolved,
|
|
9249
|
+
resolvedCount: resolved.length,
|
|
9250
|
+
skipped,
|
|
9251
|
+
skippedCount: skipped.length,
|
|
9252
|
+
unresolved,
|
|
9253
|
+
unresolvedCount: unresolved.length
|
|
9254
|
+
}, failed.length);
|
|
9255
|
+
return;
|
|
9256
|
+
}
|
|
9257
|
+
if (!configured) {
|
|
9258
|
+
console.log("Apple Music backfill is not configured (the Worker's MusicKit secrets are unset) \u2014 nothing resolved.");
|
|
9259
|
+
return;
|
|
9260
|
+
}
|
|
9261
|
+
const verb = dryRun ? "Would resolve" : "Resolved";
|
|
9262
|
+
console.log(`${verb} ${resolved.length} Apple Music URL(s); ${unresolved.length} unresolved; ${failed.length} failed; ${skipped.length} skipped.`);
|
|
9263
|
+
for (const item of resolved) {
|
|
9264
|
+
console.log(` ${item.logId}: ${item.url}`);
|
|
9265
|
+
}
|
|
9266
|
+
for (const item of failed) {
|
|
9267
|
+
console.log(` ${item.logId}: ${item.error}`);
|
|
9268
|
+
}
|
|
9269
|
+
if (failed.length > 0) {
|
|
9270
|
+
process.exitCode = 1;
|
|
9271
|
+
}
|
|
9272
|
+
}
|
|
9184
9273
|
async function runBackfillArtists(options, backfillArtistsCommand2) {
|
|
9185
9274
|
const limit = parseListLimit(options.limit);
|
|
9186
9275
|
const upserted = [];
|
|
@@ -10208,15 +10297,17 @@ async function runCatalogueList(options, catalogueListCommand2) {
|
|
|
10208
10297
|
}
|
|
10209
10298
|
for (const track of tracks) {
|
|
10210
10299
|
const identity = `${track.artists.join(", ")} \u2014 ${track.title}`;
|
|
10300
|
+
const dup = track.duplicateOf;
|
|
10301
|
+
const dupWhy = dup ? `already in the archive \u2014 ${dup.logId ? `${dup.logId} ` : ""}${dup.artists.join(", ")} \u2014 ${dup.title}` : null;
|
|
10211
10302
|
if (options.lens === "capture") {
|
|
10212
10303
|
const reason = track.captureReason;
|
|
10213
|
-
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";
|
|
10304
|
+
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");
|
|
10214
10305
|
console.log(`${String(track.capturePriority ?? 0)} ${identity.padEnd(52)} ${why2}`);
|
|
10215
10306
|
continue;
|
|
10216
10307
|
}
|
|
10217
10308
|
const match = track.nearestFinding;
|
|
10218
10309
|
const score = track.nearestFindingScore?.toFixed(2) ?? "\u2014";
|
|
10219
|
-
const why = match ? `closest to ${match.logId ? `${match.logId} ` : ""}${match.artists.join(", ")} \u2014 ${match.title}` : "nothing to compare it to yet";
|
|
10310
|
+
const why = dupWhy ?? (match ? `closest to ${match.logId ? `${match.logId} ` : ""}${match.artists.join(", ")} \u2014 ${match.title}` : "nothing to compare it to yet");
|
|
10220
10311
|
console.log(`${score} ${identity.padEnd(52)} ${why}`);
|
|
10221
10312
|
}
|
|
10222
10313
|
}
|
package/package.json
CHANGED