Add OBS group management feature #3

Merged
deco merged 14 commits from ui-improvements into main 2025-07-20 21:46:26 +03:00
14 changed files with 540 additions and 144 deletions
Showing only changes of commit 5789986bb6 - Show all commits

View file

@ -101,6 +101,17 @@ The app uses a sophisticated dual integration approach:
1. **WebSocket Connection**: Direct OBS control using obs-websocket-js with persistent connection management
2. **Text File System**: Each screen position has a corresponding text file that OBS Source Switcher monitors
**Required OBS Source Switchers** (must be created with these exact names):
- `ss_large` - Large screen source switcher
- `ss_left` - Left screen source switcher
- `ss_right` - Right screen source switcher
- `ss_top_left` - Top left screen source switcher
- `ss_top_right` - Top right screen source switcher
- `ss_bottom_left` - Bottom left screen source switcher
- `ss_bottom_right` - Bottom right screen source switcher
See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructions.
**Source Control Workflow**:
1. User selects stream in React UI
2. API writes source name to position-specific text file (e.g., `large.txt`, `left.txt`)

View file

@ -1,24 +1,19 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDatabase } from '../../../lib/database';
import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher } 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';
import { getTableName, BASE_TABLE_NAMES } from '../../../lib/constants';
interface OBSClient {
call: (method: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
}
interface OBSScene {
sceneName: string;
}
interface OBSInput {
inputName: string;
}
interface GetSceneListResponse {
currentProgramSceneName: string;
currentPreviewSceneName: string;
scenes: OBSScene[];
}
interface GetInputListResponse {
inputs: OBSInput[];
@ -33,85 +28,38 @@ const screens = [
'ss_bottom_right',
];
async function fetchTeamName(teamId: number) {
async function fetchTeamInfo(teamId: number) {
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files');
try {
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
const response = await fetch(`${baseUrl}/api/getTeamName?team_id=${teamId}`);
if (!response.ok) {
throw new Error('Failed to fetch team name');
}
const data = await response.json();
return data.team_name;
} catch (error) {
if (error instanceof Error) {
console.error('Error:', error.message);
} else {
console.error('An unknown error occurred:', error);
}
return null;
}
}
async function addBrowserSourceWithAudioControl(obs: OBSClient, sceneName: string, inputName: string, url: string) {
try {
// Step 1: Create the browser source input
await obs.call('CreateInput', {
sceneName,
inputName,
inputKind: 'browser_source',
inputSettings: {
width: 1600,
height: 900,
url,
},
const dbPath = path.join(FILE_DIRECTORY, 'sources.db');
const db = await open({
filename: dbPath,
driver: sqlite3.Database,
});
console.log(`Browser source "${inputName}" created successfully.`);
// Step 2: Wait for the input to initialize
let inputReady = false;
for (let i = 0; i < 10; i++) {
try {
await obs.call('GetInputSettings', { inputName });
inputReady = true;
break;
} catch {
console.log(`Waiting for input "${inputName}" to initialize...`);
await new Promise((resolve) => setTimeout(resolve, 500)); // Wait 500ms before retrying
}
}
if (!inputReady) {
throw new Error(`Input "${inputName}" did not initialize in time.`);
}
// Step 3: Enable "Reroute audio"
await obs.call('SetInputSettings', {
inputName,
inputSettings: {
reroute_audio: true,
},
overlay: true, // Keep existing settings and apply changes
const teamsTableName = getTableName(BASE_TABLE_NAMES.TEAMS, {
year: 2025,
season: 'summer',
suffix: 'sat'
});
console.log(`Audio rerouted for "${inputName}".`);
const teamInfo = await db.get(
`SELECT team_name, group_name FROM ${teamsTableName} WHERE team_id = ?`,
[teamId]
);
// Step 4: Mute the input
await obs.call('SetInputMute', {
inputName,
inputMuted: true,
});
await db.close();
return teamInfo;
} catch (error) {
if (error instanceof Error) {
console.error('Error fetching team info:', error.message);
} else {
console.error('An unknown error occurred:', error);
}
return null;
}
}
console.log(`Audio muted for "${inputName}".`);
} catch (error) {
if (error instanceof Error) {
console.error('Error adding browser source with audio control:', error.message);
} else {
console.error('An unknown error occurred while adding browser source:', error);
}
}
}
import { validateStreamInput } from '../../../lib/security';
@ -160,20 +108,23 @@ export async function POST(request: NextRequest) {
}
throw new Error('GetInputList failed.');
}
const teamName = await fetchTeamName(team_id);
console.log('Team Name:', teamName)
const response = await obs.call('GetSceneList');
const sceneListResponse = response as unknown as GetSceneListResponse;
const { scenes } = sceneListResponse;
const groupExists = scenes.some((scene: OBSScene) => scene.sceneName === teamName);
if (!groupExists) {
await obs.call('CreateScene', { sceneName: teamName });
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) {
await addBrowserSourceWithAudioControl(obs, teamName, obs_source_name, url)
// 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.`);
@ -196,7 +147,12 @@ export async function POST(request: NextRequest) {
}
const db = await getDatabase();
const query = `INSERT INTO streams_2025_spring_adr (name, obs_source_name, url, team_id) VALUES (?, ?, ?, ?)`;
const streamsTableName = getTableName(BASE_TABLE_NAMES.STREAMS, {
year: 2025,
season: 'summer',
suffix: 'sat'
});
const query = `INSERT INTO ${streamsTableName} (name, obs_source_name, url, team_id) VALUES (?, ?, ?, ?)`;
db.run(query, [name, obs_source_name, url, team_id])
await disconnectFromOBS();
return NextResponse.json({ message: 'Stream added successfully' }, {status: 201})

View file

@ -0,0 +1,67 @@
import { NextRequest, NextResponse } from 'next/server';
import { open } from 'sqlite';
import sqlite3 from 'sqlite3';
import path from 'path';
import { getTableName, BASE_TABLE_NAMES } from '@/lib/constants';
import { validateInteger } from '@/lib/security';
const { createGroupIfNotExists } = require('@/lib/obsClient');
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files');
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { teamId, groupName } = body;
// Validate input
if (!teamId || !groupName) {
return NextResponse.json({ error: 'Team ID and group name are required' }, { status: 400 });
}
const validTeamId = validateInteger(teamId);
if (!validTeamId) {
return NextResponse.json({ error: 'Invalid team ID' }, { status: 400 });
}
// Sanitize group name (only allow alphanumeric, spaces, dashes, underscores)
const sanitizedGroupName = groupName.replace(/[^a-zA-Z0-9\s\-_]/g, '');
if (!sanitizedGroupName || sanitizedGroupName.length === 0) {
return NextResponse.json({ error: 'Invalid group name' }, { status: 400 });
}
// Open database connection
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'
});
// Update team with group name
await db.run(
`UPDATE ${teamsTableName} SET group_name = ? WHERE team_id = ?`,
[sanitizedGroupName, validTeamId]
);
// Create group in OBS
const result = await createGroupIfNotExists(sanitizedGroupName);
await db.close();
return NextResponse.json({
success: true,
message: 'Group created/updated successfully',
groupName: sanitizedGroupName,
obsResult: result
});
} catch (error) {
console.error('Error creating group:', error);
return NextResponse.json({ error: 'Failed to create group' }, { status: 500 });
}
}

View file

@ -0,0 +1,81 @@
import { NextResponse } from 'next/server';
import { open } from 'sqlite';
import sqlite3 from 'sqlite3';
import path from 'path';
import { getTableName, BASE_TABLE_NAMES } from '@/lib/constants';
const { createGroupIfNotExists } = require('@/lib/obsClient');
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files');
export async function POST() {
try {
// Open database connection
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'
});
// Get all teams without groups
const teamsWithoutGroups = await db.all(
`SELECT team_id, team_name FROM ${teamsTableName} WHERE group_name IS NULL`
);
const syncResults = [];
for (const team of teamsWithoutGroups) {
try {
// Create group in OBS using team name
const obsResult = await createGroupIfNotExists(team.team_name);
// Update database with group name
await db.run(
`UPDATE ${teamsTableName} SET group_name = ? WHERE team_id = ?`,
[team.team_name, team.team_id]
);
syncResults.push({
teamId: team.team_id,
teamName: team.team_name,
groupName: team.team_name,
success: true,
obsResult
});
} catch (error) {
console.error(`Error syncing team ${team.team_id}:`, error);
syncResults.push({
teamId: team.team_id,
teamName: team.team_name,
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
await db.close();
const successCount = syncResults.filter(r => r.success).length;
const failureCount = syncResults.filter(r => !r.success).length;
return NextResponse.json({
success: true,
message: `Sync completed: ${successCount} successful, ${failureCount} failed`,
results: syncResults,
summary: {
total: syncResults.length,
successful: successCount,
failed: failureCount
}
});
} catch (error) {
console.error('Error syncing groups:', error);
return NextResponse.json({ error: 'Failed to sync groups' }, { status: 500 });
}
}

View file

@ -1,4 +1,3 @@
import { NextResponse } from 'next/server';
import { getDatabase } from '../../../lib/database';
import { Team } from '@/types';
import { TABLE_NAMES } from '@/lib/constants';
@ -39,14 +38,14 @@ function validateTeamInput(data: unknown): {
return {
valid: true,
data: { team_name: team_name.trim() }
data: { team_name: (team_name as string).trim() }
};
}
export const GET = withErrorHandling(async () => {
try {
const db = await getDatabase();
const teams: Team[] = await db.all(`SELECT * FROM ${TABLE_NAMES.TEAMS} ORDER BY team_name ASC`);
const teams: Team[] = await db.all(`SELECT team_id, team_name, group_name FROM ${TABLE_NAMES.TEAMS} ORDER BY team_name ASC`);
return createSuccessResponse(teams);
} catch (error) {
@ -86,7 +85,8 @@ export const POST = withErrorHandling(async (request: Request) => {
const newTeam: Team = {
team_id: result.lastID!,
team_name: team_name
team_name: team_name,
group_name: null
};
return createSuccessResponse(newTeam, 201);

View file

@ -11,6 +11,8 @@ export default function Teams() {
const [newTeamName, setNewTeamName] = useState('');
const [editingTeam, setEditingTeam] = useState<Team | null>(null);
const [editingName, setEditingName] = useState('');
const [creatingGroupForTeam, setCreatingGroupForTeam] = useState<number | null>(null);
const [isSyncing, setIsSyncing] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [updatingTeamId, setUpdatingTeamId] = useState<number | null>(null);
const [deletingTeamId, setDeletingTeamId] = useState<number | null>(null);
@ -146,6 +148,64 @@ export default function Teams() {
}
};
const handleSyncAllGroups = async () => {
if (!confirm('This will create OBS groups for all teams that don\'t have one. Continue?')) {
return;
}
setIsSyncing(true);
try {
const res = await fetch('/api/syncGroups', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (res.ok) {
const result = await res.json();
fetchTeams();
showSuccess('Groups Synced', `${result.summary.successful} groups created successfully`);
if (result.summary.failed > 0) {
showError('Some Failures', `${result.summary.failed} groups failed to create`);
}
} else {
const error = await res.json();
showError('Failed to Sync Groups', error.error || 'Unknown error occurred');
}
} catch (error) {
console.error('Error syncing groups:', error);
showError('Failed to Sync Groups', 'Network error or server unavailable');
} finally {
setIsSyncing(false);
}
};
const handleCreateGroup = async (teamId: number, teamName: string) => {
const groupName = prompt(`Enter group name for team "${teamName}":`, teamName);
if (!groupName) return;
setCreatingGroupForTeam(teamId);
try {
const res = await fetch('/api/createGroup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ teamId, groupName }),
});
if (res.ok) {
fetchTeams();
showSuccess('Group Created', `OBS group "${groupName}" created for team "${teamName}"`);
} else {
const error = await res.json();
showError('Failed to Create Group', error.error || 'Unknown error occurred');
}
} catch (error) {
console.error('Error creating group:', error);
showError('Failed to Create Group', 'Network error or server unavailable');
} finally {
setCreatingGroupForTeam(null);
}
};
const startEditing = (team: Team) => {
setEditingTeam(team);
setEditingName(team.team_name);
@ -209,7 +269,18 @@ export default function Teams() {
{/* Teams List */}
<div className="glass p-6">
<h2 className="card-title">Existing Teams</h2>
<div className="flex justify-between items-center mb-6">
<h2 className="card-title">Existing Teams</h2>
<button
onClick={handleSyncAllGroups}
disabled={isSyncing || isLoading}
className="btn btn-success"
title="Create OBS groups for all teams without groups"
>
<span className="icon">🔄</span>
{isSyncing ? 'Syncing...' : 'Sync All Groups'}
</button>
</div>
{isLoading ? (
<div className="text-center p-8">
@ -227,7 +298,7 @@ export default function Teams() {
) : (
<div className="space-y-4">
{teams.map((team) => (
<div key={team.team_id} className="glass p-4">
<div key={team.team_id} className="glass p-4 mb-4">
{editingTeam?.team_id === team.team_id ? (
<div className="form-row">
<input
@ -266,9 +337,25 @@ export default function Teams() {
<div>
<div className="font-semibold text-white">{team.team_name}</div>
<div className="text-sm text-white/60">ID: {team.team_id}</div>
{team.group_name ? (
<div className="text-sm text-green-400">OBS Group: {team.group_name}</div>
) : (
<div className="text-sm text-orange-400">No OBS Group</div>
)}
</div>
</div>
<div className="button-group">
{!team.group_name && (
<button
onClick={() => handleCreateGroup(team.team_id, team.team_name)}
disabled={creatingGroupForTeam === team.team_id || deletingTeamId === team.team_id || updatingTeamId === team.team_id}
className="btn-success btn-sm"
title="Create OBS group"
>
<span className="icon">🎬</span>
{creatingGroupForTeam === team.team_id ? 'Creating...' : 'Create Group'}
</button>
)}
<button
onClick={() => startEditing(team)}
disabled={deletingTeamId === team.team_id || updatingTeamId === team.team_id}

99
docs/OBS_SETUP.md Normal file
View file

@ -0,0 +1,99 @@
# OBS Source Switcher Setup Guide
This document explains how to configure OBS Studio to work with the Source Switcher Plugin UI.
## Prerequisites
1. OBS Studio installed
2. [OBS WebSocket plugin](https://github.com/obsproject/obs-websocket) (usually included with OBS 28+)
3. [OBS Source Switcher plugin](https://obsproject.com/forum/resources/source-switcher.1090/) installed
## Required Source Switcher Names
You must create **exactly 7 Source Switcher sources** in OBS with these specific names:
| Source Switcher Name | Screen Position | Text File |
|---------------------|-----------------|-----------|
| `ss_large` | Main/Large screen | `large.txt` |
| `ss_left` | Left screen | `left.txt` |
| `ss_right` | Right screen | `right.txt` |
| `ss_top_left` | Top left corner | `topLeft.txt` |
| `ss_top_right` | Top right corner | `topRight.txt` |
| `ss_bottom_left` | Bottom left corner | `bottomLeft.txt` |
| `ss_bottom_right` | Bottom right corner | `bottomRight.txt` |
## Setup Instructions
### 1. Configure OBS WebSocket
1. In OBS, go to **Tools → WebSocket Server Settings**
2. Enable the WebSocket server
3. Set a port (default: 4455)
4. Optionally set a password
5. Note these settings for your `.env.local` file
### 2. Create Source Switcher Sources
For each screen position:
1. In OBS, click the **+** button in Sources
2. Select **Source Switcher**
3. Name it exactly as shown in the table above (e.g., `ss_large`)
4. Configure the Source Switcher:
- **Mode**: Text File
- **File Path**: Point to the corresponding text file in your `files` directory
- **Switch Behavior**: Choose your preferred transition
### 3. Configure Text File Monitoring
Each Source Switcher should monitor its corresponding text file:
- `ss_large` → monitors `{FILE_DIRECTORY}/large.txt`
- `ss_left` → monitors `{FILE_DIRECTORY}/left.txt`
- etc.
Where `{FILE_DIRECTORY}` is the path configured in your `.env.local` file (default: `./files`)
### 4. Add Browser Sources
When you add streams through the UI, browser sources are automatically created in OBS with these settings:
- **Width**: 1600px
- **Height**: 900px
- **Audio**: Controlled via OBS (muted by default)
## How It Works
1. **Stream Selection**: When you select a stream for a screen position in the UI
2. **File Update**: The app writes the OBS source name to the corresponding text file
3. **Source Switch**: The Source Switcher detects the file change and switches to that source
4. **Group Organization**: Streams are organized into OBS groups based on their teams
## Troubleshooting
### Source Switcher not switching
- Verify the text file path is correct
- Check that the file is being updated (manually open the .txt file)
- Ensure Source Switcher is set to "Text File" mode
### Sources not appearing
- Check OBS WebSocket connection in the footer
- Verify WebSocket credentials in `.env.local`
- Ensure the source name doesn't already exist in OBS
### Missing screen positions
- Verify all 7 Source Switchers are created with exact names
- Check for typos in source names (they must match exactly)
## Environment Variables
Configure these in your `.env.local` file:
```env
# OBS WebSocket Settings
OBS_WEBSOCKET_HOST=127.0.0.1
OBS_WEBSOCKET_PORT=4455
OBS_WEBSOCKET_PASSWORD=your_password_here
# File Directory (where text files are stored)
FILE_DIRECTORY=./files
```

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

View file

@ -0,0 +1,57 @@
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import path from 'path';
import { getTableName, BASE_TABLE_NAMES } from '../lib/constants';
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files');
const addGroupNameToTeams = async () => {
try {
const dbPath = path.join(FILE_DIRECTORY, 'sources.db');
// Open database connection
const db = await open({
filename: dbPath,
driver: sqlite3.Database,
});
console.log('Database connection established.');
// Generate table name for teams
const teamsTableName = getTableName(BASE_TABLE_NAMES.TEAMS, {
year: 2025,
season: 'summer',
suffix: 'sat'
});
console.log(`Adding group_name column to ${teamsTableName}`);
// Check if column already exists
const tableInfo = await db.all(`PRAGMA table_info(${teamsTableName})`);
const hasGroupName = tableInfo.some(col => col.name === 'group_name');
if (!hasGroupName) {
// Add group_name column
await db.exec(`
ALTER TABLE ${teamsTableName}
ADD COLUMN group_name TEXT
`);
console.log(`✅ Added group_name column to ${teamsTableName}`);
} else {
console.log(` group_name column already exists in ${teamsTableName}`);
}
// Close database connection
await db.close();
console.log('Database connection closed.');
console.log('✅ Successfully updated teams table schema!');
} catch (error) {
console.error('Error updating table:', error);
process.exit(1);
}
};
// Run the script
addGroupNameToTeams();

View file

@ -60,7 +60,8 @@ const createSatSummer2025Tables = async () => {
await db.exec(`
CREATE TABLE IF NOT EXISTS ${teamsTableName} (
team_id INTEGER PRIMARY KEY,
team_name TEXT NOT NULL
team_name TEXT NOT NULL,
group_name TEXT
)
`);

View file

@ -14,4 +14,5 @@ export type Screen = {
export type Team = {
team_id: number;
team_name: string;
group_name?: string | null;
};