From 4ecd8271a2188785413d1b0c527cb4bf1e76a458 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 12 May 2026 19:39:28 -0700 Subject: [PATCH] add transcode and playback filters to env settings (#2018) --- docs/ENV_SETTINGS.md | 3 + settings.js.template | 3 + src/renderer/global.d.ts | 3 + src/renderer/store/env-settings-overrides.ts | 118 ++++++++++++++++--- 4 files changed, 110 insertions(+), 17 deletions(-) diff --git a/docs/ENV_SETTINGS.md b/docs/ENV_SETTINGS.md index 83acd95ef..053ab91a3 100644 --- a/docs/ENV_SETTINGS.md +++ b/docs/ENV_SETTINGS.md @@ -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. | --- diff --git a/settings.js.template b/settings.js.template index fd8b65708..b7df16fe1 100644 --- a/settings.js.template +++ b/settings.js.template @@ -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}"; diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts index b1a24fc73..338f66f22 100644 --- a/src/renderer/global.d.ts +++ b/src/renderer/global.d.ts @@ -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; diff --git a/src/renderer/store/env-settings-overrides.ts b/src/renderer/store/env-settings-overrides.ts index 84c9f5a2d..ed5b99e14 100644 --- a/src/renderer/store/env-settings-overrides.ts +++ b/src/renderer/store/env-settings-overrides.ts @@ -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; + 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 = { interface EnvSettingSpec { enumSet?: Set; 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)[a] ?? {}; - (obj as Record)[a] = root; - const branch = root as Record; - if (c === undefined) { - branch[b] = value; - } else { - const nested = branch[b] ?? {}; - branch[b] = nested; - (nested as Record)[c] = value; +function setAtPath(obj: EnvSettingsOverrides, path: readonly string[], value: unknown): void { + if (path.length < 2) return; + let cur: Record = obj as Record; + for (let i = 0; i < path.length - 1; i++) { + const key = path[i]!; + const existing = cur[key]; + const next: Record = + existing !== null && + existing !== undefined && + typeof existing === 'object' && + !Array.isArray(existing) + ? { ...(existing as Record) } + : {}; + 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',