anni-provider 0.1.3

Storage providers for Project Anni.
Documentation
use crate::{
    AnniProvider, AudioResourceReader, FileEntry, FileSystemProvider, ProviderError, Range,
    ResourceReader, Result,
};
use async_trait::async_trait;
use futures::StreamExt;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet, VecDeque};
use std::num::NonZeroU8;
use std::path::PathBuf;
use uuid::Uuid;

pub struct CommonStrictProvider {
    root: PathBuf,
    layer: usize,
    fs: Box<dyn FileSystemProvider + Send + Sync>,
    folders: HashMap<String, FileEntry>,
}

impl CommonStrictProvider {
    pub async fn new(
        root: PathBuf,
        layer: usize,
        fs: Box<dyn FileSystemProvider + Send + Sync>,
    ) -> Result<Self> {
        let mut me = Self {
            root,
            layer,
            fs,
            folders: HashMap::new(),
        };
        me.reload().await?;
        Ok(me)
    }
}

#[async_trait]
impl AnniProvider for CommonStrictProvider {
    async fn albums(&self) -> Result<HashSet<Cow<str>>> {
        Ok(self
            .folders
            .keys()
            .map(|c| Cow::Borrowed(c.as_str()))
            .collect())
    }

    async fn get_audio(
        &self,
        album_id: &str,
        disc_id: NonZeroU8,
        track_id: NonZeroU8,
        range: Range,
    ) -> Result<AudioResourceReader> {
        let disc = self.get_disc(album_id, disc_id).await?;
        let file = self
            .fs
            .get_file_entry_by_prefix(&disc.path, &format!("{track_id}."))
            .await?;
        self.fs.get_audio_file(&file.path, range).await
    }

    async fn get_cover(
        &self,
        album_id: &str,
        disc_id: Option<NonZeroU8>,
    ) -> Result<ResourceReader> {
        match disc_id {
            Some(disc_id) => {
                let disc = self.get_disc(album_id, disc_id).await?;
                let cover = self
                    .fs
                    .get_file_entry_by_prefix(&disc.path, "cover.jpg")
                    .await?;
                self.fs.get_file(&cover.path, Range::FULL).await
            }
            None => {
                let album = self
                    .folders
                    .get(album_id)
                    .ok_or(ProviderError::FileNotFound)?;
                let cover = self
                    .fs
                    .get_file_entry_by_prefix(&album.path, "cover.jpg")
                    .await?;
                self.fs.get_file(&cover.path, Range::FULL).await
            }
        }
    }

    async fn reload(&mut self) -> Result<()> {
        self.fs.reload().await?;
        self.reload_albums().await?;
        Ok(())
    }
}

impl CommonStrictProvider {
    pub async fn get_disc(&self, album_id: &str, disc_id: NonZeroU8) -> Result<FileEntry> {
        let folder = self
            .folders
            .get(album_id)
            .ok_or(ProviderError::FileNotFound)?;
        let mut folders = self.fs.children(&folder.path).await?;
        while let Some(folder) = folders.next().await {
            if folder.name == format!("{disc_id}") {
                return Ok(folder);
            }
        }
        Err(ProviderError::FileNotFound)
    }

    pub async fn reload_albums(&mut self) -> Result<()> {
        self.folders.clear();

        let mut vis = VecDeque::from([(self.root.clone(), 0)]);
        while let Some((ref path, layer)) = vis.pop_front() {
            log::debug!("Walking dir: {path:?}");
            let mut reader = self.fs.children(path).await?;
            if layer == self.layer {
                while let Some(entry) = reader.next().await {
                    match Uuid::parse_str(&entry.name) {
                        Ok(album_id) => {
                            self.folders.insert(album_id.to_string(), entry);
                        }
                        _ => log::warn!("Unexpected dir: {path:?}"),
                    }
                }
            } else {
                while let Some(entry) = reader.next().await {
                    vis.push_back((entry.path, layer + 1));
                }
            }
        }

        Ok(())
    }
}