mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
Refactor CoreSettings to use JSONField for value storage and update related logic for proper type handling. Adjusted serializers and forms to accommodate new data structure, ensuring seamless integration across the application.
This commit is contained in:
parent
4bfdd15b37
commit
36967c10ce
32 changed files with 866 additions and 519 deletions
|
|
@ -3,8 +3,28 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|||
import useLocalStorage from '../../hooks/useLocalStorage.jsx';
|
||||
import usePlaylistsStore from '../../store/playlists.jsx';
|
||||
import useSettingsStore from '../../store/settings.jsx';
|
||||
import { ActionIcon, Badge, Box, Card, Center, Flex, Group, Select, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { Gauge, HardDriveDownload, HardDriveUpload, SquareX, Timer, Users, Video } from 'lucide-react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Card,
|
||||
Center,
|
||||
Flex,
|
||||
Group,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
Gauge,
|
||||
HardDriveDownload,
|
||||
HardDriveUpload,
|
||||
SquareX,
|
||||
Timer,
|
||||
Users,
|
||||
Video,
|
||||
} from 'lucide-react';
|
||||
import { toFriendlyDuration } from '../../utils/dateTimeUtils.js';
|
||||
import { CustomTable, useTable } from '../tables/CustomTable/index.jsx';
|
||||
import { TableHelper } from '../../helpers/index.jsx';
|
||||
|
|
@ -87,7 +107,10 @@ const StreamConnectionCard = ({
|
|||
// If we have a channel URL, try to find the matching stream
|
||||
if (channel.url && streamData.length > 0) {
|
||||
// Try to find matching stream based on URL
|
||||
const matchingStream = getMatchingStreamByUrl(streamData, channel.url);
|
||||
const matchingStream = getMatchingStreamByUrl(
|
||||
streamData,
|
||||
channel.url
|
||||
);
|
||||
|
||||
if (matchingStream) {
|
||||
setActiveStreamId(matchingStream.id.toString());
|
||||
|
|
@ -178,9 +201,9 @@ const StreamConnectionCard = ({
|
|||
console.error('Error checking streams after switch:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Handle stream switching
|
||||
// Handle stream switching
|
||||
const handleStreamChange = async (streamId) => {
|
||||
try {
|
||||
console.log('Switching to stream ID:', streamId);
|
||||
|
|
@ -333,7 +356,7 @@ const StreamConnectionCard = ({
|
|||
});
|
||||
|
||||
// Get logo URL from the logos object if available
|
||||
const logoUrl = getLogoUrl(channel.logo_id , logos, previewedStream);
|
||||
const logoUrl = getLogoUrl(channel.logo_id, logos, previewedStream);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
|
@ -388,11 +411,11 @@ const StreamConnectionCard = ({
|
|||
style={{
|
||||
backgroundColor: '#27272A',
|
||||
}}
|
||||
color='#fff'
|
||||
color="#fff"
|
||||
maw={700}
|
||||
w={'100%'}
|
||||
>
|
||||
<Stack pos='relative' >
|
||||
<Stack pos="relative">
|
||||
<Group justify="space-between">
|
||||
<Box
|
||||
style={{
|
||||
|
|
@ -401,7 +424,7 @@ const StreamConnectionCard = ({
|
|||
}}
|
||||
w={100}
|
||||
h={50}
|
||||
display='flex'
|
||||
display="flex"
|
||||
>
|
||||
<img
|
||||
src={logoUrl || logo}
|
||||
|
|
@ -531,7 +554,7 @@ const StreamConnectionCard = ({
|
|||
variant="light"
|
||||
color={
|
||||
parseFloat(channel.ffmpeg_speed) >=
|
||||
getBufferingSpeedThreshold(settings['proxy-settings'])
|
||||
getBufferingSpeedThreshold(settings['proxy_settings'])
|
||||
? 'green'
|
||||
: 'red'
|
||||
}
|
||||
|
|
@ -587,4 +610,4 @@ const StreamConnectionCard = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default StreamConnectionCard;
|
||||
export default StreamConnectionCard;
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ const DvrSettingsForm = React.memo(({ active }) => {
|
|||
|
||||
form.setValues(formValues);
|
||||
|
||||
if (formValues['dvr-comskip-custom-path']) {
|
||||
if (formValues['comskip_custom_path']) {
|
||||
setComskipConfig((prev) => ({
|
||||
path: formValues['dvr-comskip-custom-path'],
|
||||
path: formValues['comskip_custom_path'],
|
||||
exists: prev.exists,
|
||||
}));
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ const DvrSettingsForm = React.memo(({ active }) => {
|
|||
exists: Boolean(response.exists),
|
||||
});
|
||||
if (response.path) {
|
||||
form.setFieldValue('dvr-comskip-custom-path', response.path);
|
||||
form.setFieldValue('comskip_custom_path', response.path);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -94,10 +94,10 @@ const DvrSettingsForm = React.memo(({ active }) => {
|
|||
autoClose: 3000,
|
||||
color: 'green',
|
||||
});
|
||||
form.setFieldValue('dvr-comskip-custom-path', response.path);
|
||||
form.setFieldValue('comskip_custom_path', response.path);
|
||||
useSettingsStore.getState().updateSetting({
|
||||
...(settings['dvr-comskip-custom-path'] || {
|
||||
key: 'dvr-comskip-custom-path',
|
||||
...(settings['comskip_custom_path'] || {
|
||||
key: 'comskip_custom_path',
|
||||
name: 'DVR Comskip Custom Path',
|
||||
}),
|
||||
value: response.path,
|
||||
|
|
@ -137,24 +137,19 @@ const DvrSettingsForm = React.memo(({ active }) => {
|
|||
)}
|
||||
<Switch
|
||||
label="Enable Comskip (remove commercials after recording)"
|
||||
{...form.getInputProps('dvr-comskip-enabled', {
|
||||
{...form.getInputProps('comskip_enabled', {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
id={settings['dvr-comskip-enabled']?.id || 'dvr-comskip-enabled'}
|
||||
name={settings['dvr-comskip-enabled']?.key || 'dvr-comskip-enabled'}
|
||||
id="comskip_enabled"
|
||||
name="comskip_enabled"
|
||||
/>
|
||||
<TextInput
|
||||
label="Custom comskip.ini path"
|
||||
description="Leave blank to use the built-in defaults."
|
||||
placeholder="/app/docker/comskip.ini"
|
||||
{...form.getInputProps('dvr-comskip-custom-path')}
|
||||
id={
|
||||
settings['dvr-comskip-custom-path']?.id || 'dvr-comskip-custom-path'
|
||||
}
|
||||
name={
|
||||
settings['dvr-comskip-custom-path']?.key ||
|
||||
'dvr-comskip-custom-path'
|
||||
}
|
||||
{...form.getInputProps('comskip_custom_path')}
|
||||
id="comskip_custom_path"
|
||||
name="comskip_custom_path"
|
||||
/>
|
||||
<Group align="flex-end" gap="sm">
|
||||
<FileInput
|
||||
|
|
@ -184,71 +179,50 @@ const DvrSettingsForm = React.memo(({ active }) => {
|
|||
description="Begin recording this many minutes before the scheduled start."
|
||||
min={0}
|
||||
step={1}
|
||||
{...form.getInputProps('dvr-pre-offset-minutes')}
|
||||
id={
|
||||
settings['dvr-pre-offset-minutes']?.id || 'dvr-pre-offset-minutes'
|
||||
}
|
||||
name={
|
||||
settings['dvr-pre-offset-minutes']?.key || 'dvr-pre-offset-minutes'
|
||||
}
|
||||
{...form.getInputProps('pre_offset_minutes')}
|
||||
id="pre_offset_minutes"
|
||||
name="pre_offset_minutes"
|
||||
/>
|
||||
<NumberInput
|
||||
label="End late (minutes)"
|
||||
description="Continue recording this many minutes after the scheduled end."
|
||||
min={0}
|
||||
step={1}
|
||||
{...form.getInputProps('dvr-post-offset-minutes')}
|
||||
id={
|
||||
settings['dvr-post-offset-minutes']?.id || 'dvr-post-offset-minutes'
|
||||
}
|
||||
name={
|
||||
settings['dvr-post-offset-minutes']?.key ||
|
||||
'dvr-post-offset-minutes'
|
||||
}
|
||||
{...form.getInputProps('post_offset_minutes')}
|
||||
id="post_offset_minutes"
|
||||
name="post_offset_minutes"
|
||||
/>
|
||||
<TextInput
|
||||
label="TV Path Template"
|
||||
description="Supports {show}, {season}, {episode}, {sub_title}, {channel}, {year}, {start}, {end}. Use format specifiers like {season:02d}. Relative paths are under your library dir."
|
||||
placeholder="TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv"
|
||||
{...form.getInputProps('dvr-tv-template')}
|
||||
id={settings['dvr-tv-template']?.id || 'dvr-tv-template'}
|
||||
name={settings['dvr-tv-template']?.key || 'dvr-tv-template'}
|
||||
{...form.getInputProps('tv_template')}
|
||||
id="tv_template"
|
||||
name="tv_template"
|
||||
/>
|
||||
<TextInput
|
||||
label="TV Fallback Template"
|
||||
description="Template used when an episode has no season/episode. Supports {show}, {start}, {end}, {channel}, {year}."
|
||||
placeholder="TV_Shows/{show}/{start}.mkv"
|
||||
{...form.getInputProps('dvr-tv-fallback-template')}
|
||||
id={
|
||||
settings['dvr-tv-fallback-template']?.id ||
|
||||
'dvr-tv-fallback-template'
|
||||
}
|
||||
name={
|
||||
settings['dvr-tv-fallback-template']?.key ||
|
||||
'dvr-tv-fallback-template'
|
||||
}
|
||||
{...form.getInputProps('tv_fallback_template')}
|
||||
id="tv_fallback_template"
|
||||
name="tv_fallback_template"
|
||||
/>
|
||||
<TextInput
|
||||
label="Movie Path Template"
|
||||
description="Supports {title}, {year}, {channel}, {start}, {end}. Relative paths are under your library dir."
|
||||
placeholder="Movies/{title} ({year}).mkv"
|
||||
{...form.getInputProps('dvr-movie-template')}
|
||||
id={settings['dvr-movie-template']?.id || 'dvr-movie-template'}
|
||||
name={settings['dvr-movie-template']?.key || 'dvr-movie-template'}
|
||||
{...form.getInputProps('movie_template')}
|
||||
id="movie_template"
|
||||
name="movie_template"
|
||||
/>
|
||||
<TextInput
|
||||
label="Movie Fallback Template"
|
||||
description="Template used when movie metadata is incomplete. Supports {start}, {end}, {channel}."
|
||||
placeholder="Movies/{start}.mkv"
|
||||
{...form.getInputProps('dvr-movie-fallback-template')}
|
||||
id={
|
||||
settings['dvr-movie-fallback-template']?.id ||
|
||||
'dvr-movie-fallback-template'
|
||||
}
|
||||
name={
|
||||
settings['dvr-movie-fallback-template']?.key ||
|
||||
'dvr-movie-fallback-template'
|
||||
}
|
||||
{...form.getInputProps('movie_fallback_template')}
|
||||
id="movie_fallback_template"
|
||||
name="movie_fallback_template"
|
||||
/>
|
||||
<Flex mih={50} gap="xs" justify="flex-end" align="flex-end">
|
||||
<Button type="submit" variant="default">
|
||||
|
|
@ -260,4 +234,4 @@ const DvrSettingsForm = React.memo(({ active }) => {
|
|||
);
|
||||
});
|
||||
|
||||
export default DvrSettingsForm;
|
||||
export default DvrSettingsForm;
|
||||
|
|
|
|||
|
|
@ -36,9 +36,7 @@ const NetworkAccessForm = React.memo(({ active }) => {
|
|||
}, [active]);
|
||||
|
||||
useEffect(() => {
|
||||
const networkAccessSettings = JSON.parse(
|
||||
settings['network-access'].value || '{}'
|
||||
);
|
||||
const networkAccessSettings = settings['network_access']?.value || {};
|
||||
networkAccessForm.setValues(
|
||||
Object.keys(NETWORK_ACCESS_OPTIONS).reduce((acc, key) => {
|
||||
acc[key] = networkAccessSettings[key] || '0.0.0.0/0,::/0';
|
||||
|
|
@ -51,8 +49,8 @@ const NetworkAccessForm = React.memo(({ active }) => {
|
|||
setSaved(false);
|
||||
setNetworkAccessError(null);
|
||||
const check = await checkSetting({
|
||||
...settings['network-access'],
|
||||
value: JSON.stringify(networkAccessForm.getValues()),
|
||||
...settings['network_access'],
|
||||
value: networkAccessForm.getValues(), // Send as object
|
||||
});
|
||||
|
||||
if (check.error && check.message) {
|
||||
|
|
@ -78,8 +76,8 @@ const NetworkAccessForm = React.memo(({ active }) => {
|
|||
setSaving(true);
|
||||
try {
|
||||
await updateSetting({
|
||||
...settings['network-access'],
|
||||
value: JSON.stringify(networkAccessForm.getValues()),
|
||||
...settings['network_access'],
|
||||
value: networkAccessForm.getValues(), // Send as object
|
||||
});
|
||||
setSaved(true);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -91,18 +91,13 @@ const ProxySettingsForm = React.memo(({ active }) => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if(!active) setSaved(false);
|
||||
if (!active) setSaved(false);
|
||||
}, [active]);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
if (settings['proxy-settings']?.value) {
|
||||
try {
|
||||
const proxySettings = JSON.parse(settings['proxy-settings'].value);
|
||||
proxySettingsForm.setValues(proxySettings);
|
||||
} catch (error) {
|
||||
console.error('Error parsing proxy settings:', error);
|
||||
}
|
||||
if (settings['proxy_settings']?.value) {
|
||||
proxySettingsForm.setValues(settings['proxy_settings'].value);
|
||||
}
|
||||
}
|
||||
}, [settings]);
|
||||
|
|
@ -116,8 +111,8 @@ const ProxySettingsForm = React.memo(({ active }) => {
|
|||
|
||||
try {
|
||||
const result = await updateSetting({
|
||||
...settings['proxy-settings'],
|
||||
value: JSON.stringify(proxySettingsForm.getValues()),
|
||||
...settings['proxy_settings'],
|
||||
value: proxySettingsForm.getValues(), // Send as object
|
||||
});
|
||||
// API functions return undefined on error
|
||||
if (result) {
|
||||
|
|
@ -163,4 +158,4 @@ const ProxySettingsForm = React.memo(({ active }) => {
|
|||
);
|
||||
});
|
||||
|
||||
export default ProxySettingsForm;
|
||||
export default ProxySettingsForm;
|
||||
|
|
|
|||
|
|
@ -129,8 +129,11 @@ const StreamSettingsForm = React.memo(({ active }) => {
|
|||
const values = form.getValues();
|
||||
const changedSettings = getChangedSettings(values, settings);
|
||||
|
||||
const m3uHashKeyChanged =
|
||||
settings['m3u-hash-key']?.value !== values['m3u-hash-key'].join(',');
|
||||
// Check if m3u_hash_key changed from the grouped stream_settings
|
||||
const currentHashKey =
|
||||
settings['stream_settings']?.value?.m3u_hash_key || '';
|
||||
const newHashKey = values['m3u_hash_key']?.join(',') || '';
|
||||
const m3uHashKeyChanged = currentHashKey !== newHashKey;
|
||||
|
||||
// If M3U hash key changed, show warning (unless suppressed)
|
||||
if (m3uHashKeyChanged && !isWarningSuppressed('rehash-streams')) {
|
||||
|
|
@ -161,10 +164,10 @@ const StreamSettingsForm = React.memo(({ active }) => {
|
|||
)}
|
||||
<Select
|
||||
searchable
|
||||
{...form.getInputProps('default-user-agent')}
|
||||
id={settings['default-user-agent']?.id || 'default-user-agent'}
|
||||
name={settings['default-user-agent']?.key || 'default-user-agent'}
|
||||
label={settings['default-user-agent']?.name || 'Default User Agent'}
|
||||
{...form.getInputProps('default_user_agent')}
|
||||
id="default_user_agent"
|
||||
name="default_user_agent"
|
||||
label="Default User Agent"
|
||||
data={userAgents.map((option) => ({
|
||||
value: `${option.id}`,
|
||||
label: option.name,
|
||||
|
|
@ -172,16 +175,10 @@ const StreamSettingsForm = React.memo(({ active }) => {
|
|||
/>
|
||||
<Select
|
||||
searchable
|
||||
{...form.getInputProps('default-stream-profile')}
|
||||
id={
|
||||
settings['default-stream-profile']?.id || 'default-stream-profile'
|
||||
}
|
||||
name={
|
||||
settings['default-stream-profile']?.key || 'default-stream-profile'
|
||||
}
|
||||
label={
|
||||
settings['default-stream-profile']?.name || 'Default Stream Profile'
|
||||
}
|
||||
{...form.getInputProps('default_stream_profile')}
|
||||
id="default_stream_profile"
|
||||
name="default_stream_profile"
|
||||
label="Default Stream Profile"
|
||||
data={streamProfiles.map((option) => ({
|
||||
value: `${option.id}`,
|
||||
label: option.name,
|
||||
|
|
@ -189,10 +186,10 @@ const StreamSettingsForm = React.memo(({ active }) => {
|
|||
/>
|
||||
<Select
|
||||
searchable
|
||||
{...form.getInputProps('preferred-region')}
|
||||
id={settings['preferred-region']?.id || 'preferred-region'}
|
||||
name={settings['preferred-region']?.key || 'preferred-region'}
|
||||
label={settings['preferred-region']?.name || 'Preferred Region'}
|
||||
{...form.getInputProps('preferred_region')}
|
||||
id="preferred_region"
|
||||
name="preferred_region"
|
||||
label="Preferred Region"
|
||||
data={regionChoices.map((r) => ({
|
||||
label: r.label,
|
||||
value: `${r.value}`,
|
||||
|
|
@ -204,19 +201,16 @@ const StreamSettingsForm = React.memo(({ active }) => {
|
|||
Auto-Import Mapped Files
|
||||
</Text>
|
||||
<Switch
|
||||
{...form.getInputProps('auto-import-mapped-files', {
|
||||
{...form.getInputProps('auto_import_mapped_files', {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
id={
|
||||
settings['auto-import-mapped-files']?.id ||
|
||||
'auto-import-mapped-files'
|
||||
}
|
||||
id="auto_import_mapped_files"
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<MultiSelect
|
||||
id="m3u-hash-key"
|
||||
name="m3u-hash-key"
|
||||
id="m3u_hash_key"
|
||||
name="m3u_hash_key"
|
||||
label="M3U Hash Key"
|
||||
data={[
|
||||
{
|
||||
|
|
@ -240,7 +234,7 @@ const StreamSettingsForm = React.memo(({ active }) => {
|
|||
label: 'Group',
|
||||
},
|
||||
]}
|
||||
{...form.getInputProps('m3u-hash-key')}
|
||||
{...form.getInputProps('m3u_hash_key')}
|
||||
/>
|
||||
|
||||
{rehashSuccess && (
|
||||
|
|
@ -303,4 +297,4 @@ Please ensure you have time to let this complete before proceeding.`}
|
|||
);
|
||||
});
|
||||
|
||||
export default StreamSettingsForm;
|
||||
export default StreamSettingsForm;
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ const SystemSettingsForm = React.memo(({ active }) => {
|
|||
<NumberInput
|
||||
label="Maximum System Events"
|
||||
description="Number of events to retain (minimum: 10, maximum: 1000)"
|
||||
value={form.values['max-system-events'] || 100}
|
||||
value={form.values['max_system_events'] || 100}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue('max-system-events', value);
|
||||
form.setFieldValue('max_system_events', value);
|
||||
}}
|
||||
min={10}
|
||||
max={1000}
|
||||
|
|
@ -81,4 +81,4 @@ const SystemSettingsForm = React.memo(({ active }) => {
|
|||
);
|
||||
});
|
||||
|
||||
export default SystemSettingsForm;
|
||||
export default SystemSettingsForm;
|
||||
|
|
|
|||
|
|
@ -45,12 +45,11 @@ const UiSettingsForm = React.memo(() => {
|
|||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
const tzSetting = settings['system-time-zone'];
|
||||
if (tzSetting?.value) {
|
||||
const systemSettings = settings['system_settings'];
|
||||
const tzValue = systemSettings?.value?.time_zone;
|
||||
if (tzValue) {
|
||||
timeZoneSyncedRef.current = true;
|
||||
setTimeZone((prev) =>
|
||||
prev === tzSetting.value ? prev : tzSetting.value
|
||||
);
|
||||
setTimeZone((prev) => (prev === tzValue ? prev : tzValue));
|
||||
} else if (!timeZoneSyncedRef.current && timeZone) {
|
||||
timeZoneSyncedRef.current = true;
|
||||
persistTimeZoneSetting(timeZone);
|
||||
|
|
@ -141,4 +140,4 @@ const UiSettingsForm = React.memo(() => {
|
|||
);
|
||||
});
|
||||
|
||||
export default UiSettingsForm;
|
||||
export default UiSettingsForm;
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ const StreamProfiles = () => {
|
|||
};
|
||||
|
||||
const deleteStreamProfile = async (id) => {
|
||||
if (id == settings['default-stream-profile'].value) {
|
||||
if (id == settings.default_stream_profile) {
|
||||
notifications.show({
|
||||
title: 'Cannot delete default stream-profile',
|
||||
color: 'red.5',
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ const UserAgentsTable = () => {
|
|||
|
||||
const deleteUserAgent = async (ids) => {
|
||||
if (Array.isArray(ids)) {
|
||||
if (ids.includes(settings['default-user-agent'].value)) {
|
||||
if (ids.includes(settings.default_user_agent)) {
|
||||
notifications.show({
|
||||
title: 'Cannot delete default user-agent',
|
||||
color: 'red.5',
|
||||
|
|
@ -137,7 +137,7 @@ const UserAgentsTable = () => {
|
|||
|
||||
await API.deleteUserAgents(ids);
|
||||
} else {
|
||||
if (ids == settings['default-user-agent'].value) {
|
||||
if (ids == settings.default_user_agent) {
|
||||
notifications.show({
|
||||
title: 'Cannot delete default user-agent',
|
||||
color: 'red.5',
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ import {
|
|||
useTimeHelpers,
|
||||
} from '../../utils/dateTimeUtils.js';
|
||||
import { categorizeRecordings } from '../../utils/pages/DVRUtils.js';
|
||||
import { getPosterUrl, getRecordingUrl, getShowVideoUrl } from '../../utils/cards/RecordingCardUtils.js';
|
||||
import {
|
||||
getPosterUrl,
|
||||
getRecordingUrl,
|
||||
getShowVideoUrl,
|
||||
} from '../../utils/cards/RecordingCardUtils.js';
|
||||
|
||||
vi.mock('../../store/channels');
|
||||
vi.mock('../../store/settings');
|
||||
|
|
@ -53,17 +57,28 @@ vi.mock('../../components/cards/RecordingCard', () => ({
|
|||
<span>{recording.custom_properties?.Title || 'Recording'}</span>
|
||||
<button onClick={() => onOpenDetails(recording)}>Open Details</button>
|
||||
{recording.custom_properties?.rule && (
|
||||
<button onClick={() => onOpenRecurring(recording)}>Open Recurring</button>
|
||||
<button onClick={() => onOpenRecurring(recording)}>
|
||||
Open Recurring
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../components/forms/RecordingDetailsModal', () => ({
|
||||
default: ({ opened, onClose, recording, onEdit, onWatchLive, onWatchRecording }) =>
|
||||
default: ({
|
||||
opened,
|
||||
onClose,
|
||||
recording,
|
||||
onEdit,
|
||||
onWatchLive,
|
||||
onWatchRecording,
|
||||
}) =>
|
||||
opened ? (
|
||||
<div data-testid="details-modal">
|
||||
<div data-testid="modal-title">{recording?.custom_properties?.Title}</div>
|
||||
<div data-testid="modal-title">
|
||||
{recording?.custom_properties?.Title}
|
||||
</div>
|
||||
<button onClick={onClose}>Close Modal</button>
|
||||
<button onClick={onEdit}>Edit</button>
|
||||
<button onClick={onWatchLive}>Watch Live</button>
|
||||
|
|
@ -137,7 +152,7 @@ describe('DVRPage', () => {
|
|||
|
||||
const defaultSettingsState = {
|
||||
settings: {
|
||||
'system-time-zone': { value: 'America/New_York' },
|
||||
system_settings: { value: { time_zone: 'America/New_York' } },
|
||||
},
|
||||
environment: {
|
||||
env_mode: 'production',
|
||||
|
|
@ -178,12 +193,10 @@ describe('DVRPage', () => {
|
|||
getPosterUrl.mockImplementation((recording) =>
|
||||
recording?.id ? `http://poster.url/${recording.id}` : null
|
||||
);
|
||||
getRecordingUrl.mockImplementation((custom_properties) =>
|
||||
custom_properties?.recording_url
|
||||
);
|
||||
getShowVideoUrl.mockImplementation((channel) =>
|
||||
channel?.stream_url
|
||||
getRecordingUrl.mockImplementation(
|
||||
(custom_properties) => custom_properties?.recording_url
|
||||
);
|
||||
getShowVideoUrl.mockImplementation((channel) => channel?.stream_url);
|
||||
|
||||
useChannelsStore.mockImplementation((selector) => {
|
||||
return selector ? selector(defaultChannelsState) : defaultChannelsState;
|
||||
|
|
@ -295,7 +308,9 @@ describe('DVRPage', () => {
|
|||
const state = {
|
||||
...defaultChannelsState,
|
||||
recordings: [recording],
|
||||
channels: { 1: { id: 1, name: 'Channel 1', stream_url: 'http://stream.url' } },
|
||||
channels: {
|
||||
1: { id: 1, name: 'Channel 1', stream_url: 'http://stream.url' },
|
||||
},
|
||||
};
|
||||
return selector ? selector(state) : state;
|
||||
});
|
||||
|
|
@ -362,7 +377,7 @@ describe('DVRPage', () => {
|
|||
end_time: now.add(1, 'hour').toISOString(),
|
||||
custom_properties: {
|
||||
Title: 'Recurring Show',
|
||||
rule: { id: 100 }
|
||||
rule: { id: 100 },
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -538,4 +553,4 @@ describe('DVRPage', () => {
|
|||
expect(mockShowVideo).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ describe('dateTimeUtils', () => {
|
|||
const setTimeZone = vi.fn();
|
||||
useLocalStorage.mockReturnValue(['America/New_York', setTimeZone]);
|
||||
useSettingsStore.mockReturnValue({
|
||||
'system-time-zone': { value: 'America/Los_Angeles' }
|
||||
'system_settings': { value: { time_zone: 'America/Los_Angeles' } }
|
||||
});
|
||||
|
||||
renderHook(() => dateTimeUtils.useUserTimeZone());
|
||||
|
|
|
|||
|
|
@ -7,15 +7,14 @@ import {
|
|||
toFriendlyDuration,
|
||||
} from '../dateTimeUtils.js';
|
||||
|
||||
// Parse proxy settings to get buffering_speed
|
||||
// Get buffering_speed from proxy settings
|
||||
export const getBufferingSpeedThreshold = (proxySetting) => {
|
||||
try {
|
||||
if (proxySetting?.value) {
|
||||
const proxySettings = JSON.parse(proxySetting.value);
|
||||
return parseFloat(proxySettings.buffering_speed) || 1.0;
|
||||
return parseFloat(proxySetting.value.buffering_speed) || 1.0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing proxy settings:', error);
|
||||
console.error('Error getting buffering speed:', error);
|
||||
}
|
||||
return 1.0; // Default fallback
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export const useUserTimeZone = () => {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
const tz = settings?.['system-time-zone']?.value;
|
||||
const tz = settings?.['system_settings']?.value?.time_zone;
|
||||
if (tz && tz !== timeZone) {
|
||||
setTimeZone(tz);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ export const uploadComskipIni = async (file) => {
|
|||
|
||||
export const getDvrSettingsFormInitialValues = () => {
|
||||
return {
|
||||
'dvr-tv-template': '',
|
||||
'dvr-movie-template': '',
|
||||
'dvr-tv-fallback-template': '',
|
||||
'dvr-movie-fallback-template': '',
|
||||
'dvr-comskip-enabled': false,
|
||||
'dvr-comskip-custom-path': '',
|
||||
'dvr-pre-offset-minutes': 0,
|
||||
'dvr-post-offset-minutes': 0,
|
||||
'tv_template': '',
|
||||
'movie_template': '',
|
||||
'tv_fallback_template': '',
|
||||
'movie_fallback_template': '',
|
||||
'comskip_enabled': false,
|
||||
'comskip_custom_path': '',
|
||||
'pre_offset_minutes': 0,
|
||||
'post_offset_minutes': 0,
|
||||
};
|
||||
};
|
||||
|
|
@ -2,18 +2,18 @@ import { isNotEmpty } from '@mantine/form';
|
|||
|
||||
export const getStreamSettingsFormInitialValues = () => {
|
||||
return {
|
||||
'default-user-agent': '',
|
||||
'default-stream-profile': '',
|
||||
'preferred-region': '',
|
||||
'auto-import-mapped-files': true,
|
||||
'm3u-hash-key': [],
|
||||
default_user_agent: '',
|
||||
default_stream_profile: '',
|
||||
preferred_region: '',
|
||||
auto_import_mapped_files: true,
|
||||
m3u_hash_key: [],
|
||||
};
|
||||
};
|
||||
|
||||
export const getStreamSettingsFormValidation = () => {
|
||||
return {
|
||||
'default-user-agent': isNotEmpty('Select a user agent'),
|
||||
'default-stream-profile': isNotEmpty('Select a stream profile'),
|
||||
'preferred-region': isNotEmpty('Select a region'),
|
||||
default_user_agent: isNotEmpty('Select a user agent'),
|
||||
default_stream_profile: isNotEmpty('Select a stream profile'),
|
||||
preferred_region: isNotEmpty('Select a region'),
|
||||
};
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
export const getSystemSettingsFormInitialValues = () => {
|
||||
return {
|
||||
'max-system-events': 100,
|
||||
max_system_events: 100,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { createSetting, updateSetting } from '../../pages/SettingsUtils.js';
|
||||
|
||||
export const saveTimeZoneSetting = async (tzValue, settings) => {
|
||||
const existing = settings['system-time-zone'];
|
||||
const existing = settings['system_settings'];
|
||||
const currentValue = existing?.value || {};
|
||||
const newValue = { ...currentValue, time_zone: tzValue };
|
||||
|
||||
if (existing?.id) {
|
||||
await updateSetting({ ...existing, value: tzValue });
|
||||
await updateSetting({ ...existing, value: newValue });
|
||||
} else {
|
||||
await createSetting({
|
||||
key: 'system-time-zone',
|
||||
name: 'System Time Zone',
|
||||
value: tzValue,
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -57,14 +57,14 @@ describe('DvrSettingsFormUtils', () => {
|
|||
const result = DvrSettingsFormUtils.getDvrSettingsFormInitialValues();
|
||||
|
||||
expect(result).toEqual({
|
||||
'dvr-tv-template': '',
|
||||
'dvr-movie-template': '',
|
||||
'dvr-tv-fallback-template': '',
|
||||
'dvr-movie-fallback-template': '',
|
||||
'dvr-comskip-enabled': false,
|
||||
'dvr-comskip-custom-path': '',
|
||||
'dvr-pre-offset-minutes': 0,
|
||||
'dvr-post-offset-minutes': 0,
|
||||
'tv_template': '',
|
||||
'movie_template': '',
|
||||
'tv_fallback_template': '',
|
||||
'movie_fallback_template': '',
|
||||
'comskip_enabled': false,
|
||||
'comskip_custom_path': '',
|
||||
'pre_offset_minutes': 0,
|
||||
'post_offset_minutes': 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -79,14 +79,14 @@ describe('DvrSettingsFormUtils', () => {
|
|||
it('should have correct default types', () => {
|
||||
const result = DvrSettingsFormUtils.getDvrSettingsFormInitialValues();
|
||||
|
||||
expect(typeof result['dvr-tv-template']).toBe('string');
|
||||
expect(typeof result['dvr-movie-template']).toBe('string');
|
||||
expect(typeof result['dvr-tv-fallback-template']).toBe('string');
|
||||
expect(typeof result['dvr-movie-fallback-template']).toBe('string');
|
||||
expect(typeof result['dvr-comskip-enabled']).toBe('boolean');
|
||||
expect(typeof result['dvr-comskip-custom-path']).toBe('string');
|
||||
expect(typeof result['dvr-pre-offset-minutes']).toBe('number');
|
||||
expect(typeof result['dvr-post-offset-minutes']).toBe('number');
|
||||
expect(typeof result['tv_template']).toBe('string');
|
||||
expect(typeof result['movie_template']).toBe('string');
|
||||
expect(typeof result['tv_fallback_template']).toBe('string');
|
||||
expect(typeof result['movie_fallback_template']).toBe('string');
|
||||
expect(typeof result['comskip_enabled']).toBe('boolean');
|
||||
expect(typeof result['comskip_custom_path']).toBe('string');
|
||||
expect(typeof result['pre_offset_minutes']).toBe('number');
|
||||
expect(typeof result['post_offset_minutes']).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,26 +16,26 @@ describe('StreamSettingsFormUtils', () => {
|
|||
const result = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
|
||||
expect(result).toEqual({
|
||||
'default-user-agent': '',
|
||||
'default-stream-profile': '',
|
||||
'preferred-region': '',
|
||||
'auto-import-mapped-files': true,
|
||||
'm3u-hash-key': []
|
||||
'default_user_agent': '',
|
||||
'default_stream_profile': '',
|
||||
'preferred_region': '',
|
||||
'auto_import_mapped_files': true,
|
||||
'm3u_hash_key': []
|
||||
});
|
||||
});
|
||||
|
||||
it('should return boolean true for auto-import-mapped-files', () => {
|
||||
const result = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
|
||||
expect(result['auto-import-mapped-files']).toBe(true);
|
||||
expect(typeof result['auto-import-mapped-files']).toBe('boolean');
|
||||
expect(result['auto_import_mapped_files']).toBe(true);
|
||||
expect(typeof result['auto_import_mapped_files']).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should return empty array for m3u-hash-key', () => {
|
||||
const result = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
|
||||
expect(result['m3u-hash-key']).toEqual([]);
|
||||
expect(Array.isArray(result['m3u-hash-key'])).toBe(true);
|
||||
expect(result['m3u_hash_key']).toEqual([]);
|
||||
expect(Array.isArray(result['m3u_hash_key'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a new object each time', () => {
|
||||
|
|
@ -50,7 +50,7 @@ describe('StreamSettingsFormUtils', () => {
|
|||
const result1 = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result2 = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
|
||||
expect(result1['m3u-hash-key']).not.toBe(result2['m3u-hash-key']);
|
||||
expect(result1['m3u_hash_key']).not.toBe(result2['m3u_hash_key']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -59,25 +59,25 @@ describe('StreamSettingsFormUtils', () => {
|
|||
const result = StreamSettingsFormUtils.getStreamSettingsFormValidation();
|
||||
|
||||
expect(Object.keys(result)).toEqual([
|
||||
'default-user-agent',
|
||||
'default-stream-profile',
|
||||
'preferred-region'
|
||||
'default_user_agent',
|
||||
'default_stream_profile',
|
||||
'preferred_region'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use isNotEmpty validator for default-user-agent', () => {
|
||||
it('should use isNotEmpty validator for default_user_agent', () => {
|
||||
StreamSettingsFormUtils.getStreamSettingsFormValidation();
|
||||
|
||||
expect(isNotEmpty).toHaveBeenCalledWith('Select a user agent');
|
||||
});
|
||||
|
||||
it('should use isNotEmpty validator for default-stream-profile', () => {
|
||||
it('should use isNotEmpty validator for default_stream_profile', () => {
|
||||
StreamSettingsFormUtils.getStreamSettingsFormValidation();
|
||||
|
||||
expect(isNotEmpty).toHaveBeenCalledWith('Select a stream profile');
|
||||
});
|
||||
|
||||
it('should use isNotEmpty validator for preferred-region', () => {
|
||||
it('should use isNotEmpty validator for preferred_region', () => {
|
||||
StreamSettingsFormUtils.getStreamSettingsFormValidation();
|
||||
|
||||
expect(isNotEmpty).toHaveBeenCalledWith('Select a region');
|
||||
|
|
@ -86,21 +86,21 @@ describe('StreamSettingsFormUtils', () => {
|
|||
it('should not include validation for auto-import-mapped-files', () => {
|
||||
const result = StreamSettingsFormUtils.getStreamSettingsFormValidation();
|
||||
|
||||
expect(result).not.toHaveProperty('auto-import-mapped-files');
|
||||
expect(result).not.toHaveProperty('auto_import_mapped_files');
|
||||
});
|
||||
|
||||
it('should not include validation for m3u-hash-key', () => {
|
||||
const result = StreamSettingsFormUtils.getStreamSettingsFormValidation();
|
||||
|
||||
expect(result).not.toHaveProperty('m3u-hash-key');
|
||||
expect(result).not.toHaveProperty('m3u_hash_key');
|
||||
});
|
||||
|
||||
it('should return correct validation error messages', () => {
|
||||
const result = StreamSettingsFormUtils.getStreamSettingsFormValidation();
|
||||
|
||||
expect(result['default-user-agent']).toBe('Select a user agent');
|
||||
expect(result['default-stream-profile']).toBe('Select a stream profile');
|
||||
expect(result['preferred-region']).toBe('Select a region');
|
||||
expect(result['default_user_agent']).toBe('Select a user agent');
|
||||
expect(result['default_stream_profile']).toBe('Select a stream profile');
|
||||
expect(result['preferred_region']).toBe('Select a region');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ describe('SystemSettingsFormUtils', () => {
|
|||
const result = SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
|
||||
expect(result).toEqual({
|
||||
'max-system-events': 100
|
||||
'max_system_events': 100
|
||||
});
|
||||
});
|
||||
|
||||
it('should return number value for max-system-events', () => {
|
||||
const result = SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
|
||||
expect(result['max-system-events']).toBe(100);
|
||||
expect(typeof result['max-system-events']).toBe('number');
|
||||
expect(result['max_system_events']).toBe(100);
|
||||
expect(typeof result['max_system_events']).toBe('number');
|
||||
});
|
||||
|
||||
it('should return a new object each time', () => {
|
||||
|
|
@ -29,7 +29,7 @@ describe('SystemSettingsFormUtils', () => {
|
|||
it('should have max-system-events property', () => {
|
||||
const result = SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
|
||||
expect(result).toHaveProperty('max-system-events');
|
||||
expect(result).toHaveProperty('max_system_events');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,26 +17,95 @@ export const rehashStreams = async () => {
|
|||
};
|
||||
|
||||
export const saveChangedSettings = async (settings, changedSettings) => {
|
||||
for (const updatedKey in changedSettings) {
|
||||
const existing = settings[updatedKey];
|
||||
// Group changes by their setting group based on field name prefixes
|
||||
const groupedChanges = {
|
||||
stream_settings: {},
|
||||
dvr_settings: {},
|
||||
backup_settings: {},
|
||||
system_settings: {},
|
||||
};
|
||||
|
||||
// Map of field prefixes to their groups
|
||||
const streamFields = ['default_user_agent', 'default_stream_profile', 'm3u_hash_key', 'preferred_region', 'auto_import_mapped_files'];
|
||||
const dvrFields = ['tv_template', 'movie_template', 'tv_fallback_dir', 'tv_fallback_template', 'movie_fallback_template',
|
||||
'comskip_enabled', 'comskip_custom_path', 'pre_offset_minutes', 'post_offset_minutes', 'series_rules'];
|
||||
const backupFields = ['schedule_enabled', 'schedule_frequency', 'schedule_time', 'schedule_day_of_week',
|
||||
'retention_count', 'schedule_cron_expression'];
|
||||
const systemFields = ['time_zone', 'max_system_events'];
|
||||
|
||||
for (const formKey in changedSettings) {
|
||||
let value = changedSettings[formKey];
|
||||
|
||||
// Handle special grouped settings (proxy_settings and network_access)
|
||||
if (formKey === 'proxy_settings') {
|
||||
const existing = settings['proxy_settings'];
|
||||
if (existing?.id) {
|
||||
await updateSetting({ ...existing, value });
|
||||
} else {
|
||||
await createSetting({ key: 'proxy_settings', name: 'Proxy Settings', value });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (formKey === 'network_access') {
|
||||
const existing = settings['network_access'];
|
||||
if (existing?.id) {
|
||||
await updateSetting({ ...existing, value });
|
||||
} else {
|
||||
await createSetting({ key: 'network_access', name: 'Network Access', value });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Type conversions for proper storage
|
||||
if (formKey === 'm3u_hash_key' && Array.isArray(value)) {
|
||||
value = value.join(',');
|
||||
}
|
||||
|
||||
if (['default_user_agent', 'default_stream_profile'].includes(formKey) && value != null) {
|
||||
value = parseInt(value, 10);
|
||||
}
|
||||
|
||||
const numericFields = ['pre_offset_minutes', 'post_offset_minutes', 'retention_count', 'schedule_day_of_week', 'max_system_events'];
|
||||
if (numericFields.includes(formKey) && value != null) {
|
||||
value = typeof value === 'number' ? value : parseInt(value, 10);
|
||||
}
|
||||
|
||||
const booleanFields = ['comskip_enabled', 'schedule_enabled', 'auto_import_mapped_files'];
|
||||
if (booleanFields.includes(formKey) && value != null) {
|
||||
value = typeof value === 'boolean' ? value : Boolean(value);
|
||||
}
|
||||
|
||||
// Route to appropriate group
|
||||
if (streamFields.includes(formKey)) {
|
||||
groupedChanges.stream_settings[formKey] = value;
|
||||
} else if (dvrFields.includes(formKey)) {
|
||||
groupedChanges.dvr_settings[formKey] = value;
|
||||
} else if (backupFields.includes(formKey)) {
|
||||
groupedChanges.backup_settings[formKey] = value;
|
||||
} else if (systemFields.includes(formKey)) {
|
||||
groupedChanges.system_settings[formKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Update each group that has changes
|
||||
for (const [groupKey, changes] of Object.entries(groupedChanges)) {
|
||||
if (Object.keys(changes).length === 0) continue;
|
||||
|
||||
const existing = settings[groupKey];
|
||||
const currentValue = existing?.value || {};
|
||||
const newValue = { ...currentValue, ...changes };
|
||||
|
||||
if (existing?.id) {
|
||||
const result = await updateSetting({
|
||||
...existing,
|
||||
value: changedSettings[updatedKey],
|
||||
});
|
||||
// API functions return undefined on error
|
||||
const result = await updateSetting({ ...existing, value: newValue });
|
||||
if (!result) {
|
||||
throw new Error('Failed to update setting');
|
||||
throw new Error(`Failed to update ${groupKey}`);
|
||||
}
|
||||
} else {
|
||||
const result = await createSetting({
|
||||
key: updatedKey,
|
||||
name: updatedKey.replace(/-/g, ' '),
|
||||
value: changedSettings[updatedKey],
|
||||
});
|
||||
// API functions return undefined on error
|
||||
const name = groupKey.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
||||
const result = await createSetting({ key: groupKey, name: name, value: newValue });
|
||||
if (!result) {
|
||||
throw new Error('Failed to create setting');
|
||||
throw new Error(`Failed to create ${groupKey}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,59 +115,104 @@ export const getChangedSettings = (values, settings) => {
|
|||
const changedSettings = {};
|
||||
|
||||
for (const settingKey in values) {
|
||||
// Skip grouped settings that are handled by their own dedicated forms
|
||||
if (settingKey === 'proxy_settings' || settingKey === 'network_access') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only compare against existing value if the setting exists
|
||||
const existing = settings[settingKey];
|
||||
|
||||
// Convert array values (like m3u-hash-key) to comma-separated strings
|
||||
const stringValue = Array.isArray(values[settingKey])
|
||||
? values[settingKey].join(',')
|
||||
: `${values[settingKey]}`;
|
||||
// Convert array values (like m3u_hash_key) to comma-separated strings for comparison
|
||||
let compareValue;
|
||||
let actualValue = values[settingKey];
|
||||
|
||||
if (Array.isArray(actualValue)) {
|
||||
actualValue = actualValue.join(',');
|
||||
compareValue = actualValue;
|
||||
} else {
|
||||
compareValue = String(actualValue);
|
||||
}
|
||||
|
||||
// Skip empty values to avoid validation errors
|
||||
if (!stringValue) {
|
||||
if (!compareValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
// Create new setting on save
|
||||
changedSettings[settingKey] = stringValue;
|
||||
} else if (stringValue !== String(existing.value)) {
|
||||
// If the user changed the setting's value from what's in the DB:
|
||||
changedSettings[settingKey] = stringValue;
|
||||
// Create new setting on save - preserve original type
|
||||
changedSettings[settingKey] = actualValue;
|
||||
} else if (compareValue !== String(existing.value)) {
|
||||
// If the user changed the setting's value from what's in the DB - preserve original type
|
||||
changedSettings[settingKey] = actualValue;
|
||||
}
|
||||
}
|
||||
return changedSettings;
|
||||
};
|
||||
|
||||
export const parseSettings = (settings) => {
|
||||
return Object.entries(settings).reduce((acc, [key, value]) => {
|
||||
// Modify each value based on its own properties
|
||||
switch (value.value) {
|
||||
case 'true':
|
||||
value.value = true;
|
||||
break;
|
||||
case 'false':
|
||||
value.value = false;
|
||||
break;
|
||||
}
|
||||
const parsed = {};
|
||||
|
||||
let val = null;
|
||||
switch (key) {
|
||||
case 'm3u-hash-key':
|
||||
// Split comma-separated string, filter out empty strings
|
||||
val = value.value ? value.value.split(',').filter((v) => v) : [];
|
||||
break;
|
||||
case 'dvr-pre-offset-minutes':
|
||||
case 'dvr-post-offset-minutes':
|
||||
val = Number.parseInt(value.value || '0', 10);
|
||||
if (Number.isNaN(val)) val = 0;
|
||||
break;
|
||||
default:
|
||||
val = value.value;
|
||||
break;
|
||||
}
|
||||
// Stream settings - direct mapping with underscore keys
|
||||
const streamSettings = settings['stream_settings']?.value;
|
||||
if (streamSettings && typeof streamSettings === 'object') {
|
||||
// IDs must be strings for Select components
|
||||
parsed.default_user_agent = streamSettings.default_user_agent != null ? String(streamSettings.default_user_agent) : null;
|
||||
parsed.default_stream_profile = streamSettings.default_stream_profile != null ? String(streamSettings.default_stream_profile) : null;
|
||||
parsed.preferred_region = streamSettings.preferred_region;
|
||||
parsed.auto_import_mapped_files = streamSettings.auto_import_mapped_files;
|
||||
|
||||
acc[key] = val;
|
||||
return acc;
|
||||
}, {});
|
||||
// m3u_hash_key should be array
|
||||
const hashKey = streamSettings.m3u_hash_key;
|
||||
if (typeof hashKey === 'string') {
|
||||
parsed.m3u_hash_key = hashKey ? hashKey.split(',').filter((v) => v) : [];
|
||||
} else if (Array.isArray(hashKey)) {
|
||||
parsed.m3u_hash_key = hashKey;
|
||||
} else {
|
||||
parsed.m3u_hash_key = [];
|
||||
}
|
||||
}
|
||||
|
||||
// DVR settings - direct mapping with underscore keys
|
||||
const dvrSettings = settings['dvr_settings']?.value;
|
||||
if (dvrSettings && typeof dvrSettings === 'object') {
|
||||
parsed.tv_template = dvrSettings.tv_template;
|
||||
parsed.movie_template = dvrSettings.movie_template;
|
||||
parsed.tv_fallback_dir = dvrSettings.tv_fallback_dir;
|
||||
parsed.tv_fallback_template = dvrSettings.tv_fallback_template;
|
||||
parsed.movie_fallback_template = dvrSettings.movie_fallback_template;
|
||||
parsed.comskip_enabled = typeof dvrSettings.comskip_enabled === 'boolean' ? dvrSettings.comskip_enabled : Boolean(dvrSettings.comskip_enabled);
|
||||
parsed.comskip_custom_path = dvrSettings.comskip_custom_path;
|
||||
parsed.pre_offset_minutes = typeof dvrSettings.pre_offset_minutes === 'number' ? dvrSettings.pre_offset_minutes : parseInt(dvrSettings.pre_offset_minutes, 10) || 0;
|
||||
parsed.post_offset_minutes = typeof dvrSettings.post_offset_minutes === 'number' ? dvrSettings.post_offset_minutes : parseInt(dvrSettings.post_offset_minutes, 10) || 0;
|
||||
parsed.series_rules = dvrSettings.series_rules;
|
||||
}
|
||||
|
||||
// Backup settings - direct mapping with underscore keys
|
||||
const backupSettings = settings['backup_settings']?.value;
|
||||
if (backupSettings && typeof backupSettings === 'object') {
|
||||
parsed.schedule_enabled = typeof backupSettings.schedule_enabled === 'boolean' ? backupSettings.schedule_enabled : Boolean(backupSettings.schedule_enabled);
|
||||
parsed.schedule_frequency = String(backupSettings.schedule_frequency || '');
|
||||
parsed.schedule_time = String(backupSettings.schedule_time || '');
|
||||
parsed.schedule_day_of_week = typeof backupSettings.schedule_day_of_week === 'number' ? backupSettings.schedule_day_of_week : parseInt(backupSettings.schedule_day_of_week, 10) || 0;
|
||||
parsed.retention_count = typeof backupSettings.retention_count === 'number' ? backupSettings.retention_count : parseInt(backupSettings.retention_count, 10) || 0;
|
||||
parsed.schedule_cron_expression = String(backupSettings.schedule_cron_expression || '');
|
||||
}
|
||||
|
||||
// System settings - direct mapping with underscore keys
|
||||
const systemSettings = settings['system_settings']?.value;
|
||||
if (systemSettings && typeof systemSettings === 'object') {
|
||||
parsed.time_zone = String(systemSettings.time_zone || '');
|
||||
parsed.max_system_events = typeof systemSettings.max_system_events === 'number' ? systemSettings.max_system_events : parseInt(systemSettings.max_system_events, 10) || 100;
|
||||
}
|
||||
|
||||
// Proxy and network access are already grouped objects
|
||||
if (settings['proxy_settings']?.value) {
|
||||
parsed.proxy_settings = settings['proxy_settings'].value;
|
||||
}
|
||||
if (settings['network_access']?.value) {
|
||||
parsed.network_access = settings['network_access'].value;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue