Add OBS group management feature and documentation
Some checks failed
Lint and Build / build (22) (pull_request) Failing after 32s
Lint and Build / build (20) (pull_request) Failing after 34s

- Add group_name column to teams table for mapping teams to OBS groups
- Create API endpoints for group creation (/api/createGroup) and bulk sync (/api/syncGroups)
- Update teams UI with group status display and creation buttons
- Implement automatic group assignment when adding streams
- Add comprehensive OBS setup documentation (docs/OBS_SETUP.md)
- Fix team list spacing issue with explicit margins
- Update OBS client with group management functions
- Add database migration script for existing deployments

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Decobus 2025-07-20 00:28:16 -04:00
parent c259f0d943
commit 5789986bb6
14 changed files with 540 additions and 144 deletions

View file

@ -18,7 +18,7 @@ export async function apiCall(url: string, options: RequestInit = {}): Promise<R
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...options.headers,
...(options.headers as Record<string, string> || {}),
};
// Add API key if available

View file

@ -120,48 +120,74 @@ async function addSourceToSwitcher(inputName, newSources) {
}
}
// async function addSourceToGroup(obs, teamName, obs_source_name, url) {
// try {
// // Step 1: Check if the group exists
// const { scenes } = await obs.call('GetSceneList');
// const groupExists = scenes.some((scene) => scene.sceneName === teamName);
async function createGroupIfNotExists(groupName) {
try {
const obsClient = await getOBSClient();
// Check if the group (scene) exists
const { scenes } = await obsClient.call('GetSceneList');
const groupExists = scenes.some((scene) => scene.sceneName === groupName);
// // Step 2: Create the group if it doesn't exist
// if (!groupExists) {
// console.log(`Group "${teamName}" does not exist. Creating it.`);
// await obs.call('CreateScene', { sceneName: teamName });
// } else {
// console.log(`Group "${teamName}" already exists.`);
// }
if (!groupExists) {
console.log(`Creating group "${groupName}"`);
await obsClient.call('CreateScene', { sceneName: groupName });
return { created: true, message: `Group "${groupName}" created successfully` };
} else {
console.log(`Group "${groupName}" already exists`);
return { created: false, message: `Group "${groupName}" already exists` };
}
} catch (error) {
console.error('Error creating group:', error.message);
throw error;
}
}
// // Step 3: Add the source to the group
// console.log(`Adding source "${obs_source_name}" to group "${teamName}".`);
// await obs.call('CreateInput', {
// sceneName: teamName,
// inputName: obs_source_name,
// inputKind: 'browser_source',
// inputSettings: {
// width: 1600,
// height: 900,
// url,
// control_audio: true,
// },
// });
// // Step 4: Enable "Control audio via OBS"
// await obs.call('SetInputSettings', {
// inputName: obs_source_name,
// inputSettings: {
// control_audio: true, // Enable audio control
// },
// overlay: true, // Keep existing settings and apply changes
// });
// console.log(`Source "${obs_source_name}" successfully added to group "${teamName}".`);
// } catch (error) {
// console.error('Error adding source to group:', error.message);
// }
// }
async function addSourceToGroup(groupName, sourceName, url) {
try {
const obsClient = await getOBSClient();
// Ensure group exists
await createGroupIfNotExists(groupName);
// Check if source already exists in the group
const { sceneItems } = await obsClient.call('GetSceneItemList', { sceneName: groupName });
const sourceExists = sceneItems.some(item => item.sourceName === sourceName);
if (!sourceExists) {
// Create the browser source in the group
console.log(`Adding source "${sourceName}" to group "${groupName}"`);
await obsClient.call('CreateInput', {
sceneName: groupName,
inputName: sourceName,
inputKind: 'browser_source',
inputSettings: {
width: 1600,
height: 900,
url,
control_audio: true,
},
});
// Ensure audio control is enabled
await obsClient.call('SetInputSettings', {
inputName: sourceName,
inputSettings: {
control_audio: true,
},
overlay: true,
});
console.log(`Source "${sourceName}" successfully added to group "${groupName}"`);
return { success: true, message: `Source added to group successfully` };
} else {
console.log(`Source "${sourceName}" already exists in group "${groupName}"`);
return { success: false, message: `Source already exists in group` };
}
} catch (error) {
console.error('Error adding source to group:', error.message);
throw error;
}
}
// Export all functions
@ -171,5 +197,7 @@ module.exports = {
disconnectFromOBS,
addSourceToSwitcher,
ensureConnected,
getConnectionStatus
getConnectionStatus,
createGroupIfNotExists,
addSourceToGroup
};

View file

@ -7,7 +7,7 @@ export function useDebounce<T extends (...args: any[]) => any>(
callback: T,
delay: number
): T {
const timeoutRef = useRef<NodeJS.Timeout>();
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
return useCallback((...args: Parameters<T>) => {
if (timeoutRef.current) {
@ -159,7 +159,7 @@ export function useSmartPolling(
) {
const isVisible = usePageVisibility();
const callbackRef = useRef(callback);
const intervalRef = useRef<NodeJS.Timeout>();
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// Update callback ref
React.useEffect(() => {

View file

@ -18,7 +18,15 @@ export function isValidUrl(url: string): boolean {
}
export function isPositiveInteger(value: unknown): value is number {
return Number.isInteger(value) && value > 0;
return Number.isInteger(value) && Number(value) > 0;
}
export function validateInteger(value: unknown): number | null {
const num = Number(value);
if (Number.isInteger(num) && num > 0) {
return num;
}
return null;
}
// String sanitization