Files
fluxer/fluxer_app/src/utils/MediaProxyUtils.ts
Hampus Kraft 2f557eda8c initial commit
2026-01-01 21:05:54 +00:00

168 lines
4.6 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
interface MediaProxyOptions {
width?: number;
height?: number;
format?: string;
quality?: 'high' | 'low' | 'lossless';
animated?: boolean;
}
const CSP_ALLOWED_RESOURCE_HOSTS = new Set(['fluxerusercontent.com', 'fluxerstatic.com', 'i.ytimg.com']);
const CSP_ALLOWED_RESOURCE_SUFFIXES = ['.fluxer.app', '.fluxer.media', '.youtube.com'];
function hasURLGlobals(): boolean {
return typeof URL !== 'undefined' && typeof URLSearchParams !== 'undefined';
}
function getElectronMediaProxyBase(): URL | null {
if (!hasURLGlobals()) return null;
if (typeof window.electron?.getMediaProxyUrl !== 'function') return null;
const raw = window.electron.getMediaProxyUrl();
if (!raw) return null;
try {
return new URL(raw);
} catch {
return null;
}
}
function isAllowedByDefaultCsp(hostname: string): boolean {
if (CSP_ALLOWED_RESOURCE_HOSTS.has(hostname)) return true;
return CSP_ALLOWED_RESOURCE_SUFFIXES.some((suffix) => hostname.endsWith(suffix));
}
function unwrapElectronMediaProxyUrl(url: string): {base: URL; target: string} | null {
if (!hasURLGlobals()) return null;
const base = getElectronMediaProxyBase();
if (!base) return null;
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return null;
}
if (parsed.origin !== base.origin) return null;
if (parsed.pathname !== base.pathname) return null;
const target = parsed.searchParams.get('target');
if (!target) return null;
return {base, target};
}
function shouldWrapWithElectronMediaProxy(targetUrl: string): boolean {
const base = getElectronMediaProxyBase();
if (!base) return false;
try {
const parsed = new URL(targetUrl);
if (parsed.protocol === 'blob:' || parsed.protocol === 'data:') return false;
return !isAllowedByDefaultCsp(parsed.hostname);
} catch {
return false;
}
}
function wrapWithElectronMediaProxy(targetUrl: string, base: URL): string {
const proxied = new URL(base.toString());
proxied.searchParams.set('target', targetUrl);
return proxied.toString();
}
function appendMediaProxyParams(proxyURL: string, options: MediaProxyOptions): string {
const {width, height, format, quality, animated} = options;
if (!width && !height && !format && !quality && !animated) {
return proxyURL;
}
const params = new URLSearchParams();
if (format) {
params.append('format', format);
}
if (width !== undefined) {
params.append('width', width.toString());
}
if (height !== undefined) {
params.append('height', height.toString());
}
if (quality) {
params.append('quality', quality);
}
if (animated !== undefined) {
params.append('animated', animated.toString());
}
const separator = proxyURL.includes('?') ? '&' : '?';
return `${proxyURL}${separator}${params.toString()}`;
}
export function buildMediaProxyURL(proxyURL: string, options: MediaProxyOptions = {}): string {
if (!proxyURL) return proxyURL;
const unwrapped = unwrapElectronMediaProxyUrl(proxyURL);
const base = unwrapped?.base ?? getElectronMediaProxyBase();
const rawUrl = unwrapped ? unwrapped.target : proxyURL;
const updated = appendMediaProxyParams(rawUrl, options);
if (unwrapped && base) {
return wrapWithElectronMediaProxy(updated, base);
}
if (base && shouldWrapWithElectronMediaProxy(updated)) {
return wrapWithElectronMediaProxy(updated, base);
}
return updated;
}
export function stripMediaProxyParams(proxyURL: string): string {
const unwrapped = unwrapElectronMediaProxyUrl(proxyURL);
const base = unwrapped?.base ?? getElectronMediaProxyBase();
const rawUrl = unwrapped ? unwrapped.target : proxyURL;
const url = new URL(rawUrl);
url.searchParams.delete('width');
url.searchParams.delete('height');
url.searchParams.delete('format');
url.searchParams.delete('quality');
url.searchParams.delete('animated');
const stripped = url.toString();
if (unwrapped && base) {
return wrapWithElectronMediaProxy(stripped, base);
}
if (base && shouldWrapWithElectronMediaProxy(stripped)) {
return wrapWithElectronMediaProxy(stripped, base);
}
return stripped;
}