1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2013-2015, 2020 kaliko <kaliko@azylum.org>
4 # This file is part of sima
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.
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.
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/>.
21 Plugin object to derive from
26 from .track import Track
27 from .meta import Album, Artist
32 First non-empty line of the docstring is used as description
33 Rest of the docstring at your convenience.
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.
43 """self documenting class method
45 doc = 'Undocumented plugin! Fill "{}" docstring'.format(cls.__name__)
47 doc = cls.__doc__.strip(' \n').splitlines()[0]
48 return {'name': cls.__name__,
51 def __init__(self, daemon):
53 self.__daemon = daemon
54 self.player = daemon.player
55 self.plugin_conf = None
59 return self.__class__.__name__
61 def __get_config(self):
62 """Get plugin's specific configuration from global applications's config
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'}
73 # self.log.debug('Got config for %s: %s', self, self.plugin_conf)
77 return self.plugin_conf.get('priority')
81 Called when the daemon().run() is called and
82 right after the player has connected successfully.
86 def callback_player(self):
88 Called on player changes, stopped, paused, skipped
92 def callback_player_database(self):
94 Called on player music library changes
98 def callback_playlist(self):
100 Called on playlist changes
105 def callback_next_song(self):
107 Could be use to scrobble, maintain an history…
112 def callback_need_track(self):
114 Returns a list of Track objects to add
118 def callback_need_track_fb(self):
120 Called when callback_need_track failled to find tracks to queue
121 Returns a list of Track objects to add
126 """Called on application shutdown"""
130 class AdvancedLookUp:
131 """Object to derive from for plugins
132 Exposes advanced music library look up with use of play history
135 def __init__(self, daemon):
136 self.log = daemon.log
138 self.player = daemon.player
141 def get_history(self, artist=False):
142 """Constructs list of already played artists.
144 duration = self.daemon.config.getint('sima', 'history_duration')
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]
153 def get_album_history(self, artist):
154 """Retrieve album history"""
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:
160 hist.append(Album(name=trk.album, artist=Artist(trk.artist)))
163 def get_reorg_artists_list(self, alist):
165 Move around items in artists_list in order to play first not recently
168 :param list(str) alist:
171 duration = self.daemon.config.getint('sima', 'history_duration')
172 for art in self.daemon.sdb.get_artists_history(alist, duration=duration):
175 reorg = [art for art in alist if art not in hist]
180 # Find not recently played/unplayed
181 def album_candidate(self, artist, unplayed=True):
183 :param Artist artist: Artist to fetch an album for
184 :param bool unplayed: Fetch only unplayed album
186 self.log.info('Searching an album for "%s"...' % artist)
187 albums = self.player.search_albums(artist)
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)
199 random.shuffle(albums_not_in_hist)
200 albums_not_in_hist.extend(albums_hist)
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)
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!',
213 album_to_queue = album
214 if not album_to_queue:
215 self.log.info('No album found for "%s"', artist)
217 self.log.info('%s album candidate: %s - %s', self.__class__.__name__,
218 artist, album_to_queue)
219 return album_to_queue
221 def filter_track(self, tracks, unplayed=False):
223 Extract one unplayed track from a Track object list.
225 * not already in the queue
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
232 deny_list = self.player.queue
233 not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
235 self.log.debug('All tracks already played for "%s"', artist)
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)
248 # candidates.append(trk)
251 return random.choice(candidates)
254 # vim: ai ts=4 sw=4 sts=4 expandtab