Compare commits

..

No commits in common. "devel" and "master" have entirely different histories.

29 changed files with 3 additions and 834 deletions

View File

@ -21,10 +21,6 @@ jobs:
run: | run: |
rustup component add rustfmt rustup component add rustfmt
rustup component add clippy rustup component add clippy
- name: install additional dependencies
run: |
apt update -y
apt install libssl -y
- name: config custom registry - name: config custom registry
run: | run: |
mkdir -p ~/.cargo/ mkdir -p ~/.cargo/

View File

@ -22,10 +22,6 @@ jobs:
run: | run: |
rustup component add rustfmt rustup component add rustfmt
rustup component add clippy rustup component add clippy
- name: install additional dependencies
run: |
apt update -y
apt install libssl -y
- name: config custom registry - name: config custom registry
run: | run: |
mkdir -p ~/.cargo/ mkdir -p ~/.cargo/

View File

@ -11,30 +11,8 @@ homepage = "https://git.cscherr.de/PlexSheep/beatbear"
repository = "https://git.cscherr.de/PlexSheep/beatbear" repository = "https://git.cscherr.de/PlexSheep/beatbear"
keywords = ["media", "sound", "music", "player", "jellyfin", "downloads"] keywords = ["media", "sound", "music", "player", "jellyfin", "downloads"]
[features]
default = ["backend-fs", "backend-jellyfin"]
backend-fs = []
backend-jellyfin = []
[dependencies] [dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.16", features = ["derive"] }
directories = "5.0.1"
eframe = { version = "0.28.1", optional = false }
egui = { version = "0.28.1", optional = false }
egui_extras = { version = "0.28.1", features = ["image"] }
human-panic = "2.0.1"
image = { version = "0.25.2", default-features = true, features = [
"jpeg",
"png",
] }
libpt = { version = "0.6.0", features = ["cli", "full"] }
rmp-serde = "1.3.0"
serde = { version = "1.0.208", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
thiserror = "1.0.63"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[lib] [lib]
name = "beatbaer" name = "beatbaer"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,7 +0,0 @@
[package]
name = "play_file_url"
version = "0.1.0"
edition = "2021"
[dependencies]
rodio = { version = "0.19.0", features = ["symphonia-all"] }

View File

@ -1,37 +0,0 @@
use std::fs::File;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use rodio::{Decoder, OutputStream, Source};
fn main() {
const URL: &str = "file:///home/plex/Musik/Plex/.mp3";
let f = open_file_url(URL).unwrap();
let reader = std::io::BufReader::new(f);
// Get an output stream handle to the default physical sound device
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
// Decode that sound file into a source
let source = Decoder::new(reader).unwrap();
// Play the sound directly on the device
stream_handle.play_raw(source.convert_samples()).unwrap();
std::thread::sleep(Duration::from_secs(10)); // should be fine, because tokio says
// spawn_blocking happens in another thread
}
fn open_file_url(url: &str) -> std::io::Result<File> {
if !url.starts_with("file://") {
return Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "bad"));
}
let path = url.replacen("file://", "", 1);
if let Ok(path) = PathBuf::from_str(&path) {
let f = std::fs::File::open(path)?;
Ok(f)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"not a path even without file://",
))
}
}

View File

@ -1,10 +0,0 @@
[package]
name = "try_stream"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = "0.12.7"
rodio = { version = "0.19.0", features = ["symphonia-all"] }
stream-download = { version = "0.7.2", features = ["reqwest-native-tls"] }
tokio = { version = "1.39.3", features = ["rt", "macros"] }

View File

@ -1,39 +0,0 @@
use std::error::Error;
use std::result::Result;
use std::time::Duration;
use reqwest::header::HeaderMap;
use reqwest::Client;
use rodio::{Decoder, OutputStream, Source};
use stream_download::http::HttpStream;
use stream_download::storage::memory::MemoryStorageProvider;
use stream_download::{Settings, StreamDownload};
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
const URL: &str = "https://upload.wikimedia.org/wikipedia/commons/4/48/Aadama.mp3";
let mut headers = HeaderMap::new();
headers.append("User-Agent", "Beatbär Streaming Demo".parse().unwrap());
let client = Client::builder().default_headers(headers).build()?;
let stream = HttpStream::new(client, URL.parse()?).await?;
let reader =
StreamDownload::from_stream(stream, MemoryStorageProvider, Settings::default()).await?;
// now play with rodio, or read it out
tokio::task::spawn_blocking(move || {
// Get an output stream handle to the default physical sound device
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
// Decode that sound file into a source
let source = Decoder::new(reader).unwrap();
// Play the sound directly on the device
stream_handle.play_raw(source.convert_samples()).unwrap();
std::thread::sleep(Duration::from_secs(10)); // should be fine, because tokio says
// spawn_blocking happens in another thread
})
.await?;
Ok(())
}

