Browse Source

Initial commit

Kristian Vos 1 year ago
commit
80b1f2e645
8 changed files with 306 additions and 0 deletions
  1. 3 0
      .gitignore
  2. 6 0
      Makefile
  3. 43 0
      README.md
  4. 44 0
      aw-watcher-mpv.spec
  5. 1 0
      aw_watcher_mpv/__init__.py
  6. 4 0
      aw_watcher_mpv/__main__.py
  7. 203 0
      aw_watcher_mpv/main.py
  8. 2 0
      requirements.txt

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+__pycache__/
+build/
+dist/

+ 6 - 0
Makefile

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

+ 43 - 0
README.md

@@ -0,0 +1,43 @@
+# aw-watcher-mpv
+An ActivityWatch watcher for MPV.  
+
+Based of off [aw-watcher-spotify](https://github.com/ActivityWatch/aw-watcher-spotify) and [aw-watcher-mpv](https://git.kvos.dev/kris/aw-watcher-mpd).
+
+## Development
+1. Install pip packages from the requirements.txt.  
+`pip install -r requirements.txt`
+2. Start ActivityWatch in testing mode.  
+`aw-qt --testing`
+3. `cd aw_watcher_mpv`
+4. `python main.py --testing`
+
+## Building
+1. make clean
+2. make package
+
+## Installing (Linux)
+To install aw-watcher-mpv, first build it, then follow the below steps to install and automatically start the watcher when ActivityWatch starts.
+1. `cp -R dist/aw-watcher-mpv /opt`
+2. Create `/usr/bin/aw-watcher-mpv`  
+Contents:
+
+```bash
+#!/bin/bash
+
+/opt/aw-watcher-mpv/aw-watcher-mpv "$@"
+```
+3. chmod +x `/usr/bin/aw-watcher-mpv`
+4. Edit `~/.config/activitywatch/aw-qt/aw-qt.toml`  
+Uncomment the first two lines and add `aw-watcher-mpv` to autostart.  
+Example:
+
+```toml
+[aw-qt]
+autostart_modules = ["aw-server", "aw-watcher-afk", "aw-watcher-window", "aw-watcher-mpv"]
+```
+
+## Configuring
+To configure settings for aw-watcher-mpv such as the socket file location and ActivityWatch poll time, first run the application once, and then edit `~/.config/activitywatch/aw-watcher-mpv/aw-watcher-mpv.toml`.
+
+## MPV (Linux)
+This watcher works by connecting to an MPV socket file. By default, MPV does not use a socket file, so to use this you need to use a special argument when running MPV. You can do this manually by adding `--input-ipc-server=/tmp/mpv-socket`, or you can also add this in MPV's config file(s), for example by adding `input-ipc-server=/tmp/mpv-socket` to `~/.config/mpv/mpv.conf`.

+ 44 - 0
aw-watcher-mpv.spec

@@ -0,0 +1,44 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+block_cipher = None
+
+
+a = Analysis(['aw_watcher_mpv/__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-mpv',
+          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-mpv')

+ 1 - 0
aw_watcher_mpv/__init__.py

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

+ 4 - 0
aw_watcher_mpv/__main__.py

@@ -0,0 +1,4 @@
+from aw_watcher_mpv.main import main
+
+if __name__ == "__main__":
+    main()

+ 203 - 0
aw_watcher_mpv/main.py

@@ -0,0 +1,203 @@
+#!/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
+
+from python_mpv_jsonipc import MPV
+
+import argparse
+
+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 MPV")
+parser.add_argument("--testing", action="store_true",
+                    help='run in testing mode')
+args = parser.parse_args()
+
+logger = logging.getLogger("aw-watcher-mpv")
+DEFAULT_CONFIG = """
+[aw-watcher-mpv]
+ipc_socket = "/tmp/mpv-socket"
+poll_time = 3.0"""
+
+def get_current_video(mpv) -> Optional[dict]:
+    if (mpv.command("get_property", "duration") == None):
+        return None
+    
+    current_video = {}
+    current_video["duration"] = float(mpv.command("get_property", "duration"))
+    current_video["filename"] = mpv.command("get_property", "filename")
+    current_video["time-pos"] = float(mpv.command("get_property", "time-pos"))
+    current_video["volume"] = float(mpv.command("get_property", "volume"))
+    current_video["mute"] = mpv.command("get_property", "mute")
+    current_video["audio"] = "{} - {}".format(
+        mpv.command("get_property", "current-tracks/audio/id"),
+        mpv.command("get_property", "current-tracks/audio/lang"),
+    )
+    current_video["sub"] = "{} - {}".format(
+        mpv.command("get_property", "current-tracks/sub/id"),
+        mpv.command("get_property", "current-tracks/sub/lang")
+    )
+    pause = mpv.command("get_property", "pause")
+
+    if pause == False:
+        return current_video
+    return None
+
+def data_from_video(video: dict) -> dict:
+    data = {}
+    data["filename"] = video["filename"]
+    data["duration"] = video["duration"]
+    data["volume"] = video["volume"]
+    data["mute"] = video["mute"]
+    data["audio"] = video["audio"]
+    data["sub"] = video["sub"]
+
+    logging.debug("VIDEO: {}".format(data["filename"]))
+
+    return data
+
+def mpvConnect(ipc_socket=None):
+    mpv = MPV(start_mpv=False, ipc_socket=ipc_socket)
+    return mpv
+
+def load_config():
+    from aw_core.config import load_config_toml as _load_config
+
+    return _load_config("aw-watcher-mpv", 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-mpv")
+
+    config = load_config()
+    poll_time = float(config["aw-watcher-mpv"].get("poll_time"))
+    ipc_socket = config["aw-watcher-mpv"].get("ipc_socket", None)
+    if not ipc_socket:
+        logger.warning(
+            "ipc_socket 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-mpv", testing=args.testing)
+    bucketname = "{}_{}".format(aw.client_name, aw.client_hostname)
+    aw.create_bucket(bucketname, "mpv-currently-playing", queued=True)
+    aw.connect()
+
+    mpv = None
+    last_video = None
+    video = None
+    last_start_duration = 0
+    last_elapsed = 0
+    while True:
+        try:
+            if not mpv:
+                print_statusline("Connecting to mpv...")
+                mpv = mpvConnect(ipc_socket)
+            else:
+                try:
+                    mpv.command("get_property", "duration")
+                except BrokenPipeError as e:
+                    mpv = mpvConnect(ipc_socket)
+        except Exception as e:
+            print_statusline("Connection to mpv was refused, attempting to reconnect")
+            sleep(poll_time)
+            continue
+
+        try:
+            video = get_current_video(mpv)
+        except Exception as e:
+            logger.error("Unknown Error")
+            logger.error(traceback.format_exc())
+            sleep(0.1)
+            continue
+
+        try:
+            # Outputs a new line when a video ends, giving a short history directly in the log
+            if last_video:
+                last_video_data = data_from_video(last_video)
+                if not video or (
+                    video
+                    and last_video_data["filename"] != data_from_video(video)["filename"]
+                ):
+                    video_td = timedelta(seconds=last_video["time-pos"])
+                    video_time = int(video_td.seconds / 60), int(video_td.seconds % 60)
+                    print_statusline(
+                        "Video ended ({}:{:02d}): {filename}\n".format(
+                            *video_time, **last_video_data
+                        )
+                    )
+
+                    last_video_data["startedDuration"] = last_start_duration
+
+                    event = Event(timestamp=datetime.now(timezone.utc), data=last_video_data)
+                    aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
+                elif video and not (0 < (video["time-pos"] - last_elapsed) < 6):
+                    video_td = timedelta(seconds=last_video["time-pos"])
+                    video_time = int(video_td.seconds / 60), int(video_td.seconds % 60)
+                    print_statusline(
+                        "Video seeked ahead/backwards ({}:{:02d}): {filename}\n".format(
+                            *video_time, **last_video_data
+                        )
+                    )
+
+                    last_video_data["startedDuration"] = last_start_duration
+
+                    event = Event(timestamp=datetime.now(timezone.utc), data=last_video_data)
+                    aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
+
+            if video:
+                video_data = data_from_video(video)
+                
+                if not last_video or (last_video and last_video_data["filename"] != video_data["filename"]) or not (0 < (video["time-pos"] - last_elapsed) < 6):
+                    last_start_duration = video["time-pos"]
+
+                last_elapsed = video["time-pos"]
+
+                video_data["startedDuration"] = last_start_duration
+
+                video_td = timedelta(seconds=video["time-pos"])
+                video_time = int(video_td.seconds / 60), int(video_td.seconds % 60)
+
+                print_statusline(
+                    "Current video ({}:{:02d}): {filename}".format(
+                        *video_time, **video_data
+                    )
+                )
+
+                event = Event(timestamp=datetime.now(timezone.utc), data=video_data)
+                aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
+            else:
+                print_statusline("Waiting for video to start playing...")
+
+            last_video = video
+        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-mpv-jsonipc