#!/usr/bin/env python3
"""Put the `music` player on MPRIS so the desktop's media controls reach it.

mpv speaks its own JSON IPC and nothing else: it does not register on D-Bus
unless the mpv-mpris plugin is installed (`sudo apt install mpv-mpris`), so
play/pause keys, which COSMIC routes to the active MPRIS player, would go to
some other player (typically a browser) instead.

This is such a bridge, in a form that needs no root: it owns
org.mpris.MediaPlayer2.mpv-music for as long as mpv is alive and forwards the
standard calls to the IPC socket. Started by `music`; exits by itself when mpv
does.

Dependencies: python3-gi and python3-pydbus (Debian/Ubuntu package names).
"""
import json
import os
import socket
import sys
import time

import gi
gi.require_version("GLib", "2.0")
from gi.repository import GLib
from pydbus import SessionBus

SOCK = os.environ.get("MUSIC_SOCKET") or os.path.join(
    os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "mpv-music.sock")


def ipc(command):
    """One request to mpv. Returns the `data` field, or None if it is gone."""
    try:
        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        s.settimeout(1.0)
        s.connect(SOCK)
        s.sendall((json.dumps({"command": command}) + "\n").encode())
        buf = b""
        while b"\n" not in buf:
            chunk = s.recv(4096)
            if not chunk:
                break
            buf += chunk
        s.close()
        for line in buf.split(b"\n"):
            if not line.strip():
                continue
            reply = json.loads(line)
            if "error" in reply:            # events share the socket; skip them
                return reply.get("data")
    except (OSError, ValueError):
        return None
    return None


class Player(object):
    """
    <node>
      <interface name="org.mpris.MediaPlayer2">
        <method name="Raise"/>
        <method name="Quit"/>
        <property name="CanQuit" type="b" access="read"/>
        <property name="CanRaise" type="b" access="read"/>
        <property name="HasTrackList" type="b" access="read"/>
        <property name="Identity" type="s" access="read"/>
        <property name="SupportedUriSchemes" type="as" access="read"/>
        <property name="SupportedMimeTypes" type="as" access="read"/>
      </interface>
      <interface name="org.mpris.MediaPlayer2.Player">
        <method name="PlayPause"/>
        <method name="Play"/>
        <method name="Pause"/>
        <method name="Stop"/>
        <method name="Next"/>
        <method name="Previous"/>
        <property name="PlaybackStatus" type="s" access="read"/>
        <property name="Metadata" type="a{sv}" access="read"/>
        <property name="Volume" type="d" access="readwrite"/>
        <property name="Position" type="x" access="read"/>
        <property name="Rate" type="d" access="read"/>
        <property name="MinimumRate" type="d" access="read"/>
        <property name="MaximumRate" type="d" access="read"/>
        <property name="CanGoNext" type="b" access="read"/>
        <property name="CanGoPrevious" type="b" access="read"/>
        <property name="CanPlay" type="b" access="read"/>
        <property name="CanPause" type="b" access="read"/>
        <property name="CanSeek" type="b" access="read"/>
        <property name="CanControl" type="b" access="read"/>
      </interface>
    </node>
    """

    # org.mpris.MediaPlayer2
    def Raise(self):
        pass

    def Quit(self):
        ipc(["quit"])

    CanQuit = True
    CanRaise = False
    HasTrackList = False
    Identity = "music (mpv)"
    SupportedUriSchemes = ["file"]
    SupportedMimeTypes = ["audio/mpeg", "audio/flac", "audio/ogg", "audio/mp4"]

    # org.mpris.MediaPlayer2.Player
    def PlayPause(self):
        ipc(["cycle", "pause"])

    def Play(self):
        ipc(["set_property", "pause", False])

    def Pause(self):
        ipc(["set_property", "pause", True])

    def Stop(self):
        ipc(["quit"])

    def Next(self):
        ipc(["playlist-next", "force"])

    def Previous(self):
        ipc(["playlist-prev", "force"])

    @property
    def PlaybackStatus(self):
        return "Paused" if ipc(["get_property", "pause"]) is True else "Playing"

    @property
    def Metadata(self):
        title = ipc(["get_property", "media-title"]) or ""
        path = ipc(["get_property", "path"]) or ""
        artist = ipc(["get_property", "metadata/by-key/artist"])
        if not artist and path:
            artist = os.path.basename(os.path.dirname(path))
        meta = {
            "mpris:trackid": GLib.Variant("o", "/org/mpris/MediaPlayer2/music"),
            "xesam:title": GLib.Variant("s", title),
        }
        if artist:
            meta["xesam:artist"] = GLib.Variant("as", [artist])
        if path:
            meta["xesam:url"] = GLib.Variant("s", "file://" + path)
        length = ipc(["get_property", "duration"])
        if isinstance(length, (int, float)):
            meta["mpris:length"] = GLib.Variant("x", int(length * 1e6))
        return meta

    @property
    def Volume(self):
        v = ipc(["get_property", "volume"])
        return (v or 100.0) / 100.0

    @Volume.setter
    def Volume(self, value):
        ipc(["set_property", "volume", max(0.0, min(1.5, value)) * 100.0])

    @property
    def Position(self):
        p = ipc(["get_property", "time-pos"])
        return int((p or 0) * 1e6)

    Rate = 1.0
    MinimumRate = 1.0
    MaximumRate = 1.0
    CanGoNext = True
    CanGoPrevious = True
    CanPlay = True
    CanPause = True
    CanSeek = False
    CanControl = True


def main():
    if not os.path.exists(SOCK):
        return 1
    bus = SessionBus()
    player = Player()
    # The bridge for the previous player may still be releasing the name.
    for attempt in range(6):
        try:
            published = bus.publish("org.mpris.MediaPlayer2.mpv-music",
                                    ("/org/mpris/MediaPlayer2", player))
            break
        except Exception:
            if attempt == 5:
                return 1
            time.sleep(1)
    loop = GLib.MainLoop()

    state = {}

    def watch():
        # Outliving mpv would leave a dead player advertised on the bus, and the
        # next play/pause would go to it and do nothing.
        if ipc(["get_property", "pid"]) is None:
            loop.quit()
            return False

        # Emitting PropertiesChanged is not decoration: playerctld routes a bare
        # play/pause key to the player that most recently announced a change, so
        # a silent player is one the media keys never reach while a browser tab
        # is also on the bus.
        status, meta = player.PlaybackStatus, player.Metadata
        title = meta.get("xesam:title")
        title = title.unpack() if title is not None else ""
        if (status, title) != (state.get("status"), state.get("title")):
            state.update(status=status, title=title)
            bus.con.emit_signal(
                None, "/org/mpris/MediaPlayer2",
                "org.freedesktop.DBus.Properties", "PropertiesChanged",
                GLib.Variant("(sa{sv}as)", (
                    "org.mpris.MediaPlayer2.Player",
                    {"PlaybackStatus": GLib.Variant("s", status),
                     "Metadata": GLib.Variant("a{sv}", meta)},
                    [])))
        return True

    GLib.timeout_add_seconds(1, watch)
    try:
        loop.run()
    finally:
        published.unpublish()
    return 0


if __name__ == "__main__":
    sys.exit(main())
