fluncle 0.162.0 → 0.164.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 +99 -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.162.0".trim() ? "0.162.0".trim() : "0.1.0";
564
+ currentVersion = "0.164.0".trim() ? "0.164.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.162.0".trim() ? "0.162.0".trim() : "0.1.0";
2192
+ currentVersion2 = "0.164.0".trim() ? "0.164.0".trim() : "0.1.0";
2193
2193
  });
2194
2194
 
2195
2195
  // ../../packages/registry/src/index.ts
@@ -2513,6 +2513,18 @@ var init_src = __esm(() => {
2513
2513
  url: `${SITE}/api/v1/tracks/random`,
2514
2514
  weights: { web: "secondary" }
2515
2515
  },
2516
+ {
2517
+ apiFormat: "application/json",
2518
+ discoveryUrl: `${SITE}/api/v1/openapi.json`,
2519
+ exposedContent: [
2520
+ "what just came out — newest drum & bass releases over a 30-day window, flat (limit max 100)"
2521
+ ],
2522
+ kind: "api",
2523
+ name: "api.fresh",
2524
+ route: "/api/v1/tracks/fresh",
2525
+ url: `${SITE}/api/v1/tracks/fresh`,
2526
+ weights: { web: "secondary" }
2527
+ },
2516
2528
  {
2517
2529
  apiFormat: "application/json",
2518
2530
  discoveryUrl: `${SITE}/api/v1/openapi.json`,
@@ -3024,6 +3036,19 @@ var init_src = __esm(() => {
3024
3036
  title: "Label logos",
3025
3037
  weights: { status: "hidden" }
3026
3038
  },
3039
+ {
3040
+ command: "fluncle admin backfills recording-mbids",
3041
+ exposedContent: [
3042
+ "fill each track's canonical MusicBrainz recording MBID (crawler PK strip + ISRC resolve)"
3043
+ ],
3044
+ kind: "cron",
3045
+ name: "cron.recording-mbids",
3046
+ operatorNotes: "every 60m, run by a rave-02 HOST systemd timer (docs/agents/hermes/recording-mbids-timer/). The MusicBrainz identity layer: gives every track its canonical MusicBrainz recording MBID — the one identifier that reconciles a track to the wider open music graph (MusicBrainz, Wikidata) and the anchor the `/log` MusicRecording emits as a `sameAs` + a KG `identifier`. Two fill paths: a FREE SQL strip of crawler-born rows' PK (`mb_<recording-mbid>` → the `mb_recording_id` column, no vendor call), then an ISRC→recording resolve of the findings/Spotify-born tail through the shared MusicBrainz client (`/isrc/<isrc>`). New crawler rows already carry the MBID at mint time, so this cron catches history up + drains the ISRC tail. METADATA IDENTITY ONLY — it certifies nothing and publishes nothing (agent tier, the `backfill_label_images` precedent). Worker-paced (the box holds no MusicBrainz budget): one bounded batch per tick, 1 req/s, circuit-broken on a throttle. The `tracks` row carries the durable reliability state (`mb_recording_id` + the `mb_recording_id_attempted_at` stamp, a miss stamped so it is not re-queried forever). Zero LLM tokens. Source: docs/agents/hermes/scripts/recording-mbids-sweep.*. See docs/catalogue-crawler.md.",
3047
+ probeConfig: { cadenceMs: 60 * MINUTE_MS, cronName: "fluncle-recording-mbids", kind: "cron" },
3048
+ statusDescription: "fills each track's MusicBrainz recording id",
3049
+ title: "Recording MBIDs",
3050
+ weights: { status: "hidden" }
3051
+ },
3027
3052
  {
3028
3053
  command: "fluncle admin backfills cover-masters",
3029
3054
  exposedContent: [
@@ -3879,6 +3904,7 @@ __export(exports_admin_tracks, {
3879
3904
  embedQueueCommand: () => embedQueueCommand,
3880
3905
  contextQueueCommand: () => contextQueueCommand,
3881
3906
  captureQueueCommand: () => captureQueueCommand,
3907
+ backfillRecordingMbidsCommand: () => backfillRecordingMbidsCommand,
3882
3908
  backfillLastfmCommand: () => backfillLastfmCommand,
3883
3909
  backfillDiscogsCommand: () => backfillDiscogsCommand,
3884
3910
  backfillAppleMusicCommand: () => backfillAppleMusicCommand,
@@ -4066,6 +4092,13 @@ async function backfillAppleCatalogueCommand(limit, dryRun) {
4066
4092
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4067
4093
  return adminApiPost(`/api/admin/backfill/apple-catalogue?${params.toString()}`);
4068
4094
  }
4095
+ async function backfillRecordingMbidsCommand(limit, dryRun, cursor) {
4096
+ const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4097
+ if (cursor) {
4098
+ params.set("cursor", cursor);
4099
+ }
4100
+ return adminApiPost(`/api/admin/backfill/recording-mbids?${params.toString()}`);
4101
+ }
4069
4102
  async function vehiclesCommand(limit) {
4070
4103
  const tracks = await fetchAdminTracks({ hasVideo: true, max: limit, order: "desc" });
4071
4104
  return tracks.map((track) => ({
@@ -5320,7 +5353,7 @@ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:f
5320
5353
  async function logbookGapsCommand(limit) {
5321
5354
  const query = typeof limit === "number" ? `?limit=${encodeURIComponent(String(limit))}` : "";
5322
5355
  const response = await adminApiGet(`/api/admin/logbook/gaps${query}`);
5323
- return response.gaps;
5356
+ return { gaps: response.gaps, spent: response.spent };
5324
5357
  }
5325
5358
  function resolveBody(options) {
5326
5359
  if (options.body !== undefined) {
@@ -9400,6 +9433,10 @@ function addAdminCommands(program2) {
9400
9433
  const { backfillLabelImagesCommand: backfillLabelImagesCommand2 } = await Promise.resolve().then(() => (init_admin_labels(), exports_admin_labels));
9401
9434
  await runBackfillLabelImages(options, backfillLabelImagesCommand2);
9402
9435
  });
9436
+ backfill.command("recording-mbids").description("Fill MusicBrainz recording MBIDs (crawler PK strip + ISRC resolve) over tracks").option("--dry-run", "Report the eligible worklist without any vendor call or write", false).option("--limit <limit>", "Max ISRC lookups to process", "50").option("--json", "Print JSON", false).action(async (options) => {
9437
+ const { backfillRecordingMbidsCommand: backfillRecordingMbidsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
9438
+ await runBackfillRecordingMbids(options, backfillRecordingMbidsCommand2);
9439
+ });
9403
9440
  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("--limit <limit>", "Max entities to process", "50").option("--json", "Print JSON", false).action(async (options) => {
9404
9441
  const { backfillCoverMastersCommand: backfillCoverMastersCommand2 } = await Promise.resolve().then(() => (init_admin_covers(), exports_admin_covers));
9405
9442
  await runBackfillCoverMasters(options, backfillCoverMastersCommand2);
@@ -10251,6 +10288,63 @@ async function runBackfillLabelImages(options, backfillLabelImagesCommand2) {
10251
10288
  process.exitCode = 1;
10252
10289
  }
10253
10290
  }
10291
+ async function runBackfillRecordingMbids(options, backfillRecordingMbidsCommand2) {
10292
+ const limit = parseListLimit(options.limit);
10293
+ const resolved = [];
10294
+ const missed = [];
10295
+ const failed = [];
10296
+ let cursor;
10297
+ let dryRun = options.dryRun;
10298
+ let prefixStripped = 0;
10299
+ let throttled = false;
10300
+ while (resolved.length + missed.length + failed.length < limit) {
10301
+ const remaining = limit - (resolved.length + missed.length + failed.length);
10302
+ const result = await backfillRecordingMbidsCommand2(remaining, options.dryRun, cursor);
10303
+ dryRun = result.dryRun;
10304
+ prefixStripped += result.prefixStripped;
10305
+ resolved.push(...result.resolved);
10306
+ missed.push(...result.missed);
10307
+ failed.push(...result.failed);
10308
+ if (!options.json) {
10309
+ const verb2 = result.dryRun ? "would resolve" : "resolved";
10310
+ console.log(` \u2026stripped ${result.prefixStripped} from PK; ${verb2} ${result.resolvedCount} by ISRC; ${result.missedCount} not in MusicBrainz; ${result.failedCount} failed`);
10311
+ }
10312
+ if (result.rateLimited) {
10313
+ throttled = true;
10314
+ break;
10315
+ }
10316
+ if (result.nextCursor === null) {
10317
+ break;
10318
+ }
10319
+ cursor = result.nextCursor;
10320
+ }
10321
+ if (options.json) {
10322
+ printJson({
10323
+ dryRun,
10324
+ failed,
10325
+ failedCount: failed.length,
10326
+ missed,
10327
+ missedCount: missed.length,
10328
+ ok: true,
10329
+ prefixStripped,
10330
+ rateLimited: throttled,
10331
+ resolved,
10332
+ resolvedCount: resolved.length
10333
+ });
10334
+ if (failed.length > 0) {
10335
+ process.exitCode = 1;
10336
+ }
10337
+ return;
10338
+ }
10339
+ const verb = dryRun ? "Would fill" : "Filled";
10340
+ console.log(`${verb} ${prefixStripped} recording MBID(s) from crawler PKs; ${resolved.length} by ISRC; ${missed.length} not in MusicBrainz; ${failed.length} failed.`);
10341
+ for (const item of failed) {
10342
+ console.log(` ${item.trackId}: ${item.error}`);
10343
+ }
10344
+ if (failed.length > 0) {
10345
+ process.exitCode = 1;
10346
+ }
10347
+ }
10254
10348
  async function runArtistResolveQueue(options, listArtistsCommand2) {
10255
10349
  const limit = parseListLimit(options.limit);
10256
10350
  const result = await listArtistsCommand2(limit ?? 50);
@@ -10866,9 +10960,9 @@ async function runNewsletterList(options, newsletterListCommand2) {
10866
10960
  }
10867
10961
  async function runLogbookGaps(options, logbookGapsCommand2) {
10868
10962
  const limit = options.limit === undefined ? undefined : Number.parseInt(options.limit, 10);
10869
- const gaps = await logbookGapsCommand2(limit !== undefined && Number.isFinite(limit) ? limit : undefined);
10963
+ const { gaps, spent } = await logbookGapsCommand2(limit !== undefined && Number.isFinite(limit) ? limit : undefined);
10870
10964
  if (options.json) {
10871
- printJson({ gaps, ok: true });
10965
+ printJson({ gaps, ok: true, spent });
10872
10966
  return;
10873
10967
  }
10874
10968
  if (gaps.length === 0) {
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.162.0"
34
+ "version": "0.164.0"
35
35
  }