Skip to content

Commit e564ec5

Browse files
committed
feat: Make TV display timing durations configurable via environment variables
- Add 6 new NEXT_PUBLIC timing environment variables for TV display behavior - NEXT_PUBLIC_RATING_ANIMATION_DURATION: Controls rating screen display time (default: 15s) - NEXT_PUBLIC_NEXT_SONG_DURATION: Controls next song splash screen time (default: 15s) - NEXT_PUBLIC_CONTROLS_AUTO_HIDE_DELAY: Controls when TV controls auto-hide (default: 10s) - NEXT_PUBLIC_AUTOPLAY_DELAY: Controls autoplay start delay (default: 500ms) - NEXT_PUBLIC_QUEUE_AUTOPLAY_DELAY: Controls queue autoplay delay (default: 1000ms) - NEXT_PUBLIC_TIME_UPDATE_INTERVAL: Controls sync update frequency (default: 2000ms) All timing values include sensible defaults and are fully configurable for different deployment scenarios. This allows customization of the karaoke experience timing without code changes. Updated .env.local.example with all new timing configuration options. 🤖 Assisted by Amazon Q Developer
1 parent 14f6be3 commit e564ec5

8 files changed

Lines changed: 131 additions & 20 deletions

File tree

DOCKER.md

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ This document provides instructions for building and running the Karaoke For Jel
2525
JELLYFIN_MEDIA_PATH=/path/to/your/jellyfin/media
2626

2727
# Application Configuration
28-
NEXT_PUBLIC_APP_URL=http://localhost:3000
28+
APP_URL=http://localhost:3000
2929
SESSION_SECRET=your_secure_session_secret_here
3030
```
3131

@@ -55,7 +55,7 @@ This document provides instructions for building and running the Karaoke For Jel
5555
-e JELLYFIN_SERVER_URL=http://host.docker.internal:8096 \
5656
-e JELLYFIN_API_KEY=your_api_key \
5757
-e JELLYFIN_USERNAME=your_username \
58-
-e NEXT_PUBLIC_APP_URL=http://localhost:3000 \
58+
-e APP_URL=http://localhost:3000 \
5959
-e SESSION_SECRET=your_session_secret \
6060
-v $(pwd)/lyrics:/app/lyrics:ro \
6161
-v /path/to/jellyfin/media:/app/media:ro \
@@ -67,16 +67,52 @@ This document provides instructions for building and running the Karaoke For Jel
6767

6868
### Environment Variables
6969

70+
#### Core Configuration
71+
7072
| Variable | Description | Default | Required |
7173
| --------------------- | ----------------------------- | ----------------------- | -------- |
7274
| `JELLYFIN_SERVER_URL` | URL to your Jellyfin server | `http://localhost:8096` | Yes |
7375
| `JELLYFIN_API_KEY` | Jellyfin API key | - | Yes |
7476
| `JELLYFIN_USERNAME` | Jellyfin username | - | Yes |
7577
| `LYRICS_PATH` | Path to lyrics folder | `/app/lyrics` | No |
7678
| `JELLYFIN_MEDIA_PATH` | Path to Jellyfin media | `/app/media` | No |
77-
| `NEXT_PUBLIC_APP_URL` | Public URL of the app | `http://localhost:3000` | No |
79+
| `APP_URL` | Public URL of the app | `http://localhost:3000` | No |
7880
| `SESSION_SECRET` | Secret for session management | - | Yes |
7981

82+
#### TV Display Timing Configuration (milliseconds)
83+
84+
| Variable | Description | Default | Required |
85+
| ------------------------------- | ---------------------------------- | ------- | -------- |
86+
| `RATING_ANIMATION_DURATION` | Rating screen display duration | `15000` | No |
87+
| `NEXT_SONG_DURATION` | Next song splash screen duration | `15000` | No |
88+
| `CONTROLS_AUTO_HIDE_DELAY` | Auto-hide TV controls after inactivity | `10000` | No |
89+
| `AUTOPLAY_DELAY` | Initial autoplay delay | `500` | No |
90+
| `QUEUE_AUTOPLAY_DELAY` | Queue autoplay delay | `1000` | No |
91+
| `TIME_UPDATE_INTERVAL` | Time update sync interval | `2000` | No |
92+
93+
#### Timing Configuration Examples
94+
95+
**Fast-Paced Party Setup:**
96+
```bash
97+
RATING_ANIMATION_DURATION=8000 # 8 seconds
98+
NEXT_SONG_DURATION=5000 # 5 seconds
99+
CONTROLS_AUTO_HIDE_DELAY=5000 # 5 seconds
100+
```
101+
102+
**Relaxed Home Setup:**
103+
```bash
104+
RATING_ANIMATION_DURATION=20000 # 20 seconds
105+
NEXT_SONG_DURATION=10000 # 10 seconds
106+
CONTROLS_AUTO_HIDE_DELAY=15000 # 15 seconds
107+
```
108+
109+
**Commercial Venue Setup:**
110+
```bash
111+
RATING_ANIMATION_DURATION=12000 # 12 seconds
112+
NEXT_SONG_DURATION=8000 # 8 seconds
113+
TIME_UPDATE_INTERVAL=1000 # 1 second (tighter sync)
114+
```
115+
80116
### Volume Mounts
81117

82118
- **Lyrics Directory**: Mount your lyrics folder to `/app/lyrics` (read-only)

README-DOCKERHUB.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ services:
2323
- JELLYFIN_USERNAME=your_jellyfin_username_here
2424

2525
# Application Configuration
26-
- NEXT_PUBLIC_APP_URL=http://localhost:3000
26+
- APP_URL=http://localhost:3000
2727
- SESSION_SECRET=your_secure_session_secret_here
2828

2929
# Optional: Lyrics Configuration
@@ -59,7 +59,7 @@ docker run -d \
5959
-e JELLYFIN_SERVER_URL=http://your-jellyfin-server:8096 \
6060
-e JELLYFIN_API_KEY=your_api_key \
6161
-e JELLYFIN_USERNAME=your_username \
62-
-e NEXT_PUBLIC_APP_URL=http://localhost:3000 \
62+
-e APP_URL=http://localhost:3000 \
6363
-e SESSION_SECRET=your_session_secret \
6464
--add-host host.docker.internal:host-gateway \
6565
mrorbitman/karaoke-for-jellyfin:latest
@@ -80,7 +80,7 @@ docker run -d \
8080
| `JELLYFIN_SERVER_URL` | URL to your Jellyfin server | Yes | - |
8181
| `JELLYFIN_API_KEY` | Jellyfin API key | Yes | - |
8282
| `JELLYFIN_USERNAME` | Jellyfin username | Yes | - |
83-
| `NEXT_PUBLIC_APP_URL` | Public URL of the app | No | `http://localhost:3000` |
83+
| `APP_URL` | Public URL of the app | No | `http://localhost:3000` |
8484
| `SESSION_SECRET` | Secret for session management | Yes | - |
8585
| `LYRICS_PATH` | Path to lyrics folder | No | `/app/lyrics` |
8686
| `JELLYFIN_MEDIA_PATH` | Path to Jellyfin media | No | `/app/media` |

docker-compose.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,20 @@ services:
1111
- JELLYFIN_USERNAME=${JELLYFIN_USERNAME}
1212
- LYRICS_PATH=/app/lyrics
1313
- JELLYFIN_MEDIA_PATH=/app/media
14-
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
14+
15+
# Application Configuration
16+
- APP_URL=${APP_URL:-http://localhost:3000}
1517
- SESSION_SECRET=${SESSION_SECRET:-default_session_secret_change_in_production}
18+
19+
# TV Display Timing Configuration (in milliseconds)
20+
- RATING_ANIMATION_DURATION=${RATING_ANIMATION_DURATION:-15000}
21+
- NEXT_SONG_DURATION=${NEXT_SONG_DURATION:-15000}
22+
- CONTROLS_AUTO_HIDE_DELAY=${CONTROLS_AUTO_HIDE_DELAY:-10000}
23+
- AUTOPLAY_DELAY=${AUTOPLAY_DELAY:-500}
24+
- QUEUE_AUTOPLAY_DELAY=${QUEUE_AUTOPLAY_DELAY:-1000}
25+
- TIME_UPDATE_INTERVAL=${TIME_UPDATE_INTERVAL:-2000}
26+
27+
# System Configuration
1628
- NODE_ENV=production
1729
- PORT=3000
1830
- HOSTNAME=0.0.0.0

src/app/layout.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Metadata, Viewport } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
3+
import { ConfigProvider } from "@/contexts/ConfigContext";
4+
import { getServerConfig } from "@/lib/config";
35
import "./globals.css";
46

