1use clap::{Parser, Subcommand};
2use mrc::set_property;
3use mrc::SOCKET_PATH;
4use mrc::{
5 get_property, loadfile, playlist_clear, playlist_move, playlist_next, playlist_prev,
6 playlist_remove, quit, seek,
7};
8use serde_json::json;
9use std::io::{self, Write};
10use std::path::PathBuf;
11use tracing::{debug, error, info};
12
13#[derive(Parser)]
14#[command(author, version, about)]
15struct Cli {
16 #[arg(short, long, global = true)]
17 debug: bool,
18
19 #[command(subcommand)]
20 command: CommandOptions,
21}
22
23#[derive(Subcommand)]
24enum CommandOptions {
25 Play {
27 index: Option<usize>,
29 },
30
31 Pause,
33
34 Stop,
36
37 Next,
39
40 Prev,
42
43 Seek {
45 seconds: i32,
47 },
48
49 Move {
51 index1: usize,
53
54 index2: usize,
56 },
57
58 Remove {
63 index: Option<usize>,
65 },
66
67 Clear,
69
70 List,
72
73 Add {
77 filenames: Vec<String>,
79 },
80
81 Replace {
83 filenames: Vec<String>,
85 },
86
87 Prop {
89 properties: Vec<String>,
91 },
92
93 Interactive,
95}
96
97#[tokio::main]
98async fn main() -> io::Result<()> {
99 tracing_subscriber::fmt::init();
100 let cli = Cli::parse();
101
102 if !PathBuf::from(SOCKET_PATH).exists() {
103 debug!(SOCKET_PATH);
104 error!("Error: MPV socket not found. Is MPV running?");
105 return Ok(());
106 }
107
108 match cli.command {
109 CommandOptions::Play { index } => {
110 if let Some(idx) = index {
111 info!("Playing media at index: {}", idx);
112 set_property("playlist-pos", &json!(idx), None).await?;
113 }
114 info!("Unpausing playback");
115 set_property("pause", &json!(false), None).await?;
116 }
117
118 CommandOptions::Pause => {
119 info!("Pausing playback");
120 set_property("pause", &json!(true), None).await?;
121 }
122
123 CommandOptions::Stop => {
124 info!("Stopping playback and quitting MPV");
125 quit(None).await?;
126 }
127
128 CommandOptions::Next => {
129 info!("Skipping to next item in the playlist");
130 playlist_next(None).await?;
131 }
132
133 CommandOptions::Prev => {
134 info!("Skipping to previous item in the playlist");
135 playlist_prev(None).await?;
136 }
137
138 CommandOptions::Seek { seconds } => {
139 info!("Seeking to {} seconds", seconds);
140 seek(seconds.into(), None).await?;
141 }
142
143 CommandOptions::Move { index1, index2 } => {
144 info!("Moving item from index {} to {}", index1, index2);
145 playlist_move(index1, index2, None).await?;
146 }
147
148 CommandOptions::Remove { index } => {
149 if let Some(idx) = index {
150 info!("Removing item at index {}", idx);
151 playlist_remove(Some(idx), None).await?;
152 } else {
153 info!("Removing current item from playlist");
154 playlist_remove(None, None).await?;
155 }
156 }
157
158 CommandOptions::Clear => {
159 info!("Clearing the playlist");
160 playlist_clear(None).await?;
161 }
162
163 CommandOptions::List => {
164 info!("Listing playlist items");
165 if let Some(data) = get_property("playlist", None).await? {
166 println!("{}", serde_json::to_string_pretty(&data)?);
167 }
168 }
169
170 CommandOptions::Add { filenames } => {
171 if filenames.is_empty() {
172 let e = "No files provided to add to the playlist";
173 error!("{}", e);
174 return Err(io::Error::new(io::ErrorKind::InvalidInput, e));
175 }
176
177 info!("Adding {} files to the playlist", filenames.len());
178 for filename in filenames {
179 loadfile(&filename, true, None).await?;
180 }
181 }
182
183 CommandOptions::Replace { filenames } => {
184 info!("Replacing current playlist with {} files", filenames.len());
185 if let Some(first_file) = filenames.first() {
186 loadfile(first_file, false, None).await?;
187 for filename in &filenames[1..] {
188 loadfile(filename, true, None).await?;
189 }
190 }
191 }
192
193 CommandOptions::Prop { properties } => {
194 info!("Fetching properties: {:?}", properties);
195 for property in properties {
196 if let Some(data) = get_property(&property, None).await? {
197 println!("{property}: {data}");
198 }
199 }
200 }
201
202 CommandOptions::Interactive => {
203 println!("Entering interactive mode. Type 'exit' to quit.");
204 let stdin = io::stdin();
205 let mut stdout = io::stdout();
206
207 loop {
208 print!("mpv> ");
209 stdout.flush()?;
210 let mut input = String::new();
211 stdin.read_line(&mut input)?;
212 let trimmed = input.trim();
213
214 if trimmed.eq_ignore_ascii_case("exit") {
215 println!("Exiting interactive mode.");
216 break;
217 }
218
219 let commands = vec![
222 (
223 "play [index]",
224 "Play or unpause playback, optionally at the specified index",
225 ),
226 ("pause", "Pause playback"),
227 ("stop", "Stop playback and quit MPV"),
228 ("next", "Skip to the next item in the playlist"),
229 ("prev", "Skip to the previous item in the playlist"),
230 ("seek <seconds>", "Seek to the specified position"),
231 ("clear", "Clear the playlist"),
232 ("list", "List all items in the playlist"),
233 ("add <files>", "Add files to the playlist"),
234 ("get <property>", "Get the specified property"),
235 (
236 "set <property> <value>",
237 "Set the specified property to a value",
238 ),
239 ("exit", "Quit interactive mode"),
240 ];
241
242 if trimmed.eq_ignore_ascii_case("help") {
243 println!("Valid commands:");
244 for (command, description) in commands {
245 println!(" {} - {}", command, description);
246 }
247 continue;
248 }
249
250 let parts: Vec<&str> = trimmed.split_whitespace().collect();
251 match parts.as_slice() {
252 ["play"] => {
253 info!("Unpausing playback");
254 set_property("pause", &json!(false), None).await?;
255 }
256
257 ["play", index] => {
258 if let Ok(idx) = index.parse::<usize>() {
259 info!("Playing media at index: {}", idx);
260 set_property("playlist-pos", &json!(idx), None).await?;
261 set_property("pause", &json!(false), None).await?;
262 } else {
263 println!("Invalid index: {}", index);
264 }
265 }
266
267 ["pause"] => {
268 info!("Pausing playback");
269 set_property("pause", &json!(true), None).await?;
270 }
271
272 ["stop"] => {
273 info!("Pausing playback");
274 quit(None).await?;
275 }
276
277 ["next"] => {
278 info!("Skipping to next item in the playlist");
279 playlist_next(None).await?;
280 }
281
282 ["prev"] => {
283 info!("Skipping to previous item in the playlist");
284 playlist_prev(None).await?;
285 }
286
287 ["seek", seconds] => {
288 if let Ok(sec) = seconds.parse::<i32>() {
289 info!("Seeking to {} seconds", sec);
290 seek(sec.into(), None).await?;
291 } else {
292 println!("Invalid seconds: {}", seconds);
293 }
294 }
295
296 ["clear"] => {
297 info!("Clearing the playlist");
298 playlist_clear(None).await?;
299 }
300
301 ["list"] => {
302 info!("Listing playlist items");
303 if let Some(data) = get_property("playlist", None).await? {
304 println!("{}", serde_json::to_string_pretty(&data)?);
305 }
306 }
307
308 ["add", files @ ..] => {
309 if files.is_empty() {
310 println!("No files provided to add to the playlist");
311 } else {
312 info!("Adding {} files to the playlist", files.len());
313 for file in files {
314 loadfile(file, true, None).await?;
315 }
316 }
317 }
318
319 ["get", property] => {
320 if let Some(data) = get_property(property, None).await? {
321 println!("{property}: {data}");
322 }
323 }
324
325 ["set", property, value] => {
326 let json_value = serde_json::from_str::<serde_json::Value>(value)
327 .unwrap_or_else(|_| json!(value));
328 set_property(property, &json_value, None).await?;
329 println!("Set {property} to {value}");
330 }
331
332 _ => {
333 println!("Unknown command: {}", trimmed);
334 println!("Valid commands: play <index>, pause, stop, next, prev, seek <seconds>, clear, list, add <files>, get <property>, set <property> <value>, exit");
335 }
336 }
337 }
338 }
339 }
340
341 Ok(())
342}