signal-bot/tests/test_media_tracks.py
Krzysztof kuhy Rudnicki 9d546fef9a feat: import a folder of photos without importing it twice
tool/tagger.py did not run at all: 48957a1 renamed pack.default_dir to
default_dirs and missed the caller, so the tagger raised ImportError at
module scope through two releases. Nothing could have caught it -- tool/ is
outside --cov and mypy ran with [signal_bot, fake_signal]. Both are widened
here, and tests/test_tool_imports.py imports every script under tool/.

--import was a filename check and shutil.copy2. Pointed at a phone export
that overlaps the album it re-imported the same photo under whatever name
the exporter used that time. It now identifies files by magic bytes, strips
them through the same media_strip path !add_photo uses, and dedups on exact
bytes twice: the source hash, then -- only for what survives -- the hash
after stripping. sources.json records both, so a second run on the same
folder is a no-op. What the album refuses is moved to _skipped/<reason>/
beside the source, never deleted.

Two things that made "strip on import" impossible until now:

media_tags claims -all= zeroes the QuickTime date tags and allows them by
value. It does not; measured, it leaves CreateDate at its real value, and
every mp4 was being refused. -time:all= is what zeroes them.

And a tag allowlist cannot see the worst class at all. IMG_6924.mp4 carries
mebx timed-metadata holding per-frame face bounding boxes and a face id,
and those survive a complete tag strip because they live in mdat as opaque
samples. media_tracks.py drops the track with ffmpeg and verifies with
ffprobe that only video and audio survived. This is not the ffmpeg mistake
media_strip warns about: that was ffmpeg replacing exiftool for tags, this
is track removal with exiftool still doing every tag, and the gate is a
positive read-back rather than an exit code.

The roster was write-once. The setup form only appeared on an empty album,
Album.setup overwrote the file whole, and keys were positional (p{len+1}),
so inserting somebody renumbered everyone after them and repointed every
tags.json entry at the wrong person. Keys now come from the highest number
in use, the form merges, and a person can be added mid-session.

--untagged filters the page and never the server's list. /media/{index} and
/api/tag both index the full album, so a shorter list server-side would
show item i of the album while tagging item i of the filtered set.

Measured on the live album: 174 source files -> 1 not media, 82 already in
pack, 24 duplicate in batch, 67 imported, and re-running imports nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBrE1b9HvFw3xvG9bJiDFt
2026-08-31 13:22:57 +02:00

