feat(config): wire FEATURE_AUDIO_INGEST flag + lock audio-plan decisions

Adds the Audio/Music/Podcast feature flag to the instance config so a deployer
enables/disables it in one place like the other flags. Follows the exact
FEATURE_NIP71_INGEST wiring: export in config/instance-config.js → re-export in
js/config.js → DEFAULT_FLAGS + runtimeFlags snapshot + `export let` in
js/constants.js. Defaults FALSE ("off = no trace") — only an explicit `true`
lights it up; with it off nothing audio renders and the shipped audio guard keeps
audio-only notes out of the video feeds. No consumers yet (build only).

Also locks docs/audio-integration-plan.md decisions D1–D10 to the agreed
recommendations (status → DECISIONS LOCKED).

Verified: constants + config re-export FEATURE_AUDIO_INGEST === false by default.
Lint + build clean. docs/audio-integration-plan.md, TODO #60.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-09 10:19:03 -04:00
1 parent 3d9f940a8f
commit cd556f9c1f
4 files changed
+37 -11

No files matched your search

+12
View File
@@ -252,6 +252,18 @@ export const ALLOW_NSFW_CONTENT = false;
*/
export const FEATURE_NIP71_INGEST = true;
/**
* Whether this deployment enables the Audio / Music / Podcast experience
* (dedicated Audio player, sidebar tab, and the channel / "My Content" Audio
* tabs — see docs/audio-integration-plan.md and TODO #60).
*
* Defaults to `false` — "off = no trace": with it off, nothing audio renders and
* audio-only notes stay filtered out of the video feeds (current behavior). Flip
* to `true` to light up the Audio surfaces. Watch/ingest-first; a separate
* publish flag will gate uploading audio later.
*/
export const FEATURE_AUDIO_INGEST = false;
/**
* Card liveness visibility policy — what a non-owner video card does while its
* CDN/WebTorrent liveness probes are still running:
+12 -11
View File
@@ -1,6 +1,7 @@
# Audio / Music / Podcasts — Dev Plan
Status: **DECISIONS NEEDED (D1–D10)** — no code yet. Sibling of the shorts
Status: **DECISIONS LOCKED (D1–D10, per recommendations, 2026-07-09)** — ready to
build; config flag `FEATURE_AUDIO_INGEST` wired (default off). Sibling of the shorts
(`docs/shorts-plan.md`) and live (`docs/live-ingest-plan.md` /
`docs/live-publish-plan.md`) plans; shares the media-category model with both.
Tracks TODO #60. The near-term "keep audio out of the video feed" guard is
@@ -31,7 +32,7 @@ flag-gated from day one (off = no trace), whitelist-scoped like shorts/live.
## Decisions needed
> **DECISION 1 — v1 source scope. 🔵 NEEDED.**
> **DECISION 1 — v1 source scope. ✅ LOCKED (= recommendation below).**
> Which sources count as "audio" for the first release?
> - **Option A — Native only:** kind 30078 with `imeta m audio/*` (+ a native
> `t:audio` marker for new bitvid publishing). Lowest effort — reuses the whole
@@ -42,12 +43,12 @@ flag-gated from day one (off = no trace), whitelist-scoped like shorts/live.
> PodcastIndex GUIDs). Full podcast client. Biggest — its own phase.
> _Recommendation: **A for v1**, B fast-follow, C as a later phase (D7)._
> **DECISION 2 — Discovery / whitelist scope. 🔵 NEEDED.**
> **DECISION 2 — Discovery / whitelist scope. ✅ LOCKED (= recommendation below).**
> Mirror shorts D1 / live D1: list audio only from **whitelisted/trusted authors**?
> _Recommendation: **Yes** — same scope as video/shorts/live, one consistent trust
> model. (No separate audio allowlist.)_
> **DECISION 3 — Does audio appear in the main feed, or Audio-tab only? 🔵 NEEDED.**
> **DECISION 3 — Does audio appear in the main feed, or Audio-tab only? ✅ LOCKED (= recommendation below).**
> - **Option A — Audio-tab only** (excluded from main/discovery video feeds).
> Consistent with the shipped guard that already excludes audio from video feeds,
> and with shorts D3 (Shorts-tab only).
@@ -55,7 +56,7 @@ flag-gated from day one (off = no trace), whitelist-scoped like shorts/live.
> _Recommendation: **Option A** — keeps the video feed clean; audio is a
> deliberate destination, not an interruption._
> **DECISION 4 — Player model. 🔵 NEEDED (biggest UX call).**
> **DECISION 4 — Player model. ✅ LOCKED: A for v1, architected toward B.**
> - **Option A — Reuse the player modal in "audio mode":** detect audio → swap the
> `<video>` for an `<audio>` element + big poster art, reuse the existing
> zaps/comments/moderation/share chrome. Fastest; one modal.
@@ -65,7 +66,7 @@ flag-gated from day one (off = no trace), whitelist-scoped like shorts/live.
> _Recommendation: **A for v1**, **B as the real target** — audio's whole value is
> "keep listening while you browse," so plan the architecture so A can grow into B._
> **DECISION 5 — Category signal / resolver. 🔵 NEEDED.**
> **DECISION 5 — Category signal / resolver. ✅ LOCKED (= recommendation below).**
> How is a note bucketed as audio (must be deterministic + shared)?
> Signals, in priority: (1) `imeta m audio/*` with no video variant, (2) kind
> `1222`/`1244` (if D1≥B), (3) `podcast:*` `i` tag (if D1=C), (4) native `t:audio`
@@ -75,7 +76,7 @@ flag-gated from day one (off = no trace), whitelist-scoped like shorts/live.
> own audio publishing so the Audio feed has a clean relay-side `#t` filter?
> Recommendation: **Yes.**_
> **DECISION 6 — Publish vs watch-only for v1. 🔵 NEEDED.**
> **DECISION 6 — Publish vs watch-only for v1. ✅ LOCKED (= recommendation below).**
> - **Option A — Watch/ingest-only** (mirrors live-ingest): bitvid surfaces others'
> audio, no upload UI yet.
> - **Option B — Publish too:** upload an audio file → native 30078 with
@@ -83,23 +84,23 @@ flag-gated from day one (off = no trace), whitelist-scoped like shorts/live.
> _Recommendation: **A for v1**; split publish into its own plan/phase like
> live-ingest vs live-publish._
> **DECISION 7 — Podcast-feed interop (NIP-73). 🔵 NEEDED (later phase).**
> **DECISION 7 — Podcast-feed interop (NIP-73). ✅ LOCKED: Yes, Phase 3.**
> Should bitvid reference real Podcasting 2.0 feeds/episodes (link an audio note to
> its `podcast:guid` / `podcast:item:guid`, dedupe re-uploads, interop with Fountain
> / PodcastIndex)? _Recommendation: **Yes, but Phase 3** — high interop value, but
> pulls in RSS/GUID resolution; not v1._
> **DECISION 8 — One "Audio" category, or split Music vs Podcasts? 🔵 NEEDED.**
> **DECISION 8 — One "Audio" category, or split Music vs Podcasts? ✅ LOCKED (= recommendation below).**
> _Recommendation: **one Audio category** for v1 (podcasts + music together); add a
> Music sub-filter later if Wavlake-style content appears. Splitting now needs a
> reliable music-vs-podcast signal we don't have._
> **DECISION 9 — Value-for-Value / zaps. 🔵 NEEDED.**
> **DECISION 9 — Value-for-Value / zaps. ✅ LOCKED (= recommendation below).**
> Podcasting 2.0 has streaming V4V (per-minute sats). bitvid has one-shot zaps.
> _Recommendation: **reuse the existing zap system** for v1; per-minute streaming
> V4V is a future enhancement, not v1._
> **DECISION 10 — Config flag. 🔵 NEEDED.**
> **DECISION 10 — Config flag. ✅ LOCKED (= recommendation below).**
> Name + granularity. _Recommendation: `FEATURE_AUDIO_INGEST` (off = no trace),
> later `FEATURE_AUDIO_PUBLISH`; matches `FEATURE_LIVE_INGEST` / shorts pattern._
+2
View File
@@ -25,6 +25,7 @@ import {
DEFAULT_WHITELIST_MODE_ENABLED,
ALLOW_NSFW_CONTENT,
FEATURE_NIP71_INGEST,
FEATURE_AUDIO_INGEST,
CARD_LIVENESS_POLICY,
LIVENESS_PROBE_PREFETCH_MARGIN,
DEFAULT_TRUST_SEED_NPUBS,
@@ -121,6 +122,7 @@ export const ADMIN_COMMUNITY_BLACKLIST_SOURCES = "community:blacklist:sources";
export const ADMIN_COMMUNITY_BLACKLIST_PREFIX = "community:blacklist";
export { ALLOW_NSFW_CONTENT };
export { FEATURE_NIP71_INGEST };
export { FEATURE_AUDIO_INGEST };
export { CARD_LIVENESS_POLICY };
export { LIVENESS_PROBE_PREFETCH_MARGIN };
export { DEFAULT_TRUST_SEED_NPUBS };
+11
View File
@@ -7,6 +7,7 @@ import {
DEFAULT_PLAYBACK_SOURCE,
DEFAULT_PLAYBACK_START_TIMEOUT,
FEATURE_NIP71_INGEST as CONFIG_FEATURE_NIP71_INGEST,
FEATURE_AUDIO_INGEST as CONFIG_FEATURE_AUDIO_INGEST,
CARD_LIVENESS_POLICY as CONFIG_CARD_LIVENESS_POLICY,
LIVENESS_PROBE_PREFETCH_MARGIN as CONFIG_LIVENESS_PROBE_PREFETCH_MARGIN,
IS_DEV_MODE,
@@ -194,6 +195,10 @@ const DEFAULT_FLAGS = Object.freeze({
// Inbound ingest of NIP-71 videos published by other Nostr apps. Sourced from
// the admin instance config so a deployer flips it in one place.
FEATURE_NIP71_INGEST: CONFIG_FEATURE_NIP71_INGEST !== false,
// Audio / Music / Podcast surfaces (docs/audio-integration-plan.md, TODO #60).
// Default OFF ("off = no trace") — only an explicit `true` in the instance
// config lights it up.
FEATURE_AUDIO_INGEST: CONFIG_FEATURE_AUDIO_INGEST === true,
FEATURE_HASHTAG_PREFERENCES: false,
FEATURE_TRUST_SEEDS: true, // Rollback: disable to drop baseline trust seeds without code changes.
FEATURE_TRUSTED_HIDE_CONTROLS: true,
@@ -220,6 +225,7 @@ const runtimeFlags = (() => {
FEATURE_PUBLISH_NIP71: DEFAULT_FLAGS.FEATURE_PUBLISH_NIP71,
FEATURE_NIP71_MIRROR: DEFAULT_FLAGS.FEATURE_NIP71_MIRROR,
FEATURE_NIP71_INGEST: DEFAULT_FLAGS.FEATURE_NIP71_INGEST,
FEATURE_AUDIO_INGEST: DEFAULT_FLAGS.FEATURE_AUDIO_INGEST,
FEATURE_HASHTAG_PREFERENCES: DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES,
FEATURE_TRUST_SEEDS: DEFAULT_FLAGS.FEATURE_TRUST_SEEDS,
FEATURE_TRUSTED_HIDE_CONTROLS: DEFAULT_FLAGS.FEATURE_TRUSTED_HIDE_CONTROLS,
@@ -265,6 +271,11 @@ export let FEATURE_NIP71_INGEST = coerceBoolean(
DEFAULT_FLAGS.FEATURE_NIP71_INGEST
);
export let FEATURE_AUDIO_INGEST = coerceBoolean(
runtimeFlags.FEATURE_AUDIO_INGEST,
DEFAULT_FLAGS.FEATURE_AUDIO_INGEST
);
export let FEATURE_HASHTAG_PREFERENCES = coerceBoolean(
runtimeFlags.FEATURE_HASHTAG_PREFERENCES,
DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES