Downloading prototype works

This commit is contained in:
Gvidas Juknevičius 2024-11-19 14:35:33 +02:00
parent b29caa58b4
commit fda77f6981
Signed by: MCorange
GPG Key ID: 12B1346D720B7FBB
35 changed files with 1764 additions and 1257 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
/target/ /target/
/cache/
settings.toml settings.toml
valgrind.log

42
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "ab_glyph" name = "ab_glyph"
@ -729,6 +729,9 @@ name = "camino"
version = "1.1.9" version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "cc" name = "cc"
@ -4376,8 +4379,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]] [[package]]
name = "xmpd-cli" name = "xmpd-cache"
version = "2.0.0" version = "2.0.0"
dependencies = [
"anyhow",
"camino",
"lazy_static",
"log",
"uuid",
"xmpd-cliargs",
"xmpd-manifest",
"xmpd-settings",
]
[[package]]
name = "xmpd-cliargs"
version = "2.0.0"
dependencies = [
"camino",
"clap",
"dirs",
"lazy_static",
]
[[package]] [[package]]
name = "xmpd-core" name = "xmpd-core"
@ -4386,24 +4409,20 @@ dependencies = [
"anyhow", "anyhow",
"camino", "camino",
"clap", "clap",
"dirs",
"env_logger", "env_logger",
"log", "log",
"xmpd-cli", "xmpd-cliargs",
"xmpd-gui", "xmpd-gui",
"xmpd-manifest", "xmpd-manifest",
"xmpd-settings", "xmpd-settings",
] ]
[[package]]
name = "xmpd-dl"
version = "2.0.0"
[[package]] [[package]]
name = "xmpd-gui" name = "xmpd-gui"
version = "2.0.0" version = "2.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"camino",
"eframe", "eframe",
"egui 0.27.2", "egui 0.27.2",
"egui-aesthetix", "egui-aesthetix",
@ -4412,6 +4431,8 @@ dependencies = [
"log", "log",
"tokio", "tokio",
"uuid", "uuid",
"xmpd-cache",
"xmpd-cliargs",
"xmpd-manifest", "xmpd-manifest",
"xmpd-settings", "xmpd-settings",
] ]
@ -4432,12 +4453,17 @@ name = "xmpd-settings"
version = "2.0.0" version = "2.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"camino",
"egui 0.27.2", "egui 0.27.2",
"lazy_static", "lazy_static",
"serde", "serde",
"toml", "toml",
] ]
[[package]]
name = "xmpd-tooling"
version = "2.0.0"
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "3.15.2" version = "3.15.2"

View File

@ -4,9 +4,10 @@ members=[
"xmpd-core", "xmpd-core",
"xmpd-manifest", "xmpd-manifest",
"xmpd-gui", "xmpd-gui",
#"xmpd-cli", "xmpd-cliargs",
"xmpd-dl", "xmpd-cache",
"xmpd-settings", "xmpd-settings",
"xmpd-tooling",
# "xmpd-tui" # "xmpd-tui"
] ]
@ -24,7 +25,7 @@ authors=[
anstyle = "1.0.6" anstyle = "1.0.6"
anyhow = "1.0.81" anyhow = "1.0.81"
bitflags = { version = "2.6.0", features = ["serde"] } bitflags = { version = "2.6.0", features = ["serde"] }
camino = "1.1.6" camino = { version="1.1.6", features = ["serde1"] }
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
eframe = "0.27.2" eframe = "0.27.2"
egui = { version = "0.27.2", features = ["color-hex", "serde"] } egui = { version = "0.27.2", features = ["color-hex", "serde"] }

1
assets/check.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/></svg>

After

Width:  |  Height:  |  Size: 179 B

1
assets/cross.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M256-213.85 213.85-256l224-224-224-224L256-746.15l224 224 224-224L746.15-704l-224 224 224 224L704-213.85l-224-224-224 224Z"/></svg>

After

Width:  |  Height:  |  Size: 247 B

1
assets/download.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z"/></svg>

After

Width:  |  Height:  |  Size: 279 B

1
assets/warning.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="m40-120 440-760 440 760H40Zm138-80h604L480-720 178-200Zm302-40q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Zm40-100Z"/></svg>

After

Width:  |  Height:  |  Size: 315 B

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[package] [package]
name = "xmpd-dl" name = "xmpd-cache"
edition = "2021" edition = "2021"
readme="README.md" readme="README.md"
authors.workspace = true authors.workspace = true
@ -18,3 +18,11 @@ crate-type = ["rlib"]
bench = false bench = false
[dependencies] [dependencies]
xmpd-settings.path = "../xmpd-settings"
xmpd-manifest.path = "../xmpd-manifest"
xmpd-cliargs.path = "../xmpd-cliargs"
anyhow.workspace = true
camino.workspace = true
lazy_static.workspace = true
log.workspace = true
uuid.workspace = true

View File

@ -0,0 +1,4 @@
pub mod song;
pub mod icon;
pub mod metadata;

View File

@ -0,0 +1,127 @@
use std::{collections::HashMap, ffi::OsStr, process::{Command, Stdio}, sync::{Arc, Mutex, MutexGuard}};
use xmpd_manifest::song::{Song, SourceType};
lazy_static::lazy_static!(
static ref SONG_CACHE_DL: Arc<Mutex<SongCacheDl>> = Arc::default();
);
#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord)]
pub enum SongStatus {
Downloading,
Converting,
Done
}
#[derive(Debug, Default, Clone)]
pub struct SongCacheDl {
pub jobs: HashMap<uuid::Uuid, SongStatus>,
pub current_jobs: usize,
}
impl SongCacheDl {
pub fn get() -> crate::Result<MutexGuard<'static, Self>> {
match SONG_CACHE_DL.lock() {
Ok(v) => Ok(v),
Err(e) => anyhow::bail!(format!("{e:?}"))
}
}
pub fn is_job_list_full(&self) -> bool {
self.current_jobs >= 5
}
pub fn download(&mut self, sid: uuid::Uuid, song: Song) -> crate::Result<()> {
self.current_jobs += 1;
let song_format = xmpd_settings::Settings::get().unwrap().tooling.song_format.clone();
let tooling = xmpd_settings::Settings::get()?.tooling.clone();
let mut song_cache_d = xmpd_cliargs::CLIARGS.cache_path();
song_cache_d.push("songs");
match song.source_type() {
SourceType::Youtube |
SourceType::Soundcloud => {
let mut song_p = song_cache_d.clone();
song_p.push(sid.to_string());
let song_p = song_p.with_extension(&song_format);
let mut dl_cmd = Command::new(&tooling.ytdlp_path);
dl_cmd.arg(song.url().as_str());
dl_cmd.args(["-x", "--audio-format", &song_format]);
dl_cmd.arg("-o");
dl_cmd.arg(&song_p);
if xmpd_cliargs::CLIARGS.debug {
dl_cmd.stdout(Stdio::piped());
dl_cmd.stderr(Stdio::piped());
} else {
dl_cmd.stdout(Stdio::null());
dl_cmd.stderr(Stdio::null());
}
let dl_child = dl_cmd.spawn()?;
self.jobs.insert(sid, SongStatus::Downloading);
std::thread::spawn(move || {
if let Ok(output) = dl_child.wait_with_output() {
for line in String::from_utf8(output.stdout).unwrap().lines() {
log::info!("CMD: {}", line);
}
for line in String::from_utf8(output.stderr).unwrap().lines() {
log::error!("CMD: {}", line);
}
}
let mut cache = SONG_CACHE_DL.lock().unwrap();
cache.jobs.insert(sid, SongStatus::Done);
cache.current_jobs -= 1;
});
}
SourceType::Spotify => {
// Spotdl doesnt have webm as a format so its fucking annoying, oh well
let mut song_p = song_cache_d.clone();
song_p.push(sid.to_string());
song_p.push("{output-ext}");
let mut dl_cmd = Command::new(&tooling.spotdl_path);
dl_cmd.arg(song.url().as_str());
dl_cmd.arg("--ffmpeg");
dl_cmd.arg(&tooling.ffmpeg_path);
dl_cmd.args(["--format", &song_format, "--output"]);
dl_cmd.arg(&song_p);
let arg_str = dl_cmd.get_args();
let arg_str: Vec<_> = arg_str.collect();
let arg_str = arg_str.join(OsStr::new(" ")).to_string_lossy().to_string();
log::debug!("spotify cli: {} {}", tooling.spotdl_path, arg_str);
if xmpd_cliargs::CLIARGS.debug {
dl_cmd.stdout(Stdio::piped());
dl_cmd.stderr(Stdio::piped());
} else {
dl_cmd.stdout(Stdio::null());
dl_cmd.stderr(Stdio::null());
}
let child = dl_cmd.spawn()?;
self.jobs.insert(sid, SongStatus::Downloading);
std::thread::spawn(move || {
if let Ok(output) = child.wait_with_output() {
for line in String::from_utf8(output.stdout).unwrap().lines() {
log::info!("CMD: {}", line);
}
for line in String::from_utf8(output.stderr).unwrap().lines() {
log::error!("CMD: {}", line);
}
}
let mut from = song_p.clone();
from.pop();
from.push("{song_format}.{song_format}");
let mut to = song_p.clone();
to.pop();
to.set_extension(&song_format);
std::fs::copy(&from, &to).unwrap();
from.pop();
std::fs::remove_dir_all(from).unwrap();
let mut cache = SONG_CACHE_DL.lock().unwrap();
cache.jobs.insert(sid, SongStatus::Done);
cache.current_jobs -= 1;
});
}
}
Ok(())
}
}

122
xmpd-cache/src/lib.rs Normal file
View File

@ -0,0 +1,122 @@
use std::{collections::HashMap, str::FromStr, sync::{Arc, Mutex, MutexGuard}, time::Duration};
use downloader::song::SongStatus;
use xmpd_manifest::song::Song;
pub mod downloader;
type Result<T> = anyhow::Result<T>;
lazy_static::lazy_static!(
static ref CACHE: Arc<Mutex<Cache>> = Arc::new(Mutex::new(Cache::default()));
);
#[derive(Debug, Default)]
pub struct Cache {
cache_dir: camino::Utf8PathBuf,
song_cache: HashMap<uuid::Uuid, DlStatus>,
queue: Vec<(uuid::Uuid, Song)>
// TODO: Add Icon, metadata cache
}
#[derive(Debug, Clone)]
pub enum DlStatus {
Done(camino::Utf8PathBuf),
Downloading,
Error(String),
}
impl Cache {
pub fn get() -> crate::Result<MutexGuard<'static, Self>> {
match CACHE.lock() {
Ok(l) => Ok(l),
Err(e) => Err(anyhow::anyhow!(format!("{e:?}"))),
}
}
pub fn init(&mut self) -> Result<()> {
start_cache_mv_thread();
self.cache_dir = xmpd_cliargs::CLIARGS.cache_path();
{ // Get cached songs
let mut song_cache_dir = self.cache_dir.clone();
song_cache_dir.push("songs");
for file in song_cache_dir.read_dir_utf8()? {
if let Ok(file) = file {
if !file.file_type()?.is_file() {
log::warn!("Non song file in: {}", file.path());
continue;
}
let file_path = file.path();
let file2 = file_path.with_extension("");
if let Some(file_name) = file2.file_name() {
let id = uuid::Uuid::from_str(file_name)?;
log::debug!("Found song {id}");
// TODO: Check if id is in manifest
self.song_cache.insert(id, DlStatus::Done(file_path.to_path_buf()));
}
}
}
}
{ // Get cached icons
}
{ // Get Cached meta
}
Ok(())
}
pub fn download_to_cache(&mut self, sid: uuid::Uuid, song: Song) {
self.queue.push((sid, song));
self.song_cache.insert(sid, DlStatus::Downloading);
}
pub fn download_to_cache_multiple(&mut self, mut songs: Vec<(uuid::Uuid, Song)>) {
while let Some((sid, song)) = songs.pop() {
self.download_to_cache(sid, song);
}
}
pub fn get_cached_song_status(&mut self, sid: &uuid::Uuid) -> Option<DlStatus> {
Some(self.song_cache.get(sid)?.clone())
}
}
fn start_cache_mv_thread() {
std::thread::spawn(|| {
loop {
{
std::thread::sleep(Duration::from_millis(500));
let song_format = xmpd_settings::Settings::get().unwrap().tooling.song_format.clone();
let mut done_jobs = Vec::new();
let mut dlc = downloader::song::SongCacheDl::get().unwrap();
for (sid, status) in &dlc.jobs {
if *status == SongStatus::Done {
let mut cache = CACHE.lock().unwrap();
let mut song_p = xmpd_cliargs::CLIARGS.cache_path().clone();
song_p.push("songs");
song_p.push(sid.clone().to_string());
let song_p = song_p.with_extension(&song_format);
log::debug!("Found done: {:?}: {}", song_p, song_p.exists());
if song_p.exists() {
cache.song_cache.insert(sid.clone(), DlStatus::Done(song_p));
done_jobs.push(sid.clone());
}
}
}
for sid in done_jobs {
dlc.jobs.remove(&sid);
}
}
{
let mut cache = Cache::get().unwrap();
let mut dlc = downloader::song::SongCacheDl::get().unwrap();
if !dlc.is_job_list_full() {
if let Some((sid, song)) = cache.queue.pop() {
dlc.download(sid, song).unwrap();
}
}
}
}
});
}

View File

@ -1,3 +0,0 @@
pub fn test() {
println!("Hello, world!");
}

View File

@ -1,5 +1,5 @@
[package] [package]
name = "xmpd-cli" name = "xmpd-cliargs"
edition = "2021" edition = "2021"
readme="README.md" readme="README.md"
version.workspace = true version.workspace = true
@ -18,3 +18,7 @@ crate-type = ["rlib"]
bench = false bench = false
[dependencies] [dependencies]
camino.workspace = true
clap.workspace = true
dirs.workspace = true
lazy_static.workspace = true

0
xmpd-cliargs/README.md Normal file
View File

View File

@ -1,5 +1,11 @@
use std::{path::PathBuf, str::FromStr}; use std::{path::PathBuf, str::FromStr, sync::Arc};
use camino::Utf8PathBuf;
use clap::Parser;
lazy_static::lazy_static!(
pub static ref CLIARGS: Arc<CliArgs> = Arc::new(CliArgs::parse());
);
#[derive(Debug, clap::Parser)] #[derive(Debug, clap::Parser)]
pub struct CliArgs { pub struct CliArgs {
@ -24,8 +30,8 @@ impl CliArgs {
pub fn settings_path(&self) -> PathBuf { pub fn settings_path(&self) -> PathBuf {
self.settings.clone().into_std_path_buf() self.settings.clone().into_std_path_buf()
} }
pub fn cache_path(&self) -> PathBuf { pub fn cache_path(&self) -> Utf8PathBuf {
self.cache.clone().into_std_path_buf() self.cache.clone()
} }
} }

View File

@ -21,7 +21,7 @@ name="xmpd"
path="src/main.rs" path="src/main.rs"
[dependencies] [dependencies]
xmpd-cli.path="../xmpd-cli" xmpd-cliargs.path="../xmpd-cliargs"
xmpd-gui.path="../xmpd-gui" xmpd-gui.path="../xmpd-gui"
xmpd-manifest.path="../xmpd-manifest" xmpd-manifest.path="../xmpd-manifest"
xmpd-settings.path = "../xmpd-settings" xmpd-settings.path = "../xmpd-settings"
@ -30,4 +30,3 @@ camino.workspace = true
anyhow.workspace = true anyhow.workspace = true
log.workspace = true log.workspace = true
env_logger.workspace = true env_logger.workspace = true
dirs.workspace = true

View File

@ -1,7 +1,5 @@
use log::LevelFilter; use log::LevelFilter;
use xmpd_cliargs::CliArgs;
use crate::cli::CliArgs;
pub fn init(cliargs: &CliArgs) { pub fn init(cliargs: &CliArgs) {

View File

@ -1,15 +1,18 @@
use std::borrow::BorrowMut;
use clap::Parser; use clap::Parser;
mod cli;
mod logger; mod logger;
type Result<T> = anyhow::Result<T>; type Result<T> = anyhow::Result<T>;
fn main() -> Result<()> { fn main() -> Result<()> {
let cliargs = cli::CliArgs::parse(); // NOTE: Parses on first load
let cliargs = &xmpd_cliargs::CLIARGS;
logger::init(&cliargs); logger::init(&cliargs);
log::debug!("Cli: {cliargs:?}"); log::debug!("Initialising settings");
xmpd_settings::Settings::get()?.load(Some(cliargs.settings_path()))?; xmpd_settings::Settings::get()?.load(Some(cliargs.settings_path()))?;
xmpd_gui::start(cliargs.manifest_path())?; log::debug!("Starting gui");
xmpd_gui::start()?;
Ok(()) Ok(())
} }

View File

@ -20,6 +20,8 @@ bench = false
[dependencies] [dependencies]
xmpd-manifest.path = "../xmpd-manifest" xmpd-manifest.path = "../xmpd-manifest"
xmpd-settings.path = "../xmpd-settings" xmpd-settings.path = "../xmpd-settings"
xmpd-cliargs.path = "../xmpd-cliargs"
xmpd-cache.path = "../xmpd-cache"
egui.workspace = true egui.workspace = true
eframe.workspace = true eframe.workspace = true
tokio.workspace = true tokio.workspace = true
@ -29,3 +31,4 @@ log.workspace = true
egui_extras.workspace = true egui_extras.workspace = true
egui-aesthetix = "0.2.4" egui-aesthetix = "0.2.4"
uuid.workspace = true uuid.workspace = true
camino.workspace = true

View File

@ -1,5 +1,8 @@
use egui::{RichText, Vec2}; use std::fmt::write;
use egui::{Color32, RichText, Sense, Vec2};
use song_list_nav::SearchType; use song_list_nav::SearchType;
use xmpd_cache::DlStatus;
use xmpd_manifest::{song::Song, store::BaseStore}; use xmpd_manifest::{song::Song, store::BaseStore};
use super::{CompGetter, CompUi}; use super::{CompGetter, CompUi};
@ -80,34 +83,92 @@ impl CompUi for SongList {
fn display_song_tab(ui: &mut egui::Ui, sid: &uuid::Uuid, song: &Song) { fn display_song_tab(ui: &mut egui::Ui, sid: &uuid::Uuid, song: &Song) {
let theme = handle_error_ui!(xmpd_settings::Settings::get()).theme.clone(); let theme = handle_error_ui!(xmpd_settings::Settings::get()).theme.clone();
let rct = ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add( let mut clicked = ui.add(
egui::Image::new(crate::data::NOTE_ICON) egui::Image::new(crate::data::NOTE_ICON)
.tint(theme.accent_color) .tint(theme.accent_color)
.sense(Sense::click())
.fit_to_exact_size(Vec2::new(32.0, 32.0)) .fit_to_exact_size(Vec2::new(32.0, 32.0))
); ).clicked();
ui.vertical(|ui| { ui.vertical(|ui| {
let selected_song_id = {handle_error_ui!(SongList::get()).selected_song_id}; let selected_song_id = {handle_error_ui!(SongList::get()).selected_song_id};
if selected_song_id == *sid { let label = if selected_song_id == *sid {
ui.label( ui.label(
RichText::new(song.name()) RichText::new(song.name())
.color(theme.accent_color) .color(theme.accent_color)
); )
} else { } else {
ui.label( ui.label(
RichText::new(song.name()) RichText::new(song.name())
.color(theme.text_color) .color(theme.text_color)
); )
}; };
if label.clicked() {
clicked = true;
}
ui.monospace( ui.monospace(
RichText::new(format!("By {}", song.author())) RichText::new(format!("By {}", song.author()))
.color(theme.dim_text_color) .color(theme.dim_text_color)
.size(10.0) .size(10.0)
); );
}); });
}).response.rect;
if ui.interact(rct, format!("song_list_{sid:?}").into(), egui::Sense::click()).clicked() { ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| {
ui.add_space(3.0);
let status = {
handle_error_ui!(xmpd_cache::Cache::get()).get_cached_song_status(&sid).clone()
};
match status {
Some(DlStatus::Done(p)) => {
let img = ui.add(
egui::Image::new(crate::data::CHECK_ICON)
.tint(Color32::LIGHT_GREEN)
.sense(Sense::hover())
.fit_to_exact_size(Vec2::new(16.0, 16.0))
);
img.on_hover_ui(|ui| {
ui.label(format!("Path: {p}"));
});
}
Some(DlStatus::Downloading) => {
ui.add(
egui::Spinner::new()
.color(theme.accent_color)
.size(16.0)
);
}
Some(DlStatus::Error(e)) => {
let img = ui.add(
egui::Image::new(crate::data::WARN_ICON)
.tint(Color32::LIGHT_YELLOW)
.sense(Sense::hover())
.fit_to_exact_size(Vec2::new(16.0, 16.0))
);
img.on_hover_ui(|ui| {
ui.label(e);
});
}
None => {
let img = ui.add(
egui::Image::new(crate::data::DL_ICON)
.tint(theme.accent_color)
.sense(Sense::click())
.fit_to_exact_size(Vec2::new(16.0, 16.0))
);
if img.clicked() {
handle_error_ui!(xmpd_cache::Cache::get()).download_to_cache(sid.clone(), song.clone())
}
}
}
});
if clicked {
handle_error_ui!(SongList::get()).selected_song_id = sid.clone(); handle_error_ui!(SongList::get()).selected_song_id = sid.clone();
} }
});
ui.separator(); ui.separator();
} }

