]> kaliko git repositories - mpd-sima.git/blob - sima/core.py
badc7b20240701412d86ddeb92d1eb1eeebac6b3
[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 = []
48         self._core_plugins = []
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)',
96                            len(queue), queue_trigger)
97         else:
98             queue = self.player.queue
99             self.log.debug('Currently %s track(s) ahead. (target %s)', len(queue), queue_trigger)
100         if len(queue) < queue_trigger:
101             return True
102         return False
103
104     def queue(self):
105         to_add = []
106         for plugin in self.plugins:
107             self.log.debug('callback_need_track: %s', 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         # reset change
122         self.changed = None
123         while True:
124             tmp = sleepfor.pop(0)
125             sleepfor.append(tmp)
126             self.log.info('Trying to reconnect in %4d seconds', tmp)
127             time.sleep(tmp)
128             try:
129                 self.player.connect()
130             except PlayerError as err:
131                 self.log.debug(err)
132                 continue
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         try:
151             self.player.clean()
152             self.foreach_plugin('shutdown')
153             self.player.disconnect()
154         except PlayerError as err:
155             self.log.error('Player error during shutdown: %s', err)
156         self.log.info('The way is shut, it was made by those who are dead. '
157                       'And the dead keep it…')
158         self.log.info('bye...')
159
160     def run(self):
161         try:
162             self.log.info('Connecting MPD: %(host)s:%(port)s', self.config['MPD'])
163             self.player.connect()
164             self.foreach_plugin('start')
165         except PlayerError as err:
166             self.log.warning('Player: %s', err)
167             self.reconnect_player()
168         while 42:
169             try:
170                 self.loop()
171             except PlayerError as err:
172                 self.log.warning('Player error: %s', err)
173                 self.reconnect_player()
174
175     def loop(self):
176         """Dispatching callbacks to plugins
177         """
178         # hanging here until a monitored event is raised in the player
179         if self.changed is None:  # first iteration goes through else
180             self.changed = ['playlist', 'player', 'skipped']
181         else:  # Wait for a change
182             self.changed = self.player.monitor()
183             self.log.debug('changed: %s', ', '.join(self.changed))
184         if 'playlist' in self.changed:
185             self.foreach_plugin('callback_playlist')
186         if 'player' in self.changed or 'options' in self.changed:
187             self.foreach_plugin('callback_player')
188         if 'database' in self.changed:
189             self.foreach_plugin('callback_player_database')
190         if 'skipped' in self.changed:
191             if self.player.state == 'play':
192                 self.log.info('Playing: %s', self.player.current)
193                 self.add_history()
194                 self.foreach_plugin('callback_next_song')
195         if self.need_tracks():
196             self.queue()
197
198 # VIM MODLINE
199 # vim: ai ts=4 sw=4 sts=4 expandtab