mpc.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. from __future__ import print_function
  2. import logging
  3. import signal
  4. import gevent
  5. from gevent.queue import Queue
  6. from mpd import MPDClient, ConnectionError, CommandError
  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 = 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 (ConnectionError, ConnectionRefusedError,
  33. FileNotFoundError) as exc:
  34. self.log.warning('Connection to MPD failed ({}: {})'.
  35. format(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 = MPDClient(use_unicode=True)
  53. try:
  54. mpd_client.connect(self.conf['MPD_HOST'], self.conf['MPD_PORT'])
  55. except (ConnectionError, ConnectionRefusedError,
  56. FileNotFoundError) as exc:
  57. self.log.warning('Connection to MPD failed ({}: {})'.
  58. format(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. mpd = self._get_mpd()
  70. current = mpd.currentsong()
  71. pos = int(current.get('pos', 0))
  72. for song in mpd.playlistid():
  73. if int(song['pos']) != pos:
  74. mpd.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 <{}> (events disabled)'.format(
  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 {} to playlist (from <{}>:{}={})'.
  107. format(uri, songs['timespec'].get('nick', ''),
  108. songs['aids'], spec))
  109. insert_pos = 0 if len(mpd_client.playlistid()) == 0 else \
  110. int(mpd_client.currentsong().get('pos', 0)) + 1
  111. try:
  112. mpd_client.addid(uri, insert_pos)
  113. except CommandError:
  114. self.log.exception("Cannot insert song {}".format(uri))
  115. self.tmpcleaner.watch(uri.strip())
  116. class Controller(gevent.Greenlet):
  117. def __init__(self, conf):
  118. gevent.Greenlet.__init__(self)
  119. self.log = logging.getLogger(self.__class__.__name__)
  120. self.conf = conf
  121. self.q = Queue()
  122. self.player = Player(self.conf)
  123. if 'DB_URI' in self.conf:
  124. self.monitor = Monitor(self.q, self.conf)
  125. self.monitor.parent_greenlet = self
  126. else:
  127. self.monitor = None
  128. def _run(self):
  129. if self.monitor is not None:
  130. self.monitor.start()
  131. mw = MpcWatcher(self.q, self.conf, client=None)
  132. mw.parent_greenlet = self
  133. mw.start()
  134. t = Timer(int(self.conf['CHECK_SECS']) * 1000, self.q)
  135. t.parent_greenlet = self
  136. t.start()
  137. # at the very start, run a check!
  138. gevent.Greenlet.spawn(self.player.check_playlist)
  139. while True:
  140. value = self.q.get()
  141. self.log.debug('<- %s' % str(value))
  142. # emitter = value['emitter']
  143. kind = value['kind']
  144. args = value['args']
  145. if kind == 'timer' or (kind == 'mpc' and
  146. args[0] in ('player', 'playlist',
  147. 'connect')):
  148. gevent.Greenlet.spawn(self.player.check_playlist)
  149. try:
  150. self.player.tmpcleaner.check_playlist()
  151. except:
  152. pass
  153. elif kind == 'mpc':
  154. pass
  155. elif kind == 'uris_enqueue':
  156. try:
  157. self.player.enqueue(args[0])
  158. except AssertionError:
  159. raise
  160. except Exception:
  161. self.log.exception("Error while adding to queue; "
  162. "bad audiogen output?")
  163. elif (kind == 'signal' and args[0] == signal.SIGALRM) or \
  164. kind == 'refresh':
  165. # it's a tick!
  166. self.log.debug("Reload")
  167. self.monitor.q.put(dict(kind='forcetick'))
  168. gevent.Greenlet.spawn(self.player.check_playlist)
  169. else:
  170. self.log.warning("Unknown message: %s" % str(value))