Skip to content

Commit

Permalink
Add support for multiple tags for multiple values
Browse files Browse the repository at this point in the history
MPD supports multiple tags for some values, such as the artist, the
composer, and the performer. (See
<https://mpd.readthedocs.io/en/latest/protocol.html#tags>.) There
might be multiple artists for a track, in which case mopidy-mpd would
concatenate their names into one value, separated by a semicolon. For
some users and use cases, this can be suboptimal; when tracking the
tracks you listen to on a platform such as Listenbrainz, tracks of
multiple artists do not contribute to the total number of listens of
one of the artists.

This commit changes this behaviour when the `multiple_tags'
configuration variable is set to true, to use multiple tags for values
when applicable instead of concatenating the values.
  • Loading branch information
splintersuidman committed Jun 14, 2021
1 parent 408c7c0 commit ae40231
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 32 deletions.
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ The following configuration values are available:
creating a playlist from the given tracks.
Default: ``m3u``

- ``mpd/multiple_tags``:
Use multiple tags when a track has multiple artists, album artists, composers
or performers, instead of concatenating the names separated by a semicolon
into one tag.
Default: false.


Limitations
===========
Expand Down
1 change: 1 addition & 0 deletions mopidy_mpd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def get_config_schema(self):
schema["zeroconf"] = config.String(optional=True)
schema["command_blacklist"] = config.List(optional=True)
schema["default_playlist_scheme"] = config.String()
schema["multiple_tags"] = config.Boolean(optional=True)
return schema

def setup(self, registry):
Expand Down
4 changes: 4 additions & 0 deletions mopidy_mpd/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ class MpdContext:
#: The subsytems that we want to be notified about in idle mode.
subscriptions = None

#: Whether to use multiple tags for e.g. artists.
multiple_tags = None

_uri_map = None

def __init__(
Expand All @@ -248,6 +251,7 @@ def __init__(
self.session = session
if config is not None:
self.password = config["mpd"]["password"]
self.multiple_tags = config["mpd"]["multiple_tags"]
self.core = core
self.events = set()
self.subscriptions = set()
Expand Down
1 change: 1 addition & 0 deletions mopidy_mpd/ext.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ connection_timeout = 60
zeroconf = Mopidy MPD server on $hostname
command_blacklist = listall,listallinfo
default_playlist_scheme = m3u
multiple_tags = false
23 changes: 17 additions & 6 deletions mopidy_mpd/protocol/current_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ def playlistfind(context, tag, needle):
if not tl_tracks:
return None
position = context.core.tracklist.index(tl_tracks[0]).get()
return translator.track_to_mpd_format(tl_tracks[0], position=position)
return translator.track_to_mpd_format(
tl_tracks[0], position=position, multiple_tags=context.multiple_tags
)
raise exceptions.MpdNotImplemented # TODO


Expand All @@ -207,10 +209,13 @@ def playlistid(context, tlid=None):
if not tl_tracks:
raise exceptions.MpdNoExistError("No such song")
position = context.core.tracklist.index(tl_tracks[0]).get()
return translator.track_to_mpd_format(tl_tracks[0], position=position)
return translator.track_to_mpd_format(
tl_tracks[0], position=position, multiple_tags=context.multiple_tags
)
else:
return translator.tracks_to_mpd_format(
context.core.tracklist.get_tl_tracks().get()
context.core.tracklist.get_tl_tracks().get(),
multiple_tags=context.multiple_tags,
)


Expand Down Expand Up @@ -241,7 +246,9 @@ def playlistinfo(context, parameter=None):
raise exceptions.MpdArgError("Bad song index")
if end and end > len(tl_tracks):
end = None
return translator.tracks_to_mpd_format(tl_tracks, start, end)
return translator.tracks_to_mpd_format(
tl_tracks, start, end, multiple_tags=context.multiple_tags
)


@protocol.commands.add("playlistsearch")
Expand Down Expand Up @@ -281,7 +288,8 @@ def plchanges(context, version):
tracklist_version = context.core.tracklist.get_version().get()
if version < tracklist_version:
return translator.tracks_to_mpd_format(
context.core.tracklist.get_tl_tracks().get()
context.core.tracklist.get_tl_tracks().get(),
multiple_tags=context.multiple_tags,
)
elif version == tracklist_version:
# A version match could indicate this is just a metadata update, so
Expand All @@ -293,7 +301,10 @@ def plchanges(context, version):
tl_track = context.core.playback.get_current_tl_track().get()
position = context.core.tracklist.index(tl_track).get()
return translator.track_to_mpd_format(
tl_track, position=position, stream_title=stream_title
tl_track,
position=position,
stream_title=stream_title,
multiple_tags=context.multiple_tags,
)


Expand Down
20 changes: 16 additions & 4 deletions mopidy_mpd/protocol/music_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ def find(context, *args):
if "album" not in query:
result_tracks += [_album_as_track(a) for a in _get_albums(results)]
result_tracks += _get_tracks(results)
return translator.tracks_to_mpd_format(result_tracks)
return translator.tracks_to_mpd_format(
result_tracks, multiple_tags=context.multiple_tags
)


@protocol.commands.add("findadd")
Expand Down Expand Up @@ -338,7 +340,11 @@ def listallinfo(context, uri=None):
else:
for tracks in lookup_future.get().values():
for track in tracks:
result.extend(translator.track_to_mpd_format(track))
result.extend(
translator.track_to_mpd_format(
track, multiple_tags=context.multiple_tags
)
)
return result


Expand Down Expand Up @@ -390,7 +396,11 @@ def lsinfo(context, uri=None):
else:
for tracks in lookup_future.get().values():
if tracks:
result.extend(translator.track_to_mpd_format(tracks[0]))
result.extend(
translator.track_to_mpd_format(
tracks[0], multiple_tags=context.multiple_tags
)
)

if uri in (None, "", "/"):
result.extend(protocol.stored_playlists.listplaylists(context))
Expand Down Expand Up @@ -444,7 +454,9 @@ def search(context, *args):
artists = [_artist_as_track(a) for a in _get_artists(results)]
albums = [_album_as_track(a) for a in _get_albums(results)]
tracks = _get_tracks(results)
return translator.tracks_to_mpd_format(artists + albums + tracks)
return translator.tracks_to_mpd_format(
artists + albums + tracks, multiple_tags=context.multiple_tags
)


@protocol.commands.add("searchadd")
Expand Down
5 changes: 4 additions & 1 deletion mopidy_mpd/protocol/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ def currentsong(context):
if tl_track is not None:
position = context.core.tracklist.index(tl_track).get()
return translator.track_to_mpd_format(
tl_track, position=position, stream_title=stream_title
tl_track,
position=position,
stream_title=stream_title,
multiple_tags=context.multiple_tags,
)


Expand Down
4 changes: 3 additions & 1 deletion mopidy_mpd/protocol/stored_playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ def listplaylistinfo(context, name):
for uri in track_uris:
tracks.extend(tracks_map[uri])
playlist = playlist.replace(tracks=tracks)
return translator.playlist_to_mpd_format(playlist)
return translator.playlist_to_mpd_format(
playlist, multiple_tags=context.multiple_tags
)


@protocol.commands.add("listplaylists")
Expand Down
73 changes: 60 additions & 13 deletions mopidy_mpd/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ def normalize_path(path, relative=False):
return "/".join(parts)


def track_to_mpd_format(track, position=None, stream_title=None):
def track_to_mpd_format(
track, position=None, stream_title=None, multiple_tags=False
):
"""
Format track for output to MPD client.
Expand All @@ -28,6 +30,8 @@ def track_to_mpd_format(track, position=None, stream_title=None):
:type position: integer
:param stream_title: the current streams title
:type position: string
:param multiple_tags: whether to use multiple tags for e.g. artists
:type multiple_tags: boolean
:rtype: list of two-tuples
"""
if isinstance(track, TlTrack):
Expand All @@ -42,10 +46,14 @@ def track_to_mpd_format(track, position=None, stream_title=None):
result = [
("file", track.uri),
("Time", track.length and (track.length // 1000) or 0),
("Artist", concat_multi_values(track.artists, "name")),
("Album", track.album and track.album.name or ""),
]

if multiple_tags:
result += multi_tag_list(track.artists, "name", "Artist")
else:
result.append(("Artist", concat_multi_values(track.artists, "name")))

if stream_title is not None:
result.append(("Title", stream_title))
if track.name:
Expand All @@ -69,9 +77,16 @@ def track_to_mpd_format(track, position=None, stream_title=None):
result.append(("MUSICBRAINZ_ALBUMID", track.album.musicbrainz_id))

if track.album is not None and track.album.artists:
result.append(
("AlbumArtist", concat_multi_values(track.album.artists, "name"))
)
if multiple_tags:
result += multi_tag_list(track.album.artists, "name", "AlbumArtist")
else:
result.append(
(
"AlbumArtist",
concat_multi_values(track.album.artists, "name"),
)
)

musicbrainz_ids = concat_multi_values(
track.album.artists, "musicbrainz_id"
)
Expand All @@ -84,14 +99,20 @@ def track_to_mpd_format(track, position=None, stream_title=None):
result.append(("MUSICBRAINZ_ARTISTID", musicbrainz_ids))

if track.composers:
result.append(
("Composer", concat_multi_values(track.composers, "name"))
)
if multiple_tags:
result += multi_tag_list(track.composers, "name", "Composer")
else:
result.append(
("Composer", concat_multi_values(track.composers, "name"))
)

if track.performers:
result.append(
("Performer", concat_multi_values(track.performers, "name"))
)
if multiple_tags:
result += multi_tag_list(track.performers, "name", "Performer")
else:
result.append(
("Performer", concat_multi_values(track.performers, "name"))
)

if track.genre:
result.append(("Genre", track.genre))
Expand Down Expand Up @@ -151,7 +172,29 @@ def concat_multi_values(models, attribute):
)


def tracks_to_mpd_format(tracks, start=0, end=None):
def multi_tag_list(models, attribute, tag):
"""
Format Mopidy model values for output to MPD client in a list with one tag
per value.
:param models: the models
:type models: array of :class:`mopidy.models.Artist`,
:class:`mopidy.models.Album` or :class:`mopidy.models.Track`
:param attribute: the attribute to use
:type attribute: string
:param tag: the name of the tag
:type tag: string
:rtype: list of tuples of string and attribute value
"""

return [
(tag, getattr(m, attribute))
for m in models
if getattr(m, attribute, None) is not None
]


def tracks_to_mpd_format(tracks, start=0, end=None, multiple_tags=False):
"""
Format list of tracks for output to MPD client.
Expand All @@ -164,6 +207,8 @@ def tracks_to_mpd_format(tracks, start=0, end=None):
:type start: int (positive or negative)
:param end: position after last track to include in output
:type end: int (positive or negative) or :class:`None` for end of list
:param multiple_tags: whether to use multiple tags for e.g. artists
:type multiple_tags: boolean
:rtype: list of lists of two-tuples
"""
if end is None:
Expand All @@ -173,7 +218,9 @@ def tracks_to_mpd_format(tracks, start=0, end=None):
assert len(tracks) == len(positions)
result = []
for track, position in zip(tracks, positions):
formatted_track = track_to_mpd_format(track, position)
formatted_track = track_to_mpd_format(
track, position, multiple_tags=multiple_tags
)
if formatted_track:
result.append(formatted_track)
return result
Expand Down
6 changes: 5 additions & 1 deletion tests/protocol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ class BaseTestCase(unittest.TestCase):
def get_config(self):
return {
"core": {"max_tracklist_length": 10000},
"mpd": {"password": None, "default_playlist_scheme": "dummy"},
"mpd": {
"password": None,
"default_playlist_scheme": "dummy",
"multiple_tags": False,
},
}

def setUp(self): # noqa: N802
Expand Down
8 changes: 7 additions & 1 deletion tests/test_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@

class MpdDispatcherTest(unittest.TestCase):
def setUp(self): # noqa: N802
config = {"mpd": {"password": None, "command_blacklist": ["disabled"]}}
config = {
"mpd": {
"password": None,
"command_blacklist": ["disabled"],
"multiple_tags": False,
}
}
self.backend = dummy_backend.create_proxy()
self.dispatcher = MpdDispatcher(config=config)

Expand Down
Loading

0 comments on commit ae40231

Please sign in to comment.