diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f82df29..111f3750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,11 +21,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Stream Profile form rework: Replaced the plain command text field with a dropdown listing built-in tools (FFmpeg, Streamlink, VLC, yt-dlp) plus a Custom option that reveals a free-text input. Each built-in now shows its default parameter string as a live example in the Parameters field description, updating as the command selection changes. Added descriptive help text to all fields to improve clarity. - Custom Dummy EPG form UI improvements: Reorganized the form into collapsible accordion sections (Pattern Configuration, Output Templates, Upcoming/Ended Templates, Fallback Templates, EPG Settings) for better organization. Field descriptions now appear in info icon popovers instead of taking up vertical space, making the form more compact and easier to navigate while keeping help text accessible. - XC API M3U stream URLs: M3U generation for Xtream Codes API endpoints now use proper XC-style stream URLs (`/live/username/password/channel_id`) instead of UUID-based stream endpoints, ensuring full compatibility with XC clients. (Fixes #839) ### Fixed +- Stream Profile form User-Agent not populating when editing: The User-Agent field was not correctly loaded from the existing profile when opening the edit modal. (Fixes #650) - VOD proxy connection counter leak on client disconnect: Fixed a connection leak in the VOD proxy where connection counters were not properly decremented when clients disconnected, causing the connection pool to lose track of available connections. The multi-worker connection manager now correctly handles client disconnection events across all proxy configurations. Includes three key fixes: (1) Replaced GET-check-INCR race condition with atomic INCR-first-then-check pattern in both connection managers to prevent concurrent requests exceeding max_streams; (2) Decrement profile counter directly in stream generator exit paths instead of deferring to daemon thread cleanup; (3) Decrement profile counter on create_connection() failure to release reserved slots. (Fixes #962, #971, #451, #533) - Thanks [@CodeBormen](https://github.com/CodeBormen) - XC profile refresh credential extraction with sub-paths: Fixed credential extraction in `get_transformed_credentials()` to use negative indices anchored to the known tail structure instead of hardcoded indices that broke when server URLs contained sub-paths (e.g., `http://server.com/portal/a/`). This ensures XC accounts with sub-paths in their server URLs work correctly for profile refreshes. (Fixes #945) - Thanks [@CodeBormen](https://github.com/CodeBormen) - XC EPG URL construction for accounts with sub-paths or trailing slashes: Fixed EPG URL construction in M3U forms to normalize server URL to origin before appending `xmltv.php` endpoint, preventing double slashes and incorrect path placement when server URLs include sub-paths or trailing slashes. (Fixes #800) - Thanks [@CodeBormen](https://github.com/CodeBormen) diff --git a/frontend/src/components/forms/StreamProfile.jsx b/frontend/src/components/forms/StreamProfile.jsx index 09707969..137bc2ea 100644 --- a/frontend/src/components/forms/StreamProfile.jsx +++ b/frontend/src/components/forms/StreamProfile.jsx @@ -1,28 +1,65 @@ -// Modal.js -import React, { useEffect, useMemo } from 'react'; +// StreamProfile form +import React, { useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as Yup from 'yup'; import API from '../../api'; import useUserAgentsStore from '../../store/userAgents'; -import { Modal, TextInput, Select, Button, Flex } from '@mantine/core'; +import { + Modal, + TextInput, + Textarea, + Select, + Button, + Flex, + Stack, + Checkbox, +} from '@mantine/core'; + +// Built-in commands supported by Dispatcharr out of the box. +const BUILT_IN_COMMANDS = [ + { value: 'ffmpeg', label: 'FFmpeg' }, + { value: 'streamlink', label: 'Streamlink' }, + { value: 'cvlc', label: 'VLC' }, + { value: 'yt-dlp', label: 'yt-dlp' }, + { value: '__custom__', label: 'Custom…' }, +]; + +// Default parameter examples for each built-in command. +const COMMAND_EXAMPLES = { + ffmpeg: '-user_agent {userAgent} -i {streamUrl} -c copy -f mpegts pipe:1', + streamlink: '{streamUrl} --http-header User-Agent={userAgent} best --stdout', + cvlc: '-vv -I dummy --no-video-title-show --http-user-agent {userAgent} {streamUrl} --sout #standard{access=file,mux=ts,dst=-}', + 'yt-dlp': '--hls-use-mpegts -f best -o - {streamUrl}', +}; + +// Returns '__custom__' when the command isn't one of the built-ins, +// otherwise returns the command value itself. +const toCommandSelection = (command) => + BUILT_IN_COMMANDS.find((o) => o.value === command && o.value !== '__custom__') + ? command + : '__custom__'; const schema = Yup.object({ name: Yup.string().required('Name is required'), command: Yup.string().required('Command is required'), - parameters: Yup.string().required('Parameters are is required'), + parameters: Yup.string(), }); const StreamProfile = ({ profile = null, isOpen, onClose }) => { const userAgents = useUserAgentsStore((state) => state.userAgents); + // Separate state for the dropdown selection so 'Custom…' can be chosen + // independently of the actual command string stored in the form. + const [commandSelection, setCommandSelection] = useState('ffmpeg'); + const defaultValues = useMemo( () => ({ name: profile?.name || '', command: profile?.command || '', parameters: profile?.parameters || '', is_active: profile?.is_active ?? true, - user_agent: profile?.user_agent || '', + user_agent: profile?.user_agent ? `${profile.user_agent}` : '', }), [profile] ); @@ -32,12 +69,19 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => { handleSubmit, formState: { errors, isSubmitting }, reset, + setValue, watch, } = useForm({ defaultValues, resolver: yupResolver(schema), }); + // Sync form + dropdown selection whenever the target profile or modal state changes + useEffect(() => { + reset(defaultValues); + setCommandSelection(toCommandSelection(profile?.command || '')); + }, [defaultValues, reset, profile]); + const onSubmit = async (values) => { if (profile?.id) { await API.updateStreamProfile({ id: profile.id, ...values }); @@ -49,58 +93,120 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => { onClose(); }; - useEffect(() => { - reset(defaultValues); - }, [defaultValues, reset]); - if (!isOpen) { return <>; } + const isLocked = profile ? profile.locked : false; + const isCustom = commandSelection === '__custom__'; const userAgentValue = watch('user_agent'); + const isActiveValue = watch('is_active'); return (
- - - + + - + The executable used to process the stream. +
+ Choose a built-in tool or select Custom… to enter any + executable name or path. + + } + data={BUILT_IN_COMMANDS} + disabled={isLocked} + value={commandSelection} + onChange={(val) => { + setCommandSelection(val); + // For built-in selections, write the real command value immediately + if (val !== '__custom__') { + setValue('command', val, { shouldValidate: true }); + } else { + // Clear so the user enters their own value + setValue('command', '', { shouldValidate: false }); + } + }} + error={isCustom ? undefined : errors.command?.message} + /> + + {isCustom && ( + + )} + +