View File

@ -1 +0,0 @@
pub trait Backend {}

View File

@ -1,28 +0,0 @@
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Error in the player UI")]
UiError(#[from] eframe::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
#[error(
r"The system does not seem to have the usual project dirs, like:
Linux: /home/alice/.config/barapp
MS Windows: C:\Users\Alice\AppData\Roaming\Foo Corp\Bar App\config
Mac: /Users/Alice/Library/Application Support/com.Foo-Corp.Bar-App
Closing as to not break your files accidentally."
)]
NoProjDir,
#[error(r"The store file '{0}' where Beatbär stores it's metadata is not a regular file, refusing to save or load to keep your files safe.")]
BadStoreFile(PathBuf),
#[error(r"Could not convert the store to the binary format for saving it to the disk.")]
RmpEncode(#[from] rmp_serde::encode::Error),
#[error(r"Could not decode the store from the binary format for loading it from the disk. Is your store file broken?")]
RmpDecode(#[from] rmp_serde::decode::Error),
#[error(r"Error while reading or writing to the disk. Source: {0}")]
IO(#[from] std::io::Error),
}

View File

@ -1,5 +1 @@
pub mod backend;
pub mod error;
pub mod music;
pub mod player;
pub mod store;

View File

@ -1,31 +1,3 @@
use beatbaer::error::Error; fn main() {
use beatbaer::player::Player; println!("Hello, world!");
use human_panic::{setup_panic, Metadata};
use libpt::log::info;
fn main() -> Result<(), Error> {
setup_panic!(
Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
.authors(env!("CARGO_PKG_AUTHORS"))
.homepage(env!("CARGO_PKG_HOMEPAGE"))
.support(format!(
"Public issue tracker at: {}\nor alternatively email: {}",
env!("CARGO_PKG_REPOSITORY"),
"software@cscherr.de"
))
);
let mut player = Player::build()?;
info!("starting ui");
eframe::run_native(
beatbaer::player::TITLE,
player.gui_options.clone(),
Box::new(|cc| {
player.init(cc);
Ok(Box::new(player))
}),
)?;
info!("leaving ui");
Ok(())
} }

View File

@ -1 +0,0 @@

View File

@ -1,119 +0,0 @@
use clap::Parser;
use eframe::CreationContext;
use egui::Stroke;
use libpt::cli::args::VerbosityLevel;
use libpt::log::{debug, info};
use crate::error::Error;
use crate::store::Store;
use self::ui::details::Details;
use self::ui::entry::{Entry, Kind};
use self::ui::mainpanel::MainPanel;
pub mod ui;
pub const TITLE: &str = "Beatbär";
#[derive(Parser)]
pub struct Player {
#[command(flatten)]
verbosity: VerbosityLevel,
#[clap(skip)]
pub gui_options: eframe::NativeOptions,
#[clap(skip)]
show_info_window: bool,
#[clap(skip)]
kind: Kind,
#[clap(skip)]
store: Option<Store>,
#[clap(skip)]
active_main_panel: MainPanel,
}
impl Player {
#[inline]
pub fn store(&self) -> &Store {
&self.store.as_ref().unwrap()
}
pub fn build() -> Result<Self, Error> {
let mut app = Player::parse();
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(app.verbosity.level().into())
.from_env()
.expect("could not init logger")
.add_directive(
format!("{}={}", env!("CARGO_PKG_NAME"), app.verbosity.level())
.parse()
.expect("could not init logger"),
);
tracing_subscriber::fmt::Subscriber::builder()
.with_env_filter(filter)
.with_max_level(app.verbosity.level())
.init();
debug!("logging initialized!");
debug!("level: {}", app.verbosity.level());
app.gui_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([900.0, 700.0])
.with_min_inner_size([500.0, 320.0])
.with_icon(Player::load_icon()),
..Default::default()
};
debug!("main panel on start: {:#?}", app.active_main_panel);
info!("Player ready to start UI");
Ok(app)
}
pub fn init(&mut self, _cc: &CreationContext) {
self.store = Some(Store::load().expect("could not load store"));
}
fn set_category(&mut self, kind: Kind) {
self.kind = kind;
match self.kind {
Kind::Album | Kind::Playlist => {
self.set_active_main_panel(MainPanel::Details(Default::default()))
}
Kind::Genre | Kind::Artist | Kind::Overview => {
self.set_active_main_panel(MainPanel::Overview(Default::default()))
}
Kind::Song => {
unreachable!()
}
}
}
fn category(&self) -> Kind {
self.kind
}
fn open(&mut self, entry: &Entry) {
todo!()
}
fn entries(&self) -> Vec<Entry> {
match self.category() {
Kind::Overview => self.store().albums(),
Kind::Album => self.store().albums(),
Kind::Song => self.store().songs(),
Kind::Playlist => self.store().playlists(),
Kind::Artist => self.store().artists(),
Kind::Genre => self.store().genres(),
}
}
pub fn end(&mut self) -> Result<(), Error> {
self.store().save()
}
}

View File

@ -1,58 +0,0 @@
use egui::text::LayoutJob;
use egui::{Color32, FontFamily, FontId, Label, TextFormat};
use super::Entry;
#[derive(Clone, Default, PartialEq)]
pub struct Details {
parent: Entry,
frame: egui::Frame,
}
impl Details {
pub fn new(parent: Entry) -> Self {
let frame = egui::Frame::none();
Self {
parent: parent.to_owned(),
frame,
}
}
pub(crate) fn ui(&self, ui: &mut egui::Ui, ctx: &egui::Context, player: &mut super::Player) {
ui.label("todo");
}
}
impl egui::Widget for Details {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
ui.vertical_centered(|ui| {
let r = ui.add(
egui::Image::new(self.parent.img())
.rounding(5.0)
.bg_fill(Color32::DARK_GRAY)
.shrink_to_fit()
.maintain_aspect_ratio(true),
);
let mut job = LayoutJob::default();
job.append(
&self.parent.title,
0.0,
TextFormat {
font_id: FontId::new(14.0, FontFamily::Proportional),
..Default::default()
},
);
r.union(ui.add(Label::new(job)));
r.union(ui.label(&self.parent.subtitle));
})
.response
}
}
impl std::fmt::Debug for Details {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Details")
.field("parent", &self.parent)
.finish()
}
}

