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>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user