]> kaliko git repositories - mpd-sima.git/blob - sima/lib/plugin.py
Add album queue mode to Tags plugin
[mpd-sima.git] / sima / lib / plugin.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2013-2015, 2020 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 """
21 Plugin object to derive from
22 """
23
24 import random
25
26 from .track import Track
27 from .meta import Album, Artist
28
29
30 class Plugin:
31     """
32     First non-empty line of the docstring is used as description
33     Rest of the docstring at your convenience.
34
35     The lowercased plugin Name MUST be the same as the module (file name),
36     for instance Plugin → plugin.py
37     It eases plugins discovery and simplifies the code to handle them,
38     IMHO, it's a fair trade-off.
39     """
40
41     @classmethod
42     def info(cls):
43         """self documenting class method
44         """
45         doc = 'Undocumented plugin! Fill "{}" docstring'.format(cls.__name__)
46         if cls.__doc__:
47             doc = cls.__doc__.strip(' \n').splitlines()[0]
48         return {'name': cls.__name__,
49                 'doc': doc,}
50
51     def __init__(self, daemon):
52         self.log = daemon.log
53         self.__daemon = daemon
54         self.player = daemon.player
55         self.plugin_conf = None
56         self.__get_config()
57
58     def __str__(self):
59         return self.__class__.__name__
60
61     def __get_config(self):
62         """Get plugin's specific configuration from global applications's config
63         """
64         conf = self.__daemon.config
65         for sec in conf.sections():  # Discovering plugin conf
66             if sec == self.__class__.__name__.lower():
67                 self.plugin_conf = conf[sec]
68                 if 'priority' not in self.plugin_conf:
69                     self.plugin_conf['priority'] = '80'
70         if not self.plugin_conf:
71             self.plugin_conf = {'priority': '80'}
72         #if self.plugin_conf:
73         #    self.log.debug('Got config for %s: %s', self, self.plugin_conf)
74
75     @property
76     def priority(self):
77         return self.plugin_conf.get('priority')
78
79     def start(self):
80         """
81         Called when the daemon().run() is called and
82         right after the player has connected successfully.
83         """
84         pass
85
86     def callback_player(self):
87         """
88         Called on player changes, stopped, paused, skipped
89         """
90         pass
91
92     def callback_player_database(self):
93         """
94         Called on player music library changes
95         """
96         pass
97
98     def callback_playlist(self):
99         """
100         Called on playlist changes
101         Not returning data
102         """
103         pass
104
105     def callback_next_song(self):
106         """
107         Could be use to scrobble, maintain an history…
108         Not returning data,
109         """
110         pass
111
112     def callback_need_track(self):
113         """
114         Returns a list of Track objects to add
115         """
116         pass
117
118     def callback_need_track_fb(self):
119         """
120         Called when callback_need_track failled to find tracks to queue
121         Returns a list of Track objects to add
122         """
123         pass
124
125     def shutdown(self):
126         """Called on application shutdown"""
127         pass
128
129
130 class AdvancedLookUp:
131     """Object to derive from for plugins
132     Exposes advanced music library look up with use of play history
133     """
134
135     def __init__(self, daemon):
136         self.log = daemon.log
137         self.daemon = daemon
138         self.player = daemon.player
139
140     # Query History
141     def get_history(self, artist=False):
142         """Constructs list of already played artists.
143         """
144         duration = self.daemon.config.getint('sima', 'history_duration')
145         name = None
146         if artist:
147             name = artist.name
148         from_db = self.daemon.sdb.get_history(duration=duration, artist=name)
149         hist = [Track(artist=tr[0], album=tr[1], title=tr[2],
150                       file=tr[3]) for tr in from_db]
151         return hist
152
153     def get_album_history(self, artist):
154         """Retrieve album history"""
155         hist = []
156         tracks_from_db = self.get_history(artist=artist)
157         for trk in tracks_from_db:
158             if trk.album and trk.album in hist:
159                 continue
160             hist.append(Album(name=trk.album, artist=Artist(trk.artist)))
161         return hist
162
163     def get_reorg_artists_list(self, alist):
164         """
165         Move around items in artists_list in order to play first not recently
166         played artists
167
168         :param list(str) alist:
169         """
170         hist = list()
171         duration = self.daemon.config.getint('sima', 'history_duration')
172         for art in self.daemon.sdb.get_artists_history(alist, duration=duration):
173             if art not in hist:
174                 hist.insert(0, art)
175         reorg = [art for art in alist if art not in hist]
176         reorg.extend(hist)
177         return reorg
178     # /Query History
179
180     # Find not recently played/unplayed
181     def album_candidate(self, artist, unplayed=True):
182         """
183         :param Artist artist: Artist to fetch an album for
184         :param bool unplayed: Fetch only unplayed album
185         """
186         self.log.info('Searching an album for "%s"...' % artist)
187         albums = self.player.search_albums(artist)
188         if not albums:
189             return []
190         self.log.debug('Albums candidate: %s', albums)
191         albums_hist = self.get_album_history(artist)
192         self.log.debug('Albums history: %s', albums_hist)
193         albums_not_in_hist = [a for a in albums if a.name not in albums_hist]
194         # Get to next artist if there are no unplayed albums
195         if not albums_not_in_hist:
196             self.log.info('No unplayed album found for "%s"' % artist)
197             if unplayed:
198                 return []
199         random.shuffle(albums_not_in_hist)
200         albums_not_in_hist.extend(albums_hist)
201         album_to_queue = []
202         for album in albums_not_in_hist:
203             # Controls the album found is not already queued
204             if album in {t.album for t in self.player.queue}:
205                 self.log.debug('"%s" already queued, skipping!', album)
206                 return []
207             # In random play mode use complete playlist to filter
208             if self.player.playmode.get('random'):
209                 if album in {t.album for t in self.player.playlist}:
210                     self.log.debug('"%s" already in playlist, skipping!',
211                                    album)
212                     return []
213             album_to_queue = album
214         if not album_to_queue:
215             self.log.info('No album found for "%s"', artist)
216             return []
217         self.log.info('%s album candidate: %s - %s', self.__class__.__name__,
218                       artist, album_to_queue)
219         return album_to_queue
220
221     def filter_track(self, tracks, unplayed=False):
222         """
223         Extract one unplayed track from a Track object list.
224             * not in history
225             * not already in the queue
226         """
227         artist = tracks[0].Artist
228         # In random play mode use complete playlist to filter
229         if self.player.playmode.get('random'):
230             deny_list = self.player.playlist
231         else:
232             deny_list = self.player.queue
233         not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
234         if not not_in_hist:
235             self.log.debug('All tracks already played for "%s"', artist)
236             if unplayed:
237                 return None
238         random.shuffle(not_in_hist)
239         candidates = [_ for _ in not_in_hist if _ not in deny_list]
240         # for trk in [_ for _ in not_in_hist if _ not in deny_list]:
241         #     # Should use albumartist heuristic as well
242         #     if self.plugin_conf.getboolean('single_album'):  # pylint: disable=no-member
243         #         if (trk.album == self.player.current.album or
244         #                 trk.album in [tr.album for tr in black_list]):
245         #             self.log.debug('Found unplayed track ' +
246         #                            'but from an album already queued: %s', trk)
247         #             continue
248         #     candidates.append(trk)
249         if not candidates:
250             return None
251         return random.choice(candidates)
252
253 # VIM MODLINE
254 # vim: ai ts=4 sw=4 sts=4 expandtab