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()