.\" Title: mpd_sima.cfg
.\" Author: kaliko <kaliko@azylum.org>
.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
-.\" Date: 12/16/2020
+.\" Date: 12/17/2020
.\" Manual: mpd-sima 0.16.1 User Manual
.\" Source: mpd-sima
.\" Language: English
.\"
-.TH "MPD_SIMA\&.CFG" "5" "12/16/2020" "mpd-sima" "mpd-sima 0.16.1 User Manual"
+.TH "MPD_SIMA\&.CFG" "5" "12/17/2020" "mpd-sima" "mpd-sima 0.16.1 User Manual"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.RS 4
.RE
.PP
+\fBqueue_mode=\fR\fItrack\fR
+.RS 4
+Queue mode to use among
+\fItrack\fR,
+\fIalbum\fR
+(see
+the section called \(lqQUEUE MODES\(rq
+for info about queue modes)\&.
+.RE
+.PP
\fBfilter=\fR
.RS 4
You can use here any valid MPD filter as defined in MPD protocol documentation\&.
.RS 4
How many track(s) to add\&.
.RE
+.PP
+\fBalbum_to_add=\fR\fI1\fR
+.RS 4
+How many album(s) to add\&. Only relevant in
+\fBalbum\fR
+queue mode\&.
+.RE
.SH "QUEUE MODES"
.PP
Different queue modes are available with some plugins (check for
the same with the following setting: "<code class="option">genre=rock</code>" and
"<code class="option">filter=(date =~ '198[2-9]+')</code>" (provided your MPD server
was compiled with libpcre).
- </p><dt><span class="term"><code class="option">[tags]</code></span></dt><dd></dd><dt><span class="term"><code class="option">filter=</code></span></dt><dd><p>You can use here any valid MPD filter as defined in MPD protocol documentation.</p></dd><dt><span class="term"><code class="option">comment=</code></span></dt><dd></dd><dt><span class="term"><code class="option">date=</code></span></dt><dd></dd><dt><span class="term"><code class="option">genre=</code></span></dt><dd></dd><dt><span class="term"><code class="option">label=</code></span></dt><dd></dd><dt><span class="term"><code class="option">priority=</code><em class="replaceable"><code>80</code></em></span></dt><dd><p>
+ </p><dt><span class="term"><code class="option">[tags]</code></span></dt><dd></dd><dt><span class="term"><code class="option">queue_mode=</code><em class="replaceable"><code>track</code></em></span></dt><dd><p>Queue mode to use among
+ <em class="replaceable"><code>track</code></em>,
+ <em class="replaceable"><code>album</code></em> (see <a class="xref" href="#queue_mode" title="QUEUE MODES">the section called “QUEUE MODES”</a> for info about queue modes).</p></dd><dt><span class="term"><code class="option">filter=</code></span></dt><dd><p>You can use here any valid MPD filter as defined in MPD protocol documentation.</p></dd><dt><span class="term"><code class="option">comment=</code></span></dt><dd></dd><dt><span class="term"><code class="option">date=</code></span></dt><dd></dd><dt><span class="term"><code class="option">genre=</code></span></dt><dd></dd><dt><span class="term"><code class="option">label=</code></span></dt><dd></dd><dt><span class="term"><code class="option">priority=</code><em class="replaceable"><code>80</code></em></span></dt><dd><p>
Plugin priority
- </p></dd><dt><span class="term"><code class="option">track_to_add=</code><em class="replaceable"><code>1</code></em></span></dt><dd><p>How many track(s) to add.</p></dd></div></div><div class="refsect1"><a name="queue_mode"></a><h2>QUEUE MODES</h2><p>Different queue modes are available with some plugins (check for
+ </p></dd><dt><span class="term"><code class="option">track_to_add=</code><em class="replaceable"><code>1</code></em></span></dt><dd><p>How many track(s) to add.</p></dd><dt><span class="term"><code class="option">album_to_add=</code><em class="replaceable"><code>1</code></em></span></dt><dd><p>How many album(s) to add. Only relevant in
+ <code class="option">album</code> queue mode.</p></dd></div></div><div class="refsect1"><a name="queue_mode"></a><h2>QUEUE MODES</h2><p>Different queue modes are available with some plugins (check for
<code class="option">queue_mode</code> presence in plugin config).
</p><p>mpd-sima tries preferably to chose among unplayed artists or
at least not recently played artist.</p><div class="variablelist"><dl class="variablelist"><dt><span class="term"><code class="option">track</code></span></dt><dd><p>Queue a similar track chosen at random from a similar artist.</p></dd><dt><span class="term"><code class="option">top</code></span></dt><dd><p>Queue a track from a similar artist, chosen among
<varlistentry> <!-- tags -->
<term><option>[tags]</option></term>
</varlistentry>
+ <varlistentry> <!-- tags.queue_mode -->
+ <term><option>queue_mode=</option><replaceable>track</replaceable></term>
+ <listitem>
+ <para>Queue mode to use among
+ <replaceable>track</replaceable>,
+ <replaceable>album</replaceable> (see <xref linkend="queue_mode"/> for info about queue modes).</para>
+ </listitem>
+ </varlistentry>
<varlistentry> <!-- tags.filter -->
<term><option>filter=</option></term>
<listitem>
<para>How many track(s) to add.</para>
</listitem>
</varlistentry>
+ <varlistentry> <!-- tags.album_to_add -->
+ <term><option>album_to_add=</option><replaceable>1</replaceable></term>
+ <listitem>
+ <para>How many album(s) to add. Only relevant in
+ <option>album</option> queue mode.</para>
+ </listitem>
+ </varlistentry>
</refsect2>
</refsect1>
<refsect1 id="queue_mode">
MPD_sima v0.16.1
+ * tags plugin: Add album queue mode
* tags plugin: Ensure metadata used in filter are enabled
so that MPD exposes them (closes #38)
Plugin object to derive from
"""
+import random
+
+from .track import Track
+from .meta import Album, Artist
+
class Plugin:
"""
pass
+class AdvancedLookUp:
+ """Object to derive from for plugins
+ Exposes advanced music library look up with use of play history
+ """
+
+ def __init__(self, daemon):
+ self.log = daemon.log
+ self.daemon = daemon
+ self.player = daemon.player
+
+ # Query History
+ def get_history(self, artist=False):
+ """Constructs list of already played artists.
+ """
+ duration = self.daemon.config.getint('sima', 'history_duration')
+ name = None
+ if artist:
+ name = artist.name
+ from_db = self.daemon.sdb.get_history(duration=duration, artist=name)
+ hist = [Track(artist=tr[0], album=tr[1], title=tr[2],
+ file=tr[3]) for tr in from_db]
+ return hist
+
+ def get_album_history(self, artist):
+ """Retrieve album history"""
+ hist = []
+ tracks_from_db = self.get_history(artist=artist)
+ for trk in tracks_from_db:
+ if trk.album and trk.album in hist:
+ continue
+ hist.append(Album(name=trk.album, artist=Artist(trk.artist)))
+ return hist
+
+ def get_reorg_artists_list(self, alist):
+ """
+ Move around items in artists_list in order to play first not recently
+ played artists
+
+ :param list(str) alist:
+ """
+ hist = list()
+ duration = self.daemon.config.getint('sima', 'history_duration')
+ for art in self.daemon.sdb.get_artists_history(alist, duration=duration):
+ if art not in hist:
+ hist.insert(0, art)
+ reorg = [art for art in alist if art not in hist]
+ reorg.extend(hist)
+ return reorg
+ # /Query History
+
+ # Find not recently played/unplayed
+ def album_candidate(self, artist, unplayed=True):
+ """
+ :param Artist artist: Artist to fetch an album for
+ :param bool unplayed: Fetch only unplayed album
+ """
+ self.log.info('Searching an album for "%s"...' % artist)
+ albums = self.player.search_albums(artist)
+ if not albums:
+ return []
+ self.log.debug('Albums candidate: %s', albums)
+ albums_hist = self.get_album_history(artist)
+ self.log.debug('Albums history: %s', albums_hist)
+ albums_not_in_hist = [a for a in albums if a.name not in albums_hist]
+ # Get to next artist if there are no unplayed albums
+ if not albums_not_in_hist:
+ self.log.info('No unplayed album found for "%s"' % artist)
+ if unplayed:
+ return []
+ random.shuffle(albums_not_in_hist)
+ albums_not_in_hist.extend(albums_hist)
+ album_to_queue = []
+ for album in albums_not_in_hist:
+ # Controls the album found is not already queued
+ if album in {t.album for t in self.player.queue}:
+ self.log.debug('"%s" already queued, skipping!', album)
+ return []
+ # In random play mode use complete playlist to filter
+ if self.player.playmode.get('random'):
+ if album in {t.album for t in self.player.playlist}:
+ self.log.debug('"%s" already in playlist, skipping!',
+ album)
+ return []
+ album_to_queue = album
+ if not album_to_queue:
+ self.log.info('No album found for "%s"', artist)
+ return []
+ self.log.info('%s album candidate: %s - %s', self.__class__.__name__,
+ artist, album_to_queue)
+ return album_to_queue
+
+ def filter_track(self, tracks, unplayed=False):
+ """
+ Extract one unplayed track from a Track object list.
+ * not in history
+ * not already in the queue
+ """
+ artist = tracks[0].Artist
+ # In random play mode use complete playlist to filter
+ if self.player.playmode.get('random'):
+ deny_list = self.player.playlist
+ else:
+ deny_list = self.player.queue
+ not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
+ if not not_in_hist:
+ self.log.debug('All tracks already played for "%s"', artist)
+ if unplayed:
+ return None
+ random.shuffle(not_in_hist)
+ candidates = [_ for _ in not_in_hist if _ not in deny_list]
+ # for trk in [_ for _ in not_in_hist if _ not in deny_list]:
+ # # Should use albumartist heuristic as well
+ # if self.plugin_conf.getboolean('single_album'): # pylint: disable=no-member
+ # if (trk.album == self.player.current.album or
+ # trk.album in [tr.album for tr in black_list]):
+ # self.log.debug('Found unplayed track ' +
+ # 'but from an album already queued: %s', trk)
+ # continue
+ # candidates.append(trk)
+ if not candidates:
+ return None
+ return random.choice(candidates)
+
# VIM MODLINE
# vim: ai ts=4 sw=4 sts=4 expandtab
from musicpd import CommandError
# local import
-from ...lib.plugin import Plugin
-from ...lib.track import Track
+from ...lib.plugin import Plugin, AdvancedLookUp
+from ...lib.meta import Artist, Album
from ...utils.utils import PluginException
return mpd_filter
-class Tags(Plugin):
+class Tags(Plugin, AdvancedLookUp):
"""Add track based on tags content
"""
supported_tags = {'comment', 'date', 'genre', 'label', 'originaldate'}
+ options = {'queue_mode', 'priority', 'filter', 'track_to_add', 'album_to_add'}
def __init__(self, daemon):
super().__init__(daemon)
def _control_conf(self):
sup_tags = Tags.supported_tags
config_tags = {k for k, v in self.plugin_conf.items()
- if (v and k not in ['filter', 'priority', 'track_to_add'])}
+ if (v and k not in Tags.options)}
if not self.plugin_conf.get('filter', None) and \
config_tags.isdisjoint(sup_tags):
self.log.error('Found no config for %s plugin! '
tags = config_tags & Tags.supported_tags
self.player.needed_tags |= tags
- def _get_history(self):
- """Constructs list of already played artists.
- """
- duration = self.daemon.config.getint('sima', 'history_duration')
- tracks_from_db = self.daemon.sdb.get_history(duration=duration)
- hist = [Track(file=tr[3], artist=tr[0]) for tr in tracks_from_db]
- return hist
-
def start(self):
if (0, 21, 0) > tuple(map(int, self.player.mpd_version.split('.'))):
self.log.warning('MPD protocol version: %s < 0.21.0',
raise PluginException('Badly formated filter in tags plugin configuration: "%s"'
% self.plugin_conf['filter'])
- def callback_need_track(self):
+ def callback_need_track_(self):
candidates = []
- target = self.plugin_conf.getint('track_to_add')
+ queue_mode = self.plugin_conf.get('queue_mode', 'track')
+ target = self.plugin_conf.getint(f'{queue_mode}_to_add')
tracks = self.player.find(self.mpd_filter)
random.shuffle(tracks)
- history = self._get_history()
+ history = self.get_history()
while tracks:
trk = tracks.pop()
if trk in self.player.queue or \
self.log.info('Tags candidate: {}'.format(trk))
if len(candidates) >= target:
break
+ if queue_mode == 'track':
+ return candidates
+ if queue_mode == 'album':
+ for trk in candidates:
+ self.log.info(trk.Artist)
+ _ = self.album_candidate(trk.Artist)
if not candidates:
self.log.info('Tags plugin failed to find some tracks')
return candidates
+ def callback_need_track(self):
+ candidates = []
+ queue_mode = self.plugin_conf.get('queue_mode', 'track')
+ target = self.plugin_conf.getint(f'{queue_mode}_to_add')
+ # look for artists acording to filter
+ artists = self.player.list('artist', self.mpd_filter)
+ random.shuffle(artists)
+ artists = self.get_reorg_artists_list(artists)
+ self.log.debug('Tags candidates: %s', ' / '.join(artists))
+ for artist in artists:
+ if artist in {t.Artist for t in self.player.queue}:
+ continue
+ self.log.debug('looking for %s', artist)
+ trk = self.filter_track(self.player.find_tracks(Artist(name=artist)))
+ if not trk:
+ continue
+ if queue_mode == 'track':
+ self.log.info('Tags candidate: {}'.format(trk))
+ candidates.append(trk)
+ if len(candidates) == target:
+ break
+ else:
+ album = self.album_candidate(trk.Artist, unplayed=True)
+ if not album:
+ continue
+ candidates.extend(self.player.find_tracks(album))
+ if len({t.album for t in candidates}) == target:
+ break
+ return candidates
+
# VIM MODLINE
# vim: ai ts=4 sw=4 sts=4 expandtab
'label': "",
'originaldate': "",
'filter': "",
+ 'queue_mode': "track",
'track_to_add': 1,
+ 'album_to_add': 1,
'priority': 80,
}
}