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 sqlite3 from 'sqlite3';
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 {
call: (method: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
@ -18,15 +18,7 @@ inputName: string;
interface GetInputListResponse {
inputs: OBSInput[];
}
const screens = [
'ss_large',
'ss_left',
'ss_right',
'ss_top_left',
'ss_top_right',
'ss_bottom_left',
'ss_bottom_right',
];
const screens = SOURCE_SWITCHER_NAMES;
async function fetchTeamInfo(teamId: number) {
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files');

View file

@ -1,38 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDatabase } from '../../../lib/database';
import { TABLE_NAMES } from '../../../lib/constants';
import { createErrorResponse, createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers';
export async function GET(request: NextRequest) {
try {
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 NextResponse.json(
{ error: 'Missing team_id' },
{ status: 400 }
);
return createErrorResponse('Missing team_id', 400, 'team_id parameter is required');
}
try {
const db = await getDatabase();
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]
);
if (!team) {
return NextResponse.json(
{ error: 'Team not found' },
{ status: 404 }
);
return createErrorResponse('Team not found', 404, `No team found with ID: ${teamId}`);
}
return NextResponse.json({ team_name: team.team_name });
return createSuccessResponse({ team_name: team.team_name });
} catch (error) {
console.error('Error fetching team name:', error instanceof Error ? error.message : String(error));
return NextResponse.json(
{ error: 'Failed to fetch team name' },
{ status: 500 }
);
return createDatabaseError('fetch team name', error);
}
}
export const GET = withErrorHandling(getTeamNameHandler);

View file

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

View file

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