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