Fix identity reconstruction metadata loss with v2 schema
Implements identities.json v2 schema with embedded video metadata to fix duplicate resolution by quality, which previously failed due to VideoFile objects being reconstructed with hardcoded defaults (size_bytes=0, resolution=None, codec=None). Changes: - Add --inventory flag to vlm parse command to embed video metadata - Update _video_file_from_record() to extract embedded metadata if present - Add vlm_schema_version field to identities.json (v1.0 or v2.0) - Maintain backward compatibility with v1 files (no metadata) Schema v2 format: - Embeds video_metadata object in each record (movies/series) - Contains: size_bytes, modified_timestamp, resolution, codec, duration_seconds, bitrate_kbps - Enables accurate quality comparison during duplicate analysis Testing: - Added comprehensive unit tests for io.py functions - Added CLI integration tests for parse command - Added end-to-end tests for duplicate quality comparison - All 437 existing tests still pass (1 pre-existing failure in executor) Documentation: - Updated README.md with --inventory usage examples - Updated CLAUDE.md with schema versioning details - Added workflow examples showing metadata embedding This fix resolves the critical P0 issue where duplicate resolution by_quality strategy failed completely due to missing video metadata in reconstructed VideoFile objects. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.5
parent
1f55eab304
commit
065195b83b
@@ -0,0 +1,262 @@
|
||||
"""Tests for the I/O layer module."""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from vlm.io import (
|
||||
_video_file_from_record,
|
||||
load_identities_json,
|
||||
save_identities_json,
|
||||
)
|
||||
from vlm.models import VideoFile
|
||||
|
||||
|
||||
class TestVideoFileFromRecord:
|
||||
"""Tests for _video_file_from_record function."""
|
||||
|
||||
def test_video_file_from_record_with_metadata_v2(self):
|
||||
"""Test loading VideoFile from v2 record with embedded metadata."""
|
||||
record = {
|
||||
"path": "/library/movie/The Matrix (1999).mkv",
|
||||
"filename": "The Matrix (1999).mkv",
|
||||
"category": "movie",
|
||||
"title": "The Matrix",
|
||||
"year": 1999,
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
"video_metadata": {
|
||||
"size_bytes": 1234567890,
|
||||
"modified_timestamp": "2024-01-01T10:00:00+00:00",
|
||||
"resolution": "1920x1080",
|
||||
"codec": "h264",
|
||||
"duration_seconds": 7200.5,
|
||||
"bitrate_kbps": 5000,
|
||||
},
|
||||
}
|
||||
|
||||
vf = _video_file_from_record(record)
|
||||
|
||||
assert vf.path == Path("/library/movie/The Matrix (1999).mkv")
|
||||
assert vf.filename == "The Matrix (1999).mkv"
|
||||
assert vf.category == "movie"
|
||||
assert vf.size_bytes == 1234567890
|
||||
assert vf.resolution == "1920x1080"
|
||||
assert vf.codec == "h264"
|
||||
assert vf.duration_seconds == 7200.5
|
||||
assert vf.bitrate_kbps == 5000
|
||||
assert isinstance(vf.modified_timestamp, datetime)
|
||||
|
||||
def test_video_file_from_record_without_metadata_v1(self):
|
||||
"""Test loading VideoFile from v1 record without metadata (backward compatibility)."""
|
||||
record = {
|
||||
"path": "/library/movie/Inception (2010).mkv",
|
||||
"filename": "Inception (2010).mkv",
|
||||
"category": "movie",
|
||||
"title": "Inception",
|
||||
"year": 2010,
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
}
|
||||
|
||||
vf = _video_file_from_record(record)
|
||||
|
||||
assert vf.path == Path("/library/movie/Inception (2010).mkv")
|
||||
assert vf.filename == "Inception (2010).mkv"
|
||||
assert vf.category == "movie"
|
||||
# v1 defaults
|
||||
assert vf.size_bytes == 0
|
||||
assert vf.resolution is None
|
||||
assert vf.codec is None
|
||||
assert vf.duration_seconds is None
|
||||
assert vf.bitrate_kbps is None
|
||||
assert isinstance(vf.modified_timestamp, datetime)
|
||||
|
||||
def test_video_file_from_record_partial_metadata(self):
|
||||
"""Test loading VideoFile with partial metadata (some fields missing)."""
|
||||
record = {
|
||||
"path": "/library/series/Breaking Bad S01E01.mkv",
|
||||
"filename": "Breaking Bad S01E01.mkv",
|
||||
"category": "series",
|
||||
"title": "Breaking Bad",
|
||||
"season": 1,
|
||||
"episodes": [1],
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
"video_metadata": {
|
||||
"size_bytes": 987654321,
|
||||
"modified_timestamp": "2024-02-01T15:30:00+00:00",
|
||||
"resolution": "1280x720",
|
||||
# codec missing
|
||||
# duration_seconds missing
|
||||
# bitrate_kbps missing
|
||||
},
|
||||
}
|
||||
|
||||
vf = _video_file_from_record(record)
|
||||
|
||||
assert vf.size_bytes == 987654321
|
||||
assert vf.resolution == "1280x720"
|
||||
assert vf.codec is None
|
||||
assert vf.duration_seconds is None
|
||||
assert vf.bitrate_kbps is None
|
||||
|
||||
|
||||
class TestIdentitiesJsonVersioning:
|
||||
"""Tests for identities.json schema versioning."""
|
||||
|
||||
def test_load_identities_v2_with_metadata(self, tmp_path):
|
||||
"""Test loading v2 identities.json with video metadata."""
|
||||
identities_file = tmp_path / "identities.json"
|
||||
data = {
|
||||
"vlm_schema_version": "2.0",
|
||||
"metadata": {
|
||||
"generated": "2024-01-01T10:00:00",
|
||||
"source_inventory": "inventory.csv",
|
||||
"total_files": 1,
|
||||
},
|
||||
"movies": [
|
||||
{
|
||||
"path": "/library/movie/Test (2024).mkv",
|
||||
"filename": "Test (2024).mkv",
|
||||
"category": "movie",
|
||||
"title": "Test",
|
||||
"year": 2024,
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
"video_metadata": {
|
||||
"size_bytes": 1000000,
|
||||
"modified_timestamp": "2024-01-01T00:00:00+00:00",
|
||||
"resolution": "1920x1080",
|
||||
"codec": "h264",
|
||||
"duration_seconds": 3600.0,
|
||||
"bitrate_kbps": 2500,
|
||||
},
|
||||
}
|
||||
],
|
||||
"series": [],
|
||||
"anime": [],
|
||||
"other": [],
|
||||
}
|
||||
|
||||
save_identities_json(data, identities_file)
|
||||
loaded = load_identities_json(identities_file)
|
||||
|
||||
assert loaded["vlm_schema_version"] == "2.0"
|
||||
assert len(loaded["movies"]) == 1
|
||||
assert "video_metadata" in loaded["movies"][0]
|
||||
assert loaded["movies"][0]["video_metadata"]["size_bytes"] == 1000000
|
||||
|
||||
def test_load_identities_v1_backward_compatibility(self, tmp_path):
|
||||
"""Test loading v1 identities.json without version field (backward compatibility)."""
|
||||
identities_file = tmp_path / "identities.json"
|
||||
data = {
|
||||
# No vlm_schema_version field (v1)
|
||||
"metadata": {
|
||||
"generated": "2024-01-01T10:00:00",
|
||||
"source_inventory": "inventory.csv",
|
||||
"total_files": 1,
|
||||
},
|
||||
"movies": [
|
||||
{
|
||||
"path": "/library/movie/Old (2023).mkv",
|
||||
"filename": "Old (2023).mkv",
|
||||
"category": "movie",
|
||||
"title": "Old",
|
||||
"year": 2023,
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
# No video_metadata field
|
||||
}
|
||||
],
|
||||
"series": [],
|
||||
"anime": [],
|
||||
"other": [],
|
||||
}
|
||||
|
||||
save_identities_json(data, identities_file)
|
||||
loaded = load_identities_json(identities_file)
|
||||
|
||||
# v1 files don't have version field
|
||||
assert "vlm_schema_version" not in loaded or loaded.get("vlm_schema_version") is None
|
||||
assert len(loaded["movies"]) == 1
|
||||
assert "video_metadata" not in loaded["movies"][0]
|
||||
|
||||
|
||||
class TestIdentitiesJsonRoundTrip:
|
||||
"""Tests for identities.json save/load round-trip."""
|
||||
|
||||
def test_save_and_load_v2_identities(self, tmp_path):
|
||||
"""Test saving and loading v2 identities with metadata."""
|
||||
identities_file = tmp_path / "identities_v2.json"
|
||||
original_data = {
|
||||
"vlm_schema_version": "2.0",
|
||||
"metadata": {
|
||||
"generated": "2024-02-13T12:00:00",
|
||||
"source_inventory": "test_inventory.csv",
|
||||
"total_files": 2,
|
||||
},
|
||||
"movies": [
|
||||
{
|
||||
"path": "/library/movie/Movie1 (2024).mkv",
|
||||
"filename": "Movie1 (2024).mkv",
|
||||
"category": "movie",
|
||||
"title": "Movie1",
|
||||
"year": 2024,
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
"video_metadata": {
|
||||
"size_bytes": 2000000000,
|
||||
"modified_timestamp": "2024-02-10T08:00:00+00:00",
|
||||
"resolution": "3840x2160",
|
||||
"codec": "hevc",
|
||||
"duration_seconds": 7200.0,
|
||||
"bitrate_kbps": 8000,
|
||||
},
|
||||
}
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"path": "/library/series/Show S01E01.mkv",
|
||||
"filename": "Show S01E01.mkv",
|
||||
"category": "series",
|
||||
"title": "Show",
|
||||
"season": 1,
|
||||
"episodes": [1],
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
"video_metadata": {
|
||||
"size_bytes": 500000000,
|
||||
"modified_timestamp": "2024-02-11T10:00:00+00:00",
|
||||
"resolution": "1920x1080",
|
||||
"codec": "h264",
|
||||
"duration_seconds": 2700.0,
|
||||
"bitrate_kbps": 3000,
|
||||
},
|
||||
}
|
||||
],
|
||||
"anime": [],
|
||||
"other": [],
|
||||
}
|
||||
|
||||
save_identities_json(original_data, identities_file)
|
||||
loaded_data = load_identities_json(identities_file)
|
||||
|
||||
# Verify structure
|
||||
assert loaded_data["vlm_schema_version"] == "2.0"
|
||||
assert len(loaded_data["movies"]) == 1
|
||||
assert len(loaded_data["series"]) == 1
|
||||
|
||||
# Verify movie metadata preserved
|
||||
movie = loaded_data["movies"][0]
|
||||
assert movie["video_metadata"]["size_bytes"] == 2000000000
|
||||
assert movie["video_metadata"]["resolution"] == "3840x2160"
|
||||
assert movie["video_metadata"]["codec"] == "hevc"
|
||||
|
||||
# Verify series metadata preserved
|
||||
series = loaded_data["series"][0]
|
||||
assert series["video_metadata"]["size_bytes"] == 500000000
|
||||
assert series["video_metadata"]["resolution"] == "1920x1080"
|
||||
assert series["video_metadata"]["codec"] == "h264"
|
||||
Reference in New Issue
Block a user