143 lines
5.4 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""Dropping metadata tracks exiftool cannot see, and proving they are gone."""
from __future__ import annotations
from pathlib import Path
import pytest
from signal_bot import media_tracks
from signal_bot.external import Result
class FakeRun:
"""Stands in for signal_bot.external.run, one canned answer per binary."""
def __init__(self, ffmpeg: Result, ffprobe: Result | None = None) -> None:
self.ffmpeg = ffmpeg
self.ffprobe = ffprobe or Result(ok=True, stdout="video\naudio\n", stderr="")
self.commands: list[list[str]] = []
def __call__(self, command: list[str], *, timeout: float = 0) -> Result:
"""Answer as the binary the command names, and write the output file."""
self.commands.append(list(command))
if "ffprobe" in command[0]:
return self.ffprobe
if self.ffmpeg.ok:
# The real ffmpeg leaves a file behind, and the code checks for it.
Path(command[-1]).write_bytes(b"remuxed")
return self.ffmpeg
@pytest.fixture
def found(monkeypatch: pytest.MonkeyPatch) -> None:
"""Both binaries present, so tests exercise the logic and not locate()."""
monkeypatch.setattr(media_tracks, "locate", lambda binary: f"/usr/bin/{binary}")
def _use(monkeypatch: pytest.MonkeyPatch, runner: FakeRun) -> None:
monkeypatch.setattr(media_tracks, "run", runner)
def test_a_clean_remux_returns_the_new_bytes(monkeypatch: pytest.MonkeyPatch, found: None) -> None:
_use(monkeypatch, FakeRun(Result(ok=True, stdout="", stderr="")))
assert media_tracks.flatten(b"video", ".mp4") == b"remuxed"
def test_audio_is_mapped_optionally(monkeypatch: pytest.MonkeyPatch, found: None) -> None:
# "0:a?" and not "0:a": a clip with no sound is still a clip.
runner = FakeRun(Result(ok=True, stdout="", stderr=""))
_use(monkeypatch, runner)
media_tracks.flatten(b"video", ".mp4")
assert "0:a?" in runner.commands[0]
def test_a_missing_ffmpeg_refuses_rather_than_storing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(media_tracks, "locate", lambda binary: None)
assert media_tracks.flatten(b"video", ".mp4") is None
def test_a_missing_ffprobe_refuses(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
media_tracks, "locate", lambda binary: None if binary == "ffprobe" else "/usr/bin/ffmpeg"
)
assert media_tracks.flatten(b"video", ".mp4") is None
def test_an_ffmpeg_failure_refuses(monkeypatch: pytest.MonkeyPatch, found: None) -> None:
_use(monkeypatch, FakeRun(Result(ok=False, stdout="", stderr="boom")))
assert media_tracks.flatten(b"video", ".mp4") is None
def test_a_surviving_data_stream_refuses(monkeypatch: pytest.MonkeyPatch, found: None) -> None:
# The whole point: a metadata track that outlived the re-mux must not be
# stored, because no tag allowlist downstream can see it.
_use(
monkeypatch,
FakeRun(
Result(ok=True, stdout="", stderr=""),
Result(ok=True, stdout="video\naudio\ndata\n", stderr=""),
),
)
assert media_tracks.flatten(b"video", ".mp4") is None
def test_an_unreadable_result_refuses(monkeypatch: pytest.MonkeyPatch, found: None) -> None:
_use(
monkeypatch,
FakeRun(Result(ok=True, stdout="", stderr=""), Result(ok=False, stdout="", stderr="x")),
)
assert media_tracks.flatten(b"video", ".mp4") is None
def test_a_file_ffprobe_describes_as_nothing_refuses(
monkeypatch: pytest.MonkeyPatch, found: None
) -> None:
# Empty is a failure, not "no bad streams": nobody can vouch for it.
_use(
monkeypatch,
FakeRun(Result(ok=True, stdout="", stderr=""), Result(ok=True, stdout="\n", stderr="")),
)
assert media_tracks.flatten(b"video", ".mp4") is None
def test_streams_strips_the_trailing_comma_ffprobe_emits(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
_use(monkeypatch, FakeRun(Result(ok=True, stdout="", stderr="")))
assert media_tracks.streams(tmp_path / "x.mp4", "/usr/bin/ffprobe") == ("video", "audio")
def test_streams_reports_failure_as_none(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
_use(
monkeypatch,
FakeRun(Result(ok=True, stdout="", stderr=""), Result(ok=False, stdout="", stderr="")),
)
assert media_tracks.streams(tmp_path / "x.mp4", "/usr/bin/ffprobe") is None
def test_locate_prefers_path(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(media_tracks.shutil, "which", lambda binary: f"/somewhere/{binary}")
assert media_tracks.locate("ffmpeg") == "/somewhere/ffmpeg"
def test_locate_falls_back_to_the_known_directories(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# A systemd unit's PATH is "/usr/local/bin:/usr/bin" and nothing else.
monkeypatch.setattr(media_tracks.shutil, "which", lambda binary: None)
(tmp_path / "ffmpeg").write_text("#!/bin/sh\n", encoding="utf-8")
monkeypatch.setattr(media_tracks, "FALLBACK_DIRS", (str(tmp_path),))
assert media_tracks.locate("ffmpeg") == str(tmp_path / "ffmpeg")
def test_locate_reports_nothing_when_it_is_not_installed(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(media_tracks.shutil, "which", lambda binary: None)
monkeypatch.setattr(media_tracks, "FALLBACK_DIRS", (str(tmp_path),))
assert media_tracks.locate("ffmpeg") is None