1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2013-2015, 2020-2021 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, MetaContainer
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.
86 def callback_player(self):
88 Called on player changes, stopped, paused, skipped
91 def callback_player_database(self):
93 Called on player music library changes
96 def callback_playlist(self):
98 Called on playlist changes
102 def callback_next_song(self):
104 Could be use to scrobble, maintain an history…
108 def callback_need_track(self):
110 Returns a list of Track objects to add
113 def callback_need_track_fb(self):
115 Called when callback_need_track failled to find tracks to queue
116 Returns a list of Track objects to add
120 """Called on application shutdown"""
123 class AdvancedPlugin(Plugin):
124 """Object to derive from for plugins
125 Exposes advanced music library look up with use of play history
129 def get_history(self, artist=False):
130 """Constructs Track list of already played artists.
132 :param Artist artist: Artist object to look history for
134 duration = self.main_conf.getint('sima', 'history_duration')
138 from_db = self.sdb.get_history(duration=duration, artist=name)
139 hist = [Track(artist=tr[0], album=tr[1], title=tr[2],
140 file=tr[3]) for tr in from_db]
143 def get_album_history(self, artist):
144 """Retrieve album history"""
146 tracks_from_db = self.get_history(artist=artist)
147 for trk in tracks_from_db:
148 if trk.album and trk.album in hist:
150 hist.append(Album(name=trk.album, artist=Artist(trk.artist)))
153 def get_reorg_artists_list(self, alist):
155 Move around items in alist in order to have first not recently
156 played (or about to be played) artists.
158 :param {Artist} alist: Artist objects list/container
160 queued_artist = MetaContainer([Artist(_.artist) for _ in
161 self.player.queue if _.artist])
162 not_queued_artist = alist - queued_artist
163 duration = self.main_conf.getint('sima', 'history_duration')
165 for art in self.sdb.get_artists_history(alist, duration=duration):
167 if art not in queued_artist:
171 # Find not recently played (not in history) & not in queue
172 reorg = [art for art in not_queued_artist if art not in hist]
177 # Find not recently played/unplayed
178 def album_candidate(self, artist, unplayed=True):
179 """Search an album for artist
181 :param Artist artist: Artist to fetch an album for
182 :param bool unplayed: Fetch only unplayed album
184 self.log.info('Searching an album for "%s"...' % artist)
185 albums = self.player.search_albums(artist)
188 self.log.debug('Albums candidates: %s', albums)
189 albums_hist = self.get_album_history(artist)
190 self.log.trace('Albums history: %s', [a.name for a in albums_hist])
191 albums_not_in_hist = [a for a in albums if a.name not in albums_hist]
192 # Get to next artist if there are no unplayed albums
193 if not albums_not_in_hist:
194 self.log.info('No unplayed album found for "%s"' % artist)
197 random.shuffle(albums_not_in_hist)
198 albums_not_in_hist.extend(albums_hist)
199 self.log.debug('Albums candidate: %s', albums_not_in_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
214 if not album_to_queue:
215 self.log.info('No album found for "%s"', artist)
217 self.log.info('%s plugin chose album: %s - %s',
218 self.__class__.__name__, 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)
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', False): # pylint: disable=no-member
243 if (trk.album == self.player.current.album or
244 trk.album in [tr.album for tr in deny_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