Build and publish container / build (pull_request) Successful in 7m41s
Rockbox applies the offset a ReplayGain tag carries but has no loudness analysis of its own, so an untagged mirror plays every album at whatever level it was mastered to. Albums are measured with rsgain once their tracks are in place, album gain and track gain both, leaving the device to choose between them. An album is re-measured as a whole whenever it gains, loses or replaces a track, because album gain is a property of all of its tracks and one new track makes the value stored on every sibling wrong. rsgain runs with --preserve-mtimes. Staleness here is an mtime comparison and tagging rewrites the file, so without it every levelled track would look newer than its source and the next pass would re-encode the whole library. Whether a file has already been levelled is decided by walking its ID3v2 frame headers and seeking over the bodies. Cover art is embedded in every mirror file, so reading the tag whole would turn an idle pass into a full read of the library. A missing rsgain is reported and then left alone rather than failing the pass: the mirror is still correct audio in the right place. Two existing tests move with the change. ffprobe's csv writer renders the ReplayGain side data as a trailing empty field, and a copied MP3 now differs from its source in the container while carrying identical audio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 lines
3.7 KiB
Python
138 lines
3.7 KiB
Python
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
# Ensure the project root is on sys.path when running tests.
|
|
ROOT = os.path.dirname(os.path.dirname(__file__))
|
|
if ROOT not in sys.path:
|
|
sys.path.insert(0, ROOT)
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def require_ffmpeg():
|
|
"""The tests exercise real encodes; there is little point faking them."""
|
|
for tool in ("ffmpeg", "ffprobe"):
|
|
if shutil.which(tool) is None:
|
|
pytest.skip(f"{tool} is not on PATH", allow_module_level=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def require_rsgain():
|
|
"""Skip a test that measures loudness for real rather than faking it."""
|
|
if shutil.which("rsgain") is None:
|
|
pytest.skip("rsgain is not on PATH")
|
|
|
|
|
|
@pytest.fixture
|
|
def tight_umask():
|
|
"""Run a test under a umask that would otherwise make the mirror private."""
|
|
previous = os.umask(0o077)
|
|
yield
|
|
os.umask(previous)
|
|
|
|
|
|
@pytest.fixture
|
|
def owner_hostile_umask():
|
|
"""Return a callable applying a umask that masks off the owner's read bit.
|
|
|
|
Unusual, but it is what produces a mirror tree of mode 0300 -- writable and
|
|
enterable, unreadable to the very process that built it. Applied on demand
|
|
rather than for the whole test, because the source library is built by
|
|
something else entirely and the same umask would make the test's own
|
|
fixtures unreadable before the run under test even started.
|
|
"""
|
|
previous = os.umask(0o022)
|
|
yield lambda: os.umask(0o477)
|
|
os.umask(previous)
|
|
|
|
|
|
@pytest.fixture
|
|
def make_flac():
|
|
"""Return a factory writing a short tagged FLAC file."""
|
|
|
|
def factory(
|
|
path,
|
|
title="Test Title",
|
|
artist="Test Artist",
|
|
album="Test Album",
|
|
seconds=1,
|
|
gain=0,
|
|
):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
subprocess.run(
|
|
[
|
|
"ffmpeg",
|
|
"-nostdin",
|
|
"-hide_banner",
|
|
"-loglevel",
|
|
"error",
|
|
"-y",
|
|
"-f",
|
|
"lavfi",
|
|
"-i",
|
|
f"sine=frequency=440:duration={seconds}",
|
|
# Quieter or louder than the default, for tests that need two
|
|
# tracks at different levels.
|
|
*(["-af", f"volume={gain}dB"] if gain else []),
|
|
"-metadata",
|
|
f"title={title}",
|
|
"-metadata",
|
|
f"artist={artist}",
|
|
"-metadata",
|
|
f"album={album}",
|
|
"-metadata",
|
|
"track=3",
|
|
str(path),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
return path
|
|
|
|
return factory
|
|
|
|
|
|
@pytest.fixture
|
|
def make_cover():
|
|
"""Return a factory writing a small JPEG beside an album's tracks."""
|
|
|
|
def factory(path):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
subprocess.run(
|
|
["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y",
|
|
"-f", "lavfi", "-i", "color=c=red:s=64x64:d=1", "-frames:v", "1", str(path)],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
return path
|
|
|
|
return factory
|
|
|
|
|
|
@pytest.fixture
|
|
def probe_tag():
|
|
"""Return a helper reading a single metadata tag from a file."""
|
|
|
|
def reader(path, tag):
|
|
completed = subprocess.run(
|
|
[
|
|
"ffprobe",
|
|
"-v",
|
|
"error",
|
|
"-show_entries",
|
|
f"format_tags={tag}",
|
|
"-of",
|
|
"default=noprint_wrappers=1:nokey=1",
|
|
str(path),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return completed.stdout.strip()
|
|
|
|
return reader
|