File: ftintitle.py

package info (click to toggle)
beets 2.5.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 7,988 kB
  • sloc: python: 46,429; javascript: 8,018; xml: 334; sh: 261; makefile: 125
file content (214 lines) | stat: -rw-r--r-- 7,683 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# This file is part of beets.
# Copyright 2016, Verrus, <github.com/Verrus/beets-plugin-featInTitle>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

"""Moves "featured" artists to the title from the artist field."""

from __future__ import annotations

import re
from typing import TYPE_CHECKING

from beets import plugins, ui

if TYPE_CHECKING:
    from beets.importer import ImportSession, ImportTask
    from beets.library import Item


def split_on_feat(
    artist: str, for_artist: bool = True
) -> tuple[str, str | None]:
    """Given an artist string, split the "main" artist from any artist
    on the right-hand side of a string like "feat". Return the main
    artist, which is always a string, and the featuring artist, which
    may be a string or None if none is present.
    """
    # split on the first "feat".
    regex = re.compile(plugins.feat_tokens(for_artist), re.IGNORECASE)
    parts = tuple(s.strip() for s in regex.split(artist, 1))
    if len(parts) == 1:
        return parts[0], None
    else:
        assert len(parts) == 2  # help mypy out
        return parts


def contains_feat(title: str) -> bool:
    """Determine whether the title contains a "featured" marker."""
    return bool(
        re.search(
            plugins.feat_tokens(for_artist=False),
            title,
            flags=re.IGNORECASE,
        )
    )


def find_feat_part(artist: str, albumartist: str | None) -> str | None:
    """Attempt to find featured artists in the item's artist fields and
    return the results. Returns None if no featured artist found.
    """
    # Handle a wider variety of extraction cases if the album artist is
    # contained within the track artist.
    if albumartist and albumartist in artist:
        albumartist_split = artist.split(albumartist, 1)

        # If the last element of the split (the right-hand side of the
        # album artist) is nonempty, then it probably contains the
        # featured artist.
        if albumartist_split[1] != "":
            # Extract the featured artist from the right-hand side.
            _, feat_part = split_on_feat(albumartist_split[1])
            return feat_part

        # Otherwise, if there's nothing on the right-hand side,
        # look for a featuring artist on the left-hand side.
        else:
            lhs, _ = split_on_feat(albumartist_split[0])
            if lhs:
                return lhs

    # Fall back to conservative handling of the track artist without relying
    # on albumartist, which covers compilations using a 'Various Artists'
    # albumartist and album tracks by a guest artist featuring a third artist.
    _, feat_part = split_on_feat(artist, False)
    return feat_part


class FtInTitlePlugin(plugins.BeetsPlugin):
    def __init__(self) -> None:
        super().__init__()

        self.config.add(
            {
                "auto": True,
                "drop": False,
                "format": "feat. {}",
                "keep_in_artist": False,
            }
        )

        self._command = ui.Subcommand(
            "ftintitle", help="move featured artists to the title field"
        )

        self._command.parser.add_option(
            "-d",
            "--drop",
            dest="drop",
            action="store_true",
            default=None,
            help="drop featuring from artists and ignore title update",
        )

        if self.config["auto"]:
            self.import_stages = [self.imported]

    def commands(self) -> list[ui.Subcommand]:
        def func(lib, opts, args):
            self.config.set_args(opts)
            drop_feat = self.config["drop"].get(bool)
            keep_in_artist_field = self.config["keep_in_artist"].get(bool)
            write = ui.should_write()

            for item in lib.items(args):
                if self.ft_in_title(item, drop_feat, keep_in_artist_field):
                    item.store()
                    if write:
                        item.try_write()

        self._command.func = func
        return [self._command]

    def imported(self, session: ImportSession, task: ImportTask) -> None:
        """Import hook for moving featuring artist automatically."""
        drop_feat = self.config["drop"].get(bool)
        keep_in_artist_field = self.config["keep_in_artist"].get(bool)

        for item in task.imported_items():
            if self.ft_in_title(item, drop_feat, keep_in_artist_field):
                item.store()

    def update_metadata(
        self,
        item: Item,
        feat_part: str,
        drop_feat: bool,
        keep_in_artist_field: bool,
    ) -> None:
        """Choose how to add new artists to the title and set the new
        metadata. Also, print out messages about any changes that are made.
        If `drop_feat` is set, then do not add the artist to the title; just
        remove it from the artist field.
        """
        # In case the artist is kept, do not update the artist fields.
        if keep_in_artist_field:
            self._log.info(
                "artist: {.artist} (Not changing due to keep_in_artist)", item
            )
        else:
            track_artist, _ = split_on_feat(item.artist)
            self._log.info("artist: {0.artist} -> {1}", item, track_artist)
            item.artist = track_artist

        if item.artist_sort:
            # Just strip the featured artist from the sort name.
            item.artist_sort, _ = split_on_feat(item.artist_sort)

        # Only update the title if it does not already contain a featured
        # artist and if we do not drop featuring information.
        if not drop_feat and not contains_feat(item.title):
            feat_format = self.config["format"].as_str()
            new_format = feat_format.format(feat_part)
            new_title = f"{item.title} {new_format}"
            self._log.info("title: {.title} -> {}", item, new_title)
            item.title = new_title

    def ft_in_title(
        self,
        item: Item,
        drop_feat: bool,
        keep_in_artist_field: bool,
    ) -> bool:
        """Look for featured artists in the item's artist fields and move
        them to the title.

        Returns:
            True if the item has been modified. False otherwise.
        """
        artist = item.artist.strip()
        albumartist = item.albumartist.strip()

        # Check whether there is a featured artist on this track and the
        # artist field does not exactly match the album artist field. In
        # that case, we attempt to move the featured artist to the title.
        if albumartist and artist == albumartist:
            return False

        _, featured = split_on_feat(artist)
        if not featured:
            return False

        self._log.info("{.filepath}", item)

        # Attempt to find the featured artist.
        feat_part = find_feat_part(artist, albumartist)

        if not feat_part:
            self._log.info("no featuring artists found")
            return False

        # If we have a featuring artist, move it to the title.
        self.update_metadata(item, feat_part, drop_feat, keep_in_artist_field)
        return True