selene-core 0.9.0-rc.1

Backend for selene-server
Documentation
use std::{
    fs,
    io::{Read, Seek},
    path::{Path, PathBuf},
};

use serde::{Deserialize, Serialize};
use symphonia::core::{
    formats::{FormatOptions, TrackType, probe::Hint},
    io::MediaSourceStream,
    meta::MetadataOptions,
};
use thiserror::Error;

mod codec;
pub use codec::*;

mod format;
pub use format::*;

mod sample_format;
pub use sample_format::*;

mod stream;
pub use stream::*;

#[derive(Debug, Error)]
pub enum ContainerError {
    #[error("IoError: {0}")]
    Io(#[from] std::io::Error),

    #[error("Codec Error: {0}")]
    Codec(#[from] CodecError),

    #[error("Symphonia Error: {0}")]
    Symphonia(#[from] symphonia::core::errors::Error),

    #[error("File '{0}' has no tracks/streams")]
    NoStream(PathBuf),

    #[error("File '{0}' has an unsupported container")]
    InvalidSource(PathBuf),

    #[error("'{0:?}' is an unsupported container")]
    UnsupportedContainer(Format),

    #[error("Couldn't find sample rate")]
    NoSampleRate,

    #[error("Couldn't find channel count'")]
    NoChannelCount,

    #[error("Couldn't find an audio stream")]
    NoAudioStream,

    #[error("The track is unplayable")]
    Unplayable,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct MediaContainer {
    pub(crate) format: Format,
    pub(crate) stream: Stream,
}

impl MediaContainer {
    #[must_use]
    pub fn format(&self) -> Format {
        self.format
    }

    /// Returns the file extension this [`MediaContainer`] uses.
    #[must_use]
    pub fn extension(&self) -> &'static str {
        let format = self.format;
        let codec = self.stream.codec_params.codec;
        match format {
            Format::Flac if codec.is_flac() => "flac",
            Format::Mp3 if codec.is_mp3() => "mp3",
            Format::Ogg if codec.is_vorbis() => "ogg",
            Format::Ogg if codec.is_opus() => "opus",
            Format::Ogg if codec.is_flac() => "oga",
            Format::Wav if codec.is_pcm() => "wav",
            Format::Aiff if codec.is_pcm() => "aiff",
            Format::Ape if codec.is_ape() => "ape",
            _ => panic!("Invalid format/codec pairing: {format:?} + {codec:?}"),
        }
    }

    /// Returns the mime type of this [`MediaContainer`] uses.
    #[must_use]
    pub fn mime_type(&self) -> &'static str {
        let format = self.format;
        let codec = self.stream.codec_params.codec;
        match format {
            Format::Flac if codec.is_flac() => "audio/flac",
            Format::Mp3 if codec.is_mp3() => "audio/mpeg",
            Format::Ogg if codec.is_vorbis() => "audio/ogg",
            Format::Ogg if codec.is_opus() => "audio/opus",
            Format::Ogg if codec.is_flac() => "audio/oga",
            Format::Wav if codec.is_pcm() => "audio/wav",
            Format::Aiff if codec.is_pcm() => "audio/aiff",
            Format::Ape if codec.is_ape() => "audio/x-ape",
            _ => panic!("Invalid format/codec pairing: {format:?} + {codec:?}"),
        }
    }

    #[must_use]
    pub fn stream(&self) -> &Stream {
        &self.stream
    }
}

pub fn extract_from_file(path: impl AsRef<Path>) -> Result<MediaContainer, ContainerError> {
    let path = path.as_ref();
    let mut file = fs::File::open(path)?;

    let mut buf = [0_u8; 16];
    file.read_exact(&mut buf)?;
    file.seek(std::io::SeekFrom::Start(0))?;

    let container_format =
        Format::from_buf(&buf).ok_or_else(|| ContainerError::InvalidSource(path.to_path_buf()))?;

    let mss = MediaSourceStream::new(Box::new(file), Default::default());
    let fmt_opts = FormatOptions::default();
    let metadata_opts = MetadataOptions::default();
    let probe =
        symphonia::default::get_probe().probe(&Hint::new(), mss, fmt_opts, metadata_opts)?;

    let track = probe
        .default_track(TrackType::Audio)
        .or_else(|| probe.first_track(TrackType::Audio))
        .ok_or(ContainerError::NoAudioStream)?;
    let stream = Stream::try_from(track)?;

    let container = MediaContainer {
        format: container_format,
        stream,
    };

    Ok(container)
}