Dup Ver Goto 📝

Toy Mpd Client 1

To
429 lines, 1182 words, 9995 chars Page 'ToyMpdClient1' does not exist.

My aim eventually is to write a simple web mpd client, and also rewrite a growing Python script that often runs mpc as a subprocess and then parses its output (a version of it is here — health warning: it is a mess).

At first I'm happy to create a new connection every time. Perhaps later look into non-blocking I/O so that we can create a connection at the start and keep it open.

Get Playlist

So the first exercise is to get the playlist. This is a simple case of creating a TCP socket, connecting, reading once to get the initial OK, then send the one-line command playlist, then reading until the response contains OK on its own line (regex ^OK$ with options re.M).

#!/usr/bin/env python3
from icecream import ic; ic.configureOutput(includeContext=True) # pip install icecream

host = "localhost"
port = 6600

import socket
from time import sleep
import re
import sys
args = sys.argv[1:]
try:
  port = int(args[1])
  host = args[0]
except Exception as e:
  ic(e)
  pass 

def get_playlist(host,port): # throws socket.TimeoutError
  with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.settimeout(1)
    s.connect((host,port))
    data = s.recv(4096)

    # get playlist
    s.sendall("playlist\n".encode())
    data = ""
    while True:
      d = s.recv(4096).decode()
      if re.search(r"^OK\n",d,re.M):
        d = re.split(r"^OK\n",d,re.M)[0]
        data += d
        return data
      data += d 

try:
  print(get_playlist(host,port))
except (TimeoutError,ConnectionRefusedError) as e:
  ic(e)
try:
  print(get_playlist(host,5001))
except (TimeoutError,ConnectionRefusedError) as e:
  ic(e)

Get Status

#!/usr/bin/env python3
from icecream import ic; ic.configureOutput(includeContext=True)

host = "localhost"
port = 6600

import socket
from time import sleep
import re
import sys
args = sys.argv[1:]
try:
  port = int(args[1])
  host = args[0]
except Exception as e:
  ic(e)
  pass 

def get_response(host,port,command): # throws socket.TimeoutError
  with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.settimeout(1)
    s.connect((host,port))
    data = s.recv(4096)

    # get playlist
    s.sendall((command+"\n").encode())
    data = ""
    while True:
      d = s.recv(4096).decode()
      if re.search(r"^OK\n",d,re.M):
        d = re.split(r"^OK\n",d,re.M)[0]
        data += d
        return data
      data += d 

def get_playlist(host,port):
  data = get_response(host,port,"playlist")
  lines = [ line for line in data.splitlines() if ":file:" in line ]
  tracks = [ (int(x[0]),x[2].lstrip(" ")) for x in ( line.split(":") for line in lines) ]
  return tracks

def get_status(host,port):
  data = get_response(host,port,"status")
  status = { k:v for k,v in ( line.split(": ",1) for line in data.splitlines() if ": " in line ) } 
  return status

try:
  ic(get_playlist(host,port))
  ic(get_status(host,port))
except (TimeoutError,ConnectionRefusedError) as e:
  ic(e)

Get Playlist and Status

#!/usr/bin/env python3
from icecream import ic; ic.configureOutput(includeContext=True)

host = "localhost"
port = 6600

import socket
from time import sleep
import re
import sys
args = sys.argv[1:]
try:
  port = int(args[1])
  host = args[0]
except Exception as e:
  ic(e)
  pass 

def parse_playlist(data):
  lines = [ line for line in data.splitlines() if ":file:" in line ]
  tracks = [ (int(x[0]),x[2].lstrip(" ")) for x in ( line.split(":") for line in lines) ]
  return tracks

def parse_status(data):
  status = { k:v for k,v in ( line.split(": ",1) for line in data.splitlines() if ": " in line ) } 
  return status

def get_stuff(host,port):
  with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.settimeout(1)
    s.connect((host,port))
    data = s.recv(4096)

    # get playlist
    s.sendall("playlist\n".encode())
    data = ""
    while True:
      d = s.recv(4096).decode()
      if re.search(r"^OK\n",d,re.M):
        d = re.split(r"^OK\n",d,re.M)[0]
        data += d
        break
      data += d 
    playlist = parse_playlist(data)
    s.sendall("status\n".encode())
    data = ""
    while True:
      d = s.recv(4096).decode()
      if re.search(r"^OK\n",d,re.M):
        d = re.split(r"^OK\n",d,re.M)[0]
        data += d
        break
      data += d 
    status = parse_status(data)
    if "song" in status:
      song = int(status["song"])
      try:
        songname = playlist[song][1]
      except IndexError:
        songname = "<index error>"
    else:
      songname = None

    return ( status, playlist, songname )

try:
  ic(get_stuff(host,port))
except (TimeoutError,ConnectionRefusedError) as e:
  ic(e)

Get metadata for songs in playlist

Now we reuse the same socket rather than making new ones all the time.

#!/usr/bin/env python3
from icecream import ic; ic.configureOutput(includeContext=True)

host = "localhost"
port = 6600

import socket
from time import sleep
import re
import sys
args = sys.argv[1:]
try:
  port = int(args[1])
  host = args[0]
