From 76ba13fbad16fb828f0f19a66fbb57fd7bac5a01 Mon Sep 17 00:00:00 2001 From: Bergasha Date: Wed, 19 Aug 2026 16:45:09 +1000 Subject: [PATCH] Address Codex review feedback on PR #6627 - Classify IPv4/IPv6 link-local addresses (169.254.0.0/16, fe80::/10) as LAN in Jellyfin location parsing - they can't traverse a router, so a false WAN reading was possible for common autoconfig/link-local sessions. - Fix portrait video resolution mislabeling - a 1080x1920 clip was classified off its long-axis height (1920) and showed as 1440p. Classify by the short axis instead so orientation doesn't flip the result. - Raise the compact-mode status column breakpoint from 420 to 540px - at 420 the fixed-width user (200px) and status (190px) columns left almost no room for the currentlyPlaying column, the widget's primary content. - Document the new resolution indicator in the media-server widget docs. --- apps/docs/docs/widgets/media-server/index.mdx | 2 +- .../src/jellyfin/jellyfin-integration.spec.ts | 7 +++++++ .../src/jellyfin/jellyfin-integration.ts | 2 ++ .../src/media-server/component.spec.tsx | 9 ++++++++- .../widgets/src/media-server/component.tsx | 20 ++++++++++++------- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/apps/docs/docs/widgets/media-server/index.mdx b/apps/docs/docs/widgets/media-server/index.mdx index 0c4dc18d0..a2ab91c9c 100644 --- a/apps/docs/docs/widgets/media-server/index.mdx +++ b/apps/docs/docs/widgets/media-server/index.mdx @@ -22,7 +22,7 @@ import { plexIntegration } from "@site/docs/integrations/plex"; The media server widget shows active sessions across your media servers. -It shows playback progress, pause state, time remaining, and whether each session is direct play, direct stream, or transcoding audio or video. Location and bitrate can be toggled independently. +It shows playback progress, pause state, time remaining, the streaming resolution (e.g. `1080p`), and whether each session is direct play, direct stream, or transcoding audio or video. Location and bitrate can be toggled independently. Select a session to inspect its media metadata in an anchored popover. Advanced mode adds search, sorting, location, bitrate, and a summary of playing and transcoding sessions while keeping separate integration instances identifiable. Compact visibility switches do not hide location or bitrate from the complete advanced session view. If one selected server is unavailable, a warning indicator identifies the degraded result while sessions from healthy servers remain usable. diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.spec.ts b/packages/integrations/src/jellyfin/jellyfin-integration.spec.ts index 46e21c7ed..07bbd2a9d 100644 --- a/packages/integrations/src/jellyfin/jellyfin-integration.spec.ts +++ b/packages/integrations/src/jellyfin/jellyfin-integration.spec.ts @@ -55,4 +55,11 @@ describe("parseLocation", () => { expect(parseLocation("[::ffff:192.168.1.5]:8096")).toBe("lan"); expect(parseLocation("[::ffff:8.8.8.8]:8096")).toBe("wan"); }); + + test("classifies link-local addresses as lan", () => { + // Link-local traffic can't traverse a router, so it's always LAN even though + // it isn't a private range in the traditional (RFC 1918 / ULA) sense. + expect(parseLocation("169.254.1.5:8096")).toBe("lan"); + expect(parseLocation("[fe80::1]:8096")).toBe("lan"); + }); }); diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.ts b/packages/integrations/src/jellyfin/jellyfin-integration.ts index e5cc6511e..7e3f882f3 100644 --- a/packages/integrations/src/jellyfin/jellyfin-integration.ts +++ b/packages/integrations/src/jellyfin/jellyfin-integration.ts @@ -29,7 +29,9 @@ privateAddresses.addSubnet("10.0.0.0", 8, "ipv4"); privateAddresses.addSubnet("172.16.0.0", 12, "ipv4"); privateAddresses.addSubnet("192.168.0.0", 16, "ipv4"); privateAddresses.addSubnet("127.0.0.0", 8, "ipv4"); +privateAddresses.addSubnet("169.254.0.0", 16, "ipv4"); privateAddresses.addSubnet("fc00::", 7, "ipv6"); +privateAddresses.addSubnet("fe80::", 10, "ipv6"); privateAddresses.addAddress("::1", "ipv6"); export function extractHost(remoteEndPoint: string): string { diff --git a/packages/widgets/src/media-server/component.spec.tsx b/packages/widgets/src/media-server/component.spec.tsx index 8e501ee6c..997635f2e 100644 --- a/packages/widgets/src/media-server/component.spec.tsx +++ b/packages/widgets/src/media-server/component.spec.tsx @@ -109,7 +109,8 @@ describe("getMediaServerColumnVisibility", () => { test("keeps a stable responsive column model from the zero-sized first render", () => { expect(getMediaServerColumnVisibility(0, false)).toEqual({ user: false, status: false }); expect(getMediaServerColumnVisibility(300, false)).toEqual({ user: true, status: false }); - expect(getMediaServerColumnVisibility(420, false)).toEqual({ user: true, status: true }); + expect(getMediaServerColumnVisibility(420, false)).toEqual({ user: true, status: false }); + expect(getMediaServerColumnVisibility(540, false)).toEqual({ user: true, status: true }); expect(getMediaServerColumnVisibility(0, true)).toEqual({ user: true, status: true }); }); }); @@ -149,4 +150,10 @@ describe("getResolutionLabel", () => { test("falls back to the raw height when it doesn't clear any known tier", () => { expect(getResolutionLabel(100, 100)).toBe("100p"); }); + + test("classifies a portrait video by its width, not its long-axis height", () => { + // A 1080x1920 portrait phone recording is still a 1080p source, not a 1440p one. + expect(getResolutionLabel(1080, 1920)).toBe("1080p"); + expect(getResolutionLabel(720, 1280)).toBe("720p"); + }); }); diff --git a/packages/widgets/src/media-server/component.tsx b/packages/widgets/src/media-server/component.tsx index b73822ec9..1c09fd637 100644 --- a/packages/widgets/src/media-server/component.tsx +++ b/packages/widgets/src/media-server/component.tsx @@ -51,7 +51,9 @@ type SortState = { column: SortColumn; descending: boolean } | null; export const getMediaServerColumnVisibility = (width: number, isAdvanced: boolean) => ({ user: isAdvanced || width >= 300, - status: isAdvanced || width >= 420, + // The user (200px) and status (190px) columns together need a floor here, otherwise the + // currentlyPlaying column - the primary content - gets squeezed to almost nothing. + status: isAdvanced || width >= 540, }); function getPlaybackStatus(transcoding: TranscodingDecision | undefined): PlaybackStatus { @@ -80,12 +82,16 @@ function formatBitrate(bitrateKbps: number | null | undefined): string | null { const RESOLUTION_TIER_HEIGHTS = [4320, 2160, 1440, 1080, 720, 480, 360, 240] as const; export function getResolutionLabel(width: number, height: number): string { - // Letterboxed/cinemascope sources are cropped shorter than their nominal resolution class - // (a 1920x804 encode is still "1080p"), so classify off the width-derived 16:9 height instead - // of the raw (possibly cropped) height - matches how Plex/Jellyfin label these themselves. - const effectiveHeight = Math.max(height, Math.round((width * 9) / 16)); - const tier = RESOLUTION_TIER_HEIGHTS.find((candidate) => effectiveHeight >= candidate * 0.9); - return `${tier ?? effectiveHeight}p`; + // Classify off the short axis (height for landscape, width for portrait) so orientation + // doesn't flip which dimension drives the label. Letterboxed/cinemascope sources are cropped + // shorter than their nominal resolution class (a 1920x804 encode is still "1080p"), so derive + // a floor for the short axis from the long axis assuming 16:9 - matches how Plex/Jellyfin label these themselves. + const isPortrait = height > width; + const shortAxis = isPortrait ? width : height; + const longAxis = isPortrait ? height : width; + const effectiveShortAxis = Math.max(shortAxis, Math.round((longAxis * 9) / 16)); + const tier = RESOLUTION_TIER_HEIGHTS.find((candidate) => effectiveShortAxis >= candidate * 0.9); + return `${tier ?? effectiveShortAxis}p`; } export function getSeasonEpisodeParams(