be3c086975
- 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>
160 lines
5.5 KiB
Python
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()
|