Implement Tuner v1 — Go backend, React frontend, Docker setup

- Go backend: REST API, M3U parser, FFmpeg/MediaMTX process manager
- React/Vite frontend: HLS player, admin panel, channel browser (dark theme)
- MediaMTX config for RTMP ingest + HLS output
- Multi-stage Dockerfile (Go + Bun + Alpine runtime)
- docker-compose.yml for single-container deployment
- Sample M3U playlist with test streams

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 09:47:32 -08:00
parent 2e66b8c73d
commit b8bfcefee8
33 changed files with 2257 additions and 0 deletions

293
frontend/src/App.css Normal file
View File

@@ -0,0 +1,293 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0a0a;
color: #e0e0e0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.app {
max-width: 960px;
margin: 0 auto;
padding: 16px;
}
/* Player */
.player-container {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
background: #111;
border-radius: 8px;
overflow: hidden;
}
.player-video {
width: 100%;
height: 100%;
display: block;
}
.player-video:not([src]),
.player-video[src=""] {
display: none;
}
.player-video:not([src]) ~ .player-offline,
.player-video[src=""] ~ .player-offline {
display: flex;
}
.player-offline {
display: none;
position: absolute;
inset: 0;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: #666;
letter-spacing: 0.05em;
}
/* StatusBar */
.status-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
margin-top: 8px;
background: #1a1a1a;
border-radius: 6px;
font-size: 0.875rem;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.live {
background: #22c55e;
box-shadow: 0 0 6px #22c55e80;
}
.status-dot.offline {
background: #555;
}
.status-text {
font-weight: 600;
}
.status-separator {
color: #444;
}
.status-source {
color: #aaa;
}
.status-channel {
color: #67e8f9;
}
/* Controls layout */
.controls {
display: grid;
grid-template-columns: 240px 1fr;
gap: 12px;
margin-top: 12px;
}
/* AdminPanel */
.admin-panel {
background: #1a1a1a;
border-radius: 6px;
padding: 16px;
}
.admin-title {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #888;
margin-bottom: 12px;
}
.source-buttons {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.source-btn {
flex: 1;
padding: 8px 12px;
border: 1px solid #333;
border-radius: 4px;
background: #2a2a2a;
color: #ccc;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s;
}
.source-btn:hover {
background: #333;
}
.source-btn.active {
background: #164e63;
border-color: #22d3ee;
color: #67e8f9;
}
.reload-btn {
width: 100%;
padding: 8px 12px;
border: 1px solid #333;
border-radius: 4px;
background: #2a2a2a;
color: #ccc;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.15s;
}
.reload-btn:hover {
background: #333;
}
/* ChannelList */
.channel-list {
background: #1a1a1a;
border-radius: 6px;
padding: 16px;
display: flex;
flex-direction: column;
max-height: 500px;
}
.channel-list-title {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #888;
margin-bottom: 12px;
}
.channel-filters {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.channel-search {
flex: 1;
padding: 8px 10px;
border: 1px solid #333;
border-radius: 4px;
background: #2a2a2a;
color: #e0e0e0;
font-size: 0.875rem;
outline: none;
}
.channel-search:focus {
border-color: #555;
}
.channel-search::placeholder {
color: #666;
}
.channel-group-filter {
padding: 8px 10px;
border: 1px solid #333;
border-radius: 4px;
background: #2a2a2a;
color: #e0e0e0;
font-size: 0.875rem;
outline: none;
min-width: 140px;
}
.channel-items {
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.channel-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 4px;
background: #2a2a2a;
color: #e0e0e0;
text-align: left;
cursor: pointer;
transition: all 0.15s;
font-size: 0.875rem;
width: 100%;
}
.channel-item:hover {
background: #333;
}
.channel-item.active {
border-color: #22d3ee;
background: #164e63;
}
.channel-logo {
width: 32px;
height: 32px;
border-radius: 4px;
object-fit: contain;
background: #1a1a1a;
flex-shrink: 0;
}
.channel-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.channel-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.channel-group {
font-size: 0.75rem;
color: #888;
}
.channel-empty {
padding: 24px;
text-align: center;
color: #666;
}
@media (max-width: 640px) {
.controls {
grid-template-columns: 1fr;
}
}