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.
Files changed (2) hide show
  1. package/bin/fluncle.mjs +96 -5
  2. 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.137.0".trim() ? "0.137.0".trim() : "0.1.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.137.0".trim() ? "0.137.0".trim() : "0.1.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
@@ -31,5 +31,5 @@
31
31
  "url": "git+https://github.com/mauricekleine/fluncle.git"
32
32
  },
33
33
  "type": "module",
34
- "version": "0.137.0"
34
+ "version": "0.139.0"
35
35
  }