vsd 0.5.0

A command-line utility and library for downloading streams from DASH manifests and HLS playlists.
Documentation
use crate::{
    PlaylistDownloadConfig, dash,
    error::{Error, Result},
    format::{FormatExpr, SelectType},
    hls,
    playlist::{MasterPlaylist, MediaPlaylist, PlaylistType},
    utils,
};
use base64::Engine;
use log::debug;
use reqwest::{Url, header};
use std::path::Path;
use tokio::fs;

pub async fn playlist(
    config: &PlaylistDownloadConfig,
    base_url: &Option<Url>,
    uri: &str,
) -> Result<FetchedPlaylist> {
    let path = Path::new(uri);
    let mut typ = None;

    if path.exists() {
        let Some(base_url) = base_url else {
            bail!("--baseurl flag is required for local playlist file.");
        };

        if let Some(ext) = path.extension() {
            if ext == "mpd" {
                typ = Some(PlaylistType::Dash)
            } else if ext == "m3u" || ext == "m3u8" {
                typ = Some(PlaylistType::Hls)
            }
        }

        Ok(FetchedPlaylist {
            url: base_url.to_owned(),
            data: fs::read(path).await?,
            typ,
        })
    } else if let Ok(input) = uri.parse::<Url>() {
        debug!("Fetching {} (playlist)", input);
        let response = config
            .client
            .get(input)
            .query(&*config.query)
            .send()
            .await?;

        if let Some(content_type) = response
            .headers()
            .get(header::CONTENT_TYPE)
            .and_then(|x| x.to_str().ok())
        {
            if content_type == "application/dash+xml" || content_type == "video/vnd.mpeg.dash.mpd" {
                typ = Some(PlaylistType::Dash)
            } else if content_type == "application/x-mpegurl"
                || content_type == "application/vnd.apple.mpegurl"
            {
                typ = Some(PlaylistType::Hls)
            }
        }

        Ok(FetchedPlaylist {
            url: response.url().to_owned(),
            data: utils::fetch_bytes(response).await?,
            typ,
        })
    } else {
        bail!("Unable to determine playlist type.");
    }
}

pub struct FetchedPlaylist {
    url: Url,
    data: Vec<u8>,
    typ: Option<PlaylistType>,
}

impl FetchedPlaylist {
    fn playlist_type(&self) -> Result<PlaylistType> {
        if let Some(typ) = &self.typ {
            return Ok(typ.to_owned());
        }
        if self.data.windows(7).any(|w| w == b"#EXTM3U") {
            return Ok(PlaylistType::Hls);
        }
        if self.data.windows(4).any(|w| w == b"<MPD") {
            return Ok(PlaylistType::Dash);
        }
        bail!("Unable to determine playlist type.");
    }

    pub async fn parse(
        &self,
        config: &PlaylistDownloadConfig,
        format_expr: &FormatExpr,
        select_type: &SelectType,
        partial_parse: bool,
    ) -> Result<MasterPlaylist> {
        match self.playlist_type()? {
            PlaylistType::Dash => {
                let xml = String::from_utf8_lossy(&self.data);
                let Ok(mpd) = dash_mpd::parse(&xml) else {
                    bail!("Unable to parse dash playlist.");
                };
                let mut pl = dash::parse_as_master(&self.url, &mpd).sort_streams();

                if partial_parse {
                    pl = pl.select_streams(format_expr, select_type)?;
                }

                for stream in &mut pl.streams {
                    dash::push_segments(config, &self.url, &mpd, stream).await?;
                }

                Ok(pl)
            }
            PlaylistType::Hls => {
                match m3u8_rs::parse_playlist_res(&self.data)
                    .map_err(|_| Error::Other("Unable to parse hls playlist.".into()))?
                {
                    m3u8_rs::Playlist::MasterPlaylist(m3u8) => {
                        let mut pl = hls::parse_as_master(&self.url, &m3u8).sort_streams();

                        if partial_parse {
                            pl = pl.select_streams(format_expr, select_type)?;
                        }

                        for stream in &mut pl.streams {
                            let m3u8 = if let Some(bs) = stream
                                .uri
                                .clone()
                                .strip_prefix("data:application/x-mpegurl;base64,")
                            {
                                stream.uri = self.url.to_string();
                                base64::engine::general_purpose::STANDARD.decode(bs)?
                            } else {
                                stream.uri = self.url.join(&stream.uri)?.to_string();
                                debug!("Fetching {} (media-playlist)", stream.uri);
                                let response = config
                                    .client
                                    .get(&stream.uri)
                                    .query(&*config.query)
                                    .send()
                                    .await?;
                                utils::fetch_bytes(response).await?
                            };

                            let media_playlist =
                                m3u8_rs::parse_media_playlist_res(&m3u8).map_err(|_| {
                                    Error::Other("Unable to parse hls playlist.".into())
                                })?;
                            hls::push_segments(stream, media_playlist);
                        }

                        Ok(pl)
                    }
                    m3u8_rs::Playlist::MediaPlaylist(m3u8) => {
                        let mut stream = MediaPlaylist {
                            id: utils::gen_id(self.url.as_str(), ""),
                            playlist_type: PlaylistType::Hls,
                            uri: self.url.to_string(),
                            ..Default::default()
                        };
                        hls::push_segments(&mut stream, m3u8);
                        Ok(MasterPlaylist {
                            playlist_type: PlaylistType::Hls,
                            streams: vec![stream],
                            uri: self.url.to_string(),
                        })
                    }
                }
            }
        }
    }
}