fix(library): classify resolution by width + height, not height alone
Cinematic widescreen content (1920×804 at 2.39:1, 3840×1600 21:9, etc.) was being misclassified: a 1080p source presented as 1920×804 fell to 720p because 804 < 900. Same shape for 2160p sources letterboxed below 2000px tall. ResolveResolution now takes (width, height) and picks the larger of the width-derived and height-derived buckets, so anamorphic/letterboxed sources land in the right bucket.
This commit is contained in:
parent
0b2462c82a
commit
9df38c95a3
4 changed files with 62 additions and 20 deletions
|
|
@ -241,7 +241,7 @@ func printScanSummary(cache *library.LibraryCache) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res := library.ResolveResolution(item.MediaInfo.Video.Height)
|
res := library.ResolveResolution(item.MediaInfo.Video.Width, item.MediaInfo.Video.Height)
|
||||||
if res == "" {
|
if res == "" {
|
||||||
res = "other"
|
res = "other"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,17 @@ var (
|
||||||
altEpRegex = regexp.MustCompile(`(?i)(\d{1,2})x(\d{2})`)
|
altEpRegex = regexp.MustCompile(`(?i)(\d{1,2})x(\d{2})`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResolveResolution maps a pixel height to a standard resolution label.
|
// ResolveResolution maps video dimensions to a standard resolution label.
|
||||||
func ResolveResolution(height int) string {
|
// Uses both width and height so cinematic aspect ratios (2.35:1, 2.39:1, 21:9)
|
||||||
|
// are not misclassified — e.g. a 1080p source presented as 1920×804 letterboxed
|
||||||
|
// would fall to 720p if classified by height alone.
|
||||||
|
func ResolveResolution(width, height int) string {
|
||||||
|
byHeight := resolutionByHeight(height)
|
||||||
|
byWidth := resolutionByWidth(width)
|
||||||
|
return maxResolution(byHeight, byWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolutionByHeight(height int) string {
|
||||||
switch {
|
switch {
|
||||||
case height >= 2000:
|
case height >= 2000:
|
||||||
return "2160p"
|
return "2160p"
|
||||||
|
|
@ -29,6 +38,36 @@ func ResolveResolution(height int) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolutionByWidth(width int) string {
|
||||||
|
switch {
|
||||||
|
case width >= 3400:
|
||||||
|
return "2160p"
|
||||||
|
case width >= 1800:
|
||||||
|
return "1080p"
|
||||||
|
case width >= 1200:
|
||||||
|
return "720p"
|
||||||
|
case width >= 800:
|
||||||
|
return "480p"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolutionRank = map[string]int{
|
||||||
|
"": 0,
|
||||||
|
"480p": 1,
|
||||||
|
"720p": 2,
|
||||||
|
"1080p": 3,
|
||||||
|
"2160p": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxResolution(a, b string) string {
|
||||||
|
if resolutionRank[a] >= resolutionRank[b] {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// DeriveContentType guesses "movie" or "show" from parsed metadata.
|
// DeriveContentType guesses "movie" or "show" from parsed metadata.
|
||||||
func DeriveContentType(item LibraryItem) string {
|
func DeriveContentType(item LibraryItem) string {
|
||||||
if item.Season > 0 || item.Episode > 0 {
|
if item.Season > 0 || item.Episode > 0 {
|
||||||
|
|
|
||||||
|
|
@ -8,28 +8,31 @@ import (
|
||||||
|
|
||||||
func TestResolveResolution(t *testing.T) {
|
func TestResolveResolution(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
width int
|
||||||
height int
|
height int
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{2160, "2160p"},
|
{"4K square", 3840, 2160, "2160p"},
|
||||||
{2000, "2160p"},
|
{"4K low height", 3840, 1600, "2160p"},
|
||||||
{1080, "1080p"},
|
{"1080p square", 1920, 1080, "1080p"},
|
||||||
{1920, "1080p"}, // 1920 is width, not height — height for 1080p is ~1080
|
{"1080p cinematic 2.39:1", 1920, 804, "1080p"}, // anamorphic widescreen — must not fall to 720p
|
||||||
{900, "1080p"},
|
{"1080p cinematic 2.35:1", 1920, 818, "1080p"},
|
||||||
{720, "720p"},
|
{"1080p 21:9", 2560, 1080, "1080p"},
|
||||||
{600, "720p"},
|
{"720p square", 1280, 720, "720p"},
|
||||||
{576, "480p"},
|
{"720p widescreen", 1280, 540, "720p"},
|
||||||
{480, "480p"},
|
{"480p", 854, 480, "480p"},
|
||||||
{400, "480p"},
|
{"sub-480", 640, 360, ""},
|
||||||
{360, ""},
|
{"zero", 0, 0, ""},
|
||||||
{0, ""},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := ResolveResolution(tt.height)
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got != tt.want {
|
got := ResolveResolution(tt.width, tt.height)
|
||||||
t.Errorf("ResolveResolution(%d) = %q, want %q", tt.height, got, tt.want)
|
if got != tt.want {
|
||||||
}
|
t.Errorf("ResolveResolution(%d, %d) = %q, want %q", tt.width, tt.height, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ func BuildSyncItems(cache *LibraryCache) []agent.LibrarySyncItem {
|
||||||
|
|
||||||
if item.MediaInfo != nil {
|
if item.MediaInfo != nil {
|
||||||
if item.MediaInfo.Video != nil {
|
if item.MediaInfo.Video != nil {
|
||||||
si.Resolution = ResolveResolution(item.MediaInfo.Video.Height)
|
si.Resolution = ResolveResolution(item.MediaInfo.Video.Width, item.MediaInfo.Video.Height)
|
||||||
si.VideoCodec = item.MediaInfo.Video.Codec
|
si.VideoCodec = item.MediaInfo.Video.Codec
|
||||||
si.HDR = item.MediaInfo.Video.HDR
|
si.HDR = item.MediaInfo.Video.HDR
|
||||||
si.BitDepth = item.MediaInfo.Video.BitDepth
|
si.BitDepth = item.MediaInfo.Video.BitDepth
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue