From 53b16dbf36acb26388d458d180fafec33112bba0 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 22 Feb 2026 02:20:19 -0500 Subject: [PATCH] Auto-proxy Dropbox and HTTP media stream URLs at runtime --- client/public/version.js | 2 +- client/src/audio/radioStationRuntime.ts | 39 ++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/client/public/version.js b/client/public/version.js index 2d7b73d..215cccd 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.22 R139"; +window.CHGRID_WEB_VERSION = "2026.02.22 R140"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/audio/radioStationRuntime.ts b/client/src/audio/radioStationRuntime.ts index af8cb3f..05f3efe 100644 --- a/client/src/audio/radioStationRuntime.ts +++ b/client/src/audio/radioStationRuntime.ts @@ -5,6 +5,7 @@ import { resolveSpatialMix } from './spatial'; export const RADIO_CHANNEL_OPTIONS = ['stereo', 'mono', 'left', 'right'] as const; export type RadioChannelMode = (typeof RADIO_CHANNEL_OPTIONS)[number]; +const APP_BASE_PATH = import.meta.env.BASE_URL ?? '/'; type SharedRadioSource = { streamUrl: string; @@ -108,18 +109,48 @@ function connectRadioChannelSource( }; } -function freshStreamUrl(streamUrl: string): string { +function isDropboxHost(hostname: string): boolean { + const host = hostname.toLowerCase(); + return host.endsWith('dropbox.com') || host.endsWith('dropboxusercontent.com'); +} + +function shouldProxyStreamUrl(streamUrl: string): boolean { try { const parsed = new URL(streamUrl); + if ( + parsed.origin === window.location.origin && + parsed.pathname.toLowerCase().endsWith('/media_proxy.php') + ) { + return false; + } + if (parsed.protocol === 'http:') return true; + if (parsed.protocol === 'https:' && isDropboxHost(parsed.hostname)) return true; + } catch { + return false; + } + return false; +} + +function getProxyUrlForStream(streamUrl: string): string { + const normalizedBase = APP_BASE_PATH.endsWith('/') ? APP_BASE_PATH : `${APP_BASE_PATH}/`; + const proxy = new URL(`${normalizedBase}media_proxy.php`, window.location.origin); + proxy.searchParams.set('url', streamUrl); + return proxy.toString(); +} + +function freshStreamUrl(streamUrl: string): string { + const playbackSource = shouldProxyStreamUrl(streamUrl) ? getProxyUrlForStream(streamUrl) : streamUrl; + try { + const parsed = new URL(playbackSource); const hostname = parsed.hostname.toLowerCase(); if (hostname.endsWith('dropbox.com') || hostname.endsWith('dropboxusercontent.com')) { - return streamUrl; + return playbackSource; } } catch { // Leave non-URL strings to the generic cache-buster behavior below. } - const separator = streamUrl.includes('?') ? '&' : '?'; - return `${streamUrl}${separator}chgrid_start=${Date.now()}`; + const separator = playbackSource.includes('?') ? '&' : '?'; + return `${playbackSource}${separator}chgrid_start=${Date.now()}`; } type RadioSpatialConfig = {