Skip to main content

mecomp_tui/state/
library.rs

1//! The library state store.
2//!
3//! Updates every minute, or when the user requests a rescan, ands/removes/updates a playlist, or reclusters collections.
4
5use mecomp_prost::{
6    DynamicPlaylistCreateRequest, DynamicPlaylistUpdateRequest, LibraryAnalyzeRequest,
7    LibraryBriefResponse as LibraryBrief, MusicPlayerClient, PlaylistAddListRequest, PlaylistName,
8    PlaylistRemoveSongsRequest, PlaylistRenameRequest,
9};
10use tokio::sync::{
11    broadcast,
12    mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
13};
14
15use crate::termination::Interrupted;
16
17use super::action::LibraryAction;
18
19/// The library state store.
20#[derive(Debug, Clone)]
21#[allow(clippy::module_name_repetitions)]
22pub struct LibraryState {
23    state_tx: UnboundedSender<LibraryBrief>,
24}
25
26impl LibraryState {
27    /// create a new library state store, and return the receiver for listening to state updates.
28    #[must_use]
29    pub fn new() -> (Self, UnboundedReceiver<LibraryBrief>) {
30        let (state_tx, state_rx) = unbounded_channel::<LibraryBrief>();
31
32        (Self { state_tx }, state_rx)
33    }
34
35    /// a loop that updates the library state every tick.
36    ///
37    /// # Errors
38    ///
39    /// Fails if the state cannot be sent
40    /// or if the daemon client can't connect to the server
41    /// or if the daemon returns an error
42    pub async fn main_loop(
43        &self,
44        mut daemon: MusicPlayerClient,
45        mut action_rx: UnboundedReceiver<LibraryAction>,
46        mut interrupt_rx: broadcast::Receiver<Interrupted>,
47    ) -> anyhow::Result<Interrupted> {
48        // the initial state once
49        let state = get_library(&mut daemon).await?;
50        self.state_tx.send(state)?;
51
52        loop {
53            tokio::select! {
54                // Handle the actions coming from the UI
55                // and process them to do async operations
56                Some(action) = action_rx.recv() => {
57                    handle_action(&self.state_tx, &mut daemon, action).await?;
58                },
59                // Catch and handle interrupt signal to gracefully shutdown
60                Ok(interrupted) = interrupt_rx.recv() => {
61                    break Ok(interrupted);
62                }
63            }
64        }
65    }
66}
67async fn handle_action(
68    state_tx: &UnboundedSender<LibraryBrief>,
69    daemon: &mut MusicPlayerClient,
70    action: LibraryAction,
71) -> anyhow::Result<()> {
72    let mut update = false;
73    let mut flag_update = || update = true;
74
75    match action {
76        LibraryAction::Rescan => rescan_library(daemon).await?,
77        LibraryAction::Update => flag_update(),
78        LibraryAction::Analyze => analyze_library(daemon).await?,
79        LibraryAction::Recluster => recluster_library(daemon).await?,
80        LibraryAction::CreatePlaylist(name) => daemon
81            .playlist_get_or_create(PlaylistName::new(name))
82            .await?
83            .map(|_| flag_update())
84            .into_inner(),
85        LibraryAction::RemovePlaylist(id) => daemon
86            .playlist_remove(id)
87            .await?
88            .map(|()| flag_update())
89            .into_inner(),
90        LibraryAction::RenamePlaylist(id, name) => daemon
91            .playlist_rename(PlaylistRenameRequest::new(id, name))
92            .await?
93            .map(|_| flag_update())
94            .into_inner(),
95        LibraryAction::RemoveSongsFromPlaylist(playlist, songs) => daemon
96            .playlist_remove_songs(PlaylistRemoveSongsRequest::new(playlist, songs))
97            .await?
98            .map(|()| flag_update())
99            .into_inner(),
100        LibraryAction::AddThingsToPlaylist(playlist, things) => daemon
101            .playlist_add_list(PlaylistAddListRequest::new(playlist, things))
102            .await?
103            .map(|()| flag_update())
104            .into_inner(),
105        LibraryAction::CreatePlaylistAndAddThings(name, things) => {
106            let playlist = daemon
107                .playlist_get_or_create(PlaylistName::new(name))
108                .await?
109                .into_inner();
110            daemon
111                .playlist_add_list(PlaylistAddListRequest::new(playlist, things))
112                .await?
113                .map(|()| flag_update())
114                .into_inner();
115        }
116        LibraryAction::CreateDynamicPlaylist(name, query) => daemon
117            .dynamic_playlist_create(DynamicPlaylistCreateRequest::new(name, query))
118            .await?
119            .map(|_| flag_update())
120            .into_inner(),
121        LibraryAction::RemoveDynamicPlaylist(id) => daemon
122            .dynamic_playlist_remove(id)
123            .await?
124            .map(|()| flag_update())
125            .into_inner(),
126        LibraryAction::UpdateDynamicPlaylist(id, changes) => daemon
127            .dynamic_playlist_update(DynamicPlaylistUpdateRequest::new(id, changes))
128            .await?
129            .map(|_| flag_update())
130            .into_inner(),
131    }
132
133    if update {
134        let state = get_library(daemon).await?;
135        state_tx.send(state)?;
136    }
137
138    Ok(())
139}
140
141async fn get_library(daemon: &mut MusicPlayerClient) -> anyhow::Result<LibraryBrief> {
142    Ok(daemon.library_brief(()).await?.into_inner())
143}
144
145/// initiate a rescan and wait until it's done
146async fn rescan_library(daemon: &mut MusicPlayerClient) -> anyhow::Result<()> {
147    // don't error out is a rescan is in progress
148    match daemon.library_rescan(()).await {
149        Ok(_) => Ok(()),
150        Err(e) if e.code() == tonic::Code::Aborted => Ok(()),
151        Err(e) => Err(e.into()),
152    }
153}
154
155/// initiate an analysis and wait until it's done
156async fn analyze_library(daemon: &mut MusicPlayerClient) -> anyhow::Result<()> {
157    // don't error out if an analysis is in progress
158    match daemon
159        .library_analyze(LibraryAnalyzeRequest::new(false))
160        .await
161    {
162        Ok(_) => Ok(()),
163        Err(e) if e.code() == tonic::Code::Aborted => Ok(()),
164        Err(e) => Err(e.into()),
165    }
166}
167
168/// initiate a recluster and wait until it's done
169async fn recluster_library(daemon: &mut MusicPlayerClient) -> anyhow::Result<()> {
170    match daemon.library_recluster(()).await {
171        Ok(_) => Ok(()),
172        Err(e) if e.code() == tonic::Code::Aborted => Ok(()),
173        Err(e) => Err(e.into()),
174    }
175}