View File

@ -1,66 +0,0 @@
use egui::ImageSource;
use strum::EnumIter;
#[derive(Hash, Debug, Clone, PartialEq, Eq, Copy, Default, EnumIter)]
pub enum Kind {
Playlist,
#[default]
Album,
Song,
Artist,
Genre,
Overview,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Entry {
pub kind: Kind,
pub title: String,
pub subtitle: String,
/// image url because I can't be bothered to use lifetimes everywhere
pub img_url: Option<String>,
}
impl Entry {
pub fn new(kind: Kind, title: &str, subtitle: &str, img: Option<String>) -> Self {
Self {
kind,
title: title.to_owned(),
subtitle: subtitle.to_owned(),
img_url: img,
}
}
pub fn img(&self) -> ImageSource {
if let Some(url) = &self.img_url {
ImageSource::Uri(std::borrow::Cow::from(url))
} else {
ImageSource::Bytes {
uri: std::borrow::Cow::Borrowed("builtin://icon_256.jpg"),
bytes: egui::load::Bytes::Static(super::ICON_RAW),
}
}
}
}
impl std::fmt::Display for Entry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.title)
}
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Song => "Song",
Self::Overview => "Overview",
Self::Genre => "Genre",
Self::Album => "Album",
Self::Playlist => "Playlist",
Self::Artist => "Artist",
}
)
}
}

View File

@ -1,26 +0,0 @@
use egui_extras::{Column, TableBuilder};
use super::details::Details;
use super::overview::Overview;
use super::Player;
#[derive(Clone, PartialEq, Debug)]
pub enum MainPanel {
Details(Details),
Overview(Overview),
}
impl MainPanel {
pub(crate) fn ui(&self, ui: &mut egui::Ui, ctx: &egui::Context, player: &mut Player) {
match self {
Self::Overview(ov) => ov.ui(ui, ctx, player),
Self::Details(dt) => dt.ui(ui, ctx, player),
}
}
}
impl Default for MainPanel {
fn default() -> Self {
Self::Overview(Overview::default())
}
}

View File

