diff --git a/web2/src/helpers/util.ts b/web2/src/helpers/util.ts index 07ada84f2..901046e63 100644 --- a/web2/src/helpers/util.ts +++ b/web2/src/helpers/util.ts @@ -50,3 +50,7 @@ export const fromStringResolution = ( const [h, w] = res.split('x', 1); return { widthPx: parseInt(w), heightPx: parseInt(h) }; }; + +export const hasOnlyDigits = (value: string) => { + return /^-?\d+$/.test(value); +}; diff --git a/web2/src/pages/settings/FfmpegSettingsPage.tsx b/web2/src/pages/settings/FfmpegSettingsPage.tsx index f1b7f7b41..19c1385be 100644 --- a/web2/src/pages/settings/FfmpegSettingsPage.tsx +++ b/web2/src/pages/settings/FfmpegSettingsPage.tsx @@ -1,3 +1,4 @@ +import React, { useEffect } from 'react'; import { Button, Checkbox, @@ -6,21 +7,29 @@ import { FormControlLabel, InputLabel, MenuItem, - Paper, Stack, Select, TextField, Typography, + SelectChangeEvent, + Alert, } from '@mui/material'; import { useFfmpegSettings } from '../../hooks/settingsHooks.ts'; +import { hasOnlyDigits } from '../../helpers/util.ts'; + +const supportedVideoBuffer = [ + { value: 0, string: '0 Seconds' }, + { value: 1000, string: '1 Second' }, + { value: 2000, string: '2 Seconds' }, + { value: 3000, string: '3 Seconds' }, + { value: 4000, string: '4 Seconds' }, + { value: 5000, string: '5 Seconds' }, + { value: 10000, string: '10 Seconds' }, +]; export default function FfmpegSettingsPage() { const { data, isPending, error } = useFfmpegSettings(); - if (isPending || error) { - return
; - } - const defaultFFMPEGSettings = { ffmpegExecutablePath: '/usr/bin/ffmpeg', numThreads: 4, @@ -29,68 +38,160 @@ export default function FfmpegSettingsPage() { enableTranscoding: false, }; + const [ffmpegExecutablePath, setFfmpegExecutablePath] = + React.useState(defaultFFMPEGSettings.ffmpegExecutablePath); + + const [numThreads, setNumThreads] = React.useState( + defaultFFMPEGSettings.numThreads.toString(), + ); + + const [enableLogging, setEnableLogging] = React.useState( + defaultFFMPEGSettings.enableLogging, + ); + + const [videoBufferSize, setVideoBufferSize] = React.useState( + defaultFFMPEGSettings.videoBufferSize.toString(), + ); + + const [enableTranscoding, setEnableTranscoding] = React.useState( + defaultFFMPEGSettings.enableTranscoding, + ); + + const [showFormError, setShowFormError] = React.useState(false); + + useEffect(() => { + setFfmpegExecutablePath( + data?.ffmpegExecutablePath || defaultFFMPEGSettings.ffmpegExecutablePath, + ); + + setNumThreads( + data?.numThreads.toString() || + defaultFFMPEGSettings.numThreads.toString(), + ); + + setEnableLogging( + data?.enableLogging || defaultFFMPEGSettings.enableLogging, + ); + + setVideoBufferSize( + data?.videoBufferSize.toString() || + defaultFFMPEGSettings.videoBufferSize.toString(), + ); + + setEnableTranscoding( + data?.enableTranscoding || defaultFFMPEGSettings.enableTranscoding, + ); + }, [data]); + + const handleFfmpegExecutablePath = ( + event: React.ChangeEvent, + ) => { + setFfmpegExecutablePath(event.target.value); + }; + + const handleNumThreads = (event: React.ChangeEvent) => { + setNumThreads(event.target.value); + }; + + const handleEnableLogging = () => { + setEnableLogging(!enableLogging); + }; + + const handleVideoBufferSize = (event: SelectChangeEvent) => { + setVideoBufferSize(event.target.value); + }; + + const handleEnableTranscoding = () => { + setEnableTranscoding(!enableTranscoding); + }; + + const handleValidateFields = (event: React.FocusEvent) => { + setShowFormError(!hasOnlyDigits(event.target.value)); + }; + + if (isPending || error) { + return
; + } + return ( <> - - - - - - Miscellaneous Options - - - - } - label="Log FFMPEG to console" - /> - - - Video Buffer - - - Note: If you experience playback issues upon stream start, try - increasing this. - - - - Transcoding Features - - - } - label="Enable FFMPEG Transcoding" - /> - - Transcoding is required for some features like channel overlay and - measures to prevent issues when switching episodes. The trade-off is - quality loss and additional computing resource requirements. - - - + + + + + Miscellaneous Options + + {showFormError && ( + + Invalid input. Please make sure number of threads is a number + + )} + + + + } + label="Log FFMPEG to console" + /> + + + Video Buffer + + + Note: If you experience playback issues upon stream start, try + increasing this. + + + + Transcoding Features + + + + } + label="Enable FFMPEG Transcoding" + /> + + Transcoding is required for some features like channel overlay and + measures to prevent issues when switching episodes. The trade-off is + quality loss and additional computing resource requirements. + + - + ); diff --git a/web2/src/pages/settings/GeneralSettingsPage.tsx b/web2/src/pages/settings/GeneralSettingsPage.tsx index cc81a091c..bf55c217b 100644 --- a/web2/src/pages/settings/GeneralSettingsPage.tsx +++ b/web2/src/pages/settings/GeneralSettingsPage.tsx @@ -1,9 +1,8 @@ -import { Button, Paper, Stack } from '@mui/material'; +import { Button, Stack } from '@mui/material'; export default function GeneralSettingsPage() { return ( <> - diff --git a/web2/src/pages/settings/HdhrSettingsPage.tsx b/web2/src/pages/settings/HdhrSettingsPage.tsx index 38c62152a..923ad04be 100644 --- a/web2/src/pages/settings/HdhrSettingsPage.tsx +++ b/web2/src/pages/settings/HdhrSettingsPage.tsx @@ -1,12 +1,9 @@ import { - Box, Button, Checkbox, FormControl, FormLabel, - FormControlLabel, FormHelperText, - Paper, Stack, TextField, } from '@mui/material'; @@ -28,22 +25,20 @@ export default function HdhrSettingsPage() { return ( <> - - - - Enable SSDP server - * Restart required - - - + + + Enable SSDP server + * Restart required + + diff --git a/web2/src/pages/settings/PlexSettingsPage.tsx b/web2/src/pages/settings/PlexSettingsPage.tsx index c6bdde847..b7eae9b27 100644 --- a/web2/src/pages/settings/PlexSettingsPage.tsx +++ b/web2/src/pages/settings/PlexSettingsPage.tsx @@ -27,6 +27,7 @@ import { Typography, Input, InputAdornment, + SelectChangeEvent, } from '@mui/material'; import { AddCircle, Close, Done, Edit } from '@mui/icons-material'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -49,10 +50,52 @@ const supportedResolutions = [ '3840x2160', ]; +const supportedAudioChannels = [ + '1.0', + '2.0', + '2.1', + '4.0', + '5.0', + '5.1', + '6.1', + '7.1', +]; + +const supportedAudioBoost = [ + { value: 100, string: '0 Seconds' }, + { value: 120, string: '1 Second' }, + { value: 140, string: '2 Seconds' }, + { value: 160, string: '3 Seconds' }, + { value: 180, string: '4 Seconds' }, +]; + const defaultPlexSettings = { - maxPlayableResolution: '1920x1080', - showSubtitles: false, + audioBoost: 100, + audioCodecs: ['ac3'], + directStreamBitrate: 20000, + enableDebugLogging: false, + enableSubtitles: false, + forceDirectPlay: false, + maxAudioChannels: '2.0', + maxPlayableResolution: { + widthPx: 1920, + heightPx: 1080, + }, + maxTranscodeResolution: { + widthPx: 1920, + heightPx: 1080, + }, + mediaBufferSize: 1000, + pathReplace: '', + pathReplaceWith: '', + streamPath: 'plex', + streamProtocol: 'http', + subtitleSize: 100, + transcodeBitrate: 2000, + transcodeMediaBufferSize: 20000, + updatePlayStatus: false, videoCodecs: ['h264', 'hevc', 'mpeg2video', 'av1'], + showSubtitles: false, }; export default function PlexSettingsPage() { @@ -75,27 +118,74 @@ export default function PlexSettingsPage() { ); const [videoCodecs, setVideoCodecs] = React.useState( - streamSettings?.videoCodecs || defaultPlexSettings.videoCodecs, + defaultPlexSettings.videoCodecs, ); - const [maxPlayableResolution, setMaxPlayableResolution] = useState( - defaultPlexSettings.maxPlayableResolution, + const [addVideoCodecs, setAddVideoCodecs] = React.useState(''); + + const [audioCodecs, setAudioCodecs] = React.useState( + defaultPlexSettings.audioCodecs, ); - const [addVideoCodecs, setAddVideoCodecs] = React.useState(''); + const [addAudioCodecs, setAddAudioCodecs] = React.useState(''); + + const [maxAudioChannels, setMaxAudioChannels] = React.useState( + defaultPlexSettings.maxAudioChannels, + ); + + const [directStreamBitrate, setDirectStreamBitrate] = + React.useState(''); + + const [transcodeBitrate, setTranscodeBitrate] = React.useState(''); + + const [mediaBufferSize, setMediaBufferSize] = React.useState(''); + + const [transcodeMediaBufferSize, setTranscodeMediaBufferSize] = + React.useState(''); useEffect(() => { - setVideoCodecs(streamSettings?.videoCodecs || []); + setVideoCodecs( + streamSettings?.videoCodecs || defaultPlexSettings.videoCodecs, + ); setMaxPlayableResolution( toStringResolution( - streamSettings?.maxPlayableResolution || { - widthPx: 1920, - heightPx: 1080, - }, + streamSettings?.maxPlayableResolution || + defaultPlexSettings.maxPlayableResolution, ), ); - }, [streamSettings?.maxPlayableResolution, streamSettings?.videoCodecs]); + + setMaxDirectStreamBitrate( + streamSettings?.directStreamBitrate.toString() || + defaultPlexSettings.directStreamBitrate.toString(), + ); + + setAudioCodecs( + streamSettings?.audioCodecs || defaultPlexSettings.audioCodecs, + ); + + setMaxAudioChannels( + streamSettings?.maxAudioChannels || defaultPlexSettings.maxAudioChannels, + ); + + setDirectStreamBitrate( + streamSettings?.directStreamBitrate.toString() || + defaultPlexSettings.directStreamBitrate.toString(), + ); + + setTranscodeBitrate( + streamSettings?.transcodeBitrate.toString() || + defaultPlexSettings.transcodeBitrate.toString(), + ); + setMediaBufferSize( + streamSettings?.mediaBufferSize.toString() || + defaultPlexSettings.mediaBufferSize.toString(), + ); + setTranscodeMediaBufferSize( + streamSettings?.transcodeMediaBufferSize.toString() || + defaultPlexSettings.transcodeMediaBufferSize.toString(), + ); + }, [streamSettings]); const handleVideoCodecUpdate = () => { if (!addVideoCodecs.length) { @@ -119,6 +209,50 @@ export default function PlexSettingsPage() { setAddVideoCodecs(newVideoCodecs); }; + const handleAudioCodecUpdate = () => { + if (!addAudioCodecs.length) { + return; + } + + // If there is a comma or white space at the end of user input, trim it + let newAudioCodecs: string[] = [addAudioCodecs.replace(/,\s*$/, '')]; + + if (addAudioCodecs?.indexOf(',') > -1) { + newAudioCodecs = newAudioCodecs[0].split(','); + } else { + newAudioCodecs = [newAudioCodecs[0]]; + } + + setAudioCodecs([...audioCodecs, ...newAudioCodecs]); + setAddAudioCodecs(''); + }; + + const handleAudioCodecChange = (newAudioCodecs: string) => { + setAddAudioCodecs(newAudioCodecs); + }; + + const [maxPlayableResolution, setMaxPlayableResolution] = useState( + toStringResolution(defaultPlexSettings.maxPlayableResolution), + ); + + const handleMaxPlayableResolution = (event: SelectChangeEvent) => { + setMaxPlayableResolution(event.target.value as string); + }; + + const [maxDirectStreamBitrate, setMaxDirectStreamBitrate] = useState( + defaultPlexSettings.directStreamBitrate.toString(), + ); + + const handleMaxDirectStreamBitrate = ( + event: React.ChangeEvent, + ) => { + setMaxDirectStreamBitrate(event.target.value); + }; + + const handleMaxAudioChannels = (event: SelectChangeEvent) => { + setMaxAudioChannels(event.target.value as string); + }; + const onSubtitleChange = () => { setShowSubtitles(!showSubtitles); }; @@ -146,8 +280,10 @@ export default function PlexSettingsPage() { ); }; - const removeAudioCodec = (codec: Text) => { - console.log(codec); // TODO + const removeAudioCodec = (codecToDelete: string) => () => { + setAudioCodecs( + (codecs) => codecs?.filter((codec) => codec !== codecToDelete), + ); }; const UIRouteSuccess = true; // TODO @@ -222,7 +358,7 @@ export default function PlexSettingsPage() { const renderServersTable = () => { return ( - + @@ -314,6 +450,7 @@ export default function PlexSettingsPage() { id="max-playable-resolution" label="Max Playable Resolution" value={maxPlayableResolution} + onChange={handleMaxPlayableResolution} > {supportedResolutions.map((res) => ( @@ -326,7 +463,8 @@ export default function PlexSettingsPage() { @@ -366,17 +504,34 @@ export default function PlexSettingsPage() { - Audio Codecs + handleAudioCodecChange(event.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleAudioCodecUpdate(); + } + }} + endAdornment={ + + + + + + } /> - {streamSettings.audioCodecs.map((codec) => ( + {audioCodecs.map((codec) => ( removeAudioCodec(codec)} + onDelete={removeAudioCodec(codec)} sx={{ mr: 1, mt: 1 }} /> ))} @@ -390,16 +545,14 @@ export default function PlexSettingsPage() { labelId="maximum-audio-channels-label" id="maximum-audio-channels" label="Maxium Audio Channels" - value={streamSettings.maxAudioChannels} + value={maxAudioChannels} + onChange={handleMaxAudioChannels} > - 1.0 - 2.0 - 2.1 - 4.0 - 5.0 - 5.1 - 6.1 - 7.1 + {supportedAudioChannels.map((res) => ( + + {res} + + ))} Note: 7.1 audio and on some clients, 6.1, is known to cause @@ -416,11 +569,11 @@ export default function PlexSettingsPage() { label="Audio Boost" value={streamSettings.audioBoost} > - 0 Seconds - 1 Second - 2 Seconds - 3 Seconds - 4 Seconds + {supportedAudioBoost.map((boost) => ( + + {boost.string} + + ))} Note: Only applies when downmixing to stereo. @@ -510,14 +663,14 @@ export default function PlexSettingsPage() { @@ -526,14 +679,14 @@ export default function PlexSettingsPage() { @@ -567,15 +720,15 @@ export default function PlexSettingsPage() { episode change, you will experience playback issues unless ffmpeg transcoding and normalization are also enabled. - + {renderStreamSettings()} {renderAudioSettings()} {renderSubtitleSettings()} - + Miscellaneous Options - {renderMiscSettings()} + {renderMiscSettings()} diff --git a/web2/src/pages/settings/SettingsLayout.tsx b/web2/src/pages/settings/SettingsLayout.tsx index 9a9250b4d..b152576c6 100644 --- a/web2/src/pages/settings/SettingsLayout.tsx +++ b/web2/src/pages/settings/SettingsLayout.tsx @@ -1,4 +1,4 @@ -import { Box, Tab, Tabs, Typography } from '@mui/material'; +import { Box, Paper, Tab, Tabs, Typography } from '@mui/material'; import { isNil } from 'lodash-es'; import { Link, @@ -37,43 +37,46 @@ export default function SettingsLayout() { Settings - - - - - - - - - - - - + + + + + + + + + + + + + + + ); } diff --git a/web2/src/pages/settings/XmlTvSettingsPage.tsx b/web2/src/pages/settings/XmlTvSettingsPage.tsx index 5ec6fb4c2..1d5b9743d 100644 --- a/web2/src/pages/settings/XmlTvSettingsPage.tsx +++ b/web2/src/pages/settings/XmlTvSettingsPage.tsx @@ -1,5 +1,6 @@ +import React, { useEffect } from 'react'; import { - Box, + Alert, Button, Checkbox, FormControl, @@ -10,68 +11,134 @@ import { TextField, } from '@mui/material'; import { useXmlTvSettings } from '../../hooks/settingsHooks.ts'; +import { hasOnlyDigits } from '../../helpers/util.ts'; export default function XmlTvSettingsPage() { const { data, isPending, error } = useXmlTvSettings(); - if (isPending) { - return

XML: Loading...

; - } else if (error) { - return

XML: {error.message}

; - } - const defaultXMLTVSettings = { + outputPath: '', programmingHours: 12, refreshHours: 4, enableImageCache: false, }; + const [outputPath, setOutputPath] = React.useState( + defaultXMLTVSettings.outputPath, + ); + + const [programmingHours, setProgrammingHours] = React.useState( + defaultXMLTVSettings.programmingHours.toString(), + ); + + const [refreshHours, setRefreshHours] = React.useState( + defaultXMLTVSettings.refreshHours.toString(), + ); + + const [enableImageCache, setEnableImageCache] = React.useState( + defaultXMLTVSettings.enableImageCache, + ); + + const [showFormError, setShowFormError] = React.useState(false); + + useEffect(() => { + setOutputPath(data?.outputPath || defaultXMLTVSettings.outputPath); + setProgrammingHours( + data?.programmingHours.toString() || + defaultXMLTVSettings.programmingHours.toString(), + ); + setRefreshHours( + data?.refreshHours.toString() || + defaultXMLTVSettings.refreshHours.toString(), + ); + setEnableImageCache( + data?.enableImageCache || defaultXMLTVSettings.enableImageCache, + ); + }, [data]); + + const handleProgrammingHours = ( + event: React.ChangeEvent, + ) => { + setProgrammingHours(event.target.value); + }; + + const handleRefreshHours = (event: React.ChangeEvent) => { + setRefreshHours(event.target.value); + }; + + const handleEnableImageCache = () => { + setEnableImageCache(!!enableImageCache); + }; + + const handleValidateFields = (event: React.FocusEvent) => { + setShowFormError(!hasOnlyDigits(event.target.value)); + }; + + if (isPending) { + return

XML: Loading...

; + } else if (error) { + return

XML: {error.message}

; + } + return ( <> - + + {showFormError && ( + + Invalid input. Please make sure EPG Hours & Refresh Timer is a number + + )} + + + + + + } + label="Image Cache" /> - - - - - - } label="Image Cache" /> - - If enabled the pictures used for Movie and TV Show posters will be - cached in dizqueTV's .dizqueTV folder and will be delivered by - dizqueTV's server instead of requiring calls to Plex. Note that - using fixed xmltv location in Plex (as opposed to url) will not work - correctly in this case. - - - + + If enabled the pictures used for Movie and TV Show posters will be + cached in dizqueTV's .dizqueTV folder and will be delivered by + dizqueTV's server instead of requiring calls to Plex. Note that using + fixed xmltv location in Plex (as opposed to url) will not work + correctly in this case. + + - + );