diff options
| author | tslil <tslil@posteo.de> | 2026-07-14 20:22:56 +0100 |
|---|---|---|
| committer | tslil <tslil@posteo.de> | 2026-07-14 21:25:04 +0100 |
| commit | fc5789e026473ea82237be847672ecb350e64479 (patch) | |
| tree | 6210659eb424124b55bd67a63397a37931e50b5f /src | |
| parent | 156cbfe2b0383521f540109682d98d35c88d65f8 (diff) | |
license, simplify code, add README, lose "learner" naming
Diffstat (limited to 'src')
| -rw-r--r-- | src/albums.rs | 52 | ||||
| -rw-r--r-- | src/cli.rs | 8 | ||||
| -rw-r--r-- | src/config.rs | 13 | ||||
| -rw-r--r-- | src/intuition.rs (renamed from src/learner.rs) | 12 | ||||
| -rw-r--r-- | src/main.rs | 27 | ||||
| -rw-r--r-- | src/state.rs | 24 | ||||
| -rw-r--r-- | src/trajectory.rs | 2 |
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() } @@ -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 { |
