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 pub extension: String,
18 pub size: usize,
20 pub duration: u64,
22}
23
24pub struct AudioResourceReader {
26 pub info: AudioInfo,
28 pub range: Range,
30 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 pub fn new(start: u64, end: Option<u64>) -> Self {
56 Self {
57 start,
58 end,
59 total: None,
60 }
61 }
62
63 pub fn length(&self) -> Option<u64> {
66 self.end.map(|end| end - self.start + 1)
67 }
68
69 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 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#[async_trait]
135pub trait AnniProvider: Sync {
139 async fn albums(&self) -> Result<HashSet<Cow<str>>>;
141
142 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 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 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 async fn get_cover(&self, album_id: &str, disc_id: Option<NonZeroU8>)
174 -> Result<ResourceReader>;
175
176 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 async fn children(
236 &self,
237 path: &PathBuf,
238 ) -> Result<Pin<Box<dyn Stream<Item = FileEntry> + Send>>>;
239
240 async fn get_file_entry_by_prefix(&self, parent: &PathBuf, prefix: &str) -> Result<FileEntry>;
242
243 async fn get_file(&self, path: &PathBuf, range: Range) -> Result<ResourceReader>;
245
246 async fn get_audio_info(&self, path: &PathBuf) -> Result<(String, usize)>;
248
249 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 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.len() <= 6 {
323 return Range::FULL;
324 }
325
326 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}