mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
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:
parent
e382af5ad0
commit
1f932b30b9
2 changed files with 149 additions and 41 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue