Browse Source

Initial commit

Kristian Vos 2 years ago
commit
f3560cc7cb
8 changed files with 305 additions and 0 deletions
  1. 4 0
      .gitignore
  2. 6 0
      Makefile
  3. 35 0
      README.md
  4. 44 0
      aw-watcher-mpd.spec
  5. 1 0
      aw_watcher_mpd/__init__.py
  6. 15 0
      aw_watcher_mpd/__main__.py
  7. 198 0
      aw_watcher_mpd/main.py
  8. 2 0
      requirements.txt

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+__pycache__
+
+build
+dist

+ 6 - 0
Makefile

@@ -0,0 +1,6 @@
+package:
+	pyinstaller aw-watcher-mpd.spec --clean --noconfirm
+
+clean:
+	rm -rf build dist
+	rm -rf aw_watcher_mpd/__pycache__

+ 35 - 0
README.md

@@ -0,0 +1,35 @@
+# aw-watcher-mpd
+An ActivityWatch watcher for MPD.  
+
+Based of off [aw-watcher-spotify](https://github.com/ActivityWatch/aw-watcher-spotify)
+
+## Development
+1. Install pip packages from the requirements.txt.
+2. `cd aw_watcher_mpd`
+3. `python main.py --testing`
+
+## Building
+1. make clean
+2. make package
+
+## Installing (Linux)
+To install aw-watcher-mpd, first build it, then follow the below steps to install and automatically start the watcher when ActivityWatch starts.
+1. `cp -R dist/aw-watcher-mpd /opt`
+2. Create `/usr/bin/aw-watcher-mpd`  
+Contents:
+```bash
+#!/bin/bash
+
+/opt/aw-watcher-mpd/aw-watcher-mpd "$@"
+```
+3. chmod +x `/usr/bin/aw-watcher-mpd`
+4. Edit `~/.config/activitywatch/aw-qt/aw-qt.toml`  
+Uncomment the first two lines and add `aw-watcher-mpd` to autostart.  
+Example:
+```toml
+[aw-qt]
+autostart_modules = ["aw-server", "aw-watcher-afk", "aw-watcher-window", "aw-watcher-mpd"]
+```
+
+## Configuring
+To configure settings for aw-watcher mpd such as the MPD connection details and ActivityWatch poll time, first run the application once, and then edit `~/.config/activitywatch/aw-watcher-mpd/aw-watcher-mpd.toml`.

+ 44 - 0
aw-watcher-mpd.spec

@@ -0,0 +1,44 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+block_cipher = None
+
+
+a = Analysis(['aw_watcher_mpd/__main__.py'],
+             pathex=[],
+             binaries=[],
+             datas=[],
+             hiddenimports=[],
+             hookspath=[],
+             hooksconfig={},
+             runtime_hooks=[],
+             excludes=[],
+             win_no_prefer_redirects=False,
+             win_private_assemblies=False,
+             cipher=block_cipher,
+             noarchive=False)
+pyz = PYZ(a.pure, a.zipped_data,
+             cipher=block_cipher)
+
+exe = EXE(pyz,
+          a.scripts, 
+          [],
+          exclude_binaries=True,
+          name='aw-watcher-mpd',
+          debug=False,
+          bootloader_ignore_signals=False,
+          strip=False,
+          upx=True,
+          console=True,
+          disable_windowed_traceback=False,
+          target_arch=None,
+          codesign_identity=None,
+          entitlements_file=None )
+coll = COLLECT(exe,
+               a.binaries,
+               a.zipfiles,
+               a.datas, 
+               strip=False,
+               upx=True,
+               upx_exclude=[],
+               name='aw-watcher-mpd')

+ 1 - 0
aw_watcher_mpd/__init__.py

@@ -0,0 +1 @@
+from . import main

+ 15 - 0
aw_watcher_mpd/__main__.py

@@ -0,0 +1,15 @@
+# import sys
+# import os
+
+# path = os.path.dirname(sys.modules[__name__].__file__)
+# path = os.path.join(path, "..")
+# sys.path.insert(0, path)
+
+# import aw_watcher_mpd
+
+# aw_watcher_mpd.main()
+
+from aw_watcher_mpd.main import main
+
+if __name__ == "__main__":
+    main()

+ 198 - 0
aw_watcher_mpd/main.py

@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+
+import sys
+import logging
+import traceback
+from typing import Optional
+from time import sleep
+from datetime import datetime, timezone, timedelta
+import json
+
+import argparse
+
+from mpd import MPDClient
+from mpd import ConnectionError
+
+from aw_core import dirs
+from aw_core.models import Event
+from aw_client.client import ActivityWatchClient
+
+logging.basicConfig(level=logging.WARN)
+
+parser = argparse.ArgumentParser("A watcher for MPD")
+parser.add_argument("--testing", action="store_true",
+                    help='run in testing mode')
+args = parser.parse_args()
+
+logger = logging.getLogger("aw-watcher-mpd")
+DEFAULT_CONFIG = """
+[aw-watcher-mpd]
+host = "localhost"
+port = 6600
+poll_time = 3.0"""
+
+def get_current_song(mpdClient) -> Optional[dict]:
+    current_song = mpdClient.currentsong()
+    status = mpdClient.status()
+    if current_song and status["state"] == "play":
+        current_song["elapsed"] = float(status["elapsed"])
+        return current_song
+    return None
+
+def data_from_song(song: dict) -> dict:
+    song_title = song["title"]
+    song_artist = song["artist"]
+    song_album = song["album"]
+
+    data = {}
+    data["title"] = song_title
+    data["artist"] = song_artist
+    data["album"] = song_album
+    data["duration"] = float(song["duration"])
+
+    logging.debug("SONG: {} - {} ({})".format(song_title, song_artist, song_album))
+
+    return data
+
+def mpdConnect(host=None, port=None):
+    mpdClient = MPDClient()
+    mpdClient.timeout = 1
+    mpdClient.idletimeout = None
+    mpdClient.connect(host, int(port))
+    return mpdClient
+
+def load_config():
+    from aw_core.config import load_config_toml as _load_config
+
+    return _load_config("aw-watcher-mpd", DEFAULT_CONFIG)
+
+
+def print_statusline(msg):
+    last_msg_length = (
+        len(print_statusline.last_msg) if hasattr(print_statusline, "last_msg") else 0
+    )
+    print(" " * last_msg_length, end="\r")
+    print(msg, end="\r")
+    print_statusline.last_msg = msg
+
+
+def main():
+    logging.basicConfig(level=logging.INFO)
+
+    config_dir = dirs.get_config_dir("aw-watcher-mpd")
+
+    config = load_config()
+    poll_time = float(config["aw-watcher-mpd"].get("poll_time"))
+    host = config["aw-watcher-mpd"].get("host", None)
+    port = config["aw-watcher-mpd"].get("port", None)
+    if not host or not port:
+        logger.warning(
+            "host or port not specified in config file (in folder {}).".format(
+                config_dir
+            )
+        )
+        sys.exit(1)
+
+    # TODO: Fix --testing flag and set testing as appropriate
+    aw = ActivityWatchClient("aw-watcher-mpd", testing=args.testing)
+    bucketname = "{}_{}".format(aw.client_name, aw.client_hostname)
+    aw.create_bucket(bucketname, "mpd-currently-playing", queued=True)
+    aw.connect()
+
+    mpdClient = None
+    last_song = None
+    song = None
+    last_start_duration = 0
+    last_elapsed = 0
+    while True:
+        try:
+            if not mpdClient:
+                print_statusline("Connecting to mpd...")
+                mpdClient = mpdConnect(host, port)
+            else:
+                try:
+                    mpdClient.status()
+                except ConnectionError as e:
+                    mpdClient = mpdConnect(host, port)
+        except ConnectionRefusedError as e:
+            print_statusline("Connection to mpd was refused, attempting to reconnect")
+            sleep(poll_time)
+            continue
+
+        try:
+            song = get_current_song(mpdClient)
+            # from pprint import pprint
+            # pprint(track)
+        except Exception as e:
+            logger.error("Unknown Error")
+            logger.error(traceback.format_exc())
+            sleep(0.1)
+            continue
+
+        try:
+            # Outputs a new line when a song ends, giving a short history directly in the log
+            if last_song:
+                last_song_data = data_from_song(last_song)
+                if not song or (
+                    song
+                    and last_song_data["title"] != data_from_song(song)["title"]
+                ):
+                    song_td = timedelta(seconds=last_song["elapsed"])
+                    song_time = int(song_td.seconds / 60), int(song_td.seconds % 60)
+                    print_statusline(
+                        "Song ended ({}:{:02d}): {title} - {artist} ({album})\n".format(
+                            *song_time, **last_song_data
+                        )
+                    )
+
+                    last_song_data["startedDuration"] = last_start_duration
+
+                    event = Event(timestamp=datetime.now(timezone.utc), data=last_song_data)
+                    aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
+                elif song and not (0 < (song["elapsed"] - last_elapsed) < 6):
+                    song_td = timedelta(seconds=last_song["elapsed"])
+                    song_time = int(song_td.seconds / 60), int(song_td.seconds % 60)
+                    print_statusline(
+                        "Song seeked ahead/backwards ({}:{:02d}): {title} - {artist} ({album})\n".format(
+                            *song_time, **last_song_data
+                        )
+                    )
+
+                    last_song_data["startedDuration"] = last_start_duration
+
+                    event = Event(timestamp=datetime.now(timezone.utc), data=last_song_data)
+                    aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
+
+            if song:
+                song_data = data_from_song(song)
+
+                if not last_song or (last_song and last_song_data["title"] != song_data["title"]) or not (0 < (song["elapsed"] - last_elapsed) < 6):
+                    last_start_duration = song["elapsed"]
+
+                last_elapsed = song["elapsed"]
+
+                song_data["startedDuration"] = last_start_duration
+
+                song_td = timedelta(seconds=song["elapsed"])
+                song_time = int(song_td.seconds / 60), int(song_td.seconds % 60)
+
+                print_statusline(
+                    "Current song ({}:{:02d}): {title} - {artist} ({album})".format(
+                        *song_time, **song_data
+                    )
+                )
+
+                event = Event(timestamp=datetime.now(timezone.utc), data=song_data)
+                aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
+            else:
+                print_statusline("Waiting for song to start playing...")
+
+            last_song = song
+        except Exception as e:
+            print("An exception occurred: {}".format(e))
+            traceback.print_exc()
+        sleep(poll_time)
+
+
+if __name__ == "__main__":
+    main()

+ 2 - 0
requirements.txt

@@ -0,0 +1,2 @@
+aw-client
+python-mpd2