twitch-hls-client 1.3.3

Minimal CLI client for watching/recording Twitch streams
#![allow(clippy::unnecessary_wraps)] //function pointers

pub mod playlist;
pub mod segment;

use anyhow::{Context, Result};
use std::fmt::{self, Display, Formatter};

use crate::{
    args::{ArgParser, Parser},
    http::Url,
};

#[derive(Debug)]
pub enum Error {
    Offline,
}

impl std::error::Error for Error {}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        match self {
            Self::Offline => write!(f, "Stream is offline or unavailable"),
        }
    }
}

#[derive(Debug)]
pub struct Args {
    servers: Option<Vec<Url>>,
    client_id: Option<String>,
    auth_token: Option<String>,
    never_proxy: Option<Vec<String>>,
    codecs: String,
    no_low_latency: bool,
    channel: String,
}

impl Default for Args {
    fn default() -> Self {
        Self {
            codecs: "av1,h265,h264".to_owned(),
            no_low_latency: bool::default(),
            servers: Option::default(),
            client_id: Option::default(),
            auth_token: Option::default(),
            never_proxy: Option::default(),
            channel: String::default(),
        }
    }
}

impl ArgParser for Args {
    fn parse(&mut self, parser: &mut Parser) -> Result<()> {
        parser.parse_fn_cfg(&mut self.servers, "-s", "servers", Self::split_comma)?;
        parser.parse_fn(&mut self.client_id, "--client-id", Parser::parse_opt_string)?;
        parser.parse_fn(
            &mut self.auth_token,
            "--auth-token",
            Parser::parse_opt_string,
        )?;
        parser.parse(&mut self.codecs, "--codecs")?;
        parser.parse_fn(&mut self.never_proxy, "--never-proxy", Self::split_comma)?;
        parser.parse_switch(&mut self.no_low_latency, "--no-low-latency")?;

        self.channel = parser
            .parse_free_required::<String>()
            .context("Missing channel argument")?
            .to_lowercase()
            .replace("twitch.tv/", "");

        if let Some(ref never_proxy) = self.never_proxy {
            if never_proxy.iter().any(|a| a.eq(&self.channel)) {
                self.servers = None;
            }
        }

        Ok(())
    }
}

impl Args {
    fn split_comma<T: for<'a> From<&'a str>>(arg: &str) -> Result<Option<Vec<T>>> {
        Ok(Some(arg.split(',').map(T::from).collect()))
    }
}