mirror of
https://github.com/beetbox/beets.git
synced 2026-02-17 04:43:40 +01:00
Final types, docs, ignore directories
This commit is contained in:
parent
3676c39d65
commit
32672054d4
3 changed files with 120 additions and 24 deletions
|
|
@ -17,7 +17,7 @@
|
|||
"""
|
||||
|
||||
import re
|
||||
from collections.abc import MutableMapping
|
||||
from collections.abc import Iterator, MutableMapping, ValuesView
|
||||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
|
|
@ -118,10 +118,10 @@ class FilenameMatch(MutableMapping[str, str | None]):
|
|||
if value is not None:
|
||||
self._matches[key.lower()] = str(value).strip()
|
||||
|
||||
def __getitem__(self, key) -> str | None:
|
||||
def __getitem__(self, key: str) -> str | None:
|
||||
return self._matches.get(key, None)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self._matches)
|
||||
|
||||
def __len__(self) -> int:
|
||||
|
|
@ -134,7 +134,7 @@ class FilenameMatch(MutableMapping[str, str | None]):
|
|||
def __delitem__(self, key: str) -> None:
|
||||
del self._matches[key]
|
||||
|
||||
def values(self):
|
||||
def values(self) -> ValuesView[str | None]:
|
||||
return self._matches.values()
|
||||
|
||||
|
||||
|
|
@ -155,23 +155,22 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
"year",
|
||||
],
|
||||
"patterns": {"folder": [], "file": []},
|
||||
# TODO: Add ignore parent folder
|
||||
"ignore_dirs": [],
|
||||
}
|
||||
)
|
||||
self.fields = set(self.config["fields"].as_str_seq())
|
||||
# Evaluate the user patterns to expand the fields
|
||||
self.file_patterns = self._user_pattern_to_regex(
|
||||
self.config["patterns"]["file"].as_str_seq()
|
||||
)
|
||||
self.folder_patterns = self._user_pattern_to_regex(
|
||||
self.config["patterns"]["folder"].as_str_seq()
|
||||
)
|
||||
self.register_listener("import_task_start", self.filename_task)
|
||||
|
||||
@cached_property
|
||||
def file_patterns(self) -> list[re.Pattern[str]]:
|
||||
return self._user_pattern_to_regex(
|
||||
self.config["patterns"]["file"].as_str_seq()
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def folder_patterns(self) -> list[re.Pattern[str]]:
|
||||
return self._user_pattern_to_regex(
|
||||
self.config["patterns"]["folder"].as_str_seq()
|
||||
)
|
||||
def ignored_directories(self) -> set[str]:
|
||||
return set([p.lower() for p in self.config["ignore_dirs"].as_str_seq()])
|
||||
|
||||
def filename_task(self, task: ImportTask, session: ImportSession) -> None:
|
||||
"""Examines all files in the given import task for any missing
|
||||
|
|
@ -184,8 +183,10 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
|
||||
items: list[Item] = task.items
|
||||
|
||||
# TODO: Check each of the fields to see if any are missing
|
||||
# information on the file.
|
||||
# If there's no missing data to parse
|
||||
if not self._check_missing_data(items):
|
||||
return
|
||||
|
||||
parent_folder, item_filenames = self._get_path_strings(items)
|
||||
|
||||
album_matches = self._parse_album_info(parent_folder)
|
||||
|
|
@ -196,6 +197,31 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
# Apply the information
|
||||
self._apply_matches(album_matches, track_matches)
|
||||
|
||||
def _check_missing_data(self, items: list[Item]) -> bool:
|
||||
"""Look for what fields are missing data on the items.
|
||||
Compare each field in self.fields to the fields on the
|
||||
item.
|
||||
|
||||
If all items have it, remove it from fields.
|
||||
|
||||
If any items are missing it, keep it on the fields.
|
||||
|
||||
If no fields are detect that need to be processed,
|
||||
return false to shortcut the plugin.
|
||||
"""
|
||||
remove = set()
|
||||
for field in self.fields:
|
||||
# If any field is a bad field
|
||||
if any([True for item in items if self._bad_field(item[field])]):
|
||||
continue
|
||||
else:
|
||||
remove.add(field)
|
||||
self.fields = self.fields - remove
|
||||
# If all fields have been removed, there is nothing to do
|
||||
if not len(self.fields):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _user_pattern_to_regex(
|
||||
self, patterns: list[str]
|
||||
) -> list[re.Pattern[str]]:
|
||||
|
|
@ -208,8 +234,9 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
if (regexp := self._parse_user_pattern_strings(p))
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _get_path_strings(items: list[Item]) -> tuple[str, dict[Item, str]]:
|
||||
def _get_path_strings(
|
||||
self, items: list[Item]
|
||||
) -> tuple[str, dict[Item, str]]:
|
||||
parent_folder: str = ""
|
||||
filenames: dict[Item, str] = {}
|
||||
for item in items:
|
||||
|
|
@ -218,6 +245,8 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
filenames[item] = filename
|
||||
if not parent_folder:
|
||||
parent_folder = path.parent.stem
|
||||
if parent_folder.lower() in self.ignored_directories:
|
||||
parent_folder = ""
|
||||
return parent_folder, filenames
|
||||
|
||||
def _check_user_matches(
|
||||
|
|
@ -314,7 +343,7 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
for key in match.keys():
|
||||
if key in self.fields:
|
||||
old_value = item.get(key)
|
||||
new_value = match[key] # type: ignore
|
||||
new_value = match[key]
|
||||
if self._bad_field(old_value) and new_value:
|
||||
found_data[key] = new_value
|
||||
self._log.info(f"Item updated with: {found_data.items()}")
|
||||
|
|
@ -436,7 +465,7 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
identified.
|
||||
"""
|
||||
|
||||
def swap_artist_title(tracks: list[FilenameMatch]):
|
||||
def swap_artist_title(tracks: list[FilenameMatch]) -> None:
|
||||
for track in tracks:
|
||||
artist = track["title"]
|
||||
track["title"] = track["artist"]
|
||||
|
|
@ -475,7 +504,7 @@ class FromFilenamePlugin(BeetsPlugin):
|
|||
@staticmethod
|
||||
def _equal_fields(dictionaries: list[FilenameMatch], field: str) -> bool:
|
||||
"""Checks if all values of a field on a dictionary match."""
|
||||
return len(set(d[field] for d in dictionaries)) <= 1 # type: ignore
|
||||
return len(set(d[field] for d in dictionaries)) <= 1
|
||||
|
||||
@staticmethod
|
||||
def _bad_field(field: str | int) -> bool:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ but where the filenames contain useful information like the artist and title.
|
|||
When you attempt to import a track that's missing a title, this plugin will look
|
||||
at the track's filename and parent folder, and guess a number of fields.
|
||||
|
||||
The extracted information will be used to search for metadata and match track ordering.
|
||||
The extracted information will be used to search for metadata and match track
|
||||
ordering.
|
||||
|
||||
To use the ``fromfilename`` plugin, enable it in your configuration (see
|
||||
:ref:`using-plugins`).
|
||||
|
|
@ -38,11 +39,21 @@ Default
|
|||
patterns:
|
||||
file: []
|
||||
folder: []
|
||||
ignore_dirs:
|
||||
|
||||
.. conf:: fields
|
||||
:default: [ artist, album, albumartist, catalognum, disc, media, title, track, year ]
|
||||
|
||||
The fields the plugin will guess with its default pattern matching. If a field is specified in a user pattern, that field does not need to be present on this list to be applied. If you only want the plugin contribute the track title and artist, you would put ``[title, artist]``.
|
||||
The fields the plugin will guess with its default pattern matching.
|
||||
|
||||
By default, the plugin is configured to match all fields its default
|
||||
patterns are capable of matching.
|
||||
|
||||
If a field is specified in a user pattern, that field does not need
|
||||
to be present on this list to be applied.
|
||||
|
||||
If you only want the plugin to contribute the track title and artist,
|
||||
you would put ``[title, artist]``.
|
||||
|
||||
.. conf:: patterns
|
||||
|
||||
|
|
@ -67,3 +78,9 @@ Default
|
|||
file:
|
||||
- "$title - $artist"
|
||||
|
||||
.. conf:: ignore_dirs
|
||||
:default: []
|
||||
|
||||
Specify parent directory names that will not be searched for album
|
||||
information. Useful if you use a regular directory for importing
|
||||
single files.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
|
||||
"""Tests for the fromfilename plugin."""
|
||||
|
||||
from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from beets.importer.tasks import ImportTask, SingletonImportTask
|
||||
|
|
@ -287,6 +290,7 @@ def test_parse_user_pattern_strings(string, pattern):
|
|||
|
||||
class TestFromFilename(PluginMixin):
|
||||
plugin = "fromfilename"
|
||||
preload_plugin = False
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expected_item",
|
||||
|
|
@ -707,3 +711,49 @@ class TestFromFilename(PluginMixin):
|
|||
assert res.catalognum == expected.catalognum
|
||||
assert res.year == expected.year
|
||||
assert res.title == expected.title
|
||||
|
||||
def test_no_changes(self):
|
||||
item = mock_item(
|
||||
path="/Folder/File.wav",
|
||||
albumartist="AlbumArtist",
|
||||
artist="Artist",
|
||||
title="Title",
|
||||
)
|
||||
fields = ["artist", "title", "albumartist"]
|
||||
task = mock_task([item])
|
||||
with self.configure_plugin({"fields": fields}):
|
||||
with patch.object(FromFilenamePlugin, "_get_path_strings") as mock:
|
||||
f = FromFilenamePlugin()
|
||||
f.filename_task(task, Session())
|
||||
mock.assert_not_called()
|
||||
|
||||
def test_changes_missing_values(self):
|
||||
item = mock_item(
|
||||
path="/Folder/File.wav",
|
||||
albumartist="AlbumArtist",
|
||||
artist="Artist",
|
||||
title="Title",
|
||||
)
|
||||
item2 = deepcopy(item)
|
||||
item2.title = ""
|
||||
fields = ["artist", "title", "albumartist"]
|
||||
task = mock_task([item, item2])
|
||||
with self.configure_plugin({"fields": fields}):
|
||||
with patch.object(
|
||||
FromFilenamePlugin,
|
||||
"_get_path_strings",
|
||||
return_value=("mock", {item: "mock"}),
|
||||
) as mock:
|
||||
f = FromFilenamePlugin()
|
||||
f.filename_task(task, Session())
|
||||
assert len(f.fields) == 1
|
||||
assert "title" in f.fields
|
||||
mock.assert_called()
|
||||
|
||||
def test_ignored_directories(self):
|
||||
ignored = "Incoming"
|
||||
item = mock_item(path="/tmp/" + ignored + "/01 - File.wav")
|
||||
with self.configure_plugin({"ignore_dirs": [ignored]}):
|
||||
f = FromFilenamePlugin()
|
||||
parent_folder, _ = f._get_path_strings([item])
|
||||
assert parent_folder == ""
|
||||
|
|
|
|||
Loading…
Reference in a new issue