]> kaliko git repositories - mpd-sima.git/blob - sima/core.py
Deal with lost connection (Closes #53)
[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 sorted(self._plugins, key=lambda pl: pl[0], reverse=True)]
84
85     def need_tracks(self):
86         """Is the player in need for tracks"""
87         if not self.enabled:
88             self.log.debug('Queueing disabled!')
89             return False
90         queue_trigger = self.config.getint('sima', 'queue_length')
91         if self.player.playmode.get('random'):
92             queue = self.player.playlist
93             self.log.debug('Currently %s track(s) in the playlist. (target %s)', len(queue), queue_trigger)
94         else:
95             queue = self.player.queue
96             self.log.debug('Currently %s track(s) ahead. (target %s)', len(queue), queue_trigger)
97         if len(queue) < queue_trigger:
98             return True
99         return False
100
101     def queue(self):
102         to_add = list()
103         for plugin in self.plugins:
104             self.log.debug('callback_need_track: %s', plugin)
105             pl_candidates = getattr(plugin, 'callback_need_track')()
106             if pl_candidates:
107                 to_add.extend(pl_candidates)
108             if to_add:
109                 break
110         for track in to_add:
111             self.player.add(track)
112
113     def reconnect_player(self):
114         """Trying to reconnect cycling through longer timeout
115         cycle : 5s 10s 1m 5m 20m 1h
116         """
117         sleepfor = [5, 10, 60, 300, 1200, 3600]
118         while True:
119             tmp = sleepfor.pop(0)
120             sleepfor.append(tmp)
121             self.log.info('Trying to reconnect in %4d seconds', tmp)
122             time.sleep(tmp)
123             try:
124                 self.player.connect()
125             except PlayerError as err:
126                 self.log.debug(err)
127                 continue
128             except PlayerError as err:
129                 #TODO: unhandled Player exceptions
130                 self.log.warning('Unhandled player exception: %s', err)
131             self.log.info('Got reconnected')
132             break
133         self.foreach_plugin('start')
134
135     def hup_handler(self, signum, frame):
136         self.log.warning('Caught a sighup!')
137         # Cleaning pending command
138         self.player.clean()
139         self.foreach_plugin('shutdown')
140         self.player.disconnect()
141         raise SigHup('SIGHUP caught!')
142
143     def shutdown(self):
144         """General shutdown method
145         """
146         self.log.warning('Starting shutdown.')
147         # Cleaning pending command
148         try:
149             self.player.clean()
150             self.foreach_plugin('shutdown')
151             self.player.disconnect()
152         except PlayerError as err:
153             self.log.error('Player error during shutdown: %s', err)
154         self.log.info('The way is shut, it was made by those who are dead. '
155                       'And the dead keep it…')
156         self.log.info('bye...')
157
158     def run(self):
159         """
160         """
161         try:
162             self.log.info('Connecting MPD: %(host)s:%(port)s', self.config['MPD'])
163             self.player.connect()
164             self.foreach_plugin('start')
165         except PlayerError as err:
166             self.log.warning('Player: %s', err)
167             self.reconnect_player()
168         while 42:
169             try:
170                 self.loop()
171             except PlayerError as err:
172                 self.log.warning('Player error: %s', err)
173                 self.reconnect_player()
174                 del self.changed
175
176     def loop(self):
177         """Dispatching callbacks to plugins
178         """
179         # hanging here until a monitored event is raised in the player
180         if getattr(self, 'changed', False): # first iteration exception
181             self.changed = self.player.monitor()
182         else:  # first iteration goes through else
183             self.changed = ['playlist', 'player', 'skipped']
184         self.log.debug('changed: %s', ', '.join(self.changed))
185         if 'playlist' in self.changed:
186             self.foreach_plugin('callback_playlist')
187         if 'player' in self.changed or 'options' in self.changed:
188             self.foreach_plugin('callback_player')
189         if 'database' in self.changed:
190             self.foreach_plugin('callback_player_database')
191         if 'skipped' in self.changed:
192             if self.player.state == 'play':
193                 self.log.info('Playing: %s', self.player.current)
194                 self.add_history()
195                 self.foreach_plugin('callback_next_song')
196         if self.need_tracks():
197             self.queue()
198
199 # VIM MODLINE
200 # vim: ai ts=4 sw=4 sts=4 expandtab