fix: make id3 genre mapping robust and test the id3 plugin

This commit is contained in:
randogoth 2026-10-24 21:41:04 +03:00
parent 3277e5b323
commit 675e069a51
5 changed files with 81 additions and 34 deletions

View file

@ -26,7 +26,7 @@ So a as an example an audio-file could be assigned to project `+album` and have
$ uv tool install 'orvanitag[all]'
```
Requires Python 3.11 or newer. The core only needs rich. Reading and writing embedded metadata uses optional extras: `audio` (pytaglib, needs the TagLib C library), `pdf` (pypdf), `office` (olefile, only for legacy `.doc`/`.xls`/`.ppt`) and `markdown` (PyYAML). Plugins whose extra is not installed are skipped.
Requires Python 3.11 or newer. The core only needs rich. Reading and writing embedded metadata uses optional extras: `audio` (pytaglib; its prebuilt wheels include TagLib, building from source needs the TagLib C library), `pdf` (pypdf), `office` (olefile, only for legacy `.doc`/`.xls`/`.ppt`) and `markdown` (PyYAML). Plugins whose extra is not installed are skipped.
## Command Line Tool

View file

@ -1,41 +1,29 @@
import re
from pathlib import Path
import taglib
NAME = 'id3'
TYPES = ['.mp3', '.wav', '.flac', '.ogg']
REGEX = r'\s*([@+]\w+|#\w+)'
def reader(o):
universal = {i for i in o.schema if o.schema[i]==None}.pop()
with taglib.File(o.file, save_on_exit=False) as song:
metadata = song.tags
id3map = {}
if 'id3' in o.config:
if 'map' in o.config['id3']:
id3map = o.config['id3']['map'].copy()
for context_key in id3map:
if context_key not in o.schema.keys():
del id3map[context_key]
else:
ctag = str(id3map[context_key]).upper()
if ctag in metadata:
id3map[context_key] = metadata[ctag]
if 'COMMENT' in metadata:
comment = metadata['COMMENT']
argv = o.string(comment[0].strip())
context = o.context(argv)
if not universal in context:
context[universal] = []
else:
context[universal] = [context[universal]]
if id3map.keys():
for key in id3map.keys():
if key in context.keys():
context[key] += id3map[key]
context[universal] = '; '.join(context[universal])
return context
comment = metadata.get('COMMENT', [''])[0]
context = o.context(o.string(comment))
# Audio tags mapped to categories by the folder config, e.g. `[id3.map] tags = "genre"`
mapping = o.config.get('id3', {}).get('map', {})
for context_key, tag in mapping.items():
values = [value.strip() for value in metadata.get(str(tag).upper(), []) if value.strip()]
if context_key not in o.schema or not values:
continue
if o.schema[context_key] is None: # the note
context[context_key] = '; '.join(([context[context_key]] if context.get(context_key) else []) + values)
else: # spaces would split the value into a tag and note words
context[context_key] += ['-'.join(value.split()) for value in values]
if any(context.values()):
return context
def writer(o):
string = ' '.join(o.collect(o.context))
with taglib.File(o.file, save_on_exit=True) as song:

View file

@ -40,5 +40,5 @@ include = ["orvanitag*"]
[dependency-groups]
dev = [
"pytest>=8",
"orvanitag[pdf,office,markdown]",
"orvanitag[audio,pdf,office,markdown]",
]

59
tests/test_id3.py Normal file
View file

@ -0,0 +1,59 @@
import wave
import pytest
taglib = pytest.importorskip('taglib')
import orvanitag.orvani as orvani
from orvanitag.orvani import OrvaniTag
@pytest.fixture
def cwd(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
return tmp_path
def make_wav(path, **tags):
with wave.open(str(path), 'wb') as audio:
audio.setnchannels(1)
audio.setsampwidth(2)
audio.setframerate(8000)
audio.writeframes(b'\0\0' * 800)
if tags:
with taglib.File(path, save_on_exit=True) as song:
for tag, value in tags.items():
song.tags[tag.upper()] = [value]
def scanned(cwd, config=None):
if config:
(cwd / orvani.CONFIG_FILE).write_text(config)
OrvaniTag().scan('.', False)
return (cwd / orvani.CONTEXT_FILE).read_text()
def test_write_and_read_comment(cwd):
make_wav(cwd / 'a.wav')
o = OrvaniTag()
o.add('a.wav', '+album', '@drums', '#lo-fi', 'first', 'take')
assert o.write_metadata('a.wav', 'id3')[0] == 0
with taglib.File(cwd / 'a.wav') as song:
assert song.tags['COMMENT'] == ['+album @drums #lo-fi first take']
(cwd / orvani.CONTEXT_FILE).unlink()
assert scanned(cwd) == 'a.wav --- +album @drums #lo-fi first take\n'
def test_map_genre_with_spaces_to_tags(cwd):
make_wav(cwd / 'a.wav', comment='#lofi', genre='Lo Fi')
assert scanned(cwd, '[id3.map]\ntags = "genre"\n') == 'a.wav --- #lofi #Lo-Fi\n'
def test_map_works_without_comment(cwd):
make_wav(cwd / 'a.wav', genre='Jazz', title='Night Walk')
assert scanned(cwd, '[id3.map]\ntags = "genre"\nnote = "title"\n') == 'a.wav --- #Jazz Night Walk\n'
def test_map_to_missing_tag_and_unknown_key_are_ignored(cwd):
make_wav(cwd / 'a.wav', comment='#lofi')
assert scanned(cwd, '[id3.map]\ncontexts = "albumartist"\nbogus = "genre"\n') == 'a.wav --- #lofi\n'

4
uv.lock generated
View file

@ -80,7 +80,7 @@ pdf = [
[package.dev-dependencies]
dev = [
{ name = "orvanitag", extra = ["markdown", "office", "pdf"] },
{ name = "orvanitag", extra = ["audio", "markdown", "office", "pdf"] },
{ name = "pytest" },
]
@ -97,7 +97,7 @@ provides-extras = ["audio", "pdf", "office", "markdown", "all"]
[package.metadata.requires-dev]
dev = [
{ name = "orvanitag", extras = ["pdf", "office", "markdown"] },
{ name = "orvanitag", extras = ["audio", "pdf", "office", "markdown"] },
{ name = "pytest", specifier = ">=8" },
]