selene_core/library/
collection.rs1use 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 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}