fluncle 0.66.0 → 0.68.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 +107 -12
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -423,7 +423,7 @@ function parseVersion(version) {
423
423
  var currentVersion;
424
424
  var init_version = __esm(() => {
425
425
  init_output();
426
- currentVersion = "0.66.0".trim() ? "0.66.0".trim() : "0.1.0";
426
+ currentVersion = "0.68.0".trim() ? "0.68.0".trim() : "0.1.0";
427
427
  });
428
428
 
429
429
  // src/update-notifier.ts
@@ -2138,10 +2138,13 @@ function parseVersion2(version) {
2138
2138
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2139
2139
  var init_version2 = __esm(() => {
2140
2140
  init_output();
2141
- currentVersion2 = "0.66.0".trim() ? "0.66.0".trim() : "0.1.0";
2141
+ currentVersion2 = "0.68.0".trim() ? "0.68.0".trim() : "0.1.0";
2142
2142
  });
2143
2143
 
2144
2144
  // ../../packages/registry/src/index.ts
2145
+ function liveSurfaces() {
2146
+ return SURFACES.filter((surface) => surface.pending !== true);
2147
+ }
2145
2148
  var SITE = "https://www.fluncle.com", PROBE_CADENCE_MS, PROBE_TIMEOUT_MS, MINUTE_MS, SURFACES;
2146
2149
  var init_src = __esm(() => {
2147
2150
  PROBE_CADENCE_MS = 10 * 60 * 1000;
@@ -2664,6 +2667,18 @@ var init_src = __esm(() => {
2664
2667
  operatorNotes: "Authenticated admin/agent tier. The enrichment crons drive a subset of these.",
2665
2668
  weights: { cli: "hidden" }
2666
2669
  },
2670
+ {
2671
+ exposedContent: [
2672
+ "Fluncle Lens — the browser extension that finds fluncle:// coordinates on any page and links each to its /log/<coord> finding (with a hover card from the public API)"
2673
+ ],
2674
+ kind: "extension",
2675
+ name: "extension.lens",
2676
+ operatorNotes: "Fluncle Lens (apps/extension), MV3. PENDING Chrome Web Store review — DARK until approval, then flip `pending` + set the assigned listing URL. The placeholder `url` is the store search for the listing; probeConfig is the post-approval /status check.",
2677
+ pending: true,
2678
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2679
+ url: "https://chromewebstore.google.com/search/Fluncle%20Lens",
2680
+ weights: { web: "secondary" }
2681
+ },
2667
2682
  {
2668
2683
  command: "fluncle admin tracks enrich --queue",
2669
2684
  exposedContent: [
@@ -2820,7 +2835,7 @@ var init_status = __esm(() => {
2820
2835
  init_api();
2821
2836
  serviceLabels = (() => {
2822
2837
  const labels = new Map;
2823
- for (const surface of SURFACES) {
2838
+ for (const surface of liveSurfaces()) {
2824
2839
  const match = surface.operatorNotes?.match(/service `([a-z0-9-]+)`/);
2825
2840
  const id = match?.[1];
2826
2841
  const label = surface.exposedContent[0];
@@ -3044,7 +3059,7 @@ __export(exports_admin_tracks, {
3044
3059
  backfillDiscogsCommand: () => backfillDiscogsCommand
3045
3060
  });
3046
3061
  async function fetchAdminTracks(options) {
3047
- const { hasContext, hasNote, hasObservation, hasVideo, max, order, status } = options;
3062
+ const { hasContext, hasNote, hasObservation, hasVideo, max, order, retryEmptyContext, status } = options;
3048
3063
  const results = [];
3049
3064
  let cursor;
3050
3065
  do {
@@ -3055,6 +3070,9 @@ async function fetchAdminTracks(options) {
3055
3070
  if (hasContext !== undefined) {
3056
3071
  params.set("hasContext", String(hasContext));
3057
3072
  }
3073
+ if (retryEmptyContext) {
3074
+ params.set("retryEmptyContext", "true");
3075
+ }
3058
3076
  if (hasNote !== undefined) {
3059
3077
  params.set("hasNote", String(hasNote));
3060
3078
  }
@@ -3094,8 +3112,8 @@ async function queueCommand(limit, filters = {}) {
3094
3112
  async function enrichQueueCommand(limit) {
3095
3113
  return fetchAdminTracks({ max: limit, order: "asc", status: "queue" });
3096
3114
  }
3097
- async function contextQueueCommand(limit) {
3098
- return fetchAdminTracks({ hasContext: false, max: limit, order: "asc" });
3115
+ async function contextQueueCommand(limit, retryEmptyContext = false) {
3116
+ return fetchAdminTracks({ hasContext: false, max: limit, order: "asc", retryEmptyContext });
3099
3117
  }
3100
3118
  async function observeQueueCommand(limit) {
3101
3119
  return fetchAdminTracks({
@@ -3144,6 +3162,73 @@ var init_admin_tracks = __esm(() => {
3144
3162
  init_recent2();
3145
3163
  });
3146
3164
 
3165
+ // src/version-match.ts
3166
+ function isRemix(title) {
3167
+ return REMIX_MARKER.test(title);
3168
+ }
3169
+ function stripVersionSuffix(title) {
3170
+ const parts = title.split(/\s+-\s+/);
3171
+ if (parts.length > 1 && VERSION_MARKER.test(parts[parts.length - 1] ?? "")) {
3172
+ return parts.slice(0, -1).join(" - ").trim();
3173
+ }
3174
+ return title.trim();
3175
+ }
3176
+ function tokenize(value) {
3177
+ return value.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, " ").trim().split(" ").filter(Boolean);
3178
+ }
3179
+ function versionTokens(title) {
3180
+ const parts = title.split(/\s+-\s+/);
3181
+ const tail = parts.length > 1 ? parts[parts.length - 1] ?? "" : "";
3182
+ if (parts.length > 1 && VERSION_MARKER.test(tail)) {
3183
+ return new Set(tokenize(tail));
3184
+ }
3185
+ const bracketed = /[([]([^)\]]*?)[)\]]/.exec(title);
3186
+ if (bracketed?.[1] && VERSION_MARKER.test(bracketed[1])) {
3187
+ return new Set(tokenize(bracketed[1]));
3188
+ }
3189
+ return new Set;
3190
+ }
3191
+ function versionMatches(findingTitle, candidateTitle) {
3192
+ const findingIsRemix = isRemix(findingTitle);
3193
+ const candidateIsRemix = isRemix(candidateTitle);
3194
+ if (findingIsRemix) {
3195
+ if (!candidateIsRemix) {
3196
+ return false;
3197
+ }
3198
+ const want = [...versionTokens(findingTitle)].filter((t) => !VERSION_STOPWORDS.has(t));
3199
+ if (want.length === 0) {
3200
+ return true;
3201
+ }
3202
+ const have = versionTokens(candidateTitle);
3203
+ for (const token of want) {
3204
+ if (!have.has(token)) {
3205
+ return false;
3206
+ }
3207
+ }
3208
+ return true;
3209
+ }
3210
+ return !candidateIsRemix;
3211
+ }
3212
+ function baseTitleMatches(findingTitle, candidateTitle) {
3213
+ const want = new Set(tokenize(stripVersionSuffix(findingTitle)));
3214
+ const have = new Set(tokenize(stripVersionSuffix(candidateTitle)));
3215
+ if (want.size === 0) {
3216
+ return false;
3217
+ }
3218
+ for (const token of want) {
3219
+ if (!have.has(token)) {
3220
+ return false;
3221
+ }
3222
+ }
3223
+ return true;
3224
+ }
3225
+ var VERSION_MARKER, REMIX_MARKER, VERSION_STOPWORDS;
3226
+ var init_version_match = __esm(() => {
3227
+ VERSION_MARKER = /\b(mix|edit|version|remix|dub|vip|bootleg|rework|re-?edit|flip|refix|remaster(?:ed)?|instrumental)\b/i;
3228
+ REMIX_MARKER = /\b(remix|bootleg|vip|rework|re-?edit|flip|refix)\b/i;
3229
+ VERSION_STOPWORDS = new Set(["mix", "the", "and", "feat", "ft", "edit", "version", "remix"]);
3230
+ });
3231
+
3147
3232
  // src/commands/preview-archive.ts
3148
3233
  var exports_preview_archive = {};
3149
3234
  __export(exports_preview_archive, {
@@ -3247,16 +3332,22 @@ async function resolveDeezerByIsrc(isrc) {
3247
3332
  return;
3248
3333
  }
3249
3334
  }
3335
+ function durationAgrees(findingMs, candidateSeconds) {
3336
+ if (!findingMs || !candidateSeconds) {
3337
+ return true;
3338
+ }
3339
+ return Math.abs(candidateSeconds - findingMs / 1000) <= DURATION_TOLERANCE_S;
3340
+ }
3250
3341
  async function resolveDeezerSearch(track) {
3251
3342
  const artist = track.artists[0]?.trim();
3252
3343
  if (!artist || !track.title.trim()) {
3253
3344
  return;
3254
3345
  }
3255
3346
  try {
3256
- const query = `artist:"${artist}" track:"${track.title.trim()}"`;
3347
+ const query = `artist:"${artist}" track:"${stripVersionSuffix(track.title.trim())}"`;
3257
3348
  const response = await fetch(`https://api.deezer.com/search?q=${encodeURIComponent(query)}`);
3258
3349
  const body = await response.json();
3259
- const hit = (body.data ?? []).find((item) => item.preview);
3350
+ const hit = (body.data ?? []).find((item) => item.preview && versionMatches(track.title, item.title ?? "") && baseTitleMatches(track.title, item.title ?? "") && durationAgrees(track.durationMs, item.duration));
3260
3351
  return downloadPreview(hit?.preview, "deezer:search");
3261
3352
  } catch {
3262
3353
  return;
@@ -3270,7 +3361,7 @@ async function resolveItunes(track) {
3270
3361
  try {
3271
3362
  const response = await fetch(`https://itunes.apple.com/search?term=${encodeURIComponent(`${artist} ${track.title}`)}&media=music&limit=10`);
3272
3363
  const body = await response.json();
3273
- const hit = (body.results ?? []).find((item) => item.previewUrl && normalize(item.artistName ?? "").includes(normalize(artist)));
3364
+ const hit = (body.results ?? []).find((item) => item.previewUrl && normalize(item.artistName ?? "").includes(normalize(artist)) && versionMatches(track.title, item.trackName ?? "") && baseTitleMatches(track.title, item.trackName ?? "") && durationAgrees(track.durationMs, item.trackTimeMillis ? item.trackTimeMillis / 1000 : undefined));
3274
3365
  return downloadPreview(hit?.previewUrl, "itunes");
3275
3366
  } catch {
3276
3367
  return;
@@ -3332,8 +3423,10 @@ function extensionForMime(mime) {
3332
3423
  function normalize(value) {
3333
3424
  return value.toLowerCase().replace(/[^a-z0-9]/g, "");
3334
3425
  }
3426
+ var DURATION_TOLERANCE_S = 5;
3335
3427
  var init_preview_archive = __esm(() => {
3336
3428
  init_api();
3429
+ init_version_match();
3337
3430
  });
3338
3431
 
3339
3432
  // src/commands/mixtape-youtube.ts
@@ -6471,7 +6564,7 @@ function addAdminCommands(program2) {
6471
6564
  const { trackObserveCommand: trackObserveCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
6472
6565
  await runTrackObserve(idOrLogId, options, trackObserveCommand2);
6473
6566
  });
6474
- adminTrack.command("context").description("Gather the field notes for a finding (facts only; observe speaks from them)").argument("[idOrLogId]").option("--queue", "Show the context worklist (findings missing field notes), oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--query <text>", "Override the fact-search query (else the Worker builds one)").option("--refresh", "Re-run the fetch even if a note exists (backfill/sharpen)", false).option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
6567
+ adminTrack.command("context").description("Gather the field notes for a finding (facts only; observe speaks from them)").argument("[idOrLogId]").option("--queue", "Show the context worklist (findings missing field notes), oldest first", false).option("--retry-empty", "With --queue: also re-pick finds confirmed empty last pass (widen the net)", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--query <text>", "Override the fact-search query (else the Worker builds one)").option("--refresh", "Re-run the fetch even if a note exists (backfill/sharpen)", false).option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
6475
6568
  if (options.queue) {
6476
6569
  const { contextQueueCommand: contextQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
6477
6570
  await runAdminContextQueue(options, contextQueueCommand2);
@@ -7281,7 +7374,8 @@ async function runAdminQueue(options, queueCommand2) {
7281
7374
  }
7282
7375
  async function runAdminContextQueue(options, contextQueueCommand2) {
7283
7376
  const limit = parseListLimit(options.limit);
7284
- const tracks = await contextQueueCommand2(limit);
7377
+ const retryEmpty = options.retryEmpty === true;
7378
+ const tracks = await contextQueueCommand2(limit, retryEmpty);
7285
7379
  if (options.json) {
7286
7380
  printJson({
7287
7381
  ok: true,
@@ -7295,7 +7389,8 @@ async function runAdminContextQueue(options, contextQueueCommand2) {
7295
7389
  }
7296
7390
  const { trackRows: trackRows2 } = await Promise.resolve().then(() => (init_format2(), exports_format));
7297
7391
  const noun = tracks.length === 1 ? "finding" : "findings";
7298
- console.log(`${tracks.length} ${noun} missing field notes, oldest first:`);
7392
+ const scope = retryEmpty ? " (incl. empty retries)" : "";
7393
+ console.log(`${tracks.length} ${noun} missing field notes${scope}, oldest first:`);
7299
7394
  console.log(trackRows2(tracks).join(`
7300
7395
  `));
7301
7396
  }
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.66.0"
34
+ "version": "0.68.0"
35
35
  }