Skip to content

Instantly share code, notes, and snippets.

@ernstki
Last active March 27, 2025 23:20
Show Gist options
  • Save ernstki/93bc8653556053e51c41cc1ba0c24255 to your computer and use it in GitHub Desktop.
Save ernstki/93bc8653556053e51c41cc1ba0c24255 to your computer and use it in GitHub Desktop.
Enqueue items (like YouTube playlists) with mpv
#!/usr/bin/env bash
#
# Enqueue the named file(s) / URL(s) mpv's JSON IPC; starts an instance of mpv
# if one isn't already found in the process list
#
# Author: Kevin Ernst (ernstki -at- mail.uc.edu)
# Date: 03 July 2019; updated 12 November 2021
# Source: https://gist.github.com/ernstki/93bc8653556053e51c41cc1ba0c24255
#
# Requirements:
# 1. mpv: https://mpv.io/
# - available in MacPorts on macOS; may need to 'pip install youtube-dl'
# 2. jq: https://stedolan.github.io/jq/
# - available in most package managers under that name
# 3. 'xclip' (if on Linux)
# 4. 'socat'
# - also available in MacPorts on macOS
#
(( TRACE )) && set -x
set -u
SOCKET=/tmp/mpvsocket
# wait this long after startup before enqueuing any media
STARTUPDELAY=5
MPVOPTS=(
# (any options unrecognized by this script are also passed through)
--geometry='+5%-10%'
--autofit='25%x25%'
--idle=once
--input-ipc-server=$SOCKET
# 'pip install yt-dlp' and uncomment this if the video stutters a lot
--script-opts=ytdl_hook-ytdl_path=yt-dlp
# ref: https://www.reddit.com/r/mpv/comments/q759xc
# console output from `mpv` usually has useful information when yt-dlp
# fails, so don't uncomment unless you're *sure* yt-dlp is working
#--really-quiet
)
if [[ -t 1 ]]; then
UL=$(tput sgr 0 1)
BOLD=$(tput bold)
RED=$(tput setaf 1)
RESET=$(tput sgr0)
else
UL=; BOLD=; RED=; RESET=
fi
ME=$(basename ${BASH_SOURCE[0]})
USAGE="
$ME - enqueue named file(s) or clipboard contents with mpv
${UL}usage$RESET
$ME [-h|--help] [-s|--show[-playlist|-queue]] [MEDIA [MEDIA...]]
$ME {-c|--command} JSONIPC
${UL}where$RESET
-h, --help shows this help
-s, --show-playlist, shows the current queue/playlist
--show-queue
-p, --pop pop the last entry off the queue
-c, --command CMD send arbitrary JSON IPC command (for testing)
MEDIA URL or filename(s) to enqueue with mpv
(default: read from stdin, then clipboard)
${UL}default mpv options$RESET
$(echo ${MPVOPTS[*]} | fold -s -w 78 | sed 's/^/ &/')
(options unrecognized by this script are also passed through)
"
if [[ $(uname -s) != Darwin ]]; then
# requires 'xclip' to be installed
pbpaste() { xclip -o -sel clipboard; }
fi
_ipc() {
local output quiet= data= args=()
while (( $# )); do
case $1 in
-q|--quiet) quiet=1;;
-d|--data) data=1;;
*) args+=("\"$1\"");;
esac
shift
done
# echo "{\"command\": [ $(IFS=,; echo "${args[*]}") ]}"
output=$(
socat - $SOCKET <<<"{\"command\": [ $(IFS=,; echo "${args[*]}") ]}"
)
(( $? )) &&
bail "Error sending IPC command '${args[0]}'"
error=$( jq -r .error <<<"$output" )
[[ $error == success ]] ||
bail "Error '$error' sending IPC command '${args[0]}'"
if (( !quiet )); then
if (( data )); then
echo "$output" | jq -r .data
else
echo "$output"
fi
fi
}
show_playlist() {
echo
echo " Contents of playlist"
echo " --------------------"
_ipc --data get_property playlist | jq -r '" - " + .[].filename'
echo
echo " Now playing"
echo " -----------"
_ipc --data get_property media-title \
| fold -s -w 78 \
| sed 's/^/ &/'
echo
}
send_command() {
local pid=$(pgrep mpv)
_ipc --data "$@" ||
bail "Error sending IPC command '$*' (mpv pid: $pid)"
}
enqueue_item() {
local pid=$(pgrep mpv)
local item=$1
if _ipc -q loadfile "$item" append-play; then
echo "Enqueued '$item' (mpv pid: $pid)" >&2
else
bail "Error enqueueing '$item' (mpv pid: $pid)"
fi
}
pop_item() {
local pid=$(pgrep mpv)
local count=$(_ipc --data get_property playlist-count)
if _ipc --quiet playlist-remove $(( count - 1 )); then
echo "Popped last entry from playlist (mpv pid: $pid)" >&2
else
bail "Error popping from playlist (mpv pid: $pid)"
fi
}
bail() {
echo >&2
echo " $RED${BOLD}OH NOES!${RESET} $*" >&2
echo >&2
exit 1
}
queue=()
mpvopts=("${MPVOPTS[@]}")
output=
pid=
ret=
showplaylist=
# parse input arguments
while (( $# )); do
case $1 in
-h|--h*)
echo "$USAGE"
exit
;;
-s|--show*)
show_playlist
exit
;;
-p|--pop)
pop_item
exit
;;
-c|--command)
shift
send_command "$@"
exit
;;
-*)
mpvopts+=("$1")
;;
*)
queue+=("$1")
esac
shift
done
# if no queue arguments given on command line:
# - if reading from a pipe/redirection, read that in
# - else try the clipboard
if [[ ${#queue[@]} -eq 0 ]]; then
if [[ ! -t 0 ]]; then
echo "Reading from stdin..." >&2
readarray -t stdin
queue+=("${stdin[@]}")
else
echo "Checking clipboard..." >&2
readarray -t clipboard < <(pbpaste)
queue+=("${clipboard[@]}")
fi
fi
if [[ ${#queue[@]} -eq 0 ]]; then
echo >&2
echo "$USAGE" >&2
bail "Queue cannot be empty; try passing filenames or URLs"
fi
# start mpv with IPC service enabled, if not running
pid=$(pgrep mpv)
ret=$?
if (( $ret != 0 )); then
nohup mpv "${mpvopts[@]}" 2>&1 &
pid=$!
echo "Started mpv (pid: $pid)" >&2
sleep $STARTUPDELAY
fi
# spool up the rest by sending messages to the socket
for item in "${queue[@]}"; do
enqueue_item "$item"
done
@ernstki
Copy link
Author

ernstki commented Sep 11, 2019

Needs to check for presence of socat and bail if it's not found.

@ernstki
Copy link
Author

ernstki commented Nov 22, 2023

FIXME

The script-opts=ytdl_hook-ytdl_path=yt-dlp key isn't supported until v0.33.0 (I think), and Ubuntu 20.04 only has 0.32.0. See this Reddit thread and mpv-player/mpv@93f84b5.

With 0.32.0, you just get the error message

[ytdl_hook] script-opts: unknown key ytdl_path, ignoring

which is silent, and so mpv just quits, no indication of what went wrong in nohup.out, I think because of the --no-terminal option (which I might want to reconsider).

Workaround

pipx install yt-dlp
cd ~/.local/bin
ln -s yt-dlp youtube-dl

And then comment out the line in the script that says

--script-opts=ytdl_hook-ytdl_path=yt-dlp

As a general practice, you're always better off installing yt-dlp with pip or pipx. LTS distros' repositories may include a yt-dlp package, but it's almost guaranteed to be too old to actually work with YouTube. It's a constant arms race.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment