mirror of
https://github.com/beetbox/beets.git
synced 2026-02-17 04:43:40 +01:00
Add future annotations, add vinyl track index parsing, simplify docs
This commit is contained in:
parent
4cd29f9a85
commit
177e997cb0
3 changed files with 94 additions and 12 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# This file is part of beets.
|
||||
# Copyright 2016, Jan-Erik Dahlin
|
||||
# Copyright 2016, Jan-Erik Dahlin, Henry Oberholtzer.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
|
|
@ -16,18 +16,23 @@
|
|||
(possibly also extract track and artist)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Iterator, MutableMapping, ValuesView
|
||||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from beets import config
|
||||
from beets.importer import ImportSession, ImportTask
|
||||
from beets.library import Item
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.util import displayable_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beets.importer import ImportSession, ImportTask
|
||||
from beets.library import Item
|
||||
|
||||
# Filename field extraction patterns
|
||||
RE_TRACK_INFO = re.compile(
|
||||
r"""
|
||||
|
|
@ -52,6 +57,8 @@ RE_TRACK_INFO = re.compile(
|
|||
re.VERBOSE | re.IGNORECASE,
|
||||
)
|
||||
|
||||
RE_ALPHANUM_INDEX = re.compile(r"^[A-Z]{1,2}\d{,2}\b")
|
||||
|
||||
# Catalog number extraction pattern
|
||||
RE_CATALOGNUM = re.compile(
|
||||
r"""
|
||||
|
|
@ -180,14 +187,14 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
Once the information has been obtained and checked, it
|
||||
is applied to the items to improve later metadata lookup.
|
||||
"""
|
||||
# Create the list of items to process
|
||||
# Retrieve the list of items to process
|
||||
|
||||
items: list[Item] = task.items
|
||||
|
||||
# If there's no missing data to parse
|
||||
if not self._check_missing_data(items):
|
||||
return
|
||||
|
||||
# Retrieve the path characteristics to check
|
||||
parent_folder, item_filenames = self._get_path_strings(items)
|
||||
|
||||
album_matches = self._parse_album_info(parent_folder)
|
||||
|
|
@ -259,6 +266,8 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
self, item_filenames: dict[Item, str]
|
||||
) -> dict[Item, FilenameMatch]:
|
||||
track_matches: dict[Item, FilenameMatch] = {}
|
||||
# Check for alphanumeric indices
|
||||
self._parse_alphanumeric_index(item_filenames)
|
||||
for item, filename in item_filenames.items():
|
||||
if m := self._check_user_matches(filename, self.file_patterns):
|
||||
track_matches[item] = m
|
||||
|
|
@ -267,6 +276,48 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
track_matches[item] = match
|
||||
return track_matches
|
||||
|
||||
@staticmethod
|
||||
def _parse_alphanumeric_index(item_filenames: dict[Item, str]) -> None:
|
||||
"""Before continuing to regular track matches, see if an alphanumeric
|
||||
tracklist can be extracted. "A1, B1, B2" Sometimes these are followed
|
||||
by a dash or dot and must be anchored to the start of the string.
|
||||
|
||||
All matched patterns are extracted, and replaced with integers.
|
||||
|
||||
Discs are not accounted for.
|
||||
"""
|
||||
|
||||
def match_index(filename: str) -> str:
|
||||
m = RE_ALPHANUM_INDEX.match(filename)
|
||||
if not m:
|
||||
return ""
|
||||
else:
|
||||
return m.group()
|
||||
|
||||
# Extract matches for alphanumeric indexes
|
||||
indexes: list[tuple[str, Item]] = [
|
||||
(match_index(filename), item)
|
||||
for item, filename in item_filenames.items()
|
||||
]
|
||||
# If all the tracks do not start with a vinyl index, abort
|
||||
if not all([i[0] for i in indexes]):
|
||||
return
|
||||
|
||||
# Utility function for sorting
|
||||
def index_key(x: tuple[str, Item]):
|
||||
return x[0]
|
||||
|
||||
# If all have match, sort by the matched strings
|
||||
indexes.sort(key=index_key)
|
||||
# Iterate through all the filenames
|
||||
for index, pair in enumerate(indexes):
|
||||
match, item = pair
|
||||
# Substitute the alnum index with an integer
|
||||
new_filename = item_filenames[item].replace(
|
||||
match, str(index + 1), 1
|
||||
)
|
||||
item_filenames[item] = new_filename
|
||||
|
||||
@staticmethod
|
||||
def _parse_track_info(text: str) -> FilenameMatch:
|
||||
match = RE_TRACK_INFO.match(text)
|
||||
|
|
|
|||
|
|
@ -57,16 +57,16 @@ Default
|
|||
|
||||
.. conf:: patterns
|
||||
|
||||
Users can specify patterns to improve the efficacy of the plugin. Patterns can
|
||||
be specified as ``file`` or ``folder`` patterns. ``file`` patterns are checked
|
||||
against the filename. ``folder`` patterns are checked against the parent folder
|
||||
of the file.
|
||||
Users can specify patterns to expand the set of filenames that can
|
||||
be recognized by the plugin. Patterns can be specified as ``file``
|
||||
or ``folder`` patterns. ``file`` patterns are checked against the filename.
|
||||
``folder`` patterns are checked against the parent folder of the file.
|
||||
|
||||
If ``fromfilename`` can't match the entire string to the given pattern, it will
|
||||
If ``fromfilename`` can't match the entire string to one of the given pattern, it will
|
||||
fall back to the default pattern.
|
||||
|
||||
The following custom patterns will match this path and retrieve the specified
|
||||
fields.
|
||||
For example, the following custom patterns will match this path and folder,
|
||||
and retrieve the specified fields.
|
||||
|
||||
``/music/James Lawson - 841689 (2004)/Coming Up - James Lawson & Andy Farley.mp3``
|
||||
|
||||
|
|
|
|||
|
|
@ -712,6 +712,37 @@ class TestFromFilename(PluginMixin):
|
|||
assert res.year == expected.year
|
||||
assert res.title == expected.title
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expected",
|
||||
[
|
||||
(
|
||||
mock_item(path="/temp/A - track.wav", track=1),
|
||||
mock_item(path="/temp/B - track.wav", track=2),
|
||||
mock_item(path="/temp/C - track.wav", track=3),
|
||||
),
|
||||
# Test with numbers
|
||||
(
|
||||
mock_item(path="/temp/A1 - track.wav", track=1),
|
||||
mock_item(path="/temp/A2 - track.wav", track=2),
|
||||
mock_item(path="/temp/B1 - track.wav", track=3),
|
||||
),
|
||||
# Test out of order
|
||||
(
|
||||
mock_item(path="/temp/Z - track.wav", track=3),
|
||||
mock_item(path="/temp/X - track.wav", track=1),
|
||||
mock_item(path="/temp/Y - track.wav", track=2),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_alphanumeric_index(self, expected):
|
||||
"""Test parsing an alphanumeric index string."""
|
||||
task = mock_task([mock_item(path=item.path) for item in expected])
|
||||
f = FromFilenamePlugin()
|
||||
f.filename_task(task, Session())
|
||||
assert task.items[0].track == expected[0].track
|
||||
assert task.items[1].track == expected[1].track
|
||||
assert task.items[2].track == expected[2].track
|
||||
|
||||
def test_no_changes(self):
|
||||
item = mock_item(
|
||||
path="/Folder/File.wav",
|
||||
|
|
|
|||
Loading…
Reference in a new issue