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 .meta import Artist, MetaContainer
31 First non-empty line of the docstring is used as description
32 Rest of the docstring at your convenience.
34 The lowercased plugin Name MUST be the same as the module (file name),
35 for instance Plugin → plugin.py
36 It eases plugins discovery and simplifies the code to handle them,
37 IMHO, it's a fair trade-off.
42 """self documenting class method
44 doc = f'Undocumented plugin! Fill "{cls.__name__}" docstring'
46 doc = cls.__doc__.strip(' \n').splitlines()[0]
47 return {'name': cls.__name__,
50 def __init__(self, daemon):
52 self.player = daemon.player
53 self.plugin_conf = None
54 self.main_conf = daemon.config
59 return self.__class__.__name__
61 def __get_config(self):
62 """Get plugin's specific configuration from global applications's 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.
85 def callback_player(self):
87 Called on player changes, stopped, paused, skipped
90 def callback_player_database(self):
92 Called on player music library changes
95 def callback_playlist(self):
97 Called on playlist changes
101 def callback_next_song(self):
103 Could be use to scrobble, maintain an history…
107 def callback_need_track(self):
109 Returns a list of Track objects to add
112 def callback_need_track_fb(self):
114 Called when callback_need_track failled to find tracks to queue
115 Returns a list of Track objects to add
119 """Called on application shutdown"""
122 class AdvancedPlugin(Plugin):
123 """Object to derive from for plugins
124 Exposes advanced music library look up with use of play history
128 def get_history(self):
129 """Returns a Track list of already played artists."""
130 duration = self.main_conf.getint('sima', 'history_duration')
131 return self.sdb.fetch_history(duration=duration)
133 def get_album_history(self, artist):
134 """Retrieve album history"""
135 duration = self.main_conf.getint('sima', 'history_duration')
136 return self.sdb.fetch_albums_history(needle=artist, duration=duration)
138 def get_reorg_artists_list(self, alist):
140 Move around items in alist in order to have first not recently
141 played (or about to be played) artists.
143 :param {Artist} alist: Artist objects list/container
145 queued_artist = MetaContainer([Artist(_.artist) for _ in
146 self.player.queue if _.artist])
147 not_queued_artist = alist - queued_artist
148 duration = self.main_conf.getint('sima', 'history_duration')
150 for art in self.sdb.fetch_artists_history(alist, duration=duration):
152 if art not in queued_artist:
156 # Find not recently played (not in history) & not in queue
157 reorg = [art for art in not_queued_artist if art not in hist]
162 # Find not recently played/unplayed
163 def album_candidate(self, artist, unplayed=True):
164 """Search an album for artist
166 :param Artist artist: Artist to fetch an album for
167 :param bool unplayed: Fetch only unplayed album
169 self.log.info('Searching an album for "%s"...', artist)
170 albums = self.player.search_albums(artist)
173 self.log.debug('Albums to choose from: %s', albums)
174 albums_hist = self.get_album_history(artist)
175 self.log.trace('Albums history: %s', [a.name for a in albums_hist])
176 albums_not_in_hist = [a for a in albums if a.name not in albums_hist]
177 # Get to next artist if there are no unplayed albums
178 if not albums_not_in_hist:
179 self.log.info('No unplayed album found for "%s"', artist)
182 random.shuffle(albums_not_in_hist)
183 albums_not_in_hist.extend(albums_hist)
184 self.log.trace('Album candidates: %s', albums_not_in_hist)
186 for album in albums_not_in_hist:
187 # Controls the album found is not already queued
188 if album in {t.Album.name for t in self.player.queue}:
189 self.log.debug('"%s" already queued, skipping!', album)
191 # In random play mode use complete playlist to filter
192 # Yes indeed, some users play in random with album mode :|
193 if self.player.playmode.get('random'):
194 if album in {t.Album.name for t in self.player.playlist}:
195 self.log.debug('"%s" already in playlist, skipping!',
198 album_to_queue = album
200 if not album_to_queue:
201 self.log.info('No album found for "%s"', artist)
203 self.log.info('%s plugin chose album: %s - %s',
204 self.__class__.__name__, artist, album_to_queue)
205 return album_to_queue
207 def filter_track(self, tracks, chosen=None, unplayed=False):
209 Extract one unplayed track from a Track object list.
211 * not already in the queue
213 :param list(Track) tracks: List of tracks to chose from
214 :param list(Track) chosen: List of tracks previously chosen
215 :param bool unplayed: chose only unplayed (honoring history duration setting)
219 artist = tracks[0].Artist
220 # In random play mode use complete playlist to filter
221 if self.player.playmode.get('random'):
222 deny_list = self.player.playlist
224 deny_list = self.player.queue
225 not_in_hist = list(set(tracks) - set(self.sdb.fetch_history(artist=artist)))
227 self.log.debug('All tracks already played for "%s"', artist)
230 random.shuffle(not_in_hist)
232 for trk in [_ for _ in not_in_hist if _ not in deny_list]:
233 # Should use albumartist heuristic as well
234 if self.plugin_conf.getboolean('single_album', False): # pylint: disable=no-member
235 albums = [tr.Album for tr in deny_list]
236 albums += [tr.Album for tr in chosen]
237 if (trk.Album == self.player.current.Album or
238 trk.Album in albums):
239 self.log.debug('Found unplayed track ' +
240 'but from an album already queued: %s', trk)
242 candidates.append(trk)
245 return random.choice(candidates)
248 # vim: ai ts=4 sw=4 sts=4 expandtab