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

@ -11,10 +11,12 @@ blake3.workspace = true
ec-crypto = { path = "../../crates/ec-crypto" }
ec-core = { path = "../../crates/ec-core" }
ec-chopper = { path = "../../crates/ec-chopper" }
ec-eth = { path = "../../crates/ec-eth" }
ec-hdhomerun = { path = "../../crates/ec-hdhomerun" }
ec-linux-iptv = { path = "../../crates/ec-linux-iptv" }
ec-iroh = { path = "../../crates/ec-iroh" }
ec-moq = { path = "../../crates/ec-moq" }
headless_chrome = "1"
hex = "0.4"
iroh = "0.96"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
@ -25,5 +27,16 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tower-http = { version = "0.5", features = ["fs"] }
tracing.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
block2 = "0.6"
objc2 = "0.6"
objc2-app-kit = { version = "0.3", features = ["NSBitmapImageRep", "NSImage"] }
objc2-core-foundation = "0.3"
objc2-foundation = { version = "0.3", features = ["NSData", "NSDictionary", "NSError", "NSString"] }
objc2-web-kit = { version = "0.3", features = ["WKSnapshotConfiguration", "WKWebView", "block2", "objc2-app-kit"] }
[build-dependencies]
tauri-build = { version = "2", features = [] }
[features]
custom-protocol = ["tauri/custom-protocol"]

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
[build]
dist = "../dist"
public_url = "/"
public_url = "./"

View file

@ -20,8 +20,80 @@
<link data-trunk rel="copy-dir" href="icons" />
</head>
<body>
<div
id="boot-status"
style="
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: #f7f4ef;
color: #2b241d;
font: 15px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
white-space: pre-wrap;
z-index: 9999;
"
>
Loading every.channel…
</div>
<div id="main"></div>
<script>
(() => {
const boot = document.getElementById("boot-status");
const main = document.getElementById("main");
if (!boot || !main) return;
const show = (message) => {
boot.textContent = message;
boot.style.display = "flex";
};
const hide = () => {
boot.remove();
};
const mounted = () => {
if (main.childElementCount > 0) {
hide();
return true;
}
return false;
};
new MutationObserver(() => {
mounted();
}).observe(main, { childList: true, subtree: true });
window.addEventListener("error", (event) => {
const message =
event?.error?.stack ||
event?.error?.message ||
event?.message ||
"Unknown frontend boot error";
show(`Frontend boot error\n\n${message}`);
});
window.addEventListener("unhandledrejection", (event) => {
const reason = event?.reason;
const message =
reason?.stack ||
reason?.message ||
(typeof reason === "string" ? reason : JSON.stringify(reason, null, 2)) ||
"Unknown unhandled rejection";
show(`Frontend boot rejection\n\n${message}`);
});
setTimeout(() => {
if (!mounted()) {
show("Loading every.channel is taking longer than expected…");
}
}, 4000);
})();
</script>
<script>
// Installable app shell (PWA). Keep this tiny and resilient.
if ("serviceWorker" in navigator) {

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() {

View file

@ -392,6 +392,13 @@ body::before {
color: var(--ink-muted);
}
.channel-detail {
font-size: 11px;
color: var(--ink-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.source-status {
font-size: 13px;
color: var(--ink-muted);