Skip to main content

anni_provider/providers/
no_cache.rs

1use crate::{
2    strict_album_path, AnniProvider, AudioInfo, AudioResourceReader, Range, ResourceReader,
3};
4use std::borrow::Cow;
5use std::collections::{HashSet, VecDeque};
6use std::io::SeekFrom;
7use std::num::NonZeroU8;
8use std::path::PathBuf;
9use tokio::io::{AsyncReadExt, AsyncSeekExt};
10use uuid::Uuid;
11
12/// `NoCacheStrictLocalProvider` defines a providers which serves local files without caching,
13/// which is useful for development and testing.
14///
15/// The main purpose of using this provider is to support annil for anni workspace.
16pub struct NoCacheStrictLocalProvider {
17    /// Root directory of an strict annil
18    pub root: PathBuf,
19    /// Hash layers
20    pub layer: usize,
21}
22
23#[async_trait::async_trait]
24impl AnniProvider for NoCacheStrictLocalProvider {
25    /// During the list operation, the provider will scan the whole library and return all available albums.
26    ///
27    /// Here **available** means the album directory exists and is not empty.
28    /// The non-empty assumption is made because a workspace may create an empty directory for further use.
29    async fn albums(&self) -> crate::Result<HashSet<Cow<str>>> {
30        // pre-allocate with capacity = 32
31        // assume commonly there are around 32 albums in a workspace
32        let mut albums = HashSet::with_capacity(32);
33
34        // scan the root directory
35        let mut vis = VecDeque::from([(self.root.clone(), 0)]);
36        while let Some((ref path, layer)) = vis.pop_front() {
37            log::debug!("Walking dir: {path:?}");
38            let mut reader = path.read_dir()?;
39            if layer == self.layer {
40                while let Some(entry) = reader.next() {
41                    let entry = entry?;
42                    match Uuid::parse_str(&entry.file_name().to_string_lossy()) {
43                        Ok(album_id) => {
44                            albums.insert(album_id.to_string());
45                        }
46                        _ => log::warn!("Unexpected dir: {path:?}"),
47                    }
48                }
49            } else {
50                while let Some(entry) = reader.next() {
51                    let entry = entry?;
52                    if entry.metadata()?.is_dir() {
53                        vis.push_back((entry.path(), layer + 1));
54                    }
55                }
56            }
57        }
58
59        Ok(albums.into_iter().map(Cow::Owned).collect())
60    }
61
62    async fn get_audio(
63        &self,
64        album_id: &str,
65        disc_id: NonZeroU8,
66        track_id: NonZeroU8,
67        range: Range,
68    ) -> crate::Result<AudioResourceReader> {
69        let mut audio = strict_album_path(&self.root, album_id, self.layer);
70        audio.push(disc_id.get().to_string());
71        audio.push(format!("{track_id}.flac"));
72
73        if !audio.exists() {
74            return Err(crate::ProviderError::FileNotFound);
75        }
76
77        let mut file = tokio::fs::File::open(audio).await?;
78        let metadata = file.metadata().await?;
79        let file_size = metadata.len();
80
81        file.seek(SeekFrom::Start(range.start)).await?;
82        let file = file.take(range.length_limit(file_size));
83        let reader = Box::pin(file);
84        let (duration, reader) = crate::utils::read_duration(reader, range).await?;
85
86        Ok(AudioResourceReader {
87            info: AudioInfo {
88                extension: "flac".to_string(),
89                size: file_size as usize,
90                duration,
91            },
92            range: Range {
93                start: range.start,
94                end: Some(range.end.unwrap_or(file_size - 1)),
95                total: Some(file_size),
96            },
97            reader,
98        })
99    }
100
101    async fn get_cover(
102        &self,
103        album_id: &str,
104        disc_id: Option<NonZeroU8>,
105    ) -> crate::Result<ResourceReader> {
106        let mut cover = strict_album_path(&self.root, album_id, self.layer);
107        if let Some(disc_id) = disc_id {
108            cover.push(disc_id.get().to_string());
109        }
110        cover.push(format!("cover.jpg"));
111
112        if !cover.exists() {
113            return Err(crate::ProviderError::FileNotFound);
114        }
115
116        let file = tokio::fs::File::open(cover).await?;
117        Ok(Box::pin(file))
118    }
119
120    async fn reload(&mut self) -> crate::Result<()> {
121        Ok(())
122    }
123}