1use crate::prelude::*;
2use anni_common::fs;
3use indexmap::IndexSet;
4use std::collections::{HashMap, HashSet};
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7use uuid::Uuid;
8
9pub struct RepositoryManager {
11 root: PathBuf,
12 repo: Repository,
13}
14
15impl RepositoryManager {
16 pub fn new<P>(root: P) -> RepoResult<Self>
17 where
18 P: AsRef<Path>,
19 {
20 let repo = root.as_ref().join("repo.toml");
21
22 #[cfg(feature = "git")]
23 crate::utils::git::setup_git2_internal();
24
25 Ok(Self {
26 root: root.as_ref().to_owned(),
27 repo: Repository::from_str(&fs::read_to_string(repo)?)?,
28 })
29 }
30
31 #[cfg(feature = "git")]
32 pub fn clone<P>(url: &str, root: P) -> RepoResult<Self>
33 where
34 P: AsRef<Path>,
35 {
36 crate::utils::git::setup_git2_internal();
37 git2::Repository::clone(url, root.as_ref())?;
38 Self::new(root.as_ref())
39 }
40
41 #[cfg(feature = "git")]
42 pub fn pull<P>(root: P, branch: &str) -> RepoResult<Self>
43 where
44 P: AsRef<Path>,
45 {
46 crate::utils::git::setup_git2_internal();
47 crate::utils::git::pull(root.as_ref(), branch)?;
48 Self::new(root.as_ref())
49 }
50
51 pub fn name(&self) -> &str {
52 self.repo.name()
53 }
54
55 pub fn edition(&self) -> &str {
56 self.repo.edition()
57 }
58
59 fn album_roots(&self) -> Vec<PathBuf> {
61 self.repo
62 .albums()
63 .iter()
64 .map(|album| self.root.join(album))
65 .collect()
66 }
67
68 fn default_album_root(&self) -> PathBuf {
69 self.root.join(
70 self.repo
71 .albums()
72 .get(0)
73 .map_or_else(|| "album", String::as_str),
74 )
75 }
76
77 pub fn all_album_paths(&self) -> RepoResult<Vec<PathBuf>> {
80 let mut paths = Vec::new();
81 for root in self.album_roots() {
82 let files = fs::read_dir(root)?;
83 for file in files {
84 let file = file?;
85 let path = file.path();
86 if path.is_file() {
87 if let Some(ext) = path.extension() {
88 if ext == "toml" {
89 paths.push(path);
90 }
91 }
92 } else if path.is_dir() {
93 let mut index = 0;
94 let catalog = file.file_name();
95 loop {
96 let path = path.join(&catalog).with_extension(format!("{index}.toml"));
97 if path.exists() {
98 paths.push(path);
99 index += 1;
100 } else {
101 break;
102 }
103 }
104 }
105 }
106 }
107 Ok(paths)
108 }
109
110 pub fn album_paths(&self, catalog: &str) -> RepoResult<Vec<PathBuf>> {
112 let mut paths = Vec::new();
113 for root in self.album_roots() {
114 let file = root.join(format!("{catalog}.toml"));
115 if file.exists() {
116 paths.push(file);
118 } else {
119 let folder = root.join(catalog);
120 if folder.exists() {
121 for file in fs::read_dir(folder)? {
123 let dir = file?;
124 if dir.path().extension() == Some("toml".as_ref()) {
125 paths.push(dir.path());
126 }
127 }
128 }
129 }
130 }
131 Ok(paths)
132 }
133
134 fn load_album<P>(&self, path: P) -> RepoResult<Album>
136 where
137 P: AsRef<Path>,
138 {
139 let input = fs::read_to_string(path.as_ref())?;
140 Album::from_str(&input)
141 }
142
143 pub fn load_albums(&self, catalog: &str) -> RepoResult<Vec<Album>> {
145 Ok(self
146 .album_paths(catalog)?
147 .into_iter()
148 .filter_map(|path| {
149 let album = self.load_album(&path);
150 match album {
151 Ok(album) => Some(album),
152 Err(err) => {
153 log::error!("Failed to load album in {path:?}: {err}",);
154 None
155 }
156 }
157 })
158 .collect())
159 }
160
161 pub fn add_album(&self, mut album: Album, allow_duplicate: bool) -> RepoResult<()> {
163 let catalog = album.catalog();
164 let folder = self.default_album_root().join(catalog);
165 let file = folder.with_extension("toml");
166
167 if folder.exists() {
168 let count = fs::PathWalker::new(&folder, false, false, Default::default())
170 .filter(|p|
171 p.extension() == Some("toml".as_ref()))
173 .count();
174 let new_file_name = format!("{catalog}.{count}.toml");
175 fs::write(folder.join(new_file_name), album.format_to_string())?;
176 } else if file.exists() {
177 if !allow_duplicate {
179 return Err(Error::RepoAlbumExists(catalog.to_string()));
180 }
181 fs::create_dir_all(&folder)?;
183 fs::rename(file, folder.join(format!("{catalog}.0.toml")))?;
185 fs::write(
187 folder.join(format!("{catalog}.1.toml")),
188 album.format_to_string(),
189 )?;
190 } else {
191 fs::write(&file, album.format_to_string())?;
193 }
194 Ok(())
195 }
196
197 pub fn into_owned_manager(self) -> RepoResult<OwnedRepositoryManager> {
198 OwnedRepositoryManager::new(self)
199 }
200
201 pub fn root(&self) -> &Path {
202 self.root.as_path()
203 }
204}
205
206pub struct OwnedRepositoryManager {
211 pub repo: RepositoryManager,
212
213 tags: HashMap<String, HashMap<TagType, Tag>>,
215 tags_relation: HashMap<TagRef<'static>, IndexSet<TagRef<'static>>>,
217 tag_path: HashMap<TagRef<'static>, PathBuf>,
219
220 album_tags: HashMap<TagRef<'static>, Vec<Uuid>>,
221 albums: HashMap<Uuid, Album>,
223 album_path: HashMap<Uuid, PathBuf>,
225}
226
227impl OwnedRepositoryManager {
228 pub fn new(repo: RepositoryManager) -> RepoResult<Self> {
229 let mut repo = Self {
230 repo,
231 tags: Default::default(),
232 tags_relation: Default::default(),
233 tag_path: Default::default(),
234 album_tags: Default::default(),
235 albums: Default::default(),
236 album_path: Default::default(),
237 };
238
239 let lock_file = repo.lock_file();
241 if lock_file.exists() {
242 return Err(Error::RepoInUse);
243 }
244
245 fs::write(lock_file, "")?;
246 repo.load_tags()?;
247 repo.load_albums()?;
248
249 Ok(repo)
250 }
251
252 fn lock_file(&self) -> PathBuf {
253 self.repo.root().join(".repo_lock")
254 }
255
256 pub fn album(&self, album_id: &Uuid) -> Option<&Album> {
257 self.albums.get(album_id)
258 }
259
260 pub fn album_path(&self, album_id: &Uuid) -> Option<&Path> {
261 self.album_path.get(album_id).map(|p| p.as_path())
262 }
263
264 pub fn albums(&self) -> &HashMap<Uuid, Album> {
265 &self.albums
266 }
267
268 pub fn albums_iter(&self) -> impl Iterator<Item = &Album> {
269 self.albums.values()
270 }
271
272 pub fn tag(&self, tag: &TagRef<'_>) -> Option<&Tag> {
273 self.tags
274 .get(tag.name())
275 .and_then(|tags| tags.get(tag.tag_type()))
276 }
277
278 pub fn tags_iter(&self) -> impl Iterator<Item = &Tag> {
279 self.tags.values().flat_map(|m| m.values())
280 }
281
282 pub fn tag_path<'a>(&'a self, tag: &'a TagRef<'_>) -> Option<&'a PathBuf> {
283 self.tag_path.get(tag)
284 }
285
286 pub fn child_tags<'me, 'tag>(&'me self, tag: &TagRef<'tag>) -> IndexSet<&'me TagRef<'tag>>
287 where
288 'tag: 'me,
289 {
290 self.tags_relation
291 .get(tag)
292 .map_or(IndexSet::new(), |children| children.iter().collect())
293 }
294
295 pub fn albums_tagged_by<'me, 'tag>(&'me self, tag: &'me TagRef<'tag>) -> Option<&'me Vec<Uuid>>
296 where
297 'tag: 'me,
298 {
299 self.album_tags.get(tag)
300 }
301
302 fn add_tag(&mut self, tag: Tag, tag_relative_path: PathBuf) -> Result<(), Error> {
303 if let Some(tag) = self.tag(tag.as_ref()) {
305 return Err(Error::RepoTagDuplicated(tag.get_owned_ref()));
306 }
307
308 let map = if let Some(map) = self.tags.get_mut(tag.name()) {
309 map
310 } else {
311 let map = HashMap::with_capacity(1);
312 self.tags.insert(tag.name().to_string(), map);
313 self.tags.get_mut(tag.name()).unwrap()
314 };
315 let tag_ref = tag.get_owned_ref();
316 map.insert(tag_ref.tag_type().clone(), tag);
317 self.tag_path.insert(tag_ref, tag_relative_path);
318 Ok(())
319 }
320
321 fn add_tag_relation(&mut self, parent: TagRef<'static>, child: TagRef<'static>) {
322 if let Some(children) = self.tags_relation.get_mut(&parent) {
323 children.insert(child);
324 } else {
325 let mut set = IndexSet::new();
326 set.insert(child);
327 self.tags_relation.insert(parent, set);
328 }
329 }
330
331 fn load_tags(&mut self) -> RepoResult<()> {
333 let tags_path =
335 fs::PathWalker::new(self.repo.root.join("tag"), true, false, Default::default())
336 .filter(|p| p.extension().map(|e| e == "toml").unwrap_or(false));
337
338 self.tags.clear();
340 self.tags_relation.clear();
341
342 for tag_file in tags_path {
344 let text = fs::read_to_string(&tag_file)?;
345 let tags = toml::from_str::<Tags>(&text)
346 .map_err(|e| Error::TomlParseError {
347 target: "Tags",
348 input: text,
349 err: e,
350 })?
351 .into_inner();
352 let relative_path = pathdiff::diff_paths(&tag_file, &self.repo.root).unwrap();
353
354 for tag in tags {
355 for parent in tag.parents() {
356 self.add_tag_relation(parent.0.clone(), tag.get_owned_ref());
357 }
358
359 for child in tag.simple_children() {
361 let parent = tag.get_owned_ref();
362 let full = child.clone().into_full(vec![parent.into()]);
363 self.add_tag(full, relative_path.clone())?;
364 self.add_tag_relation(tag.get_owned_ref(), child.clone());
365 }
366
367 self.add_tag(tag, relative_path.clone())?;
368 }
369 }
370
371 let all_tags: HashSet<_> = self.tags_iter().map(Tag::as_ref).collect();
373 let mut rel_tags: HashSet<_> = self.tags_relation.keys().collect();
374 let rel_children: HashSet<_> = self.tags_relation.values().flatten().collect();
375 rel_tags.extend(rel_children);
376 if !rel_tags.is_subset(&all_tags) {
377 return Err(Error::RepoTagsUndefined(
378 rel_tags.difference(&all_tags).cloned().cloned().collect(),
379 ));
380 }
381
382 Ok(())
383 }
384
385 fn load_albums(&mut self) -> RepoResult<()> {
386 self.album_tags.clear();
387
388 let mut problems = vec![];
389 for path in self.repo.all_album_paths()? {
390 let mut album = self.repo.load_album(&path)?;
391 album.resolve_tags(&self.tags)?;
392
393 let album_id = album.album_id();
394 let catalog = album.catalog();
395 let tags = album.tags();
396 if tags.is_empty() {
397 log::warn!(
399 "No tag found in album {}, catalog = {}",
400 album.album_id(),
401 catalog,
402 );
403 } else {
404 for tag_ref in tags {
405 if let None = self.tag(tag_ref) {
406 log::error!(
407 "Orphan tag {tag_ref} found in album {album_id}, catalog = {catalog}"
408 );
409 problems.push(Error::RepoTagsUndefined(vec![tag_ref.clone()]));
410 }
411
412 if !self.album_tags.contains_key(tag_ref) {
413 self.album_tags.insert(tag_ref.clone(), vec![]);
414 }
415 self.album_tags.get_mut(tag_ref).unwrap().push(album_id);
416 }
417 }
418 if let Some(album_with_same_id) = self.albums.insert(album_id, album) {
419 log::error!(
420 "Duplicated album id detected: {}",
421 album_with_same_id.album_id()
422 );
423 problems.push(Error::RepoDuplicatedAlbumId(album_id.to_string()));
424 }
425 self.album_path.insert(
426 album_id,
427 pathdiff::diff_paths(&path, &self.repo.root).unwrap(),
428 );
429 }
430
431 if problems.is_empty() {
432 Ok(())
433 } else {
434 Err(Error::MultipleErrors(problems))
435 }
436 }
437
438 pub fn check_tags_loop<'me, 'tag>(&'me self) -> Option<Vec<&'me TagRef<'tag>>>
439 where
440 'me: 'tag,
441 {
442 fn dfs<'tag, 'func>(
443 tag: &'tag TagRef<'tag>,
444 tags_relation: &'tag HashMap<TagRef<'static>, IndexSet<TagRef<'static>>>,
445 current: &'func mut HashMap<&'tag TagRef<'tag>, bool>,
446 visited: &'func mut HashMap<&'tag TagRef<'tag>, bool>,
447 mut path: Vec<&'tag TagRef<'tag>>,
448 ) -> (bool, Vec<&'tag TagRef<'tag>>) {
449 visited.insert(tag, true);
450 current.insert(tag, true);
451 path.push(tag);
452
453 if let Some(children) = tags_relation.get(tag) {
454 for child in children {
455 if let Some(true) = current.get(child) {
456 path.push(child);
457 return (true, path);
458 }
459 if !visited.get(child).map_or(false, |x| *x) {
461 let (loop_detected, loop_path) =
462 dfs(child, tags_relation, current, visited, path);
463 if loop_detected {
464 return (true, loop_path);
465 } else {
466 path = loop_path;
467 }
468 }
469 }
470 }
471
472 current.insert(tag, false);
473 path.pop();
474 (false, path)
475 }
476
477 let mut visited: HashMap<&TagRef, bool> = Default::default();
478 let mut current: HashMap<&TagRef, bool> = Default::default();
479 let tags: Vec<_> = self.tags_iter().map(|t| t.as_ref()).collect();
480 for tag in tags.into_iter() {
481 if !visited.get(&tag).map_or(false, |x| *x) {
483 let (loop_detected, path) = dfs(
484 tag,
485 &self.tags_relation,
486 &mut current,
487 &mut visited,
488 Default::default(),
489 );
490 if loop_detected {
491 return Some(path);
492 }
493 }
494 }
495
496 None
497 }
498
499 #[cfg(feature = "db-write")]
500 pub fn to_database<P>(&self, database_path: P) -> RepoResult<()>
501 where
502 P: AsRef<Path>,
503 {
504 use std::time::{SystemTime, UNIX_EPOCH};
505
506 let _ = std::fs::remove_file(database_path.as_ref());
508
509 let db = crate::db::RepoDatabaseWrite::create(database_path.as_ref())?;
510 db.write_info(self.repo.name(), self.repo.edition(), "", "")?;
512
513 let tags = self.tags_iter();
515 db.add_tags(tags)?;
516
517 for album in self.albums_iter() {
519 db.add_album(album)?;
520 }
521
522 db.create_index()?;
524
525 fs::write(
527 database_path.as_ref().with_file_name("repo.json"),
528 format!(
529 "{{\"last_modified\": {}}}",
530 SystemTime::now()
531 .duration_since(UNIX_EPOCH)
532 .unwrap()
533 .as_secs()
534 ),
535 )?;
536 Ok(())
537 }
538
539 #[cfg(feature = "search")]
540 pub fn build_search_index<P>(&self, path: P)
541 where
542 P: AsRef<Path>,
543 {
544 use crate::search::RepositorySearchManager;
545
546 let searcher = RepositorySearchManager::create(path).unwrap();
547 let mut index_writer = searcher.index.writer(100_000_000).unwrap();
548
549 for album in self.albums_iter() {
550 for (disc_id_v, disc) in album.iter().enumerate() {
551 let disc_id_v = disc_id_v + 1;
552 for (track_id_v, track) in disc.iter().enumerate() {
553 let track_id_v = track_id_v + 1;
554 index_writer
555 .add_document(searcher.build_document(
556 track.title(),
557 track.artist(),
558 &album.album_id,
559 disc_id_v as i64,
560 track_id_v as i64,
561 ))
562 .unwrap();
563 }
564 }
565 }
566 index_writer.commit().unwrap();
567 }
568}
569
570impl Drop for OwnedRepositoryManager {
571 fn drop(&mut self) {
572 let lock_file = self.lock_file();
573 if lock_file.exists() {
576 let _ = std::fs::remove_file(lock_file);
577 }
578 }
579}