cli/
cli.rs

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 media at the specified index in the playlist
26    Play {
27        /// The index of the media to play
28        index: Option<usize>,
29    },
30
31    /// Pause the currently playing media
32    Pause,
33
34    /// Stop the playback and quit MPV
35    Stop,
36
37    /// Skip to the next item in the playlist
38    Next,
39
40    /// Skip to the previous item in the playlist
41    Prev,
42
43    /// Seek to a specific position in the currently playing media
44    Seek {
45        /// The number of seconds to seek to
46        seconds: i32,
47    },
48
49    /// Move an item in the playlist from one index to another
50    Move {
51        /// The index of the item to move
52        index1: usize,
53
54        /// The index to move the item to
55        index2: usize,
56    },
57
58    /// Remove an item from the playlist
59    ///
60    /// If invoked while playlist has no entries, or if the only entry
61    /// is the active video, then this will exit MPV.
62    Remove {
63        /// The index of the item to remove (optional)
64        index: Option<usize>,
65    },
66
67    /// Clear the entire playlist
68    Clear,
69
70    /// List all the items in the playlist
71    List,
72
73    /// Add files to the playlist
74    ///
75    /// Needs at least one file to be passed.
76    Add {
77        /// The filenames of the files to add
78        filenames: Vec<String>,
79    },
80
81    /// Replace the current playlist with new files
82    Replace {
83        /// The filenames of the files to replace the playlist with
84        filenames: Vec<String>,
85    },
86
87    /// Fetch properties of the current playback or playlist
88    Prop {
89        /// The properties to fetch
90        properties: Vec<String>,
91    },
92
93    /// Enter interactive mode to send commands to MPV IPC
94    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                // I don't like this either, but it looks cleaner than a multi-line
220                // print macro just cramped in here.
221                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}