Added the rs-ts crate and made adjustments for proper TS binding generation

This commit is contained in:
MrDulfin 2025-06-08 00:55:28 -04:00
parent b8280775fa
commit 367d2ceda3
31 changed files with 273 additions and 239 deletions

View file

@ -17,4 +17,4 @@ strip = true
lto = true
opt-level = "z"
codegen-units = 1
panic = "abort"
panic = "abort"

View file

@ -44,3 +44,7 @@ rustfm-scrobble = "1.1.1"
reqwest = { version = "0.12.12", features = ["json"] }
tokio = { version = "1.43.0", features = ["macros"] }
opener = "0.7.2"
ts-rs = { version = "11.0.1", optional = true, features = ["uuid-impl", "chrono-impl", "serde_json"] }
[features]
ts = ["dep:ts-rs"]

View file

@ -7,8 +7,10 @@ use std::{
use serde::{Deserialize, Serialize};
use serde_json::to_string_pretty;
use thiserror::Error;
use ts_rs::TS;
use uuid::Uuid;
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigLibrary {
pub name: String,
@ -51,6 +53,7 @@ impl ConfigLibrary {
}
}
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct ConfigLibraries {
pub default_library: Uuid,
@ -92,6 +95,7 @@ impl ConfigLibraries {
}
}
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct ConfigConnections {
pub discord_rpc_client_id: Option<u64>,
@ -99,6 +103,7 @@ pub struct ConfigConnections {
pub last_fm_session: Option<String>,
}
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct Config {

View file

@ -28,6 +28,8 @@ use super::connections::{ConnectionsNotification, ControllerConnections};
use super::controller_handle::{LibraryCommandInput, PlayerCommandInput, QueueCommandInput};
use super::queue::{QueueAlbum, QueueSong};
use ts_rs::TS;
pub struct Controller();
type QueueItem_ = QueueItem<QueueSong, QueueAlbum>;
@ -43,6 +45,7 @@ pub enum ControllerError {
}
// 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)]
#[non_exhaustive]
pub enum PlayerLocation {
@ -370,8 +373,11 @@ impl Controller {
}
}
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Default, Serialize, Clone)]
pub struct PlaybackInfo {
#[ts(as = "(f64, f64)")]
pub position: Option<TimeDelta>,
#[ts(as = "(f64, f64)")]
pub duration: Option<TimeDelta>,
}

View file

@ -1,7 +1,6 @@
use std::path::PathBuf;
use async_channel::{Receiver, Sender};
use discord_presence::models::Command;
use uuid::Uuid;
use crate::music_storage::{

View file

@ -6,7 +6,7 @@ use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterato
use crate::{
config::Config,
music_storage::{
library::{self, MusicLibrary},
library::MusicLibrary,
playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem},
},
};

View file

@ -1,7 +1,4 @@
use crate::music_storage::{
library::Song,
queue::{Queue, QueueError, QueueItemType},
};
use crate::music_storage::queue::{Queue, QueueError, QueueItemType};
use super::{
controller::{Controller, QueueCommand, QueueResponse},

View file

@ -213,8 +213,8 @@ fn to_tag(string: String) -> Tag {
"album artist" => Tag::AlbumArtist,
"genre" => Tag::Genre,
"comment" => Tag::Comment,
"track number" => Tag::Track,
"disc number" => Tag::Disk,
"track number" => Tag::TrackNumber,
"disc number" => Tag::DiskNumber,
_ => Tag::Key(string),
}
}

View file

@ -1,6 +1,7 @@
use super::playlist::{Playlist, PlaylistFolder};
// Crate things
use super::utils::{find_images, normalize, read_file, write_file};
use crate::music_storage::playlist::PlaylistFolderItem;
use std::cmp::Ordering;
@ -19,6 +20,7 @@ use lofty::file::{AudioFile as _, TaggedFileExt as _};
use lofty::probe::Probe;
use lofty::tag::{ItemKey, ItemValue, TagType};
use rcue::parser::parse_from_file;
use serde::ser::SerializeMap;
use std::fs;
use std::path::{Path, PathBuf};
use uuid::Uuid;
@ -35,6 +37,10 @@ use serde::{Deserialize, Serialize};
use rayon::prelude::*;
use std::sync::{Arc, Mutex};
// TS
use ts_rs::TS;
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum AlbumArt {
Embedded(usize),
@ -52,6 +58,7 @@ impl AlbumArt {
/// A tag for a song
#[non_exhaustive]
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum Tag {
Title,
@ -60,23 +67,26 @@ pub enum Tag {
AlbumArtist,
Genre,
Comment,
Track,
Disk,
Key(String),
TrackNumber,
DiskNumber,
#[cfg_attr(feature = "ts", ts(type = "string"))]
Field(String),
#[serde(untagged)]
#[cfg_attr(feature = "ts", ts(type = "string"))]
Key(String),
}
impl Display for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let path_str: String = match self {
Self::Title => "TrackTitle".into(),
Self::Album => "AlbumTitle".into(),
Self::Artist => "TrackArtist".into(),
Self::Title => "Title".into(),
Self::Album => "Album".into(),
Self::Artist => "Artist".into(),
Self::AlbumArtist => "AlbumArtist".into(),
Self::Genre => "Genre".into(),
Self::Comment => "Comment".into(),
Self::Track => "TrackNumber".into(),
Self::Disk => "DiscNumber".into(),
Self::TrackNumber => "TrackNumber".into(),
Self::DiskNumber => "DiscNumber".into(),
Self::Key(key) => key.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)]
#[non_exhaustive]
pub enum InternalTag {
@ -131,6 +142,7 @@ pub enum InternalTag {
VolumeAdjustment(i8),
}
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub enum BannedType {
@ -138,6 +150,7 @@ pub enum BannedType {
All,
}
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum DoNotTrack {
@ -148,6 +161,7 @@ pub enum DoNotTrack {
Discord,
}
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum SongType {
@ -160,6 +174,7 @@ pub enum SongType {
}
/// Stores information about a single song
#[cfg_attr(feature = "ts", derive(TS), ts(export))]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Song {
pub location: Vec<URI>,
@ -171,19 +186,75 @@ pub struct Song {
pub rating: Option<u8>,
/// MIME type
pub format: Option<String>,
#[cfg_attr(
feature = "ts",
ts(type = "number"),
serde(serialize_with = "dur_ms", deserialize_with = "ms_dur")
)]
pub duration: Duration,
#[cfg_attr(feature = "ts", ts(type = "number"))]
pub play_time: Duration,
#[serde(with = "ts_milliseconds_option")]
#[cfg_attr(feature = "ts", ts(type = "number"))]
pub last_played: Option<DateTime<Utc>>,
#[serde(with = "ts_milliseconds_option")]
#[cfg_attr(feature = "ts", ts(type = "number"))]
pub date_added: Option<DateTime<Utc>>,
#[serde(with = "ts_milliseconds_option")]
#[cfg_attr(feature = "ts", ts(type = "number"))]
pub date_modified: Option<DateTime<Utc>>,
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 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 {
/// Get a tag's value
///
@ -257,13 +328,13 @@ impl Song {
for item in tag.items() {
let key = match item.key() {
ItemKey::TrackTitle => Tag::Title,
ItemKey::TrackNumber => Tag::Track,
ItemKey::TrackNumber => Tag::TrackNumber,
ItemKey::TrackArtist => Tag::Artist,
ItemKey::AlbumArtist => Tag::AlbumArtist,
ItemKey::Genre => Tag::Genre,
ItemKey::Comment => Tag::Comment,
ItemKey::AlbumTitle => Tag::Album,
ItemKey::DiscNumber => Tag::Disk,
ItemKey::DiscNumber => Tag::DiskNumber,
ItemKey::Unknown(unknown)
if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" =>
{
@ -400,7 +471,10 @@ impl Song {
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() {
Some(title) => tags.insert(Tag::Title, title),
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)]
pub enum URI {
Local(PathBuf),
Cue {
location: PathBuf,
index: usize,
#[cfg_attr(feature = "ts", ts(type = "number"))]
start: Duration,
#[cfg_attr(feature = "ts", ts(type = "number"))]
end: Duration,
},
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)]
pub enum Service {
InternetRadio,
@ -1134,7 +1212,7 @@ impl MusicLibrary {
//let norm_title = normalize(&album_title);
let disc_num = song
.get_tag(&Tag::Disk)
.get_tag(&Tag::DiskNumber)
.unwrap_or(&"".to_string())
.parse::<u16>()
.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
Some(album) => match album.discs.get_mut(&disc_num) {
Some(disc) => disc.push((
song.get_tag(&Tag::Track)
song.get_tag(&Tag::TrackNumber)
.unwrap_or(&String::new())
.parse::<u16>()
.unwrap_or_default(),
@ -1153,7 +1231,7 @@ impl MusicLibrary {
album.discs.insert(
disc_num,
vec![(
song.get_tag(&Tag::Track)
song.get_tag(&Tag::TrackNumber)
.unwrap_or(&String::new())
.parse::<u16>()
.unwrap_or_default(),
@ -1171,7 +1249,7 @@ impl MusicLibrary {
discs: BTreeMap::from([(
disc_num,
vec![(
song.get_tag(&Tag::Track)
song.get_tag(&Tag::TrackNumber)
.unwrap_or(&String::new())
.parse::<u16>()
.unwrap_or_default(),
@ -1308,8 +1386,8 @@ mod test {
&vec![
Tag::Field("location".to_string()),
Tag::Album,
Tag::Disk,
Tag::Track,
Tag::DiskNumber,
Tag::TrackNumber,
],
)
.unwrap();

View file

@ -11,7 +11,6 @@ use std::time::Duration;
// use chrono::Duration;
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
use chrono::format::Item;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use thiserror::Error;

View file

@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
dmp-core = { path = "../dmp-core" }
dmp-core = { path = "../dmp-core", features = ["ts"] }
tauri = { version = "2", features = [ "protocol-asset", "unstable"] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }

View file

@ -11,7 +11,7 @@ use tauri::{AppHandle, Emitter, State, Wry};
use tempfile::TempDir;
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]
pub async fn add_song_to_queue(
@ -47,7 +47,7 @@ pub async fn play_now(
Err(e) => return Err(e.to_string()),
};
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();
Ok(())
}

View file

@ -21,12 +21,12 @@ use dmp_core::{
use parking_lot::RwLock;
use tauri::{AppHandle, Emitter, Manager, http::Response};
use uuid::Uuid;
use wrappers::{_Song, stop};
use wrappers::stop;
use crate::wrappers::{
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,
remove_from_queue, seek, set_volume, queue_move_to
get_queue, get_song, import_playlist, next, pause, play, play_next_queue, prev, queue_move_to,
remove_from_queue, seek, set_volume,
};
use commands::{
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;
while true {
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("playing", true).unwrap();
_ = now_playing.write().insert(song);

View file

@ -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 dmp_core::{
music_controller::controller::{ControllerHandle, PlayerLocation},
music_storage::{
library::{Song, Tag, URI},
library::{Song, Tag},
queue::QueueItemType,
},
};
@ -83,7 +82,7 @@ pub async fn next(
Ok(s) => s,
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("playing", true).unwrap();
Ok(())
@ -99,7 +98,7 @@ pub async fn prev(
Err(e) => return Err(e.to_string()),
};
println!("prev");
app.emit("now_playing_change", _Song::from(&song)).unwrap();
app.emit("now_playing_change", song).unwrap();
app.emit("queue_updated", ()).unwrap();
Ok(())
}
@ -112,7 +111,7 @@ pub async fn now_playing(_ctrl_handle: State<'_, ControllerHandle>) -> Result<()
#[tauri::command]
pub async fn get_queue(
ctrl_handle: State<'_, ControllerHandle>,
) -> Result<Vec<(_Song, PlayerLocation)>, String> {
) -> Result<Vec<(Song, PlayerLocation)>, String> {
Ok(ctrl_handle
.queue_get_all()
.await
@ -121,7 +120,7 @@ pub async fn get_queue(
let QueueItemType::Single(song) = item.item else {
unreachable!("There should be no albums in the queue right now")
};
(_Song::from(&song.song), song.location)
(song.song, song.location)
})
.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]
pub async fn get_library(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec<_Song>, String> {
let songs = ctrl_handle
.lib_get_all()
.await
.iter()
.map(|song| _Song::from(song))
.collect_vec();
pub async fn get_library(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec<Song>, String> {
let songs = ctrl_handle.lib_get_all().await;
Ok(songs)
}
@ -193,23 +150,17 @@ pub async fn get_library(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec
pub async fn get_playlist(
ctrl_handle: State<'_, ControllerHandle>,
uuid: Uuid,
) -> Result<Vec<_Song>, String> {
) -> Result<Vec<Song>, String> {
let playlist = match ctrl_handle.playlist_get(uuid).await {
Ok(list) => list,
Err(_) => todo!(),
};
let songs = playlist
.tracks
.iter()
.map(|song| _Song::from(song))
.collect::<Vec<_>>();
println!(
"Got Playlist {}, len {}",
playlist.title,
playlist.tracks.len()
);
Ok(songs)
Ok(playlist.tracks)
}
#[tauri::command]
@ -264,13 +215,13 @@ pub struct PlaylistPayload {
pub async fn get_song(
ctrl_handle: State<'_, ControllerHandle>,
uuid: Uuid,
) -> Result<_Song, String> {
) -> Result<Song, String> {
let song = ctrl_handle.lib_get_song(uuid).await.0;
println!(
"got song {}",
&song.tags.get(&Tag::Title).unwrap_or(&String::new())
);
Ok(_Song::from(&song))
Ok(song)
}
#[tauri::command]
@ -334,7 +285,7 @@ pub async fn queue_move_to(
match ctrl_handle.enqueue(0).await.map_err(|e| e.to_string()) {
Ok(song) => {
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();
Ok(())
}

View file

@ -1,19 +1,20 @@
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import "./App.css";
import { Config, playbackInfo } from "./types";
// import { EventEmitter } from "@tauri-apps/plugin-shell";
// import { listen } from "@tauri-apps/api/event";
// import { fetch } from "@tauri-apps/plugin-http";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { PhysicalPosition } from "@tauri-apps/api/window";
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();
type Location = "Library" | { "Playlist": string };
// This needs to be changed to properly reflect cursor position
// this will do for now.
async function contextMenuPosition(event: React.MouseEvent) {
@ -43,16 +44,16 @@ function App() {
useEffect(() => {
const unlisten = appWindow.listen<any>("now_playing_change", ({ payload, }) => {
const unlisten = appWindow.listen<Song>("now_playing_change", ({ payload, }) => {
const displayArtwork = () => {
invoke('display_album_art', { uuid: payload.uuid }).then(() => {})
}
// console.log(event);
setNowPlaying(
<NowPlaying
title={ payload.tags.TrackTitle }
album={ payload.tags.AlbumTitle }
artist={ payload.tags.TrackArtist }
title={ payload.tags.Title }
album={ payload.tags.Album }
artist={ payload.tags.Artist }
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(() => {
const unlisten = appWindow.listen<any>("queue_updated", (_) => {
const unlisten = appWindow.listen<null>("queue_updated", (_) => {
// console.log(event);
invoke('get_queue').then((_songs) => {
let songs = _songs as any[];
let songs = _songs as [Song, PlayerLocation][];
setQueue(
songs.filter((_, i) => i != 0).map((song, i) =>
<QueueSong
song={ song[0] }
location={ song[1] as "Library" | {"Playlist" : string}}
location={ song[1] }
index={i+1}
key={ Math.floor((Math.random() * 100_000_000_000) + 1) + '_' + Date.now() }
setSelectedSong={ setSelectedSongQueue }
@ -83,8 +84,8 @@ function App() {
}, []);
useEffect(() => {
const unlisten = appWindow.listen<any>("playing", (isPlaying) => {
setPlaying(isPlaying.payload as boolean)
const unlisten = appWindow.listen<boolean>("playing", (isPlaying) => {
setPlaying(isPlaying.payload)
})
return () => { unlisten.then((f) => f()) }
}, []);
@ -133,11 +134,11 @@ interface PlaylistHeadProps {
function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playlistsInfo, setSelected }: PlaylistHeadProps) {
function getPlaylist(playlist: PlaylistInfo) {
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);
const reload = () => getPlaylist(playlist)
return (
<Song
<MainViewSong
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
location={ song.location }
playerLocation={ {"Playlist" : playlist.uuid } }
@ -157,42 +158,42 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
}
useEffect(() => {
const unlisten = appWindow.listen<any[]>("playlists_gotten", (_res) => {
// console.log(event);
let res = _res.payload as PlaylistInfo[];
playlistsInfo.current = [...res];
// console.log(playlistsInfo, res);
const unlisten = appWindow.listen<PlaylistInfo[]>("playlists_gotten", (_res) => {
const res = _res.payload;
// console.log(event);
playlistsInfo.current = [...res];
// console.log(playlistsInfo, res);
setPlaylists([
...res.map( (list) => {
const _getPlaylist = () => getPlaylist(list)
const deletePlaylist = () => {
invoke('delete_playlist', { uuid: list.uuid }).then(() => {});
invoke('get_playlists').then(() => {});
}
async function menuHandler(event: React.MouseEvent) {
event.preventDefault();
const menu = await Menu.new({
items: [
{ id: "delete_playlist" + list.uuid, text: "Delete Playlist", action: deletePlaylist }
]
});
menu.popup(await contextMenuPosition(event));
}
setPlaylists([
...res.map( (list) => {
const _getPlaylist = () => getPlaylist(list)
const deletePlaylist = () => {
invoke('delete_playlist', { uuid: list.uuid }).then(() => {});
invoke('get_playlists').then(() => {});
}
async function menuHandler(event: React.MouseEvent) {
event.preventDefault();
const menu = await Menu.new({
items: [
{ id: "delete_playlist" + list.uuid, text: "Delete Playlist", action: deletePlaylist }
]
});
menu.popup(await contextMenuPosition(event));
}
return (
<button onClick={ _getPlaylist }
onContextMenu={ menuHandler }
key={ 'playlist_' + list.uuid }>{ list.name }</button>
)
})
])
return (
<button onClick={ _getPlaylist }
onContextMenu={ menuHandler }
key={ 'playlist_' + list.uuid }>{ list.name }</button>
)
})
])
})
return () => { unlisten.then((f) => f()) }
}, []);
let handle_import = () => {
invoke('import_playlist').then((_res) => {
let res = _res as any;
let res = _res as PlaylistInfo;
setPlaylists([
...playlists,
@ -207,11 +208,11 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
setViewName("Library");
invoke('get_library').then((lib) => {
let i = 0;
setLibrary([...(lib as any[]).map((song) => {
setLibrary([...(lib as Song[]).map((song) => {
// console.log(song);
i++;
return (
<Song
<MainViewSong
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
location={ song.location }
playerLocation="Library"
@ -247,7 +248,7 @@ function MainView({ lib_ref, viewName, playlistsInfo, setSelected, selectedSong
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 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(() => {
const unlisten = appWindow.listen<any>("library_loaded", (_) => {
const unlisten = appWindow.listen<null>("library_loaded", (_) => {
console.log("library_loaded");
invoke('get_library').then((lib) => {
let i = 0;
setLibrary([...(lib as any[]).map((song) => {
i++;
setLibrary([...(lib as Song[]).map((song, i) => {
console.log("duration", song.duration)
return (
<Song
<MainViewSong
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
location={ song.location }
playerLocation="Library"
@ -305,7 +305,7 @@ function MainView({ lib_ref, viewName, playlistsInfo, setSelected, selectedSong
duration={ song.duration }
tags={ song.tags }
playlists={ playlistsInfo }
index={ i - 1 }
index={ i }
setSelected={ setSelected }
/>
)
@ -331,12 +331,12 @@ function MainView({ lib_ref, viewName, playlistsInfo, setSelected, selectedSong
interface SongProps {
location: any,
playerLocation: string | {"Playlist" : any},
location: URI[],
playerLocation: PlayerLocation,
uuid: string,
plays: number,
format?: string,
duration: string,
duration: number,
last_played?: string,
date_added?: string,
date_modified?: string,
@ -347,7 +347,7 @@ interface SongProps {
reload?: () => void
}
function Song(props: SongProps) {
function MainViewSong(props: SongProps) {
// console.log(props.tags);
// useEffect(() => {
// const unlistenPromise = listen<string>("add_song_to_queue", (event) => {
@ -363,16 +363,16 @@ function Song(props: SongProps) {
// }, []);
const setSelected = () => {
props.setSelected(props);
console.log(props.tags.TrackTitle);
console.log(props.tags.Title);
}
return(
<div
onContextMenu={ setSelected }
onClick={ setSelected }
className="song">
<p className="artist unselectable">{ props.tags.TrackArtist }</p>
<p className="title unselectable">{ props.tags.TrackTitle }</p>
<p className="album unselectable">{ props.tags.AlbumTitle }</p>
<p className="artist unselectable">{ props.tags.Artist }</p>
<p className="title unselectable">{ props.tags.Title }</p>
<p className="album unselectable">{ props.tags.Album }</p>
<p className="duration unselectable">
{ Math.round(+props.duration / 60) }:
{ (+props.duration % 60).toString().padStart(2, "0") }
@ -397,8 +397,7 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
const [lastFmLoggedIn, setLastFmLoggedIn] = useState(false);
useEffect(() => {
const unlisten = appWindow.listen<any>("playback_info", ({ payload, }) => {
const info = payload as playbackInfo;
const unlisten = appWindow.listen<PlaybackInfo>("playback_info", ({ payload: info, }) => {
const pos_ = Array.isArray(info.position) ? info.position![0] : 0;
const dur_ = Array.isArray(info.duration) ? info.duration![0] : 0;
@ -463,9 +462,9 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
}
interface NowPlayingProps {
title: string,
artist: string,
album: string,
title: string | undefined,
artist: string | undefined,
album: string | undefined,
artwork: JSX.Element
}
@ -475,7 +474,7 @@ function NowPlaying({ title, artist, album, artwork }: NowPlayingProps) {
<div className="artworkWrapper unselectable">
{ artwork }
</div>
<h3>{ title }</h3>
<h3>{ title? title : "Unknown Title" }</h3>
<p>{ artist }</p>
<p>{ album }</p>
</section>
@ -491,7 +490,7 @@ interface QueueProps {
interface selectedQueueSong {
uuid: string,
index: number
location: Location,
location: PlayerLocation,
}
function Queue({ songs, selectedSong, }: QueueProps) {
@ -530,8 +529,8 @@ function Queue({ songs, selectedSong, }: QueueProps) {
}
interface QueueSongProps {
song: any,
location: Location,
song: Song,
location: PlayerLocation,
index: number,
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 }>
<img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/>
<div className="queueSongTags">
<p className="queueSongTitle">{ song.tags.TrackTitle }</p>
<p className="queueSongArtist">{ song.tags.TrackArtist }</p>
<p className="queueSongTitle">{ song.tags.Title }</p>
<p className="queueSongArtist">{ song.tags.Artist }</p>
</div>
</div>
)

4
src/bindings/AlbumArt.ts Normal file
View 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 };

View 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
View 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, };

View 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, };

View 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>, };

View 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, };

View 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";

View 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 };

View 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], };

View 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
View 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
View 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
View 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
View 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
View 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] };

View file

@ -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],
}