@ -1,170 +0,0 @@
use egui::{IconData, Sense};
use egui_extras::{Column, TableBuilder};
use libpt::log::{error, trace, warn};
use strum::IntoEnumIterator;
pub mod details;
pub mod entry;
pub mod mainpanel;
pub mod overview;
pub mod showbox;
use super::*;
const ICON_RAW: &[u8; 36525] = include_bytes!("../../../assets/img/icon-512.jpg");
impl Player {
fn top_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
egui::menu::bar(ui, |ui| {
// NOTE: no File->Quit on web pages!
let is_web = cfg!(target_arch = "wasm32");
if !is_web {
ui.menu_button("File", |ui| {
if ui.button("Info").clicked() {
self.show_info_window = true;
}
ui.separator();
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.add_space(16.0);
}
if self.show_info_window {
self.info_diag(ctx);
}
egui::widgets::global_dark_light_mode_buttons(ui);
});
}
fn info_diag(&mut self, ctx: &egui::Context) {
trace!("rendering info dialogue");
ctx.show_viewport_immediate(
egui::ViewportId::from_hash_of(format!("{TITLE}: Information")),
egui::ViewportBuilder::default()
.with_title(format!("{TITLE}: Information"))
.with_inner_size([500.0, 200.0]),
|ctx, class| {
assert!(
class == egui::ViewportClass::Immediate,
"This egui backend doesn't support multiple viewports"
);
egui::CentralPanel::default().show(ctx, |ui| {
ui.label(format!("{TITLE} v{}", env!("CARGO_PKG_VERSION")));
ui.hyperlink_to("Source Code\n", env!("CARGO_PKG_REPOSITORY"));
ui.label(format!("Author: {}", env!("CARGO_PKG_AUTHORS")));
ui.label(format!("License: {}", env!("CARGO_PKG_LICENSE")));
ui.label(format!(
"\n{TITLE} is free software. If you paid for this you were scammed.\n"
));
////////////////////////////////////////////////////////////////
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("Powered by ");
ui.hyperlink_to("egui", "https://github.com/emilk/egui");
ui.label(" and ");
ui.hyperlink_to(
"eframe",
"https://github.com/emilk/egui/tree/master/crates/eframe",
);
ui.label(".");
});
////////////////////////////////////////////////////////////////
});
if ctx.input(|i| i.viewport().close_requested()) {
// Tell parent viewport that we should not show next frame:
self.show_info_window = false;
}
},
);
}
fn bottom_label(ui: &mut egui::Ui) {
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label(format!("{TITLE} v{}", env!("CARGO_PKG_VERSION")));
ui.label(" | Powered by ");
ui.hyperlink_to("egui", "https://github.com/emilk/egui");
ui.label(" and ");
ui.hyperlink_to(
"eframe",
"https://github.com/emilk/egui/tree/master/crates/eframe",
);
ui.label(".");
});
egui::warn_if_debug_build(ui);
});
}
fn main_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
let bind = self.active_main_panel.clone();
bind.ui(ui, ctx, self)
}
fn meta_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
ui.horizontal_centered(|ui| {
for category in Kind::iter() {
if category == Kind::Song {
continue; // no category for songs, that's just clicked and then played
}
let sl = ui.selectable_label(self.category() == category, category.to_string());
if sl.clicked() {
self.set_category(category);
}
}
});
}
pub(crate) fn load_icon() -> IconData {
let (icon_rgba, icon_width, icon_height) = {
let image = image::load_from_memory(ICON_RAW)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
}
}
pub fn set_active_main_panel(&mut self, mp: MainPanel) {
self.active_main_panel = mp;
}
}
impl eframe::App for Player {
/// Called each time the UI needs repainting, which may be many times per second.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui_extras::install_image_loaders(ctx); // apperently it's okay to have this every update
// as it's only done once? sus
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
self.top_panel(ui, ctx);
});
egui::TopBottomPanel::top("top_panel2").show(ctx, |ui| {
self.meta_panel(ui, ctx);
});
egui::CentralPanel::default().show(ctx, |ui| {
self.main_panel(ui, ctx);
});
egui::TopBottomPanel::bottom("bot_panel").show(ctx, Self::bottom_label);
}
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
match self.store().save() {
Ok(_) => (),
Err(e) => {
error!("{e}");
}
}
}
}

View File

