feat(media): URL-Player (YouTube/Instagram via ytdl/yt-dlp, MP3-Download und sofortiges Abspielen) + Frontend-URL-Feld

This commit is contained in:
vibe-bot 2025-08-08 15:22:15 +02:00
parent 018c36487d
commit 6d4dba3ad3
5 changed files with 81 additions and 3 deletions

View file

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename } from './api';
import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl } from './api';
import type { VoiceChannelInfo, Sound } from './types';
import { getCookie, setCookie } from './cookies';
@ -21,6 +21,7 @@ export default function App() {
const [selectedSet, setSelectedSet] = useState<Record<string, boolean>>({});
const selectedCount = useMemo(() => Object.values(selectedSet).filter(Boolean).length, [selectedSet]);
const [clock, setClock] = useState<string>(() => new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }).format(new Date()));
const [mediaUrl, setMediaUrl] = useState<string>('');
useEffect(() => {
(async () => {
@ -172,6 +173,15 @@ export default function App() {
<option value="rainbow">Rainbow Chaos</option>
</select>
</div>
<div className="control" style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 8 }}>
<input value={mediaUrl} onChange={(e) => setMediaUrl(e.target.value)} placeholder="YouTube/Instagram/MP3 URL..." />
<button type="button" className="tab" onClick={async () => {
if (!selected) { setError('Bitte Voice-Channel wählen'); return; }
const [guildId, channelId] = selected.split(':');
try { await playUrl(mediaUrl, guildId, channelId, volume); }
catch (e: any) { setError(e?.message || 'Play-URL fehlgeschlagen'); }
}}> Abspielen</button>
</div>
{!isAdmin && (
<div className="control" style={{ display: 'flex', gap: 8 }}>
<input type="password" value={adminPwd} onChange={(e) => setAdminPwd(e.target.value)} placeholder="Admin Passwort" />

View file

@ -88,6 +88,17 @@ export async function adminRename(from: string, to: string): Promise<string> {
return data?.to as string;
}
export async function playUrl(url: string, guildId: string, channelId: string, volume: number): Promise<void> {
const res = await fetch(`${API_BASE}/play-url`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, guildId, channelId, volume })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data?.error || 'Play-URL fehlgeschlagen');
}
}

View file

@ -88,7 +88,7 @@ header p { opacity: .8; }
}
.badge { align-self: flex-start; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.18); padding: 6px 10px; border-radius: 999px; font-size: 13px; }
.controls { display: grid; grid-template-columns: 1fr minmax(240px, 300px) 220px 180px; gap: 12px; align-items: center; margin-bottom: 18px; }
.controls { display: grid; grid-template-columns: 1fr minmax(240px, 300px) 220px 180px minmax(260px, 1fr); gap: 12px; align-items: center; margin-bottom: 18px; }
.controls.glass {
backdrop-filter: saturate(140%) blur(20px);
background: linear-gradient(135deg, rgba(255,255,255,.14), rgba(255,255,255,.06));