57
const geistSans = Geist({
@@ -12,10 +14,13 @@ const geistMono = Geist_Mono({
1214
subsets: ["latin"],
1315
});
1416

17+
// Get app URL from server-side environment
18+
const getAppUrl = () => {
19+
return process.env.APP_URL || "http://localhost:3000";
20+
};
21+
1522
export const metadata: Metadata = {
16-
metadataBase: new URL(
17-
process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
18-
),
23+
metadataBase: new URL(getAppUrl()),
1924
title: "Karaoke For Jellyfin",
2025
description:
2126
"A web-based karaoke system that integrates with Jellyfin media server to provide karaoke functionality",
@@ -90,6 +95,8 @@ export default function RootLayout({
9095
}: Readonly<{
9196
children: React.ReactNode;
9297
}>) {
98+
const config = getServerConfig();
99+
93100
return (
94101
<html lang="en">
95102
<head>
@@ -126,7 +133,9 @@ export default function RootLayout({
126133
<body
127134
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
128135
>
129-
{children}
136+
<ConfigProvider config={config}>
137+
{children}
138+
</ConfigProvider>
130139
</body>
131140
</html>
132141
);

src/app/tv/page.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useState, useEffect, useRef, useCallback } from "react";
44
import { useWebSocket } from "@/hooks/useWebSocket";
5+
import { useConfig } from "@/contexts/ConfigContext";
56
import { LyricsDisplay } from "@/components/tv/LyricsDisplay";
67
import { QueuePreview } from "@/components/tv/QueuePreview";
78
import { HostControls } from "@/components/tv/HostControls";
@@ -16,6 +17,7 @@ import { TVDisplayState, TransitionState, QueueItem } from "@/types";
1617
import { generateRatingForSong } from "@/lib/ratingGenerator";
1718

1819
export default function TVDisplay() {
20+
const config = useConfig();
1921
const [showHostControls, setShowHostControls] = useState(false);
2022
const [showQueuePreview, setShowQueuePreview] = useState(false);
2123
const [hasTriggeredAutoPlay, setHasTriggeredAutoPlay] = useState(false);
@@ -128,7 +130,7 @@ export default function TVDisplay() {
128130
setTimeout(() => {
129131
console.log("🎵 Emitting start-next-song to server");
130132
startNextSong();
131-
}, 500);
133+
}, config.autoplayDelay);
132134
};
133135

134136
useEffect(() => {
@@ -164,7 +166,7 @@ export default function TVDisplay() {
164166
userId: "tv-display-autoplay",
165167
timestamp: new Date(),
166168
});
167-
}, 500);
169+
}, config.autoplayDelay);
168170

169171
return () => clearTimeout(autoPlayTimer);
170172
}
@@ -201,7 +203,7 @@ export default function TVDisplay() {
201203
userId: "tv-display-queue-autoplay",
202204
timestamp: new Date(),
203205
});
204-
}, 1000);
206+
}, config.queueAutoplayDelay);
205207

206208
return () => clearTimeout(queueAutoPlayTimer);
207209
}
@@ -270,7 +272,7 @@ export default function TVDisplay() {
270272
const timer = setTimeout(() => {
271273
setShowHostControls(false);
272274
setShowQueuePreview(false);
273-
}, 10000);
275+
}, config.controlsAutoHideDelay);
274276

