tags: mpc mpd music player linux **Note that this is very out of date. I have added much to `mp` since writing this.** 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`. The built-in help is out of date. It is easier to get help by ```bash grep -E "(if|else|elif).*:.*#" $(which mp) ``` Example usages ```bash 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 (`mp start` uses `mpdp` which follows `mp`) ```python #!/usr/bin/python version = "2023-02-03" import os, sys, subprocess, time, re, math from subprocess import run,PIPE try: from colors import color def col(x,c,**kw): if sys.stdout.isatty(): return color(x,fg=c,**kw) else: return x except: def col(x,c): return x mpdhost = os.getenv("MPD_HOST","t420b") try: mpdport = int(os.getenv("MPD_PORT",6600)) except ValueError: print(f"MPD_PORT is not an integer, defaulting to 6600") modport = 6600 mpdbase = 6600 mpdports = [] def mpcmd(xs,**kw): return run(["mpc","-h",mpdhost,"-p",str(mpdport)]+xs,**kw) 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 -- got {p}") raise ValueError() return None p += 6600 return p def set_port(x): global mpdport p = validate_port(x) mpdport = p os.environ["MPD_PORT"] = str(p) def push_port(): mpdports.append(mpdport) def pop_port(): global mpdport p = mpdports.pop() mpdport = p os.environ["MPD_PORT"] = str(p) def getvol(): m = mpdcmdc(["status"],stdout=PIPE,stderr=PIPE) if m.returncode > 0: err = m.stderr.decode() print(f"{mpdhost}:{modport} -- error: {err.rstrip()}") return -1 x = m.stdout.decode().split("volume: ",1)[1].split("%")[0] return int(x) def gettrack(): m = mpcmd(["status"],stdout=PIPE,stderr=PIPE) if m.returncode > 0: err = m.stderr.decode() return col(f"--error: {err.rstrip()}","white",bg="red") x = m.stdout.decode().rstrip().splitlines() if len(x) == 1: return col("--stopped--","red") tr = x[0] st = x[1] if "playing" in st: st = st.replace("[playing]",col("[playing]","green")) if "paused" in st: st = st.replace("[paused]",col("[paused]","yellow")) return f"{tr} -- {st}" def getplaylists(): m = mpcmd(["lsplaylists"],stdout=PIPE) x = m.stdout.decode().rstrip().splitlines() return x def setvol(newvol): newvol = max(min(newvol,100),0) # clamp mpcmd(["mpc","vol",str(newvol)],stdout=PIPE) 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 - Consume mode (no arg means toggle) crossfade [] - 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 - Starts playing the song-number specified (1 if no arg) prev - Starts playing previous song random - random mode (no arg means toggle) repeat - repeat mode (no arg means toggle) single - single mode if state (no arg means toggle) seek [+-][] or <[+-]<0-100>%> - Seeks by h:m:s or by %, +/- means relative, else absolute seekthrough [+-][] - seeks relative, possibly into other tracks stop - Stops playing toggle - Toggles between play and pause. at song number (use play) add - Adds a song from the music database to the queue insert - 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 - Removes a queue number from the queue (0 deletes current song) mv - alias of move move - Moves song at position to the position in the queue searchplay [ ]... - Search the queue for a matching song and play it. shuffle - Shuffles all songs on the queue. load - Loads as queue, playlists stored in ~/.mpd/playlists lsplaylists: - Lists available playlists playlist [] - Lists all songs in (current queue of no arg) rm - Deletes a specific playlist save - Saves playlist as listall [] - Lists from database (no arg means list all) ls [] - Lists all files/folders in directory (no arg means root) search [ ]... - 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 - 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 [ ]... - Same as search, but tag values must match query exactly instead of doing a substring match. findadd [ ]... - Same as find, but add the result to the current queue instead of printing them. list [ ]... [group ]... - 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] [] - 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] [] - Like update, but also rescans unmodified files. albumart - Download album art for the given song and write it to stdout. readpicture - Download a picture embedded in the given song and write it to stdout. Output Commands volume [+-] - Sets the volume to (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 - Send a message to the specified channel. waitmessage - Wait for at least one message on the specified channel. subscribe - 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 : wait for n seconds wait - sleep : 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 exp_ports(x): portsel = set() 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}") return portsel def docmd(cmd,is_last=False): global mpdport, mpdhost, mpdbase if len(cmd) == 0: return c = cmd[0] if c in ["sleep","wait"]: # sleep n -- wait for n seconds (can be float, default is 2) 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): # just a number sets the relative port if len(cmd) == 1: set_port(int(c)) else: push_port() set_port(int(c)) docmd(cmd[1:]) pop_port() elif c in ["port","rp","po"]: # port n, rp n, po n -- set or show relative port if len(cmd) == 1: print(f"port={mpdport}") else: set_port(cmd[1]) elif c in ["base"]: # set port base (you probably don't want to do this, but you can) if len(cmd) > 1: mpdbase = int(cmd[1]) else: mpdbase = int(os.getenv("MPD_PORT",6600)) elif c in ["start"]: # start mpd on host with selected port if len(cmd) > 1: portsel = set() for x in cmd[1:]: portsel.update(exp_ports(x)) for p in sorted(portsel): run(["ssh","-q",mpdhost,"mpdp",str(p)]) else: run(["ssh","-q",mpdhost,"mpdp",str(mpdport-mpdbase)]) elif c in ["help","h"]: # display mpc and mp help help_both() elif c in ["help_mpc","hmpc"]: # display mpc help help_mpc() elif c in ["host"]: # set host if len(cmd) > 1: mpdhost = cmd[1] else: mpdhost = os.getenv("MPD_HOST","localhost") elif c in ["help_mp","hmp"]: # display mp help (though not up to date) help_mp() elif c in ["getvol"]: # get volume print(getvol()) elif c in ["ver"]: # show version print(f"mp version: {version}") mpcmd(["ver"]) elif c in ["vol"]: # get/set volume if len(cmd) == 1: # with no args, shows volume as getvol does return docmd(["getvol"]) if cmd[1].startswith("+"): # +n adds n to volume vol = getvol() b = int(cmd[1][1:]) setvol(vol+b) elif cmd[1].startswith("-"): # -n subtracts n from volume vol = getvol() b = int(cmd[1][1:]) setvol(vol-b) elif cmd[1].startswith("%"): # n% sets volume to n percent of what it was before vol = getvol() b = int(cmd[1][1:])/100.0 setvol(int(b*vol)) else: #subprocess.run(["mpc","vol",cmd[1]]) # anything else is passed through to mpc mpcmd(["vol",cmd[1]]) elif c[0] == "v": # v n -- same as vol n # 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(): # e.g. v5 sets volume to 50%, v9 sets volume to 90% x = int(c[1:]) if x < 0: x = 0 elif x < 10: x *= 10 elif x > 100: x = 100 docmd(["vol",str(x)]) else: # anything else starting with v is passed through to mpc #subprocess.run(["mpc"]+cmd) run(cmd) elif c in ["getp"]: # just get indices of playing mpds if len(cmd) == 1: return docmd(["getp","1-10"]) if len(cmd) > 1: portsel = set() for x in cmd[1:]: portsel.update(exp_ports(x)) push_port() pl = [] for x in sorted(portsel): set_port(x) y = gettrack() if "[playing]" in y: pl.append(x) print(" ".join(map(str,pl))) pop_port() elif c in ["isp"]: # get status of multiple running mpd's: st 3-5 gets status for mpds with rp=3,4,5 if len(cmd) > 1: portsel = set() for x in cmd[1:]: portsel.update(exp_ports(x)) push_port() for x in sorted(portsel): set_port(x) m = subprocess.run(["mpc","status"],stdout=subprocess.PIPE) lines = m.stdout.decode().splitlines() for line in lines: if line[0] == "[": print(f"{mpdhost}:{x} -- {line[1:].split(']')[0]}") break else: print(f"{mpdhost}:{x} -- stopped") pop_port() else: docmd(["status"]) elif c in ["psd","paused"]: if len(cmd) == 1: return docmd(["paused","1-10"]) if len(cmd) > 1: portsel = set() for x in cmd[1:]: portsel.update(exp_ports(x)) push_port() for x in sorted(portsel): set_port(x) y = gettrack() if "[paused]" in y: print(f"{mpdhost}:{x} -- {gettrack()}") pop_port() elif c in ["ply","playing"]: if len(cmd) == 1: return docmd(["playing","1-10"]) if len(cmd) > 1: portsel = set() for x in cmd[1:]: portsel.update(exp_ports(x)) push_port() for x in sorted(portsel): set_port(x) y = gettrack() if "[playing]" in y: print(f"{mpdhost}:{x} -- {gettrack()}") pop_port() elif c.startswith("tr") and c[2:].isnumeric(): x = int(c[2:]) x = min(20,x) x = max(0,x) if x == 0: docmd(["tr","0"]) else: docmd(["tr",f"0-{x}"]) elif c in ["tr","track"]: # show playing track (can use multiple ports and ranges e.g. 1 3-4 if len(cmd) == 1: print(gettrack()) if len(cmd) > 1: portsel = set() for x in cmd[1:]: portsel.update(exp_ports(x)) push_port() for x in sorted(portsel): set_port(x) print(f"{mpdhost}:{x} -- {gettrack()}") pop_port() elif c in ["st"]: # get status of multiple running mpd's: st 3-5 gets status for mpds with rp=3,4,5 if len(cmd) > 1: portsel = set() for x in cmd[1:]: portsel.update(exp_ports(x)) push_port() for x in sorted(portsel): print() print(f"Status for port: {x}") set_port(x) docmd(["status"]) pop_port() else: docmd(["status"]) elif c in ["sus","suspend","sys"]: # put the mpd computer to sleep (it needs 'sus' in its path) print(f"> suspending") subprocess.run(["ssh",mpdhost,"sus"]) elif c in ["ping"]: # ping the mpd computer subprocess.run(["ping",mpdhost]) elif c in ["wsh","wash"]: # wake and ssh into the mpd computer docmd(["wake"]) docmd(["ssh"]) elif c in ["sh","ssh"]: # ssh into the mpd computer subprocess.run(["ssh",mpdhost]) elif c in ["ftp","sftp","ft","f"]: # sftp into the mpd computer, optional arg is dir to cd to if len(cmd) > 1: d = cmd[1] host = f"{mpdhost}:{d}" else: host = mpdhost subprocess.run(["sftp",host]) elif c in ["z"]: # toggle random mode docmd(["random"]+cmd[1:]) elif c in ["y","s"]: # toggle single mode docmd(["single"]+cmd[1:]) elif c in ["c"]: # toggle consume mode docmd(["consume"]+cmd[1:]) elif c in ["listp"]: # list playlists in numbered list 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"]: # with no args is the same as listp, else loads playlist if len(cmd) > 1: pls = cmd[1:] xs = getplaylists() docmd(["clear"]) for x in pls: if x.isnumeric(): # can refer to playlist by its numerical index as given by listp i = int(x)-1 if i < len(xs): docmd(["load",xs[i]]) elif x in xs: # else try to load by name docmd(["load",x]) else: print(f"Playlist {x} does not exist") else: docmd(["listp"]) elif c in ["r"]: # toggle repeat mode docmd(["repeat"],cmd[1:]) elif c in ["psus"]: # pause and sus docmd(["pause"]) docmd(["sus"]) elif c in ["wp"]: # wake and play docmd(["w"],False) docmd(["play"]) elif c in ["wui"]: # wake and load ncmpcpp docmd(["w"]) docmd(["ui"]+cmd[1:]) elif c in ["ui","gui"]: # load ncmpcpp 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"]: # wake music pc using wmus print(f"> waking") subprocess.run(["wmus"]) if not is_last: print(f"> sleeping for 5 seconds") time.sleep(5) elif c == "p": # toggle play/pause if len(cmd) > 1: portsel = set() for x in cmd[1:]: portsel.update(exp_ports(x)) push_port() for p in sorted(portsel): set_port(p) docmd(["p"]) pop_port() else: print(f"> toggle play/pause (rport {mpdport-mpdbase})") 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"]) elif m := re.match(r"(\d+)(\w+)$",c): p,cx = m.groups() push_port() docmd([p]) docmd([cx]+cmd[1:]) pop_port() elif re.match(r"p(\d+)(-(\d+))?$",c): return docmd(["p",c[1:]]+cmd[1:]) else: # all else gets passed straight through to mpc 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) ``` ## mpd starter This is a quick and dirty hack using `sed` to make the necessary modifications to `~/.mpd/mpd.conf`. Basically it changes the port number, assuming that `~/.mpd/mpd.conf` uses `6600` as the port, and changes file paths from e.g. `~/.mpd/mpd.conf` to `~/.mpd/mpd-6601/mpd-6601.conf`. Then everything for the `mpd` on port `6601` goes in the subdirectory `~/.mpd/mpd-6601`. ``` #!/bin/bash p="${1-1}" # restrict port range to 6601..6999 -- 6600 is reservd for the default mpd if (( p < 1 )) || (( p > 399 )); then echo "port $p must in the range 1..399"; exit 1; fi (( p += 6600)) echo "Using port $p" mpd_dir="$HOME/.mpd/mpd-$p" mpd_conf="$mpd_dir/mpd-$p.conf" mkdir -p "$mpd_dir" sed -e "/^port/s/6600/$p/" -e "/^[a-z]*_file/s@~/.mpd@~/.mpd/mpd-$p@" ~/.mpd/mpd.conf >| "$mpd_conf" mpd "$mpd_conf" ```