#!/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 "$@"