except Exception as e:
  ic(e)
  pass 

def parse_playlist(data):
  lines = [ line for line in data.splitlines() if ":file:" in line ]
  tracks = [ (int(x[0]),x[2].lstrip(" ")) for x in ( line.split(":") for line in lines) ]
  return tracks

def parse_status(data):
  status = { k:v for k,v in ( line.split(": ",1) for line in data.splitlines() if ": " in line ) } 
  return status

def get_response(s,command):
  s.sendall((command+"\n").encode())
  data = ""
  while True:
    d = s.recv(4096).decode()
    if re.search(r"^OK\n",d,re.M):
      d = re.split(r"^OK\n",d,re.M)[0]
      data += d
      return data
    data += d 

def get_stuff(host,port):
  with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.settimeout(1)
    s.connect((host,port))
    data = s.recv(4096)

    # get playlist
    data = get_response(s,"playlist")
    playlist = parse_playlist(data)
    data = get_response(s,"status")
    status = parse_status(data)
    songmeta = []
    for n, songname in playlist:
      data = get_response(s,f'lsinfo "{songname}"')
      meta = parse_status(data)
      songmeta.append(meta)
    if "song" in status:
      songn = int(status["song"])
      try:
        csong = songmeta[songn]
      except IndexError:
        csong = { "error": "<index error>" }
    else:
      csong = None

    return ( status, playlist, songmeta, csong )

try:
  ic(get_stuff(host,port))
except (TimeoutError,ConnectionRefusedError) as e:
  ic(e)

Dump/Restore Playlist And State

I have 24 instances of mpd running on ports 6600..6623. To save state to/restore state from a .json file, I wrote this.

Usage:

mpl get 3 boing.json
mpl set 4 boing.json

Source:

#!/usr/bin/env python

import socket
from time import sleep
import re
import sys
import os
import json
from icecream import ic; ic.configureOutput(includeContext=True) # pip install icecream

default_host = "mrflibble"
base_port = 6600

def query(s,q):
  s.sendall(f"{q}\n".encode())
  data = ""
  while True:
    d = s.recv(4096).decode()
    if re.search(r"^OK\n",d,re.M):
      d = re.split(r"^OK\n",d,re.M)[0]
      data += d
      break
    data += d 
  if data.startswith("OK\n"):
    return ""
  data = data.split("\nOK\n")[0]
  return data

def restore_state(host,port,data):
  status = data['status']
  playlist = data['playlist']

  with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.settimeout(1)
    s.connect((host,port))
    data = s.recv(4096)

    query(s,"stop")
    query(s,"clear")
    print(f"Loading {len(playlist)} tracks")
    for fn in playlist:
      query(s,f'add "{fn}"')

    print(f"Restoring state")
    for k in ["repeat","random","single","consume"]:
      v = status[k]
      query(s,f"{k} {v}")

    state = status['state'] 
    if state != "stop":
      song = int(status["song"])
      elapsed = status["elapsed"]
      query(s,f"seek {song} {elapsed}")
      if state == "pause":
        query(s,f"pause 1")
      else:
        query(s,f"pause 0")

def get_status_and_playlist(host,port): # throws socket.TimeoutError
  with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.settimeout(1)
    s.connect((host,port))
    data = s.recv(4096)

    # get playlist
    data = query(s,"playlist")
    lines = data.splitlines()
    files = []
    for line in lines:
      if ": " in line:
        fn = line.split(": ",1)[-1]
        files.append(fn)
      elif line == "OK":
        break

    data = query(s,"status")
    lines = data.splitlines()
    status = {}
    for line in lines:
      if ": " in line:
        k,v = line.split(": ",1)
        status[k] = v

    return status,files

def printhelp():
  print(f"""{sys.argv[0]} <get|set> <idx> <filename>""")

def main():
  host = os.getenv("MPD_HOST",default_host)
  args = sys.argv[1:]
  try:
    cmd, idx, fn = args
    idx = int(idx)
  except ValueError:
    printhelp()
    if len(args) > 0:
      exit(1)
    else:
      exit(0)

  if cmd in cmds:
    cmds[cmd](host,idx,fn)
  else:
    print(f"Unknown command {cmd}")
    printhelp()
    exit(1)

def do_get(host,idx,fn):
  port = base_port + idx

  try:
    st,pl = get_status_and_playlist(host,port)
    data = { "status": st, "playlist": pl }
    try:
      j = json.dumps(data)
      if fn == "-":
        print(j)
      else:
        with open(fn,"wt") as f:
          print(j,file=f)
          print(f"Written {fn}")
    except Exception as e:
      print(f"#Fail({e}) writing to {fn}")

  except (TimeoutError,ConnectionRefusedError) as e:
    ic(idx,e)

def do_set(host,idx,fn):
  port = base_port + idx

  try:
    with open(fn) as f:
      data = json.load(f)
  except Exception:
    print(f"Failed to load json from {fn}")
    exit(1)

  port = base_port + idx

  try:
    restore_state(host,port,data)

  except (TimeoutError,ConnectionRefusedError) as e:
    ic(e)

cmds = {
  "get": do_get,
  "set": do_set
    }

if __name__ == "__main__":
  main()