-
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathplaylist.rs
More file actions
99 lines (92 loc) · 3.92 KB
/
playlist.rs
File metadata and controls
99 lines (92 loc) · 3.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//! Analyze a folder recursively, and make a playlist out of the file
//! provided by the user.
//! How to use: ./playlist [-o file.m3u] [-a analysis.json] <folder> <file to start the playlist from>
use anyhow::Result;
use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
use bliss_audio::decoder::Decoder as DecoderTrait;
use bliss_audio::playlist::{closest_to_songs, dedup_playlist, euclidean_distance};
use bliss_audio::Song;
use clap::{Arg, Command};
use glob::glob;
use std::env;
use std::fs;
use std::io::BufReader;
use std::path::{Path, PathBuf};
/* Analyzes a folder recursively, and make a playlist out of the file
* provided by the user. */
// How to use: ./playlist [-o file.m3u] [-a analysis.json] <folder> <file to start the playlist from>
fn main() -> Result<()> {
let matches = Command::new("playlist")
.version(env!("CARGO_PKG_VERSION"))
.author("Polochon_street")
.about("Analyze a folder and make a playlist from a target song")
.arg(Arg::new("output-playlist").short('o').long("output-playlist")
.value_name("PLAYLIST.M3U")
.help("Outputs the playlist to a file.")
.num_args(1))
.arg(Arg::new("analysis-file").short('a').long("analysis-file")
.value_name("ANALYSIS.JSON")
.help("Use the songs that have been analyzed in <analysis-file>, and appends newly analyzed songs to it. Defaults to /tmp/analysis.json.")
.default_value("/tmp/analysis.json")
.num_args(1))
.arg(Arg::new("FOLDER").help("Folders containing some songs.").required(true))
.arg(Arg::new("FIRST-SONG").help("Song to start from (can be outside of FOLDER).").required(true))
.get_matches();
let folder = matches.get_one::<String>("FOLDER").unwrap();
let file = fs::canonicalize(matches.get_one::<String>("FIRST-SONG").unwrap())?;
let pattern = Path::new(folder).join("**").join("*");
let mut songs: Vec<Song> = Vec::new();
let analysis_path = matches.get_one::<String>("analysis-file").unwrap();
let analysis_file = fs::File::open(analysis_path);
if let Ok(f) = analysis_file {
let reader = BufReader::new(f);
songs = serde_json::from_reader(reader)?;
}
let analyzed_paths = songs
.iter()
.map(|s| s.path.to_owned())
.collect::<Vec<PathBuf>>();
let paths = glob(&pattern.to_string_lossy())?
.map(|e| fs::canonicalize(e.unwrap()).unwrap())
.filter(|e| match mime_guess::from_path(e).first() {
Some(m) => m.type_() == "audio",
None => false,
})
.map(|x| x.to_string_lossy().to_string())
.collect::<Vec<String>>();
let song_iterator = Decoder::analyze_paths(
paths
.iter()
.filter(|p| !analyzed_paths.contains(&PathBuf::from(p)))
.map(|p| p.to_owned())
.collect::<Vec<String>>(),
);
let first_song = Decoder::song_from_path(file)?;
let mut analyzed_songs = vec![first_song.to_owned()];
for (path, result) in song_iterator {
match result {
Ok(song) => analyzed_songs.push(song),
Err(e) => println!("error analyzing {}: {}", path.display(), e),
};
}
let serialized = serde_json::to_string(&analyzed_songs).unwrap();
fs::write(analysis_path, serialized)?;
analyzed_songs.extend_from_slice(&songs);
let songs_to_chose_from: Vec<_> = analyzed_songs
.into_iter()
.filter(|x| x == &first_song || paths.contains(&x.path.to_string_lossy().to_string()))
.collect();
let playlist = dedup_playlist(
closest_to_songs(&[first_song], &songs_to_chose_from, &euclidean_distance),
None,
)
.map(|s| s.path.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join("\n");
if let Some(m) = matches.get_one::<String>("output-playlist") {
fs::write(m, playlist)?;
} else {
println!("{playlist}");
}
Ok(())
}