Enhancement/BugFix:

- The User-Agent field was not correctly loaded from the existing profile when opening the edit modal. (Fixes #650)
- 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.
This commit is contained in:
SergeantPanda 2026-02-19 15:51:08 -06:00
parent e382af5ad0
commit 1f932b30b9
2 changed files with 149 additions and 41 deletions

View file

@ -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)

View file

@ -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 (
<Modal opened={isOpen} onClose={onClose} title="Stream Profile">
<form onSubmit={handleSubmit(onSubmit)}>
<TextInput
label="Name"
{...register('name')}
error={errors.name?.message}
disabled={profile ? profile.locked : false}
/>
<TextInput
label="Command"
{...register('command')}
error={errors.command?.message}
disabled={profile ? profile.locked : false}
/>
<TextInput
label="Parameters"
{...register('parameters')}
error={errors.parameters?.message}
disabled={profile ? profile.locked : false}
/>
<Stack gap="sm">
<TextInput
label="Name"
description="A unique, descriptive label for this stream profile"
disabled={isLocked}
{...register('name')}
error={errors.name?.message}
/>
<Select
label="User-Agent"
{...register('user_agent')}
value={userAgentValue}
error={errors.user_agent?.message}
data={userAgents.map((ua) => ({
label: ua.name,
value: `${ua.id}`,
}))}
/>
<Select
label="Command"
description={
<>
The executable used to process the stream.
<br />
Choose a built-in tool or select <em>Custom</em> 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 && (
<TextInput
label="Custom Command"
description="Enter the executable name (e.g. ffmpeg) or full path (e.g. /usr/local/bin/mycmd)"
disabled={isLocked}
{...register('command')}
error={errors.command?.message}
/>
)}
<Textarea
label="Parameters"
description={
<>
Command-line arguments passed to the command.
<br />
Use <strong>{'{streamUrl}'}</strong> and{' '}
<strong>{'{userAgent}'}</strong> as placeholders they are
substituted at stream time.
{COMMAND_EXAMPLES[commandSelection] && (
<>
<br />
Example: <em>{COMMAND_EXAMPLES[commandSelection]}</em>
</>
)}
</>
}
autosize
minRows={2}
placeholder={
COMMAND_EXAMPLES[commandSelection] ||
'Enter command-line arguments…'
}
disabled={isLocked}
{...register('parameters')}
error={errors.parameters?.message}
/>
<Select
label="User-Agent"
description="Optional user-agent override. Falls back to the system default if not set."
clearable
data={userAgents.map((ua) => ({
label: ua.name,
value: `${ua.id}`,
}))}
value={userAgentValue}
onChange={(val) => setValue('user_agent', val ?? '')}
error={errors.user_agent?.message}
/>
<Checkbox
label="Is Active"
description="Enable or disable this stream profile"
checked={isActiveValue}
onChange={(e) => setValue('is_active', e.currentTarget.checked)}
/>
</Stack>
<Flex mih={50} gap="xs" justify="flex-end" align="flex-end">
<Button
type="submit"
variant="contained"
color="primary"
variant="filled"
disabled={isSubmitting}
size="small"
size="sm"
>
Submit
Save
</Button>
</Flex>
</form>