]> kaliko git repositories - mpd-sima.git/commitdiff
Add album queue mode to Tags plugin
authorkaliko <kaliko@azylum.org>
Thu, 17 Dec 2020 14:45:10 +0000 (15:45 +0100)
committerkaliko <kaliko@azylum.org>
Thu, 17 Dec 2020 14:45:10 +0000 (15:45 +0100)
data/man/mpd_sima.cfg.5
data/man/mpd_sima.cfg.5.html
data/man/mpd_sima.cfg.5.xml
doc/Changelog
sima/lib/plugin.py
sima/plugins/internal/tags.py
sima/utils/config.py

index d8482193d6b66d916931584324e3e219ff661783..6b98618ac847d8223d27d4be37e3561a9f717474 100644 (file)
@@ -2,12 +2,12 @@
 .\"     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
 .\" -----------------------------------------------------------------
@@ -441,6 +441,16 @@ you can achieve the same with the following setting: "\fBgenre=rock\fR" and "\fB
 .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\&.
@@ -471,6 +481,13 @@ Plugin priority
 .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
index ae847071fd05758bc31bc6deb6c845ca97afbae7..019f99c782978cf36196260949289ee636a2bdd1 100644 (file)
@@ -134,9 +134,12 @@ consume=30
               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 &#8220;QUEUE MODES&#8221;</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
index c790928a80dc8209ae9024cf8eefbc9eaef07171..83ce38622030cba0e4f33de9149d1d015e68aa28 100644 (file)
@@ -449,6 +449,14 @@ man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/
                 <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>
@@ -481,6 +489,13 @@ man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/
                         <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">
index d08b7b2c80ec53c44c025e0d548cb0f7a6397da0..ef89d1400a6330ba00c7f9026e6ae190ba646562 100644 (file)
@@ -1,5 +1,6 @@
 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)
 
index 9ce40fe24b87804c40850b516eec3fe2414360fd..3839f8754505591550f632cee6a31ab27db1b59d 100644 (file)
 Plugin object to derive from
 """
 
+import random
+
+from .track import Track
+from .meta import Album, Artist
+
 
 class Plugin:
     """
@@ -122,5 +127,128 @@ 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
index 9a931fcaecf40deebbe454fcdfee7a7612a4ed54..1536c5ce2c87a91ba464861aa9a9d4b3ebef094a 100644 (file)
@@ -28,8 +28,8 @@ import random
 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
 
 
@@ -53,10 +53,11 @@ def forge_filter(cfg):
     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)
@@ -69,7 +70,7 @@ class Tags(Plugin):
     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! '
@@ -93,14 +94,6 @@ class Tags(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',
@@ -118,12 +111,13 @@ class Tags(Plugin):
             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 \
@@ -137,9 +131,45 @@ class Tags(Plugin):
             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
index 182b036593a58ada977596653ef7569c20a0fd80..06799561ba6f72957d228db2cdc29368c5a8cd0a 100644 (file)
@@ -94,7 +94,9 @@ DEFAULT_CONF = {
             'label': "",
             'originaldate': "",
             'filter': "",
+            'queue_mode': "track",
             'track_to_add': 1,
+            'album_to_add': 1,
             'priority': 80,
             }
         }