Sync main into feature/nightly
This commit is contained in:
commit
a27eb76777
12 changed files with 79 additions and 85 deletions
4
.github/workflows/docker-build.yml
vendored
4
.github/workflows/docker-build.yml
vendored
|
|
@ -29,13 +29,13 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||||
echo "tag=main" >> $GITHUB_OUTPUT
|
echo "tag=main" >> $GITHUB_OUTPUT
|
||||||
echo "version=1.0.0" >> $GITHUB_OUTPUT
|
echo "version=1.1.0" >> $GITHUB_OUTPUT
|
||||||
echo "channel=stable" >> $GITHUB_OUTPUT
|
echo "channel=stable" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
# Ersetze Slashes durch Bindestriche für gültige Docker Tags
|
# Ersetze Slashes durch Bindestriche für gültige Docker Tags
|
||||||
CLEAN_TAG=$(echo "${{ github.ref_name }}" | sed 's/\//-/g')
|
CLEAN_TAG=$(echo "${{ github.ref_name }}" | sed 's/\//-/g')
|
||||||
echo "tag=$CLEAN_TAG" >> $GITHUB_OUTPUT
|
echo "tag=$CLEAN_TAG" >> $GITHUB_OUTPUT
|
||||||
echo "version=1.0.0-nightly" >> $GITHUB_OUTPUT
|
echo "version=1.1.0-nightly" >> $GITHUB_OUTPUT
|
||||||
echo "channel=nightly" >> $GITHUB_OUTPUT
|
echo "channel=nightly" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ RUN npm install --no-audit --no-fund
|
||||||
COPY web/ .
|
COPY web/ .
|
||||||
# Umgebungsvariable für React Build verfügbar machen (Vite liest nur VITE_*)
|
# Umgebungsvariable für React Build verfügbar machen (Vite liest nur VITE_*)
|
||||||
ARG VITE_BUILD_CHANNEL=stable
|
ARG VITE_BUILD_CHANNEL=stable
|
||||||
ARG VITE_APP_VERSION=1.0.0
|
ARG VITE_APP_VERSION=1.1.0
|
||||||
ENV VITE_BUILD_CHANNEL=$VITE_BUILD_CHANNEL
|
ENV VITE_BUILD_CHANNEL=$VITE_BUILD_CHANNEL
|
||||||
ENV VITE_APP_VERSION=$VITE_APP_VERSION
|
ENV VITE_APP_VERSION=$VITE_APP_VERSION
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,11 @@ git push origin main
|
||||||
git branch -d feature/mein-experiment
|
git branch -d feature/mein-experiment
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Versionierung & Changelog
|
||||||
|
- Versionen werden in `README.md` (Badge) gepflegt
|
||||||
|
- Änderungen dokumentieren wir in `CHANGELOG.md`
|
||||||
|
- Nightly-Entwicklung: Features zuerst im Branch `feature/nightly`, Merge nach `main` für Release
|
||||||
|
|
||||||
## Docker Images
|
## Docker Images
|
||||||
- `:latest` - Hauptversion (main branch)
|
- `:latest` - Hauptversion (main branch)
|
||||||
- `:feature-nightly` - Feature Version
|
- `:feature-nightly` - Feature Version
|
||||||
|
|
|
||||||
90
README.md
90
README.md
|
|
@ -1,93 +1,59 @@
|
||||||
# 🎵 Jukebox 420 - Discord Soundboard v1.0.0
|
# Jukebox 420 – Discord Soundboard (v1.1.1)
|
||||||
|
|
||||||
Ein modernes, feature-reiches Discord Soundboard mit Web-Frontend, Discord-Bot und Docker-Deployment. Perfekt für Gaming-Communities, Streamer und Discord-Server.
|
A modern, self‑hosted Discord soundboard with a slick web UI and a Discord bot that plays sounds into your voice channels. Easy to run via Docker, fun to use with friends.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
### 🎮 **Kern-Funktionen**
|
- Web UI (Vite + React + TypeScript), 3 themes (Dark, Rainbow, 420)
|
||||||
- **Web-Frontend** mit modernem UI und 3 Themes (Dark, Rainbow, 420)
|
- Discord bot (discord.js + @discordjs/voice)
|
||||||
- **Discord-Bot** für Voice-Channel Integration
|
- MP3 & WAV playback, ffmpeg normalization
|
||||||
- **Sound-Management** mit Ordner-Unterstützung
|
- Favorites, search, folders view (auto counters)
|
||||||
- **Live-Uhrzeit** (Berlin Timezone)
|
- Live counters and a clean header/footer
|
||||||
- **Volume Control** pro Server
|
- Admin area: bulk delete, inline rename, categories (CRUD) + bulk assign, remove custom badges
|
||||||
- **Favoriten-System** mit Cookie-Persistenz
|
- Partymode: server‑side random playback every 30–90 seconds, globally synced via SSE; Panic stops for everyone
|
||||||
|
- Persistent state: volumes, plays, totalPlays, categories, badges in `/data/sounds/state.json`
|
||||||
|
|
||||||
### 🎨 **UI/UX Features**
|
## 🚀 Quick start
|
||||||
- **3 Themes**: Dark, Rainbow, 420 (Cannabis/Trippy)
|
|
||||||
- **Responsive Design** für Desktop & Mobile
|
|
||||||
- **Glassmorphism-Effekte** mit Backdrop-Blur
|
|
||||||
- **Animierte Hintergründe** (Rainbow & 420 Theme)
|
|
||||||
- **Live-Zähler** für Sounds und Abspielungen
|
|
||||||
|
|
||||||
### 🔧 **Admin-Funktionen**
|
### 1. Requirements
|
||||||
- **Admin-Login** (Passwort-basiert)
|
- Docker & Docker Compose
|
||||||
- **Bulk-Delete** für mehrere Sounds
|
- Discord bot token with intents: `Guilds`, `GuildVoiceStates`, `DirectMessages`
|
||||||
- **Sound-Umbenennen** mit Inline-Editor
|
|
||||||
- **Checkbox-Auswahl** für Massenoperationen
|
|
||||||
|
|
||||||
### 🎵 **Audio-Features**
|
|
||||||
- **MP3 & WAV Support** für Uploads und Playback
|
|
||||||
- **Audio-Normalisierung** (Loudnorm)
|
|
||||||
- **URL-Download** für MP3/WAV Links
|
|
||||||
- **Random-Play** für zufällige Sounds
|
|
||||||
- **Panic-Button** zum sofortigen Stoppen
|
|
||||||
|
|
||||||
### 📁 **Organisation**
|
|
||||||
- **Ordner-Unterstützung** mit Tab-Navigation
|
|
||||||
- **Favoriten-Tab** für gespeicherte Sounds
|
|
||||||
- **Neu-Tab** für die letzten 10 Uploads
|
|
||||||
- **Most Played** für Top 3 Sounds
|
|
||||||
- **Suchfunktion** für alle Sounds
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
### 1. Voraussetzungen
|
|
||||||
- **Docker & Docker Compose**
|
|
||||||
- **Discord Bot Token** mit folgenden Intents:
|
|
||||||
- `Guilds`
|
|
||||||
- `GuildVoiceStates`
|
|
||||||
- `DirectMessages`
|
|
||||||
- `MessageContent`
|
|
||||||
|
|
||||||
### 2. Setup
|
### 2. Setup
|
||||||
```bash
|
```bash
|
||||||
# Repository klonen
|
# Clone repository
|
||||||
git clone https://github.com/flex420/jukebox-vibe.git
|
git clone https://github.com/flex420/jukebox-vibe.git
|
||||||
cd jukebox-vibe
|
cd jukebox-vibe
|
||||||
|
|
||||||
# .env Datei erstellen
|
# Create .env
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Konfiguration
|
### 3. Configuration
|
||||||
```env
|
```env
|
||||||
# .env Datei bearbeiten
|
# Edit the .env file
|
||||||
DISCORD_TOKEN=dein_discord_bot_token_hier
|
DISCORD_TOKEN=your_discord_bot_token_here
|
||||||
|
ADMIN_PWD=choose-a-strong-password
|
||||||
PORT=8080
|
PORT=8080
|
||||||
SOUNDS_DIR=/data/sounds
|
SOUNDS_DIR=/data/sounds
|
||||||
|
|
||||||
# Optional: Bestimmte Server erlauben
|
# Optionally restrict allowed guilds
|
||||||
ALLOWED_GUILD_IDS=GUILD_ID_1,GUILD_ID_2
|
ALLOWED_GUILD_IDS=GUILD_ID_1,GUILD_ID_2
|
||||||
|
|
||||||
# Optional: Audio-Normalisierung
|
|
||||||
NORMALIZE_AUDIO=true
|
|
||||||
NORMALIZE_TARGET=-14
|
|
||||||
NORMALIZE_THRESHOLD=-70
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Deployment
|
### 4. Deployment
|
||||||
```bash
|
```bash
|
||||||
# Container starten
|
# Start container
|
||||||
docker compose up --build -d
|
docker compose up --build -d
|
||||||
|
|
||||||
# Logs anzeigen
|
# Logs
|
||||||
docker compose logs -f
|
docker compose logs -f
|
||||||
|
|
||||||
# Status prüfen
|
# Status
|
||||||
docker compose ps
|
docker compose ps
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -204,6 +170,12 @@ docker pull flex420/jukebox-vibe:latest
|
||||||
docker run -d --name jukebox-420 -p 8199:8080 --env-file .env -v $(pwd)/data/sounds:/data/sounds flex420/jukebox-vibe:latest
|
docker run -d --name jukebox-420 -p 8199:8080 --env-file .env -v $(pwd)/data/sounds:/data/sounds flex420/jukebox-vibe:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🔒 SSL/HTTPS Hinweis (wichtig für Discord)
|
||||||
|
|
||||||
|
- Das Web-Frontend MUSS hinter HTTPS (SSL) ausgeliefert werden. Empfohlen ist ein Domain‑Mapping (Reverse Proxy) mit gültigem Zertifikat (z. B. Traefik, Nginx, Caddy, Cloudflare).
|
||||||
|
- Hintergrund: Ohne TLS kann es zu Verschlüsselungs-/Encrypt‑Fehlern kommen, und Audio wird in Discord nicht korrekt wiedergegeben.
|
||||||
|
- Praxis: Richte eine Domain wie `https://soundboard.deinedomain.tld` auf das Frontend ein und aktiviere SSL (Let’s Encrypt). Danach sollten Uploads/Playback stabil funktionieren.
|
||||||
|
|
||||||
## 📁 Projekt-Struktur
|
## 📁 Projekt-Struktur
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ services:
|
||||||
- GUILD_ID=${GUILD_ID}
|
- GUILD_ID=${GUILD_ID}
|
||||||
- ADMIN_PWD=${ADMIN_PWD}
|
- ADMIN_PWD=${ADMIN_PWD}
|
||||||
- VITE_BUILD_CHANNEL=nightly
|
- VITE_BUILD_CHANNEL=nightly
|
||||||
- VITE_APP_VERSION=1.0.0-nightly
|
- VITE_APP_VERSION=1.1.1-nightly
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/sounds:/data/sounds
|
- ./data/sounds:/data/sounds
|
||||||
- ./data/uploads:/data/uploads
|
- ./data/uploads:/data/uploads
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ services:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- VITE_BUILD_CHANNEL=stable
|
- VITE_BUILD_CHANNEL=stable
|
||||||
- VITE_APP_VERSION=1.0.0
|
- VITE_APP_VERSION=1.1.1
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/sounds:/data/sounds
|
- ./data/sounds:/data/sounds
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "jukebox-vibe",
|
"name": "jukebox-vibe",
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"description": "Discord Soundboard mit Web-Interface",
|
"description": "Discord Soundboard mit Web-Interface",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "discord-soundboard-server",
|
"name": "discord-soundboard-server",
|
||||||
"version": "0.1.0",
|
"version": "1.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
|
||||||
|
|
@ -941,6 +941,8 @@ app.post('/api/volume', (req: Request, res: Response) => {
|
||||||
// Kein aktiver Player: nur persistieren für nächste Wiedergabe
|
// Kein aktiver Player: nur persistieren für nächste Wiedergabe
|
||||||
persistedState.volumes[guildId] = safeVolume;
|
persistedState.volumes[guildId] = safeVolume;
|
||||||
writePersistedState(persistedState);
|
writePersistedState(persistedState);
|
||||||
|
// Broadcast neue Lautstärke an alle Clients
|
||||||
|
sseBroadcast({ type: 'volume', guildId, volume: safeVolume });
|
||||||
return res.json({ ok: true, volume: safeVolume, persistedOnly: true });
|
return res.json({ ok: true, volume: safeVolume, persistedOnly: true });
|
||||||
}
|
}
|
||||||
state.currentVolume = safeVolume;
|
state.currentVolume = safeVolume;
|
||||||
|
|
@ -950,6 +952,8 @@ app.post('/api/volume', (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
persistedState.volumes[guildId] = safeVolume;
|
persistedState.volumes[guildId] = safeVolume;
|
||||||
writePersistedState(persistedState);
|
writePersistedState(persistedState);
|
||||||
|
// Broadcast neue Lautstärke an alle Clients
|
||||||
|
sseBroadcast({ type: 'volume', guildId, volume: safeVolume });
|
||||||
return res.json({ ok: true, volume: safeVolume });
|
return res.json({ ok: true, volume: safeVolume });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Volume-Fehler:', e);
|
console.error('Volume-Fehler:', e);
|
||||||
|
|
@ -1075,7 +1079,7 @@ app.get('/api/events', (req: Request, res: Response) => {
|
||||||
|
|
||||||
// Snapshot senden
|
// Snapshot senden
|
||||||
try {
|
try {
|
||||||
res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive), selected: persistedState.selectedChannels ?? {} })}\n\n`);
|
res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive), selected: persistedState.selectedChannels ?? {}, volumes: persistedState.volumes ?? {} })}\n\n`);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Ping, damit Proxies die Verbindung offen halten
|
// Ping, damit Proxies die Verbindung offen halten
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "discord-soundboard-web",
|
"name": "discord-soundboard-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "1.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,15 @@ export default function App() {
|
||||||
setSelected(newVal);
|
setSelected(newVal);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
try {
|
||||||
|
const vols = msg?.volumes || {};
|
||||||
|
const cur = selectedRef.current || '';
|
||||||
|
const gid = cur ? cur.split(':')[0] : '';
|
||||||
|
if (gid && typeof vols[gid] === 'number') {
|
||||||
|
const v = vols[gid];
|
||||||
|
setVolume(v);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
} else if (msg?.type === 'channel') {
|
} else if (msg?.type === 'channel') {
|
||||||
try {
|
try {
|
||||||
const gid = msg.guildId;
|
const gid = msg.guildId;
|
||||||
|
|
@ -116,6 +125,16 @@ export default function App() {
|
||||||
if (curGid === gid) setSelected(`${gid}:${cid}`);
|
if (curGid === gid) setSelected(`${gid}:${cid}`);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
} else if (msg?.type === 'volume') {
|
||||||
|
try {
|
||||||
|
const gid = msg.guildId;
|
||||||
|
const v = msg.volume;
|
||||||
|
const cur = selectedRef.current || '';
|
||||||
|
const curGid = cur ? cur.split(':')[0] : '';
|
||||||
|
if (gid && curGid === gid && typeof v === 'number') {
|
||||||
|
setVolume(v);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => { try { unsub(); } catch {} };
|
return () => { try { unsub(); } catch {} };
|
||||||
|
|
@ -653,15 +672,7 @@ export default function App() {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</main>
|
</main>
|
||||||
{/* Footer: Version/Channel */}
|
{/* Footer intentionally left without version display */}
|
||||||
<footer className="footer-info">
|
|
||||||
<span>
|
|
||||||
v{import.meta.env.VITE_APP_VERSION || ''}
|
|
||||||
{import.meta.env.VITE_BUILD_CHANNEL === 'nightly' && (
|
|
||||||
<span className="ml-2">• Nightly</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
{showTop && (
|
{showTop && (
|
||||||
<button type="button" className="back-to-top" aria-label="Nach oben" onClick={()=>window.scrollTo({top:0, behavior:'smooth'})}>↑ Top</button>
|
<button type="button" className="back-to-top" aria-label="Nach oben" onClick={()=>window.scrollTo({top:0, behavior:'smooth'})}>↑ Top</button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
|
||||||
|
|
||||||
:root { color-scheme: dark; }
|
:root { color-scheme: dark; --range-track-h: 8px; --range-thumb-d: 20px; }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
[data-theme="dark"] body,
|
[data-theme="dark"] body,
|
||||||
body {
|
body {
|
||||||
|
|
@ -476,12 +476,13 @@ header p {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 8px;
|
height: var(--range-track-h, 8px);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
/* Sichtbarer Füllbalken über ein Hintergrund-Gradient, Breite via --_fill gesteuert */
|
/* Sichtbarer Füllbalken über ein Hintergrund-Gradient, Breite via --_fill gesteuert */
|
||||||
background: linear-gradient(var(--range-accent), var(--range-accent)) 0/var(--_fill, 0%) 100% no-repeat, var(--range-track-bg, #2c2c2c);
|
background: linear-gradient(var(--range-accent), var(--range-accent)) 0/var(--_fill, 0%) 100% no-repeat, var(--range-track-bg, #2c2c2c);
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tracks transparent halten, damit der Hintergrund-Gradient sichtbar ist */
|
/* Tracks transparent halten, damit der Hintergrund-Gradient sichtbar ist */
|
||||||
|
|
@ -496,8 +497,9 @@ header p {
|
||||||
.volume-slider::-webkit-slider-thumb {
|
.volume-slider::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 20px;
|
width: var(--range-thumb-d, 20px);
|
||||||
height: 20px;
|
height: var(--range-thumb-d, 20px);
|
||||||
|
margin-top: calc((var(--range-track-h, 8px) - var(--range-thumb-d, 20px)) / 2);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -521,7 +523,7 @@ header p {
|
||||||
--range-track-bg: #2c2c2c;
|
--range-track-bg: #2c2c2c;
|
||||||
accent-color: #0a84ff;
|
accent-color: #0a84ff;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
height: 8px;
|
height: var(--range-track-h, 8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .control.volume input[type="range"]::-webkit-slider-thumb,
|
[data-theme="dark"] .control.volume input[type="range"]::-webkit-slider-thumb,
|
||||||
|
|
@ -543,7 +545,7 @@ header p {
|
||||||
--range-track-bg: rgba(44,44,44,.8);
|
--range-track-bg: rgba(44,44,44,.8);
|
||||||
accent-color: #23a6d5;
|
accent-color: #23a6d5;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
height: 8px;
|
height: var(--range-track-h, 8px);
|
||||||
border: 1px solid #3a3a3c;
|
border: 1px solid #3a3a3c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue