fluncle 0.99.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.
- package/bin/fluncle.mjs +286 -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.
|
|
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.
|
|
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: [
|
|
@@ -5097,6 +5169,39 @@ var init_auth_lastfm = __esm(() => {
|
|
|
5097
5169
|
init_api();
|
|
5098
5170
|
});
|
|
5099
5171
|
|
|
5172
|
+
// src/commands/admin-artists.ts
|
|
5173
|
+
var exports_admin_artists = {};
|
|
5174
|
+
__export(exports_admin_artists, {
|
|
5175
|
+
resolveArtistCommand: () => resolveArtistCommand,
|
|
5176
|
+
listArtistsCommand: () => listArtistsCommand,
|
|
5177
|
+
followArtistsCommand: () => followArtistsCommand,
|
|
5178
|
+
backfillArtistsCommand: () => backfillArtistsCommand
|
|
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
|
+
}
|
|
5190
|
+
async function backfillArtistsCommand(limit, dryRun, cursor) {
|
|
5191
|
+
const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
|
|
5192
|
+
if (cursor) {
|
|
5193
|
+
params.set("cursor", cursor);
|
|
5194
|
+
}
|
|
5195
|
+
return adminApiPost(`/api/admin/backfill/artists?${params.toString()}`);
|
|
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
|
+
}
|
|
5201
|
+
var init_admin_artists = __esm(() => {
|
|
5202
|
+
init_api();
|
|
5203
|
+
});
|
|
5204
|
+
|
|
5100
5205
|
// src/interactive.ts
|
|
5101
5206
|
var exports_interactive = {};
|
|
5102
5207
|
__export(exports_interactive, {
|
|
@@ -7560,6 +7665,10 @@ function addListenCommands(program2) {
|
|
|
7560
7665
|
const { recentCommand: recentCommand3 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
7561
7666
|
await runRecent(options, recentCommand3);
|
|
7562
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
|
+
});
|
|
7563
7672
|
program2.command("mixtapes").description("Fluncle's checkpoint sets").option("--json", "Print JSON", false).action(async (options) => {
|
|
7564
7673
|
const { mixtapesCommand: mixtapesCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7565
7674
|
await runMixtapes(options, mixtapesCommand2);
|
|
@@ -7898,6 +8007,14 @@ function addAdminCommands(program2) {
|
|
|
7898
8007
|
const { authLastfmCommand: authLastfmCommand2 } = await Promise.resolve().then(() => (init_auth_lastfm(), exports_auth_lastfm));
|
|
7899
8008
|
await authLastfmCommand2(options);
|
|
7900
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
|
+
});
|
|
7901
8018
|
const backfill = configureCommand(admin.command("backfills").description("Backfill operator-only archives"));
|
|
7902
8019
|
backfill.action(() => {
|
|
7903
8020
|
backfill.outputHelp();
|
|
@@ -7914,6 +8031,19 @@ function addAdminCommands(program2) {
|
|
|
7914
8031
|
const { backfillDiscogsCommand: backfillDiscogsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7915
8032
|
await runBackfillDiscogs(options, backfillDiscogsCommand2);
|
|
7916
8033
|
});
|
|
8034
|
+
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) => {
|
|
8035
|
+
const { backfillArtistsCommand: backfillArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
|
|
8036
|
+
await runBackfillArtists(options, backfillArtistsCommand2);
|
|
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
|
+
});
|
|
7917
8047
|
}
|
|
7918
8048
|
async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCommand2) {
|
|
7919
8049
|
if (!idOrLogId || !options.file || !options.source || !options.mime) {
|
|
@@ -8126,6 +8256,133 @@ async function runBackfillDiscogs(options, backfillDiscogsCommand2) {
|
|
|
8126
8256
|
console.log(` ${item.logId}: release ${item.releaseId}${master}`);
|
|
8127
8257
|
}
|
|
8128
8258
|
}
|
|
8259
|
+
async function runBackfillArtists(options, backfillArtistsCommand2) {
|
|
8260
|
+
const limit = parseListLimit(options.limit);
|
|
8261
|
+
const upserted = [];
|
|
8262
|
+
const failed = [];
|
|
8263
|
+
const skipped = [];
|
|
8264
|
+
let cursor;
|
|
8265
|
+
let dryRun = options.dryRun;
|
|
8266
|
+
while (upserted.length + failed.length < limit) {
|
|
8267
|
+
const remaining = limit - (upserted.length + failed.length);
|
|
8268
|
+
const result = await backfillArtistsCommand2(remaining, options.dryRun, cursor);
|
|
8269
|
+
dryRun = result.dryRun;
|
|
8270
|
+
upserted.push(...result.upserted);
|
|
8271
|
+
failed.push(...result.failed);
|
|
8272
|
+
skipped.push(...result.skipped);
|
|
8273
|
+
if (!options.json) {
|
|
8274
|
+
const verb2 = result.dryRun ? "would upsert" : "upserted";
|
|
8275
|
+
console.log(` \u2026${verb2} ${result.upsertedCount}; ${result.failedCount} failed; ${result.skippedCount} skipped`);
|
|
8276
|
+
}
|
|
8277
|
+
if (result.nextCursor === null) {
|
|
8278
|
+
break;
|
|
8279
|
+
}
|
|
8280
|
+
cursor = result.nextCursor;
|
|
8281
|
+
}
|
|
8282
|
+
if (options.json) {
|
|
8283
|
+
printJson({
|
|
8284
|
+
dryRun,
|
|
8285
|
+
failed,
|
|
8286
|
+
failedCount: failed.length,
|
|
8287
|
+
ok: true,
|
|
8288
|
+
skipped,
|
|
8289
|
+
skippedCount: skipped.length,
|
|
8290
|
+
upserted,
|
|
8291
|
+
upsertedCount: upserted.length
|
|
8292
|
+
});
|
|
8293
|
+
return;
|
|
8294
|
+
}
|
|
8295
|
+
const verb = dryRun ? "Would upsert" : "Upserted";
|
|
8296
|
+
console.log(`${verb} ${upserted.length} artist entity row(s); ${failed.length} failed; ${skipped.length} skipped.`);
|
|
8297
|
+
for (const logId of upserted) {
|
|
8298
|
+
console.log(` ${logId}`);
|
|
8299
|
+
}
|
|
8300
|
+
for (const item of failed) {
|
|
8301
|
+
console.log(` ${item.logId}: ${item.error}`);
|
|
8302
|
+
}
|
|
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
|
+
}
|
|
8129
8386
|
async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
8130
8387
|
if (!idOrLogId) {
|
|
8131
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]");
|
|
@@ -8629,6 +8886,33 @@ async function runRecent(options, recentCommand3) {
|
|
|
8629
8886
|
console.log(trackRows2(tracks).join(`
|
|
8630
8887
|
`));
|
|
8631
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
|
+
}
|
|
8632
8916
|
async function runMixtapes(options, mixtapesCommand2) {
|
|
8633
8917
|
const mixtapes = await mixtapesCommand2();
|
|
8634
8918
|
if (options.json) {
|
package/package.json
CHANGED