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.player = daemon.player
54 self.plugin_conf = None
55 self.main_conf = daemon.config
60 return self.__class__.__name__
62 def __get_config(self):
63 """Get plugin's specific configuration from global applications's config
66 for sec in conf.sections(): # Discovering plugin conf
67 if sec == self.__class__.__name__.lower():
68 self.plugin_conf = conf[sec]
69 if 'priority' not in self.plugin_conf:
70 self.plugin_conf['priority'] = '80'
71 if not self.plugin_conf:
72 self.plugin_conf = {'priority': '80'}
74 # self.log.debug('Got config for %s: %s', self, self.plugin_conf)
78 return self.plugin_conf.get('priority')
82 Called when the daemon().run() is called and
83 right after the player has connected successfully.
87 def callback_player(self):
89 Called on player changes, stopped, paused, skipped
93 def callback_player_database(self):
95 Called on player music library changes
99 def callback_playlist(self):
101 Called on playlist changes
106 def callback_next_song(self):
108 Could be use to scrobble, maintain an history…
113 def callback_need_track(self):
115 Returns a list of Track objects to add
119 def callback_need_track_fb(self):
121 Called when callback_need_track failled to find tracks to queue
122 Returns a list of Track objects to add
127 """Called on application shutdown"""
131 class AdvancedPlugin(Plugin):
132 """Object to derive from for plugins
133 Exposes advanced music library look up with use of play history
137 def get_history(self, artist=False):
138 """Constructs Track list of already played artists.
140 :param Artist artist: Artist object to look history for
142 duration = self.main_conf.getint('sima', 'history_duration')
146 from_db = self.sdb.get_history(duration=duration, artist=name)
147 hist = [Track(artist=tr[0], album=tr[1], title=tr[2],
148 file=tr[3]) for tr in from_db]
151 def get_album_history(self, artist):
152 """Retrieve album history"""
154 tracks_from_db = self.get_history(artist=artist)
155 for trk in tracks_from_db:
156 if trk.album and trk.album in hist:
158 hist.append(Album(name=trk.album, artist=Artist(trk.artist)))
161 def get_reorg_artists_list(self, alist):
163 Move around items in artists_list in order to play first not recently
166 :param list(str) alist:
169 duration = self.main_conf.getint('sima', 'history_duration')
170 for art in self.sdb.get_artists_history(alist, duration=duration):
173 reorg = [art for art in alist if art not in hist]
178 # Find not recently played/unplayed
179 def album_candidate(self, artist, unplayed=True):
180 """Search an album for artist
182 :param Artist artist: Artist to fetch an album for
183 :param bool unplayed: Fetch only unplayed album
185 self.log.info('Searching an album for "%s"...' % artist)
186 albums = self.player.search_albums(artist)
189 self.log.debug('Albums candidate: %s', albums)
190 albums_hist = self.get_album_history(artist)
191 self.log.debug('Albums history: %s', [a.name for a in albums_hist])
192 albums_not_in_hist = [a for a in albums if a.name not in albums_hist]
193 # Get to next artist if there are no unplayed albums
194 if not albums_not_in_hist:
195 self.log.info('No unplayed album found for "%s"' % artist)
198 random.shuffle(albums_not_in_hist)
199 albums_not_in_hist.extend(albums_hist)
201 for album in albums_not_in_hist:
202 # Controls the album found is not already queued
203 if album in {t.album for t in self.player.queue}:
204 self.log.debug('"%s" already queued, skipping!', album)
206 # In random play mode use complete playlist to filter
207 if self.player.playmode.get('random'):
208 if album in {t.album for t in self.player.playlist}:
209 self.log.debug('"%s" already in playlist, skipping!',
212 album_to_queue = album
213 if not album_to_queue:
214 self.log.info('No album found for "%s"', artist)
216 self.log.info('%s album candidate: %s - %s', self.__class__.__name__,
217 artist, album_to_queue)
218 return album_to_queue
220 def filter_track(self, tracks, unplayed=False):
222 Extract one unplayed track from a Track object list.
224 * not already in the queue
226 artist = tracks[0].Artist
227 # In random play mode use complete playlist to filter
228 if self.player.playmode.get('random'):
229 deny_list = self.player.playlist
231 deny_list = self.player.queue
232 not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
234 self.log.debug('All tracks already played for "%s"', artist)
237 random.shuffle(not_in_hist)
239 for trk in [_ for _ in not_in_hist if _ not in deny_list]:
240 # Should use albumartist heuristic as well
241 if self.plugin_conf.getboolean('single_album', False): # pylint: disable=no-member
242 if (trk.album == self.player.current.album or
243 trk.album in [tr.album for tr in deny_list]):
244 self.log.debug('Found unplayed track ' +
245 'but from an album already queued: %s', trk)
247 candidates.append(trk)
250 return random.choice(candidates)
253 # vim: ai ts=4 sw=4 sts=4 expandtab