diff --git a/app/api/addStream/route.ts b/app/api/addStream/route.ts index 4bb22d6..750962e 100644 --- a/app/api/addStream/route.ts +++ b/app/api/addStream/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase } from '../../../lib/database'; -import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher, createStreamGroup } from '../../../lib/obsClient'; +import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher, createGroupIfNotExists, addSourceToGroup } from '../../../lib/obsClient'; import { open } from 'sqlite'; import sqlite3 from 'sqlite3'; import path from 'path'; @@ -44,7 +44,7 @@ async function fetchTeamInfo(teamId: number) { }); const teamInfo = await db.get( - `SELECT team_name, group_name, group_uuid FROM ${teamsTableName} WHERE team_id = ?`, + `SELECT team_name, group_name FROM ${teamsTableName} WHERE team_id = ?`, [teamId] ); @@ -63,15 +63,8 @@ async function fetchTeamInfo(teamId: number) { import { validateStreamInput } from '../../../lib/security'; -// Generate OBS source name from team scene name and stream name -function generateOBSSourceName(teamSceneName: string, streamName: string): string { - const cleanTeamName = teamSceneName.toLowerCase().replace(/\s+/g, '_'); - const cleanStreamName = streamName.toLowerCase().replace(/\s+/g, '_'); - return `${cleanTeamName}_${cleanStreamName}`; -} - export async function POST(request: NextRequest) { - let name: string, url: string, team_id: number, obs_source_name: string; + let name: string, obs_source_name: string, url: string, team_id: number; // Parse and validate request body try { @@ -85,26 +78,13 @@ export async function POST(request: NextRequest) { }, { status: 400 }); } - ({ name, url, team_id } = validation.data!); + ({ name, obs_source_name, url, team_id } = validation.data!); } catch { return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }); } try { - // Fetch team info first to generate proper OBS source name - const teamInfo = await fetchTeamInfo(team_id); - if (!teamInfo) { - throw new Error('Team not found'); - } - - console.log('Team Info:', teamInfo); - - // Use group_name if it exists, otherwise use team_name - const groupName = teamInfo.group_name || teamInfo.team_name; - - // Generate OBS source name with team scene name prefix - obs_source_name = generateOBSSourceName(groupName, name); // Connect to OBS WebSocket console.log("Pre-connect") @@ -129,58 +109,29 @@ export async function POST(request: NextRequest) { throw new Error('GetInputList failed.'); } + const teamInfo = await fetchTeamInfo(team_id); + if (!teamInfo) { + throw new Error('Team not found'); + } + + console.log('Team Info:', teamInfo); + + // Use group_name if it exists, otherwise use team_name + const groupName = teamInfo.group_name || teamInfo.team_name; + const sourceExists = inputs.some((input: OBSInput) => input.inputName === obs_source_name); if (!sourceExists) { - // Create stream group with text overlay - await createStreamGroup(groupName, name, teamInfo.team_name, url); - - // Update team with group UUID if not set - if (!teamInfo.group_uuid) { - const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files'); - const dbPath = path.join(FILE_DIRECTORY, 'sources.db'); - const db = await open({ - filename: dbPath, - driver: sqlite3.Database, - }); - - const teamsTableName = getTableName(BASE_TABLE_NAMES.TEAMS, { - year: 2025, - season: 'summer', - suffix: 'sat' - }); - - try { - // Get the scene UUID for the group - const obsClient = await getOBSClient(); - const { scenes } = await obsClient.call('GetSceneList'); - const scene = scenes.find((s: { sceneName: string; sceneUuid: string }) => s.sceneName === groupName); - - if (scene) { - await db.run( - `UPDATE ${teamsTableName} SET group_name = ?, group_uuid = ? WHERE team_id = ?`, - [groupName, scene.sceneUuid, team_id] - ); - console.log(`Updated team ${team_id} with group UUID: ${scene.sceneUuid}`); - } else { - console.log(`Scene "${groupName}" not found in OBS`); - } - } catch (error) { - console.error('Error updating team group UUID:', error); - } finally { - await db.close(); - } - } + // Create/ensure group exists and add source to it + await createGroupIfNotExists(groupName); + await addSourceToGroup(groupName, obs_source_name, url); console.log(`OBS source "${obs_source_name}" created.`); for (const screen of screens) { try { - const cleanGroupName = groupName.toLowerCase().replace(/\s+/g, '_'); - const cleanStreamName = name.toLowerCase().replace(/\s+/g, '_'); - const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`; await addSourceToSwitcher(screen, [ - { hidden: false, selected: false, value: streamGroupName }, + { hidden: false, selected: false, value: obs_source_name }, ]); } catch (error) { if (error instanceof Error) { diff --git a/app/api/getActive/route.ts b/app/api/getActive/route.ts index a54b4b4..c8ff45d 100644 --- a/app/api/getActive/route.ts +++ b/app/api/getActive/route.ts @@ -47,8 +47,8 @@ export async function GET() { const chicken = fs.existsSync(chickenPath) ? fs.readFileSync(chickenPath, 'utf-8') : null; // For SaT - return NextResponse.json({ large, left, right, topLeft, topRight, bottomLeft, bottomRight }, {status: 201}) - // return NextResponse.json({ tank, tree, kitty, chicken }, {status: 201}) + // return NextResponse.json({ large, left, right, topLeft, topRight, bottomLeft, bottomRight }, {status: 201}) + return NextResponse.json({ tank, tree, kitty, chicken }, {status: 201}) } catch (error) { console.error('Error reading active sources:', error); return NextResponse.json({ error: 'Failed to read active sources' }, {status: 500}); diff --git a/app/api/setActive/route.ts b/app/api/setActive/route.ts index 61fb3b8..a2e107a 100644 --- a/app/api/setActive/route.ts +++ b/app/api/setActive/route.ts @@ -5,7 +5,6 @@ import { FILE_DIRECTORY } from '../../../config'; import { getDatabase } from '../../../lib/database'; import { Stream } from '@/types'; import { validateScreenInput } from '../../../lib/security'; -import { TABLE_NAMES } from '../../../lib/constants'; export async function POST(request: NextRequest) { // Parse and validate request body @@ -28,7 +27,7 @@ export async function POST(request: NextRequest) { try { const db = await getDatabase(); const stream: Stream | undefined = await db.get( - `SELECT * FROM ${TABLE_NAMES.STREAMS} WHERE id = ?`, + 'SELECT * FROM streams_2025_spring_adr WHERE id = ?', [id] ); @@ -38,9 +37,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Stream not found' }, { status: 400 }); } - // Use stream group name instead of individual obs_source_name - const streamGroupName = `${stream.name.toLowerCase().replace(/\s+/g, '_')}_stream`; - fs.writeFileSync(filePath, streamGroupName); + fs.writeFileSync(filePath, stream.obs_source_name); return NextResponse.json({ message: `${screen} updated successfully.` }, { status: 200 }); } catch (error) { console.error('Error updating active source:', error); diff --git a/app/api/teams/route.ts b/app/api/teams/route.ts index 88ec53e..2ccdaf0 100644 --- a/app/api/teams/route.ts +++ b/app/api/teams/route.ts @@ -8,12 +8,11 @@ import { createDatabaseError, parseRequestBody } from '@/lib/apiHelpers'; -import { createGroupIfNotExists, createTextSource } from '@/lib/obsClient'; // Validation for team creation function validateTeamInput(data: unknown): { valid: boolean; - data?: { team_name: string; create_obs_group?: boolean }; + data?: { team_name: string }; errors?: Record } { const errors: Record = {}; @@ -23,7 +22,7 @@ function validateTeamInput(data: unknown): { return { valid: false, errors }; } - const { team_name, create_obs_group } = data as { team_name?: unknown; create_obs_group?: unknown }; + const { team_name } = data as { team_name?: unknown }; if (!team_name || typeof team_name !== 'string') { errors.team_name = 'Team name is required and must be a string'; @@ -39,10 +38,7 @@ function validateTeamInput(data: unknown): { return { valid: true, - data: { - team_name: (team_name as string).trim(), - create_obs_group: create_obs_group === true - } + data: { team_name: (team_name as string).trim() } }; } @@ -64,7 +60,7 @@ export const POST = withErrorHandling(async (request: Request) => { return bodyResult.response; } - const { team_name, create_obs_group } = bodyResult.data; + const { team_name } = bodyResult.data; try { const db = await getDatabase(); @@ -82,37 +78,16 @@ export const POST = withErrorHandling(async (request: Request) => { ); } - let groupName: string | null = null; - let groupUuid: string | null = null; - - // Create OBS group and text source if requested - if (create_obs_group) { - try { - const obsResult = await createGroupIfNotExists(team_name); - groupName = team_name; - groupUuid = obsResult.sceneUuid; - - // Create text source for the team - const textSourceName = team_name.toLowerCase().replace(/\s+/g, '_') + '_text'; - await createTextSource(team_name, textSourceName, team_name); - - console.log(`OBS group and text source created for team "${team_name}"`); - } catch (obsError) { - console.error('Error creating OBS group:', obsError); - // Continue with team creation even if OBS fails - } - } - const result = await db.run( - `INSERT INTO ${TABLE_NAMES.TEAMS} (team_name, group_name, group_uuid) VALUES (?, ?, ?)`, - [team_name, groupName, groupUuid] + `INSERT INTO ${TABLE_NAMES.TEAMS} (team_name) VALUES (?)`, + [team_name] ); const newTeam: Team = { team_id: result.lastID!, team_name: team_name, - group_name: groupName, - group_uuid: groupUuid + group_name: null, + group_uuid: null }; return createSuccessResponse(newTeam, 201); diff --git a/app/page.tsx b/app/page.tsx index 37f33fd..2dc242d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -98,15 +98,10 @@ export default function Home() { const handleSetActive = useCallback(async (screen: ScreenType, id: number | null) => { const selectedStream = streams.find((stream) => stream.id === id); - // Generate stream group name for optimistic updates - const streamGroupName = selectedStream - ? `${selectedStream.name.toLowerCase().replace(/\s+/g, '_')}_stream` - : null; - // Update local state immediately for optimistic updates setActiveSources((prev) => ({ ...prev, - [screen]: streamGroupName, + [screen]: selectedStream?.obs_source_name || null, })); // Debounced backend update @@ -210,6 +205,29 @@ export default function Home() { + {/* Manage Streams Section */} + {streams.length > 0 && ( +
+

Manage Streams

+
+ {streams.map((stream) => ( +
+
+

{stream.name}

+

{stream.obs_source_name}

+
+ + ✏️ + Edit + +
+ ))} +
+
+ )} {/* Toast Notifications */} diff --git a/app/streams/page.tsx b/app/streams/page.tsx index 8520c19..75aa281 100644 --- a/app/streams/page.tsx +++ b/app/streams/page.tsx @@ -17,6 +17,7 @@ interface Stream { export default function AddStream() { const [formData, setFormData] = useState({ name: '', + obs_source_name: '', twitch_username: '', team_id: null, }); @@ -124,6 +125,9 @@ export default function AddStream() { errors.name = 'Stream name must be at least 2 characters'; } + if (!formData.obs_source_name.trim()) { + errors.obs_source_name = 'OBS source name is required'; + } if (!formData.twitch_username.trim()) { errors.twitch_username = 'Twitch username is required'; @@ -158,7 +162,7 @@ export default function AddStream() { const data = await response.json(); if (response.ok) { showSuccess('Stream Added', `"${formData.name}" has been added successfully`); - setFormData({ name: '', twitch_username: '', team_id: null }); + setFormData({ name: '', obs_source_name: '', twitch_username: '', team_id: null }); setValidationErrors({}); fetchData(); } else { @@ -210,6 +214,28 @@ export default function AddStream() { )} + {/* OBS Source Name */} +
+ + + {validationErrors.obs_source_name && ( +
+ {validationErrors.obs_source_name} +
+ )} +
{/* Twitch Username */}
@@ -289,21 +315,13 @@ export default function AddStream() { {streams.map((stream) => { const team = teams.find(t => t.id === stream.team_id); return ( -
+
-
-
+
+
{stream.name.charAt(0).toUpperCase()}
-
+
{stream.name}
OBS: {stream.obs_source_name}
Team: {team?.name || 'Unknown'}
@@ -311,13 +329,12 @@ export default function AddStream() {
ID: {stream.id}
-
+
diff --git a/files/ss_large.txt b/files/ss_large.txt deleted file mode 100644 index 5fc4d8d..0000000 --- a/files/ss_large.txt +++ /dev/null @@ -1 +0,0 @@ -wa_stream \ No newline at end of file diff --git a/files/ss_left.txt b/files/ss_left.txt deleted file mode 100644 index e69de29..0000000 diff --git a/files/ss_right.text b/files/ss_right.text deleted file mode 100644 index e69de29..0000000 diff --git a/lib/obsClient.js b/lib/obsClient.js index d2f5872..4d323b3 100644 --- a/lib/obsClient.js +++ b/lib/obsClient.js @@ -99,22 +99,12 @@ async function addSourceToSwitcher(inputName, newSources) { // Step 1: Get current input settings const { inputSettings } = await obsClient.call('GetInputSettings', { inputName }); - console.log('Current Settings for', inputName, ':', inputSettings); + // console.log('Current Settings:', inputSettings); - // Step 2: Initialize sources array if it doesn't exist or is not an array - let currentSources = []; - if (Array.isArray(inputSettings.sources)) { - currentSources = inputSettings.sources; - } else if (inputSettings.sources) { - console.log('Sources is not an array, converting:', typeof inputSettings.sources); - // Try to convert if it's an object or other format - currentSources = []; - } + // Step 2: Add new sources to the sources array + const updatedSources = [...inputSettings.sources, ...newSources]; - // Step 3: Add new sources to the sources array - const updatedSources = [...currentSources, ...newSources]; - - // Step 4: Update the settings with the new sources array + // Step 3: Update the settings with the new sources array await obsClient.call('SetInputSettings', { inputName, inputSettings: { @@ -212,238 +202,6 @@ async function addSourceToGroup(groupName, sourceName, url) { } } -async function getAvailableTextInputKind() { - try { - const obsClient = await getOBSClient(); - const { inputKinds } = await obsClient.call('GetInputKindList'); - - console.log('Available input kinds:', inputKinds); - - // Check for text input kinds in order of preference - const textKinds = ['text_gdiplus_v2', 'text_gdiplus', 'text_ft2_source_v2', 'text_ft2_source', 'text_source']; - - for (const kind of textKinds) { - if (inputKinds.includes(kind)) { - console.log(`Found text input kind: ${kind}`); - return kind; - } - } - - // Fallback - find any input kind that contains 'text' - const textKind = inputKinds.find(kind => kind.toLowerCase().includes('text')); - if (textKind) { - console.log(`Found fallback text input kind: ${textKind}`); - return textKind; - } - - throw new Error('No text input kind found'); - } catch (error) { - console.error('Error getting available input kinds:', error.message); - throw error; - } -} - -async function createTextSource(sceneName, textSourceName, text) { - try { - const obsClient = await getOBSClient(); - - // Check if text source already exists globally in OBS - const { inputs } = await obsClient.call('GetInputList'); - const existingInput = inputs.find(input => input.inputName === textSourceName); - - if (!existingInput) { - console.log(`Creating text source "${textSourceName}" in scene "${sceneName}"`); - - // Get the correct text input kind for this OBS installation - const inputKind = await getAvailableTextInputKind(); - - const inputSettings = { - text, - font: { - face: 'Arial', - size: 72, - style: 'Bold' - }, - color: 0xFFFFFFFF, // White text - outline: true, - outline_color: 0xFF000000, // Black outline - outline_size: 4 - }; - - await obsClient.call('CreateInput', { - sceneName, - inputName: textSourceName, - inputKind, - inputSettings - }); - - console.log(`Text source "${textSourceName}" created successfully with kind "${inputKind}"`); - return { success: true, message: 'Text source created successfully' }; - } else { - console.log(`Text source "${textSourceName}" already exists globally, updating settings`); - - // Update existing text source settings - const inputSettings = { - text, - font: { - face: 'Arial', - size: 72, - style: 'Bold' - }, - color: 0xFFFFFFFF, // White text - outline: true, - outline_color: 0xFF000000, // Black outline - outline_size: 4 - }; - - await obsClient.call('SetInputSettings', { - inputName: textSourceName, - inputSettings - }); - - console.log(`Text source "${textSourceName}" settings updated`); - return { success: true, message: 'Text source settings updated' }; - } - } catch (error) { - console.error('Error creating text source:', error.message); - throw error; - } -} - -async function createStreamGroup(groupName, streamName, teamName, url) { - try { - const obsClient = await getOBSClient(); - - // Ensure team scene exists - await createGroupIfNotExists(groupName); - - const cleanGroupName = groupName.toLowerCase().replace(/\s+/g, '_'); - const cleanStreamName = streamName.toLowerCase().replace(/\s+/g, '_'); - const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`; - const sourceName = `${cleanGroupName}_${cleanStreamName}`; - const textSourceName = teamName.toLowerCase().replace(/\s+/g, '_') + '_text'; - - // Create a nested scene for this stream (acts as a group) - try { - await obsClient.call('CreateScene', { sceneName: streamGroupName }); - console.log(`Created nested scene "${streamGroupName}" for stream grouping`); - } catch (sceneError) { - console.log(`Nested scene "${streamGroupName}" might already exist`); - } - - // Create text source globally (reused across streams in the team) - await createTextSource(groupName, textSourceName, teamName); - - // Create browser source globally - const { inputs } = await obsClient.call('GetInputList'); - const browserSourceExists = inputs.some(input => input.inputName === sourceName); - - if (!browserSourceExists) { - await obsClient.call('CreateInput', { - sceneName: streamGroupName, // Create in the nested scene - inputName: sourceName, - inputKind: 'browser_source', - inputSettings: { - width: 1920, - height: 1080, - url, - control_audio: true, - }, - }); - console.log(`Created browser source "${sourceName}" in nested scene`); - } else { - // Add existing source to nested scene - await obsClient.call('CreateSceneItem', { - sceneName: streamGroupName, - sourceName: sourceName - }); - } - - // Add text source to nested scene - try { - await obsClient.call('CreateSceneItem', { - sceneName: streamGroupName, - sourceName: textSourceName - }); - } catch (e) { - console.log('Text source might already be in nested scene'); - } - - // Get the scene items in the nested scene - const { sceneItems: nestedSceneItems } = await obsClient.call('GetSceneItemList', { sceneName: streamGroupName }); - - // Find the browser source and text source items in nested scene - const browserSourceItem = nestedSceneItems.find(item => item.sourceName === sourceName); - const textSourceItem = nestedSceneItems.find(item => item.sourceName === textSourceName); - - // Position the sources properly in the nested scene - if (browserSourceItem && textSourceItem) { - try { - // Position text overlay at top-left of the browser source - await obsClient.call('SetSceneItemTransform', { - sceneName: streamGroupName, // In the nested scene - sceneItemId: textSourceItem.sceneItemId, - sceneItemTransform: { - positionX: 10, - positionY: 10, - scaleX: 1.0, - scaleY: 1.0 - } - }); - - console.log(`Stream sources positioned in nested scene "${streamGroupName}"`); - } catch (positionError) { - console.error('Failed to position sources:', positionError.message || positionError); - } - } - - // Now add the nested scene to the team scene as a group - const { sceneItems: teamSceneItems } = await obsClient.call('GetSceneItemList', { sceneName: groupName }); - const nestedSceneInTeam = teamSceneItems.some(item => item.sourceName === streamGroupName); - - if (!nestedSceneInTeam) { - try { - const { sceneItemId } = await obsClient.call('CreateSceneItem', { - sceneName: groupName, - sourceName: streamGroupName, - sceneItemEnabled: true - }); - console.log(`Added nested scene "${streamGroupName}" to team scene "${groupName}"`); - - // Set bounds to 1600x900 to match the source switcher dimensions - await obsClient.call('SetSceneItemTransform', { - sceneName: groupName, - sceneItemId: sceneItemId, - sceneItemTransform: { - alignment: 5, // Center alignment - boundsAlignment: 0, // Center bounds alignment - boundsType: 'OBS_BOUNDS_SCALE_INNER', // Scale to fit inside bounds - boundsWidth: 1600, - boundsHeight: 900, - scaleX: 1.0, - scaleY: 1.0 - } - }); - console.log(`Set bounds for nested scene to 1600x900`); - } catch (e) { - console.error('Failed to add nested scene to team scene:', e.message); - } - } - - console.log(`Stream group "${streamGroupName}" created as nested scene in team "${groupName}"`); - return { - success: true, - message: 'Stream group created as nested scene', - streamGroupName, - sourceName, - textSourceName - }; - } catch (error) { - console.error('Error creating stream group:', error.message); - throw error; - } -} - // Export all functions module.exports = { @@ -454,8 +212,5 @@ module.exports = { ensureConnected, getConnectionStatus, createGroupIfNotExists, - addSourceToGroup, - createTextSource, - createStreamGroup, - getAvailableTextInputKind + addSourceToGroup }; \ No newline at end of file diff --git a/lib/performance.ts b/lib/performance.ts index 4355393..fa8a079 100644 --- a/lib/performance.ts +++ b/lib/performance.ts @@ -43,9 +43,7 @@ export function createStreamLookupMaps(streams: Array<{ id: number; obs_source_n const idToStreamMap = new Map(); streams.forEach(stream => { - // Generate stream group name to match what's written to files - const streamGroupName = `${stream.name.toLowerCase().replace(/\s+/g, '_')}_stream`; - sourceToIdMap.set(streamGroupName, stream.id); + sourceToIdMap.set(stream.obs_source_name, stream.id); idToStreamMap.set(stream.id, stream); }); diff --git a/lib/security.ts b/lib/security.ts index d5bc705..e38cecf 100644 --- a/lib/security.ts +++ b/lib/security.ts @@ -38,6 +38,7 @@ export function sanitizeString(input: string, maxLength: number = 100): string { // Validation schemas export interface StreamInput { name: string; + obs_source_name: string; url: string; team_id: number; } @@ -57,6 +58,11 @@ export function validateStreamInput(input: unknown): { valid: boolean; errors: s errors.push('Name must be 100 characters or less'); } + if (!data.obs_source_name || typeof data.obs_source_name !== 'string') { + errors.push('OBS source name is required and must be a string'); + } else if (data.obs_source_name.length > 100) { + errors.push('OBS source name must be 100 characters or less'); + } if (!data.url || typeof data.url !== 'string') { errors.push('URL is required and must be a string'); @@ -77,6 +83,7 @@ export function validateStreamInput(input: unknown): { valid: boolean; errors: s errors: [], data: { name: sanitizeString(data.name as string), + obs_source_name: sanitizeString(data.obs_source_name as string), url: data.url as string, team_id: data.team_id as number, },