Skip to main content

anni_provider/providers/
convention.rs

1use 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                            // look for inner discs
145                            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}