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, 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 self.player.queue])
161 not_queued_artist = alist - queued_artist
162 duration = self.main_conf.getint('sima', 'history_duration')
164 for art in self.sdb.get_artists_history(alist, duration=duration):
166 if art not in queued_artist:
170 # Find not recently played (not in history) & not in queue
171 reorg = [art for art in not_queued_artist if art not in hist]
176 # Find not recently played/unplayed
177 def album_candidate(self, artist, unplayed=True):
178 """Search an album for artist
180 :param Artist artist: Artist to fetch an album for
181 :param bool unplayed: Fetch only unplayed album
183 self.log.info('Searching an album for "%s"...' % artist)
184 albums = self.player.search_albums(artist)
187 self.log.debug('Albums candidates: %s', albums)
188 albums_hist = self.get_album_history(artist)
189 self.log.trace('Albums history: %s', [a.name for a in albums_hist])
190 albums_not_in_hist = [a for a in albums if a.name not in albums_hist]
191 # Get to next artist if there are no unplayed albums
192 if not albums_not_in_hist:
193 self.log.info('No unplayed album found for "%s"' % artist)
196 random.shuffle(albums_not_in_hist)
197 albums_not_in_hist.extend(albums_hist)
198 self.log.debug('Albums candidate: %s', albums_not_in_hist)
200 for album in albums_not_in_hist:
201 # Controls the album found is not already queued
202 if album in {t.album for t in self.player.queue}:
203 self.log.debug('"%s" already queued, skipping!', album)
205 # In random play mode use complete playlist to filter
206 if self.player.playmode.get('random'):
207 if album in {t.album for t in self.player.playlist}:
208 self.log.debug('"%s" already in playlist, skipping!',
211 album_to_queue = album
212 if not album_to_queue:
213 self.log.info('No album found for "%s"', artist)
215 self.log.info('%s album candidate: %s - %s', self.__class__.__name__,
216 artist, album_to_queue)
217 return album_to_queue
219 def filter_track(self, tracks, unplayed=False):
221 Extract one unplayed track from a Track object list.
223 * not already in the queue
225 artist = tracks[0].Artist
226 # In random play mode use complete playlist to filter
227 if self.player.playmode.get('random'):
228 deny_list = self.player.playlist
230 deny_list = self.player.queue
231 not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
233 self.log.debug('All tracks already played for "%s"', artist)
236 random.shuffle(not_in_hist)
238 for trk in [_ for _ in not_in_hist if _ not in deny_list]:
239 # Should use albumartist heuristic as well
240 if self.plugin_conf.getboolean('single_album', False): # pylint: disable=no-member
241 if (trk.album == self.player.current.album or
242 trk.album in [tr.album for tr in deny_list]):
243 self.log.debug('Found unplayed track ' +
244 'but from an album already queued: %s', trk)
246 candidates.append(trk)
249 return random.choice(candidates)
252 # vim: ai ts=4 sw=4 sts=4 expandtab