//! Has all of the structs for managing the state //! of tracks, as well as downloading them & finding new ones. //! //! There are several structs which represent the different stages //! that go on in downloading and playing tracks. When first queued, //! the downloader will return a [`Queued`] track. //! //! Then, when it's time to play the track, it is decoded into //! a [`Decoded`] track, which includes all the information //! in the form of [`Info`]. use std::{fmt::Debug, io::Cursor, time::Duration}; use bytes::Bytes; use rodio::{Decoder, Source as _}; use unicode_segmentation::UnicodeSegmentation as _; pub mod list; pub use list::List; pub mod error; pub mod format; pub use error::{Error, Result}; use crate::tracks::error::WithTrackContext as _; /// Just a shorthand for a decoded [Bytes]. pub type DecodedData = Decoder>; /// Tracks which are still waiting in the queue, and can't be played yet. /// /// This means that only the data & track name are included. #[derive(PartialEq, Eq)] pub struct Queued { /// Display name of the track. pub display: String, /// Full downloadable path/url of the track. pub path: String, /// The raw data of the track, which is not decoded and /// therefore much more memory efficient. pub data: Bytes, } impl Debug for Queued { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Queued") .field("display", &self.display) .field("path", &self.path) .field("data", &self.data.len()) .finish() } } impl Queued { /// This will actually decode and format the track, /// returning a [`DecodedTrack`] which can be played /// and also has a duration & formatted name. pub fn decode(self) -> Result { Decoded::new(self) } /// Creates a new queued track. pub fn new(path: String, data: Bytes, display: Option) -> Result { let display = match display { None => self::format::name(&path)?, Some(custom) => custom, }; Ok(Self { display, path, data, }) } } /// The [`Info`] struct, which has the name and duration of a track. /// /// This is not included in [Track] as the duration has to be acquired /// from the decoded data and not from the raw data. #[derive(Debug, Eq, PartialEq, Clone)] pub struct Info { /// The full downloadable path/url of the track. pub path: String, /// This is a formatted name, so it doesn't include the full path. pub display: String, /// This is the *actual* terminal width of the track name, used to make /// the UI consistent. pub width: usize, /// The duration of the track, this is an [Option] because there are /// cases where the duration of a track is unknown. pub duration: Option, } impl Info { /// Converts the info back into a full track list entry. pub fn to_entry(&self) -> String { let mut entry = self.path.clone(); entry.push('!'); entry.push_str(&self.display); entry } /// Creates a new [`Info`] from decoded data & the queued track. pub fn new(decoded: &DecodedData, path: String, display: String) -> Result { Ok(Self { duration: decoded.total_duration(), width: display.graphemes(true).count(), path, display, }) } } /// This struct is separate from [Track] since it is generated lazily from /// a track, and not when the track is first downloaded. pub struct Decoded { /// Has both the formatted name and some information from the decoded data. pub info: Info, /// The decoded data, which is able to be played by [rodio]. pub data: DecodedData, } impl Decoded { /// Creates a new track. /// This is equivalent to [`QueuedTrack::decode`]. pub fn new(track: Queued) -> Result { let (path, display) = (track.path.clone(), track.display.clone()); let data = Decoder::builder() .with_byte_len(track.data.len().try_into()?) .with_data(Cursor::new(track.data)) .build() .track(track.display)?; let info = Info::new(&data, path, display)?; Ok(Self { info, data }) } }