main.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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. import argparse
  10. from mpd import MPDClient
  11. from mpd import ConnectionError
  12. from aw_core import dirs
  13. from aw_core.models import Event
  14. from aw_client.client import ActivityWatchClient
  15. logging.basicConfig(level=logging.WARN)
  16. parser = argparse.ArgumentParser("A watcher for MPD")
  17. parser.add_argument("--testing", action="store_true",
  18. help='run in testing mode')
  19. args = parser.parse_args()
  20. logger = logging.getLogger("aw-watcher-mpd")
  21. DEFAULT_CONFIG = """
  22. [aw-watcher-mpd]
  23. host = "localhost"
  24. port = 6600
  25. poll_time = 3.0"""
  26. def get_current_song(mpdClient) -> Optional[dict]:
  27. current_song = mpdClient.currentsong()
  28. status = mpdClient.status()
  29. if current_song and status["state"] == "play":
  30. current_song["elapsed"] = float(status["elapsed"])
  31. return current_song
  32. return None
  33. def data_from_song(song: dict) -> dict:
  34. song_title = song["title"]
  35. song_artist = song["artist"]
  36. song_album = song["album"]
  37. data = {}
  38. data["title"] = song_title
  39. data["artist"] = song_artist
  40. data["album"] = song_album
  41. data["duration"] = float(song["duration"])
  42. logging.debug("SONG: {} - {} ({})".format(song_title, song_artist, song_album))
  43. return data
  44. def mpdConnect(host=None, port=None):
  45. mpdClient = MPDClient()
  46. mpdClient.timeout = 1
  47. mpdClient.idletimeout = None
  48. mpdClient.connect(host, int(port))
  49. return mpdClient
  50. def load_config():
  51. from aw_core.config import load_config_toml as _load_config
  52. return _load_config("aw-watcher-mpd", DEFAULT_CONFIG)
  53. def print_statusline(msg):
  54. last_msg_length = (
  55. len(print_statusline.last_msg) if hasattr(print_statusline, "last_msg") else 0
  56. )
  57. print(" " * last_msg_length, end="\r")
  58. print(msg, end="\r")
  59. print_statusline.last_msg = msg
  60. def main():
  61. logging.basicConfig(level=logging.INFO)
  62. config_dir = dirs.get_config_dir("aw-watcher-mpd")
  63. config = load_config()
  64. poll_time = float(config["aw-watcher-mpd"].get("poll_time"))
  65. host = config["aw-watcher-mpd"].get("host", None)
  66. port = config["aw-watcher-mpd"].get("port", None)
  67. if not host or not port:
  68. logger.warning(
  69. "host or port not specified in config file (in folder {}).".format(
  70. config_dir
  71. )
  72. )
  73. sys.exit(1)
  74. # TODO: Fix --testing flag and set testing as appropriate
  75. aw = ActivityWatchClient("aw-watcher-mpd", testing=args.testing)
  76. bucketname = "{}_{}".format(aw.client_name, aw.client_hostname)
  77. aw.create_bucket(bucketname, "mpd-currently-playing", queued=True)
  78. aw.connect()
  79. mpdClient = None
  80. last_song = None
  81. song = None
  82. last_start_duration = 0
  83. last_elapsed = 0
  84. while True:
  85. try:
  86. if not mpdClient:
  87. print_statusline("Connecting to mpd...")
  88. mpdClient = mpdConnect(host, port)
  89. else:
  90. try:
  91. mpdClient.status()
  92. except ConnectionError as e:
  93. mpdClient = mpdConnect(host, port)
  94. except ConnectionRefusedError as e:
  95. print_statusline("Connection to mpd was refused, attempting to reconnect")
  96. sleep(poll_time)
  97. continue
  98. try:
  99. song = get_current_song(mpdClient)
  100. # from pprint import pprint
  101. # pprint(track)
  102. except Exception as e:
  103. logger.error("Unknown Error")
  104. logger.error(traceback.format_exc())
  105. sleep(0.1)
  106. continue
  107. try:
  108. # Outputs a new line when a song ends, giving a short history directly in the log
  109. if last_song:
  110. last_song_data = data_from_song(last_song)
  111. if not song or (
  112. song
  113. and last_song_data["title"] != data_from_song(song)["title"]
  114. ):
  115. song_td = timedelta(seconds=last_song["elapsed"])
  116. song_time = int(song_td.seconds / 60), int(song_td.seconds % 60)
  117. print_statusline(
  118. "Song ended ({}:{:02d}): {title} - {artist} ({album})\n".format(
  119. *song_time, **last_song_data
  120. )
  121. )
  122. last_song_data["startedDuration"] = last_start_duration
  123. event = Event(timestamp=datetime.now(timezone.utc), data=last_song_data)
  124. aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
  125. elif song and not (0 < (song["elapsed"] - last_elapsed) < 6):
  126. song_td = timedelta(seconds=last_song["elapsed"])
  127. song_time = int(song_td.seconds / 60), int(song_td.seconds % 60)
  128. print_statusline(
  129. "Song seeked ahead/backwards ({}:{:02d}): {title} - {artist} ({album})\n".format(
  130. *song_time, **last_song_data
  131. )
  132. )
  133. last_song_data["startedDuration"] = last_start_duration
  134. event = Event(timestamp=datetime.now(timezone.utc), data=last_song_data)
  135. aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
  136. if song:
  137. song_data = data_from_song(song)
  138. if not last_song or (last_song and last_song_data["title"] != song_data["title"]) or not (0 < (song["elapsed"] - last_elapsed) < 6):
  139. last_start_duration = song["elapsed"]
  140. last_elapsed = song["elapsed"]
  141. song_data["startedDuration"] = last_start_duration
  142. song_td = timedelta(seconds=song["elapsed"])
  143. song_time = int(song_td.seconds / 60), int(song_td.seconds % 60)
  144. print_statusline(
  145. "Current song ({}:{:02d}): {title} - {artist} ({album})".format(
  146. *song_time, **song_data
  147. )
  148. )
  149. event = Event(timestamp=datetime.now(timezone.utc), data=song_data)
  150. aw.heartbeat(bucketname, event, pulsetime=poll_time + 1, queued=True)
  151. else:
  152. print_statusline("Waiting for song to start playing...")
  153. last_song = song
  154. except Exception as e:
  155. print("An exception occurred: {}".format(e))
  156. traceback.print_exc()
  157. sleep(poll_time)
  158. if __name__ == "__main__":
  159. main()