From efdf497c59952d14861b10f9af9e50c7dc079253 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:36:56 -0400 Subject: [PATCH] fix(feed): filter cached audio-only notes at render + hydration, not just ingest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The convert-time guard (17b48cbf) only blocks FRESH events. Audio notes cached before it existed are rehydrated straight from IndexedDB/localStorage as already-converted video objects (`sig: "unavailable (cached)"`), bypassing convertEventToVideo — so they kept showing in the main feed and channel grids. Extract the audio check into a reusable object-level predicate `isAudioOnlyVideoObject(video)` (url + magnet + imeta tags) and apply it at every boundary a cached object can slip through: - convertEventToVideo now delegates to it (fresh ingest, unchanged behavior) - PersistenceManager.applyCachedPayload drops audio-only on hydration (purges the cache going forward) - NostrClient.getActiveVideos filters it (main feed, immediate effect) - nostrService.shouldIncludeVideo filters it above the viewer-is-author short-circuit (channel/author grids, incl. the creator's own channel) Audio-only = no magnet AND no imeta `m video/*` AND (imeta `m audio/*` OR an unambiguous audio url); `.ogg` alone stays video. Bumped client.js size baseline (+6 lines). Tests: video-schema-and-conversion (+3 predicate scenarios, 11/11); nostr-service + ingest-adapter suites green. Lint + build clean. TODO #60. Co-Authored-By: Claude Opus 4.8 --- js/nostr/client.js | 11 +++-- js/nostr/index.js | 1 + js/nostr/managers/PersistenceManager.js | 7 +++ js/nostr/nip71.js | 35 +++++++++++---- js/services/nostrService.js | 9 +++- scripts/check-file-size.mjs | 2 +- tests/video-schema-and-conversion.test.mjs | 52 +++++++++++++++++++++- 7 files changed, 103 insertions(+), 14 deletions(-) diff --git a/js/nostr/client.js b/js/nostr/client.js index e48f4b92..05572104 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -25,6 +25,7 @@ import { buildNip71MetadataTags, collectNip71PointerRequests, convertEventToVideo, + isAudioOnlyVideoObject, extractNip71MetadataFromTags, getDTagValueFromTags, mergeNip71MetadataIntoVideo as mergeNip71MetadataIntoVideoHelper, @@ -4469,9 +4470,13 @@ export class NostrClient { } getActiveVideos() { - return Array.from(this.activeMap.values()).sort( - (a, b) => b.created_at - a.created_at - ); + // Filter audio-only notes (podcasts/music) here too, not just at ingest: + // video objects rehydrated from the persisted cache bypass + // convertEventToVideo, so a note cached before the audio guard existed would + // otherwise still reach the feed. See TODO #60. + return Array.from(this.activeMap.values()) + .filter((video) => !isAudioOnlyVideoObject(video)) + .sort((a, b) => b.created_at - a.created_at); } // Optimistically add a just-published video event to the local caches so it diff --git a/js/nostr/index.js b/js/nostr/index.js index a2c6714d..90f2da15 100644 --- a/js/nostr/index.js +++ b/js/nostr/index.js @@ -18,6 +18,7 @@ export { buildNip71VideoEvent, collectNip71PointerRequests, convertEventToVideo, + isAudioOnlyVideoObject, extractNip71MetadataFromTags, getDTagValueFromTags, mergeNip71MetadataIntoVideo, diff --git a/js/nostr/managers/PersistenceManager.js b/js/nostr/managers/PersistenceManager.js index 9f96c759..16b0a9fa 100644 --- a/js/nostr/managers/PersistenceManager.js +++ b/js/nostr/managers/PersistenceManager.js @@ -1,4 +1,5 @@ import { EventsCacheStore } from "./EventsCacheStore.js"; +import { isAudioOnlyVideoObject } from "../nip71.js"; import { devLogger, userLogger } from "../../utils/logger.js"; import { isDevMode } from "../../config.js"; import { NOTE_TYPES } from "../../nostrEventSchemas.js"; @@ -154,6 +155,12 @@ export class PersistenceManager { continue; } + // Drop audio-only notes (podcasts/music) cached before the audio guard + // existed so they don't get rehydrated into the feed. See TODO #60. + if (isAudioOnlyVideoObject(video)) { + continue; + } + this.client.applyRootCreatedAt(video); const activeKey = this.client.getActiveKey(video); diff --git a/js/nostr/nip71.js b/js/nostr/nip71.js index 4e4706f3..928010ae 100644 --- a/js/nostr/nip71.js +++ b/js/nostr/nip71.js @@ -1497,6 +1497,31 @@ function detectImetaMediaKinds(tags = []) { // treated as audio when an `imeta m audio/*` tag says so. const AUDIO_ONLY_URL_PATTERN = /\.(mp3|m4a|aac|wav|flac|opus|oga|weba)(?:[?#]|$)/i; +// Whether a (converted) video object is an audio-only note — a podcast/music +// note published as a native kind-30078. bitvid has no