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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
use clap::{Parser, Subcommand};
use std::error::Error;
use crate::MidiPlayer;
// use reqwest::blocking as reqwest_blocking;
#[derive(Parser)]
#[command(name = env!("CARGO_PKG_NAME"))]
#[command(about = env!("CARGO_PKG_DESCRIPTION"))]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(author = env!("CARGO_PKG_AUTHORS"))]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
/// Loop the entire playlist continuously
#[arg(long)]
pub loop_playlist: bool,
/// Loop individual songs
#[arg(long)]
pub loop_individual_songs: bool,
/// Delay between songs in seconds
#[arg(long, default_value = "0")]
pub delay_between_songs: u32,
/// Scan segment duration in seconds
#[arg(long, default_value = "30")]
pub scan_duration: u32,
/// Start scan segments at random positions
#[arg(long)]
pub scan_random_start: bool,
/// Use TUI mode with split panels (menu + playback info)
#[arg(short = 't', long)]
pub tui: bool,
/// Add MIDI files to the dynamic playlist
#[arg(long = "add-song")]
pub add_songs: Vec<std::path::PathBuf>,
/// Scan directories and add all MIDI files to the dynamic playlist
#[arg(long = "scan-directory")]
pub scan_directories: Vec<std::path::PathBuf>,
/// Enable IPC event publishing for playback
#[arg(long)]
pub ipc: bool,
}
#[derive(Subcommand)]
pub enum Commands {
/// List all available songs
List,
/// Play a specific song
Play {
/// Song index to play
song_index: usize,
/// Track numbers to play (comma-separated, 0 for all tracks)
#[arg(long, value_delimiter = ',')]
tracks: Option<Vec<usize>>,
/// Tempo in BPM
#[arg(long)]
tempo: Option<u32>,
},
/// Play all songs in sequence
PlayAll,
/// Play songs in random order
PlayRandom,
/// Scan mode - play portions of songs
Scan {
/// Scan mode: 1=sequential, 2=random positions, 3=progressive
#[arg(long, default_value = "1")]
mode: u32,
/// Duration of each scan segment in seconds
#[arg(long)]
duration: Option<u32>,
},
/// List only dynamically loaded songs
ListDynamic,
/// Clear all dynamically loaded songs
ClearDynamic,
/// Run in interactive mode (default)
Interactive,
}
pub fn run_cli() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
let mut player = MidiPlayer::new()?; // Apply CLI configuration
{
let config = player.get_config_mut();
config.loop_playlist = cli.loop_playlist;
config.loop_individual_songs = cli.loop_individual_songs;
config.delay_between_songs_ms = cli.delay_between_songs * 1000;
config.scan_segment_duration_ms = cli.scan_duration * 1000;
config.scan_random_start = cli.scan_random_start;
}
player.init_ipc_publisher()?; // Initialize IPC publisher
// Process global options to add songs/directories to dynamic playlist
for path in &cli.add_songs {
let path_str = path.to_string_lossy();
// if path_str.starts_with("http://") || path_str.starts_with("https://") {
// // Download file from URL
// match reqwest_blocking::get(path_str.as_ref()) {
// Ok(mut resp) => {
// if resp.status().is_success() {
// let mut buf = Vec::new();
// if let Err(e) = resp.read_to_end(&mut buf) {
// eprintln!("❌ Failed to read from {}: {}", path_str, e);
// continue;
// }
// // Guess file type from URL
// if path_str.ends_with(".mid") || path_str.ends_with(".midi") {
// if let Err(e) = player.add_song_from_midi_data(&buf, Some(&path_str)) {
// eprintln!("❌ Failed to add MIDI from {}: {}", path_str, e);
// }
// } else if path_str.ends_with(".xml") || path_str.ends_with(".musicxml") {
// if let Err(e) = player.add_song_from_musicxml_data(&buf, Some(&path_str)) {
// eprintln!("❌ Failed to add MusicXML from {}: {}", path_str, e);
// }
// } else {
// eprintln!("❌ Unknown file type for URL: {}", path_str);
// }
// } else {
// eprintln!("❌ Failed to download {}: HTTP {}", path_str, resp.status());
// }
// }
// Err(e) => {
// eprintln!("❌ Failed to download {}: {}", path_str, e);
// }
// }
// } else
if path_str.ends_with(".mid") || path_str.ends_with(".midi") {
if let Err(e) = player.add_song_from_file(path) {
eprintln!("❌ Failed to add {}: {}", path.display(), e);
}
// } else if path_str.ends_with(".xml") || path_str.ends_with(".musicxml") {
// if let Err(e) = player.add_song_from_musicxml_file(path) {
// eprintln!("❌ Failed to add {}: {}", path.display(), e);
// }
} else {
eprintln!("❌ Unknown file type: {}", path.display());
}
}
// for path in &cli.scan_directories {
// match player.scan_directory_with_musicxml(path) {
// Ok(count) => println!("✅ Added {} songs from {}", count, path.display()),
// Err(e) => eprintln!("❌ Failed to scan {}: {}", path.display(), e),
// }
// }
for path in &cli.scan_directories {
match player.scan_directory(path) {
Ok(count) => println!("✅ Added {} songs from {}", count, path.display()),
Err(e) => eprintln!("❌ Failed to scan {}: {}", path.display(), e),
}
}
match cli.command {
Some(Commands::List) => {
player.list_songs();
}
Some(Commands::Play {
song_index,
tracks,
tempo,
}) => {
if song_index >= player.get_songs().len() {
eprintln!(
"❌ Invalid song index {}. Use 'list' command to see available songs.",
song_index
);
std::process::exit(1);
}
let loop_individual = player.get_config().loop_individual_songs;
let result: Result<(), Box<dyn Error>> = if loop_individual {
// For looping, we need to handle it differently
loop {
if cli.ipc {
player.play_song_with_ipc(song_index)?;
} else {
let continue_playing =
player.play_song(song_index, tracks.clone(), tempo)?;
if !continue_playing {
break;
}
}
}
Ok(())
} else {
if cli.ipc {
player.play_song_with_ipc(song_index)?;
} else {
player.play_song(song_index, tracks, tempo)?;
}
Ok(())
};
result?;
}
Some(Commands::PlayAll) => {
if cli.ipc {
// Play all songs with IPC event publishing
for i in 0..player.get_total_song_count() {
player.play_song_with_ipc(i)?;
}
} else {
player.play_all_songs()?;
}
}
Some(Commands::PlayRandom) => {
player.play_random_song()?;
}
Some(Commands::Scan { mode, duration }) => {
let scan_duration = duration.unwrap_or(cli.scan_duration);
player.scan_mode_non_interactive(scan_duration, mode)?;
}
Some(Commands::ListDynamic) => {
player.list_dynamic_songs();
}
Some(Commands::ClearDynamic) => {
player.clear_dynamic_songs();
}
Some(Commands::Interactive) | None => {
// Choose between TUI and CLI mode
if cli.tui {
player.run_tui_mode()?;
} else {
player.run_interactive()?;
}
}
}
Ok(())
}
pub fn print_help() {
let _cli = Cli::parse_from(["e_midi", "--help"]);
}
// Helper function to validate song index
pub fn validate_song_index(player: &MidiPlayer, index: usize) -> Result<(), String> {
if index >= player.get_songs().len() {
Err(format!(
"Invalid song index {}. Available songs: 0-{}",
index,
player.get_songs().len() - 1
))
} else {
Ok(())
}
}
// Helper function to format song list for CLI output
pub fn format_song_list(player: &MidiPlayer) -> String {
let mut output = String::new();
output.push_str("Available Songs:\n");
for (i, song) in player.get_songs().iter().enumerate() {
output.push_str(&format!(
"{}: {} ({} tracks, default tempo: {} BPM)\n",
i,
song.name,
song.tracks.len(),
song.default_tempo
));
}
output
}