Skip to main content

wavyzon_plugin/
source.rs

1//! Source traits. A source resolves queries and links to [`Track`]s; a
2//! [`Playable`] source can also open an audio stream for them.
3
4use std::fmt;
5
6use async_trait::async_trait;
7use tokio::io::AsyncRead;
8
9use crate::{Failure, LoadResult, Track};
10
11/// How the bytes of a [`Stream`] are encoded.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Format {
14    /// Raw interleaved signed 16-bit little-endian stereo PCM at 48 kHz.
15    Pcm48,
16    /// Any container/codec the decoder can probe (MP3, AAC/M4A, FLAC, Ogg
17    /// Vorbis, WAV…). `hint` is a file extension or MIME type that speeds up
18    /// probing (`"mp3"`, `"audio/ogg"`); it may be `None`.
19    Encoded { hint: Option<String> },
20}
21
22impl Format {
23    pub fn mp3() -> Self {
24        Format::Encoded {
25            hint: Some("mp3".into()),
26        }
27    }
28}
29
30/// An open audio stream.
31pub struct Stream {
32    pub body: Box<dyn AsyncRead + Send + Unpin>,
33    pub format: Format,
34    /// The 48 kHz sample (per channel) where `body` starts. A source that
35    /// honoured a seek in [`Playable::open`] sets this to the real start; a
36    /// source that ignored it leaves `0` and the player skips forward itself.
37    pub start_sample: i64,
38    /// Whether the stream has no end (internet radio, live).
39    pub live: bool,
40}
41
42impl fmt::Debug for Stream {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.debug_struct("Stream")
45            .field("format", &self.format)
46            .field("start_sample", &self.start_sample)
47            .field("live", &self.live)
48            .finish_non_exhaustive()
49    }
50}
51
52/// Resolves searches, links and identifiers to tracks.
53#[async_trait]
54pub trait Source: Send + Sync + 'static {
55    /// Unique name, stored in [`Track::source`] (`"soundcloud"`).
56    fn name(&self) -> &str;
57
58    /// Search prefix without the colon (`"scsearch"`), if the source searches.
59    fn search_prefix(&self) -> Option<&str> {
60        None
61    }
62
63    /// Plain-text search.
64    async fn search(&self, _query: &str) -> Result<Vec<Track>, Failure> {
65        Ok(vec![])
66    }
67
68    /// Whether this source handles `url`.
69    fn matches_url(&self, _url: &str) -> bool {
70        false
71    }
72
73    /// Resolve a link (track, playlist, album…).
74    async fn load_url(&self, url: &str) -> Result<LoadResult, Failure> {
75        Err(Failure::common(
76            self.name(),
77            "links are not supported",
78            Some(url.to_string()),
79        ))
80    }
81
82    /// Fetch one track by identifier.
83    async fn track(&self, identifier: &str) -> Result<Track, Failure>;
84
85    /// The playback capability, if this source can stream audio.
86    fn playable(&self) -> Option<&dyn Playable> {
87        None
88    }
89
90    /// ISRC lookup capability, used to mirror metadata-only sources.
91    fn isrc_lookup(&self) -> Option<&dyn IsrcLookup> {
92        None
93    }
94
95    /// Recommendation capability (radio/autoplay).
96    fn recommender(&self) -> Option<&dyn Recommender> {
97        None
98    }
99}
100
101/// A source that can stream audio.
102#[async_trait]
103pub trait Playable: Send + Sync {
104    /// Open `track`, ideally starting at `start_ms` (see [`Stream::start_sample`]).
105    async fn open(&self, track: &Track, start_ms: i64) -> Result<Stream, Failure>;
106
107    /// Whether `open` honours `start_ms` without downloading from the start.
108    fn seekable(&self) -> bool {
109        false
110    }
111
112    /// Hint that `track` will probably play soon (resolve URLs, warm caches).
113    async fn warm(&self, _track: &Track) {}
114}
115
116#[async_trait]
117pub trait IsrcLookup: Send + Sync {
118    async fn by_isrc(&self, isrc: &str) -> Result<Track, Failure>;
119}
120
121#[async_trait]
122pub trait Recommender: Send + Sync {
123    async fn recommend(&self, identifier: &str, limit: usize) -> Result<Vec<Track>, Failure>;
124}