use mineral_model::SongId;
use mineral_task::{ChannelFetchKind, PlaylistWriteOp, Priority, TaskEvent, TaskKind, WriteError};
use crate::player::PlayerCore;
fn map_write_error(e: &WriteError) -> mineral_stats::PlaylistError {
match e {
WriteError::AuthRequired => mineral_stats::PlaylistError::AuthRequired,
WriteError::RateLimited => mineral_stats::PlaylistError::RateLimited,
WriteError::NotSupported => mineral_stats::PlaylistError::NotSupported,
WriteError::Api { .. } => mineral_stats::PlaylistError::Api,
WriteError::Network
| WriteError::Parse
| WriteError::InvalidData
| WriteError::Storage
| WriteError::NotFound => mineral_stats::PlaylistError::Other,
}
}
impl PlayerCore {
pub(crate) fn consume_events_once(&self) {
let events = self.inner.scheduler.drain_events();
if events.is_empty() {
return;
}
let mut forward = Vec::with_capacity(events.len());
for ev in events {
match ev {
TaskEvent::LyricsReady { song_id, lyrics } => {
self.handle_lyrics_ready(&song_id, lyrics);
}
TaskEvent::PlaylistsFetched { source, playlists } => {
self.library_concluded(source, Some(playlists));
}
TaskEvent::AlbumsFetched { source, albums } => {
let albums = self.library().replace_saved_albums(source, albums);
forward.push(TaskEvent::SavedAlbumsSnapshot { albums });
}
TaskEvent::SearchResults {
source,
kind,
query,
page,
payload,
has_more,
} => {
self.record_search_result(source, kind, &query, page, &payload);
forward.push(TaskEvent::SearchResults {
source,
kind,
query,
page,
payload,
has_more,
});
}
TaskEvent::FetchDone {
kind,
source,
target_ref,
from_user,
outcome,
latency_ms,
} => {
self.record_fetch(kind, source, target_ref, from_user, outcome, latency_ms);
}
TaskEvent::PlaylistWriteDone { op, error } => {
self.record_playlist_op(&op, error.as_ref());
if error.is_none() {
self.refresh_after_write(&op);
}
forward.push(TaskEvent::PlaylistWriteDone { op, error });
}
other => forward.push(other),
}
}
for ev in forward {
self.notify().task_event(ev);
}
}
fn refresh_after_write(&self, op: &PlaylistWriteOp) {
match op {
PlaylistWriteOp::AddSongs { id, .. } | PlaylistWriteOp::RemoveSongs { id, .. } => {
self.inner.scheduler.submit(
TaskKind::ChannelFetch(ChannelFetchKind::PlaylistDetail {
id: id.clone(),
load: mineral_channel_core::PlaylistLoad::Complete,
}),
Priority::User,
);
}
PlaylistWriteOp::Create { source, .. } => {
self.submit_my_playlists(*source);
}
PlaylistWriteOp::Delete { id }
| PlaylistWriteOp::Rename { id, .. }
| PlaylistWriteOp::SetDescription { id, .. } => {
self.submit_my_playlists(id.namespace());
}
}
}
pub(crate) fn handle_lyrics_ready(&self, song_id: &SongId, lyrics: mineral_model::Lyrics) {
let mut st = self.inner.state.lock();
let want = st.current_song.as_ref().map(|t| &t.id);
if want == Some(song_id) {
mineral_log::debug!(target: "player", song_id = song_id.as_str(), action = "store", "lyrics ready");
st.current_lyrics = Some(lyrics);
st.current_lyrics_song_id = Some(song_id.clone());
st.bump_current();
} else {
mineral_log::debug!(target: "player", song_id = song_id.as_str(), action = "drop", "lyrics ready");
}
}
pub(crate) fn record_fetch(
&self,
kind: mineral_task::ChannelFetchKindTag,
source: mineral_model::SourceKind,
target_ref: Option<String>,
from_user: bool,
outcome: mineral_task::TaskOutcome,
latency_ms: u64,
) {
let fetch_kind = match kind {
mineral_task::ChannelFetchKindTag::MyAlbums => mineral_stats::FetchKind::MyAlbums,
mineral_task::ChannelFetchKindTag::MyPlaylists => mineral_stats::FetchKind::MyPlaylists,
mineral_task::ChannelFetchKindTag::PlaylistDetail => {
mineral_stats::FetchKind::PlaylistDetail
}
mineral_task::ChannelFetchKindTag::Lyrics => mineral_stats::FetchKind::Lyrics,
mineral_task::ChannelFetchKindTag::RemotePlayCount => {
mineral_stats::FetchKind::RemotePlayCount
}
mineral_task::ChannelFetchKindTag::Search => mineral_stats::FetchKind::Search,
mineral_task::ChannelFetchKindTag::ArtistDetail => {
mineral_stats::FetchKind::ArtistDetail
}
mineral_task::ChannelFetchKindTag::ArtistAlbums => {
mineral_stats::FetchKind::ArtistAlbums
}
mineral_task::ChannelFetchKindTag::AlbumDetail => mineral_stats::FetchKind::AlbumDetail,
};
let (actor, trigger) = if from_user {
(
mineral_stats::Actor::User,
mineral_stats::FetchTrigger::User,
)
} else {
(
mineral_stats::Actor::System,
mineral_stats::FetchTrigger::System,
)
};
let fetch_outcome = match outcome {
mineral_task::TaskOutcome::Ok => mineral_stats::FetchOutcome::Ok,
mineral_task::TaskOutcome::Failed => mineral_stats::FetchOutcome::Failed,
mineral_task::TaskOutcome::Cancelled => mineral_stats::FetchOutcome::Cancelled,
};
self.inner.stats.event(mineral_stats::StatsEvent::Behavior {
actor,
event: mineral_stats::BehaviorEvent::Fetch {
fetch_kind,
source,
target_ref,
trigger,
outcome: fetch_outcome,
latency_ms: i64::try_from(latency_ms).unwrap_or(i64::MAX),
},
});
}
pub(crate) fn record_search_result(
&self,
source: mineral_model::SourceKind,
kind: mineral_model::SearchKind,
query: &str,
page: mineral_channel_core::Page,
payload: &mineral_task::SearchPayload,
) {
use mineral_model::SearchKind;
use mineral_task::SearchPayload;
let target = match kind {
SearchKind::Song => mineral_stats::SearchTargetKind::Song,
SearchKind::Album => mineral_stats::SearchTargetKind::Album,
SearchKind::Artist => mineral_stats::SearchTargetKind::Artist,
SearchKind::Playlist => mineral_stats::SearchTargetKind::Playlist,
SearchKind::User => return, };
let count = match payload {
SearchPayload::Songs(v) => v.len(),
SearchPayload::Albums(v) => v.len(),
SearchPayload::Playlists(v) => v.len(),
SearchPayload::Artists(v) => v.len(),
};
let page_no = i64::from(page.offset.checked_div(page.limit).unwrap_or(0));
self.inner.stats.record_search(
mineral_stats::Actor::User,
query,
target,
source,
page_no,
Some(i64::try_from(count).unwrap_or(i64::MAX)),
mineral_stats::SearchOutcome::Ok,
);
}
pub(crate) fn record_playlist_op(
&self,
op: &PlaylistWriteOp,
error: Option<&mineral_task::WriteError>,
) {
use mineral_stats::{PlaylistOpKind, PlaylistRef};
let (op_name, playlist_ref) = match op {
PlaylistWriteOp::Create { source, name } => (
PlaylistOpKind::Create,
PlaylistRef::Creating {
source: *source,
name: name.clone(),
},
),
PlaylistWriteOp::Delete { id } => {
(PlaylistOpKind::Delete, PlaylistRef::Existing(id.clone()))
}
PlaylistWriteOp::AddSongs { id, .. } => {
(PlaylistOpKind::Add, PlaylistRef::Existing(id.clone()))
}
PlaylistWriteOp::RemoveSongs { id, .. } => {
(PlaylistOpKind::Remove, PlaylistRef::Existing(id.clone()))
}
PlaylistWriteOp::Rename { id, .. } => {
(PlaylistOpKind::Rename, PlaylistRef::Existing(id.clone()))
}
PlaylistWriteOp::SetDescription { id, .. } => (
PlaylistOpKind::SetDescription,
PlaylistRef::Existing(id.clone()),
),
};
let songs = op.songs();
let song = if songs.len() == 1 {
songs.first().cloned()
} else {
None
};
let (outcome, error_kind) = match error {
None => (mineral_stats::OpOutcome::Ok, None),
Some(e) => (mineral_stats::OpOutcome::Failed, Some(map_write_error(e))),
};
self.inner.stats.event(mineral_stats::StatsEvent::Behavior {
actor: mineral_stats::Actor::User,
event: mineral_stats::BehaviorEvent::PlaylistOp {
op: op_name,
playlist_ref,
song,
song_count: i64::try_from(songs.len()).unwrap_or(i64::MAX),
outcome,
error_kind,
},
});
}
pub(crate) fn record_stream_resolution(
&self,
song: &SongId,
outcome: mineral_stats::StreamOutcome,
for_prefetch: bool,
) {
self.inner.stats.event(mineral_stats::StatsEvent::System(
mineral_stats::SystemEvent::StreamResolution {
song: song.clone(),
quality_requested: self.playback_quality().as_str().to_owned(),
outcome,
for_prefetch,
},
));
}
}