anni_provider/providers/
convention.rs1use crate::{
2 AnniProvider, AudioResourceReader, FileEntry, FileSystemProvider, ProviderError, Range,
3 ResourceReader, Result,
4};
5use anni_repo::db::RepoDatabaseRead;
6use anni_repo::library::{AlbumFolderInfo, DiscFolderInfo};
7use async_trait::async_trait;
8use parking_lot::Mutex;
9use std::borrow::Cow;
10use std::collections::{HashMap, HashSet};
11use std::num::NonZeroU8;
12use std::path::PathBuf;
13use std::str::FromStr;
14use tokio_stream::StreamExt;
15
16pub struct CommonConventionProvider {
17 root: PathBuf,
18 fs: Box<dyn FileSystemProvider + Send + Sync>,
19 repo: Mutex<RepoDatabaseRead>,
20
21 pub albums: HashMap<String, FileEntry>,
22 pub discs: HashMap<String, Vec<FileEntry>>,
23}
24
25impl CommonConventionProvider {
26 pub async fn new(
27 root: PathBuf,
28 repo: RepoDatabaseRead,
29 fs: Box<dyn FileSystemProvider + Send + Sync>,
30 ) -> Result<Self> {
31 let mut me = Self {
32 root,
33 fs,
34 repo: Mutex::new(repo),
35
36 albums: HashMap::new(),
37 discs: HashMap::new(),
38 };
39 me.reload().await?;
40 Ok(me)
41 }
42}
43
44#[async_trait]
45impl AnniProvider for CommonConventionProvider {
46 async fn albums(&self) -> Result<HashSet<Cow<str>>> {
47 Ok(self
48 .albums
49 .keys()
50 .map(|s| Cow::Borrowed(s.as_str()))
51 .collect())
52 }
53
54 async fn get_audio(
55 &self,
56 album_id: &str,
57 disc_id: NonZeroU8,
58 track_id: NonZeroU8,
59 range: Range,
60 ) -> Result<AudioResourceReader> {
61 let disc = self.get_disc(album_id, disc_id)?;
62 let file = self
63 .fs
64 .get_file_entry_by_prefix(&disc.path, &format!("{track_id:02}."))
65 .await?;
66 self.fs.get_audio_file(&file.path, range).await
67 }
68
69 async fn get_cover(
70 &self,
71 album_id: &str,
72 disc_id: Option<NonZeroU8>,
73 ) -> Result<ResourceReader> {
74 let folder = match disc_id {
75 Some(disc_id) => self.get_disc(album_id, disc_id)?,
76 _ => self
77 .albums
78 .get(album_id)
79 .ok_or(ProviderError::FileNotFound)?,
80 };
81 self.fs
82 .get_file(&folder.path.join("cover.jpg"), Range::FULL)
83 .await
84 }
85
86 async fn reload(&mut self) -> Result<()> {
87 self.fs.reload().await?;
88 self.repo.lock().reload()?;
89 self.reload_albums().await?;
90 Ok(())
91 }
92}
93
94impl CommonConventionProvider {
95 pub fn get_disc(&self, album_id: &str, disc_id: NonZeroU8) -> Result<&FileEntry> {
96 if let Some(album) = self.albums.get(album_id) {
97 if let Some(folders) = self.discs.get(album_id) {
98 folders
99 .get((disc_id.get() - 1) as usize)
100 .ok_or(ProviderError::FileNotFound)
101 } else {
102 Ok(album)
103 }
104 } else {
105 Err(ProviderError::FileNotFound)
106 }
107 }
108
109 pub async fn reload_albums(&mut self) -> Result<()> {
110 self.albums.clear();
111 self.discs.clear();
112
113 let mut to_visit = vec![self.root.clone()];
114 while let Some(dir) = to_visit.pop() {
115 self.walk_dir_impl(dir, &mut to_visit).await?;
116 }
117
118 Ok(())
119 }
120
121 async fn walk_dir_impl(&mut self, dir: PathBuf, to_visit: &mut Vec<PathBuf>) -> Result<()> {
122 log::debug!("Walking dir: {}", dir.display());
123 let mut dir = self.fs.children(&dir).await?;
124 while let Some(entry) = dir.next().await {
125 if let Ok(AlbumFolderInfo {
126 release_date,
127 catalog,
128 title,
129 edition,
130 disc_count,
131 }) = AlbumFolderInfo::from_str(&entry.name)
132 {
133 log::debug!("Found album {} at: {:?}", catalog, entry.path);
134 let album_id = self.repo.lock().match_album(
135 &catalog,
136 &release_date,
137 disc_count as u8,
138 &title,
139 edition.as_deref(),
140 )?;
141 match album_id {
142 Some(album_id) => {
143 if disc_count > 1 {
144 let discs = self.walk_discs(&entry.path, disc_count).await?;
146 self.discs.insert(album_id.to_string(), discs);
147 }
148 self.albums.insert(album_id.to_string(), entry);
149 }
150 None => {
151 log::warn!("Album ID not found for {}, ignoring...", catalog);
152 }
153 }
154 } else {
155 to_visit.push(entry.path.clone());
156 }
157 }
158 Ok(())
159 }
160
161 async fn walk_discs(&self, album: &PathBuf, size: usize) -> Result<Vec<FileEntry>> {
162 let mut discs = Vec::new();
163 let mut dir = self.fs.children(album).await?;
164 while let Some(entry) = dir.next().await {
165 if let Ok(DiscFolderInfo { info, disc_id }) = DiscFolderInfo::from_str(&entry.name) {
166 log::debug!("Found disc {} at: {:?}", info.catalog, entry.path);
167 if disc_id <= size {
168 discs.push((disc_id, entry));
169 }
170 }
171 }
172 discs.sort_by(|a, b| a.0.cmp(&b.0));
173 Ok(discs.into_iter().map(|(_, entry)| entry).collect())
174 }
175}