extract logic for friendly_name and use in wizard

This commit is contained in:
Josh Hawkins 2025-10-12 12:36:46 -05:00
parent 3c86363f82
commit 84ac8d3654
3 changed files with 74 additions and 34 deletions

View File

@ -22,6 +22,7 @@ import { LuTrash2, LuPlus } from "react-icons/lu";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import { processCameraName } from "@/utils/cameraUtil";
type ConfigSetBody = {
requires_restart: number;
@ -30,12 +31,6 @@ type ConfigSetBody = {
config_data: any;
update_topic?: string;
};
const generateFixedHash = (name: string): string => {
const encoded = encodeURIComponent(name);
const base64 = btoa(encoded);
const cleanHash = base64.replace(/[^a-zA-Z0-9]/g, "").substring(0, 8);
return `cam_${cleanHash.toLowerCase()}`;
};
const RoleEnum = z.enum(["audio", "detect", "record"]);
type Role = z.infer<typeof RoleEnum>;
@ -168,19 +163,15 @@ export default function CameraEditForm({
const saveCameraConfig = (values: FormValues) => {
setIsLoading(true);
let finalCameraName = values.cameraName;
let friendly_name: string | undefined = undefined;
const isValidName = /^[a-zA-Z0-9_-]+$/.test(values.cameraName);
if (!isValidName) {
finalCameraName = generateFixedHash(finalCameraName);
friendly_name = values.cameraName;
}
const { finalCameraName, friendlyName } = processCameraName(
values.cameraName,
);
const configData: ConfigSetBody["config_data"] = {
cameras: {
[finalCameraName]: {
enabled: values.enabled,
...(friendly_name && { friendly_name }),
...(friendlyName && { friendly_name: friendlyName }),
ffmpeg: {
inputs: values.ffmpeg.inputs.map((input) => ({
path: input.path,

View File

@ -19,6 +19,7 @@ import type {
CameraConfigData,
ConfigSetBody,
} from "@/types/cameraWizard";
import { processCameraName } from "@/utils/cameraUtil";
type WizardState = {
wizardData: Partial<WizardFormData>;
@ -158,12 +159,17 @@ export default function CameraWizardDialog({
setIsLoading(true);
// Process camera name and friendly name
const { finalCameraName, friendlyName } = processCameraName(
wizardData.cameraName,
);
// Convert wizard data to Frigate config format
const cameraName = wizardData.cameraName;
const configData: CameraConfigData = {
cameras: {
[cameraName]: {
[finalCameraName]: {
enabled: true,
...(friendlyName && { friendly_name: friendlyName }),
ffmpeg: {
inputs: wizardData.streams.map((stream, index) => {
const isRestreamed =
@ -171,8 +177,8 @@ export default function CameraWizardDialog({
if (isRestreamed) {
const go2rtcStreamName =
wizardData.streams!.length === 1
? cameraName
: `${cameraName}_${index + 1}`;
? finalCameraName
: `${finalCameraName}_${index + 1}`;
return {
path: `rtsp://127.0.0.1:8554/${go2rtcStreamName}`,
input_args: "preset-rtsp-restream",
@ -190,30 +196,26 @@ export default function CameraWizardDialog({
},
};
// Add friendly name if different from camera name
if (wizardData.cameraName !== cameraName) {
configData.cameras[cameraName].friendly_name = wizardData.cameraName;
}
// Add live.streams configuration for go2rtc streams
if (wizardData.streams && wizardData.streams.length > 0) {
configData.cameras[cameraName].live = {
configData.cameras[finalCameraName].live = {
streams: {},
};
wizardData.streams.forEach((_, index) => {
const go2rtcStreamName =
wizardData.streams!.length === 1
? cameraName
: `${cameraName}_${index + 1}`;
configData.cameras[cameraName].live!.streams[`Stream ${index + 1}`] =
go2rtcStreamName;
? finalCameraName
: `${finalCameraName}_${index + 1}`;
configData.cameras[finalCameraName].live!.streams[
`Stream ${index + 1}`
] = go2rtcStreamName;
});
}
const requestBody: ConfigSetBody = {
requires_restart: 1,
config_data: configData,
update_topic: `config/cameras/${cameraName}/add`,
update_topic: `config/cameras/${finalCameraName}/add`,
};
axios
@ -228,8 +230,8 @@ export default function CameraWizardDialog({
// Use camera name with index suffix for multiple streams
const streamName =
wizardData.streams!.length === 1
? cameraName
: `${cameraName}_${index + 1}`;
? finalCameraName
: `${finalCameraName}_${index + 1}`;
go2rtcStreams[streamName] = [stream.url];
});
@ -260,7 +262,7 @@ export default function CameraWizardDialog({
Promise.allSettled(updatePromises).then(() => {
toast.success(
t("cameraWizard.save.successWithLive", {
cameraName: wizardData.cameraName,
cameraName: friendlyName || finalCameraName,
}),
{ position: "top-center" },
);
@ -272,7 +274,7 @@ export default function CameraWizardDialog({
// log the error but don't fail the entire save
toast.warning(
t("cameraWizard.save.successWithoutLive", {
cameraName: wizardData.cameraName,
cameraName: friendlyName || finalCameraName,
}),
{ position: "top-center" },
);
@ -283,7 +285,7 @@ export default function CameraWizardDialog({
// No valid streams found
toast.success(
t("cameraWizard.save.successWithoutLive", {
cameraName: wizardData.cameraName,
cameraName: friendlyName || finalCameraName,
}),
{ position: "top-center" },
);

View File

@ -0,0 +1,47 @@
/**
* Generates a fixed-length hash from a camera name for use as a valid camera identifier.
* Used when user enters a display name with spaces or special characters.
*
* @param name - The original camera name/display name
* @returns A valid camera identifier (lowercase, alphanumeric, max 8 chars)
*/
export const generateFixedHash = (name: string): string => {
const encoded = encodeURIComponent(name);
const base64 = btoa(encoded);
const cleanHash = base64.replace(/[^a-zA-Z0-9]/g, "").substring(0, 8);
return `cam_${cleanHash.toLowerCase()}`;
};
/**
* Checks if a string is a valid camera name identifier.
* Valid camera names contain only letters, numbers, underscores, and hyphens.
*
* @param name - The camera name to validate
* @returns True if the name is valid, false otherwise
*/
export const isValidCameraName = (name: string): boolean => {
return /^[a-zA-Z0-9_-]+$/.test(name);
};
/**
* Processes a user-entered camera name and returns both the final camera name
* and friendly name for Frigate configuration.
*
* @param userInput - The name entered by the user (could be display name)
* @returns Object with finalCameraName and friendlyName
*/
export const processCameraName = (
userInput: string,
): {
finalCameraName: string;
friendlyName?: string;
} => {
if (isValidCameraName(userInput)) {
return { finalCameraName: userInput };
} else {
return {
finalCameraName: generateFixedHash(userInput),
friendlyName: userInput,
};
}
};