diff --git a/README.md b/README.md index 4400a71..b970695 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/orvanitag/plugins/id3.py b/orvanitag/plugins/id3.py index f43e60e..e80532d 100644 --- a/orvanitag/plugins/id3.py +++ b/orvanitag/plugins/id3.py @@ -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: diff --git a/pyproject.toml b/pyproject.toml index f2caf48..402ebab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,5 +40,5 @@ include = ["orvanitag*"] [dependency-groups] dev = [ "pytest>=8", - "orvanitag[pdf,office,markdown]", + "orvanitag[audio,pdf,office,markdown]", ] diff --git a/tests/test_id3.py b/tests/test_id3.py new file mode 100644 index 0000000..30ae676 --- /dev/null +++ b/tests/test_id3.py @@ -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' diff --git a/uv.lock b/uv.lock index bfc69fe..7f87402 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, ]