]> kaliko git repositories - mpd-sima.git/blob - sima/core.py
Fixed bug in MPD client reconnection
[mpd-sima.git] / sima / core.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009, 2010, 2011, 2013, 2014, 2015 Jack 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 .client import PlayerClient
29 from .client import PlayerError, PlayerUnHandledError
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 = self.__get_player()  # Player client
50         self.short_history = deque(maxlen=60)
51
52     def __get_player(self):
53         """Instanciate the player"""
54         host = self.config.get('MPD', 'host')
55         port = self.config.get('MPD', 'port')
56         pswd = self.config.get('MPD', 'password', fallback=None)
57         return PlayerClient(host, port, pswd)
58
59     def add_history(self):
60         """Handle local, in memory, short history"""
61         self.short_history.appendleft(self.player.current)
62
63     def register_plugin(self, plugin_class):
64         """Registers plugin in Sima instance..."""
65         plgn = plugin_class(self)
66         prio = int(plgn.priority)
67         self._plugins.append((prio, plgn))
68
69     def register_core_plugin(self, plugin_class):
70         """Registers core plugins"""
71         plgn = plugin_class(self)
72         prio = int(plgn.priority)
73         self._core_plugins.append((prio, plgn))
74
75     def foreach_plugin(self, method, *args, **kwds):
76         """Plugin's callbacks dispatcher"""
77         self.log.trace('dispatching %s to plugins', method)  # pylint: disable=no-member
78         for plugin in self.core_plugins:
79             getattr(plugin, method)(*args, **kwds)
80         for plugin in self.plugins:
81             getattr(plugin, method)(*args, **kwds)
82
83     @property
84     def core_plugins(self):
85         return [plugin[1] for plugin in
86                 sorted(self._core_plugins, key=lambda pl: pl[0], reverse=True)]
87
88     @property
89     def plugins(self):
90         return [plugin[1] for plugin in sorted(self._plugins, key=lambda pl: pl[0], reverse=True)]
91
92     def need_tracks(self):
93         """Is the player in need for tracks"""
94         if not self.enabled:
95             self.log.debug('Queueing disabled!')
96             return False
97         queue_trigger = self.config.getint('sima', 'queue_length')
98         if self.player.playmode.get('random'):
99             queue = self.player.playlist
100             self.log.debug('Currently %s track(s) in the playlist. (target %s)', len(queue), queue_trigger)
101         else:
102             queue = self.player.queue
103             self.log.debug('Currently %s track(s) ahead. (target %s)', len(queue), queue_trigger)
104         if len(queue) < queue_trigger:
105             return True
106         return False
107
108     def queue(self):
109         to_add = list()
110         for plugin in self.plugins:
111             self.log.info('callback_need_track: %s', plugin)
112             pl_candidates = getattr(plugin, 'callback_need_track')()
113             if pl_candidates:
114                 to_add.extend(pl_candidates)
115             if to_add:
116                 break
117         for track in to_add:
118             self.player.add(track)
119
120     def reconnect_player(self):
121         """Trying to reconnect cycling through longer timeout
122         cycle : 5s 10s 1m 5m 20m 1h
123         """
124         sleepfor = [5, 10, 60, 300, 1200, 3600]
125         while True:
126             tmp = sleepfor.pop(0)
127             sleepfor.append(tmp)
128             self.log.info('Trying to reconnect in {:>4d} seconds'.format(tmp))
129             time.sleep(tmp)
130             try:
131                 self.player.connect()
132             except PlayerError as err:
133                 self.log.debug(err)
134                 continue
135             except PlayerUnHandledError as err:
136                 #TODO: unhandled Player exceptions
137                 self.log.warning('Unhandled player exception: %s', err)
138             self.log.info('Got reconnected')
139             break
140         self.foreach_plugin('start')
141
142     def hup_handler(self, signum, frame):
143         self.log.warning('Caught a sighup!')
144         # Cleaning pending command
145         self.player.clean()
146         self.foreach_plugin('shutdown')
147         self.player.disconnect()
148         raise SigHup('SIGHUP caught!')
149
150     def shutdown(self):
151         """General shutdown method
152         """
153         self.log.warning('Starting shutdown.')
154         # Cleaning pending command
155         self.player.clean()
156         self.foreach_plugin('shutdown')
157         self.player.disconnect()
158
159         self.log.info('The way is shut, it was made by those who are dead. '
160                       'And the dead keep it…')
161         self.log.info('bye...')
162
163     def run(self):
164         """
165         """
166         try:
167             self.log.info('Connecting MPD: {0}:{1}'.format(*self.player._mpd))
168             self.player.connect()
169             self.foreach_plugin('start')
170         except (PlayerError, PlayerUnHandledError) as err:
171             self.log.warning('Player: %s', err)
172             self.reconnect_player()
173         while 42:
174             try:
175                 self.loop()
176             except PlayerUnHandledError as err:
177                 #TODO: unhandled Player exceptions
178                 self.log.warning('Unhandled player exception: %s', err)
179                 del self.player
180                 self.player = self.__get_player()
181                 time.sleep(5)
182             except PlayerError as err:
183                 self.log.warning('Player error: %s', err)
184                 self.reconnect_player()
185                 del self.changed
186
187     def loop(self):
188         """Dispatching callbacks to plugins
189         """
190         # hanging here untill a monitored event is raised in the player
191         if getattr(self, 'changed', False): # first iteration exception
192             self.changed = self.player.monitor()
193         else:  # first iteration goes through else
194             self.changed = ['playlist', 'player', 'skipped']
195         self.log.debug('changed: %s', ', '.join(self.changed))
196         if 'playlist' in self.changed:
197             self.foreach_plugin('callback_playlist')
198         if 'player' in self.changed or 'options' in self.changed:
199             self.foreach_plugin('callback_player')
200         if 'database' in self.changed:
201             self.foreach_plugin('callback_player_database')
202         if 'skipped' in self.changed:
203             if self.player.state == 'play':
204                 self.log.info('Playing: %s', self.player.current)
205                 self.add_history()
206                 self.foreach_plugin('callback_next_song')
207         if self.need_tracks():
208             self.queue()
209
210 # VIM MODLINE
211 # vim: ai ts=4 sw=4 sts=4 expandtab