Advance forge rollout, Ethereum rails, and NBC sources

This commit is contained in:
every.channel 2026-04-01 15:58:49 -07:00
parent be26313225
commit 7d84510eac
No known key found for this signature in database
88 changed files with 11230 additions and 302 deletions

View file

@ -130,6 +130,24 @@ struct ProbeStreamArgs {
input: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BootstrapNbcAuthArgs {
input: Option<String>,
stream_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct BootstrapNbcAuthResult {
input_url: String,
stream_id: Option<String>,
hidden_mode: bool,
surfaced_auth: bool,
data_dir: Option<String>,
status: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct ManualSourceOptions {
@ -777,6 +795,13 @@ fn App() -> Element {
let active_id = selected.read().as_ref().map(|s| s.id.clone());
let playback_url = playback.read().as_ref().map(|p| p.url.clone());
let now_playing = selected.read().clone();
let bootstrap_stream = selected
.read()
.as_ref()
.filter(|stream| stream_source_kind(stream) == "nbc")
.cloned();
let bootstrap_input_value = add_input.read().clone();
let bootstrap_input_is_nbc = looks_like_nbc_input(&bootstrap_input_value);
let current_share = share_info.read().clone();
let source_list = sources.read().clone();
@ -889,11 +914,87 @@ fn App() -> Element {
span { "{source.ip.clone().unwrap_or_default()}" }
}
}
button {
class: "source-menu-action",
onclick: move |_| refresh_sources(),
"Refresh"
button {
class: "source-menu-action",
onclick: move |_| refresh_sources(),
"Refresh"
}
if bootstrap_stream.is_some() || bootstrap_input_is_nbc {
div { class: "source-menu-divider" }
div { class: "source-menu-section",
div { class: "source-menu-title", "NBC auth" }
div { class: "source-menu-status",
"Warm a hidden NBC session. The window only appears if MVPD auth is needed."
}
if let Some(stream) = bootstrap_stream.clone() {
button {
class: "source-menu-action",
onclick: move |_| {
if !tauri_available() {
status.set("Tauri backend not available (open the Tauri app)".to_string());
return;
}
let stream_id = stream.id.clone();
let stream_title = stream.title.clone();
let args = BootstrapNbcAuthArgs {
input: None,
stream_id: Some(stream_id),
};
let mut status = status.clone();
spawn(async move {
status.set(format!("Bootstrapping NBC auth for {}", stream_title));
match tauri_invoke::<BootstrapNbcAuthResult, _>("bootstrap_nbc_auth", &args).await {
Ok(result) => {
if result.surfaced_auth {
status.set("NBC auth ready after interactive sign-in".to_string());
} else {
status.set("NBC auth ready in hidden mode".to_string());
}
}
Err(err) => {
status.set(format!("NBC bootstrap error: {err}"));
}
}
});
},
"Bootstrap selected NBC"
}
}
if bootstrap_input_is_nbc {
button {
class: "source-menu-action",
onclick: move |_| {
if !tauri_available() {
status.set("Tauri backend not available (open the Tauri app)".to_string());
return;
}
let input = bootstrap_input_value.clone();
let args = BootstrapNbcAuthArgs {
input: Some(input.clone()),
stream_id: None,
};
let mut status = status.clone();
spawn(async move {
status.set(format!("Bootstrapping NBC auth for {}", input));
match tauri_invoke::<BootstrapNbcAuthResult, _>("bootstrap_nbc_auth", &args).await {
Ok(result) => {
if result.surfaced_auth {
status.set("NBC auth ready after interactive sign-in".to_string());
} else {
status.set("NBC auth ready in hidden mode".to_string());
}
}
Err(err) => {
status.set(format!("NBC bootstrap error: {err}"));
}
}
});
},
"Bootstrap pasted NBC URL"
}
}
}
}
div { class: "source-menu-divider" }
div { class: "source-menu-section",
div { class: "source-menu-title", "Add stream" }
@ -1707,6 +1808,7 @@ fn App() -> Element {
option { value: "linux-dvb", "Linux DVB" }
option { value: "hls", "HLS" }
option { value: "ytdlp", "yt-dlp" }
option { value: "nbc", "NBC" }
option { value: "moq", "Link" }
}
button {
@ -1753,6 +1855,8 @@ fn App() -> Element {
.map(|id| id == &stream.id)
.unwrap_or(false);
let card_class = if is_active { "channel-card active" } else { "channel-card" };
let channel_subtitle = stream_display_subtitle(stream);
let channel_detail = stream_display_detail(stream);
let moq_endpoint = stream
.metadata
.iter()
@ -1835,8 +1939,11 @@ fn App() -> Element {
});
},
div { class: "channel-title", "{stream.title}" }
div { class: "channel-meta",
{stream.number.clone().unwrap_or_default()}
if let Some(channel_subtitle) = channel_subtitle.clone() {
div { class: "channel-meta", "{channel_subtitle}" }
}
if let Some(channel_detail) = channel_detail.clone() {
div { class: "channel-detail", "{channel_detail}" }
}
if !stream.source.is_empty() {
div { class: "channel-badge source", "{stream.source}" }
@ -2293,6 +2400,65 @@ fn stream_has_drm(metadata: &[StreamMetadata]) -> bool {
})
}
fn stream_metadata_value<'a>(stream: &'a StreamDescriptor, key: &str) -> Option<&'a str> {
stream
.metadata
.iter()
.find(|entry| entry.key == key)
.map(|entry| entry.value.trim())
.filter(|value| !value.is_empty())
}
fn stream_display_subtitle(stream: &StreamDescriptor) -> Option<String> {
if stream_source_kind(stream) == "nbc" {
if let Some(program) =
stream_metadata_value(stream, "current_program").or_else(|| stream.number.as_deref())
{
return Some(format!("Now: {}", program.trim()));
}
if let Some(brand) = stream_metadata_value(stream, "nbc_brand") {
return Some(brand.to_string());
}
return None;
}
stream
.number
.as_ref()
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.map(|value| value.to_string())
}
fn stream_display_detail(stream: &StreamDescriptor) -> Option<String> {
if stream_source_kind(stream) != "nbc" {
return None;
}
let mut parts = Vec::new();
if let Some(badge) = stream_metadata_value(stream, "badge") {
parts.push(badge.to_string());
}
if let Some(entitlement) = stream_metadata_value(stream, "entitlement") {
if !entitlement.eq_ignore_ascii_case("free") {
parts.push("Adobe auth".to_string());
}
} else if stream_has_drm(&stream.metadata) {
parts.push("Adobe auth".to_string());
}
if parts.is_empty() {
None
} else {
Some(parts.join(""))
}
}
fn looks_like_nbc_input(value: &str) -> bool {
let value = value.trim().to_ascii_lowercase();
value.starts_with("https://www.nbc.com/") || value.starts_with("https://nbc.com/")
}
fn stream_source_kind(stream: &StreamDescriptor) -> String {
let source = stream.source.trim();
if !source.is_empty() {