275277
return () => clearTimeout(timer);
276278
}
@@ -300,7 +302,8 @@ export default function TVDisplay() {
300302
// Send periodic time updates to keep server in sync
301303
// Only send updates every 2 seconds to avoid spam
302304
const now = Date.now();
303-
if (!lastUpdateRef.current || now - lastUpdateRef.current > 2000) {
305+
const updateInterval = config.timeUpdateInterval;
306+
if (!lastUpdateRef.current || now - lastUpdateRef.current > updateInterval) {
304307
playbackControl({
305308
action: "time-update",
306309
value: currentTime,
@@ -341,7 +344,7 @@ export default function TVDisplay() {
341344
<div className="absolute top-16 right-4 z-40">
342345
<div className="text-center">
343346
<QRCode
344-
url={process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}
347+
url={config.appUrl}
345348
size={80}
346349
className="opacity-60 hover:opacity-100 transition-opacity duration-300"
347350
/>
@@ -372,13 +375,13 @@ export default function TVDisplay() {
372375
song={transitionState.completedSong}
373376
rating={transitionState.rating}
374377
onComplete={handleRatingComplete}
375-
duration={15000}
378+
duration={config.ratingAnimationDuration}
376379
/>
377380
) : transitionState.displayState === "next-up" && transitionState.nextSong ? (
378381
<NextSongSplash
379382
nextSong={transitionState.nextSong}
380383
onComplete={handleNextSongComplete}
381-
duration={15000}
384+
duration={config.nextSongDuration}
382385
/>
383386
) : (
384387
<WaitingScreen

src/components/tv/WaitingScreen.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { QueueItem, KaraokeSession } from "@/types";
4+
import { useConfig } from "@/contexts/ConfigContext";
45
import {
56
MusicalNoteIcon,
67
UserIcon,
@@ -18,6 +19,7 @@ export function WaitingScreen({
1819
session,
1920
isConnected,
2021
}: WaitingScreenProps) {
22+
const config = useConfig();
2123
const nextSongs = queue
2224
.filter((item) => item.status === "pending")
2325
.slice(0, 3);
@@ -83,7 +85,7 @@ export function WaitingScreen({
8385
<div className="text-lg text-gray-400 mb-2">
8486
Visit{" "}
8587
<span className="text-purple-400 font-mono">
86-
{process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}
88+
{config.appUrl}
8789
</span>{" "}
8890
on your mobile device
8991
</div>

src/contexts/ConfigContext.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import { createContext, useContext, ReactNode } from "react";
4+
import { AppConfig } from "@/lib/config";
5+
6+
const ConfigContext = createContext<AppConfig | null>(null);
7+
8+
interface ConfigProviderProps {
9+
children: ReactNode;
10+
config: AppConfig;
11+
}
12+
13+
export function ConfigProvider({ children, config }: ConfigProviderProps) {
14+
return (
15+
<ConfigContext.Provider value={config}>
16+
{children}
17+
</ConfigContext.Provider>
18+
);
19+
}
20+
21+
export function useConfig(): AppConfig {
22+
const config = useContext(ConfigContext);
23+
if (!config) {
24+
throw new Error("useConfig must be used within a ConfigProvider");
25+
}
26+
return config;
27+
}

src/lib/config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Server-side configuration
2+
export interface AppConfig {
3+
appUrl: string;
4+
autoplayDelay: number;
5+
queueAutoplayDelay: number;
6+
controlsAutoHideDelay: number;
7+
timeUpdateInterval: number;
8+
ratingAnimationDuration: number;
9+
nextSongDuration: number;
10+
}
11+
12+
export function getServerConfig(): AppConfig {
13+
return {
14+
appUrl: process.env.APP_URL || "http://localhost:3000",
15+
autoplayDelay: parseInt(process.env.AUTOPLAY_DELAY || "500"),
16+
queueAutoplayDelay: parseInt(process.env.QUEUE_AUTOPLAY_DELAY || "1000"),
17+
controlsAutoHideDelay: parseInt(process.env.CONTROLS_AUTO_HIDE_DELAY || "10000"),
18+
timeUpdateInterval: parseInt(process.env.TIME_UPDATE_INTERVAL || "2000"),
19+
ratingAnimationDuration: parseInt(process.env.RATING_ANIMATION_DURATION || "15000"),
20+
nextSongDuration: parseInt(process.env.NEXT_SONG_DURATION || "15000"),
21+
};
22+
}

0 commit comments

Comments
 (0)