mod cache;
mod multivariant;
mod playlist;
mod segment;
pub use multivariant::Stream;
pub use playlist::Playlist;
pub use segment::{Handler, ResetError};
use std::{
borrow::Cow,
fmt::{self, Debug, Display, Formatter},
};
use anyhow::{Context, Result, bail, ensure};
use crate::{
args::{Parse, Parser},
http::{StatusError, Url},
};
#[derive(Debug)]
pub struct OfflineError;
impl std::error::Error for OfflineError {}
impl Display for OfflineError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Stream is offline or unavailable")
}
}
pub struct Args {
servers: Option<Vec<Url>>,
print_streams: bool,
no_low_latency: bool,
passthrough: Passthrough,
client_id: Option<String>,
auth_token: Option<String>,
codecs: Cow<'static, str>,
never_proxy: Option<Vec<String>>,
playlist_cache_dir: Option<String>,
use_cache_only: bool,
write_cache_only: bool,
force_playlist_url: Option<Url>,
channel: String,
quality: Option<String>,
}
impl Default for Args {
fn default() -> Self {
Self {
codecs: "av1,h265,h264".into(),
servers: Option::default(),
print_streams: bool::default(),
no_low_latency: bool::default(),
passthrough: Passthrough::default(),
client_id: Option::default(),
auth_token: Option::default(),
never_proxy: Option::default(),
playlist_cache_dir: Option::default(),
use_cache_only: bool::default(),
write_cache_only: bool::default(),
force_playlist_url: Option::default(),
channel: String::default(),
quality: Option::default(),
}
}
}
impl Debug for Args {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let hide_option = |arg: &Option<String>| -> Option<&'static str> {
match arg {
Some(_) => Some("<hidden>"),
None => None,
}
};
f.debug_struct("Args")
.field("servers", &self.servers)
.field("print_streams", &self.print_streams)
.field("no_low_latency", &self.no_low_latency)
.field("passthrough", &self.passthrough)
.field("client_id", &hide_option(&self.client_id))
.field("auth_token", &hide_option(&self.auth_token))
.field("codecs", &self.codecs)
.field("never_proxy", &self.never_proxy)
.field("playlist_cache_dir", &self.playlist_cache_dir)
.field("use_cache_only", &self.use_cache_only)
.field("write_cache_only", &self.write_cache_only)
.field("force_playlist_url", &self.force_playlist_url)
.field("channel", &self.channel)
.field("quality", &self.quality)
.finish()
}
}
impl Parse for Args {
fn parse(&mut self, parser: &mut Parser) -> Result<()> {
parser.parse_comma_list_cfg(&mut self.servers, "-s", "servers")?;
parser.parse_switch(&mut self.print_streams, "--print-streams")?;
parser.parse_switch(&mut self.no_low_latency, "--no-low-latency")?;
parser.parse_fn(&mut self.passthrough, "--passthrough", Passthrough::new)?;
parser.parse_opt(&mut self.client_id, "--client-id")?;
parser.parse_opt(&mut self.auth_token, "--auth-token")?;
parser.parse_cow_string(&mut self.codecs, "--codecs")?;
parser.parse_comma_list(&mut self.never_proxy, "--never-proxy")?;
parser.parse_opt(&mut self.playlist_cache_dir, "--playlist-cache-dir")?;
parser.parse_switch(&mut self.use_cache_only, "--use-cache-only")?;
parser.parse_switch(&mut self.write_cache_only, "--write-cache-only")?;
parser.parse_opt(&mut self.force_playlist_url, "--force-playlist-url")?;
if self.use_cache_only || self.write_cache_only {
ensure!(
self.playlist_cache_dir.is_some(),
"--playlist-cache-dir not configured"
);
}
ensure!(
!(self.use_cache_only && self.write_cache_only),
"--use-cache-only and --write-cache-only cannot be used together"
);
let channel = parser
.parse_free_required()
.context("Missing channel argument")?;
self.channel = channel
.rsplit_once('/')
.map_or(channel.as_str(), |s| s.1)
.to_lowercase();
parser.parse_free(&mut self.quality, "quality")?;
if self.print_streams {
self.quality = None;
}
if let Some(never_proxy) = &self.never_proxy
&& never_proxy.iter().any(|a| a.eq(&self.channel))
{
self.servers = None;
}
Ok(())
}
}
impl Args {
pub fn channel(&self) -> &str {
&self.channel
}
}
#[derive(Debug, Default)]
enum Passthrough {
Variant,
Multivariant,
#[default]
Disabled,
}
impl Passthrough {
fn new(arg: &str) -> Result<Self> {
match arg {
"variant" => Ok(Self::Variant),
"multivariant" => Ok(Self::Multivariant),
"disabled" => Ok(Self::Disabled),
_ => bail!("Invalid passthrough mode"),
}
}
}
fn map_if_offline(error: anyhow::Error) -> anyhow::Error {
if StatusError::is_not_found(&error) {
return OfflineError.into();
}
error
}