use new resolver setup

This commit is contained in:
Rowan S-L 2023-12-19 10:29:12 -05:00
parent f36fbac800
commit b0bdb8f946
6 changed files with 133 additions and 376 deletions

View File

@ -2,13 +2,10 @@ Source(
name: "yt",
format: "flac",
kind: Shell(
cmd: "yt-dlp",
cmd: "bash",
args: [
"-x",
"--audio-format", "flac",
"--audio-quality", "0",
"-o", "${output}",
"https://youtube.com/watch?v=${input}"
"-c",
"yt-dlp -x --audio-format flac --audio-quality 0 -o ${output} https://youtube.com/watch?v=${input} && mv ${output}.flac ${output}",
]
)
)

View File

@ -2,6 +2,7 @@
extern crate tracing;
use std::{
collections::{HashMap, HashSet},
env, fs,
io::{self, BufRead},
path::PathBuf,
@ -10,11 +11,8 @@ use std::{
use clap::{Parser, Subcommand};
use color_eyre::eyre::{anyhow, bail, Result};
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use heck::ToSnakeCase;
use resolver::Resolver;
use uuid::Uuid;
use crate::schema::DlPlaylist;
use schema::Playlist;
mod cache;
mod cfg;
@ -54,6 +52,18 @@ enum Command {
},
/// Print version information
Version,
/// Garbage collect store
///
/// deletes any downloaded files that are no longer referenced
/// by any playlists
GC {
/// directory to "run in"
#[arg(long = "in")]
run_in: Option<PathBuf>,
/// find, but do not remove, unreferenced files
#[arg(long)]
dry_run: bool,
},
}
fn main() -> Result<()> {
@ -69,11 +79,10 @@ fn main() -> Result<()> {
res.create_dirs()?;
log::initialize_logging(Some(res.tmp_file("dmm.log")))?;
res.resolve()?;
let config = res.out().config.clone();
let chosen = {
let chosen: Playlist = {
let mut scores = vec![];
let matcher = SkimMatcherV2::default().ignore_case();
for (i, j) in res.out().cache.playlists.iter().enumerate() {
for (i, j) in res.out().playlists.iter().enumerate() {
if let Some(score) = matcher.fuzzy_match(&j.name, &playlist) {
scores.push((score, i));
}
@ -85,16 +94,20 @@ fn main() -> Result<()> {
return Ok(());
} else {
scores.sort_by_key(|score| score.0);
let chosen = &res.out().cache.playlists[scores[0].1];
chosen
let chosen = &res.out().playlists[scores[0].1];
chosen.clone()
}
};
let mut app = ui::app::App::new(config, 15.0, chosen.clone())?;
let mut app = ui::app::App::new(res, 15.0, chosen)?;
app.run()?;
}
Command::Version => {
println!("{}", project_meta::version());
}
Command::GC { run_in, dry_run } => {
log::initialize_logging(None)?;
gc(run_in, dry_run)?;
}
}
Ok(())
}
@ -153,70 +166,69 @@ fn download(run_in: Option<PathBuf>, name: String) -> Result<()> {
}
}
let src = chosen.clone();
let dest = res.dirs().cache.clone();
download_playlist(src, dest)?;
download_playlist(src, &res.out().cache)?;
}
Ok(())
}
/// src: <playlist>.ron file (in playlists/)
/// dest: (cache/) directory (a new subdir will be created for this playlist)
fn download_playlist(playlist: schema::Playlist, dest: PathBuf) -> Result<()> {
let out_dir_name = playlist.name.to_snake_case();
let out_dir = dest.join(out_dir_name);
if out_dir.try_exists()? {
info!("Playlist already exists, checking for changes");
let dl_playlist_str = fs::read_to_string(out_dir.join("index.ron"))?;
let dl_playlist = ron::from_str::<schema::DlPlaylist>(&dl_playlist_str)?;
let diff = dl_playlist.gen_diff(&playlist);
if diff.changes.is_empty() {
info!("No changes to playlist, nothing to do");
return Ok(());
fn download_playlist(playlist: schema::Playlist, cache: &cache::CacheDir) -> Result<()> {
info!("downloading tracks in playlist {} to cache", playlist.name);
for track in &playlist.tracks {
info!("downloading {}", track.meta.name);
let source = playlist.find_source(&track.src).ok_or(anyhow!(
"Could not find source {} for track {}",
track.src,
track.meta.name
))?;
let hash = cache::Hash::generate(source, &track.input);
if cache.find(hash).is_some() {
info!("track exists in cache [skiping]");
continue;
}
diff.display();
println!("Apply changes? [y/N]:");
let Some(next) = io::stdin().lock().lines().next() else {
bail!("Failed to get input");
};
match next?.as_str() {
"y" | "Y" => {}
_ => {
info!("Aborting");
return Ok(());
let path = cache.create(hash);
source.execute(track.input.clone(), &path)?;
debug!("download complete");
}
info!("Done!");
Ok(())
}
fn gc(run_in: Option<PathBuf>, dry_run: bool) -> Result<()> {
let mut res = Resolver::new(resolve_run_path(run_in)?);
res.create_dirs()?;
res.resolve()?;
let mut hashes = HashSet::new();
let mut source_map = HashMap::new();
for playlist in &res.out().playlists {
for source in playlist.resolved_sources.as_ref().unwrap() {
source_map.insert(source.name.clone(), source.clone());
}
for track in &playlist.tracks {
let source = source_map
.get(&track.src)
.expect("Cannot find source for track");
let hash = cache::Hash::generate(source, &track.input);
hashes.insert(hash);
}
}
let mut bytes_removed = 0u64;
let mut files_removed = 0usize;
for entry in res.dirs().cache.read_dir()? {
let entry = entry?;
let hash = entry
.path()
.to_str()
.expect("path not utf-8")
.parse::<cache::Hash>()?;
if !hashes.contains(&hash) {
info!("deleting {}", hash.to_string());
bytes_removed += entry.metadata()?.len();
files_removed += 1;
if !dry_run {
fs::remove_file(entry.path())?;
}
}
} else {
info!("Downloading playlist {} to {:?}", playlist.name, out_dir);
fs::create_dir(&out_dir)?;
let mut dl_playlist = DlPlaylist {
directory: Default::default(),
name: playlist.name.clone(),
sources: playlist.resolved_sources.clone().unwrap(),
tracks: vec![],
};
for track in &playlist.tracks {
info!("Downloading {}", track.meta.name);
let source = playlist.find_source(&track.src).ok_or(anyhow!(
"Could not find source {} for track {}",
track.src,
track.meta.name
))?;
let uuid = Uuid::new_v4();
let path = out_dir.join(uuid.to_string());
source.execute(track.input.clone(), &path)?;
debug!("Download complete");
dl_playlist.tracks.push(schema::DlTrack {
track: track.clone(),
track_id: uuid,
});
}
let dl_playlist_str = ron::ser::to_string_pretty(
&dl_playlist,
ron::ser::PrettyConfig::new().struct_names(true),
)?;
fs::write(out_dir.join("index.ron"), dl_playlist_str.as_bytes())?;
info!("Downloading playlist complete");
}
info!("removed {files_removed} entries, freed {bytes_removed} bytes");
Ok(())
}

View File

@ -6,8 +6,9 @@ use std::{
use color_eyre::eyre::{anyhow, Result};
use crate::{
cache::CacheDir,
cfg::Config,
schema::{self, DlPlaylist, Playlist, Source},
schema::{self, Playlist, Source},
};
struct State {
@ -19,12 +20,7 @@ pub struct Output {
pub config: Config,
pub sources: Vec<Source>,
pub playlists: Vec<Playlist>,
pub cache: Cache,
}
#[derive(Default)]
pub struct Cache {
pub playlists: Vec<DlPlaylist>,
pub cache: CacheDir,
}
pub struct Directories {
@ -130,17 +126,7 @@ impl Resolver {
}
{
for pl_dir in fs::read_dir(&self.d.cache)?.filter_map(Result::ok) {
if pl_dir.file_type()?.is_dir() {
let index_path = pl_dir.path().join("index.ron");
let index_str = fs::read_to_string(&index_path)?;
let mut index = ron::from_str::<schema::DlPlaylist>(&index_str)?;
index.directory = pl_dir.path();
self.o.cache.playlists.push(index);
} else {
panic!("{pl_dir:?} in cache is not a directory");
}
}
self.o.cache = CacheDir::new(self.d.cache.clone());
}
self.s.resolved = true;

View File

@ -1,255 +1,16 @@
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
process::Command,
};
use color_eyre::eyre::{anyhow, bail, Result};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Link {
pub music_directory: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct DlPlaylist {
#[serde(skip)]
pub directory: PathBuf,
pub name: String,
/// resolved source
pub sources: Vec<Source>,
pub tracks: Vec<DlTrack>,
}
impl DlPlaylist {
#[allow(dead_code)]
pub fn find_source(&self, name: &str) -> Option<&Source> {
self.sources.iter().find(|x| x.name == name)
}
pub fn gen_diff(&self, other: &Playlist) -> DlPlaylistDiff {
let mut diff = DlPlaylistDiff { changes: vec![] };
if self.name != other.name {
diff.changes.push(DiffChange::Name {
old: self.name.clone(),
new: other.name.clone(),
});
}
let mut source_map: HashMap<String, Source> = HashMap::new();
// set of sources (by [new] name) that have changed output specification
let mut source_changed_output: HashSet<String> = HashSet::new();
// map of source names (old name -> new name)
let mut source_o2n_names: HashMap<String, String> = HashMap::new();
for source in self.sources.clone() {
source_map.insert(source.name.clone(), source);
}
for source in other.resolved_sources.clone().unwrap() {
if let Some(old) = source_map.remove(&source.name) {
if source.format != old.format {
source_changed_output.insert(source.name.clone());
diff.changes.push(DiffChange::SourceChangeFormat {
name: source.name.clone(),
old: old.format.clone(),
new: source.format.clone(),
})
}
// if these are not matching, then emit DiffChange::SourceReplaceKind
if old.kind != source.kind {
source_changed_output.insert(source.name.clone());
diff.changes.push(DiffChange::SourceModifyKind {
name: source.name.clone(),
old: old.kind,
new: source.kind,
});
}
} else {
diff.changes.push(DiffChange::AddSource { new: source });
}
}
for source in source_map.into_values() {
diff.changes.push(DiffChange::DelSource { removed: source });
}
let mut source_map: HashMap<SourceKind, String> = HashMap::new();
for source in self.sources.clone() {
source_map.insert(source.kind, source.name);
}
for source in other.sources.clone() {
if let Some(old) = source_map.remove(&source.kind) {
if old != source.name {
source_o2n_names.insert(old.clone(), source.name.clone());
diff.changes.push(DiffChange::SourceChangeName {
old,
new: source.name,
})
}
}
}
let mut track_map: HashMap<Meta, Track> = HashMap::new();
for track in self.tracks.clone() {
track_map.insert(track.track.meta.clone(), track.track.clone());
}
for track in other.tracks.clone() {
if let Some(old) = track_map.remove(&track.meta) {
if track.src != old.src || track.input != old.input {
diff.changes
.push(DiffChange::TrackChangedSource { old, new: track });
}
} else {
diff.changes.push(DiffChange::AddTrack { new: track });
}
}
for track in track_map.into_values() {
diff.changes.push(DiffChange::DelTrack { removed: track });
}
let mut track_map: HashMap<(String, ron::Value), Track> = HashMap::new();
for track in self.tracks.clone() {
track_map.insert(
(track.track.src.clone(), track.track.input.clone()),
track.track.clone(),
);
}
for track in other.tracks.clone() {
if let Some(old) = track_map.remove(&(track.src.clone(), track.input.clone())) {
if old.meta != track.meta {
diff.changes
.push(DiffChange::TrackLikelyChangedMeta { old, new: track })
}
}
}
diff
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct DlPlaylistDiff {
pub changes: Vec<DiffChange>,
}
impl DlPlaylistDiff {
pub fn display(&self) {
println!(" --- playlist diff --- ");
for change in &self.changes {
use DiffChange::*;
match change.clone() {
Name { old, new } => println!(" M [playlist/name]\t {old} -> {new}"),
AddSource { new } => println!(
" + [source]\t {name} | output: {format} | kind: {variant}",
name = new.name,
format = new.format,
variant = match new.kind {
SourceKind::Shell { .. } => "shell",
}
),
DelSource { removed } => println!(" - [source]\t {name}", name = removed.name),
SourceChangeName { old, new } => println!(" M [source/name]\t {old} -> {new}"),
SourceChangeFormat { name, old, new } => {
println!(" M [source/format]\t of source {name}: {old} -> {new}")
}
SourceReplaceKind { name, old, new } => {
println!(
" M [source/kind]\t of source {name}: {old} -> {new}",
old = match old {
SourceKind::Shell { .. } => "shell",
},
new = match new {
SourceKind::Shell { .. } => "shell",
}
)
}
SourceModifyKind { name, old, new } => match (old, new) {
(
SourceKind::Shell {
cmd: old_cmd,
args: old_args,
},
SourceKind::Shell { cmd, args },
) => {
if old_cmd != cmd && old_args != args {
println!(" M [source/kind/all]\t of source {name}:\n M [cmd]\t {old_cmd} -> {cmd}\n M [args]\t {old_args:?} -> {args:?}")
} else if old_cmd != cmd {
println!(" M [source/kind/-cmd]\t of source {name}: {old_cmd} -> {cmd}")
} else if old_args != args {
println!(" M [source/kind/-args]\t of source {name}: {old_args:?} -> {args:?}")
}
}
},
AddTrack { new } => println!(
" + [track]\t '{name}' by {artist}",
name = new.meta.name,
artist = new.meta.artist
),
DelTrack { removed } => println!(
" - [track]\t '{name}' by {artist}",
name = removed.meta.name,
artist = removed.meta.artist
),
TrackLikelyChangedMeta { old, new } => println!(" M [track/rename]\n M [track/rename/name]\t {} -> {}\n M [track/rename/artist]\t {} -> {}", old.meta.name, new.meta.name, old.meta.artist, new.meta.artist),
TrackChangedSource { old, new } => println!(" M [track/source]\t track '{name}' by {artist}: {} with {:?} -> {} with {:?}", old.src, old.input, new.src, new.input, name=old.meta.name, artist=old.meta.artist,),
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum DiffChange {
Name {
old: String,
new: String,
},
AddSource {
new: Source,
},
DelSource {
removed: Source,
},
SourceChangeName {
old: String,
new: String,
},
SourceChangeFormat {
name: String,
old: String,
new: String,
},
SourceReplaceKind {
name: String,
old: SourceKind,
new: SourceKind,
},
/// modification to the *content* of SourceKind (variant is the same)
SourceModifyKind {
name: String,
old: SourceKind,
new: SourceKind,
},
AddTrack {
new: Track,
},
DelTrack {
removed: Track,
},
/// the metadata of a track (name or author) changed, but the `src` and `input` did not.
/// this likely means that the track was re-named
TrackLikelyChangedMeta {
old: Track,
new: Track,
},
/// the metadata of a track (name/author) is the same, but the `src` and/or `input` changed.
TrackChangedSource {
old: Track,
new: Track,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct DlTrack {
pub track: Track,
pub track_id: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Playlist {
#[serde(skip)]
@ -294,18 +55,12 @@ impl Source {
let args = args
.iter()
.map(|arg| {
Ok(if arg.contains("${input}") {
arg.replace("${input}", &input)
} else if arg.contains("${output}") {
arg.replace(
"${output}",
output
.to_str()
.ok_or(anyhow!("output path not valid UTF-8"))?,
)
} else {
arg.to_owned()
})
Ok(arg.replace("${input}", &input).replace(
"${output}",
output
.to_str()
.ok_or(anyhow!("output path not valid UTF-8"))?,
))
})
.collect::<Result<Vec<String>>>()?;
let res = Command::new(cmd).args(args).status()?;

View File

@ -1,3 +1,5 @@
use std::sync::Arc;
use color_eyre::eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::prelude::Rect;
@ -8,7 +10,7 @@ use super::{
mode::Mode,
tui,
};
use crate::{cfg::Config, schema::DlPlaylist};
use crate::{resolver::Resolver, schema::Playlist};
pub struct App {
pub frame_rate: f64,
@ -16,12 +18,13 @@ pub struct App {
pub should_quit: bool,
pub mode: Mode,
pub last_tick_key_events: Vec<KeyEvent>,
pub config: Config,
pub resolver: Arc<Resolver>,
}
impl App {
pub fn new(config: Config, frame_rate: f64, pl: DlPlaylist) -> Result<Self> {
let home = Home::new(pl)?;
pub fn new(res: Resolver, frame_rate: f64, pl: Playlist) -> Result<Self> {
let resolver = Arc::new(res);
let home = Home::new(pl.clone(), resolver.clone())?;
let fps = FpsCounter::default();
let mode = Mode::Home;
Ok(Self {
@ -30,7 +33,7 @@ impl App {
should_quit: false,
mode,
last_tick_key_events: Vec::new(),
config,
resolver,
})
}
@ -45,7 +48,7 @@ impl App {
}
for component in self.components.iter_mut() {
component.register_config_handler(self.config.clone())?;
component.register_config_handler(self.resolver.out().config.clone())?;
}
for component in self.components.iter_mut() {
@ -59,7 +62,7 @@ impl App {
tui::Event::Render => action_tx.send(Action::Render)?,
tui::Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
tui::Event::Key(key) => {
if let Some(keymap) = self.config.keybinds.get(&self.mode) {
if let Some(keymap) = self.resolver.out().config.keybinds.get(&self.mode) {
if let Some(action) = keymap.get(&vec![key]) {
log::info!("Got action: {action:?}");
action_tx.send(action.clone())?;

View File

@ -1,4 +1,4 @@
use std::{cmp, fs, iter, path::PathBuf, sync::Arc};
use std::{cmp, fs, iter, sync::Arc};
use color_eyre::eyre::{anyhow, bail, Result};
use cpal::traits::{DeviceTrait, HostTrait};
@ -9,9 +9,11 @@ use ratatui::{prelude::*, widgets::*};
use super::{Component, Frame};
use crate::{
cache,
cfg::{self, Config},
player2::{self, SingleTrackPlayer},
schema::DlPlaylist,
resolver::Resolver,
schema::Playlist,
ui::{action::Action, mode::Mode, symbol},
};
@ -51,8 +53,7 @@ pub struct Home {
command_tx: Option<Sender<Action>>,
// info bar
c_track_idx: usize,
playlist: DlPlaylist,
playlist_dir: PathBuf,
playlist: Playlist,
// player
player: SingleTrackPlayer,
sel_method: TrackSelectionMethod,
@ -65,11 +66,13 @@ pub struct Home {
t_list_state: ListState,
/// jump to track # when receiving TrackComplete (takes precedence over normal track selection)
jump_on_track_complete: Option<usize>,
// resolver
resolver: Arc<Resolver>,
}
impl Home {
pub fn new(dl_pl: DlPlaylist) -> Result<Self> {
info!("Loaded playlist {name}", name = dl_pl.name);
pub fn new(pl: Playlist, res: Arc<Resolver>) -> Result<Self> {
info!("Loaded playlist {name}", name = pl.name);
debug!("Initializing audio backend");
let host = cpal::default_host();
@ -86,12 +89,10 @@ impl Home {
};
let player = SingleTrackPlayer::new(Arc::new(config), Arc::new(device))?;
let pl_dir = dl_pl.directory.clone();
Ok(Self {
command_tx: None,
c_track_idx: 0,
playlist: dl_pl,
playlist_dir: pl_dir,
playlist: pl,
player,
sel_method: TrackSelectionMethod::Sequential,
repeat: Repeat::RepeatPlaylist,
@ -99,6 +100,7 @@ impl Home {
cfg: Config::default(),
t_list_state: ListState::default().with_selected(Some(0)),
jump_on_track_complete: None,
resolver: res,
})
}
@ -140,21 +142,24 @@ impl Home {
return Ok(());
}
let track = self.playlist.tracks.get(self.c_track_idx).unwrap();
let track_path =
self.playlist_dir
.read_dir()?
.find(|res| {
res.as_ref().is_ok_and(|entry| {
entry.path().file_stem().is_some_and(|name| {
name.to_string_lossy() == track.track_id.to_string()
})
})
})
.ok_or(anyhow!("BUG: could not file file for downloaded track"))??
.path();
let hash = cache::Hash::generate(
self.resolver
.out()
.sources
.iter()
.find(|x| x.name == track.src)
.ok_or(anyhow!("could not find track source"))?,
&track.input,
);
let track_path = self
.resolver
.out()
.cache
.find(hash)
.ok_or(anyhow!("could not find file for track!"))?;
let track_fmt = self
.playlist
.find_source(&track.track.src)
.find_source(&track.src)
.unwrap()
.format
.clone();
@ -204,8 +209,8 @@ impl Component for Home {
.summary("DMM Player")
.body(&format!(
"Now Playing: {name}\nby {artist}",
name = track.track.meta.name,
artist = track.track.meta.artist
name = track.meta.name,
artist = track.meta.artist
))
.show()?;
}
@ -338,7 +343,6 @@ impl Component for Home {
.into(),
"".fg(Color::Yellow),
self.playlist.tracks[self.c_track_idx]
.track
.meta
.name
.clone()
@ -379,7 +383,7 @@ impl Component for Home {
);
f.render_widget(playlist, info_layout[0]);
let sel_track = &self.playlist.tracks[self.t_list_state.selected().unwrap()].track;
let sel_track = &self.playlist.tracks[self.t_list_state.selected().unwrap()];
let track = Paragraph::new(vec![
Line::from(sel_track.meta.name.clone().italic()),
Line::from(vec!["by: ".bold(), sel_track.meta.artist.clone().into()]),
@ -449,7 +453,7 @@ impl Component for Home {
},
i.to_string().into(),
": ".into(),
track.track.meta.name.clone().italic(),
track.meta.name.clone().italic(),
]))
})
.collect::<Vec<_>>(),