Skip to main content

selene_core/library/
collection.rs

1use std::{convert::Infallible, str::FromStr};
2
3use blake3::Hash;
4use lunar_lib::database::{DatabaseEntry, EntryId, TransactionError};
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    database::LibraryDb,
9    library::{
10        album::Album,
11        collectable::Collectable,
12        collection::rules::{AlbumRule, RuleGroup, TrackRule},
13        image_art::ImageArt,
14        track::Track,
15    },
16};
17
18pub mod frontend_impls;
19pub mod trait_impls;
20
21pub mod rules;
22
23mod hardcoded_dynamic_collections;
24pub use hardcoded_dynamic_collections::*;
25
26#[derive(Debug, thiserror::Error)]
27pub enum CollectionCreationError {
28    #[error("Collection name resolved to an empty string")]
29    EmptyName,
30
31    #[error("Collection name resolved to a reserved collection name")]
32    ReservedName(String),
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub enum CollectionType {
37    Static { collectables: Vec<Collectable> },
38    Dynamic { rules: Vec<DynamicCollectionRules> },
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub enum DynamicCollectionRules {
43    Track(RuleGroup<TrackRule>),
44    Album(RuleGroup<AlbumRule>),
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Collection {
49    id: CollectionId,
50    pub name: String,
51    pub cover_art: Option<ImageArt>,
52
53    pub items: CollectionType,
54    read_only: bool,
55}
56
57impl Collection {
58    /// Creates a new static collection with the input name
59    ///
60    /// # Errors
61    ///
62    /// This function will return `None` if the input name is empty or is trimmed to an empty string
63    fn new_static(name: String) -> Result<Self, CollectionCreationError> {
64        let id = CollectionId::from_str(&name).unwrap();
65
66        Ok(Self {
67            id,
68            name,
69            cover_art: None,
70            items: CollectionType::Static {
71                collectables: Vec::new(),
72            },
73            read_only: false,
74        })
75    }
76
77    pub fn collectables(&self, db: &LibraryDb) -> Result<Vec<Collectable>, TransactionError> {
78        match &self.items {
79            CollectionType::Static { collectables } => Ok(collectables.clone()),
80            CollectionType::Dynamic { rules } => Ok(resolve_rules(rules, db)?),
81        }
82    }
83}
84
85#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
86pub struct CollectionId(Hash);
87
88impl CollectionId {
89    #[must_use]
90    pub fn to_selene_id(&self) -> String {
91        format!("collection:{}", self.0)
92    }
93}
94
95impl EntryId for CollectionId {
96    type Entry = Collection;
97    type IdDb = LibraryDb;
98}
99
100impl std::ops::Deref for CollectionId {
101    type Target = [u8; 32];
102
103    fn deref(&self) -> &Self::Target {
104        self.0.as_bytes()
105    }
106}
107
108impl FromStr for CollectionId {
109    type Err = Infallible;
110
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        Ok(Self(blake3::hash(s.as_bytes())))
113    }
114}
115
116pub fn resolve_rules(
117    rules: &[DynamicCollectionRules],
118    db: &LibraryDb,
119) -> Result<Vec<Collectable>, TransactionError> {
120    let tracks = Track::db_get_all(db)?;
121    let albums = Album::db_get_all(db)?;
122
123    let mut collectables = Vec::new();
124
125    for group in rules {
126        match group {
127            DynamicCollectionRules::Track(rule_group) => {
128                let tracks = rule_group.filter(&tracks);
129                collectables.extend(tracks.map(|t| Collectable::Track(t.id())));
130            }
131            DynamicCollectionRules::Album(rule_group) => {
132                let albums = rule_group.filter(&albums);
133                collectables.extend(albums.map(|t| Collectable::Album(t.id())));
134            }
135        }
136    }
137
138    Ok(collectables)
139}