media/mpd/ MpcHelperScript
I have the following (gradually growing) python script, called mp
, with shorthands for many common use cases. In addition,
any unrecognised input gets passed through to mpc
. Commands are separated by commas (and commas needn't
be surrounded by whitespace: the script joins all args and splits by commas, then splits individual
commands by whitespace. Note that mpc commands that need quoting are incompatible with this script, so
then you'll need to actually run mpc
rather than mp
.
Example usages
mp p # play/pause
mp ui # run ncmpcpp
mp port n # set MPD_PORT to 6600+n
mp v6 # set volume to 60%
mp vol +5 # increase volume by 5
mp vol n% # set vol to n% of current value
mp port 5, ui # load ncmcpp for mpd on port 6605
the code
#!/usr/bin/python
version = "2023-01-26"
import os, sys, subprocess, time, re, math
mpdhost = os.getenv("MPD_HOST","turnip")
mpdport = os.getenv("MPD_PORT",6600)
mpdbase = 6600
def validate_port(p):
p = int(p)
if p < 0 or p > 399:
print(f"UI port offset must be in the range 1..399")
return None
p += 6600
return p
def set_port(x):
p = validate_port(x)
mpdport = p
os.environ["MPD_PORT"] = str(p)
def getvol():
m = subprocess.run(["mpc","status"],stdout=subprocess.PIPE)
x = m.stdout.decode().split("volume: ",1)[1].split("%")[0]
return int(x)
def getplaylists():
m = subprocess.run(["mpc","lsplaylists"],stdout=subprocess.PIPE)
x = m.stdout.decode().rstrip().splitlines()
return x
def setvol(newvol):
newvol = max(min(newvol,100),0) # clamp
subprocess.run(["mpc","vol",str(newvol)])
def newline():
print()
def get_width():
return os.get_terminal_size().columns
def showline(x):
width = get_width()
padding = width - len(x)
if padding < 2:
print(f"\r{x}",end="")
return
padding -= 2
padding_r = padding // 2
padding_l = padding - padding_r
pad_l = "="*padding_l
pad_r = "="*padding_r
print(f"\r{pad_l}[{x}]{pad_r}",end="")
def help_both():
help_mp()
print()
help_mpc()
def help_mpc():
print("""mpc commands (non mp commands passed through to mpc)
consume <on|off> - Consume mode (no arg means toggle)
crossfade [<seconds>] - Gets/set amount of crossfading between songs (0 disables)
current [--wait] - Show the currently playing song (--wait waits until the song/state changes)
queued - Show the currently queued (next) song
next - Starts playing next song on queue.
pause - Pauses playing.
play <position> - Starts playing the song-number specified (1 if no arg)
prev - Starts playing previous song
random <on|off> - random mode (no arg means toggle)
repeat <on|off> - repeat mode (no arg means toggle)
single <on|once|off> - single mode if state (no arg means toggle)
seek [+-][<HH:MM:SS>] or <[+-]<0-100>%> - Seeks by h:m:s or by %, +/- means relative, else absolute
seekthrough [+-][<HH:MM:SS>] - seeks relative, possibly into other tracks
stop - Stops playing
toggle - Toggles between play and pause. at song number (use play)
add <file> - Adds a song from the music database to the queue
insert <file> - Like add except it adds song(s) after the currently playing one, rather than at the end
clear - Empties the queue.
crop - Remove all songs except for the currently playing song
del <songpos> - Removes a queue number from the queue (0 deletes current song)
mv - alias of move
move <from> <to> - Moves song at position <from> to the position <to> in the queue
searchplay <type> <query> [<type> <query>]... - Search the queue for a matching song and play it.
shuffle - Shuffles all songs on the queue.
load <file> - Loads <file> as queue, playlists stored in ~/.mpd/playlists
lsplaylists: - Lists available playlists
playlist [<playlist>] - Lists all songs in <playlist> (current queue of no arg)
rm <file> - Deletes a specific playlist
save <file> - Saves playlist as <file>
listall [<file>] - Lists <song file> from database (no arg means list all)
ls [<directory>] - Lists all files/folders in directory (no arg means root)
search <type> <query> [<type> <query>]... - Searches for substrings in song tags. Any number of tag type and query combinations can be specified. Possible tag types are: artist, album, title, track, name, genre, date, composer, performer, comment, disc, filename, or any (to match any tag).
search <expression> - Searches with a filter expression,
e.g. mpc search '((artist == "Kraftwerk") AND (title == "Metall auf
Metall"))'
Check the MPD protocol documentation for details. This syntax can be used with find and findadd as well. (Requires libmpdclient 2.16 and MPD 0.21)
find <type> <query> [<type> <query>]... - Same as search, but tag values must match query exactly instead of doing a substring match.
findadd <type> <query> [<type> <query>]... - Same as find, but add the result to the current queue instead of printing them.
list <type> [<type> <query>]... [group <type>]... - Return a list of all tags of given tag type. Optional search type/query limit results in a way similar to search. Results can be grouped by one or more tags. Example:
e.g. mpc list album
stats - Displays statistics about MPD.
update [--wait] [<path>] - Scans for updated files in the music directory. The optional parameter path (relative to the music directory) may limit the scope of the update. With --wait, mpc waits until MPD has finished the update.
rescan [--wait] [<path>] - Like update, but also rescans unmodified files.
albumart <file> - Download album art for the given song and write it to stdout.
readpicture <file> - Download a picture embedded in the given song and write it to stdout.
Output Commands
volume [+-]<num> - Sets the volume to <num> (0-100). If + or - is used, then it adjusts the volume relative to the current volume.
outputs - Lists all available outputs
Client-to-client Commands
channels - List the channels that other clients have
subscribed to.
sendmessage <channel> <message> - Send a message to the
specified channel.
waitmessage <channel> - Wait for at least one message on
the specified channel.
subscribe <channel> - Subscribe to the specified channel
and continuously receive messages.
idle [events] - Waits until an event occurs. Prints a list of event names, one per line. If you specify a list of events, only these events are considered.
idleloop [events] - Similar to idle, but re-enters "idle" state after events have been printed. If you specify a list of events, only these events are considered.
status [format] - Without an argument print a three line status
output equivalent to "mpc" with no arguments. If a format string is given then the delimiters are processed exactly as how they are for metadata. See the '-f' option in Options
Name Description
%totaltime% The total duration of the song.
%currenttime% The time that the client is currently at.
%percenttime% The percentage of time elapsed for the current song.
%songpos% The position of the current song within the playlist.
%length% The number of songs within the playlist
%state% Either 'playing' or 'paused'
%volume% The current volume spaced out to 4 characters including a percent sign
%random% Current status of random mode. 'on' or 'off'
%repeat% Current status of repeat mode. 'on' or 'off'
%single% Current status of single mode. 'on', 'once', or 'off'
%consume% Current status of consume mode. 'on' or 'off'
version - Reports the version of the protocol spoken, not the real
version of the daemon.
""")
def help_mp():
print("""mp commands (other commands passed through to mpc)
sleep - sleep <n>: wait for n seconds
wait - sleep <n>: wait for n seconds (alias of sleep)
getvol - get volume
vol - set volume 43 to set to 43, +7 to increase by 7, -5 to decrease by 5, %43.5 to scale by 43.5%
v - same as vol
vXX - where XX is numeric, same as vol XX, but if X is in the range 1-9 it is multiplied by 10.
sus - suspend (i.e. put machine to sleep)
suspend - suspend (alias of sus)
sys - suspend (alias of sus, common typo of sus)
ping - ping music player
wsh - wake and ssh to music player
wash - wake and ssh (alias of wsh)
sh - ssh to music player
ssh - ssh to music player
ftp - sftp to music player
sftp - sftp to music player
ft - sftp to music player
f - sftp to music player
z - random on/off (no arg means toggle)
y - single on/off (no arg means toggle)
s - single on/off (no arg means toggle)
c - consume on/off (no arg means toggle)
listp - list playlists
lp - load playlist (by name or number, no arg means list playlists)
pl - load playlist (alias of lp)
r - repeat on/off (no arg means toggle)
psus - pause and suspend
wp - wake and play
wui - wake and launch ui (ncmpcpp)
ui - launch ui (ncmpcpp)
gui - launch ui (alias of ui)
wake - wake
w - wake
h - help on mp and mpc commands
hmp - help on mp commands
hmpc - help on mpc commands
help - help on mp and mpc commands
help_mp - help on mp commands
help_mpc - help on mpc commands
""")
def docmd(cmd,is_last=False):
global mpdport
if len(cmd) == 0:
return
c = cmd[0]
if c in ["sleep","wait"]:
if len(cmd) > 1:
t = float(cmd[1])
else:
t = 2.0
t0 = int(math.floor(t))
t1 = t - t0
if t1 < 0.01:
t1 = 0
print()
showline(f"sleeping for {t} seconds")
if t1 > 0:
time.sleep(t1)
while t0 > 0:
showline(f"sleeping for {t0} seconds")
t0 -= 1
time.sleep(1)
showline(f"finished sleeping")
newline()
elif (c.isnumeric() and int(c) >= 0 and int(c) <= 399):
set_port(c)
elif c in ["port","rp","po"]:
if len(cmd) == 1:
print(f"port={mpdport}")
else:
set_port(cmd[1])
elif c in ["help","h"]:
help_both()
elif c in ["help_mpc","hmpc"]:
help_mpc()
elif c in ["help_mp","hmp"]:
help_mp()
elif c in ["getvol"]:
print(getvol())
elif c in ["ver"]:
print(f"mp version: {version}")
subprocess.run(["mpc","ver"])
elif c in ["vol"]:
if cmd[1].startswith("+"):
vol = getvol()
b = int(cmd[1][1:])
setvol(vol+b)
elif cmd[1].startswith("-"):
vol = getvol()
b = int(cmd[1][1:])
setvol(vol-b)
elif cmd[1].startswith("%"):
vol = getvol()
b = int(cmd[1][1:])/100.0
setvol(int(b*vol))
else:
subprocess.run(["mpc","vol",cmd[1]])
elif c[0] == "v":
# this "v" command case must be the last of all commands beginning with v
if c == "v":
cmd[0] = "vol"
docmd(cmd)
elif c[1:].isnumeric():
x = int(c[1:])
if x < 0:
x = 0
elif x < 10:
x *= 10
elif x > 100:
x = 100
docmd(["vol",str(x)])
else:
subprocess.run(["mpc"]+cmd)
elif c in ["st"]:
if len(cmd) > 1:
portsel = set()
for x in cmd[1:]:
if "-" in x:
fr,to = x.split("-",1)
try:
fr = int(fr)
to = int(to)
if to < fr:
fr,to = to,fr
if fr < 0 or to < 0 or fr > 399 or to > 399:
raise ValueError()
except ValueError:
print(f"Invalid port range: {x}")
for y in range(fr,to+1):
portsel.add(y)
else:
try:
y = int(x)
if y < 0 or y > 399:
raise ValueError()
portsel.add(int(x))
except ValueError:
print(f"Invalid port {x}")
orig_port = mpdport
for x in sorted(portsel):
print()
print(f"Status for port: {x}")
set_port(x)
docmd(["status"])
set_port(orig_port)
docmd(["status"])
elif c in ["sus","suspend","sys"]:
print(f"> suspending")
subprocess.run(["ssh",mpdhost,"sus"])
elif c in ["ping"]:
subprocess.run(["ping",mpdhost])
elif c in ["wsh","wash"]:
docmd(["wake"])
docmd(["ssh"])
elif c in ["sh","ssh"]:
subprocess.run(["ssh",mpdhost])
elif c in ["ftp","sftp","ft","f"]:
if len(cmd) > 1:
d = cmd[1]
host = f"{mpdhost}:{d}"
else:
host = mpdhost
subprocess.run(["sftp",host])
elif c in ["z"]:
docmd(["random"]+cmd[1:])
elif c in ["y","s"]:
docmd(["single"]+cmd[1:])
elif c in ["c"]:
docmd(["consume"]+cmd[1:])
elif c in ["listp"]:
xs = getplaylists()
nl = len(str(len(xs)+1))
for i,x in enumerate(xs):
n = ((" "*nl)+str(i+1))[-nl:]
print(f"{n}: {x}")
elif c in ["lp","pl"]:
if len(cmd) > 1:
pls = cmd[1:]
xs = getplaylists()
docmd(["clear"])
for x in pls:
if x.isnumeric():
i = int(x)-1
if i < len(xs):
docmd(["load",xs[i]])
elif x in xs:
docmd(["load",x])
else:
print(f"Playlist {x} does not exist")
else:
docmd(["listp"])
elif c in ["r"]:
docmd(["repeat"],cmd[1:])
elif c in ["psus"]:
docmd(["pause"])
docmd(["sus"])
elif c in ["wp"]:
docmd(["w"],False)
docmd(["play"])
elif c in ["wui"]:
docmd(["w"])
docmd(["ui"]+cmd[1:])
elif c in ["ui","gui"]:
if len(cmd) > 1:
p = validate_port(cmd[1])
if p is not None:
subprocess.run(['ncmpcpp','-p',str(p)])
else:
subprocess.run(['ncmpcpp'])
elif c in ["wake","w"]:
print(f"> waking")
subprocess.run(["wmus"])
if not is_last:
print(f"> sleeping for 5 seconds")
time.sleep(5)
elif c == "p":
print(f"> toggle play/pause")
a = subprocess.run(["mpc"],stdout=subprocess.PIPE)
b = a.stdout.decode("utf8").split("\n")[1].split(" ")[0]
if b == "[playing]":
print(f">> pausing")
subprocess.run(["mpc","pause"])
else:
print(f">> playing")
subprocess.run(["mpc","play"])
else:
print(f"> mpc {' '.join(cmd)}")
subprocess.run(["mpc"]+cmd)
def main():
cmds = [[]]
args = sys.argv[1:]
if len(args) == 0:
args = ["status"]
argstr = " ".join(args)
argsp = argstr.split(",")
cmds = []
for arg in argsp:
arg = arg.strip()
xs = list(map(lambda t: t.strip(), re.split(r"\s+",arg)))
cmds.append(xs)
for arg in args:
if ',' in arg:
xs = arg.split(",")
xs = list(map(lambda t: t.strip(),xs))
last_cmd_idx = len(cmds) - 1
for i,cmd in enumerate(cmds):
docmd(cmd,i+1==last_cmd_idx)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print(f"Exiting due to keyboard interrupt.")
exit(1)