feat: add tagspaces plugin with folder and any-file-type plugin support

This commit is contained in:
randogoth 2026-10-25 10:32:22 +02:00
parent 712fe264be
commit 4cfbd581bc
6 changed files with 225 additions and 9 deletions

117
tests/test_tagspaces.py Normal file
View file

@ -0,0 +1,117 @@
import json
import sys
import pytest
import orvanitag.orvani as orvani
from orvanitag.cmd import main
from orvanitag.entry import Entry
from orvanitag.orvani import OrvaniTag
from orvanitag.errors import UnsupportedType
@pytest.fixture
def cwd(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
return tmp_path
def sidecar(cwd, name):
return json.loads((cwd / '.ts' / f'{name}.json').read_text())
def test_sidecar_roundtrip(cwd):
(cwd / 'song.mp3').touch()
OrvaniTag().add('song.mp3', '+album', '@drums', '#lofi', 'a note')
OrvaniTag().write_metadata('song.mp3', 'tagspaces')
data = sidecar(cwd, 'song.mp3')
assert [tag['title'] for tag in data['tags']] == ['+album', '@drums', 'lofi']
assert data['tags'][0]['type'] == 'sidecar' and len(data['id']) == 32
(cwd / orvani.CONTEXT_FILE).unlink()
assert OrvaniTag().scan('.') == ({'song.mp3': Entry(['album'], ['drums'], ['lofi'])}, [])
def test_existing_sidecar_keeps_its_data(cwd):
(cwd / 'a.heic').touch()
(cwd / '.ts').mkdir()
(cwd / '.ts' / 'a.heic.json').write_text(json.dumps({
'appName': 'TagSpaces', 'id': 'x1', 'description': '# Trip\n\nin *markdown*', 'lastUpdated': 1,
'tags': [{'title': 'new york', 'color': '#ffcc24', 'textcolor': '#ffffff', 'type': 'sidecar'},
{'title': '#gone', 'type': 'sidecar'}],
}))
assert OrvaniTag().read_metadata('a.heic') == Entry(tags=['new_york', 'gone'])
OrvaniTag().remove('a.heic', '#gone')
OrvaniTag().add('a.heic', '@city')
OrvaniTag().write_metadata('a.heic', 'tagspaces')
data = sidecar(cwd, 'a.heic')
assert data['tags'] == [{'title': '@city', 'type': 'sidecar'},
{'title': 'new york', 'color': '#ffcc24', 'textcolor': '#ffffff', 'type': 'sidecar'}]
assert data['description'] == '# Trip\n\nin *markdown*' and data['id'] == 'x1' and data['appName'] == 'TagSpaces'
assert data['lastUpdated'] > 1
def test_folder_tags(cwd):
(cwd / 'sub' / 'deeper').mkdir(parents=True)
OrvaniTag().add('sub', '+trip')
OrvaniTag().write_metadata('sub', 'tagspaces')
assert json.loads((cwd / 'sub' / '.ts' / 'tsm.json').read_text())['tags'] == [{'title': '+trip', 'type': 'sidecar'}]
(cwd / 'sub' / 'deeper' / '.ts').mkdir()
(cwd / 'sub' / 'deeper' / '.ts' / 'tsm.json').write_text('{"tags": [{"title": "deep"}]}')
(cwd / orvani.CONTEXT_FILE).unlink()
assert OrvaniTag().scan('sub') == ({'sub/deeper': Entry(tags=['deep'])}, []) # the scanned folder itself is skipped
assert OrvaniTag().scan('.', recursive=True)[0] == {'sub': Entry(['trip']), 'sub/deeper': Entry(tags=['deep'])}
assert (cwd / orvani.CONTEXT_FILE).read_text() == 'sub/ --- +trip\n'
def test_name_tags_are_read_and_not_repeated(cwd):
(cwd / 'IMG[vacation alps].heic').touch()
assert OrvaniTag().read_metadata('IMG[vacation alps].heic') == Entry(tags=['vacation', 'alps'])
OrvaniTag().add('IMG[vacation alps].heic', '#beach')
OrvaniTag().write_metadata('IMG[vacation alps].heic', 'tagspaces')
assert sidecar(cwd, 'IMG[vacation alps].heic')['tags'] == [{'title': 'beach', 'type': 'sidecar'}]
assert (cwd / 'IMG[vacation alps].heic').exists()
def test_clear_keeps_the_sidecar_data(cwd):
(cwd / 'a.txt').touch()
(cwd / 'b.txt').touch()
(cwd / '.ts').mkdir()
(cwd / '.ts' / 'a.txt.json').write_text('{"id": "x1", "description": "d", "tags": [{"title": "t"}]}')
OrvaniTag().write_metadata('a.txt', 'tagspaces', clear=True)
assert sidecar(cwd, 'a.txt') == {'id': 'x1', 'description': 'd', 'tags': []}
OrvaniTag().write_metadata('b.txt', 'tagspaces', clear=True)
assert not (cwd / '.ts' / 'b.txt.json').exists()
def test_broken_sidecar_is_a_scan_error(cwd):
(cwd / 'a.txt').touch()
(cwd / '.ts').mkdir()
(cwd / '.ts' / 'a.txt.json').write_text('{broken')
found, errors = OrvaniTag().scan('.')
assert found == {} and len(errors) == 1 and 'a.txt.json is no valid JSON' in errors[0]
def test_configured_types_narrow_plugins_for_any_type(cwd):
(cwd / orvani.CONFIG_FILE).write_text('[tagspaces]\ntypes = [".jpg"]\n')
for name in ['a.jpg', 'a.png']:
(cwd / name).touch()
OrvaniTag().add(name, '#x')
OrvaniTag().write_metadata('a.jpg', 'tagspaces')
with pytest.raises(UnsupportedType):
OrvaniTag().write_metadata('a.png', 'tagspaces')
def test_other_plugins_do_not_handle_folders(cwd, monkeypatch, capsys):
(cwd / 'sub.py').mkdir()
(cwd / 'sub.py' / 'x.py').write_text('# otag: #inner\n')
OrvaniTag().add('sub.py', '#x')
with pytest.raises(UnsupportedType, match="can't write to folders"):
OrvaniTag().write_metadata('sub.py', 'comment')
assert OrvaniTag().scan('.')[0] == {}
monkeypatch.setattr(sys, 'argv', ['otag', 'w', 'tagspaces', 'sub.py/'])
main()
assert (cwd / 'sub.py' / '.ts' / 'tsm.json').exists()