mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-06-22 22:52:59 -05:00
Added the rs-ts
crate and made adjustments for proper TS binding generation
This commit is contained in:
parent
b8280775fa
commit
367d2ceda3
31 changed files with 273 additions and 239 deletions
|
@ -44,3 +44,7 @@ rustfm-scrobble = "1.1.1"
|
||||||
reqwest = { version = "0.12.12", features = ["json"] }
|
reqwest = { version = "0.12.12", features = ["json"] }
|
||||||
tokio = { version = "1.43.0", features = ["macros"] }
|
tokio = { version = "1.43.0", features = ["macros"] }
|
||||||
opener = "0.7.2"
|
opener = "0.7.2"
|
||||||
|
ts-rs = { version = "11.0.1", optional = true, features = ["uuid-impl", "chrono-impl", "serde_json"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
ts = ["dep:ts-rs"]
|
||||||
|
|
|
@ -7,8 +7,10 @@ use std::{
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::to_string_pretty;
|
use serde_json::to_string_pretty;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ts_rs::TS;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ConfigLibrary {
|
pub struct ConfigLibrary {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -51,6 +53,7 @@ impl ConfigLibrary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct ConfigLibraries {
|
pub struct ConfigLibraries {
|
||||||
pub default_library: Uuid,
|
pub default_library: Uuid,
|
||||||
|
@ -92,6 +95,7 @@ impl ConfigLibraries {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct ConfigConnections {
|
pub struct ConfigConnections {
|
||||||
pub discord_rpc_client_id: Option<u64>,
|
pub discord_rpc_client_id: Option<u64>,
|
||||||
|
@ -99,6 +103,7 @@ pub struct ConfigConnections {
|
||||||
pub last_fm_session: Option<String>,
|
pub last_fm_session: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
|
|
@ -28,6 +28,8 @@ use super::connections::{ConnectionsNotification, ControllerConnections};
|
||||||
use super::controller_handle::{LibraryCommandInput, PlayerCommandInput, QueueCommandInput};
|
use super::controller_handle::{LibraryCommandInput, PlayerCommandInput, QueueCommandInput};
|
||||||
use super::queue::{QueueAlbum, QueueSong};
|
use super::queue::{QueueAlbum, QueueSong};
|
||||||
|
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
pub struct Controller();
|
pub struct Controller();
|
||||||
|
|
||||||
type QueueItem_ = QueueItem<QueueSong, QueueAlbum>;
|
type QueueItem_ = QueueItem<QueueSong, QueueAlbum>;
|
||||||
|
@ -43,6 +45,7 @@ pub enum ControllerError {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move this to a different location to be used elsewhere
|
// TODO: move this to a different location to be used elsewhere
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum PlayerLocation {
|
pub enum PlayerLocation {
|
||||||
|
@ -370,8 +373,11 @@ impl Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Default, Serialize, Clone)]
|
#[derive(Debug, Default, Serialize, Clone)]
|
||||||
pub struct PlaybackInfo {
|
pub struct PlaybackInfo {
|
||||||
|
#[ts(as = "(f64, f64)")]
|
||||||
pub position: Option<TimeDelta>,
|
pub position: Option<TimeDelta>,
|
||||||
|
#[ts(as = "(f64, f64)")]
|
||||||
pub duration: Option<TimeDelta>,
|
pub duration: Option<TimeDelta>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use async_channel::{Receiver, Sender};
|
use async_channel::{Receiver, Sender};
|
||||||
use discord_presence::models::Command;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::music_storage::{
|
use crate::music_storage::{
|
||||||
|
|
|
@ -6,7 +6,7 @@ use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterato
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
music_storage::{
|
music_storage::{
|
||||||
library::{self, MusicLibrary},
|
library::MusicLibrary,
|
||||||
playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem},
|
playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use crate::music_storage::{
|
use crate::music_storage::queue::{Queue, QueueError, QueueItemType};
|
||||||
library::Song,
|
|
||||||
queue::{Queue, QueueError, QueueItemType},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
controller::{Controller, QueueCommand, QueueResponse},
|
controller::{Controller, QueueCommand, QueueResponse},
|
||||||
|
|
|
@ -213,8 +213,8 @@ fn to_tag(string: String) -> Tag {
|
||||||
"album artist" => Tag::AlbumArtist,
|
"album artist" => Tag::AlbumArtist,
|
||||||
"genre" => Tag::Genre,
|
"genre" => Tag::Genre,
|
||||||
"comment" => Tag::Comment,
|
"comment" => Tag::Comment,
|
||||||
"track number" => Tag::Track,
|
"track number" => Tag::TrackNumber,
|
||||||
"disc number" => Tag::Disk,
|
"disc number" => Tag::DiskNumber,
|
||||||
_ => Tag::Key(string),
|
_ => Tag::Key(string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use super::playlist::{Playlist, PlaylistFolder};
|
use super::playlist::{Playlist, PlaylistFolder};
|
||||||
// Crate things
|
// Crate things
|
||||||
use super::utils::{find_images, normalize, read_file, write_file};
|
use super::utils::{find_images, normalize, read_file, write_file};
|
||||||
|
|
||||||
use crate::music_storage::playlist::PlaylistFolderItem;
|
use crate::music_storage::playlist::PlaylistFolderItem;
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
@ -19,6 +20,7 @@ use lofty::file::{AudioFile as _, TaggedFileExt as _};
|
||||||
use lofty::probe::Probe;
|
use lofty::probe::Probe;
|
||||||
use lofty::tag::{ItemKey, ItemValue, TagType};
|
use lofty::tag::{ItemKey, ItemValue, TagType};
|
||||||
use rcue::parser::parse_from_file;
|
use rcue::parser::parse_from_file;
|
||||||
|
use serde::ser::SerializeMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -35,6 +37,10 @@ use serde::{Deserialize, Serialize};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
// TS
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
pub enum AlbumArt {
|
pub enum AlbumArt {
|
||||||
Embedded(usize),
|
Embedded(usize),
|
||||||
|
@ -52,6 +58,7 @@ impl AlbumArt {
|
||||||
|
|
||||||
/// A tag for a song
|
/// A tag for a song
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum Tag {
|
pub enum Tag {
|
||||||
Title,
|
Title,
|
||||||
|
@ -60,23 +67,26 @@ pub enum Tag {
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
Genre,
|
Genre,
|
||||||
Comment,
|
Comment,
|
||||||
Track,
|
TrackNumber,
|
||||||
Disk,
|
DiskNumber,
|
||||||
Key(String),
|
#[cfg_attr(feature = "ts", ts(type = "string"))]
|
||||||
Field(String),
|
Field(String),
|
||||||
|
#[serde(untagged)]
|
||||||
|
#[cfg_attr(feature = "ts", ts(type = "string"))]
|
||||||
|
Key(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Tag {
|
impl Display for Tag {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let path_str: String = match self {
|
let path_str: String = match self {
|
||||||
Self::Title => "TrackTitle".into(),
|
Self::Title => "Title".into(),
|
||||||
Self::Album => "AlbumTitle".into(),
|
Self::Album => "Album".into(),
|
||||||
Self::Artist => "TrackArtist".into(),
|
Self::Artist => "Artist".into(),
|
||||||
Self::AlbumArtist => "AlbumArtist".into(),
|
Self::AlbumArtist => "AlbumArtist".into(),
|
||||||
Self::Genre => "Genre".into(),
|
Self::Genre => "Genre".into(),
|
||||||
Self::Comment => "Comment".into(),
|
Self::Comment => "Comment".into(),
|
||||||
Self::Track => "TrackNumber".into(),
|
Self::TrackNumber => "TrackNumber".into(),
|
||||||
Self::Disk => "DiscNumber".into(),
|
Self::DiskNumber => "DiscNumber".into(),
|
||||||
Self::Key(key) => key.into(),
|
Self::Key(key) => key.into(),
|
||||||
Self::Field(f) => f.into(),
|
Self::Field(f) => f.into(),
|
||||||
};
|
};
|
||||||
|
@ -121,6 +131,7 @@ impl Display for Field {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum InternalTag {
|
pub enum InternalTag {
|
||||||
|
@ -131,6 +142,7 @@ pub enum InternalTag {
|
||||||
VolumeAdjustment(i8),
|
VolumeAdjustment(i8),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum BannedType {
|
pub enum BannedType {
|
||||||
|
@ -138,6 +150,7 @@ pub enum BannedType {
|
||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum DoNotTrack {
|
pub enum DoNotTrack {
|
||||||
|
@ -148,6 +161,7 @@ pub enum DoNotTrack {
|
||||||
Discord,
|
Discord,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum SongType {
|
pub enum SongType {
|
||||||
|
@ -160,6 +174,7 @@ pub enum SongType {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stores information about a single song
|
/// Stores information about a single song
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
pub struct Song {
|
pub struct Song {
|
||||||
pub location: Vec<URI>,
|
pub location: Vec<URI>,
|
||||||
|
@ -171,19 +186,75 @@ pub struct Song {
|
||||||
pub rating: Option<u8>,
|
pub rating: Option<u8>,
|
||||||
/// MIME type
|
/// MIME type
|
||||||
pub format: Option<String>,
|
pub format: Option<String>,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "ts",
|
||||||
|
ts(type = "number"),
|
||||||
|
serde(serialize_with = "dur_ms", deserialize_with = "ms_dur")
|
||||||
|
)]
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
|
#[cfg_attr(feature = "ts", ts(type = "number"))]
|
||||||
pub play_time: Duration,
|
pub play_time: Duration,
|
||||||
#[serde(with = "ts_milliseconds_option")]
|
#[serde(with = "ts_milliseconds_option")]
|
||||||
|
#[cfg_attr(feature = "ts", ts(type = "number"))]
|
||||||
pub last_played: Option<DateTime<Utc>>,
|
pub last_played: Option<DateTime<Utc>>,
|
||||||
#[serde(with = "ts_milliseconds_option")]
|
#[serde(with = "ts_milliseconds_option")]
|
||||||
|
#[cfg_attr(feature = "ts", ts(type = "number"))]
|
||||||
pub date_added: Option<DateTime<Utc>>,
|
pub date_added: Option<DateTime<Utc>>,
|
||||||
#[serde(with = "ts_milliseconds_option")]
|
#[serde(with = "ts_milliseconds_option")]
|
||||||
|
#[cfg_attr(feature = "ts", ts(type = "number"))]
|
||||||
pub date_modified: Option<DateTime<Utc>>,
|
pub date_modified: Option<DateTime<Utc>>,
|
||||||
pub album_art: Vec<AlbumArt>,
|
pub album_art: Vec<AlbumArt>,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "ts",
|
||||||
|
// The combination of "Tag" and "Map" doesn't seem to play well, possibly because of the 'Field' variant,
|
||||||
|
// which will cause type errors on the TS side otherwise
|
||||||
|
ts(type = "any"),
|
||||||
|
serde(serialize_with = "tags_strings")
|
||||||
|
)]
|
||||||
pub tags: BTreeMap<Tag, String>,
|
pub tags: BTreeMap<Tag, String>,
|
||||||
pub internal_tags: Vec<InternalTag>,
|
pub internal_tags: Vec<InternalTag>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ts")]
|
||||||
|
fn tags_strings<S: serde::Serializer>(
|
||||||
|
tags: &BTreeMap<Tag, String>,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut map = serializer.serialize_map(Some(tags.len()))?;
|
||||||
|
for (k, v) in tags {
|
||||||
|
map.serialize_entry(&k.to_string(), &v)?;
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ts")]
|
||||||
|
fn dur_ms<S: serde::Serializer>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.serialize_u64(duration.as_secs())
|
||||||
|
}
|
||||||
|
#[cfg(feature = "ts")]
|
||||||
|
fn ms_dur<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Duration, D::Error> {
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use serde::de::Visitor;
|
||||||
|
|
||||||
|
struct De(PhantomData<fn() -> Duration>);
|
||||||
|
impl<'de> Visitor<'de> for De {
|
||||||
|
type Value = Duration;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("Duration as seconds in the form of a u64")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Ok(Duration::from_secs(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deserializer.deserialize_u64(De(PhantomData))
|
||||||
|
}
|
||||||
|
|
||||||
impl Song {
|
impl Song {
|
||||||
/// Get a tag's value
|
/// Get a tag's value
|
||||||
///
|
///
|
||||||
|
@ -257,13 +328,13 @@ impl Song {
|
||||||
for item in tag.items() {
|
for item in tag.items() {
|
||||||
let key = match item.key() {
|
let key = match item.key() {
|
||||||
ItemKey::TrackTitle => Tag::Title,
|
ItemKey::TrackTitle => Tag::Title,
|
||||||
ItemKey::TrackNumber => Tag::Track,
|
ItemKey::TrackNumber => Tag::TrackNumber,
|
||||||
ItemKey::TrackArtist => Tag::Artist,
|
ItemKey::TrackArtist => Tag::Artist,
|
||||||
ItemKey::AlbumArtist => Tag::AlbumArtist,
|
ItemKey::AlbumArtist => Tag::AlbumArtist,
|
||||||
ItemKey::Genre => Tag::Genre,
|
ItemKey::Genre => Tag::Genre,
|
||||||
ItemKey::Comment => Tag::Comment,
|
ItemKey::Comment => Tag::Comment,
|
||||||
ItemKey::AlbumTitle => Tag::Album,
|
ItemKey::AlbumTitle => Tag::Album,
|
||||||
ItemKey::DiscNumber => Tag::Disk,
|
ItemKey::DiscNumber => Tag::DiskNumber,
|
||||||
ItemKey::Unknown(unknown)
|
ItemKey::Unknown(unknown)
|
||||||
if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" =>
|
if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" =>
|
||||||
{
|
{
|
||||||
|
@ -400,7 +471,10 @@ impl Song {
|
||||||
tags.insert(Tag::Artist, artist.clone());
|
tags.insert(Tag::Artist, artist.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
tags.insert(Tag::Track, track.no.parse().unwrap_or((i + 1).to_string()));
|
tags.insert(
|
||||||
|
Tag::TrackNumber,
|
||||||
|
track.no.parse().unwrap_or((i + 1).to_string()),
|
||||||
|
);
|
||||||
match track.title.clone() {
|
match track.title.clone() {
|
||||||
Some(title) => tags.insert(Tag::Title, title),
|
Some(title) => tags.insert(Tag::Title, title),
|
||||||
None => match track.isrc.clone() {
|
None => match track.isrc.clone() {
|
||||||
|
@ -498,13 +572,16 @@ impl Song {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum URI {
|
pub enum URI {
|
||||||
Local(PathBuf),
|
Local(PathBuf),
|
||||||
Cue {
|
Cue {
|
||||||
location: PathBuf,
|
location: PathBuf,
|
||||||
index: usize,
|
index: usize,
|
||||||
|
#[cfg_attr(feature = "ts", ts(type = "number"))]
|
||||||
start: Duration,
|
start: Duration,
|
||||||
|
#[cfg_attr(feature = "ts", ts(type = "number"))]
|
||||||
end: Duration,
|
end: Duration,
|
||||||
},
|
},
|
||||||
Remote(Service, String),
|
Remote(Service, String),
|
||||||
|
@ -590,6 +667,7 @@ impl Display for URI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum Service {
|
pub enum Service {
|
||||||
InternetRadio,
|
InternetRadio,
|
||||||
|
@ -1134,7 +1212,7 @@ impl MusicLibrary {
|
||||||
//let norm_title = normalize(&album_title);
|
//let norm_title = normalize(&album_title);
|
||||||
|
|
||||||
let disc_num = song
|
let disc_num = song
|
||||||
.get_tag(&Tag::Disk)
|
.get_tag(&Tag::DiskNumber)
|
||||||
.unwrap_or(&"".to_string())
|
.unwrap_or(&"".to_string())
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.unwrap_or(1);
|
.unwrap_or(1);
|
||||||
|
@ -1143,7 +1221,7 @@ impl MusicLibrary {
|
||||||
// If the album is in the list, add the track to the appropriate disc within the album
|
// If the album is in the list, add the track to the appropriate disc within the album
|
||||||
Some(album) => match album.discs.get_mut(&disc_num) {
|
Some(album) => match album.discs.get_mut(&disc_num) {
|
||||||
Some(disc) => disc.push((
|
Some(disc) => disc.push((
|
||||||
song.get_tag(&Tag::Track)
|
song.get_tag(&Tag::TrackNumber)
|
||||||
.unwrap_or(&String::new())
|
.unwrap_or(&String::new())
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
@ -1153,7 +1231,7 @@ impl MusicLibrary {
|
||||||
album.discs.insert(
|
album.discs.insert(
|
||||||
disc_num,
|
disc_num,
|
||||||
vec![(
|
vec![(
|
||||||
song.get_tag(&Tag::Track)
|
song.get_tag(&Tag::TrackNumber)
|
||||||
.unwrap_or(&String::new())
|
.unwrap_or(&String::new())
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
@ -1171,7 +1249,7 @@ impl MusicLibrary {
|
||||||
discs: BTreeMap::from([(
|
discs: BTreeMap::from([(
|
||||||
disc_num,
|
disc_num,
|
||||||
vec![(
|
vec![(
|
||||||
song.get_tag(&Tag::Track)
|
song.get_tag(&Tag::TrackNumber)
|
||||||
.unwrap_or(&String::new())
|
.unwrap_or(&String::new())
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
@ -1308,8 +1386,8 @@ mod test {
|
||||||
&vec![
|
&vec![
|
||||||
Tag::Field("location".to_string()),
|
Tag::Field("location".to_string()),
|
||||||
Tag::Album,
|
Tag::Album,
|
||||||
Tag::Disk,
|
Tag::DiskNumber,
|
||||||
Tag::Track,
|
Tag::TrackNumber,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -11,7 +11,6 @@ use std::time::Duration;
|
||||||
|
|
||||||
// use chrono::Duration;
|
// use chrono::Duration;
|
||||||
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
|
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
|
||||||
use chrono::format::Item;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
|
@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dmp-core = { path = "../dmp-core" }
|
dmp-core = { path = "../dmp-core", features = ["ts"] }
|
||||||
tauri = { version = "2", features = [ "protocol-asset", "unstable"] }
|
tauri = { version = "2", features = [ "protocol-asset", "unstable"] }
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
|
@ -11,7 +11,7 @@ use tauri::{AppHandle, Emitter, State, Wry};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{LAST_FM_API_KEY, LAST_FM_API_SECRET, wrappers::_Song};
|
use crate::{LAST_FM_API_KEY, LAST_FM_API_SECRET};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn add_song_to_queue(
|
pub async fn add_song_to_queue(
|
||||||
|
@ -47,7 +47,7 @@ pub async fn play_now(
|
||||||
Err(e) => return Err(e.to_string()),
|
Err(e) => return Err(e.to_string()),
|
||||||
};
|
};
|
||||||
app.emit("queue_updated", ()).unwrap();
|
app.emit("queue_updated", ()).unwrap();
|
||||||
app.emit("now_playing_change", _Song::from(&song)).unwrap();
|
app.emit("now_playing_change", &song).unwrap();
|
||||||
app.emit("playing", true).unwrap();
|
app.emit("playing", true).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,12 @@ use dmp_core::{
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use tauri::{AppHandle, Emitter, Manager, http::Response};
|
use tauri::{AppHandle, Emitter, Manager, http::Response};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use wrappers::{_Song, stop};
|
use wrappers::stop;
|
||||||
|
|
||||||
use crate::wrappers::{
|
use crate::wrappers::{
|
||||||
add_song_to_playlist, clear_queue, delete_playlist, get_library, get_playlist, get_playlists,
|
add_song_to_playlist, clear_queue, delete_playlist, get_library, get_playlist, get_playlists,
|
||||||
get_queue, get_song, import_playlist, next, pause, play, play_next_queue, prev,
|
get_queue, get_song, import_playlist, next, pause, play, play_next_queue, prev, queue_move_to,
|
||||||
remove_from_queue, seek, set_volume, queue_move_to
|
remove_from_queue, seek, set_volume,
|
||||||
};
|
};
|
||||||
use commands::{
|
use commands::{
|
||||||
add_song_to_queue, display_album_art, last_fm_init_auth, play_now, remove_from_lib_playlist,
|
add_song_to_queue, display_album_art, last_fm_init_auth, play_now, remove_from_lib_playlist,
|
||||||
|
@ -212,7 +212,7 @@ fn start_controller(app: AppHandle) -> Result<(), String> {
|
||||||
let next_song_notification = next_song_notification;
|
let next_song_notification = next_song_notification;
|
||||||
while true {
|
while true {
|
||||||
let song = next_song_notification.recv().unwrap();
|
let song = next_song_notification.recv().unwrap();
|
||||||
app.emit("now_playing_change", _Song::from(&song)).unwrap();
|
app.emit("now_playing_change", &song).unwrap();
|
||||||
app.emit("queue_updated", ()).unwrap();
|
app.emit("queue_updated", ()).unwrap();
|
||||||
app.emit("playing", true).unwrap();
|
app.emit("playing", true).unwrap();
|
||||||
_ = now_playing.write().insert(song);
|
_ = now_playing.write().insert(song);
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
use std::{collections::BTreeMap, path::PathBuf, thread::spawn};
|
use std::{path::PathBuf, thread::spawn};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc, serde::ts_milliseconds_option};
|
|
||||||
use crossbeam::channel::Sender;
|
use crossbeam::channel::Sender;
|
||||||
use dmp_core::{
|
use dmp_core::{
|
||||||
music_controller::controller::{ControllerHandle, PlayerLocation},
|
music_controller::controller::{ControllerHandle, PlayerLocation},
|
||||||
music_storage::{
|
music_storage::{
|
||||||
library::{Song, Tag, URI},
|
library::{Song, Tag},
|
||||||
queue::QueueItemType,
|
queue::QueueItemType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -83,7 +82,7 @@ pub async fn next(
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => return Err(e.to_string()),
|
Err(e) => return Err(e.to_string()),
|
||||||
};
|
};
|
||||||
app.emit("now_playing_change", _Song::from(&song)).unwrap();
|
app.emit("now_playing_change", song).unwrap();
|
||||||
app.emit("queue_updated", ()).unwrap();
|
app.emit("queue_updated", ()).unwrap();
|
||||||
app.emit("playing", true).unwrap();
|
app.emit("playing", true).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -99,7 +98,7 @@ pub async fn prev(
|
||||||
Err(e) => return Err(e.to_string()),
|
Err(e) => return Err(e.to_string()),
|
||||||
};
|
};
|
||||||
println!("prev");
|
println!("prev");
|
||||||
app.emit("now_playing_change", _Song::from(&song)).unwrap();
|
app.emit("now_playing_change", song).unwrap();
|
||||||
app.emit("queue_updated", ()).unwrap();
|
app.emit("queue_updated", ()).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -112,7 +111,7 @@ pub async fn now_playing(_ctrl_handle: State<'_, ControllerHandle>) -> Result<()
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_queue(
|
pub async fn get_queue(
|
||||||
ctrl_handle: State<'_, ControllerHandle>,
|
ctrl_handle: State<'_, ControllerHandle>,
|
||||||
) -> Result<Vec<(_Song, PlayerLocation)>, String> {
|
) -> Result<Vec<(Song, PlayerLocation)>, String> {
|
||||||
Ok(ctrl_handle
|
Ok(ctrl_handle
|
||||||
.queue_get_all()
|
.queue_get_all()
|
||||||
.await
|
.await
|
||||||
|
@ -121,7 +120,7 @@ pub async fn get_queue(
|
||||||
let QueueItemType::Single(song) = item.item else {
|
let QueueItemType::Single(song) = item.item else {
|
||||||
unreachable!("There should be no albums in the queue right now")
|
unreachable!("There should be no albums in the queue right now")
|
||||||
};
|
};
|
||||||
(_Song::from(&song.song), song.location)
|
(song.song, song.location)
|
||||||
})
|
})
|
||||||
.collect_vec())
|
.collect_vec())
|
||||||
}
|
}
|
||||||
|
@ -141,51 +140,9 @@ pub async fn remove_from_queue(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Grab Album art from custom protocol
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
pub struct _Song {
|
|
||||||
pub location: Vec<URI>,
|
|
||||||
pub uuid: Uuid,
|
|
||||||
pub plays: i32,
|
|
||||||
pub format: Option<String>,
|
|
||||||
pub duration: String,
|
|
||||||
#[serde(with = "ts_milliseconds_option")]
|
|
||||||
pub last_played: Option<DateTime<Utc>>,
|
|
||||||
#[serde(with = "ts_milliseconds_option")]
|
|
||||||
pub date_added: Option<DateTime<Utc>>,
|
|
||||||
#[serde(with = "ts_milliseconds_option")]
|
|
||||||
pub date_modified: Option<DateTime<Utc>>,
|
|
||||||
pub tags: BTreeMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Song> for _Song {
|
|
||||||
fn from(value: &Song) -> Self {
|
|
||||||
_Song {
|
|
||||||
location: value.location.clone(),
|
|
||||||
uuid: value.uuid.clone(),
|
|
||||||
plays: value.plays.clone(),
|
|
||||||
duration: value.duration.as_secs().to_string(),
|
|
||||||
format: value.format.clone().map(|format| format.to_string()),
|
|
||||||
last_played: value.last_played,
|
|
||||||
date_added: value.date_added,
|
|
||||||
date_modified: value.date_modified,
|
|
||||||
tags: value
|
|
||||||
.tags
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| (k.to_string(), v.clone()))
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_library(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec<_Song>, String> {
|
pub async fn get_library(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec<Song>, String> {
|
||||||
let songs = ctrl_handle
|
let songs = ctrl_handle.lib_get_all().await;
|
||||||
.lib_get_all()
|
|
||||||
.await
|
|
||||||
.iter()
|
|
||||||
.map(|song| _Song::from(song))
|
|
||||||
.collect_vec();
|
|
||||||
Ok(songs)
|
Ok(songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,23 +150,17 @@ pub async fn get_library(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec
|
||||||
pub async fn get_playlist(
|
pub async fn get_playlist(
|
||||||
ctrl_handle: State<'_, ControllerHandle>,
|
ctrl_handle: State<'_, ControllerHandle>,
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
) -> Result<Vec<_Song>, String> {
|
) -> Result<Vec<Song>, String> {
|
||||||
let playlist = match ctrl_handle.playlist_get(uuid).await {
|
let playlist = match ctrl_handle.playlist_get(uuid).await {
|
||||||
Ok(list) => list,
|
Ok(list) => list,
|
||||||
Err(_) => todo!(),
|
Err(_) => todo!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let songs = playlist
|
|
||||||
.tracks
|
|
||||||
.iter()
|
|
||||||
.map(|song| _Song::from(song))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
println!(
|
println!(
|
||||||
"Got Playlist {}, len {}",
|
"Got Playlist {}, len {}",
|
||||||
playlist.title,
|
playlist.title,
|
||||||
playlist.tracks.len()
|
playlist.tracks.len()
|
||||||
);
|
);
|
||||||
Ok(songs)
|
Ok(playlist.tracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
@ -264,13 +215,13 @@ pub struct PlaylistPayload {
|
||||||
pub async fn get_song(
|
pub async fn get_song(
|
||||||
ctrl_handle: State<'_, ControllerHandle>,
|
ctrl_handle: State<'_, ControllerHandle>,
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
) -> Result<_Song, String> {
|
) -> Result<Song, String> {
|
||||||
let song = ctrl_handle.lib_get_song(uuid).await.0;
|
let song = ctrl_handle.lib_get_song(uuid).await.0;
|
||||||
println!(
|
println!(
|
||||||
"got song {}",
|
"got song {}",
|
||||||
&song.tags.get(&Tag::Title).unwrap_or(&String::new())
|
&song.tags.get(&Tag::Title).unwrap_or(&String::new())
|
||||||
);
|
);
|
||||||
Ok(_Song::from(&song))
|
Ok(song)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
@ -334,7 +285,7 @@ pub async fn queue_move_to(
|
||||||
match ctrl_handle.enqueue(0).await.map_err(|e| e.to_string()) {
|
match ctrl_handle.enqueue(0).await.map_err(|e| e.to_string()) {
|
||||||
Ok(song) => {
|
Ok(song) => {
|
||||||
app.emit("queue_updated", ()).unwrap();
|
app.emit("queue_updated", ()).unwrap();
|
||||||
app.emit("now_playing_change", _Song::from(&song)).unwrap();
|
app.emit("now_playing_change", song).unwrap();
|
||||||
app.emit("playing", true).unwrap();
|
app.emit("playing", true).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
93
src/App.tsx
93
src/App.tsx
|
@ -1,19 +1,20 @@
|
||||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { Config, playbackInfo } from "./types";
|
|
||||||
// import { EventEmitter } from "@tauri-apps/plugin-shell";
|
// import { EventEmitter } from "@tauri-apps/plugin-shell";
|
||||||
// import { listen } from "@tauri-apps/api/event";
|
// import { listen } from "@tauri-apps/api/event";
|
||||||
// import { fetch } from "@tauri-apps/plugin-http";
|
// import { fetch } from "@tauri-apps/plugin-http";
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import { PhysicalPosition } from "@tauri-apps/api/window";
|
import { PhysicalPosition } from "@tauri-apps/api/window";
|
||||||
import { Menu, Submenu, SubmenuOptions } from "@tauri-apps/api/menu";
|
import { Menu, Submenu, SubmenuOptions } from "@tauri-apps/api/menu";
|
||||||
|
import { Config } from "./bindings/Config";
|
||||||
|
import { PlaybackInfo } from "./bindings/PlaybackInfo";
|
||||||
|
import { PlayerLocation } from "./bindings/PlayerLocation";
|
||||||
|
import { URI } from "./bindings/URI";
|
||||||
|
import { Song } from "./bindings/Song";
|
||||||
|
|
||||||
const appWindow = getCurrentWebviewWindow();
|
const appWindow = getCurrentWebviewWindow();
|
||||||
|
|
||||||
type Location = "Library" | { "Playlist": string };
|
|
||||||
|
|
||||||
// This needs to be changed to properly reflect cursor position
|
// This needs to be changed to properly reflect cursor position
|
||||||
// this will do for now.
|
// this will do for now.
|
||||||
async function contextMenuPosition(event: React.MouseEvent) {
|
async function contextMenuPosition(event: React.MouseEvent) {
|
||||||
|
@ -43,16 +44,16 @@ function App() {
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = appWindow.listen<any>("now_playing_change", ({ payload, }) => {
|
const unlisten = appWindow.listen<Song>("now_playing_change", ({ payload, }) => {
|
||||||
const displayArtwork = () => {
|
const displayArtwork = () => {
|
||||||
invoke('display_album_art', { uuid: payload.uuid }).then(() => {})
|
invoke('display_album_art', { uuid: payload.uuid }).then(() => {})
|
||||||
}
|
}
|
||||||
// console.log(event);
|
|
||||||
setNowPlaying(
|
setNowPlaying(
|
||||||
<NowPlaying
|
<NowPlaying
|
||||||
title={ payload.tags.TrackTitle }
|
title={ payload.tags.Title }
|
||||||
album={ payload.tags.AlbumTitle }
|
album={ payload.tags.Album }
|
||||||
artist={ payload.tags.TrackArtist }
|
artist={ payload.tags.Artist }
|
||||||
artwork={ <img src={convertFileSrc("abc") + "?" + payload.uuid } id="nowPlayingArtwork" alt="Now Playing Artwork" key={payload.uuid} onDoubleClick={ displayArtwork } /> }
|
artwork={ <img src={convertFileSrc("abc") + "?" + payload.uuid } id="nowPlayingArtwork" alt="Now Playing Artwork" key={payload.uuid} onDoubleClick={ displayArtwork } /> }
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -62,15 +63,15 @@ function App() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = appWindow.listen<any>("queue_updated", (_) => {
|
const unlisten = appWindow.listen<null>("queue_updated", (_) => {
|
||||||
// console.log(event);
|
// console.log(event);
|
||||||
invoke('get_queue').then((_songs) => {
|
invoke('get_queue').then((_songs) => {
|
||||||
let songs = _songs as any[];
|
let songs = _songs as [Song, PlayerLocation][];
|
||||||
setQueue(
|
setQueue(
|
||||||
songs.filter((_, i) => i != 0).map((song, i) =>
|
songs.filter((_, i) => i != 0).map((song, i) =>
|
||||||
<QueueSong
|
<QueueSong
|
||||||
song={ song[0] }
|
song={ song[0] }
|
||||||
location={ song[1] as "Library" | {"Playlist" : string}}
|
location={ song[1] }
|
||||||
index={i+1}
|
index={i+1}
|
||||||
key={ Math.floor((Math.random() * 100_000_000_000) + 1) + '_' + Date.now() }
|
key={ Math.floor((Math.random() * 100_000_000_000) + 1) + '_' + Date.now() }
|
||||||
setSelectedSong={ setSelectedSongQueue }
|
setSelectedSong={ setSelectedSongQueue }
|
||||||
|
@ -83,8 +84,8 @@ function App() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = appWindow.listen<any>("playing", (isPlaying) => {
|
const unlisten = appWindow.listen<boolean>("playing", (isPlaying) => {
|
||||||
setPlaying(isPlaying.payload as boolean)
|
setPlaying(isPlaying.payload)
|
||||||
})
|
})
|
||||||
return () => { unlisten.then((f) => f()) }
|
return () => { unlisten.then((f) => f()) }
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -133,11 +134,11 @@ interface PlaylistHeadProps {
|
||||||
function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playlistsInfo, setSelected }: PlaylistHeadProps) {
|
function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playlistsInfo, setSelected }: PlaylistHeadProps) {
|
||||||
function getPlaylist(playlist: PlaylistInfo) {
|
function getPlaylist(playlist: PlaylistInfo) {
|
||||||
invoke('get_playlist', { uuid: playlist.uuid }).then((list) => {
|
invoke('get_playlist', { uuid: playlist.uuid }).then((list) => {
|
||||||
setLibrary([...(list as any[]).map((song, i) => {
|
setLibrary([...(list as Song[]).map((song, i) => {
|
||||||
// console.log(song);
|
// console.log(song);
|
||||||
const reload = () => getPlaylist(playlist)
|
const reload = () => getPlaylist(playlist)
|
||||||
return (
|
return (
|
||||||
<Song
|
<MainViewSong
|
||||||
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
||||||
location={ song.location }
|
location={ song.location }
|
||||||
playerLocation={ {"Playlist" : playlist.uuid } }
|
playerLocation={ {"Playlist" : playlist.uuid } }
|
||||||
|
@ -157,9 +158,9 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = appWindow.listen<any[]>("playlists_gotten", (_res) => {
|
const unlisten = appWindow.listen<PlaylistInfo[]>("playlists_gotten", (_res) => {
|
||||||
|
const res = _res.payload;
|
||||||
// console.log(event);
|
// console.log(event);
|
||||||
let res = _res.payload as PlaylistInfo[];
|
|
||||||
playlistsInfo.current = [...res];
|
playlistsInfo.current = [...res];
|
||||||
// console.log(playlistsInfo, res);
|
// console.log(playlistsInfo, res);
|
||||||
|
|
||||||
|
@ -192,7 +193,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
|
||||||
}, []);
|
}, []);
|
||||||
let handle_import = () => {
|
let handle_import = () => {
|
||||||
invoke('import_playlist').then((_res) => {
|
invoke('import_playlist').then((_res) => {
|
||||||
let res = _res as any;
|
let res = _res as PlaylistInfo;
|
||||||
|
|
||||||
setPlaylists([
|
setPlaylists([
|
||||||
...playlists,
|
...playlists,
|
||||||
|
@ -207,11 +208,11 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
|
||||||
setViewName("Library");
|
setViewName("Library");
|
||||||
invoke('get_library').then((lib) => {
|
invoke('get_library').then((lib) => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
setLibrary([...(lib as any[]).map((song) => {
|
setLibrary([...(lib as Song[]).map((song) => {
|
||||||
// console.log(song);
|
// console.log(song);
|
||||||
i++;
|
i++;
|
||||||
return (
|
return (
|
||||||
<Song
|
<MainViewSong
|
||||||
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
||||||
location={ song.location }
|
location={ song.location }
|
||||||
playerLocation="Library"
|
playerLocation="Library"
|
||||||
|
@ -247,7 +248,7 @@ function MainView({ lib_ref, viewName, playlistsInfo, setSelected, selectedSong
|
||||||
|
|
||||||
|
|
||||||
const addToQueue = (_: string) => {
|
const addToQueue = (_: string) => {
|
||||||
invoke('add_song_to_queue', { uuid: selectedSong.current!.uuid, location: selectedSong.current!.playerLocation }).then(() => {});
|
invoke('add_song_to_queue', { uuid: selectedSong.current?.uuid, location: selectedSong.current?.playerLocation }).then(() => {});
|
||||||
}
|
}
|
||||||
const playNow = () => invoke("play_now", { uuid: selectedSong.current!.uuid, location: selectedSong.current!.playerLocation }).then(() => {})
|
const playNow = () => invoke("play_now", { uuid: selectedSong.current!.uuid, location: selectedSong.current!.playerLocation }).then(() => {})
|
||||||
const playNext = () => invoke("play_next_queue", { uuid: selectedSong.current!.uuid, location: selectedSong.current!.playerLocation }).then(() => {})
|
const playNext = () => invoke("play_next_queue", { uuid: selectedSong.current!.uuid, location: selectedSong.current!.playerLocation }).then(() => {})
|
||||||
|
@ -289,14 +290,13 @@ function MainView({ lib_ref, viewName, playlistsInfo, setSelected, selectedSong
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = appWindow.listen<any>("library_loaded", (_) => {
|
const unlisten = appWindow.listen<null>("library_loaded", (_) => {
|
||||||
console.log("library_loaded");
|
console.log("library_loaded");
|
||||||
invoke('get_library').then((lib) => {
|
invoke('get_library').then((lib) => {
|
||||||
let i = 0;
|
setLibrary([...(lib as Song[]).map((song, i) => {
|
||||||
setLibrary([...(lib as any[]).map((song) => {
|
console.log("duration", song.duration)
|
||||||
i++;
|
|
||||||
return (
|
return (
|
||||||
<Song
|
<MainViewSong
|
||||||
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
||||||
location={ song.location }
|
location={ song.location }
|
||||||
playerLocation="Library"
|
playerLocation="Library"
|
||||||
|
@ -305,7 +305,7 @@ function MainView({ lib_ref, viewName, playlistsInfo, setSelected, selectedSong
|
||||||
duration={ song.duration }
|
duration={ song.duration }
|
||||||
tags={ song.tags }
|
tags={ song.tags }
|
||||||
playlists={ playlistsInfo }
|
playlists={ playlistsInfo }
|
||||||
index={ i - 1 }
|
index={ i }
|
||||||
setSelected={ setSelected }
|
setSelected={ setSelected }
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -331,12 +331,12 @@ function MainView({ lib_ref, viewName, playlistsInfo, setSelected, selectedSong
|
||||||
|
|
||||||
|
|
||||||
interface SongProps {
|
interface SongProps {
|
||||||
location: any,
|
location: URI[],
|
||||||
playerLocation: string | {"Playlist" : any},
|
playerLocation: PlayerLocation,
|
||||||
uuid: string,
|
uuid: string,
|
||||||
plays: number,
|
plays: number,
|
||||||
format?: string,
|
format?: string,
|
||||||
duration: string,
|
duration: number,
|
||||||
last_played?: string,
|
last_played?: string,
|
||||||
date_added?: string,
|
date_added?: string,
|
||||||
date_modified?: string,
|
date_modified?: string,
|
||||||
|
@ -347,7 +347,7 @@ interface SongProps {
|
||||||
reload?: () => void
|
reload?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function Song(props: SongProps) {
|
function MainViewSong(props: SongProps) {
|
||||||
// console.log(props.tags);
|
// console.log(props.tags);
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const unlistenPromise = listen<string>("add_song_to_queue", (event) => {
|
// const unlistenPromise = listen<string>("add_song_to_queue", (event) => {
|
||||||
|
@ -363,16 +363,16 @@ function Song(props: SongProps) {
|
||||||
// }, []);
|
// }, []);
|
||||||
const setSelected = () => {
|
const setSelected = () => {
|
||||||
props.setSelected(props);
|
props.setSelected(props);
|
||||||
console.log(props.tags.TrackTitle);
|
console.log(props.tags.Title);
|
||||||
}
|
}
|
||||||
return(
|
return(
|
||||||
<div
|
<div
|
||||||
onContextMenu={ setSelected }
|
onContextMenu={ setSelected }
|
||||||
onClick={ setSelected }
|
onClick={ setSelected }
|
||||||
className="song">
|
className="song">
|
||||||
<p className="artist unselectable">{ props.tags.TrackArtist }</p>
|
<p className="artist unselectable">{ props.tags.Artist }</p>
|
||||||
<p className="title unselectable">{ props.tags.TrackTitle }</p>
|
<p className="title unselectable">{ props.tags.Title }</p>
|
||||||
<p className="album unselectable">{ props.tags.AlbumTitle }</p>
|
<p className="album unselectable">{ props.tags.Album }</p>
|
||||||
<p className="duration unselectable">
|
<p className="duration unselectable">
|
||||||
{ Math.round(+props.duration / 60) }:
|
{ Math.round(+props.duration / 60) }:
|
||||||
{ (+props.duration % 60).toString().padStart(2, "0") }
|
{ (+props.duration % 60).toString().padStart(2, "0") }
|
||||||
|
@ -397,8 +397,7 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
const [lastFmLoggedIn, setLastFmLoggedIn] = useState(false);
|
const [lastFmLoggedIn, setLastFmLoggedIn] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = appWindow.listen<any>("playback_info", ({ payload, }) => {
|
const unlisten = appWindow.listen<PlaybackInfo>("playback_info", ({ payload: info, }) => {
|
||||||
const info = payload as playbackInfo;
|
|
||||||
const pos_ = Array.isArray(info.position) ? info.position![0] : 0;
|
const pos_ = Array.isArray(info.position) ? info.position![0] : 0;
|
||||||
const dur_ = Array.isArray(info.duration) ? info.duration![0] : 0;
|
const dur_ = Array.isArray(info.duration) ? info.duration![0] : 0;
|
||||||
|
|
||||||
|
@ -463,9 +462,9 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NowPlayingProps {
|
interface NowPlayingProps {
|
||||||
title: string,
|
title: string | undefined,
|
||||||
artist: string,
|
artist: string | undefined,
|
||||||
album: string,
|
album: string | undefined,
|
||||||
artwork: JSX.Element
|
artwork: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -475,7 +474,7 @@ function NowPlaying({ title, artist, album, artwork }: NowPlayingProps) {
|
||||||
<div className="artworkWrapper unselectable">
|
<div className="artworkWrapper unselectable">
|
||||||
{ artwork }
|
{ artwork }
|
||||||
</div>
|
</div>
|
||||||
<h3>{ title }</h3>
|
<h3>{ title? title : "Unknown Title" }</h3>
|
||||||
<p>{ artist }</p>
|
<p>{ artist }</p>
|
||||||
<p>{ album }</p>
|
<p>{ album }</p>
|
||||||
</section>
|
</section>
|
||||||
|
@ -491,7 +490,7 @@ interface QueueProps {
|
||||||
interface selectedQueueSong {
|
interface selectedQueueSong {
|
||||||
uuid: string,
|
uuid: string,
|
||||||
index: number
|
index: number
|
||||||
location: Location,
|
location: PlayerLocation,
|
||||||
}
|
}
|
||||||
|
|
||||||
function Queue({ songs, selectedSong, }: QueueProps) {
|
function Queue({ songs, selectedSong, }: QueueProps) {
|
||||||
|
@ -530,8 +529,8 @@ function Queue({ songs, selectedSong, }: QueueProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueueSongProps {
|
interface QueueSongProps {
|
||||||
song: any,
|
song: Song,
|
||||||
location: Location,
|
location: PlayerLocation,
|
||||||
index: number,
|
index: number,
|
||||||
setSelectedSong: (song: selectedQueueSong) => void,
|
setSelectedSong: (song: selectedQueueSong) => void,
|
||||||
}
|
}
|
||||||
|
@ -545,8 +544,8 @@ function QueueSong({ song, location, index, setSelectedSong }: QueueSongProps) {
|
||||||
<div className="queueSong unselectable" onAuxClickCapture={ setSelected } onClick={ setSelected } onContextMenu={ setSelected }>
|
<div className="queueSong unselectable" onAuxClickCapture={ setSelected } onClick={ setSelected } onContextMenu={ setSelected }>
|
||||||
<img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/>
|
<img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/>
|
||||||
<div className="queueSongTags">
|
<div className="queueSongTags">
|
||||||
<p className="queueSongTitle">{ song.tags.TrackTitle }</p>
|
<p className="queueSongTitle">{ song.tags.Title }</p>
|
||||||
<p className="queueSongArtist">{ song.tags.TrackArtist }</p>
|
<p className="queueSongArtist">{ song.tags.Artist }</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
4
src/bindings/AlbumArt.ts
Normal file
4
src/bindings/AlbumArt.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { URI } from "./URI";
|
||||||
|
|
||||||
|
export type AlbumArt = { "Embedded": number } | { "External": URI };
|
3
src/bindings/BannedType.ts
Normal file
3
src/bindings/BannedType.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type BannedType = "Shuffle" | "All";
|
5
src/bindings/Config.ts
Normal file
5
src/bindings/Config.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { ConfigConnections } from "./ConfigConnections";
|
||||||
|
import type { ConfigLibraries } from "./ConfigLibraries";
|
||||||
|
|
||||||
|
export type Config = { path: string, backup_folder: string | null, libraries: ConfigLibraries, connections: ConfigConnections, state_path: string, };
|
3
src/bindings/ConfigConnections.ts
Normal file
3
src/bindings/ConfigConnections.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type ConfigConnections = { discord_rpc_client_id: bigint | null, listenbrainz_token: string | null, last_fm_session: string | null, };
|
4
src/bindings/ConfigLibraries.ts
Normal file
4
src/bindings/ConfigLibraries.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { ConfigLibrary } from "./ConfigLibrary";
|
||||||
|
|
||||||
|
export type ConfigLibraries = { default_library: string, library_folder: string, libraries: Array<ConfigLibrary>, };
|
3
src/bindings/ConfigLibrary.ts
Normal file
3
src/bindings/ConfigLibrary.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type ConfigLibrary = { name: string, path: string, uuid: string, scan_folders: Array<string> | null, };
|
3
src/bindings/DoNotTrack.ts
Normal file
3
src/bindings/DoNotTrack.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type DoNotTrack = "LastFM" | "LibreFM" | "MusicBrainz" | "Discord";
|
5
src/bindings/InternalTag.ts
Normal file
5
src/bindings/InternalTag.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { DoNotTrack } from "./DoNotTrack";
|
||||||
|
import type { SongType } from "./SongType";
|
||||||
|
|
||||||
|
export type InternalTag = { "DoNotTrack": DoNotTrack } | { "SongType": SongType } | { "SongLink": [string, SongType] } | { "VolumeAdjustment": number };
|
3
src/bindings/PlaybackInfo.ts
Normal file
3
src/bindings/PlaybackInfo.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type PlaybackInfo = { position: [number, number], duration: [number, number], };
|
3
src/bindings/PlayerLocation.ts
Normal file
3
src/bindings/PlayerLocation.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type PlayerLocation = "Test" | "Library" | { "Playlist": string } | "File" | "Custom";
|
3
src/bindings/Service.ts
Normal file
3
src/bindings/Service.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type Service = "InternetRadio" | "Spotify" | "Youtube" | "None";
|
14
src/bindings/Song.ts
Normal file
14
src/bindings/Song.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { AlbumArt } from "./AlbumArt";
|
||||||
|
import type { BannedType } from "./BannedType";
|
||||||
|
import type { InternalTag } from "./InternalTag";
|
||||||
|
import type { URI } from "./URI";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores information about a single song
|
||||||
|
*/
|
||||||
|
export type Song = { location: Array<URI>, uuid: string, plays: number, skips: number, favorited: boolean, banned: BannedType | null, rating: number | null,
|
||||||
|
/**
|
||||||
|
* MIME type
|
||||||
|
*/
|
||||||
|
format: string | null, duration: number, play_time: number, last_played: number, date_added: number, date_modified: number, album_art: Array<AlbumArt>, tags: any, internal_tags: Array<InternalTag>, };
|
3
src/bindings/SongType.ts
Normal file
3
src/bindings/SongType.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type SongType = "Main" | "Instrumental" | "Remix" | { "Custom": string };
|
6
src/bindings/Tag.ts
Normal file
6
src/bindings/Tag.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tag for a song
|
||||||
|
*/
|
||||||
|
export type Tag = "Title" | "Album" | "Artist" | "AlbumArtist" | "Genre" | "Comment" | "TrackNumber" | "DiskNumber" | { "Field": string } | string;
|
4
src/bindings/URI.ts
Normal file
4
src/bindings/URI.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { Service } from "./Service";
|
||||||
|
|
||||||
|
export type URI = { "Local": string } | { "Cue": { location: string, index: number, start: number, end: number, } } | { "Remote": [Service, string] };
|
70
src/types.ts
70
src/types.ts
|
@ -1,70 +0,0 @@
|
||||||
export interface Configlibrary {
|
|
||||||
name: string,
|
|
||||||
path: string,
|
|
||||||
uuid: string,
|
|
||||||
scan_folders?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigLibraries {
|
|
||||||
default_library: string,
|
|
||||||
library_folder: string,
|
|
||||||
libraries: Configlibrary[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
path: string,
|
|
||||||
backup_folder?: string,
|
|
||||||
libraries: ConfigLibraries,
|
|
||||||
volume: number,
|
|
||||||
connections: ConfigConnections,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigConnections {
|
|
||||||
discord_rpc_client_id?: number,
|
|
||||||
last_fm_session?: string,
|
|
||||||
listenbrainz_token?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Song {
|
|
||||||
location: URI[],
|
|
||||||
uuid: string,
|
|
||||||
plays: number,
|
|
||||||
skips: number,
|
|
||||||
favorited: boolean,
|
|
||||||
banned?: BannedType,
|
|
||||||
rating?: number,
|
|
||||||
format?: string,
|
|
||||||
duration: number,
|
|
||||||
play_time: number,
|
|
||||||
last_played?: number,
|
|
||||||
date_added?: number,
|
|
||||||
date_modified?: number,
|
|
||||||
album_art: AlbumArt[],
|
|
||||||
tags: Map<Tag, String>,
|
|
||||||
internal_tags: InternalTag[],
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum InternalTag {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Tag {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AlbumArt {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum URI {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum BannedType {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface playbackInfo {
|
|
||||||
position?: [number, number],
|
|
||||||
duration?: [number, number],
|
|
||||||
}
|
|
Loading…
Reference in a new issue