@ -1,47 +0,0 @@
use egui::Sense;
use egui_extras::{Column, TableBuilder};
use libpt::log::{debug, warn};
use super::showbox::ShowBox;
use super::Player;
#[derive(Clone, Default, PartialEq)]
pub struct Overview {
frame: egui::Frame,
}
impl Overview {
pub fn ui(&self, ui: &mut egui::Ui, ctx: &egui::Context, player: &mut Player) {
let mut tb = TableBuilder::new(ui);
let entries_per_line: usize = ctx.screen_rect().width() as usize / 256;
for _ in 0..entries_per_line {
tb = tb.column(Column::remainder());
}
tb.body(|mut body| {
for line in player.entries().chunks(entries_per_line) {
body.row(240.0, |mut row| {
for e in line {
row.col(|ui| {
let shobo = ui.add(ShowBox::new(e));
let shobo = shobo.interact(Sense::click());
if shobo.clicked() {
debug!("showbox \"{:#?}\" was clicked", shobo);
warn!("clicking for showbox not defined yet");
player.open(e);
}
});
}
});
}
});
}
}
impl std::fmt::Debug for Overview {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Overview").finish()
}
}

View File

@ -1,53 +0,0 @@
use egui::text::LayoutJob;
use egui::{Color32, FontFamily, FontId, Label, TextFormat};
use super::Entry;
pub struct ShowBox {
entry: Entry,
frame: egui::Frame,
}
impl ShowBox {
pub fn new(entry: &Entry) -> Self {
let frame = egui::Frame::none();
Self {
entry: entry.to_owned(),
frame,
}
}
}
impl egui::Widget for ShowBox {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
ui.vertical_centered(|ui| {
let r = ui.add(
egui::Image::new(self.entry.img())
.rounding(5.0)
.bg_fill(Color32::DARK_GRAY)
.shrink_to_fit()
.maintain_aspect_ratio(true),
);
let mut job = LayoutJob::default();
job.append(
&self.entry.title,
0.0,
TextFormat {
font_id: FontId::new(14.0, FontFamily::Proportional),
..Default::default()
},
);
r.union(ui.add(Label::new(job)));
r.union(ui.label(&self.entry.subtitle));
})
.response
}
}
impl std::fmt::Debug for ShowBox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShowBox")
.field("entry", &self.entry)
.finish()
}
}

View File

View File

View File

View File

@ -1,104 +0,0 @@
use std::fs::File;
use std::io::{BufReader, Write};
use std::path::PathBuf;
use libpt::log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use crate::error::Error;
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct Store {
dummy: String,
}
impl Store {
pub fn projdir() -> Option<directories::ProjectDirs> {
directories::ProjectDirs::from("de.cscherr", "Beatbär", "Beatbär")
}
pub fn genres(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn artists(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn playlists(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn songs(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn albums(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn save(&self) -> Result<(), Error> {
info!("saving the store to file");
debug!("Store: {self:#?}");
if let Some(dirs) = Self::projdir() {
let mut store_path: PathBuf = dirs.data_local_dir().into();
store_path.push("store.msgpack");
if !store_path.exists() {
std::fs::create_dir_all(
store_path
.parent()
.expect("beatbär storefile has no parent????"),
)?;
warn!("The Beatbär store at '{}' does not exist. Creating it anew. This is normal if you start Beatbär for the first time.", store_path.as_path().to_string_lossy());
} else if !store_path.is_file() {
let e = Error::BadStoreFile(store_path);
error!("{e}");
return Err(e);
}
let mut file = File::options()
.write(true)
.append(false)
.create(true)
.truncate(true)
.open(store_path)?;
let repr = rmp_serde::to_vec(self)?;
file.write_all(&repr)?;
info!("store file was written");
Ok(())
} else {
Err(Error::NoProjDir)
}
}
pub fn load() -> Result<Self, Error> {
info!("loading the store to file");
if let Some(dirs) = Self::projdir() {
let mut store_path: PathBuf = dirs.data_local_dir().into();
store_path.push("store.msgpack");
if !store_path.exists() {
warn!(
"The Beatbär store at '{}' does not exist. Loading an empty store instead.",
store_path.as_path().to_string_lossy()
);
return Ok(Self::default());
} else if !store_path.is_file() {
let e = Error::BadStoreFile(store_path);
error!("{e}");
return Err(e);
}
let file = File::options().read(true).write(false).open(store_path)?;
let store = rmp_serde::from_read(file)?;
info!("store file was loaded");
debug!("Store: {store:#?}");
Ok(store)
} else {
let e = Error::NoProjDir;
error!("{}", e);
Err(e)
}
}
}

View File

@ -1 +0,0 @@
pub struct Playlist;

View File

@ -1,2 +0,0 @@
#[derive(Clone, Debug, Hash)]
pub struct Song {}