fluncle 0.180.0 → 0.182.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 +104 -2
  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.180.0".trim() ? "0.180.0".trim() : "0.1.0";
564
+ currentVersion = "0.182.0".trim() ? "0.182.0".trim() : "0.1.0";
565
565
  });
566
566
 
567
567
  // src/update-notifier.ts
@@ -2260,7 +2260,7 @@ function parseVersion2(version) {
2260
2260
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2261
2261
  var init_version2 = __esm(() => {
2262
2262
  init_output();
2263
- currentVersion2 = "0.180.0".trim() ? "0.180.0".trim() : "0.1.0";
2263
+ currentVersion2 = "0.182.0".trim() ? "0.182.0".trim() : "0.1.0";
2264
2264
  });
2265
2265
 
2266
2266
  // ../../packages/registry/src/index.ts
@@ -2504,6 +2504,14 @@ var init_src = __esm(() => {
2504
2504
  url: `${SITE}/privacy`,
2505
2505
  weights: { web: "tertiary" }
2506
2506
  },
2507
+ {
2508
+ exposedContent: ["the terms of use"],
2509
+ kind: "web_route",
2510
+ name: "web.terms",
2511
+ route: "/terms",
2512
+ url: `${SITE}/terms`,
2513
+ weights: { web: "tertiary" }
2514
+ },
2507
2515
  {
2508
2516
  exposedContent: ["the Galaxy game's front door (root rewrites to /galaxy)"],
2509
2517
  kind: "subdomain",
@@ -3204,6 +3212,19 @@ var init_src = __esm(() => {
3204
3212
  title: "Recording MBIDs",
3205
3213
  weights: { status: "hidden" }
3206
3214
  },
3215
+ {
3216
+ command: "fluncle admin backfills artist-edges",
3217
+ exposedContent: [
3218
+ "fold each edge-less track's artists_json names onto existing artist identities → track_artists edges"
3219
+ ],
3220
+ kind: "cron",
3221
+ name: "cron.artist-edges",
3222
+ operatorNotes: "every 60m, run by a rave-02 HOST systemd timer (docs/agents/hermes/artist-edges-timer/). The track_artists GRAPH BACKFILL (RFC artist-primary-capture, slice 0): the graph is crawl-era-only (born 2026-07-15) — only ~12.3k of ~37.5k tracks carry edges — so this folds each edge-less track's `artists_json` NAMES onto EXISTING `artists` identities and writes the `track_artists` edges, making the graph as full as honest matching allows (slice 1's identity-keyed capture authorization reads it). The matcher is IDENTITY-HONEST: each name matches by exact case-insensitive fold, then via `artist_aliases` (kind='name', status auto|confirmed — the search resolver's alias semantics); a fold two distinct identities share is ambiguous and matches nothing (fail-closed). It MINTS NOTHING — a bare name is not enough identity to create an entity — and reports the UNMATCHED RESIDUAL (credited names with no identity), which decides whether a later paced MusicBrainz credit-sweep is worth running. METADATA / GRAPH IDENTITY ONLY — it certifies nothing and publishes nothing (agent tier, the `backfill_recording_mbids` precedent). Worker-paced with NO vendor call (pure DB set-based matching, so no rate limit / circuit breaker): one bounded batch per tick folds the whole ~1.8k-row artist+alias corpus into one in-memory map and matches each track batch against it. The `tracks` row carries the durable reliability state (the `artist_edges_backfilled_at` stamp on EVERY visited row — matched, partial, or zero — so the worklist drains and a re-run is a no-op). New tracks are minted WITH edges (publish path + crawler link), so this catches history up and drains in a handful of ticks. Zero LLM tokens. Source: docs/agents/hermes/scripts/artist-edges-sweep.*. See docs/artist-relationship.md.",
3223
+ probeConfig: { cadenceMs: 60 * MINUTE_MS, cronName: "fluncle-artist-edges", kind: "cron" },
3224
+ statusDescription: "folds artists_json names onto artist identities",
3225
+ title: "Artist edges",
3226
+ weights: { status: "hidden" }
3227
+ },
3207
3228
  {
3208
3229
  command: "fluncle admin backfills label-lineage",
3209
3230
  exposedContent: [
@@ -4128,6 +4149,7 @@ __export(exports_admin_tracks, {
4128
4149
  backfillRecordingMbidsCommand: () => backfillRecordingMbidsCommand,
4129
4150
  backfillLastfmCommand: () => backfillLastfmCommand,
4130
4151
  backfillDiscogsCommand: () => backfillDiscogsCommand,
4152
+ backfillArtistEdgesCommand: () => backfillArtistEdgesCommand,
4131
4153
  backfillAppleMusicCommand: () => backfillAppleMusicCommand,
4132
4154
  backfillAppleCatalogueCommand: () => backfillAppleCatalogueCommand
4133
4155
  });
@@ -4320,6 +4342,13 @@ async function backfillRecordingMbidsCommand(limit, dryRun, cursor) {
4320
4342
  }
4321
4343
  return adminApiPost(`/api/admin/backfill/recording-mbids?${params.toString()}`);
4322
4344
  }
4345
+ async function backfillArtistEdgesCommand(limit, dryRun, cursor) {
4346
+ const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4347
+ if (cursor) {
4348
+ params.set("cursor", cursor);
4349
+ }
4350
+ return adminApiPost(`/api/admin/backfill/artist-edges?${params.toString()}`);
4351
+ }
4323
4352
  async function vehiclesCommand(limit) {
4324
4353
  const tracks = await fetchAdminTracks({ hasVideo: true, max: limit, order: "desc" });
4325
4354
  return tracks.map((track) => ({
@@ -6108,6 +6137,23 @@ var init_mixtape_mixcloud2 = __esm(() => {
6108
6137
  PICTURE_MAX_BYTES2 = 10 * 1024 * 1024;
6109
6138
  });
6110
6139
 
6140
+ // src/commands/auth-tiktok.ts
6141
+ var exports_auth_tiktok = {};
6142
+ __export(exports_auth_tiktok, {
6143
+ authTikTokCommand: () => authTikTokCommand
6144
+ });
6145
+ async function authTikTokCommand() {
6146
+ const response = await adminApiGet("/api/admin/tiktok/auth/start");
6147
+ console.log(`Open this TikTok authorization URL:
6148
+
6149
+ ${response.authUrl}
6150
+
6151
+ After approving access, TikTok returns to the Fluncle admin callback and stores the refresh token server-side.`);
6152
+ }
6153
+ var init_auth_tiktok = __esm(() => {
6154
+ init_api();
6155
+ });
6156
+
6111
6157
  // src/commands/auth-lastfm.ts
6112
6158
  var exports_auth_lastfm = {};
6113
6159
  __export(exports_auth_lastfm, {
@@ -9433,6 +9479,10 @@ function addAdminCommands(program2) {
9433
9479
  const { authMixcloudCommand: authMixcloudCommand3 } = await Promise.resolve().then(() => (init_mixtape_mixcloud2(), exports_mixtape_mixcloud2));
9434
9480
  await authMixcloudCommand3();
9435
9481
  });
9482
+ auth.command("tiktok").description("Authorize TikTok access (Display API per-video metrics)").action(async () => {
9483
+ const { authTikTokCommand: authTikTokCommand2 } = await Promise.resolve().then(() => (init_auth_tiktok(), exports_auth_tiktok));
9484
+ await authTikTokCommand2();
9485
+ });
9436
9486
  auth.command("lastfm").description("Authorize Last.fm access (love-on-add). Run once for the URL, then again with --token").option("--token <token>", "The approved request token, to mint the session key").action(async (options) => {
9437
9487
  const { authLastfmCommand: authLastfmCommand2 } = await Promise.resolve().then(() => (init_auth_lastfm(), exports_auth_lastfm));
9438
9488
  await authLastfmCommand2(options);
@@ -9712,6 +9762,10 @@ function addAdminCommands(program2) {
9712
9762
  const { backfillRecordingMbidsCommand: backfillRecordingMbidsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
9713
9763
  await runBackfillRecordingMbids(options, backfillRecordingMbidsCommand2);
9714
9764
  });
9765
+ backfill.command("artist-edges").description("Fold artists_json names onto existing artist identities \u2192 track_artists").option("--dry-run", "Report the classification without writing edges or stamps", false).option("--limit <limit>", "Max tracks to visit", "200").option("--json", "Print JSON", false).action(async (options) => {
9766
+ const { backfillArtistEdgesCommand: backfillArtistEdgesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
9767
+ await runBackfillArtistEdges(options, backfillArtistEdgesCommand2);
9768
+ });
9715
9769
  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) => {
9716
9770
  const { backfillCoverMastersCommand: backfillCoverMastersCommand2 } = await Promise.resolve().then(() => (init_admin_covers(), exports_admin_covers));
9717
9771
  await runBackfillCoverMasters(options, backfillCoverMastersCommand2);
@@ -10711,6 +10765,54 @@ async function runBackfillRecordingMbids(options, backfillRecordingMbidsCommand2
10711
10765
  process.exitCode = 1;
10712
10766
  }
10713
10767
  }
10768
+ async function runBackfillArtistEdges(options, backfillArtistEdgesCommand2) {
10769
+ const limit = parseListLimit(options.limit) ?? 200;
10770
+ const fullyMatched = [];
10771
+ const partiallyMatched = [];
10772
+ const zeroMatched = [];
10773
+ let cursor;
10774
+ let dryRun = options.dryRun;
10775
+ let edgesWritten = 0;
10776
+ let scanned = 0;
10777
+ let unmatchedNames = 0;
10778
+ while (scanned < limit) {
10779
+ const remaining = limit - scanned;
10780
+ const result = await backfillArtistEdgesCommand2(remaining, options.dryRun, cursor);
10781
+ dryRun = result.dryRun;
10782
+ scanned += result.scanned;
10783
+ edgesWritten += result.edgesWritten;
10784
+ unmatchedNames += result.unmatchedNames;
10785
+ fullyMatched.push(...result.fullyMatched);
10786
+ partiallyMatched.push(...result.partiallyMatched);
10787
+ zeroMatched.push(...result.zeroMatched);
10788
+ if (!options.json) {
10789
+ const verb2 = result.dryRun ? "would write" : "wrote";
10790
+ console.log(` \u2026visited ${result.scanned}; ${verb2} ${result.edgesWritten} edge(s); ${result.fullyMatchedCount} full, ${result.partiallyMatchedCount} partial, ${result.zeroMatchedCount} zero; ${result.unmatchedNames} name(s) unmatched`);
10791
+ }
10792
+ if (result.nextCursor === null) {
10793
+ break;
10794
+ }
10795
+ cursor = result.nextCursor;
10796
+ }
10797
+ if (options.json) {
10798
+ printJson({
10799
+ dryRun,
10800
+ edgesWritten,
10801
+ fullyMatched,
10802
+ fullyMatchedCount: fullyMatched.length,
10803
+ ok: true,
10804
+ partiallyMatched,
10805
+ partiallyMatchedCount: partiallyMatched.length,
10806
+ scanned,
10807
+ unmatchedNames,
10808
+ zeroMatched,
10809
+ zeroMatchedCount: zeroMatched.length
10810
+ });
10811
+ return;
10812
+ }
10813
+ const verb = dryRun ? "Would write" : "Wrote";
10814
+ 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.`);
10815
+ }
10714
10816
  async function runArtistResolveQueue(options, listArtistsCommand2) {
10715
10817
  const limit = parseListLimit(options.limit);
10716
10818
  const result = await listArtistsCommand2(limit ?? 50);
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.180.0"
34
+ "version": "0.182.0"
35
35
  }