Change the default value of the `time_format` setting in the mpd module so that it matches the default value used by `TimeWrapper`. This fixes a bug where the seconds weren't displaying correctly due to a typo. Before: `02:MS` After: `2:06`
225 lines
7.9 KiB
Python
225 lines
7.9 KiB
Python
from collections import defaultdict
|
|
import socket
|
|
from os.path import basename
|
|
from math import floor
|
|
|
|
from i3pystatus import IntervalModule, formatp
|
|
from i3pystatus.core.util import TimeWrapper
|
|
|
|
|
|
class MPD(IntervalModule):
|
|
"""
|
|
Displays various information from MPD (the music player daemon)
|
|
|
|
.. rubric:: Available formatters (uses :ref:`formatp`)
|
|
|
|
* `{title}` — (the title of the current song)
|
|
* `{album}` — (the album of the current song, can be an empty string \
|
|
(e.g. for online streams))
|
|
* `{artist}` — (can be empty, too)
|
|
* `{album_artist}` — (can be empty)
|
|
* `{filename}` — (file name with out extension and path; empty unless \
|
|
title is empty)
|
|
* `{song_elapsed}` — (Position in the currently playing song, uses \
|
|
:ref:`TimeWrapper`, default is `%m:%S`)
|
|
* `{song_length}` — (Length of the current song, same as song_elapsed)
|
|
* `{pos}` — (Position of current song in playlist, one-based)
|
|
* `{len}` — (Songs in playlist)
|
|
* `{status}` — (play, pause, stop mapped through the `status` dictionary)
|
|
* `{bitrate}` — (Current bitrate in kilobit/s)
|
|
* `{volume}` — (Volume set in MPD)
|
|
|
|
.. rubric:: Available callbacks
|
|
|
|
* ``switch_playpause`` — Plays if paused or stopped, otherwise pauses. \
|
|
Emulates ``mpc toggle``.
|
|
* ``stop`` — Stops playback. Emulates ``mpc stop``.
|
|
* ``next_song`` — Goes to next track in the playlist. Emulates ``mpc \
|
|
next``.
|
|
* ``previous_song`` — Goes to previous track in the playlist. Emulates \
|
|
``mpc prev``.
|
|
* ``mpd_command`` — Send a command directly to MPD's socket. The command \
|
|
is the second element of the list. Documentation for available commands can \
|
|
be found at https://www.musicpd.org/doc/protocol/command_reference.html
|
|
|
|
Example module registration with callbacks:
|
|
|
|
::
|
|
|
|
status.register("mpd",
|
|
on_leftclick="switch_playpause",
|
|
on_rightclick=["mpd_command", "stop"],
|
|
on_middleclick=["mpd_command", "shuffle"],
|
|
on_upscroll=["mpd_command", "seekcur -10"],
|
|
on_downscroll=["mpd_command", "seekcur +10"])
|
|
|
|
Note that ``next_song`` and ``previous_song``, and their ``mpd_command`` \
|
|
equivalents, are ignored while mpd is stopped.
|
|
|
|
"""
|
|
|
|
interval = 1
|
|
|
|
settings = (
|
|
("host"),
|
|
("port", "MPD port. If set to 0, host will we interpreted as a Unix \
|
|
socket."),
|
|
("format", "formatp string"),
|
|
("status", "Dictionary mapping pause, play and stop to output"),
|
|
("color", "The color of the text"),
|
|
("color_map", "The mapping from state to color of the text"),
|
|
("max_field_len", "Defines max length for in truncate_fields defined \
|
|
fields, if truncated, ellipsis are appended as indicator. It's applied \
|
|
*before* max_len. Value of 0 disables this."),
|
|
("max_len", "Defines max length for the hole string, if exceeding \
|
|
fields specefied in truncate_fields are truncated equaly. If truncated, \
|
|
ellipsis are appended as indicator. It's applied *after* max_field_len. Value \
|
|
of 0 disables this."),
|
|
("time_format", "format string for 'pos' and 'len' fields"),
|
|
("truncate_fields", "fields that will be truncated if exceeding \
|
|
max_field_len or max_len."),
|
|
("hide_inactive", "Hides status information when MPD is not running"),
|
|
("password", "A password for access to MPD. (This is sent in \
|
|
cleartext to the server.)"),
|
|
)
|
|
|
|
host = "localhost"
|
|
port = 6600
|
|
password = None
|
|
s = None
|
|
format = "{title} {status}"
|
|
status = {
|
|
"pause": "▷",
|
|
"play": "▶",
|
|
"stop": "◾",
|
|
}
|
|
color = "#FFFFFF"
|
|
color_map = {}
|
|
max_field_len = 25
|
|
max_len = 100
|
|
time_format = "%m:%S"
|
|
truncate_fields = ("title", "album", "artist", "album_artist")
|
|
hide_inactive = False
|
|
on_leftclick = "switch_playpause"
|
|
on_rightclick = "next_song"
|
|
on_upscroll = on_rightclick
|
|
on_downscroll = "previous_song"
|
|
|
|
def _mpd_command(self, sock, command):
|
|
try:
|
|
sock.send((command + "\n").encode("utf-8"))
|
|
except Exception as e:
|
|
if self.port != 0:
|
|
self.s = socket.create_connection((self.host, self.port))
|
|
else:
|
|
self.s = socket.socket(family=socket.AF_UNIX)
|
|
self.s.connect(self.host)
|
|
sock = self.s
|
|
sock.recv(8192)
|
|
if self.password is not None:
|
|
sock.send('password "{}"\n'.format(self.password).
|
|
encode("utf-8"))
|
|
sock.recv(8192)
|
|
sock.send((command + "\n").encode("utf-8"))
|
|
try:
|
|
reply = sock.recv(16384).decode("utf-8", "replace")
|
|
replylines = reply.split("\n")[:-2]
|
|
|
|
return dict(
|
|
(line.split(": ", 1)) for line in replylines
|
|
)
|
|
except Exception as e:
|
|
return None
|
|
|
|
def run(self):
|
|
try:
|
|
status = self._mpd_command(self.s, "status")
|
|
playback_state = status["state"]
|
|
if playback_state == "stop":
|
|
currentsong = {}
|
|
else:
|
|
currentsong = self._mpd_command(self.s, "currentsong") or {}
|
|
except Exception:
|
|
if self.hide_inactive:
|
|
self.output = {
|
|
"full_text": ""
|
|
}
|
|
if hasattr(self, "data"):
|
|
del self.data
|
|
return
|
|
|
|
fdict = {
|
|
"pos": int(status.get("song", 0)) + 1,
|
|
"len": int(status.get("playlistlength", 0)),
|
|
"status": self.status[playback_state],
|
|
"volume": int(status.get("volume", 0)),
|
|
|
|
"title": currentsong.get("Title", ""),
|
|
"album": currentsong.get("Album", ""),
|
|
"artist": currentsong.get("Artist", ""),
|
|
"album_artist": currentsong.get("AlbumArtist", ""),
|
|
"song_length": TimeWrapper(currentsong.get("Time", 0), default_format=self.time_format),
|
|
"song_elapsed": TimeWrapper(float(status.get("elapsed", 0)), default_format=self.time_format),
|
|
"bitrate": int(status.get("bitrate", 0)),
|
|
}
|
|
|
|
if not fdict["title"] and "file" in currentsong:
|
|
fdict["filename"] = '.'.join(
|
|
basename(currentsong["file"]).split('.')[:-1])
|
|
else:
|
|
fdict["filename"] = ""
|
|
|
|
if self.max_field_len > 0:
|
|
for key in self.truncate_fields:
|
|
if len(fdict[key]) > self.max_field_len:
|
|
fdict[key] = fdict[key][:self.max_field_len - 1] + "…"
|
|
|
|
self.data = fdict
|
|
full_text = formatp(self.format, **fdict).strip()
|
|
full_text_len = len(full_text)
|
|
if full_text_len > self.max_len and self.max_len > 0:
|
|
shrink = floor((self.max_len - full_text_len)
|
|
/ len(self.truncate_fields)) - 1
|
|
|
|
for key in self.truncate_fields:
|
|
fdict[key] = fdict[key][:shrink] + "…"
|
|
|
|
full_text = formatp(self.format, **fdict).strip()
|
|
color_map = defaultdict(lambda: self.color, self.color_map)
|
|
self.output = {
|
|
"full_text": full_text,
|
|
"color": color_map[playback_state],
|
|
}
|
|
|
|
def switch_playpause(self):
|
|
try:
|
|
self._mpd_command(self.s, "play"
|
|
if self._mpd_command(self.s, "status")["state"]
|
|
in ["pause", "stop"] else "pause 1")
|
|
except Exception as e:
|
|
pass
|
|
|
|
def stop(self):
|
|
try:
|
|
self._mpd_command(self.s, "stop")
|
|
except Exception as e:
|
|
pass
|
|
|
|
def next_song(self):
|
|
try:
|
|
self._mpd_command(self.s, "next")
|
|
except Exception as e:
|
|
pass
|
|
|
|
def previous_song(self):
|
|
try:
|
|
self._mpd_command(self.s, "previous")
|
|
except Exception as e:
|
|
pass
|
|
|
|
def mpd_command(self, command):
|
|
try:
|
|
self._mpd_command(self.s, command)
|
|
except Exception as e:
|
|
pass
|