Skip to content

Commit 177e997

Browse files
Add future annotations, add vinyl track index parsing, simplify docs
1 parent 4cd29f9 commit 177e997

File tree

3 files changed

+94
-12
lines changed

3 files changed

+94
-12
lines changed

beetsplug/fromfilename.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# This file is part of beets.
2-
# Copyright 2016, Jan-Erik Dahlin
2+
# Copyright 2016, Jan-Erik Dahlin, Henry Oberholtzer.
33
#
44
# Permission is hereby granted, free of charge, to any person obtaining
55
# a copy of this software and associated documentation files (the
@@ -16,18 +16,23 @@
1616
(possibly also extract track and artist)
1717
"""
1818

19+
from __future__ import annotations
20+
1921
import re
2022
from collections.abc import Iterator, MutableMapping, ValuesView
2123
from datetime import datetime
2224
from functools import cached_property
2325
from pathlib import Path
26+
from typing import TYPE_CHECKING
2427

2528
from beets import config
26-
from beets.importer import ImportSession, ImportTask
27-
from beets.library import Item
2829
from beets.plugins import BeetsPlugin
2930
from beets.util import displayable_path
3031

32+
if TYPE_CHECKING:
33+
from beets.importer import ImportSession, ImportTask
34+
from beets.library import Item
35+
3136
# Filename field extraction patterns
3237
RE_TRACK_INFO = re.compile(
3338
r"""
@@ -52,6 +57,8 @@
5257
re.VERBOSE | re.IGNORECASE,
5358
)
5459

60+
RE_ALPHANUM_INDEX = re.compile(r"^[A-Z]{1,2}\d{,2}\b")
61+
5562
# Catalog number extraction pattern
5663
RE_CATALOGNUM = re.compile(
5764
r"""
@@ -180,14 +187,14 @@ def filename_task(self, task: ImportTask, session: ImportSession) -> None:
180187
Once the information has been obtained and checked, it
181188
is applied to the items to improve later metadata lookup.
182189
"""
183-
# Create the list of items to process
190+
# Retrieve the list of items to process
184191

185192
items: list[Item] = task.items
186193

187194
# If there's no missing data to parse
188195
if not self._check_missing_data(items):
189196
return
190-
197+
# Retrieve the path characteristics to check
191198
parent_folder, item_filenames = self._get_path_strings(items)
192199

193200
album_matches = self._parse_album_info(parent_folder)
@@ -259,6 +266,8 @@ def _build_track_matches(
259266
self, item_filenames: dict[Item, str]
260267
) -> dict[Item, FilenameMatch]:
261268
track_matches: dict[Item, FilenameMatch] = {}
269+
# Check for alphanumeric indices
270+
self._parse_alphanumeric_index(item_filenames)
262271
for item, filename in item_filenames.items():
263272
if m := self._check_user_matches(filename, self.file_patterns):
264273
track_matches[item] = m
@@ -267,6 +276,48 @@ def _build_track_matches(
267276
track_matches[item] = match
268277
return track_matches
269278

279+
@staticmethod
280+
def _parse_alphanumeric_index(item_filenames: dict[Item, str]) -> None:
281+
"""Before continuing to regular track matches, see if an alphanumeric
282+
tracklist can be extracted. "A1, B1, B2" Sometimes these are followed
283+
by a dash or dot and must be anchored to the start of the string.
284+
285+
All matched patterns are extracted, and replaced with integers.
286+
287+
Discs are not accounted for.
288+
"""
289+
290+
def match_index(filename: str) -> str:
291+
m = RE_ALPHANUM_INDEX.match(filename)
292+
if not m:
293+
return ""
294+
else:
295+
return m.group()
296+
297+
# Extract matches for alphanumeric indexes
298+
indexes: list[tuple[str, Item]] = [
299+
(match_index(filename), item)
300+
for item, filename in item_filenames.items()
301+
]
302+
# If all the tracks do not start with a vinyl index, abort
303+
if not all([i[0] for i in indexes]):
304+
return
305+
306+
# Utility function for sorting
307+
def index_key(x: tuple[str, Item]):
308+
return x[0]
309+
310+
# If all have match, sort by the matched strings
311+
indexes.sort(key=index_key)
312+
# Iterate through all the filenames
313+
for index, pair in enumerate(indexes):
314+
match, item = pair
315+
# Substitute the alnum index with an integer
316+
new_filename = item_filenames[item].replace(
317+
match, str(index + 1), 1
318+
)
319+
item_filenames[item] = new_filename
320+
270321
@staticmethod
271322
def _parse_track_info(text: str) -> FilenameMatch:
272323
match = RE_TRACK_INFO.match(text)

docs/plugins/fromfilename.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,16 @@ Default
5757

5858
.. conf:: patterns
5959

60-
Users can specify patterns to improve the efficacy of the plugin. Patterns can
61-
be specified as ``file`` or ``folder`` patterns. ``file`` patterns are checked
62-
against the filename. ``folder`` patterns are checked against the parent folder
63-
of the file.
60+
Users can specify patterns to expand the set of filenames that can
61+
be recognized by the plugin. Patterns can be specified as ``file``
62+
or ``folder`` patterns. ``file`` patterns are checked against the filename.
63+
``folder`` patterns are checked against the parent folder of the file.
6464

65-
If ``fromfilename`` can't match the entire string to the given pattern, it will
65+
If ``fromfilename`` can't match the entire string to one of the given pattern, it will
6666
fall back to the default pattern.
6767

68-
The following custom patterns will match this path and retrieve the specified
69-
fields.
68+
For example, the following custom patterns will match this path and folder,
69+
and retrieve the specified fields.
7070

7171
``/music/James Lawson - 841689 (2004)/Coming Up - James Lawson & Andy Farley.mp3``
7272

test/plugins/test_fromfilename.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,37 @@ def test_user_patterns(self, patterns, expected):
712712
assert res.year == expected.year
713713
assert res.title == expected.title
714714

715+
@pytest.mark.parametrize(
716+
"expected",
717+
[
718+
(
719+
mock_item(path="/temp/A - track.wav", track=1),
720+
mock_item(path="/temp/B - track.wav", track=2),
721+
mock_item(path="/temp/C - track.wav", track=3),
722+
),
723+
# Test with numbers
724+
(
725+
mock_item(path="/temp/A1 - track.wav", track=1),
726+
mock_item(path="/temp/A2 - track.wav", track=2),
727+
mock_item(path="/temp/B1 - track.wav", track=3),
728+
),
729+
# Test out of order
730+
(
731+
mock_item(path="/temp/Z - track.wav", track=3),
732+
mock_item(path="/temp/X - track.wav", track=1),
733+
mock_item(path="/temp/Y - track.wav", track=2),
734+
),
735+
],
736+
)
737+
def test_alphanumeric_index(self, expected):
738+
"""Test parsing an alphanumeric index string."""
739+
task = mock_task([mock_item(path=item.path) for item in expected])
740+
f = FromFilenamePlugin()
741+
f.filename_task(task, Session())
742+
assert task.items[0].track == expected[0].track
743+
assert task.items[1].track == expected[1].track
744+
assert task.items[2].track == expected[2].track
745+
715746
def test_no_changes(self):
716747
item = mock_item(
717748
path="/Folder/File.wav",

0 commit comments

Comments
 (0)