commit 14c46fc5f208c2a4f3eed4adfc34f59bd84345a7 Author: mrkmntal Date: Fri Sep 18 19:37:44 2026 -0400 Initial Bash mpv remote diff --git a/mpvremote2.sh b/mpvremote2.sh new file mode 100755 index 0000000..961cf72 --- /dev/null +++ b/mpvremote2.sh @@ -0,0 +1,359 @@ +#!/usr/bin/env bash + +# ============================================================================== +# GPLv3: You better join us now and share the software, motherfucker. +# Keep the code free as in freedom. +# If you're gonna sell it, give 'em the code too. +# +# WTF IS THIS? +# A quick-and-dirty "TV style" remote control for mpv over a UNIX socket. +# +# ASSUMPTIONS: +# 1. mpv is already running. +# 2. mpv was launched with: +# --input-ipc-server=/tmp/mpv-socket +# or the socket is configured in mpv.conf. +# ============================================================================== + +SOCKET="/tmp/mpv-socket" +MEDIA_DIR="$HOME/GDrive/Videos" + +if [[ ! -S "$SOCKET" ]]; then + echo "Error: Socket $SOCKET not found or mpv is not running." + exit 1 +fi + +# ------------------------------------------------------------------------------ +# Send an mpv IPC command. +# +# Usage: +# mpv-cmd "cycle" "pause" +# mpv-cmd "seek" "10" +# mpv-cmd "loadfile" "/path/to/file" "append-play" +# ------------------------------------------------------------------------------ + +mpv-cmd() { + local json_args="" + local arg + + for arg in "$@"; do + # Escape JSON string characters. + arg="${arg//\\/\\\\}" + arg="${arg//\"/\\\"}" + arg="${arg//$'\n'/\\n}" + arg="${arg//$'\r'/\\r}" + arg="${arg//$'\t'/\\t}" + + if [[ -n "$json_args" ]]; then + json_args+="," + fi + + json_args+="\"$arg\"" + done + + printf '{"command":[%s]}\n' "$json_args" | + socat -t 1 - "$SOCKET" >/dev/null 2>&1 +} + +# ------------------------------------------------------------------------------ +# Get an mpv property. +# +# Requires jq. This avoids trying to parse JSON with sed. +# ------------------------------------------------------------------------------ + +mpv-get() { + local prop="$1" + + if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq is required for property queries." >&2 + return 1 + fi + + printf '{"command":["get_property","%s"]}\n' "$prop" | + socat -t 1 - "$SOCKET" 2>/dev/null | + jq -r '.data // empty' +} + +# ------------------------------------------------------------------------------ +# Queue a video. +# ------------------------------------------------------------------------------ + +queue_picker() { + echo + echo "--- Queue Media from $MEDIA_DIR ---" + + if [[ ! -d "$MEDIA_DIR" ]]; then + echo "Directory not found: $MEDIA_DIR" + return 1 + fi + + local clean_dir="${MEDIA_DIR%/}" + declare -A video_map + + local exts=( + "mp4" + "mkv" + "avi" + "webm" + ) + + local find_args=() + + for ext in "${exts[@]}"; do + if [[ ${#find_args[@]} -gt 0 ]]; then + find_args+=("-o") + fi + + find_args+=("-iname" "*.$ext") + done + + while IFS= read -r -d '' file; do + local rel_name="${file#"${clean_dir}/"}" + video_map["$rel_name"]="$file" + done < <( + find "$clean_dir" \ + -type f \ + \( "${find_args[@]}" \) \ + -print0 + ) + + if [[ ${#video_map[@]} -eq 0 ]]; then + echo "No video files found." + return 1 + fi + + local selected_key="" + + if command -v fzf >/dev/null 2>&1; then + selected_key=$( + printf '%s\n' "${!video_map[@]}" | + sort | + fzf --prompt="Select video to queue: " + ) + else + echo "Choose a file to queue (or Cancel):" + + PS3="Enter number: " + + local keys=("${!video_map[@]}") + + select choice in "${keys[@]}" "Cancel"; do + if [[ "$choice" == "Cancel" || -z "$choice" ]]; then + echo "Cancelled." + return 0 + fi + + selected_key="$choice" + break + done + fi + + if [[ -n "$selected_key" && -n "${video_map[$selected_key]:-}" ]]; then + local abs_path="${video_map[$selected_key]}" + + echo "Queuing: $(basename "$abs_path")" + + mpv-cmd "loadfile" "$abs_path" "append-play" + mpv-cmd "set_property" "pause" "no" + fi +} + +cleanup() { + if [[ -n "${old_stty:-}" ]]; then + stty "$old_stty" 2>/dev/null + else + stty sane 2>/dev/null + fi + echo +} + +# ------------------------------------------------------------------------------ +# Edit the current playlist via fzf. +# +# Enter -> skip to selected item +# Del -> remove selected item +# q/Esc/Left -> return to remote +# ------------------------------------------------------------------------------ + +edit_playlist() { + local playlist_json fzf_input selected key_pressed idx + + while true; do + playlist_json=$(mpv-get "playlist") + + if [[ -z "$playlist_json" || "$playlist_json" == "[]" ]]; then + echo "Playlist is empty." + return + fi + + fzf_input=$(printf '%s' "$playlist_json" | jq -r ' + to_entries[] | + (if .value.current == true then "▶ " else "" end) as $prefix | + "\(.key)|\($prefix)\(.value.title // (.value.filename | split("/") | last))" + ') + + if [[ -z "$fzf_input" ]]; then + echo "Playlist is empty." + return + fi + + readarray -t result < <( + printf '%s\n' "$fzf_input" | fzf \ + --delimiter='|' \ + --with-nth=2 \ + --prompt="Playlist (Enter=skip, Del=remove, Esc/q/←=back): " \ + --expect=del,left,esc,q \ + --header="Current Playlist" + ) + + key_pressed="${result[0]:-}" + selected="${result[1]:-}" + + if [[ -z "$selected" ]]; then + return + fi + + idx="${selected%%|*}" + + if [[ -z "$key_pressed" ]]; then + # Enter pressed - skip to index + mpv-cmd "set_property" "playlist-pos" "$idx" + return + elif [[ "$key_pressed" == "del" ]]; then + mpv-cmd "playlist-remove" "$idx" + # Refresh loop + continue + else + # q, esc, left + return + fi + done +} + +remote() { + local old_stty + old_stty=$(stty -g) + + trap 'exit 130' INT + trap 'exit 143' TERM + trap cleanup EXIT + + stty -echo -icanon min 1 time 0 + + echo "=== MPV Terminal Remote ===" + echo "[Space] Pause | [h/l] Seek -10s/+10s | [j/k] Vol -/+ | [s] Subtitle" + echo "[o] Open/Queue Video | [e] Edit Playlist | [n] Next Track | [p] Prev Track" + echo "[r] Repeat Current Vid | [R] Repeat Playlist | [c] Controls | [q] Quit" + echo "Listening..." + + while IFS= read -r -n1 key; do + case "$key" in + ' ') + mpv-cmd "cycle" "pause" + is_paused=$(mpv-get "pause") + + if [[ "$is_paused" == "true" ]]; then + echo ">> Video Paused..." + else + echo ">> Video Unpaused..." + fi + ;; + + c) + echo "=== MPV Terminal Remote ===" + echo "[Space] Pause | [h/l] Seek -10s/+10s | [j/k] Vol -/+ | [s] Subtitle" + echo "[o] Open/Queue Video | [e] Edit Playlist | [n] Next Track | [p] Prev Track" + echo "[r] Repeat Current Vid | [R] Repeat Playlist | [c] Controls | [q] Quit" + ;; + h) + mpv-cmd "seek" "-10" + echo ">> Seeked -10s" + ;; + + l) + mpv-cmd "seek" "10" + echo ">> Seeked +10s" + ;; + + k) + mpv-cmd "add" "volume" "5" + vol=$(mpv-get "volume") + echo ">> Volume Level: ${vol%.*}" + ;; + + j) + mpv-cmd "add" "volume" "-5" + vol=$(mpv-get "volume") + echo ">> Volume Level: ${vol%.*}" + ;; + + s) + mpv-cmd "cycle" "sub" + sub_state=$(mpv-get "sub") + + if [[ "$sub_state" == "false" || + "$sub_state" == "no" || + -z "$sub_state" ]]; then + echo ">> Subtitles Disabled..." + else + echo ">> Subtitles Enabled... ($sub_state)" + fi + ;; + + n) + mpv-cmd "playlist-next" + echo ">> Next Track" + ;; + + p) + mpv-cmd "playlist-prev" + echo ">> Prev Track" + ;; + r) + f_loop=$(mpv-get "loop-file") + + if [[ "$f_loop" == "no" || "$f_loop" == "false" || -z "$f_loop" ]]; then + mpv-cmd "set_property" "loop-file" "inf" + echo ">> Loop Current Vid: yes (inf)" + else + mpv-cmd "set_property" "loop-file" "no" + echo ">> Loop Current Vid: no" + fi + ;; + + R) + p_loop=$(mpv-get "loop-playlist") + + if [[ "$p_loop" == "no" || "$p_loop" == "false" || -z "$p_loop" ]]; then + mpv-cmd "set_property" "loop-playlist" "inf" + echo ">> Loop Current Playlist: yes (inf)" + else + mpv-cmd "set_property" "loop-playlist" "no" + echo ">> Loop Current Playlist: no" + fi + ;; + + e) + stty "$old_stty" + edit_playlist + stty -echo -icanon min 1 time 0 + echo "Listening..." + ;; + + o) + stty "$old_stty" + queue_picker + stty -echo -icanon min 1 time 0 + echo "Listening..." + ;; + + q) + echo + echo "Exiting remote." + exit 0 + ;; + esac + done +} + +remote "$@"