add transcode and playback filters to env settings (#2018)

This commit is contained in:
jeffvli
2026-05-12 19:39:28 -07:00
parent ce7a319d2f
commit 4ecd8271a2
4 changed files with 110 additions and 17 deletions
+3
View File
@@ -66,6 +66,9 @@ These variables override app settings **on first run** when no persisted setting
| `playback.scrobble.scrobbleAtDuration` | `240` | `FS_PLAYBACK_SCROBBLE_AT_DURATION` | Seconds of playback before scrobble. |
| `playback.scrobble.scrobbleAtPercentage` | `75` | `FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE` | Percentage of track before scrobble. |
| `playback.transcode.enabled` | `false` | `FS_PLAYBACK_TRANSCODE_ENABLED` | `true` / `false` — Enable transcoding. |
| `playback.transcode.format` | *(unset)* | `FS_PLAYBACK_TRANSCODE_FORMAT` | Transcode format string (codec/container), e.g. server-specific value. Empty = use default. |
| `playback.transcode.bitrate` | *(unset)* | `FS_PLAYBACK_TRANSCODE_BITRATE` | Transcode bitrate (number, kbps or as defined by server). |
| `playback.filters` | `[]` | `FS_PLAYBACK_FILTERS` | JSON array of player filters: each object needs `id`, `field`, `operator`, `value`; optional `isEnabled`. Invalid JSON or shape is ignored. |
---
+3
View File
@@ -58,6 +58,9 @@ window.FS_PLAYBACK_SCROBBLE_NOTIFY = "${FS_PLAYBACK_SCROBBLE_NOTIFY}";
window.FS_PLAYBACK_SCROBBLE_AT_DURATION = "${FS_PLAYBACK_SCROBBLE_AT_DURATION}";
window.FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE = "${FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE}";
window.FS_PLAYBACK_TRANSCODE_ENABLED = "${FS_PLAYBACK_TRANSCODE_ENABLED}";
window.FS_PLAYBACK_TRANSCODE_FORMAT = "${FS_PLAYBACK_TRANSCODE_FORMAT}";
window.FS_PLAYBACK_TRANSCODE_BITRATE = "${FS_PLAYBACK_TRANSCODE_BITRATE}";
window.FS_PLAYBACK_FILTERS = "${FS_PLAYBACK_FILTERS}";
window.FS_DISCORD_ENABLED = "${FS_DISCORD_ENABLED}";
window.FS_DISCORD_CLIENT_ID = "${FS_DISCORD_CLIENT_ID}";
+3
View File
@@ -65,13 +65,16 @@ declare global {
FS_LYRICS_TRANSLATION_API_KEY?: string;
FS_LYRICS_TRANSLATION_TARGET_LANGUAGE?: string;
FS_PLAYBACK_AUDIO_FADE_ON_STATUS_CHANGE?: string;
FS_PLAYBACK_FILTERS?: string;
FS_PLAYBACK_MEDIA_SESSION?: string;
FS_PLAYBACK_PRESERVE_PITCH?: string;
FS_PLAYBACK_SCROBBLE_AT_DURATION?: string;
FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE?: string;
FS_PLAYBACK_SCROBBLE_ENABLED?: string;
FS_PLAYBACK_SCROBBLE_NOTIFY?: string;
FS_PLAYBACK_TRANSCODE_BITRATE?: string;
FS_PLAYBACK_TRANSCODE_ENABLED?: string;
FS_PLAYBACK_TRANSCODE_FORMAT?: string;
FS_PLAYBACK_WEB_AUDIO?: string;
LEGACY_AUTHENTICATION?: boolean | string;
REMOTE_URL?: string;
+101 -17
View File
@@ -1,7 +1,73 @@
import type { SettingsState } from './settings.store';
import type { PlayerFilter, SettingsState } from './settings.store';
import { sanitizeCss } from '/@/renderer/utils/sanitize';
const PLAYER_FILTER_FIELDS = new Set([
'albumArtist',
'artist',
'duration',
'favorite',
'genre',
'name',
'note',
'path',
'playCount',
'rating',
'year',
]);
const PLAYER_FILTER_OPERATORS = new Set([
'after',
'afterDate',
'before',
'beforeDate',
'contains',
'endsWith',
'gt',
'inTheLast',
'inTheRange',
'inTheRangeDate',
'is',
'isNot',
'lt',
'notContains',
'notInTheLast',
'regex',
'startsWith',
]);
function isValidPlayerFilter(item: unknown): item is PlayerFilter {
if (!item || typeof item !== 'object' || Array.isArray(item)) return false;
const o = item as Record<string, unknown>;
if (typeof o.id !== 'string') return false;
if (typeof o.field !== 'string' || !PLAYER_FILTER_FIELDS.has(o.field)) return false;
if (typeof o.operator !== 'string' || !PLAYER_FILTER_OPERATORS.has(o.operator)) return false;
if (!isValidPlayerFilterValue(o.value)) return false;
if (o.isEnabled !== undefined && typeof o.isEnabled !== 'boolean') return false;
return true;
}
function isValidPlayerFilterValue(value: unknown): boolean {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return true;
}
if (!Array.isArray(value)) return false;
return value.every((v) => typeof v === 'string' || typeof v === 'number');
}
function parsePlaybackFiltersJson(raw: string): unknown {
const t = raw.trim();
if (t === '') return undefined;
try {
const v = JSON.parse(t) as unknown;
if (!Array.isArray(v)) return undefined;
if (!v.every(isValidPlayerFilter)) return undefined;
return v;
} catch {
return undefined;
}
}
const APP_THEMES = new Set([
'ayuDark',
'ayuLight',
@@ -55,28 +121,29 @@ type DeepPartial<T> = {
interface EnvSettingSpec {
enumSet?: Set<string>;
key: string;
path: [string, string, string] | [string, string];
path: readonly string[];
skipIfEmpty?: boolean;
transform?: (raw: string) => unknown;
type: 'bool' | 'enum' | 'num' | 'string';
}
function setAtPath(
obj: EnvSettingsOverrides,
path: [string, string, string] | [string, string],
value: unknown,
): void {
const [a, b, c] = path;
const root = (obj as Record<string, unknown>)[a] ?? {};
(obj as Record<string, unknown>)[a] = root;
const branch = root as Record<string, unknown>;
if (c === undefined) {
branch[b] = value;
} else {
const nested = branch[b] ?? {};
branch[b] = nested;
(nested as Record<string, unknown>)[c] = value;
function setAtPath(obj: EnvSettingsOverrides, path: readonly string[], value: unknown): void {
if (path.length < 2) return;
let cur: Record<string, unknown> = obj as Record<string, unknown>;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i]!;
const existing = cur[key];
const next: Record<string, unknown> =
existing !== null &&
existing !== undefined &&
typeof existing === 'object' &&
!Array.isArray(existing)
? { ...(existing as Record<string, unknown>) }
: {};
cur[key] = next;
cur = next;
}
cur[path[path.length - 1]!] = value;
}
const RGB_ACCENT_REGEX = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
@@ -252,6 +319,23 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
path: ['playback', 'transcode', 'enabled'],
type: 'bool',
},
{
key: 'FS_PLAYBACK_TRANSCODE_FORMAT',
path: ['playback', 'transcode', 'format'],
skipIfEmpty: true,
type: 'string',
},
{
key: 'FS_PLAYBACK_TRANSCODE_BITRATE',
path: ['playback', 'transcode', 'bitrate'],
type: 'num',
},
{
key: 'FS_PLAYBACK_FILTERS',
path: ['playback', 'filters'],
transform: parsePlaybackFiltersJson,
type: 'string',
},
{ key: 'FS_DISCORD_ENABLED', path: ['discord', 'enabled'], type: 'bool' },
{
key: 'FS_DISCORD_CLIENT_ID',