use crate::player::PlayerCore;
use mineral_model::{PlaylistId, SourceKind};
use mineral_protocol::{FailureKind, PlaylistOp};
use mineral_stats::{PlaylistOpKind, PlaylistRef};
#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
#[error("playlist name must contain visible text and no control characters")]
InvalidName,
#[error("queue is empty")]
EmptyQueue,
#[error("playlist {0} cannot be managed")]
Protected(PlaylistId),
#[error(transparent)]
Store(#[from] crate::persistence::Error),
}
impl Error {
pub(crate) fn kind(&self) -> FailureKind {
match self {
Self::InvalidName | Self::EmptyQueue => FailureKind::Invalid,
Self::Protected(_) | Self::Store(crate::persistence::Error::Disabled) => {
FailureKind::Unavailable
}
Self::Store(crate::persistence::Error::PlaylistNotFound { .. }) => {
FailureKind::NotFound
}
Self::Store(_) => FailureKind::Internal,
}
}
}
impl PlayerCore {
pub(crate) async fn playlist_operation(
&self,
op: PlaylistOp,
) -> Result<Option<PlaylistId>, Error> {
let (kind, reference) = match &op {
PlaylistOp::SaveQueue { name } => (
PlaylistOpKind::Create,
PlaylistRef::Creating {
source: SourceKind::MINERAL,
name: name.clone(),
},
),
PlaylistOp::Rename { id, .. } => {
(PlaylistOpKind::Rename, PlaylistRef::Existing(id.clone()))
}
PlaylistOp::Delete { id } => {
(PlaylistOpKind::Delete, PlaylistRef::Existing(id.clone()))
}
};
let songs = matches!(&op, PlaylistOp::SaveQueue { .. })
.then(|| self.inner.state.lock().queue.clone());
let result = match op {
PlaylistOp::SaveQueue { name } => {
let name = checked_name(&name)?;
let songs = songs.as_deref().ok_or(Error::EmptyQueue)?;
if songs.is_empty() {
return Err(Error::EmptyQueue);
}
self.persist()
.create_user_playlist(name, songs)
.await
.map(Some)
.map_err(Into::into)
}
PlaylistOp::Rename { id, name } => {
check_managed(&id)?;
self.persist()
.rename_user_playlist(&id, checked_name(&name)?)
.await?;
Ok(None)
}
PlaylistOp::Delete { id } => {
check_managed(&id)?;
self.persist().delete_user_playlist(&id).await?;
Ok(None)
}
};
let count = songs.as_ref().map_or(0, Vec::len);
let reference = match &result {
Ok(Some(id)) => PlaylistRef::Existing(id.clone()),
_ => reference,
};
mineral_log::info!(target: "playlists", operation = ?kind, playlist = ?reference, songs = count, success = result.is_ok(), "playlist operation finished");
self.inner.stats.event(mineral_stats::StatsEvent::Behavior {
actor: mineral_stats::Actor::User,
event: mineral_stats::BehaviorEvent::PlaylistOp {
op: kind,
playlist_ref: reference,
song: None,
song_count: i64::try_from(count).unwrap_or(i64::MAX),
outcome: if result.is_ok() {
mineral_stats::OpOutcome::Ok
} else {
mineral_stats::OpOutcome::Failed
},
error_kind: result
.as_ref()
.err()
.map(|_| mineral_stats::PlaylistError::Other),
},
});
if result.is_ok() {
self.submit_my_playlists(SourceKind::MINERAL);
}
result
}
}
fn checked_name(name: &str) -> Result<&str, Error> {
let name = name.trim();
if name.is_empty() || name.chars().any(char::is_control) {
return Err(Error::InvalidName);
}
Ok(name)
}
fn check_managed(id: &PlaylistId) -> Result<(), Error> {
if id.namespace() != SourceKind::MINERAL || id.value() == "favorites" {
return Err(Error::Protected(id.clone()));
}
Ok(())
}