use super::disc::{DiscOptions, DiscStream};
use super::iso::IsoStream;
use super::network::NetworkStream;
use super::null::NullStream;
use super::stdio::StdioStream;
use super::{IOStream, M2tsStream, MkvStream};
use crate::disc::DiscTitle;
use std::io::{self, BufReader, BufWriter};
use std::path::{Path, PathBuf};
const IO_BUF_SIZE: usize = 4 * 1024 * 1024;
const MKV_LOOKAHEAD_DEFAULT: usize = 10 * 1024 * 1024;
const MKV_LOOKAHEAD_UHD: usize = 100 * 1024 * 1024;
pub enum StreamUrl {
Disc { device: Option<PathBuf> },
M2ts { path: PathBuf },
Mkv { path: PathBuf },
Network { addr: String },
Stdio,
Iso { path: PathBuf },
Null,
Unknown { raw: String },
}
impl StreamUrl {
pub fn scheme(&self) -> &str {
match self {
StreamUrl::Disc { .. } => "disc",
StreamUrl::M2ts { .. } => "m2ts",
StreamUrl::Mkv { .. } => "mkv",
StreamUrl::Network { .. } => "network",
StreamUrl::Stdio => "stdio",
StreamUrl::Iso { .. } => "iso",
StreamUrl::Null => "null",
StreamUrl::Unknown { .. } => "unknown",
}
}
pub fn path_str(&self) -> &str {
match self {
StreamUrl::Disc { device: Some(p) } => p.to_str().unwrap_or(""),
StreamUrl::Disc { device: None } => "",
StreamUrl::M2ts { path } | StreamUrl::Mkv { path } | StreamUrl::Iso { path } => {
path.to_str().unwrap_or("")
}
StreamUrl::Network { addr } => addr,
StreamUrl::Stdio | StreamUrl::Null => "",
StreamUrl::Unknown { raw } => raw,
}
}
pub fn is_disc_source(&self) -> bool {
matches!(self, StreamUrl::Disc { .. } | StreamUrl::Iso { .. })
}
}
pub fn parse_url(url: &str) -> StreamUrl {
if let Some(rest) = url.strip_prefix("disc://") {
return if rest.is_empty() {
StreamUrl::Disc { device: None }
} else {
StreamUrl::Disc { device: Some(PathBuf::from(rest)) }
};
}
if let Some(rest) = url.strip_prefix("m2ts://") {
return StreamUrl::M2ts { path: PathBuf::from(rest) };
}
if let Some(rest) = url.strip_prefix("mkv://") {
return StreamUrl::Mkv { path: PathBuf::from(rest) };
}
if let Some(rest) = url.strip_prefix("network://") {
return StreamUrl::Network { addr: rest.to_string() };
}
if url == "null://" || url.starts_with("null://") {
return StreamUrl::Null;
}
if url == "stdio://" || url.starts_with("stdio://") {
return StreamUrl::Stdio;
}
if let Some(rest) = url.strip_prefix("iso://") {
return StreamUrl::Iso { path: PathBuf::from(rest) };
}
StreamUrl::Unknown { raw: url.to_string() }
}
fn validate_file_path(path: &Path, scheme: &str) -> io::Result<()> {
if path.as_os_str().is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{scheme}:// requires a file path (e.g. {scheme}://movie.{scheme})"),
));
}
if path.file_name().is_none() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{scheme}://{} is not a valid file path — must include a filename", path.display()),
));
}
Ok(())
}
fn validate_network_addr(addr: &str) -> io::Result<()> {
if addr.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"network:// requires host:port (e.g. network://0.0.0.0:9000)",
));
}
if !addr.contains(':') {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("network://{addr} missing port — use network://{addr}:PORT"),
));
}
Ok(())
}
pub fn open_input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn IOStream>> {
let parsed = parse_url(url);
match parsed {
StreamUrl::Disc { device } => {
let disc_opts = DiscOptions {
device,
keydb_path: opts.keydb_path.as_ref().map(|p| p.into()),
title_index: opts.title_index,
};
let stream = DiscStream::open(disc_opts)
.map_err(|e| io::Error::other(e.to_string()))?;
Ok(Box::new(stream))
}
StreamUrl::M2ts { ref path } => {
validate_file_path(path, "m2ts")?;
let file = std::fs::File::open(path)
.map_err(|e| io::Error::new(e.kind(),
format!("m2ts://{}: {}", path.display(), e)))?;
let reader = BufReader::with_capacity(IO_BUF_SIZE, file);
Ok(Box::new(M2tsStream::open(reader)?))
}
StreamUrl::Mkv { ref path } => {
validate_file_path(path, "mkv")?;
let file = std::fs::File::open(path)
.map_err(|e| io::Error::new(e.kind(),
format!("mkv://{}: {}", path.display(), e)))?;
let reader = BufReader::with_capacity(IO_BUF_SIZE, file);
Ok(Box::new(MkvStream::open(reader)?))
}
StreamUrl::Network { ref addr } => {
validate_network_addr(addr)?;
Ok(Box::new(NetworkStream::listen(addr)?))
}
StreamUrl::Stdio => {
Ok(Box::new(StdioStream::input()))
}
StreamUrl::Iso { ref path } => {
validate_file_path(path, "iso")?;
let scan_opts = match &opts.keydb_path {
Some(p) => crate::disc::ScanOptions::with_keydb(p),
None => crate::disc::ScanOptions::default(),
};
Ok(Box::new(IsoStream::open(&path.to_string_lossy(), opts.title_index, &scan_opts)?))
}
StreamUrl::Null => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
"null:// is write-only — cannot use as input"))
}
StreamUrl::Unknown { ref raw } => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("'{}' is not a valid stream URL — use scheme://path (e.g. mkv://movie.mkv, disc://, m2ts://movie.m2ts)", raw)))
}
}
}
pub fn open_output(url: &str, meta: &DiscTitle) -> io::Result<Box<dyn IOStream>> {
let parsed = parse_url(url);
match parsed {
StreamUrl::Disc { .. } => {
Err(io::Error::new(io::ErrorKind::Unsupported,
"disc:// is read-only — cannot use as output"))
}
StreamUrl::Iso { ref path } => {
validate_file_path(path, "iso")?;
Ok(Box::new(IsoStream::create(&path.to_string_lossy())?.meta(meta)))
}
StreamUrl::Null => {
Ok(Box::new(NullStream::new().meta(meta)))
}
StreamUrl::Stdio => {
Ok(Box::new(StdioStream::output().meta(meta)))
}
StreamUrl::M2ts { ref path } => {
validate_file_path(path, "m2ts")?;
let file = std::fs::File::create(path)
.map_err(|e| io::Error::new(e.kind(),
format!("m2ts://{}: {}", path.display(), e)))?;
let writer = BufWriter::with_capacity(IO_BUF_SIZE, file);
Ok(Box::new(M2tsStream::new(writer).meta(meta)))
}
StreamUrl::Mkv { ref path } => {
validate_file_path(path, "mkv")?;
let file = std::fs::File::create(path)
.map_err(|e| io::Error::new(e.kind(),
format!("mkv://{}: {}", path.display(), e)))?;
let writer = BufWriter::with_capacity(IO_BUF_SIZE, file);
let lookahead = if meta.streams.len() > 15 {
MKV_LOOKAHEAD_UHD
} else {
MKV_LOOKAHEAD_DEFAULT
};
Ok(Box::new(MkvStream::new(writer).meta(meta).max_buffer(lookahead)))
}
StreamUrl::Network { ref addr } => {
validate_network_addr(addr)?;
Ok(Box::new(NetworkStream::connect(addr)?.meta(meta)))
}
StreamUrl::Unknown { ref raw } => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("'{}' is not a valid stream URL — use scheme://path (e.g. mkv://movie.mkv, m2ts://movie.m2ts, null://)", raw)))
}
}
}
#[derive(Default)]
pub struct InputOptions {
pub keydb_path: Option<String>,
pub title_index: Option<usize>,
}