]> kaliko git repositories - mpd-sima.git/blob - sima/core.py
eea108d3a078eb15ecc88913b8bd98a532b21abc
[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         self.log.trace('dispatching %s to plugins', method)  # pylint: disable=no-member
77         for plugin in self.core_plugins:
78             getattr(plugin, method)(*args, **kwds)
79         for plugin in self.plugins:
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 %s track(s) ahead. (target %s)', len(queue), queue_trigger)
99         if len(queue) < queue_trigger:
100             return True
101         return False
102
103     def queue(self):
104         to_add = list()
105         for plugin in self.plugins:
106             self.log.info('running %s', plugin)
107             pl_candidates = getattr(plugin, 'callback_need_track')()
108             if pl_candidates:
109                 to_add.extend(pl_candidates)
110             if to_add:
111                 break
112         for track in to_add:
113             self.player.add(track)
114
115     def reconnect_player(self):
116         """Trying to reconnect cycling through longer timeout
117         cycle : 5s 10s 1m 5m 20m 1h
118         """
119         sleepfor = [5, 10, 60, 300, 1200, 3600]
120         while True:
121             tmp = sleepfor.pop(0)
122             sleepfor.append(tmp)
123             self.log.info('Trying to reconnect in {:>4d} seconds'.format(tmp))
124             time.sleep(tmp)
125             try:
126                 self.player.connect()
127             except PlayerError as err:
128                 self.log.debug(err)
129                 continue
130             except PlayerUnHandledError as err:
131                 #TODO: unhandled Player exceptions
132                 self.log.warning('Unhandled player exception: %s', err)
133             self.log.info('Got reconnected')
134             break
135         self.foreach_plugin('start')
136
137     def hup_handler(self, signum, frame):
138         self.log.warning('Caught a sighup!')
139         # Cleaning pending command
140         self.player.clean()
141         self.foreach_plugin('shutdown')
142         self.player.disconnect()
143         raise SigHup('SIGHUP caught!')
144
145     def shutdown(self):
146         """General shutdown method
147         """
148         self.log.warning('Starting shutdown.')
149         # Cleaning pending command
150         self.player.clean()
151         self.foreach_plugin('shutdown')
152         self.player.disconnect()
153
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: {0}:{1}'.format(*self.player._mpd))
163             self.player.connect()
164             self.foreach_plugin('start')
165         except (PlayerError, PlayerUnHandledError) as err:
166             self.log.warning('Player: %s', err)
167             self.reconnect_player()
168         while 42:
169             try:
170                 self.loop()
171             except PlayerUnHandledError as err:
172                 #TODO: unhandled Player exceptions
173                 self.log.warning('Unhandled player exception: %s', err)
174                 del self.player
175                 self.player = PlayerClient()
176                 time.sleep(10)
177             except PlayerError as err:
178                 self.log.warning('Player error: %s', err)
179                 self.reconnect_player()
180                 del self.changed
181
182     def loop(self):
183         """Dispatching callbacks to plugins
184         """
185         # hanging here untill a monitored event is raised in the player
186         if getattr(self, 'changed', False): # first iteration exception
187             self.changed = self.player.monitor()
188         else:  # first iteration goes through else
189             self.changed = ['playlist', 'player', 'skipped']
190         self.log.debug('changed: %s', ', '.join(self.changed))
191         if 'playlist' in self.changed:
192             self.foreach_plugin('callback_playlist')
193         if 'player' in self.changed or 'options' in self.changed:
194             self.foreach_plugin('callback_player')
195         if 'database' in self.changed:
196             self.foreach_plugin('callback_player_database')
197         if 'skipped' in self.changed:
198             if self.player.state == 'play':
199                 self.log.info('Playing: %s', self.player.current)
200                 self.add_history()
201                 self.foreach_plugin('callback_next_song')
202         if self.need_tracks():
203             self.queue()
204
205 # VIM MODLINE
206 # vim: ai ts=4 sw=4 sts=4 expandtab