diff options
author | metamuffin <metamuffin@disroot.org> | 2023-01-29 14:45:25 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2023-01-29 14:45:25 +0100 |
commit | de8d69d2886ae50e28da210fc690c99457a804bb (patch) | |
tree | b9d4fca9acd7d5fb844f4c76c8c338770d943df7 | |
parent | 0d9dc5672b0ba0c6c9988b0422837ceb00a5d7b8 (diff) | |
download | jellything-de8d69d2886ae50e28da210fc690c99457a804bb.tar jellything-de8d69d2886ae50e28da210fc690c99457a804bb.tar.bz2 jellything-de8d69d2886ae50e28da210fc690c99457a804bb.tar.zst |
more seeking code + expire cookies
-rw-r--r-- | Cargo.lock | 177 | ||||
-rw-r--r-- | matroska/src/write.rs | 8 | ||||
-rw-r--r-- | remuxer/src/import/mod.rs | 3 | ||||
-rw-r--r-- | remuxer/src/lib.rs | 26 | ||||
-rw-r--r-- | remuxer/src/segment_extractor.rs | 24 | ||||
-rw-r--r-- | remuxer/src/trim_writer.rs | 18 | ||||
-rw-r--r-- | server/Cargo.toml | 3 | ||||
-rw-r--r-- | server/src/config.rs | 4 | ||||
-rw-r--r-- | server/src/routes/mod.rs | 4 | ||||
-rw-r--r-- | server/src/routes/ui/account/mod.rs | 11 | ||||
-rw-r--r-- | server/src/routes/ui/account/session.rs | 32 | ||||
-rw-r--r-- | server/src/routes/ui/error.rs | 5 | ||||
-rw-r--r-- | server/src/routes/ui/home.rs | 2 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 6 |
14 files changed, 269 insertions, 54 deletions
@@ -47,6 +47,15 @@ dependencies = [ ] [[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "anyhow" version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -354,6 +363,22 @@ dependencies = [ ] [[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] name = "cipher" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -401,6 +426,16 @@ dependencies = [ ] [[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] name = "concurrent-queue" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -423,11 +458,17 @@ dependencies = [ "rand 0.8.5", "sha2", "subtle", - "time", + "time 0.3.17", "version_check", ] [[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] name = "cpufeatures" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -498,6 +539,50 @@ dependencies = [ ] [[package]] +name = "cxx" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322296e2f2e5af4270b54df9e85a02ff037e271af20ba3e7fe1575515dc840b8" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "017a1385b05d631e7875b1f151c9f012d37b53491e2a87f65bff5c262b2111d8" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26bbb078acf09bc1ecda02d4223f03bdd28bd4874edcb0379138efc499ce971" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357f40d1f06a24b60ae1fe122542c1fb05d28d32acb2aed064e84bc2ad1e252e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "devise" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -769,7 +854,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -932,6 +1017,30 @@ dependencies = [ ] [[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] name = "indexmap" version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1033,6 +1142,7 @@ dependencies = [ "argon2", "async-std", "chashmap", + "chrono", "env_logger", "jellycommon", "jellyremuxer", @@ -1094,6 +1204,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] name = "linux-raw-sys" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1199,7 +1318,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -1234,6 +1353,25 @@ dependencies = [ ] [[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] name = "num_cpus" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1671,7 +1809,7 @@ dependencies = [ "serde", "state", "tempfile", - "time", + "time 0.3.17", "tokio", "tokio-stream", "tokio-util", @@ -1718,7 +1856,7 @@ dependencies = [ "smallvec 1.10.0", "stable-pattern", "state", - "time", + "time 0.3.17", "tokio", "uncased", ] @@ -1762,6 +1900,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] name = "serde" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1978,6 +2122,17 @@ dependencies = [ [[package]] name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" @@ -2186,6 +2341,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] name = "unicode-xid" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2247,6 +2408,12 @@ dependencies = [ [[package]] name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/matroska/src/write.rs b/matroska/src/write.rs index 2555380..8fc17c3 100644 --- a/matroska/src/write.rs +++ b/matroska/src/write.rs @@ -76,7 +76,13 @@ impl<W: Write> EbmlWriter<W> { impl<W: Seek> Seek for EbmlWriter<W> { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { - self.inner.seek(pos) + self.inner.seek(pos)?; + match pos { + std::io::SeekFrom::Start(s) => self.position = s as usize, + std::io::SeekFrom::End(_) => unimplemented!(), + std::io::SeekFrom::Current(s) => self.position += s as usize, + } + Ok(self.position as u64) } } diff --git a/remuxer/src/import/mod.rs b/remuxer/src/import/mod.rs index 01b211b..49a9aa5 100644 --- a/remuxer/src/import/mod.rs +++ b/remuxer/src/import/mod.rs @@ -210,12 +210,13 @@ fn import_read_segment( "simple block: track={} tso={}", block.track, block.timestamp_off ); + debug!("{pts} {}", block.timestamp_off); seek_index .entry(block.track) .or_insert(SeekIndex { blocks: vec![] }) .blocks .push(BlockIndex { - pts: pts + block.timestamp_off as u64, + pts: (pts as i64 + block.timestamp_off as i64) as u64, source_off: position, size: block.data.len(), }); diff --git a/remuxer/src/lib.rs b/remuxer/src/lib.rs index 03d469e..0034cf8 100644 --- a/remuxer/src/lib.rs +++ b/remuxer/src/lib.rs @@ -7,16 +7,12 @@ pub mod import; pub mod segment_extractor; pub mod trim_writer; -use crate::{ - segment_extractor::{AbsoluteBlock, SegmentExtractIter}, - trim_writer::TrimWriter, -}; +use crate::{segment_extractor::SegmentExtractIter, trim_writer::TrimWriter}; use anyhow::{anyhow, Context}; use jellycommon::{BlockIndex, ItemInfo, SeekIndex, SourceTrack, SourceTrackKind}; use jellymatroska::{ block::Block, read::EbmlReader, - unflatten::Unflatten, write::{vint_length, EbmlWriter}, Master, MatroskaTag, }; @@ -188,10 +184,11 @@ impl RemuxerContext { break; } p += 1; // simpleblock tag - p += vint_length(1 + 2 + 1 + best_block.size as u64); // simpleblock size vint - p += 1 + 2 + 1; // block {tracknum, pts_off, flags} - // TODO does not work, if more than 127 tracks are present - p += best_block.size; // block payload + let simpleblock_size = 1 + 2 + 1 // block {tracknum, pts_off, flags} + // TODO does not work, if more than 127 tracks are present + + best_block.size; // block payload + p += vint_length(simpleblock_size as u64); // simpleblock size vint + p += simpleblock_size; cluster.push((best_index, best_block)) } info!("segment layout computed ({} clusters)", clusters.len()); @@ -225,11 +222,12 @@ impl RemuxerContext { let segment_start_position = output.position(); let mut skip = 0; - for cluster in &segment_layout { - if (cluster.position + segment_start_position) > range.start { + for (i, cluster) in segment_layout.iter().enumerate() { + if (cluster.position + segment_start_position) < range.start { + skip += i; + } else { break; } - skip += 1; } if skip != 0 { info!("skipping {skip} clusters"); @@ -237,7 +235,6 @@ impl RemuxerContext { } struct ReaderD<'a> { - _info: SourceTrack, peek: Option<Block>, stream: SegmentExtractIter<'a>, mapped: u64, @@ -258,7 +255,6 @@ impl RemuxerContext { mapped: inp.mapped, peek: Some(stream.next()?), stream, - _info: inp.info.clone(), }); } info!( @@ -266,7 +262,7 @@ impl RemuxerContext { (Instant::now() - timing_cp).as_millis() ); - for (cluster_index, cluster) in segment_layout.into_iter().skip(skip).enumerate() { + for (cluster_index, cluster) in segment_layout.into_iter().enumerate().skip(skip) { info!( "writing cluster {cluster_index} (pts_base={}) with {} blocks", cluster.timestamp, diff --git a/remuxer/src/segment_extractor.rs b/remuxer/src/segment_extractor.rs index 095bdfe..c44a511 100644 --- a/remuxer/src/segment_extractor.rs +++ b/remuxer/src/segment_extractor.rs @@ -1,24 +1,22 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use jellymatroska::{block::Block, read::EbmlReader, unflatten::IterWithPos, MatroskaTag}; use log::{debug, trace}; -use std::collections::VecDeque; -pub struct AbsoluteBlock { - pub pts_base: u64, - pub inner: Block, -} +// pub struct AbsoluteBlock { +// pub pts_base: u64, +// pub inner: Block, +// } +// impl AbsoluteBlock { +// pub fn pts(&self) -> u64 { +// self.inner.timestamp_off as u64 + self.pts_base +// } +// } pub struct SegmentExtractIter<'a> { segment: &'a mut EbmlReader, extract: u64, } -impl AbsoluteBlock { - pub fn pts(&self) -> u64 { - self.inner.timestamp_off as u64 + self.pts_base - } -} - impl<'a> SegmentExtractIter<'a> { pub fn new(segment: &'a mut EbmlReader, extract: u64) -> Self { Self { segment, extract } @@ -31,6 +29,7 @@ impl<'a> SegmentExtractIter<'a> { MatroskaTag::Void(_) => (), MatroskaTag::Crc32(_) => (), MatroskaTag::Cluster(_) => (), + MatroskaTag::Timestamp(_) => (), MatroskaTag::SimpleBlock(buf) | MatroskaTag::Block(buf) => { let block = Block::parse(&buf)?; if block.track == self.extract { @@ -38,6 +37,7 @@ impl<'a> SegmentExtractIter<'a> { return Ok(block); } } + MatroskaTag::Cues(_) => bail!("reached cues, this is the end"), _ => debug!("(rs) tag ignored: {item:?}"), } } diff --git a/remuxer/src/trim_writer.rs b/remuxer/src/trim_writer.rs index 65d3589..bed90e7 100644 --- a/remuxer/src/trim_writer.rs +++ b/remuxer/src/trim_writer.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::anyhow; -use log::warn; +use log::{trace, warn}; pub struct TrimWriter<W> { inner: W, @@ -36,12 +36,16 @@ impl<W: Write> Write for TrimWriter<W> { )); } - let buf = &buf[start..end]; - if !buf.is_empty() { - self.inner.write_all(buf)?; - self.position += buf.len() - } - Ok(buf.len()) + let tbuf = &buf[start..end]; + Ok(if !tbuf.is_empty() { + trace!("trim={start}..{end} avail={}", buf.len()); + let sz = self.inner.write(tbuf)?; + self.position += sz; + sz + } else { + trace!("skip={}", buf.len()); + buf.len() + }) } fn flush(&mut self) -> std::io::Result<()> { diff --git a/server/Cargo.toml b/server/Cargo.toml index a3214fa..954161f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -17,6 +17,7 @@ once_cell = "1.17.0" chashmap = "2.2.2" argon2 = "0.4.1" rand = "0.8.5" +chrono = { version = "0.4.23", features = ["serde"] } async-std = "1.12.0" rocket = { version = "0.5.0-rc.2", features = ["secrets"] } @@ -28,4 +29,4 @@ sled = "0.34.7" typed-sled = "0.2.3" [features] -bypass-auth = []
\ No newline at end of file +bypass-auth = [] diff --git a/server/src/config.rs b/server/src/config.rs index 4b61960..0de7e90 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -10,13 +10,15 @@ use std::{fs::File, path::PathBuf}; pub struct GlobalConfig { pub brand: String, pub slogan: String, - pub asset_dir: PathBuf, + pub asset_path: PathBuf, pub database_path: PathBuf, pub library_path: PathBuf, + pub admin_username: String, pub admin_password: String, pub cookie_key: String, + pub login_expire: i64, } pub fn load_global_config() -> GlobalConfig { diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index d1bf7fc..c567d94 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -48,7 +48,7 @@ pub fn build_rocket( Box::pin(async {}) })) .register("/", catchers![r_catch]) - .mount("/assets", FileServer::from(&CONF.asset_dir)) + .mount("/assets", FileServer::from(&CONF.asset_path)) .mount( "/", routes![ @@ -77,5 +77,5 @@ pub fn build_rocket( #[get("/favicon.ico")] fn r_favicon() -> MyResult<File> { - Ok(File::open(CONF.asset_dir.join("favicon.ico"))?) + Ok(File::open(CONF.asset_path.join("favicon.ico"))?) } diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs index e7031ff..63c01c5 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/routes/ui/account/mod.rs @@ -6,6 +6,8 @@ pub mod admin; pub mod session; +use self::session::SessionCookie; + use super::{error::MyError, layout::LayoutPage}; use crate::{ database::{Database, User}, @@ -157,7 +159,14 @@ pub fn r_account_login_post( Err(anyhow!("invalid password"))? } - jar.add_private(Cookie::build("user", user.name).permanent().finish()); + jar.add_private( + Cookie::build( + "user", + serde_json::to_string(&SessionCookie::new(user.name)).unwrap(), + ) + .permanent() + .finish(), + ); Ok(Redirect::found(uri!(r_home()))) } diff --git a/server/src/routes/ui/account/session.rs b/server/src/routes/ui/account/session.rs index 6059311..6795c06 100644 --- a/server/src/routes/ui/account/session.rs +++ b/server/src/routes/ui/account/session.rs @@ -5,19 +5,36 @@ */ use crate::{ database::{Database, User}, - routes::ui::error::MyError, + routes::ui::error::MyError, CONF, }; use anyhow::anyhow; +use chrono::{DateTime, Duration, Utc}; use rocket::{ outcome::Outcome, request::{self, FromRequest}, Request, State, }; +use serde::{Deserialize, Serialize}; pub struct Session { pub user: User, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionCookie { + name: String, + expire: DateTime<Utc>, +} + +impl SessionCookie { + pub fn new(name: String) -> Self { + Self { + name, + expire: Utc::now() + Duration::days(CONF.login_expire), + } + } +} + impl Session { pub async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> { #[cfg(not(feature = "bypass-auth"))] @@ -26,14 +43,21 @@ impl Session { .get_private("user") .ok_or(anyhow!("login required"))?; #[cfg(not(feature = "bypass-auth"))] - let username = cookie.value(); + let cookie = serde_json::from_str::<SessionCookie>(cookie.value())?; #[cfg(feature = "bypass-auth")] - let username = crate::CONF.admin_username.to_string(); + let cookie = SessionCookie { + name: crate::CONF.admin_username.to_string(), + expire: Utc::now() + Duration::days(CONF.login_expire), + }; + + if cookie.expire < Utc::now() { + Err(anyhow!("cookie expired"))?; + } let db = req.guard::<&State<Database>>().await.unwrap(); let user = db .users - .get(&username.to_string())? + .get(&cookie.name.to_string())? .ok_or(anyhow!("user not found"))?; Ok(Session { user }) diff --git a/server/src/routes/ui/error.rs b/server/src/routes/ui/error.rs index 59e322a..7913e1a 100644 --- a/server/src/routes/ui/error.rs +++ b/server/src/routes/ui/error.rs @@ -70,3 +70,8 @@ impl From<sled::Error> for MyError { MyError(anyhow::anyhow!("{err}")) } } +impl From<serde_json::Error> for MyError { + fn from(err: serde_json::Error) -> Self { + MyError(anyhow::anyhow!("{err}")) + } +} diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs index a8d9c65..f81e04f 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/routes/ui/home.rs @@ -25,7 +25,7 @@ pub fn r_home(_sess: Session, library: &State<Library>) -> DynLayoutPage { #[get("/", rank = 2)] pub async fn r_home_unpriv() -> MyResult<DynLayoutPage<'static>> { - let front = read_to_string(CONF.asset_dir.join("front.htm")).await?; + let front = read_to_string(CONF.asset_path.join("front.htm")).await?; Ok(LayoutPage { title: "Home".to_string(), content: markup::new! { diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index 866787a..20b451f 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -95,11 +95,11 @@ pub fn player_conf<'a>(item: Arc<Item>) -> MyResult<DynLayoutPage<'a>> { fieldset.subtitles { legend { "Subtitles" } - @for (i, (tid, track)) in sub_tracks.iter().enumerate() { - input[type="radio", id=tid, name="s", value=tid, checked=i==0]; + @for (_i, (tid, track)) in sub_tracks.iter().enumerate() { + input[type="radio", id=tid, name="s", value=tid]; label[for=tid] { @format!("{track}") } br; } - input[type="radio", id="s-none", name="s", value=""]; + input[type="radio", id="s-none", name="s", value="", checked=true]; label[for="s-none"] { "No subtitles" } } |