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,171 @@
|
||||
"""Tests for the parse CLI command."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from vlm.cli import main
|
||||
|
||||
|
||||
class TestParseCommand:
|
||||
"""Tests for vlm parse command."""
|
||||
|
||||
def test_parse_without_inventory_v1_schema(self, tmp_path):
|
||||
"""Test parse without --inventory flag produces v1 schema."""
|
||||
# Create a simple inventory CSV (without metadata)
|
||||
inventory_csv = tmp_path / "inventory.csv"
|
||||
inventory_csv.write_text(
|
||||
"# vlm inventory\n"
|
||||
"path,filename,size_bytes,modified_timestamp,category,resolution,codec,duration_seconds,bitrate_kbps\n"
|
||||
"/library/movie/Matrix (1999).mkv,Matrix (1999).mkv,1000000,2024-01-01T00:00:00,movie,,,\n"
|
||||
"/library/series/Breaking Bad S01E01.mkv,Breaking Bad S01E01.mkv,500000,2024-01-02T00:00:00,series,,,\n"
|
||||
)
|
||||
|
||||
output_json = tmp_path / "identities.json"
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"--config",
|
||||
str(tmp_path / "config.yaml"),
|
||||
"parse",
|
||||
"--input",
|
||||
str(inventory_csv),
|
||||
"--output",
|
||||
str(output_json),
|
||||
],
|
||||
)
|
||||
|
||||
# Command should succeed
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Load output and verify v1 schema (no version field or version is 1.0)
|
||||
with open(output_json) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# v1 schema - no vlm_schema_version or it's "1.0"
|
||||
assert data.get("vlm_schema_version", "1.0") == "1.0"
|
||||
|
||||
# Verify no video_metadata in records
|
||||
if data.get("movies"):
|
||||
for movie in data["movies"]:
|
||||
assert "video_metadata" not in movie
|
||||
if data.get("series"):
|
||||
for series in data["series"]:
|
||||
assert "video_metadata" not in series
|
||||
|
||||
def test_parse_with_inventory_v2_schema(self, tmp_path):
|
||||
"""Test parse with --inventory flag produces v2 schema with embedded metadata."""
|
||||
# Create inventory CSV with full metadata
|
||||
inventory_csv = tmp_path / "inventory.csv"
|
||||
inventory_csv.write_text(
|
||||
"# vlm inventory\n"
|
||||
"path,filename,size_bytes,modified_timestamp,category,resolution,codec,duration_seconds,bitrate_kbps\n"
|
||||
"/library/movie/Matrix (1999).mkv,Matrix (1999).mkv,2000000000,2024-01-01T10:00:00+00:00,movie,1920x1080,h264,7200.5,5000\n"
|
||||
"/library/series/Breaking Bad S01E01.mkv,Breaking Bad S01E01.mkv,500000000,2024-01-02T15:00:00+00:00,series,1280x720,h264,2700.0,3000\n"
|
||||
)
|
||||
|
||||
output_json = tmp_path / "identities.json"
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"--config",
|
||||
str(tmp_path / "config.yaml"),
|
||||
"parse",
|
||||
"--input",
|
||||
str(inventory_csv),
|
||||
"--output",
|
||||
str(output_json),
|
||||
"--inventory",
|
||||
str(inventory_csv),
|
||||
],
|
||||
)
|
||||
|
||||
# Command should succeed
|
||||
assert result.exit_code == 0
|
||||
assert "Loading video metadata from:" in result.output
|
||||
assert "Loaded metadata for 2 files" in result.output
|
||||
|
||||
# Load output and verify v2 schema
|
||||
with open(output_json) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# v2 schema version
|
||||
assert data["vlm_schema_version"] == "2.0"
|
||||
|
||||
# Verify video_metadata embedded in movie record
|
||||
assert len(data["movies"]) == 1
|
||||
movie = data["movies"][0]
|
||||
assert "video_metadata" in movie
|
||||
assert movie["video_metadata"]["size_bytes"] == 2000000000
|
||||
assert movie["video_metadata"]["resolution"] == "1920x1080"
|
||||
assert movie["video_metadata"]["codec"] == "h264"
|
||||
assert movie["video_metadata"]["duration_seconds"] == 7200.5
|
||||
assert movie["video_metadata"]["bitrate_kbps"] == 5000
|
||||
|
||||
# Verify video_metadata embedded in series record
|
||||
assert len(data["series"]) == 1
|
||||
series = data["series"][0]
|
||||
assert "video_metadata" in series
|
||||
assert series["video_metadata"]["size_bytes"] == 500000000
|
||||
assert series["video_metadata"]["resolution"] == "1280x720"
|
||||
assert series["video_metadata"]["codec"] == "h264"
|
||||
assert series["video_metadata"]["duration_seconds"] == 2700.0
|
||||
assert series["video_metadata"]["bitrate_kbps"] == 3000
|
||||
|
||||
def test_parse_with_inventory_partial_metadata(self, tmp_path):
|
||||
"""Test parse with inventory that has partial metadata."""
|
||||
# Create inventory with some files having metadata, some not
|
||||
inventory_csv = tmp_path / "inventory.csv"
|
||||
inventory_csv.write_text(
|
||||
"# vlm inventory\n"
|
||||
"path,filename,size_bytes,modified_timestamp,category,resolution,codec,duration_seconds,bitrate_kbps\n"
|
||||
"/library/movie/Matrix (1999).mkv,Matrix (1999).mkv,2000000000,2024-01-01T10:00:00+00:00,movie,1920x1080,h264,,\n"
|
||||
"/library/movie/Inception (2010).mkv,Inception (2010).mkv,1500000000,2024-01-02T10:00:00+00:00,movie,,,\n"
|
||||
)
|
||||
|
||||
output_json = tmp_path / "identities.json"
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"--config",
|
||||
str(tmp_path / "config.yaml"),
|
||||
"parse",
|
||||
"--input",
|
||||
str(inventory_csv),
|
||||
"--output",
|
||||
str(output_json),
|
||||
"--inventory",
|
||||
str(inventory_csv),
|
||||
],
|
||||
)
|
||||
|
||||
# Command should succeed
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Load output and verify
|
||||
with open(output_json) as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert data["vlm_schema_version"] == "2.0"
|
||||
assert len(data["movies"]) == 2
|
||||
|
||||
# First movie has partial metadata
|
||||
movie1 = data["movies"][0]
|
||||
assert movie1["video_metadata"]["size_bytes"] == 2000000000
|
||||
assert movie1["video_metadata"]["resolution"] == "1920x1080"
|
||||
assert movie1["video_metadata"]["codec"] == "h264"
|
||||
assert movie1["video_metadata"]["duration_seconds"] is None
|
||||
assert movie1["video_metadata"]["bitrate_kbps"] is None
|
||||
|
||||
# Second movie has minimal metadata
|
||||
movie2 = data["movies"][1]
|
||||
assert movie2["video_metadata"]["size_bytes"] == 1500000000
|
||||
assert movie2["video_metadata"]["resolution"] is None
|
||||
assert movie2["video_metadata"]["codec"] is None
|
||||
Reference in New Issue
Block a user