Files
MusicWorkshop/backend/tests/test_repair_preview.py
T
liumangmang be3c086975 chore: add project configs, backend repair services, docs, and code quality tooling
- Add pre-commit hooks (ruff, black, prettier) and ESLint/Prettier configs
- Add backend repair services (execution, orchestration, preview) with tests
- Add project documentation (CLAUDE.md, README.md, design specs and plans)
- Add MissingTagsInlinePanel component for exception handling
- Add pyproject.toml with ruff/black configuration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 15:49:37 +08:00

160 lines
5.5 KiB
Python

import os
import tempfile
import unittest
from pathlib import Path
os.environ['MUSIC_WORKSHOP_DB_PATH'] = str(
Path(tempfile.gettempdir()) / f'music_workshop_repair_preview_{next(tempfile._get_candidate_names())}.db'
)
from backend.app.exception_service import ExceptionService
from backend.app.repair_runner import RepairService
from backend.app.task_store import TaskStore
class RepairPreviewTests(unittest.TestCase):
def setUp(self):
self.db_path = Path(os.environ['MUSIC_WORKSHOP_DB_PATH'])
if self.db_path.exists():
self.db_path.unlink()
self.output_root = Path(tempfile.gettempdir()) / f'music-workshop-output-{next(tempfile._get_candidate_names())}'
self.task_store = TaskStore(self.db_path)
self.exception_service = ExceptionService(self.task_store)
self.repair_service = RepairService(
self.task_store,
self.exception_service,
matcher=None,
preprocessor=None,
task_stream=None
)
self.task = self.task_store.create_task_if_idle(
{
'input': '/tmp/input',
'output': str(self.output_root),
'trash': '/tmp/trash'
}
)
def test_save_and_organize_preview_returns_final_library_preview(self):
item = self._insert_metadata_exception(
original_tags_json={
'title': 'Raw Title',
'artist': 'Raw Artist',
'album': 'Raw Album',
'year': 1999
},
matched_metadata_json={
'title': 'Matched Title',
'artist': 'Matched Artist',
'album': 'Matched Album',
'album_artist': 'Matched Artist',
'track_number': 7
},
match_source='musicbrainz'
)
preview = self.repair_service.preview(
{
'exception_ids': [item['id']],
'action': 'save_and_organize',
'params': {'metadata_patch': {'title': 'Manual Title'}}
},
{'output': str(self.output_root), 'trash': '/tmp/trash'}
)
item_preview = preview['items'][0]['final_library_preview']
self.assertEqual(item_preview['metadata']['title'], 'Manual Title')
self.assertEqual(item_preview['metadata']['artist'], 'Matched Artist')
self.assertEqual(item_preview['metadata']['album_artist'], 'Matched Artist')
self.assertEqual(item_preview['metadata']['year'], 1999)
self.assertEqual(item_preview['metadata_sources']['title'], '手动编辑')
self.assertEqual(item_preview['metadata_sources']['artist'], 'MusicBrainz')
self.assertEqual(item_preview['metadata_sources']['year'], '原始标签')
self.assertEqual(item_preview['target_relative_path'], 'M/Matched Artist/Matched Album/07 - Manual Title.flac')
self.assertEqual(
item_preview['target_file_path'],
str(self.output_root / 'M/Matched Artist/Matched Album/07 - Manual Title.flac')
)
def test_save_and_organize_preview_rejects_missing_required_metadata(self):
item = self._insert_metadata_exception(
original_tags_json={'title': 'Raw Title'},
matched_metadata_json={'title': 'Matched Title'},
match_source='netease'
)
with self.assertRaisesRegex(ValueError, 'title、artist、album_artist'):
self.repair_service.preview(
{
'exception_ids': [item['id']],
'action': 'save_and_organize',
'params': {'metadata_patch': {}}
},
{'output': str(self.output_root), 'trash': '/tmp/trash'}
)
def test_save_and_organize_preview_manual_patch_overrides_candidate_and_raw(self):
item = self._insert_metadata_exception(
original_tags_json={
'title': 'Raw Title',
'artist': 'Raw Artist',
'album_artist': 'Raw Album Artist'
},
matched_metadata_json={
'title': 'Candidate Title',
'artist': 'Candidate Artist',
'album_artist': 'Candidate Album Artist',
'album': 'Candidate Album'
},
match_source='netease'
)
preview = self.repair_service.preview(
{
'exception_ids': [item['id']],
'action': 'save_and_organize',
'params': {
'metadata_patch': {
'artist': 'Manual Artist',
'album_artist': 'Manual Album Artist'
}
}
},
{'output': str(self.output_root), 'trash': '/tmp/trash'}
)
item_preview = preview['items'][0]['final_library_preview']
self.assertEqual(item_preview['metadata']['title'], 'Candidate Title')
self.assertEqual(item_preview['metadata']['artist'], 'Manual Artist')
self.assertEqual(item_preview['metadata']['album_artist'], 'Manual Album Artist')
self.assertEqual(item_preview['metadata_sources']['title'], '网易云')
self.assertEqual(item_preview['metadata_sources']['artist'], '手动编辑')
self.assertEqual(item_preview['metadata_sources']['album_artist'], '手动编辑')
def _insert_metadata_exception(self, **overrides):
filename = overrides.pop('filename', f'item-{next(tempfile._get_candidate_names())}.flac')
extension = Path(filename).suffix or '.flac'
return self.task_store.insert_task_item(
self.task['task_id'],
original_path=f'/tmp/input/{filename}',
current_file_path=f'/tmp/input/{filename}',
relative_path=f'Artist/Album/{filename}',
filename=filename,
extension=extension,
size_bytes=123456,
modified_at='2024-01-01T00:00:00Z',
local_cover=None,
local_lyric=None,
scan_status='queued',
scan_reason=None,
scan_message=None,
match_status='low_score',
match_reason='score_gap_too_small',
match_message='匹配候选分数过低',
**overrides
)
if __name__ == '__main__':
unittest.main()