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,252 @@
|
||||
"""Integration tests for duplicate resolution with embedded video metadata."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from vlm.io import load_identities_json, identities_to_analysis_input
|
||||
from vlm.analysis import detect_duplicates
|
||||
|
||||
|
||||
class TestDuplicateQualityWithMetadata:
|
||||
"""Test that embedded metadata enables accurate quality comparison."""
|
||||
|
||||
def test_duplicate_quality_comparison_with_v2_metadata(self, tmp_path):
|
||||
"""Test that v2 identities with embedded metadata produce accurate quality comparison."""
|
||||
# Create v2 identities.json with two duplicates having different quality
|
||||
identities_file = tmp_path / "identities.json"
|
||||
data = {
|
||||
"vlm_schema_version": "2.0",
|
||||
"metadata": {
|
||||
"generated": "2024-02-13T12:00:00",
|
||||
"source_inventory": "inventory.csv",
|
||||
"total_files": 2,
|
||||
},
|
||||
"movies": [
|
||||
{
|
||||
"path": "/library/movie/Matrix (1999) 1080p.mkv",
|
||||
"filename": "Matrix (1999) 1080p.mkv",
|
||||
"category": "movie",
|
||||
"title": "The Matrix",
|
||||
"year": 1999,
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
"video_metadata": {
|
||||
"size_bytes": 2000000000,
|
||||
"modified_timestamp": "2024-01-01T10:00:00+00:00",
|
||||
"resolution": "1920x1080",
|
||||
"codec": "h264",
|
||||
"duration_seconds": 7200.0,
|
||||
"bitrate_kbps": 5000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"path": "/library/movie/Matrix (1999) 720p.mkv",
|
||||
"filename": "Matrix (1999) 720p.mkv",
|
||||
"category": "movie",
|
||||
"title": "The Matrix",
|
||||
"year": 1999,
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
"video_metadata": {
|
||||
"size_bytes": 1000000000,
|
||||
"modified_timestamp": "2024-01-02T10:00:00+00:00",
|
||||
"resolution": "1280x720",
|
||||
"codec": "h264",
|
||||
"duration_seconds": 7200.0,
|
||||
"bitrate_kbps": 2500,
|
||||
},
|
||||
},
|
||||
],
|
||||
"series": [],
|
||||
"anime": [],
|
||||
"other": [],
|
||||
}
|
||||
|
||||
# Save identities
|
||||
with open(identities_file, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
# Load identities
|
||||
loaded = load_identities_json(identities_file)
|
||||
|
||||
# Convert to analysis input
|
||||
movie_identities, series_identities, video_files = identities_to_analysis_input(loaded)
|
||||
|
||||
# Create identity-file pairs
|
||||
identity_file_pairs = list(zip(movie_identities, video_files))
|
||||
|
||||
# Detect duplicates
|
||||
duplicates = detect_duplicates(identity_file_pairs)
|
||||
|
||||
# Verify we found 1 duplicate group
|
||||
assert len(duplicates) == 1
|
||||
duplicate_group = duplicates[0]
|
||||
|
||||
# Verify duplicate group has 2 files
|
||||
assert len(duplicate_group.files) == 2
|
||||
|
||||
# Verify quality comparison has actual metadata (not zeros/nulls)
|
||||
quality_comparison = duplicate_group.quality_comparison
|
||||
assert len(quality_comparison) == 2
|
||||
|
||||
# First file (1080p) should have correct metadata
|
||||
file1 = quality_comparison[0]
|
||||
assert file1["size_bytes"] == 2000000000
|
||||
assert file1["resolution"] == "1920x1080"
|
||||
assert file1["codec"] == "h264"
|
||||
assert file1["bitrate_kbps"] == 5000
|
||||
|
||||
# Second file (720p) should have correct metadata
|
||||
file2 = quality_comparison[1]
|
||||
assert file2["size_bytes"] == 1000000000
|
||||
assert file2["resolution"] == "1280x720"
|
||||
assert file2["codec"] == "h264"
|
||||
assert file2["bitrate_kbps"] == 2500
|
||||
|
||||
def test_duplicate_quality_comparison_with_v1_no_metadata(self, tmp_path):
|
||||
"""Test that v1 identities without metadata produce limited quality comparison."""
|
||||
# Create v1 identities.json without embedded metadata (backward compatibility)
|
||||
identities_file = tmp_path / "identities.json"
|
||||
data = {
|
||||
# No vlm_schema_version (v1)
|
||||
"metadata": {
|
||||
"generated": "2024-02-13T12:00:00",
|
||||
"source_inventory": "inventory.csv",
|
||||
"total_files": 2,
|
||||
},
|
||||
"movies": [
|
||||
{
|
||||
"path": "/library/movie/Inception (2010) 1080p.mkv",
|
||||
"filename": "Inception (2010) 1080p.mkv",
|
||||
"category": "movie",
|
||||
"title": "Inception",
|
||||
"year": 2010,
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
# No video_metadata
|
||||
},
|
||||
{
|
||||
"path": "/library/movie/Inception (2010) 720p.mkv",
|
||||
"filename": "Inception (2010) 720p.mkv",
|
||||
"category": "movie",
|
||||
"title": "Inception",
|
||||
"year": 2010,
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
# No video_metadata
|
||||
},
|
||||
],
|
||||
"series": [],
|
||||
"anime": [],
|
||||
"other": [],
|
||||
}
|
||||
|
||||
# Save identities
|
||||
with open(identities_file, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
# Load identities
|
||||
loaded = load_identities_json(identities_file)
|
||||
|
||||
# Convert to analysis input
|
||||
movie_identities, series_identities, video_files = identities_to_analysis_input(loaded)
|
||||
|
||||
# Create identity-file pairs
|
||||
identity_file_pairs = list(zip(movie_identities, video_files))
|
||||
|
||||
# Detect duplicates
|
||||
duplicates = detect_duplicates(identity_file_pairs)
|
||||
|
||||
# Verify we found 1 duplicate group
|
||||
assert len(duplicates) == 1
|
||||
duplicate_group = duplicates[0]
|
||||
|
||||
# Verify quality comparison exists but has limited data (v1 defaults)
|
||||
quality_comparison = duplicate_group.quality_comparison
|
||||
assert len(quality_comparison) == 2
|
||||
|
||||
# v1 files have size_bytes=0 and no resolution/codec
|
||||
for quality_info in quality_comparison:
|
||||
assert quality_info["size_bytes"] == 0
|
||||
assert "resolution" not in quality_info # None values are not included
|
||||
assert "codec" not in quality_info
|
||||
|
||||
def test_series_duplicate_quality_comparison_with_v2_metadata(self, tmp_path):
|
||||
"""Test series duplicates with embedded metadata."""
|
||||
identities_file = tmp_path / "identities.json"
|
||||
data = {
|
||||
"vlm_schema_version": "2.0",
|
||||
"metadata": {
|
||||
"generated": "2024-02-13T12:00:00",
|
||||
"source_inventory": "inventory.csv",
|
||||
"total_files": 2,
|
||||
},
|
||||
"movies": [],
|
||||
"series": [
|
||||
{
|
||||
"path": "/library/series/Breaking Bad S01E01 1080p.mkv",
|
||||
"filename": "Breaking Bad S01E01 1080p.mkv",
|
||||
"category": "series",
|
||||
"title": "Breaking Bad",
|
||||
"season": 1,
|
||||
"episodes": [1],
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
"video_metadata": {
|
||||
"size_bytes": 800000000,
|
||||
"modified_timestamp": "2024-01-01T10:00:00+00:00",
|
||||
"resolution": "1920x1080",
|
||||
"codec": "hevc",
|
||||
"duration_seconds": 2700.0,
|
||||
"bitrate_kbps": 4000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"path": "/library/series/Breaking Bad S01E01 720p.mkv",
|
||||
"filename": "Breaking Bad S01E01 720p.mkv",
|
||||
"category": "series",
|
||||
"title": "Breaking Bad",
|
||||
"season": 1,
|
||||
"episodes": [1],
|
||||
"confidence": 0.9,
|
||||
"needs_review": False,
|
||||
"video_metadata": {
|
||||
"size_bytes": 400000000,
|
||||
"modified_timestamp": "2024-01-02T10:00:00+00:00",
|
||||
"resolution": "1280x720",
|
||||
"codec": "h264",
|
||||
"duration_seconds": 2700.0,
|
||||
"bitrate_kbps": 2000,
|
||||
},
|
||||
},
|
||||
],
|
||||
"anime": [],
|
||||
"other": [],
|
||||
}
|
||||
|
||||
# Save and load
|
||||
with open(identities_file, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
loaded = load_identities_json(identities_file)
|
||||
movie_identities, series_identities, video_files = identities_to_analysis_input(loaded)
|
||||
|
||||
# Create identity-file pairs for series
|
||||
identity_file_pairs = list(zip(series_identities, video_files))
|
||||
|
||||
# Detect duplicates
|
||||
duplicates = detect_duplicates(identity_file_pairs)
|
||||
|
||||
# Verify duplicate detected
|
||||
assert len(duplicates) == 1
|
||||
duplicate_group = duplicates[0]
|
||||
assert len(duplicate_group.files) == 2
|
||||
|
||||
# Verify quality comparison has metadata
|
||||
quality_comparison = duplicate_group.quality_comparison
|
||||
assert quality_comparison[0]["resolution"] == "1920x1080"
|
||||
assert quality_comparison[0]["codec"] == "hevc"
|
||||
assert quality_comparison[1]["resolution"] == "1280x720"
|
||||
assert quality_comparison[1]["codec"] == "h264"
|
||||
Reference in New Issue
Block a user