mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
Enhancement: M3U Profile form (XC accounts): added a **Simple / Advanced** mode toggle for credential-based URL rewriting. In Simple mode users enter just a new username and password; the search and replace patterns are built automatically from the account's current credentials. In Advanced mode the full regex fields are shown as before. The selected mode is saved to custom_properties.xcMode and auto-detected on existing profiles (a profile whose search pattern matches the account's current username/password is recognised as Simple automatically). The Live Regex Demonstration panel is hidden in Simple mode.
This commit is contained in:
parent
698f9ff4c0
commit
9579248485
2 changed files with 149 additions and 21 deletions
|
|
@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Changed
|
||||
|
||||
- M3U Profile form (XC accounts): added a **Simple / Advanced** mode toggle for credential-based URL rewriting. In Simple mode users enter just a new username and password; the search and replace patterns are built automatically from the account's current credentials. In Advanced mode the full regex fields are shown as before. The selected mode is saved to `custom_properties.xcMode` and auto-detected on existing profiles (a profile whose search pattern matches the account's current `username/password` is recognised as Simple automatically). The Live Regex Demonstration panel is hidden in Simple mode.
|
||||
- XtreamCodes VOD endpoints (`/movie/` and `/series/`) no longer redirect clients to a UUID-based proxy URL. Requests are now handled directly in the proxy layer via `stream_xc_movie` and `stream_xc_episode`, which call `stream_vod()` internally. The original XC path is preserved for the client throughout the stream.
|
||||
- `CustomTable` column layout now supports flexible (`grow`) columns alongside fixed-width ones:
|
||||
- Column definitions accept a `grow` property (boolean or number) to opt into flex layout. A numeric value sets the flex-grow weight, allowing relative sizing between grow columns (e.g. `grow: 2` gives a column twice the share of spare space as `grow: 1`).
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
Grid,
|
||||
Textarea,
|
||||
NumberInput,
|
||||
SegmentedControl,
|
||||
} from '@mantine/core';
|
||||
import { DateTimePicker } from '@mantine/dates';
|
||||
import { useWebSocket } from '../../WebSocket';
|
||||
|
|
@ -26,6 +27,10 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
|
|||
const [replacePattern, setReplacePattern] = useState('');
|
||||
const [debouncedPatterns, setDebouncedPatterns] = useState({});
|
||||
const [sampleInput, setSampleInput] = useState('');
|
||||
const [xcMode, setXcMode] = useState('simple');
|
||||
const [newUsername, setNewUsername] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [simpleErrors, setSimpleErrors] = useState({});
|
||||
const isDefaultProfile = profile?.is_default;
|
||||
|
||||
const isXC = m3u?.account_type === 'XC';
|
||||
|
|
@ -45,12 +50,12 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
|
|||
const schema = Yup.object({
|
||||
name: Yup.string().required('Name is required'),
|
||||
search_pattern: Yup.string().when([], {
|
||||
is: () => !isDefaultProfile,
|
||||
is: () => !isDefaultProfile && !isXC,
|
||||
then: (schema) => schema.required('Search pattern is required'),
|
||||
otherwise: (schema) => schema.notRequired(),
|
||||
}),
|
||||
replace_pattern: Yup.string().when([], {
|
||||
is: () => !isDefaultProfile,
|
||||
is: () => !isDefaultProfile && !isXC,
|
||||
then: (schema) => schema.required('Replace pattern is required'),
|
||||
otherwise: (schema) => schema.notRequired(),
|
||||
}),
|
||||
|
|
@ -64,6 +69,7 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
|
|||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
setError,
|
||||
} = useForm({
|
||||
defaultValues,
|
||||
resolver: yupResolver(schema),
|
||||
|
|
@ -83,6 +89,32 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
|
|||
expDateValue = null;
|
||||
}
|
||||
|
||||
// For XC simple mode: validate simple inputs and build patterns from credentials
|
||||
if (isXC && xcMode === 'simple' && !isDefaultProfile) {
|
||||
const errs = {};
|
||||
if (!newUsername.trim()) errs.newUsername = 'New username is required';
|
||||
if (!newPassword.trim()) errs.newPassword = 'New password is required';
|
||||
if (Object.keys(errs).length > 0) {
|
||||
setSimpleErrors(errs);
|
||||
return;
|
||||
}
|
||||
setSimpleErrors({});
|
||||
values.search_pattern = `${m3u?.username || ''}/${m3u?.password || ''}`;
|
||||
values.replace_pattern = `${newUsername.trim()}/${newPassword.trim()}`;
|
||||
}
|
||||
|
||||
// For XC advanced mode: validate regex pattern fields
|
||||
if (isXC && xcMode === 'advanced' && !isDefaultProfile) {
|
||||
if (!searchPattern.trim()) {
|
||||
setError('search_pattern', { message: 'Search pattern is required' });
|
||||
return;
|
||||
}
|
||||
if (!replacePattern.trim()) {
|
||||
setError('replace_pattern', { message: 'Replace pattern is required' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For default profiles, only send name and custom_properties (notes)
|
||||
let submitValues;
|
||||
if (isDefaultProfile) {
|
||||
|
|
@ -105,6 +137,7 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
|
|||
// Preserve existing custom_properties and add/update notes
|
||||
...(profile?.custom_properties || {}),
|
||||
notes: values.notes || '',
|
||||
...(isXC ? { xcMode } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -200,12 +233,63 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
|
|||
reset(defaultValues);
|
||||
setSearchPattern(profile?.search_pattern || '');
|
||||
setReplacePattern(profile?.replace_pattern || '');
|
||||
}, [defaultValues, profile, reset]);
|
||||
if (isXC && !isDefaultProfile) {
|
||||
const storedMode = profile?.custom_properties?.xcMode;
|
||||
let detectedMode;
|
||||
if (storedMode) {
|
||||
detectedMode = storedMode;
|
||||
} else if (
|
||||
profile?.search_pattern &&
|
||||
profile.search_pattern === `${m3u?.username}/${m3u?.password}`
|
||||
) {
|
||||
detectedMode = 'simple';
|
||||
} else if (profile?.search_pattern) {
|
||||
detectedMode = 'advanced';
|
||||
} else {
|
||||
detectedMode = 'simple';
|
||||
}
|
||||
setXcMode(detectedMode);
|
||||
if (detectedMode === 'simple') {
|
||||
const rp = profile?.replace_pattern || '';
|
||||
const idx = rp.indexOf('/');
|
||||
setNewUsername(idx === -1 ? rp : rp.slice(0, idx));
|
||||
setNewPassword(idx === -1 ? '' : rp.slice(idx + 1));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
defaultValues,
|
||||
isDefaultProfile,
|
||||
isXC,
|
||||
m3u?.password,
|
||||
m3u?.username,
|
||||
profile,
|
||||
reset,
|
||||
]);
|
||||
|
||||
const handleSampleInputChange = (e) => {
|
||||
setSampleInput(e.target.value);
|
||||
};
|
||||
|
||||
const handleXcModeChange = (mode) => {
|
||||
if (mode === 'advanced' && xcMode === 'simple') {
|
||||
// Pre-populate regex fields from current simple values
|
||||
const sp = `${m3u?.username || ''}/${m3u?.password || ''}`;
|
||||
const rp = `${newUsername}/${newPassword}`;
|
||||
setSearchPattern(sp);
|
||||
setReplacePattern(rp);
|
||||
setValue('search_pattern', sp);
|
||||
setValue('replace_pattern', rp);
|
||||
} else if (mode === 'simple' && xcMode === 'advanced') {
|
||||
// Parse current replace pattern back into username/password
|
||||
const idx = replacePattern.indexOf('/');
|
||||
setNewUsername(
|
||||
idx === -1 ? replacePattern : replacePattern.slice(0, idx)
|
||||
);
|
||||
setNewPassword(idx === -1 ? '' : replacePattern.slice(idx + 1));
|
||||
}
|
||||
setXcMode(mode);
|
||||
};
|
||||
|
||||
// Local regex testing for immediate visual feedback
|
||||
const getHighlightedSearchText = () => {
|
||||
if (!searchPattern || !sampleInput) return sampleInput;
|
||||
|
|
@ -267,22 +351,65 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
|
|||
{/* Only show search/replace fields for non-default profiles */}
|
||||
{!isDefaultProfile && (
|
||||
<>
|
||||
<TextInput
|
||||
label="Search Pattern (Regex)"
|
||||
description="A regular expression matching the part of the stream URL you want to replace. For most users, matching just the credentials is enough."
|
||||
placeholder="e.g. username1/password1"
|
||||
value={searchPattern}
|
||||
onChange={onSearchPatternUpdate}
|
||||
error={errors.search_pattern?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="Replace Pattern"
|
||||
description="The value to substitute in place of the matched text. Use $1, $2, etc. to reference regex capture groups."
|
||||
placeholder="e.g. username2/password2"
|
||||
value={replacePattern}
|
||||
onChange={onReplacePatternUpdate}
|
||||
error={errors.replace_pattern?.message}
|
||||
/>
|
||||
{isXC && (
|
||||
<SegmentedControl
|
||||
mt="xs"
|
||||
mb="xs"
|
||||
fullWidth
|
||||
size="xs"
|
||||
value={xcMode}
|
||||
onChange={handleXcModeChange}
|
||||
data={[
|
||||
{ label: 'Simple', value: 'simple' },
|
||||
{ label: 'Advanced (Regex)', value: 'advanced' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{isXC && xcMode === 'simple' ? (
|
||||
<>
|
||||
<TextInput
|
||||
label="New Username"
|
||||
description="Your updated XC account username. The current username in all stream URLs will be replaced with this."
|
||||
placeholder="e.g. username2"
|
||||
value={newUsername}
|
||||
onChange={(e) => {
|
||||
setNewUsername(e.target.value);
|
||||
setSimpleErrors((s) => ({ ...s, newUsername: undefined }));
|
||||
}}
|
||||
error={simpleErrors.newUsername}
|
||||
/>
|
||||
<TextInput
|
||||
label="New Password"
|
||||
description="Your updated XC account password. The current password in all stream URLs will be replaced with this."
|
||||
placeholder="e.g. password2"
|
||||
value={newPassword}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
setSimpleErrors((s) => ({ ...s, newPassword: undefined }));
|
||||
}}
|
||||
error={simpleErrors.newPassword}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TextInput
|
||||
label="Search Pattern (Regex)"
|
||||
description="A regular expression matching the part of the stream URL you want to replace. For most users, matching just the credentials is enough."
|
||||
placeholder="e.g. username1/password1"
|
||||
value={searchPattern}
|
||||
onChange={onSearchPatternUpdate}
|
||||
error={errors.search_pattern?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="Replace Pattern"
|
||||
description="The value to substitute in place of the matched text. Use $1, $2, etc. to reference regex capture groups."
|
||||
placeholder="e.g. username2/password2"
|
||||
value={replacePattern}
|
||||
onChange={onReplacePatternUpdate}
|
||||
error={errors.replace_pattern?.message}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
@ -326,8 +453,8 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
|
|||
</Flex>
|
||||
</form>
|
||||
|
||||
{/* Only show regex demonstration for non-default profiles */}
|
||||
{!isDefaultProfile && (
|
||||
{/* Only show regex demonstration for non-default profiles in advanced mode */}
|
||||
{!isDefaultProfile && (!isXC || xcMode === 'advanced') && (
|
||||
<>
|
||||
<Title order={4} mt={15} mb={10}>
|
||||
Live Regex Demonstration
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue