use std::io::Cursor;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use mineral_channel_core::{ChannelCaps, Error, MusicChannel, Page, PageResult};
use mineral_model::{
Album, AlbumId, Artist, Lyrics, PlaybackMediaInfo, Playlist, PlaylistId, Song, SongId,
SourceKind,
};
use mineral_playback::{
DirectMedia, MediaReader, OpenOptions, OpenedMedia, PlaybackProvider, PlaybackRegistry,
PlaybackRequest, PreparedPlayback, SeekSupport,
};
use parking_lot::Mutex;
#[derive(Default)]
pub(super) struct RecordingChannel {
pub(super) calls: Arc<Mutex<Vec<(SongId, bool, u64)>>>,
pub(super) liked_ids: Option<rustc_hash::FxHashSet<SongId>>,
pub(super) playlists: Option<Vec<Playlist>>,
}
struct TestPlaybackProvider {
source: SourceKind,
delay: Duration,
fail: bool,
direct: bool,
}
struct TestPreparedPlayback {
direct: Option<DirectMedia>,
info: PlaybackMediaInfo,
}
#[async_trait]
impl PlaybackProvider for TestPlaybackProvider {
fn source(&self) -> SourceKind {
self.source
}
async fn resolve(
&self,
request: PlaybackRequest,
cancellation: tokio_util::sync::CancellationToken,
) -> Result<Box<dyn PreparedPlayback>, mineral_playback::Error> {
if !self.delay.is_zero() {
tokio::select! {
biased;
() = cancellation.cancelled() => {
return Err(mineral_playback::Error::Cancelled);
}
() = tokio::time::sleep(self.delay) => {}
}
}
if cancellation.is_cancelled() {
return Err(mineral_playback::Error::Cancelled);
}
if self.fail {
return Err(mineral_playback::Error::Provider {
source: Box::new(std::io::Error::other("test playback unavailable")),
});
}
let info = PlaybackMediaInfo {
song_id: request.song_id().clone(),
bitrate_bps: Some(320_000),
size: Some(4),
format: Some(mineral_model::AudioFormat::Mp3),
bit_depth: None,
substituted: false,
};
let url = url::Url::parse(&format!(
"https://example.com/{}.mp3",
request.song_id().value()
))
.map_err(|source| mineral_playback::Error::Provider {
source: Box::new(source),
})?;
let direct = self.direct.then(|| {
DirectMedia::remote(
info.clone(),
url,
Vec::new(),
mineral_model::StreamLayout::Contiguous,
)
});
Ok(Box::new(TestPreparedPlayback { direct, info }))
}
}
#[async_trait]
impl PreparedPlayback for TestPreparedPlayback {
fn direct_media(&self) -> Option<&DirectMedia> {
self.direct.as_ref()
}
async fn open(
self: Box<Self>,
options: OpenOptions,
) -> Result<OpenedMedia, mineral_playback::Error> {
if options.cancellation().is_cancelled() {
return Err(mineral_playback::Error::Cancelled);
}
let bytes = b"test".to_vec();
let byte_len = Some(u64::try_from(bytes.len()).map_err(|source| {
mineral_playback::Error::Provider {
source: Box::new(source),
}
})?);
let reader: Box<dyn MediaReader> = Box::new(Cursor::new(bytes));
Ok(OpenedMedia::new(
reader,
SeekSupport::RandomAccess,
byte_len,
self.info,
None,
options.cancellation().clone(),
))
}
}
pub(super) fn test_playback_registry(
channels: &[Arc<dyn MusicChannel>],
delay: Duration,
fail: bool,
direct: bool,
) -> color_eyre::Result<PlaybackRegistry> {
let mut sources = rustc_hash::FxHashSet::default();
let providers = channels
.iter()
.filter_map(|channel| sources.insert(channel.source()).then_some(channel.source()))
.map(|source| {
let provider: Arc<dyn PlaybackProvider> = Arc::new(TestPlaybackProvider {
source,
delay,
fail,
direct,
});
provider
})
.collect::<Vec<Arc<dyn PlaybackProvider>>>();
Ok(PlaybackRegistry::new(providers)?)
}
#[async_trait]
impl MusicChannel for RecordingChannel {
fn source(&self) -> SourceKind {
SourceKind::NETEASE
}
fn caps(&self) -> ChannelCaps {
ChannelCaps::builder()
.searchable(Vec::new())
.playlist_edit(false)
.artist_sections(mineral_channel_core::ArtistSections::new(vec![
mineral_channel_core::ArtistSectionKind::TopSongs,
mineral_channel_core::ArtistSectionKind::Albums,
]))
.build()
}
async fn search_songs(
&self,
_query: &str,
_page: Page,
) -> mineral_channel_core::Result<PageResult<Song>> {
Err(Error::NotSupported)
}
async fn search_albums(
&self,
_query: &str,
_page: Page,
) -> mineral_channel_core::Result<PageResult<Album>> {
Err(Error::NotSupported)
}
async fn search_playlists(
&self,
_query: &str,
_page: Page,
) -> mineral_channel_core::Result<PageResult<Playlist>> {
Err(Error::NotSupported)
}
async fn songs_detail(&self, _ids: &[SongId]) -> mineral_channel_core::Result<Vec<Song>> {
Err(Error::NotSupported)
}
async fn album_detail(&self, _id: &AlbumId) -> mineral_channel_core::Result<Album> {
Err(Error::NotSupported)
}
async fn playlist_detail(
&self,
_id: &PlaylistId,
_load: mineral_channel_core::PlaylistLoad,
) -> mineral_channel_core::Result<mineral_channel_core::PlaylistDetail> {
Err(Error::NotSupported)
}
async fn lyrics(&self, _id: &SongId) -> mineral_channel_core::Result<Lyrics> {
Err(Error::NotSupported)
}
async fn artist_detail(
&self,
_id: &mineral_model::ArtistId,
) -> mineral_channel_core::Result<Artist> {
Err(Error::NotSupported)
}
async fn on_played(
&self,
id: &SongId,
completed: bool,
listen_ms: u64,
) -> mineral_channel_core::Result<()> {
self.calls.lock().push((id.clone(), completed, listen_ms));
Ok(())
}
async fn liked_song_ids(&self) -> mineral_channel_core::Result<rustc_hash::FxHashSet<SongId>> {
self.liked_ids.clone().ok_or(Error::NotSupported)
}
async fn my_playlists(&self) -> mineral_channel_core::Result<Vec<Playlist>> {
self.playlists.clone().ok_or(Error::NotSupported)
}
}