]> kaliko git repositories - mpd-sima.git/blob - sima/core.py
Simplified first loop iteration detection
[mpd-sima.git] / sima / core.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009-2015, 2020, 2021 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         self.changed = None
52
53     def add_history(self):
54         """Handle local, in memory, short history"""
55         self.short_history.appendleft(self.player.current)
56
57     def register_plugin(self, plugin_class):
58         """Registers plugin in Sima instance..."""
59         plgn = plugin_class(self)
60         prio = int(plgn.priority)
61         self._plugins.append((prio, plgn))
62
63     def register_core_plugin(self, plugin_class):
64         """Registers core plugins"""
65         plgn = plugin_class(self)
66         prio = int(plgn.priority)
67         self._core_plugins.append((prio, plgn))
68
69     def foreach_plugin(self, method, *args, **kwds):
70         """Plugin's callbacks dispatcher"""
71         self.log.trace('dispatching %s to plugins', method)  # pylint: disable=no-member
72         for plugin in self.core_plugins:
73             getattr(plugin, method)(*args, **kwds)
74         for plugin in self.plugins:
75             getattr(plugin, method)(*args, **kwds)
76
77     @property
78     def core_plugins(self):
79         return [plugin[1] for plugin in
80                 sorted(self._core_plugins, key=lambda pl: pl[0], reverse=True)]
81
82     @property
83     def plugins(self):
84         return [plugin[1] for plugin in
85                 sorted(self._plugins, key=lambda pl: pl[0], reverse=True)]
86
87     def need_tracks(self):
88         """Is the player in need for tracks"""
89         if not self.enabled:
90             self.log.debug('Queueing disabled!')
91             return False
92         queue_trigger = self.config.getint('sima', 'queue_length')
93         if self.player.playmode.get('random'):
94             queue = self.player.playlist
95             self.log.debug('Currently %s track(s) in the playlist. (target %s)', len(queue), queue_trigger)
96         else:
97             queue = self.player.queue
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.debug('callback_need_track: %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         # reset change
121         self.changed = None
122         while True:
123             tmp = sleepfor.pop(0)
124             sleepfor.append(tmp)
125             self.log.info('Trying to reconnect in %4d seconds', tmp)
126             time.sleep(tmp)
127             try:
128                 self.player.connect()
129             except PlayerError as err:
130                 self.log.debug(err)
131                 continue
132             except PlayerError as err:
133                 # TODO: unhandled Player exceptions
134                 self.log.warning('Unhandled player exception: %s', err)
135             self.log.info('Got reconnected')
136             break
137         self.foreach_plugin('start')
138
139     def hup_handler(self, signum, frame):
140         self.log.warning('Caught a sighup!')
141         # Cleaning pending command
142         self.player.clean()
143         self.foreach_plugin('shutdown')
144         self.player.disconnect()
145         raise SigHup('SIGHUP caught!')
146
147     def shutdown(self):
148         """General shutdown method
149         """
150         self.log.warning('Starting shutdown.')
151         # Cleaning pending command
152         try:
153             self.player.clean()
154             self.foreach_plugin('shutdown')
155             self.player.disconnect()
156         except PlayerError as err:
157             self.log.error('Player error during shutdown: %s', err)
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: %(host)s:%(port)s', self.config['MPD'])
167             self.player.connect()
168             self.foreach_plugin('start')
169         except PlayerError as err:
170             self.log.warning('Player: %s', err)
171             self.reconnect_player()
172         while 42:
173             try:
174                 self.loop()
175             except PlayerError as err:
176                 self.log.warning('Player error: %s', err)
177                 self.reconnect_player()
178
179     def loop(self):
180         """Dispatching callbacks to plugins
181         """
182         # hanging here until a monitored event is raised in the player
183         if self.changed is None:  # first iteration goes through else
184             self.changed = ['playlist', 'player', 'skipped']
185         else:  # Wait for a change
186             self.changed = self.player.monitor()
187             self.log.debug('changed: %s', ', '.join(self.changed))
188         if 'playlist' in self.changed:
189             self.foreach_plugin('callback_playlist')
190         if 'player' in self.changed or 'options' in self.changed:
191             self.foreach_plugin('callback_player')
192         if 'database' in self.changed:
193             self.foreach_plugin('callback_player_database')
194         if 'skipped' in self.changed:
195             if self.player.state == 'play':
196                 self.log.info('Playing: %s', self.player.current)
197                 self.add_history()
198                 self.foreach_plugin('callback_next_song')
199         if self.need_tracks():
200             self.queue()
201
202 # VIM MODLINE
203 # vim: ai ts=4 sw=4 sts=4 expandtab