From: kaliko
Date: Thu, 17 Dec 2020 14:45:10 +0000 (+0100)
Subject: Add album queue mode to Tags plugin
X-Git-Tag: 0.16.1~15
X-Git-Url: https://git.kaliko.me/?a=commitdiff_plain;h=35012360f9d528bca294f6bbe2ca3ad843d09630;p=mpd-sima.git
Add album queue mode to Tags plugin
---
diff --git a/data/man/mpd_sima.cfg.5 b/data/man/mpd_sima.cfg.5
index d848219..6b98618 100644
--- a/data/man/mpd_sima.cfg.5
+++ b/data/man/mpd_sima.cfg.5
@@ -2,12 +2,12 @@
.\" Title: mpd_sima.cfg
.\" Author: kaliko
.\" Generator: DocBook XSL Stylesheets v1.79.1
-.\" 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
diff --git a/data/man/mpd_sima.cfg.5.html b/data/man/mpd_sima.cfg.5.html
index ae84707..019f99c 100644
--- a/data/man/mpd_sima.cfg.5.html
+++ b/data/man/mpd_sima.cfg.5.html
@@ -134,9 +134,12 @@ consume=30
the same with the following setting: "genre=rock" and
"filter=(date =~ '198[2-9]+')" (provided your MPD server
was compiled with libpcre).
-
[tags]
filter=
You can use here any valid MPD filter as defined in MPD protocol documentation.
You can use here any valid MPD filter as defined in MPD protocol documentation.
comment=
date=
genre=
label=
priority=80
Plugin priority
-
track_to_add=1
How many track(s) to add.
QUEUE MODES
Different queue modes are available with some plugins (check for
+
track_to_add=1
How many track(s) to add.
album_to_add=1
How many album(s) to add. Only relevant in
+ album queue mode.
QUEUE MODES
Different queue modes are available with some plugins (check for
queue_mode presence in plugin config).
mpd-sima tries preferably to chose among unplayed artists or
at least not recently played artist.
track
Queue a similar track chosen at random from a similar artist.
top
Queue a track from a similar artist, chosen among
diff --git a/data/man/mpd_sima.cfg.5.xml b/data/man/mpd_sima.cfg.5.xml
index c790928..83ce386 100644
--- a/data/man/mpd_sima.cfg.5.xml
+++ b/data/man/mpd_sima.cfg.5.xml
@@ -449,6 +449,14 @@ man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/
+
+ track
+
+ Queue mode to use among
+ track,
+ album (see for info about queue modes).
+
+
@@ -481,6 +489,13 @@ man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/
How many track(s) to add.
+
+ 1
+
+ How many album(s) to add. Only relevant in
+ queue mode.
+
+
diff --git a/doc/Changelog b/doc/Changelog
index d08b7b2..ef89d14 100644
--- a/doc/Changelog
+++ b/doc/Changelog
@@ -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)
diff --git a/sima/lib/plugin.py b/sima/lib/plugin.py
index 9ce40fe..3839f87 100644
--- a/sima/lib/plugin.py
+++ b/sima/lib/plugin.py
@@ -21,6 +21,11 @@
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
diff --git a/sima/plugins/internal/tags.py b/sima/plugins/internal/tags.py
index 9a931fc..1536c5c 100644
--- a/sima/plugins/internal/tags.py
+++ b/sima/plugins/internal/tags.py
@@ -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
diff --git a/sima/utils/config.py b/sima/utils/config.py
index 182b036..0679956 100644
--- a/sima/utils/config.py
+++ b/sima/utils/config.py
@@ -94,7 +94,9 @@ DEFAULT_CONF = {
'label': "",
'originaldate': "",
'filter': "",
+ 'queue_mode': "track",
'track_to_add': 1,
+ 'album_to_add': 1,
'priority': 80,
}
}