Skip to content

Commit af07f45

Browse files
Refactor plugin, add type hints
1 parent afc26fa commit af07f45

File tree

1 file changed

+65
-64
lines changed

1 file changed

+65
-64
lines changed

beetsplug/fromfilename.py

Lines changed: 65 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818

1919
import os
2020
import re
21+
import typing
2122

22-
from beets import plugins
23+
from beets.importer import ImportSession, ImportTask
24+
from beets.library import Item
25+
from beets.plugins import BeetsPlugin
2326
from beets.util import displayable_path
2427

2528
# Filename field extraction patterns.
@@ -42,20 +45,22 @@
4245
]
4346

4447

45-
def equal(seq):
48+
def equal(seq: list[str]):
4649
"""Determine whether a sequence holds identical elements."""
4750
return len(set(seq)) <= 1
4851

4952

50-
def equal_fields(matchdict, field):
53+
def equal_fields(matchdict: dict[typing.Any, dict[str, str]], field: str):
5154
"""Do all items in `matchdict`, whose values are dictionaries, have
5255
the same value for `field`? (If they do, the field is probably not
5356
the title.)
5457
"""
55-
return equal(m[field] for m in matchdict.values())
58+
return equal(list(m[field] for m in matchdict.values()))
5659

5760

58-
def all_matches(names, pattern):
61+
def all_matches(
62+
names: dict[Item, str], pattern: str
63+
) -> dict[Item, dict[str, str]] | None:
5964
"""If all the filenames in the item/filename mapping match the
6065
pattern, return a dictionary mapping the items to dictionaries
6166
giving the value for each named subpattern in the match. Otherwise,
@@ -74,7 +79,7 @@ def all_matches(names, pattern):
7479
return matches
7580

7681

77-
def bad_title(title):
82+
def bad_title(title: str) -> bool:
7883
"""Determine whether a given title is "bad" (empty or otherwise
7984
meaningless) and in need of replacement.
8085
"""
@@ -84,77 +89,28 @@ def bad_title(title):
8489
return False
8590

8691

87-
def apply_matches(d, log):
88-
"""Given a mapping from items to field dicts, apply the fields to
89-
the objects.
90-
"""
91-
some_map = list(d.values())[0]
92-
keys = some_map.keys()
93-
94-
# Only proceed if the "tag" field is equal across all filenames.
95-
if "tag" in keys and not equal_fields(d, "tag"):
96-
return
97-
98-
# Given both an "artist" and "title" field, assume that one is
99-
# *actually* the artist, which must be uniform, and use the other
100-
# for the title. This, of course, won't work for VA albums.
101-
# Only check for "artist": patterns containing it, also contain "title"
102-
if "artist" in keys:
103-
if equal_fields(d, "artist"):
104-
artist = some_map["artist"]
105-
title_field = "title"
106-
elif equal_fields(d, "title"):
107-
artist = some_map["title"]
108-
title_field = "artist"
109-
else:
110-
# Both vary. Abort.
111-
return
112-
113-
for item in d:
114-
if not item.artist:
115-
item.artist = artist
116-
log.info("Artist replaced with: {.artist}", item)
117-
# otherwise, if the pattern contains "title", use that for title_field
118-
elif "title" in keys:
119-
title_field = "title"
120-
else:
121-
title_field = None
122-
123-
# Apply the title and track, if any.
124-
for item in d:
125-
if title_field and bad_title(item.title):
126-
item.title = str(d[item][title_field])
127-
log.info("Title replaced with: {.title}", item)
128-
129-
if "track" in d[item] and item.track == 0:
130-
item.track = int(d[item]["track"])
131-
log.info("Track replaced with: {.track}", item)
132-
133-
134-
# Plugin structure and hook into import process.
135-
136-
137-
class FromFilenamePlugin(plugins.BeetsPlugin):
138-
def __init__(self):
92+
class FromFilenamePlugin(BeetsPlugin):
93+
def __init__(self) -> None:
13994
super().__init__()
14095
self.register_listener("import_task_start", self.filename_task)
14196

142-
def filename_task(self, task, session):
97+
def filename_task(self, task: ImportTask, session: ImportSession) -> None:
14398
"""Examine each item in the task to see if we can extract a title
14499
from the filename. Try to match all filenames to a number of
145100
regexps, starting with the most complex patterns and successively
146101
trying less complex patterns. As soon as all filenames match the
147102
same regex we can make an educated guess of which part of the
148103
regex that contains the title.
149104
"""
150-
items = task.items if task.is_album else [task.item]
105+
# Create the list of items to process
106+
items: list[Item] = task.items
151107

152108
# Look for suspicious (empty or meaningless) titles.
153109
missing_titles = sum(bad_title(i.title) for i in items)
154110

155111
if missing_titles:
156112
# Get the base filenames (no path or extension).
157-
names = {}
113+
names: dict[Item, str] = {}
158114
for item in items:
159115
path = displayable_path(item.path)
160116
name, _ = os.path.splitext(os.path.basename(path))
@@ -163,6 +119,51 @@ def filename_task(self, task, session):
163119
# Look for useful information in the filenames.
164120
for pattern in PATTERNS:
165121
self._log.debug(f"Trying pattern: {pattern}")
166-
d = all_matches(names, pattern)
167-
if d:
168-
apply_matches(d, self._log)
122+
if d := all_matches(names, pattern):
123+
self._apply_matches(d)
124+
125+
def _apply_matches(self, d: dict[Item, dict[str, str]]) -> None:
126+
"""Given a mapping from items to field dicts, apply the fields to
127+
the objects.
128+
"""
129+
some_map = list(d.values())[0]
130+
keys = some_map.keys()
131+
132+
# Only proceed if the "tag" field is equal across all filenames.
133+
if "tag" in keys and not equal_fields(d, "tag"):
134+
return
135+
136+
# Given both an "artist" and "title" field, assume that one is
137+
# *actually* the artist, which must be uniform, and use the other
138+
# for the title. This, of course, won't work for VA albums.
139+
# Only check for "artist": patterns containing it, also contain "title"
140+
if "artist" in keys:
141+
if equal_fields(d, "artist"):
142+
artist = some_map["artist"]
143+
title_field = "title"
144+
elif equal_fields(d, "title"):
145+
artist = some_map["title"]
146+
title_field = "artist"
147+
else:
148+
# Both vary. Abort.
149+
return
150+
151+
for item in d:
152+
if not item.artist:
153+
item.artist = artist
154+
self._log.info(f"Artist replaced with: {item.artist}")
155+
# otherwise, if the pattern contains "title", use that for title_field
156+
elif "title" in keys:
157+
title_field = "title"
158+
else:
159+
title_field = None
160+
161+
# Apply the title and track, if any.
162+
for item in d:
163+
if title_field and bad_title(item.title):
164+
item.title = str(d[item][title_field])
165+
self._log.info(f"Title replaced with: {item.title}")
166+
167+
if "track" in d[item] and item.track == 0:
168+
item.track = int(d[item]["track"])
169+
self._log.info(f"Track replaced with: {item.track}")

0 commit comments

Comments
 (0)