Dup Ver Goto 📝

Mpd

PT2/media/player does not exist
To
859 lines, 3703 words, 28286 chars Page 'Mpd' does not exist.

I use the MPD Music Player Daemon as my main music player at home. I have a Lenovo T420 in the corner (with broken screen) that acts as a general server. It has a Presonus audio interface attached, feeding into a mixer that then mixes sound with my TV and sends the result to a hifi amplifier.

I run 16 user-mode instances of mpd so that I can have 16 simultaneous playlists and play states. These are on ports 6600..6615. I then have a helper script which generally defers to mpc (though also can wakeonlan and suspend the T420). This permits many shorthands for common tasks, such as

mp %pause    # pause any playing instances
mp 3,p       # toggle play/pause on mpd instance 3
mp 4v7       # set volume to 70% on mpd instance 4

In general, args are delimited by commas at the top level, so mp concatenates all arguments using spaces as a delimiter, and then splits by commas. A number on its own selects a particular mpd instance. If combined (as in 4v7), then the v7 command (which expands to mpc volume 70) is sent to instance 4, but the selected instance isn't changed. Thus

mp 3,4v7,v5,play

will expand to

mpc -h <host> -p 6604 volume 70
mpc -h <host> -p 6603 volume 50
mpc -h <host> -p 6603 play

(You can see that it saves a lot of typing.)

The scripts are below.

Source

mp

The help is out of date. A massive if/else construct is used since match didn't exist when I started writing this, and sometimes I want to do things like if cmd[0] == "v" and other logic. And I can't be bothered to rewrite it.

#!/usr/bin/python
version = "2023-06-19"
import os, sys, subprocess, time, re, math
import platform
from subprocess import run,PIPE
import json
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:
  # if mpdhpst is localhost, use 127.0.0.1
  with open("/etc/hostname") as f:
    etchostname = f.read().strip()
    if etchostname.lower() == mpdhost.lower():
      mpdhost = "127.0.0.1"
except Exception:
  pass
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):
  cmd = ["mpc","-h",mpdhost,"-p",str(mpdport)]+xs
  #print(f"{cmd=}")
  return run(cmd,**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():
  global last_play_state
  m = mpcmd(["status"],stdout=PIPE,stderr=PIPE)
  if m.returncode > 0:
     err = m.stderr.decode()
     print(f"{mpdhost}:{modport} -- error: {err.rstrip()}")
     return -1
  o = m.stdout.decode()
  if "\n[playing]" in o:
    last_play_state = "playing"
  elif "\n[paused]" in o:
    last_play_state = "paused"
  else:
    last_play_state = "stopped"
  m = re.search(r"volume:\s*(\w+)",o)
  try:
    return int(m.group(1))
  except ValueError:
    return -1
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")
  if len(x) == 2:
    if "Updating" in x[0]:
      return col("--stopped, updating db--","red")
  try:
    tr = x[0]
    st = x[1]
    for line in x:
      m = re.match(r"volume:\s*(\d+%|n/a)",line)
      if m:
        break
    else:
      return col("--failed to find volume--","red")
  except IndexError:
    print(f"# fail {len(x)} : {x=}")
    return col("--fail--","red")
  g = m.group(1)
  if "playing" in st:
    cx = "green"
    st = st.replace("[playing]",col("[playing]",cx))
    st += " " + col("vol: "+g,cx)
  if "paused" in st:
    cx = "yellow"
    st = st.replace("[paused]",col("[paused]",cx))
    st += " " + col("vol: "+g,cx)
  return f"{tr} -- {st}"
def make_track_data(
    playstate="undefined",
    volume=0,
    repeat=False,
    random=False,
    single=False,
    consume=False,
    track_name="",
    pl_pos=0,
    pl_len=0,
    t_cur="",
    t_total="",
    t_pc=0):
    return { "playstate": playstate,
    "volume": volume,
    "repeat": repeat,
    "random": random,
    "single": single,
    "consume": consume,
    "track_name": track_name,
    "pl_pos": pl_pos,
    "pl_len": pl_len,
    "t_cur": t_cur,
    "t_total": t_total,
    "t_pc": t_pc }
def parse_volume_line(x):
  m = re.match(r"volume: (\S+)\s+repeat: (\S+)\s+random: (\S+)\s+single: (\S+)\s+consume: (\S+)",x)
  if not m:
    return -1,False,False,False,False
  vol,rep,ran,sin,cons = m.groups()
  if vol == "n/a":
    vol = -1
  else:
    vol = int(vol.rstrip("%"))
  rep = True if rep == "on" else False
  ran = True if ran == "on" else False
  sin = True if sin == "on" else False
  cons = True if cons == "on" else False
  return vol,rep,ran,sin,cons
def parse_status_line(x):
  m = re.match(r"\[(\w+)\]\s+#(\d+)/(\d+)\s+([^/]+)/(\S+)\s+\((\d+)%\)",x)
  if not m:
    return -1,0,0,"#error4","#error4",0
  else:
    state,pl_num,pl_len,t_cur,t_total,t_pc = m.groups()
  try:
    pl_num = int(pl_num)
    pl_len = int(pl_len)
    t_pc = float(t_pc)
  except ValueError:
    state = "error"
  return state,pl_num,pl_len,t_cur,t_total,t_pc
def gettrack_data():
  m = mpcmd(["status"],stdout=PIPE,stderr=PIPE)
  if m.returncode > 0:
    return make_track_data(playstate="error",track_name="#error1")
  x = m.stdout.decode().rstrip().splitlines()
  if len(x) == 1:
    vol,rep,ran,sin,cons = parse_volume_line(x[0])
    track_name= "#stopped"
    return make_track_data(playstate="stopped",volume=vol,repeat=rep,random=ran,single=sin,consume=cons,track_name=track_name)
  if len(x) == 2:
    if "Updating" in x[0]:
      vol,rep,ran,sin,cons = parse_volume_line(x[1])
      track_name= "#stopped,updating"
    return make_track_data(playstate="stopped",volume=vol,repeat=rep,random=ran,single=sin,consume=cons,track_name=track_name)
  x = [ y for y in x if not "Updating DB (" in y ]
  vol,rep,ran,sin,cons = parse_volume_line(x[2])
  if vol < 0:
    track_name = "#error3"
  track_name = x[0]
  state,pl_pos,pl_len,t_cur,t_total,t_pc = parse_status_line(x[1])
  return make_track_data(playstate=state,volume=vol,repeat=rep,random=ran,single=sin,consume=cons,track_name=track_name,pl_pos=pl_pos,pl_len=pl_len,t_cur=t_cur,t_total=t_total,t_pc=t_pc)
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(["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 <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 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 m := re.match(r"([\.\d]+)sl$",c):
    return docmd([f"sl{m.group(1)}"],is_last=is_last)
  elif m := re.match(r"sl([\.\d]+)$",c):
    a = m.group(1)
    try:
      t = float(a)
    except ValueError:
      print(f"Sleep time {a} not valid, must be a float or int")
      return
    return docmd(["sleep",t],is_last=is_last)
  elif 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[0] == "%":
    cmd=c[1:]
    if len(cmd) == 0:
      return
    if cmd[0] == "p":
      cmd = "pause"
    elif cmd[0] == "s":
      cmd = "stop"
    else:
      print(f"Not doing {cmd} en masse")
      return
    print(f"Doing {cmd} all")
    tr_range = os.getenv("TR_RANGE","0-15")
    portsel = set()
    try:
      portsel.update(exp_ports(tr_range))
    except Exception:
      print(f"Invalid TR_RANGE: {tr_range}")
    push_port()
    for x in sorted(portsel):
      set_port(x)
      docmd([cmd])
    pop_port()
  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 in ["vols"]:
    if len(cmd) == 1:
      docmd(["vols",os.getenv("TR_RANGE","0-15")])
    else:
      portsel = set()
      for x in cmd[1:]:
        portsel.update(exp_ports(x))
      push_port()
      for x in sorted(portsel):
        set_port(x)
        v = getvol()
        if v == -1:
          t = col("--stopped--","red")
        else:
          if last_play_state == "playing":
            t = col(str(v)+"%","green")
          else:
            t = col(str(v)+"%","yellow")
        print(f"{mpdhost}:{x: >3} -- {t}")
      pop_port()
  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:
      mpcmd(cmd)
  elif c in ["getp"]: # just get indices of playing mpds
    if len(cmd) == 1:
      return docmd(["getp","0-15"])
    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","0-15"])
    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","0-15"])
    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 ["json"]:
    # json for mpd managers
    if len(cmd) == 1:
      # print(gettrack())
      tr_range = os.getenv("TR_RANGE","0-15")
      docmd(["json",tr_range])
    if len(cmd) > 1:
      portsel = set()
      for x in cmd[1:]:
        portsel.update(exp_ports(x))
      push_port()
      track_data = []
      for x in sorted(portsel):
        set_port(x)
        track_data.append(gettrack_data())
      pop_port()
      json.dump(track_data,sys.stdout)
      exit()
  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())
      tr_range = os.getenv("TR_RANGE","0-15")
      docmd(["tr",tr_range])
    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: >3} -- {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,"susp"])
  elif c in ["ping"]: # ping the mpd computer
    p = platform.platform() 
    npings = 16
    if len(cmd) > 1:
      try:
        npings = int(cmd[1])
        npings = min(32,max(0,npings))
      except Exception:
        pass
    args = []
    if "Linux" in p:
      pingargs = ["-c","1","-w","1"]
      for n in range(npings):
        m = subprocess.run(["ping"]+pingargs+[mpdhost])
        if m.returncode == 0:
          break
    elif "macOS" in p:
      pingargs = ["-c","1","-W","1"]
      for n in range(npings):
        m = subprocess.run(["ping"]+pingargs+[mpdhost])
        if m.returncode == 0:
          break
    else:
      # windows
      pingargs = ["-n","1","-w","1"]
      for n in range(npings):
        m = subprocess.run(["ping"]+pingargs+[mpdhost])
        if m.returncode == 0:
          break
  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"])
    docmd(["ping"])
    docmd(["play"])
  elif c in ["wui"]: # wake and load ncmpcpp
    docmd(["w"])
    docmd(["ping"])
    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(["wakey",mpdhost])
  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})")
      docmd(["toggle"])
  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)}")
    mpcmd(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)

make_new_mpd_conf

This creates a subdirectory of ~/.mpd and adjusts the port number and conf directory.

#!/usr/bin/env python
import os
import sys
from subprocess import run
from setproctitle import setproctitle
args = sys.argv[1:]
if len(args) == 0:
  args = ["1"]
rp = int(args[0])
if rp < 0 or rp > 99:
  print(f"Invalid rp: {p}")
  exit(1)
base_port = 6600
port = rp + base_port
print(f"Using port {port}")
home = os.getenv("HOME")
mpd_dir = f"{home}/.mpd/mpd-{port}" 
mpd_conf=f"{mpd_dir}/mpd-{port}.conf"
os.makedirs(mpd_dir,exist_ok=True)
with open(f"{home}/.mpd/mpd.conf") as f:
  conf0 = f.read().rstrip().splitlines()
  conf1 = [line.replace("6600",str(port)) for line in conf0]
  conf1 = [line.replace('"~/.mpd',f'"{mpd_dir}') if not "playlists" in line else line for line in conf1]
  with open(mpd_conf,"wt") as g:
    print(f"writing {mpd_conf}")
    g.write("\n".join(conf1))
    g.flush()
setproctitle(f"Mpd_{rp}")
run(["mpd",mpd_conf])