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.

The built-in help is out of date. It is easier to get help by

grep -E "(if|else|elif).*:.*#" $(which 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 (mp start uses mpdp which follows mp)

#!/usr/bin/env python
version = "2022-08-29"
import os, sys, subprocess, time, re, math
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
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):
  global mpdport
  p = validate_port(x)
  print(f"set_port {x=} {p=}")
  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"]: # 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
    set_port(int(c))
  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 ["start"]:
    subprocess.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 ["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}")
    subprocess.run(["mpc","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
  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)
  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:]:
        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)
    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
    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: # 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"