]> kaliko git repositories - mpd-sima.git/blob - sima/core.py
4b7307ea72796cc57fee1ba0d9208ccb37cf6830
[mpd-sima.git] / sima / core.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009-2015, 2020 kaliko <kaliko@azylum.org>
3 #
4 #  This file is part of sima
5 #
6 #  sima is free software: you can redistribute it and/or modify
7 #  it under the terms of the GNU General Public License as published by
8 #  the Free Software Foundation, either version 3 of the License, or
9 #  (at your option) any later version.
10 #
11 #  sima is distributed in the hope that it will be useful,
12 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #  GNU General Public License for more details.
15 #
16 #  You should have received a copy of the GNU General Public License
17 #  along with sima.  If not, see <http://www.gnu.org/licenses/>.
18 #
19 #
20 """Core Object dealing with plugins and player client
21 """
22
23 import time
24
25 from collections import deque
26 from logging import getLogger
27
28 from .mpdclient import MPD as PlayerClient
29 from .mpdclient import PlayerError
30 from .lib.simadb import SimaDB
31 from .lib.daemon import Daemon
32 from .utils.utils import SigHup
33
34
35 class Sima(Daemon):
36     """Main class, plugin and player management
37     """
38
39     def __init__(self, conf):
40         # Set daemon
41         Daemon.__init__(self, conf.get('daemon', 'pidfile'))
42         self.enabled = True
43         self.config = conf
44         self.sdb = SimaDB(db_path=conf.get('sima', 'db_file'))
45         PlayerClient.database = self.sdb
46         self.log = getLogger('sima')
47         self._plugins = list()
48         self._core_plugins = list()
49         self.player = PlayerClient(conf)  # MPD client
50         self.short_history = deque(maxlen=60)
51
52     def add_history(self):
53         """Handle local, in memory, short history"""
54         self.short_history.appendleft(self.player.current)
55
56     def register_plugin(self, plugin_class):
57         """Registers plugin in Sima instance..."""
58         plgn = plugin_class(self)
59         prio = int(plgn.priority)
60         self._plugins.append((prio, plgn))
61
62     def register_core_plugin(self, plugin_class):
63         """Registers core plugins"""
64         plgn = plugin_class(self)
65         prio = int(plgn.priority)
66         self._core_plugins.append((prio, plgn))
67
68     def foreach_plugin(self, method, *args, **kwds):
69         """Plugin's callbacks dispatcher"""
70         self.log.trace('dispatching %s to plugins', method)  # pylint: disable=no-member
71         for plugin in self.core_plugins:
72             getattr(plugin, method)(*args, **kwds)
73         for plugin in self.plugins:
74             getattr(plugin, method)(*args, **kwds)
75
76     @property
77     def core_plugins(self):
78         return [plugin[1] for plugin in
79                 sorted(self._core_plugins, key=lambda pl: pl[0], reverse=True)]
80
81     @property
82     def plugins(self):
83         return [plugin[1] for plugin in
84                 sorted(self._plugins, key=lambda pl: pl[0], reverse=True)]
85
86     def need_tracks(self):
87         """Is the player in need for tracks"""
88         if not self.enabled:
89             self.log.debug('Queueing disabled!')
90             return False
91         queue_trigger = self.config.getint('sima', 'queue_length')
92         if self.player.playmode.get('random'):
93             queue = self.player.playlist
94             self.log.debug('Currently %s track(s) in the playlist. (target %s)', len(queue), queue_trigger)
95         else:
96             queue = self.player.queue
97             self.log.debug('Currently %s track(s) ahead. (target %s)', len(queue), queue_trigger)
98         if len(queue) < queue_trigger:
99             return True
100         return False
101
102     def queue(self):
103         to_add = list()
104         for plugin in self.plugins:
105             self.log.debug('callback_need_track: %s', plugin)
106             pl_candidates = getattr(plugin, 'callback_need_track')()
107             if pl_candidates:
108                 to_add.extend(pl_candidates)
109             if to_add:
110                 break
111         for track in to_add:
112             self.player.add(track)
113
114     def reconnect_player(self):
115         """Trying to reconnect cycling through longer timeout
116         cycle : 5s 10s 1m 5m 20m 1h
117         """
118         sleepfor = [5, 10, 60, 300, 1200, 3600]
119         while True:
120             tmp = sleepfor.pop(0)
121             sleepfor.append(tmp)
122             self.log.info('Trying to reconnect in %4d seconds', tmp)
123             time.sleep(tmp)
124             try:
125                 self.player.connect()
126             except PlayerError as err:
127                 self.log.debug(err)
128                 continue
129             except PlayerError as err:
130                 # TODO: unhandled Player exceptions
131                 self.log.warning('Unhandled player exception: %s', err)
132             self.log.info('Got reconnected')
133             break
134         self.foreach_plugin('start')
135
136     def hup_handler(self, signum, frame):
137         self.log.warning('Caught a sighup!')
138         # Cleaning pending command
139         self.player.clean()
140         self.foreach_plugin('shutdown')
141         self.player.disconnect()
142         raise SigHup('SIGHUP caught!')
143
144     def shutdown(self):
145         """General shutdown method
146         """
147         self.log.warning('Starting shutdown.')
148         # Cleaning pending command
149         try:
150             self.player.clean()
151             self.foreach_plugin('shutdown')
152             self.player.disconnect()
153         except PlayerError as err:
154             self.log.error('Player error during shutdown: %s', err)
155         self.log.info('The way is shut, it was made by those who are dead. '
156                       'And the dead keep it…')
157         self.log.info('bye...')
158
159     def run(self):
160         """
161         """
162         try:
163             self.log.info('Connecting MPD: %(host)s:%(port)s', self.config['MPD'])
164             self.player.connect()
165             self.foreach_plugin('start')
166         except PlayerError as err:
167             self.log.warning('Player: %s', err)
168             self.reconnect_player()
169         while 42:
170             try:
171                 self.loop()
172             except PlayerError as err:
173                 self.log.warning('Player error: %s', err)
174                 self.reconnect_player()
175                 del self.changed
176
177     def loop(self):
178         """Dispatching callbacks to plugins
179         """
180         # hanging here until a monitored event is raised in the player
181         if getattr(self, 'changed', False):  # first iteration exception
182             self.changed = self.player.monitor()
183         else:  # first iteration goes through else
184             self.changed = ['playlist', 'player', 'skipped']
185         self.log.debug('changed: %s', ', '.join(self.changed))
186         if 'playlist' in self.changed:
187             self.foreach_plugin('callback_playlist')
188         if 'player' in self.changed or 'options' in self.changed:
189             self.foreach_plugin('callback_player')
190         if 'database' in self.changed:
191             self.foreach_plugin('callback_player_database')
192         if 'skipped' in self.changed:
193             if self.player.state == 'play':
194                 self.log.info('Playing: %s', self.player.current)
195                 self.add_history()
196                 self.foreach_plugin('callback_next_song')
197         if self.need_tracks():
198             self.queue()
199
200 # VIM MODLINE
201 # vim: ai ts=4 sw=4 sts=4 expandtab