aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/albums.rs52
-rw-r--r--src/cli.rs8
-rw-r--r--src/config.rs13
-rw-r--r--src/intuition.rs (renamed from src/learner.rs)12
-rw-r--r--src/main.rs27
-rw-r--r--src/state.rs24
-rw-r--r--src/trajectory.rs2
7 files changed, 74 insertions, 64 deletions
diff --git a/src/albums.rs b/src/albums.rs
index 17966ff..c03e953 100644
--- a/src/albums.rs
+++ b/src/albums.rs
@@ -1,5 +1,5 @@
use std::collections::HashSet;
-use std::fs;
+use std::path::PathBuf;
use expanduser::expanduser;
use walkdir::WalkDir;
@@ -12,45 +12,31 @@ pub fn load(config: &Config) -> HashSet<String> {
return HashSet::new();
};
- let mut new_albums: HashSet<String> = HashSet::new();
+ let mut album_dirs: HashSet<PathBuf> = HashSet::new();
for entry in WalkDir::new(&root_path) {
- let entry = match entry {
- Ok(e) if e.path().is_dir() => e,
- _ => continue,
- };
+ let Ok(entry) = entry else { continue };
- if !contains_audio(&entry, config) {
+ let path = entry.path();
+ if path.is_dir() {
continue;
- }
+ };
+
+ let is_audio_file = path
+ .extension()
+ .and_then(|ext| ext.to_str())
+ .is_some_and(|ext| config.audio_exts.contains(&ext.to_lowercase()));
- if let Some(rel_str) = entry
- .path()
- .strip_prefix(&root_path)
- .ok()
- .and_then(|p| p.to_str())
+ if is_audio_file
+ && let Some(parent) = path.parent()
+ && parent != root_path
{
- new_albums.insert(rel_str.to_string());
+ album_dirs.insert(parent.to_path_buf());
}
}
- new_albums
-}
-
-fn contains_audio(entry: &walkdir::DirEntry, config: &Config) -> bool {
- fs::read_dir(entry.path()).ok().is_some_and(|mut entries| {
- entries.any(|e| {
- e.as_ref()
- .is_ok_and(|entry| is_audio_file(&entry.path(), config))
- })
- })
-}
-
-fn is_audio_file(path: &std::path::Path, config: &Config) -> bool {
- !path.is_dir()
- && path
- .extension()
- .and_then(|ext| ext.to_str())
- .map(str::to_lowercase)
- .is_some_and(|ext| config.audio_exts.contains(&format!(".{ext}")))
+ album_dirs
+ .iter()
+ .filter_map(|p| p.strip_prefix(&root_path).ok()?.to_str().map(String::from))
+ .collect()
}
diff --git a/src/cli.rs b/src/cli.rs
index 7be3fd4..b408dae 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -5,7 +5,7 @@ pub enum Action {
NewConfig,
NewState,
DumpTrajectory,
- DumpLearner,
+ DumpIntuition,
Run,
}
@@ -25,8 +25,8 @@ Options:
--new-config Write a default config file and exit.
--new-state Write a default state file and exit.
--dump-trajectory Dump the play trajectory as JSON to stdout.
- --dump-learner Dump learner weights as JSON to stdout.
-<no option> Suggest and play the next album"
+ --dump-intuition Dump intuition data as JSON to stdout.
+ <no option> Suggest and play the next album"
);
}
@@ -48,7 +48,7 @@ pub fn parse() -> Option<Action> {
Some("--new-config") => Some(Action::NewConfig),
Some("--new-state") => Some(Action::NewState),
Some("--dump-trajectory") => Some(Action::DumpTrajectory),
- Some("--dump-learner") => Some(Action::DumpLearner),
+ Some("--dump-intuition") => Some(Action::DumpIntuition),
Some(unknown) => {
eprintln!("Unknown option: {unknown}");
std::process::exit(1);
diff --git a/src/config.rs b/src/config.rs
index 15b449a..67721b4 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,13 +1,13 @@
use directories::{ProjectDirs, UserDirs};
use serde::{Deserialize, Serialize};
-use std::fs;
+use std::{collections::HashSet, fs};
use expanduser::expanduser;
#[derive(Serialize, Deserialize)]
pub struct Config {
pub music_root: String,
- pub audio_exts: Vec<String>,
+ pub audio_exts: HashSet<String>,
pub skip_window_secs: u64,
pub temperature: f32,
pub low_weight: f32,
@@ -19,7 +19,12 @@ pub struct Config {
fn default_config_path() -> String {
let name = "mood.json";
ProjectDirs::from("qualifier", "organisation", "mood")
- .and_then(|pd| pd.config_dir().join(name).to_str().map(std::string::ToString::to_string))
+ .and_then(|pd| {
+ pd.config_dir()
+ .join(name)
+ .to_str()
+ .map(std::string::ToString::to_string)
+ })
.unwrap_or(format!("~/.config/mood/{name}"))
}
@@ -50,7 +55,7 @@ impl Default for Config {
.and_then(|p| p.to_str().map(std::string::ToString::to_string))
})
.unwrap_or("~/Music".to_string()),
- audio_exts: [".mp3", ".flac", ".wav", ".m4a", ".ogg", ".vorbis"]
+ audio_exts: ["mp3", "flac", "wav", "m4a", "ogg", "vorbis", "mp4"]
.into_iter()
.map(String::from)
.collect(),
diff --git a/src/learner.rs b/src/intuition.rs
index 942d52e..f994029 100644
--- a/src/learner.rs
+++ b/src/intuition.rs
@@ -43,7 +43,7 @@ fn softmax_sample(items: &[(String, f32)], temperature: f32) -> Option<(String,
enum Episode {
Continue {
group: HashSet<String>,
- avoid: Option<String>,
+ avoid: String,
committed: String,
},
Escape {
@@ -54,7 +54,7 @@ enum Episode {
}
#[derive(Serialize, Deserialize, Default)]
-pub struct Learner {
+pub struct Intuition {
history: Vec<Episode>,
}
@@ -63,7 +63,7 @@ pub enum Learning {
MoreToSkip(Vec<String>, String),
}
-impl Learner {
+impl Intuition {
pub fn learn(&mut self, learning: &Learning) {
let stamp = chrono::Utc::now().format("%+").to_string();
match learning {
@@ -77,7 +77,7 @@ impl Learner {
Learning::MoreToSkip(trajectory, new) => {
self.history.push(Episode::Continue {
group: trajectory.iter().map(String::clone).collect(),
- avoid: Some(new.clone()),
+ avoid: new.clone(),
committed: stamp,
});
}
@@ -135,9 +135,7 @@ impl Learner {
*g_w += config.max_weight * w;
}
}
- if let Some(a) = avoid
- && let Some(a_w) = items.get_mut(a)
- {
+ if let Some(a_w) = items.get_mut(avoid) {
*a_w -= config.low_weight * w;
}
}
diff --git a/src/main.rs b/src/main.rs
index d16b50f..2a1f2c9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,9 +1,26 @@
+/* mood --- A music player governed by your moods.
+ Copyright (C) 2026 tslil clingman
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
use std::{process::Command, process::exit};
mod albums;
mod cli;
mod config;
-mod learner;
+mod intuition;
mod state;
mod trajectory;
@@ -44,12 +61,12 @@ fn main() {
let action = action.unwrap();
match action {
- Action::DumpLearner => {
+ Action::DumpIntuition => {
let state = load_state_no_default();
println!(
"{}",
- state.dump_learner().unwrap_or_else(|e| {
- eprintln!("Failed to serialize state (dump-learner): {e}");
+ state.dump_intuition().unwrap_or_else(|e| {
+ eprintln!("Failed to serialise state (dump-intuition): {e}");
exit(1);
})
);
@@ -60,7 +77,7 @@ fn main() {
println!(
"{}",
state.dump_trajectory().unwrap_or_else(|e| {
- eprintln!("Failed to serialize state (dump-trajectory): {e}");
+ eprintln!("Failed to serialise state (dump-trajectory): {e}");
exit(1);
})
);
diff --git a/src/state.rs b/src/state.rs
index 195d5cf..47998f3 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -4,19 +4,24 @@ use directories::ProjectDirs;
use expanduser::expanduser;
use serde::{Deserialize, Serialize};
-use crate::{albums, config::Config, learner::Learner, trajectory::Trajectory};
+use crate::{albums, config::Config, intuition::Intuition, trajectory::Trajectory};
fn default_state_path() -> String {
let name = "mood.json.zstd";
ProjectDirs::from("qualifier", "organisation", "mood")
- .and_then(|pd| pd.config_dir().join(name).to_str().map(std::string::ToString::to_string))
+ .and_then(|pd| {
+ pd.config_dir()
+ .join(name)
+ .to_str()
+ .map(std::string::ToString::to_string)
+ })
.unwrap_or(format!("~/.config/mood/{name}"))
}
#[derive(Serialize, Deserialize, Default)]
pub struct State {
trajectory: Trajectory,
- learner: Learner,
+ intuition: Intuition,
}
impl State {
@@ -25,16 +30,15 @@ impl State {
if albums.is_empty() {
println!("Found no albums in {}", config.music_root);
return None;
- } else {
- let l = albums.len();
- println!("Found {} album{}", l, if l > 1 { "s" } else { "" });
}
+ let l = albums.len();
+ println!("Found {} album{}", l, if l > 1 { "s" } else { "" });
let learning = self.trajectory.step(config.skip_window_secs);
if let Some(ref learning) = learning {
- self.learner.learn(learning);
+ self.intuition.learn(learning);
}
- if let Some((new_album, prob)) = self.learner.sample(
+ if let Some((new_album, prob)) = self.intuition.sample(
self.trajectory.slice(),
&learning.as_ref().into(),
&albums,
@@ -77,8 +81,8 @@ impl State {
}
impl State {
- pub fn dump_learner(&self) -> Result<String, String> {
- serde_json::to_string_pretty(&self.learner).map_err(|e| e.to_string())
+ pub fn dump_intuition(&self) -> Result<String, String> {
+ serde_json::to_string_pretty(&self.intuition).map_err(|e| e.to_string())
}
pub fn dump_trajectory(&self) -> Result<String, String> {
diff --git a/src/trajectory.rs b/src/trajectory.rs
index 7f41a65..04d5aab 100644
--- a/src/trajectory.rs
+++ b/src/trajectory.rs
@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use std::time::SystemTime;
-use crate::learner::Learning;
+use crate::intuition::Learning;
#[derive(Serialize, Deserialize)]
struct LastData {