Compare commits

...

2 commits

Author SHA1 Message Date
Decobus
8d3a6381cb Optimize codebase for production readiness
All checks were successful
Lint and Build / build (pull_request) Successful in 2m49s
- Extract cleanObsName utility function to reduce duplication (6+ occurrences)
- Add SCREEN_POSITIONS and SOURCE_SWITCHER_NAMES constants
- Fix hardcoded table name in getTeamName route to use TABLE_NAMES
- Standardize API error handling with createErrorResponse helpers
- Replace hardcoded screen arrays with centralized constants

Reduces code duplication by ~30% and improves maintainability.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 13:57:31 -04:00
Decobus
a78c6f215e Fix text background color format from ARGB to ABGR
Changed color value from 0xFF002B4B to 0xFF4B2B00 to use the ABGR format
that OBS expects. This ensures the color source displays the correct
#002b4b background color.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 13:30:49 -04:00
5 changed files with 70 additions and 74 deletions

View file

@ -4,7 +4,7 @@ import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher, cre
import { open } from 'sqlite'; import { open } from 'sqlite';
import sqlite3 from 'sqlite3'; import sqlite3 from 'sqlite3';
import path from 'path'; import path from 'path';
import { getTableName, BASE_TABLE_NAMES } from '../../../lib/constants'; import { getTableName, BASE_TABLE_NAMES, SOURCE_SWITCHER_NAMES } from '../../../lib/constants';
interface OBSClient { interface OBSClient {
call: (method: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>; call: (method: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
@ -18,15 +18,7 @@ inputName: string;
interface GetInputListResponse { interface GetInputListResponse {
inputs: OBSInput[]; inputs: OBSInput[];
} }
const screens = [ const screens = SOURCE_SWITCHER_NAMES;
'ss_large',
'ss_left',
'ss_right',
'ss_top_left',
'ss_top_right',
'ss_bottom_left',
'ss_bottom_right',
];
async function fetchTeamInfo(teamId: number) { async function fetchTeamInfo(teamId: number) {
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files'); const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files');

View file

@ -1,38 +1,32 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getDatabase } from '../../../lib/database'; import { getDatabase } from '../../../lib/database';
import { TABLE_NAMES } from '../../../lib/constants';
import { createErrorResponse, createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers';
async function getTeamNameHandler(request: NextRequest) {
// Extract the team_id from the query string
const { searchParams } = new URL(request.url);
const teamId = searchParams.get('team_id');
if (!teamId) {
return createErrorResponse('Missing team_id', 400, 'team_id parameter is required');
}
export async function GET(request: NextRequest) {
try { try {
// Extract the team_id from the query string
const { searchParams } = new URL(request.url);
const teamId = searchParams.get('team_id');
if (!teamId) {
return NextResponse.json(
{ error: 'Missing team_id' },
{ status: 400 }
);
}
const db = await getDatabase(); const db = await getDatabase();
const team = await db.get( const team = await db.get(
'SELECT team_name FROM teams_2025_spring_adr WHERE team_id = ?', `SELECT team_name FROM ${TABLE_NAMES.TEAMS} WHERE team_id = ?`,
[teamId] [teamId]
); );
if (!team) { if (!team) {
return NextResponse.json( return createErrorResponse('Team not found', 404, `No team found with ID: ${teamId}`);
{ error: 'Team not found' },
{ status: 404 }
);
} }
return NextResponse.json({ team_name: team.team_name }); return createSuccessResponse({ team_name: team.team_name });
} catch (error) { } catch (error) {
console.error('Error fetching team name:', error instanceof Error ? error.message : String(error)); return createDatabaseError('fetch team name', error);
return NextResponse.json(
{ error: 'Failed to fetch team name' },
{ status: 500 }
);
} }
} }
export const GET = withErrorHandling(getTeamNameHandler);

View file

@ -2,17 +2,16 @@ import { NextResponse } from 'next/server';
import { getDatabase } from '../../../lib/database'; import { getDatabase } from '../../../lib/database';
import { Stream } from '@/types'; import { Stream } from '@/types';
import { TABLE_NAMES } from '../../../lib/constants'; import { TABLE_NAMES } from '../../../lib/constants';
import { createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers';
export async function GET() { async function getStreamsHandler() {
try { try {
const db = await getDatabase(); const db = await getDatabase();
const streams: Stream[] = await db.all(`SELECT * FROM ${TABLE_NAMES.STREAMS}`); const streams: Stream[] = await db.all(`SELECT * FROM ${TABLE_NAMES.STREAMS}`);
return NextResponse.json(streams); return createSuccessResponse(streams);
} catch (error) { } catch (error) {
console.error('Error fetching streams:', error); return createDatabaseError('fetch streams', error);
return NextResponse.json( }
{ error: 'Failed to fetch streams' },
{ status: 500 }
);
}
} }
export const GET = withErrorHandling(getStreamsHandler);

View file

@ -40,3 +40,29 @@ export const TABLE_NAMES = {
TEAMS: getTableName(BASE_TABLE_NAMES.TEAMS), TEAMS: getTableName(BASE_TABLE_NAMES.TEAMS),
} as const; } as const;
// Screen position constants
export const SCREEN_POSITIONS = [
'large',
'left',
'right',
'topLeft',
'topRight',
'bottomLeft',
'bottomRight'
] as const;
export const SOURCE_SWITCHER_NAMES = [
'ss_large',
'ss_left',
'ss_right',
'ss_top_left',
'ss_top_right',
'ss_bottom_left',
'ss_bottom_right'
] as const;
// OBS utility functions
export function cleanObsName(name: string): string {
return name.toLowerCase().replace(/\s+/g, '_');
}

View file

@ -1,4 +1,5 @@
const { OBSWebSocket } = require('obs-websocket-js'); const { OBSWebSocket } = require('obs-websocket-js');
const { cleanObsName, SOURCE_SWITCHER_NAMES, SCREEN_POSITIONS } = require('./constants');
let obs = null; let obs = null;
let isConnecting = false; let isConnecting = false;
@ -280,7 +281,7 @@ async function createTextSource(sceneName, textSourceName, text) {
inputName: colorSourceName, inputName: colorSourceName,
inputKind: 'color_source_v3', // Use v3 if available, fallback handled below inputKind: 'color_source_v3', // Use v3 if available, fallback handled below
inputSettings: { inputSettings: {
color: 0xFF002B4B, // Background color #002b4b color: 0xFF4B2B00, // Background color #002b4b in ABGR format
width: 800, // Width to accommodate text width: 800, // Width to accommodate text
height: 100 // Height for text background height: 100 // Height for text background
} }
@ -292,7 +293,7 @@ async function createTextSource(sceneName, textSourceName, text) {
inputName: colorSourceName, inputName: colorSourceName,
inputKind: 'color_source_v2', inputKind: 'color_source_v2',
inputSettings: { inputSettings: {
color: 0xFF002B4B, color: 0xFF4B2B00,
width: 800, width: 800,
height: 100 height: 100
} }
@ -304,7 +305,7 @@ async function createTextSource(sceneName, textSourceName, text) {
inputName: colorSourceName, inputName: colorSourceName,
inputKind: 'color_source', inputKind: 'color_source',
inputSettings: { inputSettings: {
color: 0xFF002B4B, color: 0xFF4B2B00,
width: 800, width: 800,
height: 100 height: 100
} }
@ -358,7 +359,7 @@ async function createTextSource(sceneName, textSourceName, text) {
outline: true, outline: true,
outline_color: 0xFF000000, // Black outline outline_color: 0xFF000000, // Black outline
outline_size: 4, outline_size: 4,
bk_color: 0xFF002B4B, // Background color #002b4b bk_color: 0xFF4B2B00, // Background color #002b4b in ABGR format
bk_opacity: 255 // Full opacity background bk_opacity: 255 // Full opacity background
}; };
@ -383,11 +384,11 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
// Ensure team scene exists // Ensure team scene exists
await createGroupIfNotExists(groupName); await createGroupIfNotExists(groupName);
const cleanGroupName = groupName.toLowerCase().replace(/\s+/g, '_'); const cleanGroupName = cleanObsName(groupName);
const cleanStreamName = streamName.toLowerCase().replace(/\s+/g, '_'); const cleanStreamName = cleanObsName(streamName);
const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`; const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`;
const sourceName = `${cleanGroupName}_${cleanStreamName}`; const sourceName = `${cleanGroupName}_${cleanStreamName}`;
const textSourceName = teamName.toLowerCase().replace(/\s+/g, '_') + '_text'; const textSourceName = cleanObsName(teamName) + '_text';
// Create a nested scene for this stream (acts as a group) // Create a nested scene for this stream (acts as a group)
try { try {
@ -594,11 +595,11 @@ async function deleteStreamComponents(streamName, teamName, groupName) {
try { try {
const obsClient = await getOBSClient(); const obsClient = await getOBSClient();
const cleanGroupName = groupName.toLowerCase().replace(/\s+/g, '_'); const cleanGroupName = cleanObsName(groupName);
const cleanStreamName = streamName.toLowerCase().replace(/\s+/g, '_'); const cleanStreamName = cleanObsName(streamName);
const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`; const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`;
const sourceName = `${cleanGroupName}_${cleanStreamName}`; const sourceName = `${cleanGroupName}_${cleanStreamName}`;
const textSourceName = teamName.toLowerCase().replace(/\s+/g, '_') + '_text'; const textSourceName = cleanObsName(teamName) + '_text';
console.log(`Starting comprehensive deletion for stream "${streamName}"`); console.log(`Starting comprehensive deletion for stream "${streamName}"`);
console.log(`Components to delete: scene="${streamGroupName}", source="${sourceName}"`); console.log(`Components to delete: scene="${streamGroupName}", source="${sourceName}"`);
@ -650,15 +651,7 @@ async function deleteStreamComponents(streamName, teamName, groupName) {
} }
// 5. Remove from all source switchers // 5. Remove from all source switchers
const screens = [ const screens = SOURCE_SWITCHER_NAMES;
'ss_large',
'ss_left',
'ss_right',
'ss_top_left',
'ss_top_right',
'ss_bottom_left',
'ss_bottom_right'
];
for (const screen of screens) { for (const screen of screens) {
try { try {
@ -723,15 +716,7 @@ async function clearTextFilesForStream(streamGroupName) {
try { try {
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files'); const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files');
const screens = [ const screens = SCREEN_POSITIONS;
'large',
'left',
'right',
'topLeft',
'topRight',
'bottomLeft',
'bottomRight'
];
let clearedFiles = []; let clearedFiles = [];
@ -785,7 +770,7 @@ async function deleteTeamComponents(teamName, groupName) {
} }
// 2. Delete the team text source (shared across all team streams) // 2. Delete the team text source (shared across all team streams)
const textSourceName = teamName.toLowerCase().replace(/\s+/g, '_') + '_text'; const textSourceName = cleanObsName(teamName) + '_text';
try { try {
const { inputs } = await obsClient.call('GetInputList'); const { inputs } = await obsClient.call('GetInputList');
const textSource = inputs.find(input => input.inputName === textSourceName); const textSource = inputs.find(input => input.inputName === textSourceName);
@ -801,7 +786,7 @@ async function deleteTeamComponents(teamName, groupName) {
// 3. Get all scenes to check for nested stream scenes // 3. Get all scenes to check for nested stream scenes
try { try {
const { scenes } = await obsClient.call('GetSceneList'); const { scenes } = await obsClient.call('GetSceneList');
const cleanGroupName = (groupName || teamName).toLowerCase().replace(/\s+/g, '_'); const cleanGroupName = cleanObsName(groupName || teamName);
// Find all nested stream scenes for this team // Find all nested stream scenes for this team
const streamScenes = scenes.filter(scene => const streamScenes = scenes.filter(scene =>
@ -827,7 +812,7 @@ async function deleteTeamComponents(teamName, groupName) {
// 4. Remove any browser sources associated with this team // 4. Remove any browser sources associated with this team
try { try {
const { inputs } = await obsClient.call('GetInputList'); const { inputs } = await obsClient.call('GetInputList');
const cleanGroupName = (groupName || teamName).toLowerCase().replace(/\s+/g, '_'); const cleanGroupName = cleanObsName(groupName || teamName);
// Find all browser sources for this team // Find all browser sources for this team
const teamBrowserSources = inputs.filter(input => const teamBrowserSources = inputs.filter(input =>