fluncle 0.100.0 → 0.101.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 +221 -2
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -557,7 +557,7 @@ function parseVersion(version) {
557
557
  var currentVersion;
558
558
  var init_version = __esm(() => {
559
559
  init_output();
560
- currentVersion = "0.100.0".trim() ? "0.100.0".trim() : "0.1.0";
560
+ currentVersion = "0.101.0".trim() ? "0.101.0".trim() : "0.1.0";
561
561
  });
562
562
 
563
563
  // src/update-notifier.ts
@@ -1022,6 +1022,24 @@ var init_recent = __esm(() => {
1022
1022
  init_api();
1023
1023
  });
1024
1024
 
1025
+ // src/commands/artists.ts
1026
+ var exports_artists = {};
1027
+ __export(exports_artists, {
1028
+ artistsListCommand: () => artistsListCommand,
1029
+ artistsGetCommand: () => artistsGetCommand
1030
+ });
1031
+ async function artistsListCommand() {
1032
+ const response = await publicApiGet("/api/v1/artists");
1033
+ return response.artists;
1034
+ }
1035
+ async function artistsGetCommand(slug) {
1036
+ const response = await publicApiGet(`/api/v1/artists/${encodeURIComponent(slug)}`);
1037
+ return response.artist;
1038
+ }
1039
+ var init_artists = __esm(() => {
1040
+ init_api();
1041
+ });
1042
+
1025
1043
  // src/commands/mixtape-api.ts
1026
1044
  async function mixtapeListCommand() {
1027
1045
  const response = await adminApiGet("/api/admin/mixtapes");
@@ -2187,7 +2205,7 @@ function parseVersion2(version) {
2187
2205
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2188
2206
  var init_version2 = __esm(() => {
2189
2207
  init_output();
2190
- currentVersion2 = "0.100.0".trim() ? "0.100.0".trim() : "0.1.0";
2208
+ currentVersion2 = "0.101.0".trim() ? "0.101.0".trim() : "0.1.0";
2191
2209
  });
2192
2210
 
2193
2211
  // ../../packages/registry/src/index.ts
@@ -2308,6 +2326,18 @@ var init_src = __esm(() => {
2308
2326
  url: `${SITE}/radio`,
2309
2327
  weights: { web: "secondary" }
2310
2328
  },
2329
+ {
2330
+ discoveryUrl: `${SITE}/llms.txt`,
2331
+ exposedContent: [
2332
+ "/artist/:slug — one artist's page: every published finding from that artist, plus their identity links"
2333
+ ],
2334
+ kind: "web_route",
2335
+ name: "web.artist",
2336
+ operatorNotes: "Slug is real-name kebab-case (e.g. /artist/dbridge). Read-only; only artists with at least one published finding resolve (others 404). No probeConfig — the route is slug-parameterized, so there is no fixed URL to GET-probe.",
2337
+ route: "/artist",
2338
+ url: `${SITE}/artist`,
2339
+ weights: { ssh: "secondary", web: "secondary" }
2340
+ },
2311
2341
  {
2312
2342
  exposedContent: ["the privacy policy"],
2313
2343
  kind: "web_route",
@@ -2420,6 +2450,20 @@ var init_src = __esm(() => {
2420
2450
  url: `${SITE}/api/v1/mixtapes`,
2421
2451
  weights: { web: "secondary" }
2422
2452
  },
2453
+ {
2454
+ apiFormat: "application/json",
2455
+ discoveryUrl: `${SITE}/api/v1/openapi.json`,
2456
+ exposedContent: [
2457
+ "every artist with at least one published finding, finding-count ordered, as JSON",
2458
+ "/api/v1/artists/{slug} — one artist's identity and finding count, as JSON"
2459
+ ],
2460
+ kind: "api",
2461
+ name: "api.artists",
2462
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2463
+ route: "/api/v1/artists",
2464
+ url: `${SITE}/api/v1/artists`,
2465
+ weights: { web: "secondary" }
2466
+ },
2423
2467
  {
2424
2468
  apiFormat: "application/json",
2425
2469
  discoveryUrl: `${SITE}/api/v1/openapi.json`,
@@ -2657,6 +2701,13 @@ var init_src = __esm(() => {
2657
2701
  name: "cli.mixtapes",
2658
2702
  weights: { cli: "secondary" }
2659
2703
  },
2704
+ {
2705
+ command: "fluncle artists",
2706
+ exposedContent: ["every artist with at least one published finding (bare `slug` looks one up)"],
2707
+ kind: "cli",
2708
+ name: "cli.artists",
2709
+ weights: { cli: "secondary" }
2710
+ },
2660
2711
  {
2661
2712
  command: "fluncle open",
2662
2713
  exposedContent: ["pick a track, open it in Spotify"],
@@ -2790,6 +2841,27 @@ var init_src = __esm(() => {
2790
2841
  probeConfig: { cadenceMs: 30 * MINUTE_MS, cronName: "fluncle-backfill", kind: "cron" },
2791
2842
  weights: { status: "hidden" }
2792
2843
  },
2844
+ {
2845
+ exposedContent: [
2846
+ "resolve each artist's social identity: MB url-rel walk + Firecrawl gap-fill (TikTok + YouTube)"
2847
+ ],
2848
+ kind: "cron",
2849
+ name: "cron.artist-sweep",
2850
+ operatorNotes: "every 60m. --no-agent trigger; the Worker does the MB walk + Firecrawl /v2/extract + YouTube channel resolution. Zero on-box tokens. MB rows land as status=auto (trusted); Firecrawl rows as status=candidate (operator-confirm before public). Source: docs/agents/hermes/scripts/artist-sweep.*",
2851
+ probeConfig: { cadenceMs: 60 * MINUTE_MS, cronName: "fluncle-artist-sweep", kind: "cron" },
2852
+ weights: { status: "hidden" }
2853
+ },
2854
+ {
2855
+ command: "fluncle admin artists follow",
2856
+ exposedContent: [
2857
+ "auto-follow high-confidence artists on Spotify + YouTube — the championing motion (--no-agent, Worker HTTP)"
2858
+ ],
2859
+ kind: "cron",
2860
+ name: "cron.artist-follow",
2861
+ operatorNotes: "every 6h. Pure HTTP trigger, zero LLM tokens. Agent tier (the box holds no Spotify/YouTube tokens; the Worker does the PUT /me/following + subscriptions.insert and stamps followed_at). Idempotent by followed_at IS NULL, acting only on status IN (auto, confirmed); quota-paced. Mixcloud is link-only (cut). Source: docs/agents/hermes/scripts/artist-follow-sweep.*. Probed on /status as cron.artist-follow.",
2862
+ probeConfig: { cadenceMs: 6 * 60 * MINUTE_MS, cronName: "fluncle-artist-follow", kind: "cron" },
2863
+ weights: { status: "hidden" }
2864
+ },
2793
2865
  {
2794
2866
  command: "fluncle admin tracks social --capture",
2795
2867
  exposedContent: [
@@ -5100,8 +5172,21 @@ var init_auth_lastfm = __esm(() => {
5100
5172
  // src/commands/admin-artists.ts
5101
5173
  var exports_admin_artists = {};
5102
5174
  __export(exports_admin_artists, {
5175
+ resolveArtistCommand: () => resolveArtistCommand,
5176
+ listArtistsCommand: () => listArtistsCommand,
5177
+ followArtistsCommand: () => followArtistsCommand,
5103
5178
  backfillArtistsCommand: () => backfillArtistsCommand
5104
5179
  });
5180
+ async function listArtistsCommand(limit, cursor) {
5181
+ const params = new URLSearchParams({ limit: String(limit) });
5182
+ if (cursor) {
5183
+ params.set("cursor", cursor);
5184
+ }
5185
+ return adminApiGet(`/api/admin/artists?${params.toString()}`);
5186
+ }
5187
+ async function resolveArtistCommand(artistId) {
5188
+ return adminApiPost(`/api/admin/artists/${encodeURIComponent(artistId)}/resolve`);
5189
+ }
5105
5190
  async function backfillArtistsCommand(limit, dryRun, cursor) {
5106
5191
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
5107
5192
  if (cursor) {
@@ -5109,6 +5194,10 @@ async function backfillArtistsCommand(limit, dryRun, cursor) {
5109
5194
  }
5110
5195
  return adminApiPost(`/api/admin/backfill/artists?${params.toString()}`);
5111
5196
  }
5197
+ async function followArtistsCommand(limit, dryRun) {
5198
+ const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
5199
+ return adminApiPost(`/api/admin/artists/follow?${params.toString()}`);
5200
+ }
5112
5201
  var init_admin_artists = __esm(() => {
5113
5202
  init_api();
5114
5203
  });
@@ -7576,6 +7665,10 @@ function addListenCommands(program2) {
7576
7665
  const { recentCommand: recentCommand3 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
7577
7666
  await runRecent(options, recentCommand3);
7578
7667
  });
7668
+ program2.command("artists").description("Browse artists in Fluncle's archive").argument("[slug]", "Artist slug \u2014 omit for the full list").option("--json", "Print JSON", false).action(async (slug, options) => {
7669
+ const { artistsListCommand: artistsListCommand2, artistsGetCommand: artistsGetCommand2 } = await Promise.resolve().then(() => (init_artists(), exports_artists));
7670
+ await runArtists(slug, options, { artistsGetCommand: artistsGetCommand2, artistsListCommand: artistsListCommand2 });
7671
+ });
7579
7672
  program2.command("mixtapes").description("Fluncle's checkpoint sets").option("--json", "Print JSON", false).action(async (options) => {
7580
7673
  const { mixtapesCommand: mixtapesCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7581
7674
  await runMixtapes(options, mixtapesCommand2);
@@ -7914,6 +8007,14 @@ function addAdminCommands(program2) {
7914
8007
  const { authLastfmCommand: authLastfmCommand2 } = await Promise.resolve().then(() => (init_auth_lastfm(), exports_auth_lastfm));
7915
8008
  await authLastfmCommand2(options);
7916
8009
  });
8010
+ const artists = configureCommand(admin.command("artists").description("Artist entity + championing commands"));
8011
+ artists.action(() => {
8012
+ artists.outputHelp();
8013
+ });
8014
+ artists.command("follow").description("Auto-follow high-confidence artists on Spotify + YouTube (drains the queue)").option("--dry-run", "Report who WOULD be followed without calling the platforms", false).option("--limit <limit>", "Max artists to follow this run", "20").option("--json", "Print JSON", false).action(async (options) => {
8015
+ const { followArtistsCommand: followArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
8016
+ await runFollowArtists(options, followArtistsCommand2);
8017
+ });
7917
8018
  const backfill = configureCommand(admin.command("backfills").description("Backfill operator-only archives"));
7918
8019
  backfill.action(() => {
7919
8020
  backfill.outputHelp();
@@ -7934,6 +8035,15 @@ function addAdminCommands(program2) {
7934
8035
  const { backfillArtistsCommand: backfillArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
7935
8036
  await runBackfillArtists(options, backfillArtistsCommand2);
7936
8037
  });
8038
+ artists.command("resolve").description("Resolve an artist's social identity (MB url-rels + Firecrawl gap-fill)").argument("[artistId]").option("--queue", "Show the resolve worklist (artists awaiting resolution), oldest first", false).option("--limit <limit>", "Number of artists to show with --queue", "50").option("--json", "Print JSON", false).allowExcessArguments().action(async (artistId, options) => {
8039
+ if (options.queue) {
8040
+ const { listArtistsCommand: listArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
8041
+ await runArtistResolveQueue(options, listArtistsCommand2);
8042
+ return;
8043
+ }
8044
+ const { resolveArtistCommand: resolveArtistCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
8045
+ await runArtistResolve(artistId, options, resolveArtistCommand2);
8046
+ });
7937
8047
  }
7938
8048
  async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCommand2) {
7939
8049
  if (!idOrLogId || !options.file || !options.source || !options.mime) {
@@ -8191,6 +8301,88 @@ async function runBackfillArtists(options, backfillArtistsCommand2) {
8191
8301
  console.log(` ${item.logId}: ${item.error}`);
8192
8302
  }
8193
8303
  }
8304
+ async function runArtistResolveQueue(options, listArtistsCommand2) {
8305
+ const limit = parseListLimit(options.limit);
8306
+ const result = await listArtistsCommand2(limit ?? 50);
8307
+ if (options.json) {
8308
+ printJson({ artists: result.artists, ok: true });
8309
+ return;
8310
+ }
8311
+ if (result.artists.length === 0) {
8312
+ console.log("Every artist has been resolved. Nothing waiting on the sweep.");
8313
+ return;
8314
+ }
8315
+ const noun = result.artists.length === 1 ? "artist" : "artists";
8316
+ console.log(`${result.artists.length} ${noun} awaiting resolution, oldest first:`);
8317
+ for (const artist of result.artists) {
8318
+ console.log(` ${artist.id} ${artist.name}`);
8319
+ }
8320
+ }
8321
+ async function runArtistResolve(artistId, options, resolveArtistCommand2) {
8322
+ if (!artistId) {
8323
+ throw new Error("Usage: fluncle admin artists resolve <artist_id> [--json]");
8324
+ }
8325
+ const result = await resolveArtistCommand2(artistId);
8326
+ if (options.json) {
8327
+ printJson(result);
8328
+ return;
8329
+ }
8330
+ if (result.rateLimited) {
8331
+ console.log(`MusicBrainz throttled the walk for ${artistId}. The sweep will swing back around.`);
8332
+ return;
8333
+ }
8334
+ const mbid = result.mbid ?? "(no MusicBrainz match)";
8335
+ const noun = result.socialsCount === 1 ? "social" : "socials";
8336
+ console.log(`Resolved ${artistId}: mbid=${mbid}, ${result.socialsCount} ${noun}.`);
8337
+ for (const social of result.socials) {
8338
+ console.log(` ${social.platform} (${social.source}): ${social.url}`);
8339
+ }
8340
+ if (result.wikidataQid) {
8341
+ console.log(` wikidata: ${result.wikidataQid}`);
8342
+ }
8343
+ }
8344
+ async function runFollowArtists(options, followArtistsCommand2) {
8345
+ const limit = parseListLimit(options.limit);
8346
+ const followed = [];
8347
+ const failed = [];
8348
+ let dryRun = options.dryRun;
8349
+ let remaining = 0;
8350
+ while (followed.length + failed.length < limit) {
8351
+ const batchLimit = Math.min(50, limit - (followed.length + failed.length));
8352
+ const result = await followArtistsCommand2(batchLimit, options.dryRun);
8353
+ dryRun = result.dryRun;
8354
+ remaining = result.remaining;
8355
+ followed.push(...result.followed);
8356
+ failed.push(...result.failed);
8357
+ if (!options.json) {
8358
+ const verb2 = result.dryRun ? "would follow" : "followed";
8359
+ console.log(` \u2026${verb2} ${result.followedCount}; ${result.failedCount} failed; ${result.remaining} remaining`);
8360
+ }
8361
+ if (remaining === 0 || result.dryRun || result.followedCount === 0) {
8362
+ break;
8363
+ }
8364
+ }
8365
+ if (options.json) {
8366
+ printJson({
8367
+ dryRun,
8368
+ failed,
8369
+ failedCount: failed.length,
8370
+ followed,
8371
+ followedCount: followed.length,
8372
+ ok: true,
8373
+ remaining
8374
+ });
8375
+ return;
8376
+ }
8377
+ const verb = dryRun ? "Would follow" : "Followed";
8378
+ console.log(`${verb} ${followed.length} artist(s); ${failed.length} failed; ${remaining} remaining.`);
8379
+ for (const item of followed) {
8380
+ console.log(` ${item.platform}: ${item.artistName}`);
8381
+ }
8382
+ for (const item of failed) {
8383
+ console.log(` ${item.platform} ${item.socialId}: ${item.error}`);
8384
+ }
8385
+ }
8194
8386
  async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
8195
8387
  if (!idOrLogId) {
8196
8388
  throw new Error("Missing id. Usage: fluncle admin tracks video <track_id|log_id> (--dir <dir> | --footage <file> [--footage-social <file>] [--footage-notext <file>] [--footage-landscape <file>] [--footage-landscape-social <file>] [--poster <file>] [--cover <file>] [--note <file>] [--composition <file>] [--props <file>] [--render <file>] [--intent <file>] [--metrics <file>] [--scene <file>] | --plate <file> [--plate-background <file>]) [--allow-partial]");
@@ -8694,6 +8886,33 @@ async function runRecent(options, recentCommand3) {
8694
8886
  console.log(trackRows2(tracks).join(`
8695
8887
  `));
8696
8888
  }
8889
+ async function runArtists(slug, options, commands) {
8890
+ if (slug) {
8891
+ const artist = await commands.artistsGetCommand(slug);
8892
+ if (options.json) {
8893
+ printJson({ artist, ok: true });
8894
+ return;
8895
+ }
8896
+ console.log(`${artist.name} (${artist.slug})`);
8897
+ console.log(`Findings: ${artist.findingCount}`);
8898
+ if (artist.spotifyUrl) {
8899
+ console.log(`Spotify: ${artist.spotifyUrl}`);
8900
+ }
8901
+ return;
8902
+ }
8903
+ const artists = await commands.artistsListCommand();
8904
+ if (options.json) {
8905
+ printJson({ artists, ok: true });
8906
+ return;
8907
+ }
8908
+ if (artists.length === 0) {
8909
+ console.log("No artists in the archive yet.");
8910
+ return;
8911
+ }
8912
+ for (const a of artists) {
8913
+ console.log(`${a.name.padEnd(40)} ${String(a.findingCount).padStart(3)} finding${a.findingCount === 1 ? "" : "s"}`);
8914
+ }
8915
+ }
8697
8916
  async function runMixtapes(options, mixtapesCommand2) {
8698
8917
  const mixtapes = await mixtapesCommand2();
8699
8918
  if (options.json) {
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.100.0"
34
+ "version": "0.101.0"
35
35
  }