View File

@ -1,4 +1,7 @@
use crate::components::{CompGetter, CompUi}; use uuid::Uuid;
use xmpd_manifest::store::BaseStore;
use crate::components::{left_nav::LeftNav, CompGetter, CompUi};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum SearchType { pub enum SearchType {
@ -9,14 +12,16 @@ pub enum SearchType {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct SongListNav { pub struct SongListNav {
text: String text: String,
} }
component_register!(SongListNav); component_register!(SongListNav);
impl CompUi for SongListNav { impl CompUi for SongListNav {
fn draw(ui: &mut egui::Ui, _: &mut crate::GuiState) -> crate::Result<()> { fn draw(ui: &mut egui::Ui, state: &mut crate::GuiState) -> crate::Result<()> {
let theme = xmpd_settings::Settings::get()?.theme.clone(); let theme = xmpd_settings::Settings::get()?.theme.clone();
let pid = {LeftNav::get()?.selected_playlist_id.clone()};
ui.horizontal(|ui| { ui.horizontal(|ui| {
let search_icon = egui::Image::new(crate::data::SEARCH_ICON) let search_icon = egui::Image::new(crate::data::SEARCH_ICON)
.fit_to_exact_size(egui::Vec2::new(16.0, 16.0)) .fit_to_exact_size(egui::Vec2::new(16.0, 16.0))
@ -25,6 +30,31 @@ impl CompUi for SongListNav {
{ {
ui.text_edit_singleline(&mut handle_error_ui!(SongListNav::get()).text); ui.text_edit_singleline(&mut handle_error_ui!(SongListNav::get()).text);
} }
ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| {
let img = ui.add(
egui::Image::new(crate::data::DL_ICON)
.tint(theme.accent_color)
.sense(egui::Sense::click())
.fit_to_exact_size(egui::Vec2::new(16.0, 16.0))
);
if img.clicked() {
let songs: Vec<_>;
match pid {
Some(pid) => {
songs = state.manifest.store().get_playlist(&pid).unwrap().songs().to_vec();
}
None => {
songs = state.manifest.store().get_songs().keys().cloned().collect();
}
}
for sid in &songs {
if let Some(song) = state.manifest.store().get_song(&sid) {
handle_error_ui!(xmpd_cache::Cache::get()).download_to_cache(sid.clone(), song.clone())
}
}
}
});
}); });
Ok(()) Ok(())
} }

View File

@ -14,16 +14,19 @@ impl CompUi for TopNav {
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
if ui.button("Settings").clicked() { if ui.button("Settings").clicked() {
state.windows.toggle(&WindowId::Settings, true); state.windows.toggle(&WindowId::Settings, true);
ui.close_menu();
} }
}); });
ui.menu_button("Manifest", |ui| { ui.menu_button("Manifest", |ui| {
if ui.button("Save").clicked() { if ui.button("Save").clicked() {
handle_error_ui!(state.manifest.save()); handle_error_ui!(state.manifest.save());
ui.close_menu();
} }
}); });
ui.menu_button("Help", |ui| { ui.menu_button("Help", |ui| {
if ui.button("Source").clicked() { if ui.button("Source").clicked() {
ui.ctx().open_url(egui::OpenUrl::new_tab("https://git.mcorangehq.xyz/XOR64/music")); ui.ctx().open_url(egui::OpenUrl::new_tab("https://git.mcorangehq.xyz/XOR64/music"));
ui.close_menu();
} }
}); });

View File

@ -6,3 +6,6 @@ pub const PREV_ICON: egui::ImageSource = egui::include_image!("../../assets/pre
pub const NEXT_ICON: egui::ImageSource = egui::include_image!("../../assets/next.svg"); pub const NEXT_ICON: egui::ImageSource = egui::include_image!("../../assets/next.svg");
pub const PLAY_ICON: egui::ImageSource = egui::include_image!("../../assets/play.svg"); pub const PLAY_ICON: egui::ImageSource = egui::include_image!("../../assets/play.svg");
pub const PAUSE_ICON: egui::ImageSource = egui::include_image!("../../assets/pause.svg"); pub const PAUSE_ICON: egui::ImageSource = egui::include_image!("../../assets/pause.svg");
pub const CHECK_ICON: egui::ImageSource = egui::include_image!("../../assets/check.svg");
pub const DL_ICON: egui::ImageSource = egui::include_image!("../../assets/download.svg");
pub const WARN_ICON: egui::ImageSource = egui::include_image!("../../assets/warning.svg");

View File

@ -1,6 +1,6 @@
#![feature(async_closure)] #![feature(async_closure)]
use std::{path::{Path, PathBuf}, time::Duration}; use std::time::Duration;
use xmpd_manifest::{store::JsonStore, Manifest}; use xmpd_manifest::{store::JsonStore, Manifest};
#[macro_use] #[macro_use]
@ -15,10 +15,10 @@ const W_NAME: &str = "xmpd v2.0.0a";
type Result<T> = anyhow::Result<T>; type Result<T> = anyhow::Result<T>;
pub fn start(manifest_path: PathBuf) -> Result<()> { pub fn start() -> Result<()> {
xmpd_cache::Cache::get()?.init();
let options = eframe::NativeOptions::default(); let options = eframe::NativeOptions::default();
let mut state = GuiState::new(&manifest_path)?; let mut state = GuiState::new()?;
let theme = xmpd_settings::Settings::get()?.theme.clone();
let res = eframe::run_simple_native(W_NAME, options, move |ctx, _frame| { let res = eframe::run_simple_native(W_NAME, options, move |ctx, _frame| {
egui_extras::install_image_loaders(ctx); egui_extras::install_image_loaders(ctx);
state.windows.clone().draw_all(ctx, &mut state); state.windows.clone().draw_all(ctx, &mut state);
@ -31,20 +31,15 @@ pub fn start(manifest_path: PathBuf) -> Result<()> {
Ok(()) Ok(())
} }
pub enum Message {
}
pub struct GuiState { pub struct GuiState {
pub manifest: Manifest<JsonStore>, pub manifest: Manifest<JsonStore>,
pub windows: windows::Windows pub windows: windows::Windows,
} }
impl GuiState { impl GuiState {
pub fn new(manifest_path: &Path) -> Result<Self> { pub fn new() -> Result<Self> {
Ok(Self { Ok(Self {
manifest: Manifest::new(manifest_path)?, manifest: Manifest::new(&xmpd_cliargs::CLIARGS.manifest_path())?,
windows: windows::Windows::new(), windows: windows::Windows::new(),
}) })
} }

View File

@ -1,6 +1,6 @@
use xmpd_settings::theme::Theme; use xmpd_settings::theme::Theme;
use crate::{components::CompUi, GuiState}; use crate::{components::{song_list, CompUi}, GuiState};
pub fn draw(ctx: &egui::Context, state: &mut GuiState) -> crate::Result<()> { pub fn draw(ctx: &egui::Context, state: &mut GuiState) -> crate::Result<()> {
// The central panel the region left after adding TopPanel's and SidePanel's // The central panel the region left after adding TopPanel's and SidePanel's
@ -22,19 +22,22 @@ pub fn draw(ctx: &egui::Context, state: &mut GuiState) -> crate::Result<()> {
let avail = ui.available_size(); let avail = ui.available_size();
let main_height = avail.y * 0.91; let main_height = avail.y * 0.91;
let left_nav_width = (avail.x * 0.25).clamp(0.0, 200.0);
let song_list_width = (avail.x - left_nav_width - 35.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.set_height(main_height); ui.set_height(main_height);
ui.group(|ui| { ui.group(|ui| {
ui.set_height(main_height); ui.set_height(main_height);
ui.set_max_width((avail.x * 0.25).clamp(0.0, 200.0)); ui.set_max_width(left_nav_width);
handle_error_ui!(crate::components::left_nav::LeftNav::draw(ui, state)); handle_error_ui!(crate::components::left_nav::LeftNav::draw(ui, state));
}); });
ui.vertical(|ui| { ui.vertical(|ui| {
ui.group(|ui| { ui.group(|ui| {
ui.set_width(avail.x * 0.75); ui.set_width(song_list_width);
handle_error_ui!(crate::components::song_list::song_list_nav::SongListNav::draw(ui, state)); handle_error_ui!(crate::components::song_list::song_list_nav::SongListNav::draw(ui, state));
}); });
ui.group(|ui| { ui.group(|ui| {
ui.set_width(song_list_width);
handle_error_ui!(crate::components::song_list::SongList::draw(ui, state)); handle_error_ui!(crate::components::song_list::SongList::draw(ui, state));
}); });
}); });
@ -58,7 +61,7 @@ fn get_themed_frame(theme: &Theme) -> egui::Frame {
egui::Frame::none() egui::Frame::none()
.fill(theme.primary_bg_color) .fill(theme.primary_bg_color)
.stroke(egui::Stroke::new( .stroke(egui::Stroke::new(
1.0, 5.0,
theme.secondary_bg_color, theme.secondary_bg_color,
)) ))
} }

View File

@ -1,32 +1,37 @@
use std::str::FromStr;
use super::Window; use super::Window;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SettingsW { pub struct SettingsW {
accent_color: egui::Color32, ytdlp_p: String,
primary_bg_color: egui::Color32, spotdl_p: String,
secondary_bg_color: egui::Color32, ffmpeg_p: String,
text_color: egui::Color32, song_fmt: String,
dim_text_color: egui::Color32,
} }
impl Default for SettingsW { impl Default for SettingsW {
fn default() -> Self { fn default() -> Self {
let def = xmpd_settings::theme::Theme::default(); let tooling = xmpd_settings::Settings::get().unwrap().tooling.clone();
Self { Self {
accent_color: def.accent_color, ytdlp_p: tooling.ytdlp_path.to_string(),
primary_bg_color: def.primary_bg_color, spotdl_p: tooling.spotdl_path.to_string(),
secondary_bg_color: def.secondary_bg_color, ffmpeg_p: tooling.ffmpeg_path.to_string(),
text_color: def.text_color, song_fmt: tooling.song_format
dim_text_color: def.dim_text_color
} }
} }
} }
impl Window for SettingsW { impl Window for SettingsW {
#[allow(irrefutable_let_patterns)]
fn draw(&mut self, ui: &mut egui::Ui, _: &mut crate::GuiState) -> crate::Result<()> { fn draw(&mut self, ui: &mut egui::Ui, _: &mut crate::GuiState) -> crate::Result<()> {
let theme = &mut xmpd_settings::Settings::get()?.theme; ui.group(|ui| {
ui.vertical(|ui| {
ui.horizontal(|ui| {
{
let theme = &mut handle_error_ui!(xmpd_settings::Settings::get()).theme;
ui.group(|ui| { ui.group(|ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.heading("Theme"); ui.heading("Theme");
@ -35,6 +40,41 @@ impl Window for SettingsW {
Self::add_theme_button(&mut theme.secondary_bg_color, ui, "Secondary BG"); Self::add_theme_button(&mut theme.secondary_bg_color, ui, "Secondary BG");
Self::add_theme_button(&mut theme.text_color, ui, "Text"); Self::add_theme_button(&mut theme.text_color, ui, "Text");
Self::add_theme_button(&mut theme.dim_text_color, ui, "Dim Text"); Self::add_theme_button(&mut theme.dim_text_color, ui, "Dim Text");
if ui.button("Reset").clicked() {
*theme = xmpd_settings::theme::Theme::default();
}
});
});
}
{
ui.group(|ui| {
ui.vertical(|ui| {
ui.heading("Tooling paths");
Self::add_tooling_input(&mut self.ytdlp_p, ui, "stdlp");
Self::add_tooling_input(&mut self.spotdl_p, ui, "spotdl");
Self::add_tooling_input(&mut self.ffmpeg_p, ui, "ffmpeg");
Self::add_tooling_input(&mut self.song_fmt, ui, "Format");
});
});
}
});
ui.with_layout(egui::Layout::bottom_up(egui::Align::RIGHT), |ui| {
if ui.button("Save").clicked() {
let mut settings = handle_error_ui!(xmpd_settings::Settings::get());
if let Ok(p) = camino::Utf8PathBuf::from_str(&self.ytdlp_p) {
settings.tooling.ytdlp_path = p;
}
if let Ok(p) = camino::Utf8PathBuf::from_str(&self.spotdl_p) {
settings.tooling.spotdl_path = p;
}
if let Ok(p) = camino::Utf8PathBuf::from_str(&self.ffmpeg_p) {
settings.tooling.ffmpeg_path = p;
}
settings.tooling.song_format.clone_from(&self.song_fmt);
handle_error_ui!(settings.save(None));
}
});
}); });
}); });
Ok(()) Ok(())
@ -48,4 +88,10 @@ impl SettingsW {
ui.color_edit_button_srgba(rf); ui.color_edit_button_srgba(rf);
}); });
} }
fn add_tooling_input(inp: &mut String, ui: &mut egui::Ui, name: &str) {
ui.horizontal(|ui|{
ui.label(format!("{name}: "));
ui.text_edit_singleline(inp);
});
}
} }

View File

@ -8,6 +8,7 @@ authors.workspace = true
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
camino.workspace = true
egui.workspace = true egui.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
serde.workspace = true serde.workspace = true

View File

@ -1,8 +1,10 @@
use std::{path::PathBuf, sync::{Arc, Mutex, MutexGuard}}; use std::{path::PathBuf, sync::{Arc, Mutex, MutexGuard}};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use theme::Theme; use theme::Theme;
use tooling::Tooling;
pub mod theme; pub mod theme;
pub mod tooling;
lazy_static::lazy_static!( lazy_static::lazy_static!(
static ref SETTINGS: Arc<Mutex<Settings>> = Arc::new(Mutex::new(Settings::default())); static ref SETTINGS: Arc<Mutex<Settings>> = Arc::new(Mutex::new(Settings::default()));
@ -14,7 +16,10 @@ pub type Result<T> = anyhow::Result<T>;
pub struct Settings { pub struct Settings {
#[serde(skip)] #[serde(skip)]
settings_path: PathBuf, settings_path: PathBuf,
#[serde(default)]
pub theme: Theme, pub theme: Theme,
#[serde(default)]
pub tooling: Tooling,
} }
impl Settings { impl Settings {
@ -27,6 +32,11 @@ impl Settings {
pub fn load(&mut self, path: Option<PathBuf>) -> Result<()> { pub fn load(&mut self, path: Option<PathBuf>) -> Result<()> {
let path = path.unwrap_or(self.settings_path.clone()); let path = path.unwrap_or(self.settings_path.clone());
if !path.exists() { if !path.exists() {
let mut dir = path.clone();
dir.pop();
if !dir.exists() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(&path, "[theme]")?; std::fs::write(&path, "[theme]")?;
self.save(Some(path.clone()))?; self.save(Some(path.clone()))?;
} }

View File

@ -0,0 +1,42 @@
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tooling {
#[serde(default="Tooling::default_ytdlp_path")]
pub ytdlp_path: camino::Utf8PathBuf,
#[serde(default="Tooling::default_spotdl_path")]
pub spotdl_path: camino::Utf8PathBuf,
#[serde(default="Tooling::default_ffmpeg_path")]
pub ffmpeg_path: camino::Utf8PathBuf,
#[serde(default="Tooling::default_song_format")]
pub song_format: String,
}
impl Default for Tooling {
fn default() -> Self {
Self {
ytdlp_path: Self::default_ytdlp_path(),
spotdl_path: Self::default_spotdl_path(),
ffmpeg_path: Self::default_ffmpeg_path(),
song_format: Self::default_song_format(),
}
}
}
impl Tooling {
fn default_ytdlp_path() -> camino::Utf8PathBuf {
camino::Utf8PathBuf::from_str("yt-dlp").unwrap()
}
fn default_spotdl_path() -> camino::Utf8PathBuf {
camino::Utf8PathBuf::from_str("spotdl").unwrap()
}
fn default_ffmpeg_path() -> camino::Utf8PathBuf {
camino::Utf8PathBuf::from_str("ffmpeg").unwrap()
}
fn default_song_format() -> String {
String::from("flac")
}
}

9
xmpd-tooling/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "xmpd-tooling"
edition = "2021"
version.workspace = true
repository.workspace = true
license.workspace = true
authors.workspace = true
[dependencies]

0
xmpd-tooling/src/lib.rs Normal file
View File