main.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. #!/usr/bin/env python3
  2. import sys
  3. import logging
  4. import traceback
  5. from typing import Optional
  6. from time import sleep
  7. from datetime import datetime, timezone, timedelta
  8. import json
  9. from python_mpv_jsonipc import MPV
  10. import argparse
  11. from aw_core import dirs
  12. from aw_core.models import Event
  13. from aw_client.client import ActivityWatchClient
  14. logging.basicConfig(level=logging.WARN)
  15. parser = argparse.ArgumentParser("A watcher for MPV")
  16. parser.add_argument("--testing", action="store_true",
  17. help='run in testing mode')
  18. args, unknown = parser.parse_known_args()
  19. logger = logging.getLogger("aw-watcher-mpv")
  20. DEFAULT_CONFIG = """
  21. [aw-watcher-mpv]
  22. ipc_socket = "/tmp/mpv-socket"
  23. poll_time = 3.0"""
  24. def get_current_video(mpv) -> Optional[dict]:
  25. if (mpv.command("get_property", "duration") == None):
  26. return None
  27. current_video = {}
  28. current_video["duration"] = float(mpv.command("get_property", "duration"))
  29. current_video["filename"] = mpv.command("get_property", "filename")
  30. current_video["time-pos"] = float(mpv.command("get_property", "time-pos"))
  31. current_video["volume"] = float(mpv.command("get_property", "volume"))
  32. current_video["mute"] = mpv.command("get_property", "mute")
  33. current_video["audio"] = "{} - {}".format(
  34. mpv.command("get_property", "current-tracks/audio/id"),
  35. mpv.command("get_property", "current-tracks/audio/lang"),
  36. )
  37. current_video["sub"] = "{} - {}".format(
  38. mpv.command("get_property", "current-tracks/sub/id"),
  39. mpv.command("get_property", "current-tracks/sub/lang")
  40. )
  41. pause = mpv.command("get_property", "pause")
  42. if pause == False:
  43. return current_video
  44. return None
  45. def data_from_video(video: dict) -> dict:
  46. data = {}
  47. data["filename"] = video["filename"]
  48. data["duration"] = video["duration"]
  49. data["volume"] = video["volume"]
  50. data["mute"] = video["mute"]
  51. data["audio"] = video["audio"]
  52. data["sub"] = video["sub"]
  53. logging.debug("VIDEO: {}".format(data["filename"]))
  54. return data
  55. def mpvConnect(ipc_socket=None):
  56. mpv = MPV(start_mpv=False, ipc_socket=ipc_socket)
  57. return mpv
  58. def load_config():
  59. from aw_core.config import load_config_toml as _load_config
  60. return _load_config("aw-watcher-mpv", DEFAULT_CONFIG)
  61. def print_statusline(msg):
  62. last_msg_length = (
  63. len(print_statusline.last_msg) if hasattr(print_statusline, "last_msg") else 0
  64. )
  65. print(" " * last_msg_length, end="\r")
  66. print(msg, end="\r")
  67. print_statusline.last_msg = msg
  68. def main():
  69. logging.basicConfig(level=logging.INFO)
  70. config_dir = dirs.get_config_dir("aw-watcher-mpv")
  71. config = load_config()
  72. poll_time = float(config["aw-watcher-mpv"].get("poll_time"))
  73. ipc_socket = config["aw-watcher-mpv"].get("ipc_socket", None)
  74. if not ipc_socket:
  75. logger.warning(
  76. "ipc_socket not specified in config file (in folder {}).".format(
  77. config_dir
  78. )
  79. )
  80. sys.exit(1)
  81. # TODO: Fix --testing flag and set testing as appropriate
  82. aw = ActivityWatchClient("aw-watcher-mpv", testing=args.testing)
  83. bucketname = "{}_{}".format(aw.client_name, aw.client_hostname)
  84. aw.create_bucket(bucketname, "mpv-currently-playing", queued=True)
  85. aw.connect()
  86. mpv = None
  87. last_video = None
  88. video = None
  89. last_start_duration = 0
  90. last_elapsed = 0
  91. while True:
  92. try:
  93. if not mpv:
  94. print_statusline("Connecting to mpv...")
  95. mpv = mpvConnect(ipc_socket)
  96. else:
  97. try:
  98. mpv.command("get_property", "duration")
  99. except BrokenPipeError as e:
  100. mpv = mpvConnect(ipc_socket)
  101. except Exception as e:
  102. print_statusline("Connection to mpv was refused, attempting to reconnect")
  103. sleep(poll_time)
  104. continue
  105. try:
  106. video = get_current_video(mpv)
  107. except Exception as e:
  108. logger.error("Unknown Error")
  109. logger.error(traceback.format_exc())
  110. sleep(0.1)
  111. continue
  112. try:
  113. # Outputs a new line when a video ends, giving a short history directly in the log
  114. if last_video:
  115. last_video_data = data_from_video(last_video)
  116. if not video or (
  117. video
  118. and last_video_data["filename"] != data_from_video(video)["filename"]
  119. ):
  120. video_td = timedelta(seconds=last_video["time-pos"])
  121. video_time = int(video_td.seconds / 60), int(video_td.seconds % 60)
  122. print_statusline(
  123. "Video ended ({}:{:02d}): {filename}\n".format(
  124. *video_time, **last_video_data
  125. )
  126. )
  127. last_video_data["startedDuration"] = last_start_duration
  128. event = Event(timestamp=datetime.now(timezone.utc), data=last_video_data)
  129. aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
  130. elif video and not (0 < (video["time-pos"] - last_elapsed) < 6):
  131. video_td = timedelta(seconds=last_video["time-pos"])
  132. video_time = int(video_td.seconds / 60), int(video_td.seconds % 60)
  133. print_statusline(
  134. "Video seeked ahead/backwards ({}:{:02d}): {filename}\n".format(
  135. *video_time, **last_video_data
  136. )
  137. )
  138. last_video_data["startedDuration"] = last_start_duration
  139. event = Event(timestamp=datetime.now(timezone.utc), data=last_video_data)
  140. aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
  141. if video:
  142. video_data = data_from_video(video)
  143. if not last_video or (last_video and last_video_data["filename"] != video_data["filename"]) or not (0 < (video["time-pos"] - last_elapsed) < 6):
  144. last_start_duration = video["time-pos"]
  145. last_elapsed = video["time-pos"]
  146. video_data["startedDuration"] = last_start_duration
  147. video_td = timedelta(seconds=video["time-pos"])
  148. video_time = int(video_td.seconds / 60), int(video_td.seconds % 60)
  149. print_statusline(
  150. "Current video ({}:{:02d}): {filename}".format(
  151. *video_time, **video_data
  152. )
  153. )
  154. event = Event(timestamp=datetime.now(timezone.utc), data=video_data)
  155. aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
  156. else:
  157. print_statusline("Waiting for video to start playing...")
  158. last_video = video
  159. except Exception as e:
  160. print("An exception occurred: {}".format(e))
  161. traceback.print_exc()
  162. sleep(poll_time)
  163. if __name__ == "__main__":
  164. main()