unarr/internal/library/mediainfo/ffprobe_test.go
Deivid Soto 3e0f3a5a64
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
feat(cli): upgrade command, rich status, and version cache
- Replace `upgrade` stub with real command (alias for `self-update`)
- Also register `update` as alias: `unarr update` works too
- Rewrite `status` to show full config, disk usage, daemon state, and
  update availability with colored sections
- Add version check cache (1h TTL) so `status` is instant on repeat runs
- Guard against division by zero on empty filesystems
- Guard against negative durations from clock skew
- Guard against stale PID via heartbeat recency check (2 min)
- Add comprehensive test coverage across agent, engine, upgrade, usenet,
  arr, library, mediaserver, and UI packages
- Improve Makefile coverage target to exclude cmd/ glue code
- Fix stream handler resource cleanup and ffprobe error handling
2026-03-31 22:05:43 +02:00

430 lines
9.9 KiB
Go

package mediainfo
import (
"testing"
)
func TestParseDuration(t *testing.T) {
tests := []struct {
input string
want float64
}{
{"", 0},
{"0", 0},
{"-5", 0},
{"invalid", 0},
{"7423.500000", 7423.5},
{"120.123456", 120.123},
{"3600", 3600},
{"0.001", 0.001},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := parseDuration(tt.input)
if got != tt.want {
t.Errorf("parseDuration(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
func TestTagValue(t *testing.T) {
tags := map[string]string{
"language": "eng",
"title": "Main Audio",
"HANDLER": "VideoHandler",
}
tests := []struct {
key string
want string
}{
{"language", "eng"},
{"title", "Main Audio"},
{"handler", "VideoHandler"},
{"missing", ""},
}
for _, tt := range tests {
t.Run(tt.key, func(t *testing.T) {
got := tagValue(tags, tt.key)
if got != tt.want {
t.Errorf("tagValue(tags, %q) = %q, want %q", tt.key, got, tt.want)
}
})
}
}
func TestTagValueNil(t *testing.T) {
got := tagValue(nil, "language")
if got != "" {
t.Errorf("tagValue(nil, language) = %q, want empty", got)
}
}
func TestContainsAny(t *testing.T) {
tests := []struct {
s string
subs []string
want bool
}{
{"yuv420p10le", []string{"10le", "10be", "p010"}, true},
{"yuv420p12be", []string{"10le", "10be", "p010"}, false},
{"yuv420p12be", []string{"12le", "12be"}, true},
{"yuv420p", []string{"10le", "10be"}, false},
{"", []string{"any"}, false},
{"something", []string{}, false},
}
for _, tt := range tests {
got := containsAny(tt.s, tt.subs...)
if got != tt.want {
t.Errorf("containsAny(%q, %v) = %v, want %v", tt.s, tt.subs, got, tt.want)
}
}
}
func TestParseFFprobeOutput_BasicH264(t *testing.T) {
data := ffprobeOutput{
Format: ffprobeFormat{Duration: "7423.5"},
Streams: []ffprobeStream{
{
CodecType: "video",
CodecName: "h264",
Profile: "High",
Width: 1920,
Height: 1080,
RFrameRate: "24000/1001",
},
{
CodecType: "audio",
CodecName: "aac",
Channels: 2,
Tags: map[string]string{"language": "eng"},
Disposition: map[string]int{"default": 1},
},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatalf("parseFFprobeOutput: %v", err)
}
if mi.Video == nil {
t.Fatal("expected video info")
}
if mi.Video.Codec != "h264" {
t.Errorf("codec = %q, want h264", mi.Video.Codec)
}
if mi.Video.Width != 1920 || mi.Video.Height != 1080 {
t.Errorf("dimensions = %dx%d, want 1920x1080", mi.Video.Width, mi.Video.Height)
}
if mi.Video.Profile != "High" {
t.Errorf("profile = %q, want High", mi.Video.Profile)
}
if mi.Video.Duration != 7423.5 {
t.Errorf("duration = %v, want 7423.5", mi.Video.Duration)
}
if mi.Video.FrameRate < 23.975 || mi.Video.FrameRate > 23.977 {
t.Errorf("frameRate = %v, want ~23.976", mi.Video.FrameRate)
}
if len(mi.Audio) != 1 {
t.Fatalf("audio tracks = %d, want 1", len(mi.Audio))
}
if mi.Audio[0].Lang != "en" {
t.Errorf("audio lang = %q, want en", mi.Audio[0].Lang)
}
if !mi.Audio[0].Default {
t.Error("expected default audio track")
}
}
func TestParseFFprobeOutput_HEVC_HDR10(t *testing.T) {
data := ffprobeOutput{
Format: ffprobeFormat{Duration: "3600"},
Streams: []ffprobeStream{
{
CodecType: "video",
CodecName: "hevc",
Width: 3840,
Height: 2160,
BitsPerRaw: "10",
ColorSpace: "bt2020nc",
ColorTransfer: "smpte2084",
RFrameRate: "24/1",
},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.HDR != "HDR10" {
t.Errorf("hdr = %q, want HDR10", mi.Video.HDR)
}
if mi.Video.BitDepth != 10 {
t.Errorf("bitDepth = %d, want 10", mi.Video.BitDepth)
}
}
func TestParseFFprobeOutput_DolbyVisionWithHDR10(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{
CodecType: "video",
CodecName: "hevc",
Width: 3840,
Height: 2160,
ColorSpace: "bt2020nc",
ColorTransfer: "smpte2084",
SideDataList: []sideData{{SideDataType: "DOVI configuration record"}},
},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.HDR != "DV+HDR10" {
t.Errorf("hdr = %q, want DV+HDR10", mi.Video.HDR)
}
}
func TestParseFFprobeOutput_DolbyVisionOnly(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{
CodecType: "video",
CodecName: "hevc",
Width: 3840,
Height: 2160,
SideDataList: []sideData{{SideDataType: "DOVI configuration record"}},
},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.HDR != "DV" {
t.Errorf("hdr = %q, want DV", mi.Video.HDR)
}
}
func TestParseFFprobeOutput_HLG(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{
CodecType: "video",
CodecName: "hevc",
Width: 3840,
Height: 2160,
ColorSpace: "bt2020nc",
ColorTransfer: "arib-std-b67",
},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.HDR != "HLG" {
t.Errorf("hdr = %q, want HLG", mi.Video.HDR)
}
}
func TestParseFFprobeOutput_MultiAudioAndSubtitles(t *testing.T) {
data := ffprobeOutput{
Format: ffprobeFormat{Duration: "5400"},
Streams: []ffprobeStream{
{CodecType: "video", CodecName: "h264", Width: 1920, Height: 1080},
{
CodecType: "audio", CodecName: "ac3", Channels: 6,
Tags: map[string]string{"language": "eng", "title": "English 5.1"},
Disposition: map[string]int{"default": 1},
},
{
CodecType: "audio", CodecName: "aac", Channels: 2,
Tags: map[string]string{"language": "spa"},
},
{
CodecType: "subtitle", CodecName: "subrip",
Tags: map[string]string{"language": "eng"},
},
{
CodecType: "subtitle", CodecName: "ass",
Tags: map[string]string{"language": "spa"},
Disposition: map[string]int{"forced": 1},
},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if len(mi.Audio) != 2 {
t.Fatalf("audio tracks = %d, want 2", len(mi.Audio))
}
if mi.Audio[0].Title != "English 5.1" {
t.Errorf("audio[0].title = %q", mi.Audio[0].Title)
}
if len(mi.Subtitles) != 2 {
t.Fatalf("subtitle tracks = %d, want 2", len(mi.Subtitles))
}
if !mi.Subtitles[1].Forced {
t.Error("expected subtitle[1] to be forced")
}
if len(mi.Languages) != 2 {
t.Errorf("languages = %v, want 2 entries", mi.Languages)
}
}
func TestParseFFprobeOutput_BitDepthFromPixFmt(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{CodecType: "video", CodecName: "hevc", Width: 1920, Height: 1080, PixFmt: "yuv420p10le"},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.BitDepth != 10 {
t.Errorf("bitDepth = %d, want 10", mi.Video.BitDepth)
}
}
func TestParseFFprobeOutput_12BitFromPixFmt(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{CodecType: "video", CodecName: "hevc", Width: 1920, Height: 1080, PixFmt: "yuv420p12be"},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.BitDepth != 12 {
t.Errorf("bitDepth = %d, want 12", mi.Video.BitDepth)
}
}
func TestParseFFprobeOutput_DurationFromStreamFallback(t *testing.T) {
data := ffprobeOutput{
Format: ffprobeFormat{Duration: ""},
Streams: []ffprobeStream{
{CodecType: "video", CodecName: "h264", Width: 1280, Height: 720, Duration: "1800.5"},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.Duration != 1800.5 {
t.Errorf("duration = %v, want 1800.5", mi.Video.Duration)
}
}
func TestParseFFprobeOutput_NoStreams(t *testing.T) {
data := ffprobeOutput{}
_, err := parseFFprobeOutput(data)
if err == nil {
t.Error("expected error for no streams")
}
}
func TestParseFFprobeOutput_OnlyFirstVideoStream(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{CodecType: "video", CodecName: "h264", Width: 1920, Height: 1080},
{CodecType: "video", CodecName: "mjpeg", Width: 320, Height: 240}, // cover art
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.Codec != "h264" {
t.Errorf("should use first video stream, got codec %q", mi.Video.Codec)
}
if mi.Video.Width != 1920 {
t.Errorf("width = %d, should be from first video stream", mi.Video.Width)
}
}
func TestParseFFprobeOutput_SMPTE2084_WithoutBT2020(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{CodecType: "video", CodecName: "hevc", Width: 3840, Height: 2160, ColorTransfer: "smpte2084"},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.HDR != "HDR10" {
t.Errorf("hdr = %q, want HDR10", mi.Video.HDR)
}
}
func TestParseFFprobeOutput_AribWithoutBT2020(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{CodecType: "video", CodecName: "hevc", Width: 3840, Height: 2160, ColorTransfer: "arib-std-b67", ColorSpace: "other"},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.HDR != "HLG" {
t.Errorf("hdr = %q, want HLG", mi.Video.HDR)
}
}
func TestParseFFprobeOutput_AudioOnly(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{CodecType: "audio", CodecName: "flac", Channels: 2, Tags: map[string]string{"language": "eng"}},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video != nil {
t.Error("expected no video info for audio-only")
}
if len(mi.Audio) != 1 {
t.Errorf("audio tracks = %d, want 1", len(mi.Audio))
}
}
func TestParseFFprobeOutput_FrameRateNoSlash(t *testing.T) {
data := ffprobeOutput{
Streams: []ffprobeStream{
{CodecType: "video", CodecName: "h264", Width: 1920, Height: 1080, RFrameRate: "30"},
},
}
mi, err := parseFFprobeOutput(data)
if err != nil {
t.Fatal(err)
}
if mi.Video.FrameRate != 0 {
t.Errorf("frameRate = %v, want 0 (no slash)", mi.Video.FrameRate)
}
}