Skip to main content

anni_provider/
common.rs

1use async_trait::async_trait;
2use std::borrow::Cow;
3use std::collections::HashSet;
4use std::num::NonZeroU8;
5use std::path::PathBuf;
6use std::pin::Pin;
7use thiserror::Error;
8use tokio::io::AsyncRead;
9use tokio_stream::Stream;
10
11pub type Result<T> = std::result::Result<T, ProviderError>;
12pub type ResourceReader = Pin<Box<dyn AsyncRead + Send>>;
13
14#[derive(Clone)]
15pub struct AudioInfo {
16    /// File extension of the file
17    pub extension: String,
18    /// File size of the file
19    pub size: usize,
20    /// Audio duration of the file, in milliseconds
21    pub duration: u64,
22}
23
24/// AudioResourceReader abstracts the file result a provider returns with extra information of audio
25pub struct AudioResourceReader {
26    /// Audio info
27    pub info: AudioInfo,
28    /// File range
29    pub range: Range,
30    /// Async Reader for the file
31    pub reader: ResourceReader,
32}
33
34#[derive(Clone, Copy)]
35pub struct Range {
36    pub start: u64,
37    pub end: Option<u64>,
38    pub total: Option<u64>,
39}
40
41impl Range {
42    pub const FULL: Range = Range {
43        start: 0,
44        end: None,
45        total: None,
46    };
47
48    pub const FLAC_HEADER: Range = Range {
49        start: 0,
50        end: Some(42),
51        total: None,
52    };
53
54    /// create a new range with given start and end offset
55    pub fn new(start: u64, end: Option<u64>) -> Self {
56        Self {
57            start,
58            end,
59            total: None,
60        }
61    }
62
63    /// get the length of the range
64    /// if the range is full, returns None
65    pub fn length(&self) -> Option<u64> {
66        self.end.map(|end| end - self.start + 1)
67    }
68
69    /// return length limited by a limit(usually actual file size)
70    pub fn length_limit(&self, limit: u64) -> u64 {
71        let end = match self.end {
72            Some(end) => std::cmp::min(end, limit),
73            None => limit,
74        };
75        end - self.start + 1
76    }
77
78    /// return a new Range with updated end property
79    pub fn end_with(&self, end: u64) -> Self {
80        Self {
81            start: self.start,
82            end: match self.end {
83                Some(e) => Some(e.min(end - 1)),
84                None => Some(end - 1),
85            },
86            total: match self.total {
87                Some(total) => Some(total.min(end)),
88                None => Some(end),
89            },
90        }
91    }
92
93    pub fn is_full(&self) -> bool {
94        self.start == 0 && self.end.is_none()
95    }
96
97    pub fn contains_flac_header(&self) -> bool {
98        if self.start == 0 {
99            match self.end {
100                Some(end) => end + 1 >= 42,
101                None => true,
102            }
103        } else {
104            false
105        }
106    }
107
108    pub fn to_range_header(&self) -> Option<String> {
109        if self.is_full() {
110            None
111        } else {
112            Some(match self.end {
113                Some(end) => format!("bytes={}-{}", self.start, end),
114                None => format!("bytes={}-", self.start),
115            })
116        }
117    }
118
119    pub fn to_content_range_header(&self) -> String {
120        if self.is_full() {
121            "bytes */*".to_string()
122        } else {
123            match (self.end, self.total) {
124                (Some(end), Some(total)) => format!("bytes {}-{}/{}", self.start, end, total),
125                (Some(end), None) => format!("bytes {}-{}/*", self.start, end),
126                _ => format!("bytes {}-", self.start),
127            }
128        }
129    }
130}
131
132/// AnniProvider is a common trait for anni resource providers.
133/// It provides functions to get cover, audio, album list and reload.
134#[async_trait]
135// work around to add a default implementation for has_albums()
136// https://github.com/rust-lang/rust/issues/51443
137// https://docs.rs/async-trait/latest/async_trait/index.html#dyn-traits
138pub trait AnniProvider: Sync {
139    /// Get album information provided by provider.
140    async fn albums(&self) -> Result<HashSet<Cow<str>>>;
141
142    /// Returns whether given album exists
143    async fn has_album(&self, album_id: &str) -> bool {
144        self.albums()
145            .await
146            .unwrap_or(HashSet::new())
147            .contains(album_id)
148    }
149
150    /// Get audio info describing basic information of the audio file.
151    async fn get_audio_info(
152        &self,
153        album_id: &str,
154        disc_id: NonZeroU8,
155        track_id: NonZeroU8,
156    ) -> Result<AudioInfo> {
157        Ok(self
158            .get_audio(album_id, disc_id, track_id, Range::FLAC_HEADER)
159            .await?
160            .info)
161    }
162
163    /// Returns a reader implements AsyncRead for content reading
164    async fn get_audio(
165        &self,
166        album_id: &str,
167        disc_id: NonZeroU8,
168        track_id: NonZeroU8,
169        range: Range,
170    ) -> Result<AudioResourceReader>;
171
172    /// Returns a cover of corresponding album
173    async fn get_cover(&self, album_id: &str, disc_id: Option<NonZeroU8>)
174        -> Result<ResourceReader>;
175
176    /// Reloads the provider for new albums
177    async fn reload(&mut self) -> Result<()>;
178}
179
180#[async_trait]
181impl AnniProvider for Box<dyn AnniProvider + Send + Sync> {
182    async fn albums(&self) -> Result<HashSet<Cow<str>>> {
183        self.as_ref().albums().await
184    }
185
186    async fn has_album(&self, album_id: &str) -> bool {
187        self.as_ref().has_album(album_id).await
188    }
189
190    async fn get_audio_info(
191        &self,
192        album_id: &str,
193        disc_id: NonZeroU8,
194        track_id: NonZeroU8,
195    ) -> Result<AudioInfo> {
196        self.as_ref()
197            .get_audio_info(album_id, disc_id, track_id)
198            .await
199    }
200
201    async fn get_audio(
202        &self,
203        album_id: &str,
204        disc_id: NonZeroU8,
205        track_id: NonZeroU8,
206        range: Range,
207    ) -> Result<AudioResourceReader> {
208        self.as_ref()
209            .get_audio(album_id, disc_id, track_id, range)
210            .await
211    }
212
213    async fn get_cover(
214        &self,
215        album_id: &str,
216        disc_id: Option<NonZeroU8>,
217    ) -> Result<ResourceReader> {
218        self.as_ref().get_cover(album_id, disc_id).await
219    }
220
221    async fn reload(&mut self) -> Result<()> {
222        self.as_mut().reload().await
223    }
224}
225
226#[derive(Clone)]
227pub struct FileEntry {
228    pub name: String,
229    pub path: PathBuf,
230}
231
232#[async_trait]
233pub trait FileSystemProvider: Sync {
234    /// List sub folders
235    async fn children(
236        &self,
237        path: &PathBuf,
238    ) -> Result<Pin<Box<dyn Stream<Item = FileEntry> + Send>>>;
239
240    /// Get file entry in a folder with given prefix
241    async fn get_file_entry_by_prefix(&self, parent: &PathBuf, prefix: &str) -> Result<FileEntry>;
242
243    /// Get file reader
244    async fn get_file(&self, path: &PathBuf, range: Range) -> Result<ResourceReader>;
245
246    /// Get audio info: (extension ,size)
247    async fn get_audio_info(&self, path: &PathBuf) -> Result<(String, usize)>;
248
249    // TODO: move this method to a sub trait
250    async fn get_audio_file(&self, path: &PathBuf, range: Range) -> Result<AudioResourceReader> {
251        let reader = self.get_file(path, range).await?;
252        let metadata = self.get_audio_info(path).await?;
253        let (duration, reader) = crate::utils::read_duration(reader, range).await?;
254        Ok(AudioResourceReader {
255            info: AudioInfo {
256                extension: metadata.0,
257                size: metadata.1,
258                duration,
259            },
260            range: Range {
261                start: range.start,
262                end: Some(range.end.unwrap_or(metadata.1 as u64 - 1)),
263                total: Some(metadata.1 as u64),
264            },
265            reader,
266        })
267    }
268
269    /// Reload
270    async fn reload(&mut self) -> Result<()>;
271}
272
273#[derive(Debug, Error)]
274pub enum ProviderError {
275    #[error("invalid path")]
276    InvalidPath,
277
278    #[error("file not found")]
279    FileNotFound,
280
281    #[error(transparent)]
282    IOError(#[from] std::io::Error),
283
284    #[cfg(feature = "repo")]
285    #[error(transparent)]
286    RepoError(#[from] anni_repo::error::Error),
287
288    #[cfg(feature = "anni-google-drive3")]
289    #[error(transparent)]
290    OAuthError(#[from] anni_google_drive3::oauth2::Error),
291
292    #[cfg(feature = "anni-google-drive3")]
293    #[error(transparent)]
294    DriveError(#[from] anni_google_drive3::Error),
295
296    #[cfg(feature = "reqwest")]
297    #[error(transparent)]
298    RequestError(#[from] reqwest::Error),
299
300    #[error(transparent)]
301    FlacError(#[from] anni_flac::error::FlacError),
302
303    #[error("an error occurred")]
304    GeneralError,
305}
306
307pub fn strict_album_path(root: &PathBuf, album_id: &str, layer: usize) -> PathBuf {
308    let mut res = root.clone();
309    for i in 0..layer {
310        res.push(match &album_id[i * 2..=i * 2 + 1].trim_start_matches('0') {
311            &"" => "0",
312            s @ _ => s,
313        });
314    }
315    res.join(album_id)
316}
317
318pub(crate) fn content_range_to_range(content_range: Option<&str>) -> Range {
319    match content_range {
320        Some(content_range) => {
321            // if content range header is invalid, return the full range
322            if content_range.len() <= 6 {
323                return Range::FULL;
324            }
325
326            // else, parse the range
327            // Content-Range: bytes 0-1023/10240
328            //                      | offset = 6
329            let content_range = &content_range[6..];
330            let (from, content_range) =
331                content_range.split_once('-').unwrap_or((content_range, ""));
332            let (to, total) = content_range.split_once('/').unwrap_or((content_range, ""));
333
334            Range {
335                start: from.parse().unwrap_or(0),
336                end: to.parse().ok(),
337                total: total.parse().ok(),
338            }
339        }
340        None => Range::FULL,
341    }
342}