title: Toy Mpd Client 1 tags: python mpd client server network 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](/aw/media/mpd/MpcHelperScript) — 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`). ```py #!/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 ```py #!/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 ```py #!/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 = "" 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": "" } 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: ```bash mpl get 3 boing.json mpl set 4 boing.json ``` Source: ```py #!/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]} """) 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() ```