tags can now be used

filldb: Year and Genre will be read from MP3/ID3 tags and added as Tags.
client: Tags can (and will) be displayed in the status bar.
client: StatusBar text can be configured via ~/.config/musicdb-client/config_gui.toml
client: Added a proper default config file at src/config_gui.toml
        (note: this is in src/ so that include_bytes! can use a path without /, so it will compile on windows)
client: users will need to add the new `[text]` section to their gui_config.toml!
This commit is contained in:
Mark 2023-09-20 22:31:56 +02:00
parent c6b75180bb
commit f429f17876
8 changed files with 484 additions and 179 deletions

View File

@ -0,0 +1,32 @@
font = ''
[text]
# define the text displayed in the application.
# escape sequences:
# \t: song title
# \a: album name
# \A: artist name
# \s1.0;: set the scale (to the default: 1.0)
# \h1.0;: set the height-alignment (to the default: 1.0 / align text to be on one baseline)
# \cRRGGBB: set color to this hex value.
# \cFFFFFF: default color (white)
# \<char>: <char> (\\ => \, \# => #, \% => %, ...)
# custom properties:
# %<mode><search text>%
# %_word% returns the first property that includes "word"
# %>Year=% returns the end of the first property that starts with "Year=",
# so if a song has "Year=2019", this would return "2019".
# %=Value% returns something if a property "Value" is found.
# IF:
# ?<condition>#<then>#<else>#
# If <condition> is not empty, the entire block will be replaced by the value generated by <then>.
# If <condition> is empty, the entire block will be replaced by the value generated by <else>.
# Examples:
# ?\A#by \A##
# If we know the artist's name, write "by " followed by the name,
# if not, don't write anything (## -> <else> is empty)
# ?\t#\t#(no title found)#
# If we know the title, write it. If not, write "(no title found)" instead.
status_bar = '''\t
\s0.5;?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##?%>Year=%#\c808080 (%>Year=%)##'''

View File

