Spaces:
Running
Running
File size: 20,766 Bytes
5f400f8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | """/mediainfo: reading a file and laying the report out."""
import pytest
from bot import mediainfo as mi
from bot import telegraph
from .conftest import needs_ffmpeg
TEXT = """General
Complete name : /tmp/work/minfo1_2/probe.mkv
Format : Matroska
Conformance errors : 2
0x8538067 : Yes
General compliance : Element size 1 is more than maximal permitted size 2 (offset 0x34)
Matroska : Yes
CRC_Error_Pos : 943
Writing library : Lavf
Video
ID : 1
Format : AVC
Title : \x07bell\x1b[0m here
Menu
00:00:00.000 : Intro
Plain line without a separator
Empty value :
"""
FFPROBE = {
"format": {
"format_long_name": "Matroska / WebM", "duration": "1441.419",
"tags": {"title": "Show", "ENCODER": "libebml", "creation_time": "2026-01-01T00:00:00Z", "WEIRD_KEY": "x"},
},
"streams": [
{"index": 0, "codec_type": "video", "codec_name": "hevc", "codec_long_name": "H.265 / HEVC", "profile": "Main 10",
"level": 123, "width": 1920, "height": 1080, "avg_frame_rate": "24000/1001", "pix_fmt": "yuv420p10le",
"bits_per_raw_sample": "10", "display_aspect_ratio": "16:9", "color_space": "bt709",
"disposition": {"default": 1}, "tags": {"BPS": "3200000", "DURATION": "00:24:01.419000000"}},
{"index": 1, "codec_type": "audio", "codec_name": "aac", "profile": "LC", "channels": 2, "channel_layout": "stereo",
"sample_rate": "48000", "disposition": {"default": 1, "forced": 0}, "tags": {"language": "jpn", "title": "Japanese"}},
{"index": 2, "codec_type": "audio", "codec_name": "ac3", "channels": 1, "sample_rate": "44100", "tags": {"language": "hin"}},
{"index": 3, "codec_type": "subtitle", "codec_name": "ass", "disposition": {"forced": 1}, "tags": {"language": "eng"}},
{"index": 4, "codec_type": "attachment", "codec_name": "ttf", "tags": {"filename": "font.ttf"}},
],
"chapters": [{"start_time": "0.000000", "tags": {"title": "Intro"}}, {"start_time": "90.5", "tags": {"title": "Main"}}],
}
def lines_of(report, title):
return dict(report.section(title).lines)
def report_of(*sections, **kw):
return mi.Report(sections=list(sections), engine=kw.pop("engine", "MediaInfo"), **kw)
# ---------------------------------------------------------------------------
# MediaInfo's number formats
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("num, text", [
(0, "0 Bytes"), (999, "999 Bytes"), (1024, "1.00 KiB"), (71 * 1024, "71.0 KiB"), (93550, "91.4 KiB"),
(845 * 1024 ** 2, "845 MiB"), (int(1.46 * 1024 ** 3), "1.46 GiB"), (int(1023.9 * 1024), "1.00 MiB"), (5 * 1024 ** 4, "5.00 TiB"),
])
def test_sizes_are_written_like_mediainfo_does(num, text):
assert mi.mi_size(num) == text
@pytest.mark.parametrize("bps, text", [
(62, "62 b/s"), (370_000, "370 kb/s"), (1_936_000, "1 936 kb/s"), (9_999_000, "9 999 kb/s"),
(12_340_000, "12.3 Mb/s"), (150_000_000, "150 Mb/s"),
])
def test_bit_rates_are_written_like_mediainfo_does(bps, text):
assert mi.mi_rate(bps) == text
@pytest.mark.parametrize("ms, text", [
(2023, "2 s 23 ms"), (1_441_000, "24 min 1 s"), (5_040_000, "1 h 24 min"), (0, "0 s 0 ms"), (59_999.6, "1 min 0 s"),
])
def test_durations_are_written_like_mediainfo_does(ms, text):
assert mi.mi_duration(ms) == text
# ---------------------------------------------------------------------------
# reading MediaInfo's text
# ---------------------------------------------------------------------------
def test_the_text_is_split_into_sections_of_labels_and_values():
sections = mi.parse_text(TEXT)
assert [s.title for s in sections] == ["General", "Video", "Menu"]
general = sections[0].lines
assert general[0] == ("Complete name", "/tmp/work/minfo1_2/probe.mkv")
assert (" 0x8538067", "Yes") in general and (" General compliance", general[4][1]) == general[4] # nesting is kept
video = dict(sections[1].lines)
assert video["Title"] == "bell[0m here" # control characters are gone
menu = sections[2].lines
assert menu[0] == ("00:00:00.000", "Intro") and ("", "Plain line without a separator") in menu and ("Empty value", "") in menu
def test_windows_line_endings_and_blank_runs_are_fine():
sections = mi.parse_text("General\r\nFormat : MPEG-4\r\n\r\n\r\nAudio\r\nFormat : AAC\r\n")
assert [(s.title, s.lines) for s in sections] == [("General", [("Format", "MPEG-4")]), ("Audio", [("Format", "AAC")])]
# ---------------------------------------------------------------------------
# the General section of a file we only have the start of
# ---------------------------------------------------------------------------
def test_a_cut_short_file_gets_its_real_name_and_size_and_no_false_alarms():
report = report_of(*mi.parse_text(TEXT), truncated=True)
mi.fix_general(report, "Show 01.mkv", 700 * 1024 ** 2, 1_440_000.0)
general = report.section("General")
assert [label for label, _ in general.lines] == ["Complete name", "Format", "Writing library", "File size", "Overall bit rate"]
values = dict(general.lines)
assert values["Complete name"] == "Show 01.mkv" and values["File size"] == "700 MiB"
assert values["Overall bit rate"] == "4 078 kb/s" # 700 MiB over 24 minutes
def test_a_cut_short_file_with_no_duration_loses_a_bit_rate_that_would_be_wrong():
report = report_of(*mi.parse_text("General\nFormat : X\nOverall bit rate : 11.9 kb/s\nOverall bit rate mode : Constant\n"), truncated=True)
mi.fix_general(report, "a.bin", 5000, None)
assert [label for label, _ in report.section("General").lines] == ["Complete name", "Format", "Overall bit rate mode", "File size"]
def test_a_whole_file_keeps_everything_mediainfo_says_about_it():
report = report_of(*mi.parse_text(TEXT))
mi.fix_general(report, "x.mkv", 1000, 1000.0)
labels = [label for label, _ in report.section("General").lines]
assert "Conformance errors" in labels and "CRC_Error_Pos" in labels and " 0x8538067" in labels # real problems stay visible
assert dict(report.section("General").lines)["File size"] == "1000 Bytes" and "Overall bit rate" not in labels
def test_a_report_with_no_general_section_gets_one():
report = report_of(truncated=False, engine="")
mi.fix_general(report, "mystery.bin", 2048, None)
assert report.sections[0].title == "General" and report.sections[0].lines == [("Complete name", "mystery.bin"), ("File size", "2.00 KiB")]
# ---------------------------------------------------------------------------
# the report
# ---------------------------------------------------------------------------
def test_the_summary_line_counts_the_streams():
general = mi.Section("General", [("Format", "Matroska"), ("Duration", "24 min 1 s")])
streams = [mi.Section("Video"), mi.Section("Audio #1"), mi.Section("Audio #2"), mi.Section("Text #1"), mi.Section("Menu")]
assert report_of(general, *streams).summary() == "Matroska · 24 min 1 s · 1 video · 2 audio · 1 subtitle"
assert report_of(general).summary() == "Matroska · 24 min 1 s"
assert report_of().summary() == ""
def test_only_real_streams_count_as_streams():
assert not report_of(mi.Section("General"), mi.Section("Menu"), mi.Section("Telegram")).has_streams
assert report_of(mi.Section("General"), mi.Section("Image")).has_streams
assert report_of(mi.Section("General"), mi.Section("Text #12")).has_streams
def test_the_telegram_section_lists_what_telegram_knows_and_skips_the_rest():
full = mi.telegram_section("Show 01.mkv", 93550, mime="video/x-matroska", sent_as="Video", sent_on="2026-10-21 09:00:00 UTC",
dc="4", caption="[HDR] Dual")
assert full.title == "Telegram" and full.lines == [
("File name", "Show 01.mkv"), ("File size", "91.4 KiB (93 550 bytes)"), ("MIME type", "video/x-matroska"),
("Sent as", "Video"), ("Sent on", "2026-10-21 09:00:00 UTC"), ("Data center", "4"), ("Caption", "[HDR] Dual"),
]
assert mi.telegram_section("x", 0).lines == [("File name", "x")]
# ---------------------------------------------------------------------------
# laying a section out
# ---------------------------------------------------------------------------
def test_values_line_up_after_the_labels():
section = mi.Section("Video", [("Format", "AVC"), ("Duration", "2 s"), ("ID", "1")])
assert mi.render_section(section) == "Format : AVC\nDuration : 2 s\nID : 1"
def test_long_values_wrap_under_the_start_of_the_value():
section = mi.Section("Video", [("Encoding settings", " / ".join(f"option{i}={i}" for i in range(40))), ("ID", "1")])
lines = mi.render_section(section, width=64).splitlines()
assert lines[0].startswith("Encoding settings : option0=0 / ") and len(lines) > 3
indent = " " * len("Encoding settings : ")
assert all(line.startswith(indent) for line in lines[1:-1]) and lines[-1].startswith("ID")
assert all(len(line) <= 64 + 8 for line in lines) # a wrapped value gets at least MIN_VALUE columns
def test_a_very_long_label_does_not_push_every_value_to_the_right():
section = mi.Section("Video", [("ID", "1"), ("Format settings, Reference frames", "1 frame"), ("Format", "AVC")])
lines = mi.render_section(section).splitlines()
assert lines == ["ID : 1", "Format settings, Reference frames : 1 frame", "Format : AVC"]
def test_unbroken_text_is_still_wrapped_and_lines_end_cleanly():
section = mi.Section("Menu", [("", "x" * 150), ("Blank", ""), ("Name", "y" * 200)])
lines = mi.render_section(section, width=64).splitlines()
assert max(len(line) for line in lines) <= 64 + 8 and all(line == line.rstrip() for line in lines)
assert "".join(line.strip() for line in lines).count("y") == 200
def test_values_can_be_shortened_to_fit():
section = mi.Section("General", [("Comment", "z" * 500)])
line = mi.render_section(section, max_value=50)
assert line.endswith("…") and len(line.split(" : ")[1]) == 50
# ---------------------------------------------------------------------------
# the Telegraph page
# ---------------------------------------------------------------------------
def test_every_section_becomes_a_heading_and_a_block_and_the_notes_come_last():
report = report_of(mi.Section("General", [("Format", "Matroska")]), mi.Section("Telegram", [("File name", "a.mkv")]), truncated=True)
nodes = mi.to_nodes(report)
assert [n["tag"] for n in nodes] == ["h4", "pre", "h4", "pre", "p", "p"]
assert nodes[0]["children"] == ["General"] and nodes[1]["children"] == ["Format : Matroska"]
notes = [n["children"][0]["children"][0] for n in nodes if n["tag"] == "p"]
assert "Only the start of the file was read" in notes[0] and notes[1] == "Made by the bot with MediaInfo."
def test_extra_notes_are_kept_and_an_unknown_engine_is_not_named():
report = report_of(mi.Section("General", [("Format", "x")]), engine="", notes=["No tracks could be read."])
notes = [n["children"][0]["children"][0] for n in mi.to_nodes(report) if n["tag"] == "p"]
assert notes == ["No tracks could be read.", "Made by the bot."]
def test_a_page_that_is_too_big_has_its_long_values_shortened_first():
big = mi.Section("Video", [(f"Setting {i}", "v" * 600) for i in range(60)])
report = report_of(mi.Section("General", [("Format", "x")]), big, mi.Section("Telegram", [("File name", "a")]))
assert telegraph.content_size(mi.to_nodes(report, limit=10 ** 9)) > 30_000
nodes = mi.to_nodes(report, limit=30_000)
assert telegraph.content_size(nodes) <= 30_000
titles = [n["children"][0] for n in nodes if n["tag"] == "h4"]
assert titles == ["General", "Video", "Telegram"] # nothing dropped, only shortened
assert "…" in nodes[3]["children"][0]
def test_a_file_with_hundreds_of_tracks_drops_the_last_sections_but_keeps_the_ends():
sections = [mi.Section("General", [("Format", "Matroska")])]
sections += [mi.Section(f"Text #{i}", [("Format", "UTF-8"), ("Title", "t" * 80), ("Language", "English")]) for i in range(1, 301)]
sections.append(mi.Section("Telegram", [("File name", "big.mkv")]))
nodes = mi.to_nodes(report_of(*sections), limit=12_000)
assert telegraph.content_size(nodes) <= 12_000
titles = [n["children"][0] for n in nodes if n["tag"] == "h4"]
assert titles[0] == "General" and titles[-1] == "Telegram" and 3 < len(titles) < 302
notes = [n["children"][0]["children"][0] for n in nodes if n["tag"] == "p"]
assert any("page size limit" in note for note in notes)
def test_the_default_limit_is_what_telegraph_accepts():
report = report_of(mi.Section("General", [("Format", "x")]))
assert telegraph.content_size(mi.to_nodes(report)) <= telegraph.CONTENT_LIMIT < 64 * 1024
def test_the_plain_text_version_has_everything_the_page_has():
report = report_of(mi.Section("General", [("Format", "Matroska"), ("Duration", "2 s")]), mi.Section("Telegram", [("File name", "a.mkv")]), truncated=True)
text = mi.render_text(report)
assert text.startswith("General\nFormat : Matroska\nDuration : 2 s\n\nTelegram\nFile name : a.mkv\n")
assert "Only the start of the file was read" in text and text.rstrip().endswith("Made by the bot with MediaInfo.") and text.endswith("\n")
# ---------------------------------------------------------------------------
# the ffprobe fallback
# ---------------------------------------------------------------------------
def test_ffprobe_output_becomes_mediainfo_style_sections():
sections = mi.ffprobe_sections(FFPROBE, "Show 01.mkv", 700 * 1024 ** 2)
assert [s.title for s in sections] == ["General", "Video", "Audio #1", "Audio #2", "Text", "Menu"]
report = report_of(*sections)
general = lines_of(report, "General")
assert general["Complete name"] == "Show 01.mkv" and general["Format"] == "Matroska / WebM" and general["File size"] == "700 MiB"
assert general["Duration"] == "24 min 1 s" and general["Overall bit rate"] == "4 074 kb/s"
assert general["Title"] == "Show" and general["Writing application"] == "libebml" and general["Encoded date"] == "2026-01-01T00:00:00Z"
assert general["Weird Key"] == "x" and general["Attachments"] == "font.ttf"
video = lines_of(report, "Video")
assert video["Format"] == "H.265 / HEVC" and video["Format profile"] == "Main 10@L4.1" and video["Frame rate"] == "23.976 FPS"
assert video["Width"] == "1920 pixels" and video["Height"] == "1080 pixels" and video["Bit depth"] == "10 bits"
assert video["Bit rate"] == "3 200 kb/s" and video["Duration"] == "24 min 1 s" and video["Default"] == "Yes" and video["Forced"] == "No"
first, second = lines_of(report, "Audio #1"), lines_of(report, "Audio #2")
assert first["Language"] == "Japanese" and first["Title"] == "Japanese" and first["Channel(s)"] == "2 channels"
assert first["Sampling rate"] == "48 kHz" and first["Format profile"] == "LC" and first["Default"] == "Yes"
assert second["Language"] == "Hindi" and second["Channel(s)"] == "1 channel" and second["Sampling rate"] == "44.1 kHz"
text = lines_of(report, "Text")
assert text["Language"] == "English" and text["Forced"] == "Yes" and text["Default"] == "No"
assert report.section("Menu").lines == [("00:00:00.000", "Intro"), ("00:01:30.500", "Main")]
@pytest.mark.parametrize("codec, level, shown", [
("h264", 41, "L4.1"), ("h264", 40, "L4"), ("h264", 12, "L1.2"), ("hevc", 120, "L4"), ("hevc", 153, "L5.1"),
("h264", -99, ""), ("vp9", 41, ""), ("h264", None, ""),
])
def test_codec_levels_are_shown_the_way_each_codec_counts_them(codec, level, shown):
assert mi._level({"codec_name": codec, "level": level}) == shown
def test_ffprobe_with_nothing_to_show_gives_just_a_general_section():
sections = mi.ffprobe_sections({"format": {"format_name": "mp3"}}, "a.mp3", 0)
assert [s.title for s in sections] == ["General"] and sections[0].lines == [("Complete name", "a.mp3"), ("Format", "mp3")]
# ---------------------------------------------------------------------------
# real files
# ---------------------------------------------------------------------------
@needs_ffmpeg
async def test_a_real_mkv_is_read_by_mediainfo(media_dir):
pytest.importorskip("pymediainfo")
path = media_dir / "sample.mkv"
report = await mi.read_file(path, "Show 01.mkv", path.stat().st_size, extra=mi.telegram_section("Show 01.mkv", path.stat().st_size))
assert report.engine == "MediaInfo" and report.has_streams and not report.truncated
assert [s.title for s in report.sections] == ["General", "Video", "Audio #1", "Audio #2", "Audio #3", "Text #1", "Text #2", "Menu", "Telegram"]
general = lines_of(report, "General")
assert general["Complete name"] == "Show 01.mkv" and general["Format"] == "Matroska" # not the path it was read from
assert lines_of(report, "Audio #1")["Language"] == "Japanese" and lines_of(report, "Text #2")["Language"] == "English"
assert "AVC" in lines_of(report, "Video")["Format"] and lines_of(report, "Video")["Width"] == "320 pixels"
assert report.summary().startswith("Matroska · 2 s") and report.summary().endswith("1 video · 3 audio · 2 subtitle")
@needs_ffmpeg
async def test_without_mediainfo_ffprobe_gives_the_same_layout(media_dir, monkeypatch):
monkeypatch.setattr(mi, "_mediainfo", lambda path: None)
path = media_dir / "sample.mkv"
report = await mi.read_file(path, "Show 01.mkv", path.stat().st_size)
assert report.engine == "ffprobe" and report.has_streams
assert [s.title for s in report.sections] == ["General", "Video", "Audio #1", "Audio #2", "Audio #3", "Text #1", "Text #2", "Menu"]
assert lines_of(report, "General")["Complete name"] == "Show 01.mkv" and lines_of(report, "Audio #3")["Language"] == "Hindi"
assert report.section("Menu").lines == [("00:00:00.000", "Intro"), ("00:00:01.000", "Main")]
@needs_ffmpeg
async def test_a_failing_mediainfo_falls_back_to_ffprobe(media_dir, monkeypatch):
def boom(path):
raise RuntimeError("libmediainfo crashed")
monkeypatch.setattr(mi, "_mediainfo", boom)
report = await mi.read_file(media_dir / "sample.mp4", "Movie.mp4", (media_dir / "sample.mp4").stat().st_size)
assert report.engine == "ffprobe" and report.has_streams and lines_of(report, "Audio #2")["Language"] == "English"
@needs_ffmpeg
async def test_an_audio_file_shows_its_tags(media_dir):
path = media_dir / "sample.mp3"
report = await mi.read_file(path, "Song.mp3", path.stat().st_size)
assert report.has_streams and report.section("Audio") is not None
assert "Old" in lines_of(report, "General").values() # the title tag, however the engine labels it
@needs_ffmpeg
async def test_only_the_start_of_an_mkv_still_gets_the_real_size_and_bit_rate(media_dir, tmp_path):
pytest.importorskip("pymediainfo")
whole = media_dir / "sample.mkv"
cut = tmp_path / "probe.mkv"
cut.write_bytes(whole.read_bytes()[:3000])
real = whole.stat().st_size
expected = await mi.read_file(whole, "Show 01.mkv", real)
report = await mi.read_file(cut, "Show 01.mkv", real)
assert report.truncated and report.has_streams
general, reference = lines_of(report, "General"), lines_of(expected, "General")
assert general["File size"] == reference["File size"] and general["Overall bit rate"] == reference["Overall bit rate"]
assert not any("Conformance" in label or label.strip() == "CRC_Error_Pos" for label, _ in report.section("General").lines)
@needs_ffmpeg
async def test_an_mp4_cut_before_its_index_has_no_streams_but_still_a_report(media_dir, tmp_path):
cut = tmp_path / "probe.mp4"
cut.write_bytes((media_dir / "sample.mp4").read_bytes()[:3000])
report = await mi.read_file(cut, "Movie.mp4", 40 * 1024 * 1024)
assert report.truncated and not report.has_streams
assert lines_of(report, "General")["Complete name"] == "Movie.mp4" and lines_of(report, "General")["File size"] == "40.0 MiB"
async def test_a_file_that_is_not_media_still_gets_a_general_section(tmp_path):
path = tmp_path / "notes.txt"
path.write_text("hello\n" * 100)
report = await mi.read_file(path, "notes.txt", path.stat().st_size)
assert not report.has_streams and not report.truncated
general = lines_of(report, "General")
assert general["Complete name"] == "notes.txt" and general["File size"] == "600 Bytes"
|