Skip to main content

selene_core/library/
collection.rs

1use std::{convert::Infallible, str::FromStr};
2
3use blake3::Hash;
4use lunar_lib::database::{DatabaseEntry, DatabaseError, EntryId};
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    database::LibraryDb,
9    library::{
10        album::{Album, AlbumId},
11        artist::ArtistId,
12        collection::rules::{AlbumRule, RuleGroup, TrackRule},
13        image_art::ImageArt,
14        track::{Track, TrackId},
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>, DatabaseError> {
78        match self.items {
79            CollectionType::Static { collectables } => Ok(collectables),
80            CollectionType::Dynamic { rules } => Ok(resolve_rules(&rules, db)?),
81        }
82    }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86pub enum Collectable {
87    Track(TrackId),
88    Artist(ArtistId),
89    Album(AlbumId),
90    Collection(CollectionId),
91}
92
93impl Collectable {
94    #[must_use] 
95    pub fn to_selene_id(&self) -> String {
96        match self {
97            Collectable::Track(track_id) => track_id.to_selene_id(),
98            Collectable::Artist(artist_id) => artist_id.to_selene_id(),
99            Collectable::Album(album_id) => album_id.to_selene_id(),
100            Collectable::Collection(collection_id) => collection_id.to_selene_id(),
101        }
102    }
103}
104
105#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
106pub struct CollectionId {
107    id: Hash,
108}
109
110impl CollectionId {
111    #[must_use] 
112    pub fn to_selene_id(&self) -> String {
113        format!("collection:{}", self.id)
114    }
115}
116
117impl EntryId for CollectionId {
118    type Entry = Collection;
119    type IdDb = LibraryDb;
120}
121
122impl std::ops::Deref for CollectionId {
123    type Target = [u8; 32];
124
125    fn deref(&self) -> &Self::Target {
126        self.id.as_bytes()
127    }
128}
129
130impl FromStr for CollectionId {
131    type Err = Infallible;
132
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        Ok(Self {
135            id: blake3::hash(s.as_bytes()),
136        })
137    }
138}
139
140pub fn resolve_rules(
141    rules: &[DynamicCollectionRules],
142    db: &LibraryDb,
143) -> Result<Vec<Collectable>, DatabaseError> {
144    let tracks = Track::db_get_all_from(db)?;
145    let albums = Album::db_get_all_from(db)?;
146
147    let mut collectables = Vec::new();
148
149    for group in rules {
150        match group {
151            DynamicCollectionRules::Track(rule_group) => {
152                let tracks = rule_group.filter(&tracks);
153                collectables.extend(tracks.map(|t| Collectable::Track(t.id())));
154            }
155            DynamicCollectionRules::Album(rule_group) => {
156                let albums = rule_group.filter(&albums);
157                collectables.extend(albums.map(|t| Collectable::Album(t.id())));
158            }
159        }
160    }
161
162    Ok(collectables)
163}