@ -28,7 +28,7 @@ use speedy2d::{
Graphics2D,
};
use crate::{gui_screen::GuiScreen, gui_wrappers::WithFocusHotkey};
use crate::{gui_screen::GuiScreen, gui_wrappers::WithFocusHotkey, textcfg};
pub enum GuiEvent {
Refresh,
@ -50,6 +50,7 @@ pub fn main(
let mut scroll_pixels_multiplier = 1.0;
let mut scroll_lines_multiplier = 3.0;
let mut scroll_pages_multiplier = 0.75;
let status_bar_text;
match std::fs::read_to_string(&config_file) {
Ok(cfg) => {
if let Ok(table) = cfg.parse::<toml::Table>() {
@ -85,8 +86,26 @@ pub fn main(
{
scroll_pages_multiplier = v;
}
if let Some(t) = table.get("text").and_then(|v| v.as_table()) {
if let Some(v) = t.get("status_bar").and_then(|v| v.as_str()) {
match v.parse() {
Ok(v) => status_bar_text = v,
Err(e) => {
eprintln!("[toml] `text.status_bar couldn't be parsed: {e}`");
std::process::exit(30);
}
}
} else {
eprintln!("[toml] missing the required `text.status_bar` string value.");
std::process::exit(30);
}
} else {
eprintln!("[toml] missing the required `[text]` section!");
std::process::exit(30);
}
} else {
eprintln!("Couldn't parse config file {config_file:?} as toml!");
std::process::exit(30);
}
}
Err(e) => {
@ -94,7 +113,9 @@ pub fn main(
if let Some(p) = config_file.parent() {
_ = std::fs::create_dir_all(p);
}
_ = std::fs::write(&config_file, "font = ''");
if std::fs::write(&config_file, include_bytes!("config_gui.toml")).is_ok() {
eprintln!("[info] created a default config file.");
}
std::process::exit(25);
}
}
@ -126,9 +147,14 @@ pub fn main(
scroll_pixels_multiplier,
scroll_lines_multiplier,
scroll_pages_multiplier,
GuiConfig { status_bar_text },
));
}
pub struct GuiConfig {
pub status_bar_text: textcfg::TextBuilder,
}
pub struct Gui {
pub event_sender: UserEventSender<GuiEvent>,
pub database: Arc<Mutex<Database>>,
@ -150,6 +176,7 @@ pub struct Gui {
pub scroll_pixels_multiplier: f64,
pub scroll_lines_multiplier: f64,
pub scroll_pages_multiplier: f64,
pub gui_config: Option<GuiConfig>,
}
impl Gui {
fn new(
@ -163,6 +190,7 @@ impl Gui {
scroll_pixels_multiplier: f64,
scroll_lines_multiplier: f64,
scroll_pages_multiplier: f64,
gui_config: GuiConfig,
) -> Self {
database.lock().unwrap().update_endpoints.push(
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
@ -227,6 +255,7 @@ impl Gui {
scroll_pixels_multiplier,
scroll_lines_multiplier,
scroll_pages_multiplier,
gui_config: Some(gui_config),
}
}
}
@ -430,6 +459,7 @@ pub struct DrawInfo<'a> {
Dragging,
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
)>,
pub gui_config: &'a GuiConfig,
}
/// Generic wrapper over anything that implements GuiElemTrait
@ -817,6 +847,7 @@ impl WindowHandler<GuiEvent> for Gui {
);
let mut dblock = self.database.lock().unwrap();
let mut covers = self.covers.take().unwrap();
let cfg = self.gui_config.take().unwrap();
let mut info = DrawInfo {
actions: Vec::with_capacity(0),
pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()),
@ -830,6 +861,7 @@ impl WindowHandler<GuiEvent> for Gui {
child_has_keyboard_focus: true,
line_height: self.line_height,
dragging: self.dragging.take(),
gui_config: &cfg,
};
self.gui.draw(&mut info, graphics);
let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0));
@ -864,6 +896,7 @@ impl WindowHandler<GuiEvent> for Gui {
}
// cleanup
drop(info);
self.gui_config = Some(cfg);
self.covers = Some(covers);
drop(dblock);
for a in actions {

View File

@ -31,20 +31,11 @@ impl CurrentSong {
pub fn new(config: GuiElemCfg) -> Self {
Self {
config,
children: vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.0), (1.0, 0.5))),
"".to_owned(),
Color::from_int_rgb(180, 180, 210),
None,
Vec2::new(0.0, 1.0),
)),
GuiElem::new(AdvancedLabel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.5), (1.0, 1.0))),
Vec2::new(0.0, 0.0),
vec![],
)),
],
children: vec![GuiElem::new(AdvancedLabel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.0), (1.0, 1.0))),
Vec2::new(0.0, 0.5),
vec![],
))],
cover_pos: Rectangle::new(Vec2::ZERO, Vec2::ZERO),
covers: VecDeque::new(),
prev_song: None,
@ -140,139 +131,34 @@ impl GuiElemTrait for CurrentSong {
// redraw
if self.config.redraw {
self.config.redraw = false;
let (name, subtext) = if let Some(song) = new_song {
if let Some(song) = info.database.get_song(&song) {
let sub = match (
info.database.artists().get(&song.artist),
song.album
.as_ref()
.and_then(|id| info.database.albums().get(id)),
) {
(None, None) => vec![],
(Some(artist), None) => vec![
(
Content::new("by ".to_owned(), Self::color_by(0.0)),
1.0,
1.0,
),
(
Content::new(artist.name.to_owned(), Self::color_artist(0.0)),
1.0,
1.0,
),
],
(None, Some(album)) => vec![
(Content::new(String::new(), Color::TRANSPARENT), 0.0, 1.0),
(
Content::new("on ".to_owned(), Self::color_on(0.0)),
1.0,
1.0,
),
(
Content::new(album.name.to_owned(), Self::color_album(0.0)),
1.0,
1.0,
),
],
(Some(artist), Some(album)) => vec![
(
Content::new("by ".to_owned(), Self::color_by(0.0)),
1.0,
1.0,
),
(
Content::new(
format!("{} ", artist.name),
Self::color_artist(0.0),
),
1.0,
1.0,
),
(
Content::new("on ".to_owned(), Self::color_on(0.0)),
1.0,
1.0,
),
(
Content::new(album.name.to_owned(), Self::color_album(0.0)),
1.0,
1.0,
),
],
};
(song.title.clone(), sub)
} else {
(
"< song not in db >".to_owned(),
vec![(
Content::new(
"you may need to restart the client to resync the database"
.to_owned(),
Color::from_rgb(0.8, 0.5, 0.5),
),
1.0,
1.0,
)],
)
}
} else {
(String::new(), vec![])
};
*self.children[0]
.try_as_mut::<Label>()
.unwrap()
.content
.text() = name;
self.children[1]
.try_as_mut::<AdvancedLabel>()
.unwrap()
.content = subtext;
self.text_updated = Some(Instant::now());
if let Some(song) = new_song {
let status_bar_text = info
.gui_config
.status_bar_text
.gen(&info.database, info.database.get_song(&song));
self.children[0]
.try_as_mut::<AdvancedLabel>()
.unwrap()
.content = status_bar_text;
self.text_updated = Some(Instant::now());
}
}
}
if let Some(updated) = &self.text_updated {
if let Some(h) = &info.helper {
h.request_redraw();
}
let prog = updated.elapsed().as_secs_f32();
*self.children[0]
.try_as_mut::<Label>()
let mut prog = updated.elapsed().as_secs_f32();
if prog >= 1.0 {
prog = 1.0;
self.text_updated = None;
}
self.children[0]
.try_as_mut::<AdvancedLabel>()
.unwrap()
.content
.color() = Self::color_title((prog / 1.5).min(1.0));
let subtext = self.children[1].try_as_mut::<AdvancedLabel>().unwrap();
match subtext.content.len() {
2 => {
*subtext.content[0].0.color() = Self::color_by(prog.min(1.0));
*subtext.content[1].0.color() =
Self::color_artist((prog.max(0.5) - 0.5).min(1.0));
if prog >= 1.5 {
self.text_updated = None;
}
}
3 => {
*subtext.content[0].0.color() = Self::color_on(prog.min(1.0));
*subtext.content[1].0.color() =
Self::color_album((prog.max(0.5) - 0.5).min(1.0));
if prog >= 1.5 {
self.text_updated = None;
}
}
4 => {
*subtext.content[0].0.color() = Self::color_by(prog.min(1.0));
*subtext.content[1].0.color() =
Self::color_artist((prog.max(0.5) - 0.5).min(1.0));
*subtext.content[2].0.color() = Self::color_on((prog.max(1.0) - 1.0).min(1.0));
*subtext.content[3].0.color() =
Self::color_album((prog.max(1.5) - 1.5).min(1.0));
if prog >= 2.5 {
self.text_updated = None;
}
}
_ => {
self.text_updated = None;
}
}
.iter_mut()
.count();
}
// drawing stuff
if self.config.pixel_pos.size() != info.pos.size() {

View File

@ -268,21 +268,19 @@ pub struct AdvancedLabel {
pub align: Vec2,
/// (Content, Size-Scale, Height)
/// Size-Scale and Height should default to 1.0.
pub content: Vec<(Content, f32, f32)>,
pub content: Vec<Vec<(Content, f32, f32)>>,
/// the position from where content drawing starts.
/// recalculated when layouting is performed.
content_pos: Vec2,
content_height: f32,
}
impl AdvancedLabel {
pub fn new(config: GuiElemCfg, align: Vec2, content: Vec<(Content, f32, f32)>) -> Self {
pub fn new(config: GuiElemCfg, align: Vec2, content: Vec<Vec<(Content, f32, f32)>>) -> Self {
Self {
config,
children: vec![],
align,
content,
content_pos: Vec2::ZERO,
content_height: 0.0,
}
}
}
@ -308,55 +306,77 @@ impl GuiElemTrait for AdvancedLabel {
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.config.redraw
|| self.config.pixel_pos.size() != info.pos.size()
|| self.content.iter().any(|(c, _, _)| c.will_redraw())
|| self
.content
.iter()
.any(|v| v.iter().any(|(c, _, _)| c.will_redraw()))
{
self.config.redraw = false;
let mut len = 0.0;
let mut height = 0.0;
for (c, scale, _) in &self.content {
let mut size = info
.font
.layout_text(&c.text, 1.0, TextOptions::new())
.size();
len += size.x * scale;
if size.y * scale > height {
height = size.y * scale;
let mut max_len = 0.0;
let mut total_height = 0.0;
for line in &self.content {
let mut len = 0.0;
let mut height = 0.0;
for (c, scale, _) in line {
let size = info
.font
.layout_text(&c.text, 1.0, TextOptions::new())
.size();
len += size.x * scale;
if size.y * scale > height {
height = size.y * scale;
}
}
if len > max_len {
max_len = len;
}
total_height += height;
}
if len > 0.0 && height > 0.0 {
let scale1 = info.pos.width() / len;
let scale2 = info.pos.height() / height;
if max_len > 0.0 && total_height > 0.0 {
let scale1 = info.pos.width() / max_len;
let scale2 = info.pos.height() / total_height;
let scale;
self.content_pos = if scale1 < scale2 {
// use all available width
scale = scale1;
self.content_height = height * scale;
let pad = info.pos.height() - self.content_height;
Vec2::new(0.0, pad * self.align.y)
Vec2::new(
0.0,
(info.pos.height() - (total_height * scale)) * self.align.y,
)
} else {
// use all available height
scale = scale2;
self.content_height = info.pos.height();
let pad = info.pos.width() - len * scale;
Vec2::new(pad * self.align.x, 0.0)
Vec2::new((info.pos.width() - (max_len * scale)) * self.align.x, 0.0)
};
for (c, s, _) in &mut self.content {
c.formatted = Some(info.font.layout_text(
&c.text,
scale * (*s),
TextOptions::new(),
));
for line in &mut self.content {
for (c, s, _) in line {
c.formatted = Some(info.font.layout_text(
&c.text,
scale * (*s),
TextOptions::new(),
));
}
}
}
}
let pos_y = info.pos.top_left().y + self.content_pos.y;
let mut pos_x = info.pos.top_left().x + self.content_pos.x;
for (c, _, h) in &self.content {
if let Some(f) = &c.formatted {
let y = pos_y + (self.content_height - f.height()) * h;
g.draw_text(Vec2::new(pos_x, y), c.color, f);
pos_x += f.width();
let pos_x_start = info.pos.top_left().x + self.content_pos.x;
let mut pos_y = info.pos.top_left().y + self.content_pos.y;
for line in &self.content {
let mut pos_x = pos_x_start;
let height = line
.iter()
.filter_map(|v| v.0.formatted.as_ref())
.map(|f| f.height())
.reduce(f32::max)
.unwrap_or(0.0);
for (c, _, h) in line {
if let Some(f) = &c.formatted {
let y = pos_y + (height - f.height()) * h;
g.draw_text(Vec2::new(pos_x, y), c.color, f);
pos_x += f.width();
}
}
pos_y += height;
}
}
}

View File

@ -42,6 +42,7 @@ mod gui_settings;
mod gui_text;
#[cfg(feature = "speedy2d")]
mod gui_wrappers;
mod textcfg;
#[derive(Clone, Copy)]
enum Mode {

View File

@ -0,0 +1,326 @@
use std::{
fmt::Display,
iter::Peekable,
str::{Chars, FromStr},
};
use musicdb_lib::data::{database::Database, song::Song, GeneralData, SongId};
use speedy2d::color::Color;
use crate::gui_text::Content;
#[derive(Debug)]
pub struct TextBuilder(pub Vec<TextPart>);
#[derive(Debug)]
pub enum TextPart {
LineBreak,
SetColor(Color),
SetScale(f32),
SetHeightAlign(f32),
// - - - - -
Literal(String),
SongTitle,
AlbumName,
ArtistName,
/// Searches for a tag with exactly the provided value.
/// Returns nothing or one of the following characters:
/// `s` for Song, `a` for Album, and `A` for Artist.
TagEq(String),
/// Searches for a tag which starts with the provided string, then returns the end of it.
/// If the search string is the entire tag, returns an empty string (which is not `nothing` because it is a TextPart::Literal, so it counts as `something` in an `if`).
TagEnd(String),
/// Searches for a tag which contains the provided string, then returns that tag's value.
TagContains(String),
/// If `1` is something, uses `2`.
/// If `1` is nothing, uses `3`.
If(TextBuilder, TextBuilder, TextBuilder),
}
impl TextBuilder {
pub fn gen(&self, db: &Database, current_song: Option<&Song>) -> Vec<Vec<(Content, f32, f32)>> {
let mut out = vec![];
let mut line = vec![];
self.gen_to(db, current_song, &mut out, &mut line, &mut 1.0, &mut 1.0);
if !line.is_empty() {
out.push(line)
}
out
}
pub fn gen_to(
&self,
db: &Database,
current_song: Option<&Song>,
out: &mut Vec<Vec<(Content, f32, f32)>>,
line: &mut Vec<(Content, f32, f32)>,
scale: &mut f32,
align: &mut f32,
) {
let mut color = Color::WHITE;
macro_rules! push {
($e:expr) => {
line.push((Content::new($e, color), *scale, *align))
};
}
fn all_general<'a>(
db: &'a Database,
current_song: &'a Option<&'a Song>,
) -> [Option<&'a GeneralData>; 3] {
if let Some(s) = current_song {
if let Some(al) = s.album.and_then(|id| db.albums().get(&id)) {
if let Some(a) = db.artists().get(&s.artist) {
[Some(&s.general), Some(&al.general), Some(&a.general)]
} else {
[Some(&s.general), Some(&al.general), None]
}
} else if let Some(a) = db.artists().get(&s.artist) {
[Some(&s.general), None, Some(&a.general)]
} else {
[Some(&s.general), None, None]
}
} else {
[None, None, None]
}
}
for part in &self.0 {
match part {
TextPart::LineBreak => out.push(std::mem::replace(line, vec![])),
TextPart::SetColor(c) => color = *c,
TextPart::SetScale(v) => *scale = *v,
TextPart::SetHeightAlign(v) => *align = *v,
TextPart::Literal(s) => push!(s.to_owned()),
TextPart::SongTitle => {
if let Some(s) = current_song {
push!(s.title.to_owned());
}
}
TextPart::AlbumName => {
if let Some(s) = current_song {
if let Some(album) = s.album.and_then(|id| db.albums().get(&id)) {
push!(album.name.to_owned());
}
}
}
TextPart::ArtistName => {
if let Some(s) = current_song {
if let Some(artist) = db.artists().get(&s.artist) {
push!(artist.name.to_owned());
}
}
}
TextPart::TagEq(p) => {
for (i, gen) in all_general(db, &current_song).into_iter().enumerate() {
if let Some(_) = gen.and_then(|gen| gen.tags.iter().find(|t| *t == p)) {
push!(match i {
0 => 's',
1 => 'a',
2 => 'A',
_ => unreachable!("array length should be 3"),
}
.to_string());
break;
}
}
}
TextPart::TagEnd(p) => {
for gen in all_general(db, &current_song) {
if let Some(t) =
gen.and_then(|gen| gen.tags.iter().find(|t| t.starts_with(p)))
{
push!(t[p.len()..].to_owned());
break;
}
}
}
TextPart::TagContains(p) => {
for gen in all_general(db, &current_song) {
if let Some(t) = gen.and_then(|gen| gen.tags.iter().find(|t| t.contains(p)))
{
push!(t.to_owned());
break;
}
}
}
TextPart::If(condition, yes, no) => {
if !condition.gen(db, current_song).is_empty() {
yes.gen_to(db, current_song, out, line, scale, align);
} else {
no.gen_to(db, current_song, out, line, scale, align);
}
}
}
}
}
}
impl FromStr for TextBuilder {
type Err = TextBuilderParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_chars(&mut s.chars())
}
}
impl TextBuilder {
fn from_chars(chars: &mut Chars) -> Result<Self, TextBuilderParseError> {
let mut vec = vec![];
let mut current = String::new();
macro_rules! done {
() => {
if !current.is_empty() {
// if it starts with at least one space, replace the first space with
// a No-Break space, as recommended in `https://github.com/QuantumBadger/Speedy2D/issues/45`,
// to avoid an issue where leading whitespaces are removed when drawing text.
if current.starts_with(' ') {
current = current.replacen(' ', "\u{00A0}", 1);
}
vec.push(TextPart::Literal(std::mem::replace(
&mut current,
String::new(),
)));
}
};
}
loop {
if let Some(ch) = chars.next() {
match ch {
'\n' => {
done!();
vec.push(TextPart::LineBreak);
}
'\\' => match chars.next() {
None => current.push('\\'),
Some('t') => {
done!();
vec.push(TextPart::SongTitle);
}
Some('a') => {
done!();
vec.push(TextPart::AlbumName);
}
Some('A') => {
done!();
vec.push(TextPart::ArtistName);
}
Some('s') => {
done!();
vec.push(TextPart::SetScale({
let mut str = String::new();
loop {
match chars.next() {
None | Some(';') => break,
Some(c) => str.push(c),
}
}
if let Ok(v) = str.parse() {
v
} else {
return Err(TextBuilderParseError::CouldntParse(
str,
"number (float)".to_string(),
));
}
}))
}
Some('h') => {
done!();
vec.push(TextPart::SetHeightAlign({
let mut str = String::new();
loop {
match chars.next() {
None | Some(';') => break,
Some(c) => str.push(c),
}
}
if let Ok(v) = str.parse() {
v
} else {
return Err(TextBuilderParseError::CouldntParse(
str,
"number (float)".to_string(),
));
}
}))
}
Some('c') => {
done!();
vec.push(TextPart::SetColor({
let mut str = String::new();
for _ in 0..6 {
if let Some(ch) = chars.next() {
str.push(ch);
} else {
return Err(TextBuilderParseError::TooFewCharsForColor);
}
}
if let Ok(i) = u32::from_str_radix(&str, 16) {
Color::from_hex_rgb(i)
} else {
return Err(TextBuilderParseError::ColorNotHex);
}
}));
}
Some(ch) => current.push(ch),
},
'%' => {
done!();
let mode = if let Some(ch) = chars.next() {
ch
} else {
return Err(TextBuilderParseError::UnclosedPercent);
};
loop {
match chars.next() {
Some('%') => {
let s = std::mem::replace(&mut current, String::new());
vec.push(match mode {
'=' => TextPart::TagEq(s),
'>' => TextPart::TagEnd(s),
'_' => TextPart::TagContains(s),
c => return Err(TextBuilderParseError::TagModeUnknown(c)),
});
break;
}
Some(ch) => current.push(ch),
None => return Err(TextBuilderParseError::UnclosedPercent),
}
}
}
'?' => {
done!();
vec.push(TextPart::If(
Self::from_chars(chars)?,
Self::from_chars(chars)?,
Self::from_chars(chars)?,
));
}
'#' => break,
ch => current.push(ch),
}
} else {
break;
}
}
done!();
Ok(Self(vec))
}
}
#[derive(Debug)]
pub enum TextBuilderParseError {
UnclosedPercent,
TagModeUnknown(char),
TooFewCharsForColor,
ColorNotHex,
CouldntParse(String, String),
}
impl Display for TextBuilderParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnclosedPercent => write!(
f,
"Unclosed %: Syntax is %<mode><search>%, where <mode> is _, >, or =."
),
Self::TagModeUnknown(mode) => {
write!(f, "Unknown tag mode '{mode}': Allowed are only _, > or =.")
}
Self::TooFewCharsForColor => write!(f, "Too few chars for color: Syntax is \\cRRGGBB."),
Self::ColorNotHex => write!(f, "Color value wasn't a hex number! Syntax is \\cRRGGBB, where R, G, and B are values from 0-9 and A-F (hex 0-F)."),
Self::CouldntParse(v, t) => write!(f, "Couldn't parse value '{v}' to type '{t}'."),
}
}
}

Binary file not shown.

View File

@ -58,6 +58,13 @@ fn main() {
eprintln!("searching for artists...");
let mut artists = HashMap::new();
for song in songs {
let mut general = GeneralData::default();
if let Some(year) = song.1.year() {
general.tags.push(format!("Year={year}"));
}
if let Some(genre) = song.1.genre_parsed() {
general.tags.push(format!("Genre={genre}"));
}
let (artist_id, album_id) = if let Some(artist) = song
.1
.album_artist()
@ -135,7 +142,7 @@ fn main() {
artist: artist_id,
more_artists: vec![],
cover: None,
general: GeneralData::default(),
general,
cached_data: Arc::new(Mutex::new(None)),
});
}