mpc.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. from __future__ import print_function
  2. import logging
  3. import signal
  4. import gevent
  5. from gevent.queue import Queue
  6. import mpd
  7. from .event import Monitor
  8. from .eventutils import ParentedLet, Timer
  9. from .audiogen import audiogenerate
  10. from .unused import UnusedCleaner
  11. def get_mpd_client(conf):
  12. client = mpd.MPDClient(use_unicode=True)
  13. client.connect(conf['MPD_HOST'], conf['MPD_PORT'])
  14. return client
  15. class MpcWatcher(ParentedLet):
  16. def __init__(self, queue, conf, client=None):
  17. ParentedLet.__init__(self, queue)
  18. self.log = logging.getLogger(self.__class__.__name__)
  19. self.conf = conf
  20. self.client = client # assume it is already connected, or None
  21. def refresh_client(self):
  22. self.client = get_mpd_client(self.conf)
  23. def do_business(self):
  24. first_after_connection = True
  25. while True:
  26. try:
  27. if self.client is None:
  28. self.refresh_client()
  29. if first_after_connection:
  30. yield('mpc', 'connect')
  31. status = self.client.idle()[0]
  32. except (mpd.ConnectionError, ConnectionRefusedError,
  33. FileNotFoundError) as exc:
  34. self.log.warning('Connection to MPD failed (%s: %s)',
  35. exc.__class__.__name__, exc)
  36. self.client = None
  37. first_after_connection = True
  38. gevent.sleep(5)
  39. continue
  40. else:
  41. first_after_connection = False
  42. yield ('mpc', status)
  43. class Player:
  44. def __init__(self, conf):
  45. self.conf = conf
  46. self.log = logging.getLogger(self.__class__.__name__)
  47. self.min_playlist_length = 10
  48. self.tmpcleaner = UnusedCleaner(conf)
  49. self._continous_audiospec = self.conf['CONTINOUS_AUDIOSPEC']
  50. self.events_enabled = True
  51. def _get_mpd(self):
  52. mpd_client = mpd.MPDClient(use_unicode=True)
  53. try:
  54. mpd_client.connect(self.conf['MPD_HOST'], self.conf['MPD_PORT'])
  55. except (mpd.ConnectionError, ConnectionRefusedError,
  56. FileNotFoundError) as exc:
  57. self.log.warning('Connection to MPD failed (%s: %s)',
  58. exc.__class__.__name__, exc)
  59. raise gevent.GreenletExit()
  60. return mpd_client
  61. @property
  62. def continous_audiospec(self):
  63. return self._continous_audiospec
  64. @continous_audiospec.setter
  65. def continous_audiospec(self, spec):
  66. self._continous_audiospec = self.conf['CONTINOUS_AUDIOSPEC'] \
  67. if spec is None else spec
  68. def clear_everything_but_current_song():
  69. mpdc = self._get_mpd()
  70. current = mpdc.currentsong()
  71. pos = int(current.get('pos', 0))
  72. for song in mpdc.playlistid():
  73. if int(song['pos']) != pos:
  74. mpdc.deleteid(song['id'])
  75. gevent.Greenlet.spawn(clear_everything_but_current_song)
  76. def check_playlist(self):
  77. mpd_client = self._get_mpd()
  78. songs = mpd_client.playlist()
  79. current = mpd_client.currentsong()
  80. pos = int(current.get('pos', 0)) + 1
  81. if(len(songs) - pos >= self.min_playlist_length):
  82. return
  83. self.log.info('need to add new songs')
  84. picker = gevent.Greenlet(audiogenerate,
  85. self.continous_audiospec)
  86. def add(greenlet):
  87. uris = greenlet.value
  88. for uri in uris:
  89. assert type(uri) is str, type(uri)
  90. self.tmpcleaner.watch(uri.strip())
  91. mpd_client.add(uri.strip())
  92. picker.link_value(add)
  93. picker.start()
  94. def enqueue(self, songs):
  95. assert type(songs) is dict
  96. assert 'uris' in songs
  97. spec = [aspec.get('nick', aspec.eid) for aspec in songs['audiospecs']]
  98. if not self.events_enabled:
  99. self.log.debug('Ignoring <%s> (events disabled)',
  100. ','.join(spec)
  101. )
  102. return
  103. mpd_client = self._get_mpd()
  104. for uri in reversed(songs['uris']):
  105. assert type(uri) is str
  106. self.log.info('Adding %s to playlist (from <%s>:%s=%s)',
  107. uri,
  108. songs['timespec'].get('nick', ''),
  109. songs['aids'], spec)
  110. insert_pos = 0 if len(mpd_client.playlistid()) == 0 else \
  111. int(mpd_client.currentsong().get('pos', 0)) + 1
  112. try:
  113. mpd_client.addid(uri, insert_pos)
  114. except mpd.CommandError:
  115. self.log.exception("Cannot insert song %s", uri)
  116. self.tmpcleaner.watch(uri.strip())
  117. class Controller(gevent.Greenlet):
  118. def __init__(self, conf):
  119. gevent.Greenlet.__init__(self)
  120. self.log = logging.getLogger(self.__class__.__name__)
  121. self.conf = conf
  122. self.q = Queue()
  123. self.player = Player(self.conf)
  124. if 'DB_URI' in self.conf:
  125. self.monitor = Monitor(self.q, self.conf)
  126. self.monitor.parent_greenlet = self
  127. else:
  128. self.monitor = None
  129. def _run(self):
  130. if self.monitor is not None:
  131. self.monitor.start()
  132. mw = MpcWatcher(self.q, self.conf, client=None)
  133. mw.parent_greenlet = self
  134. mw.start()
  135. t = Timer(int(self.conf['CHECK_SECS']) * 1000, self.q)
  136. t.parent_greenlet = self
  137. t.start()
  138. # at the very start, run a check!
  139. gevent.Greenlet.spawn(self.player.check_playlist)
  140. while True:
  141. value = self.q.get()
  142. self.log.debug('<- %s', str(value))
  143. # emitter = value['emitter']
  144. kind = value['kind']
  145. args = value['args']
  146. if kind == 'timer' or (kind == 'mpc' and
  147. args[0] in ('player', 'playlist',
  148. 'connect')):
  149. gevent.Greenlet.spawn(self.player.check_playlist)
  150. try:
  151. self.player.tmpcleaner.check_playlist()
  152. except:
  153. pass
  154. elif kind == 'mpc':
  155. pass
  156. elif kind == 'uris_enqueue':
  157. try:
  158. self.player.enqueue(args[0])
  159. except AssertionError:
  160. raise
  161. except Exception:
  162. self.log.exception("Error while adding to queue; "
  163. "bad audiogen output?")
  164. elif (kind == 'signal' and args[0] == signal.SIGALRM) or \
  165. kind == 'refresh':
  166. # it's a tick!
  167. self.log.debug("Reload")
  168. self.monitor.q.put(dict(kind='forcetick'))
  169. gevent.Greenlet.spawn(self.player.check_playlist)
  170. else:
  171. self.log.warning("Unknown message: %s", str(value))