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