# Copyright (C) 2015, Juan Riquelme González
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import math
import subprocess
from functools import partial
from libqtile import pangocffi
from libqtile.widget import base
def format_time(time_seconds_string):
"""Format time in seconds as [h:]mm:ss."""
return str(datetime.timedelta(seconds=float(time_seconds_string))).lstrip("0").lstrip(":")
[docs]class Cmus(base.ThreadPoolText):
"""A simple Cmus widget.
Show the metadata of now listening song and allow basic mouse
control from the bar:
- toggle pause (or play if stopped) on left click;
- skip forward in playlist on scroll up;
- skip backward in playlist on scroll down.
The following fields (extracted from ``cmus-remote -C status``) are available in the `format`
string:
- ``status``: cmus playback status, one of "playing", "paused" or "stopped".
- ``file``
- ``position``: Current position in [h:]mm:ss.
- ``position_percent``: Current position in percent.
- ``remaining``: Remaining time in [h:]mm:ss.
- ``remaining_percent``: Remaining time in percent.
- ``duration``: Total length in [h:]mm:ss.
- ``artist``
- ``album``
- ``albumartist``
- ``composer``
- ``comment``
- ``date``
- ``discnumber``
- ``genre``
- ``title``: Title or filename if no title is available.
- ``tracknumber``
- ``stream``
- ``status_text``: Text indicating the playback status, corresponds to one of `playing_text`,
`paused_text` or `stopped_text`.
Cmus (https://cmus.github.io) should be installed.
"""
defaults = [
("format", "{status_text}{artist} - {title}", "Format of playback info."),
("stream_format", "{status_text}{stream}", "Format of playback info for streams."),
(
"no_artist_format",
"{status_text}{title}",
"Format of playback info if no artist available.",
),
("playing_text", "♫ ", "Text to display when playing, if chosen."),
("playing_color", "00ff00", "Text colour when playing."),
("paused_text", "♫ ", "Text to display when paused, if chosen."),
("paused_color", "cecece", "Text color when paused."),
("stopped_text", "♫ ", "Text to display when stopped, if chosen."),
("stopped_color", "cecece", "Text color when stopped."),
("update_interval", 0.5, "Update Time in seconds."),
(
"play_icon",
"♫ ",
"DEPRECATED Text to display when playing, paused, and stopped, if chosen.",
),
("play_color", "", "DEPRECATED Text colour when playing."),
("noplay_color", "", "DEPRECATED Text colour when paused or stopped."),
]
def __init__(self, **config):
base.ThreadPoolText.__init__(self, "", **config)
self.add_defaults(Cmus.defaults)
self.status = ""
self.local = None
self.add_callbacks(
{
"Button1": self.play,
"Button4": partial(subprocess.Popen, ["cmus-remote", "-n"]),
"Button5": partial(subprocess.Popen, ["cmus-remote", "-r"]),
}
)
def _configure(self, qtile, parent_bar):
base.ThreadPoolText._configure(self, qtile, parent_bar)
# Backwards compatibility
if self.play_color:
self.playing_color = self.play_color
self.paused_color = self.play_color
if self.noplay_color:
self.stopped_color = self.noplay_color
def get_info(self):
"""Return a dictionary with info about the current cmus status."""
try:
output = self.call_process(["cmus-remote", "-C", "status"])
except subprocess.CalledProcessError as err:
output = err.output
if output.startswith("status"):
output = output.splitlines()
info = {
"status": "",
"file": "",
"position": "",
"position_percent": "",
"remaining": "",
"remaining_percent": "",
"duration": "",
"artist": "",
"album": "",
"albumartist": "",
"composer": "",
"comment": "",
"date": "",
"discnumber": "",
"genre": "",
"title": "",
"tracknumber": "",
"stream": "",
"status_text": "",
"play_icon": self.play_icon,
}
for line in output:
if line.startswith("set"):
break
for data in info:
match = data + " "
if match in line:
index = line.index(data)
if index < 5:
info[data] = line[len(data) + index :].strip()
break
# Set status text
status = info["status"]
info["status_text"] = getattr(self, f"{status}_text", self.stopped_text)
# Format and process duration and position
if info["position"] != "" and info["duration"] != "" and int(info["duration"]) > 0:
info["position_percent"] = (
str(math.floor(int(info["position"]) / int(info["duration"]) * 100)) + "%"
)
info["remaining_percent"] = (
str(
math.ceil(
(int(info["duration"]) - int(info["position"]))
/ int(info["duration"])
* 100
)
)
+ "%"
)
info["remaining"] = format_time(int(info["duration"]) - int(info["position"]))
info["position"] = format_time(info["position"])
info["duration"] = format_time(info["duration"])
else:
info["duration"] = ""
info["position"] = ""
return info
def now_playing(self):
"""Return a string with the now playing info."""
info = self.get_info()
now_playing = ""
if info:
display_format = self.format
status = info["status"]
if self.status != status:
self.status = status
if self.status == "playing":
self.layout.colour = self.playing_color
elif self.status == "paused":
self.layout.colour = self.paused_color
else:
self.layout.colour = self.stopped_color
self.local = info["file"].startswith("/")
if self.local:
if not info["title"]:
info["title"] = info["file"].split("/")[-1]
if not info["artist"]:
display_format = self.no_artist_format
elif info["stream"]:
display_format = self.stream_format
# Handle case if cmus was started and no file is selected yet
elif not info["file"]:
display_format = ""
now_playing = display_format.format(**info)
if now_playing.strip() == info["status_text"].strip():
now_playing = ""
return pangocffi.markup_escape_text(now_playing)
def play(self):
"""Play music if stopped, else toggle pause."""
if self.status in ("playing", "paused"):
subprocess.Popen(["cmus-remote", "-u"])
elif self.status == "stopped":
subprocess.Popen(["cmus-remote", "-p"])
def poll(self):
"""Poll content for the text box."""
return self.now_playing()