From f6b296d053034837826f55ae006ef156771b4784 Mon Sep 17 00:00:00 2001 From: Nordi98 Date: Sun, 3 Aug 2025 17:36:16 +0200 Subject: [PATCH] ed --- resources/[tools]/nordi_dj/client/main.lua | 1068 ++++++++++---------- resources/[tools]/nordi_dj/config.lua | 72 +- resources/[tools]/nordi_dj/html/script.js | 8 +- resources/[tools]/nordi_dj/server/main.lua | 843 ++++++++------- 4 files changed, 1055 insertions(+), 936 deletions(-) diff --git a/resources/[tools]/nordi_dj/client/main.lua b/resources/[tools]/nordi_dj/client/main.lua index b40af42e4..c2e9cfa2e 100644 --- a/resources/[tools]/nordi_dj/client/main.lua +++ b/resources/[tools]/nordi_dj/client/main.lua @@ -1,16 +1,20 @@ local QBCore = exports['qb-core']:GetCoreObject() local PlayerData = {} -local currentDJBooth = nil +local currentBooth = nil local isPlaying = false local currentVolume = Config.DefaultVolume local currentSong = nil -local playlists = {} -local currentPlaylist = nil -local currentSongIndex = 1 +local activeBooths = {} +local isUIOpen = false +local isDJBooth = false +local nearestBooth = nil -- Events RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() PlayerData = QBCore.Functions.GetPlayerData() + + -- Frage Server nach aktiven DJ-Booths + TriggerServerEvent('dj:server:requestActiveDJs') end) RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo) @@ -30,17 +34,211 @@ RegisterCommand('opendj', function() return end - local nearbyBooth = GetNearbyDJBooth() - if nearbyBooth then - currentDJBooth = nearbyBooth - OpenDJMenu() - else + if not isDJBooth then lib.notify({ title = 'DJ System', - description = 'Du bist nicht in der Nähe einer DJ Booth!', + description = 'Du musst an einem DJ Pult stehen!', type = 'error' }) + return end + + OpenDJInterface() +end) + +-- Musik abspielen (von Server empfangen) +RegisterNetEvent('dj:client:playMusic', function(data) + -- Speichere Booth-Informationen + activeBooths[data.booth.name] = { + url = data.url, + title = data.title, + volume = data.volume, + coords = data.booth.coords, + range = data.range + } + + -- Prüfe ob Spieler in Reichweite ist + local playerCoords = GetEntityCoords(PlayerPedId()) + local distance = #(playerCoords - data.booth.coords) + + if distance <= data.range then + -- Berechne Lautstärke basierend auf Distanz + local volumeMultiplier = 1.0 - (distance / data.range) + local adjustedVolume = math.floor(data.volume * volumeMultiplier) + + -- Spiele Musik ab + SendNUIMessage({ + type = 'playMusic', + url = data.url, + volume = adjustedVolume, + title = data.title + }) + + -- Setze lokale Variablen + currentBooth = { + name = data.booth.name, + coords = data.booth.coords + } + isPlaying = true + currentSong = { + title = data.title, + url = data.url + } + + -- Zeige Benachrichtigung + lib.notify({ + title = 'DJ System', + description = 'Spielt: ' .. data.title, + type = 'info' + }) + end +end) + +-- Musik stoppen (von Server empfangen) +RegisterNetEvent('dj:client:stopMusic', function(boothName) + -- Entferne Booth-Informationen + if activeBooths[boothName] then + activeBooths[boothName] = nil + end + + -- Wenn der Spieler diese Musik hört, stoppe sie + if currentBooth and currentBooth.name == boothName then + SendNUIMessage({ + type = 'stopMusic' + }) + + currentBooth = nil + isPlaying = false + currentSong = nil + + lib.notify({ + title = 'DJ System', + description = 'Musik gestoppt', + type = 'info' + }) + end +end) + +-- Lautstärke ändern (von Server empfangen) +RegisterNetEvent('dj:client:setVolume', function(data) + -- Aktualisiere Booth-Informationen + if activeBooths[data.booth.name] then + activeBooths[data.booth.name].volume = data.volume + activeBooths[data.booth.name].range = data.range + end + + -- Wenn der Spieler diese Musik hört, ändere die Lautstärke + if currentBooth and currentBooth.name == data.booth.name then + -- Berechne Lautstärke basierend auf Distanz + local playerCoords = GetEntityCoords(PlayerPedId()) + local distance = #(playerCoords - data.booth.coords) + local volumeMultiplier = 1.0 - (distance / data.range) + local adjustedVolume = math.floor(data.volume * volumeMultiplier) + + SendNUIMessage({ + type = 'setVolume', + volume = adjustedVolume + }) + + currentVolume = data.volume + end +end) + +-- Empfange aktive DJs +RegisterNetEvent('dj:client:receiveActiveDJs', function(booths) + activeBooths = booths + + -- Prüfe ob Spieler in Reichweite eines aktiven DJ-Booths ist + local playerCoords = GetEntityCoords(PlayerPedId()) + + for boothName, boothData in pairs(activeBooths) do + local distance = #(playerCoords - boothData.coords) + + if distance <= boothData.range then + -- Berechne Lautstärke basierend auf Distanz + local volumeMultiplier = 1.0 - (distance / boothData.range) + local adjustedVolume = math.floor(boothData.volume * volumeMultiplier) + + -- Spiele Musik ab + SendNUIMessage({ + type = 'playMusic', + url = boothData.url, + volume = adjustedVolume, + title = boothData.title + }) + + currentBooth = { + name = boothName, + coords = boothData.coords + } + isPlaying = true + currentSong = { + title = boothData.title, + url = boothData.url + } + + lib.notify({ + title = 'DJ System', + description = 'Spielt: ' .. boothData.title, + type = 'info' + }) + + break + end + end +end) + +-- NUI Callbacks +RegisterNUICallback('djInterfaceClosed', function(data, cb) + SetNuiFocus(false, false) + isUIOpen = false + cb('ok') +end) + +RegisterNUICallback('deckStateChanged', function(data, cb) + if Config.Debug then + print(string.format('[DJ System] Deck %s %s: %s', + data.deck, + data.isPlaying and 'playing' or 'stopped', + data.track and data.track.title or 'No track' + )) + end + + -- Wenn Deck A oder B abspielt, sende an Server + if data.isPlaying and data.track then + PlayMusicAsDJ(data.track.title, data.track.url, currentVolume) + end + + cb('ok') +end) + +RegisterNUICallback('volumeChanged', function(data, cb) + if data.volume then + SetVolumeAsDJ(data.volume) + end + cb('ok') +end) + +RegisterNUICallback('stopMusic', function(data, cb) + StopMusicAsDJ() + cb('ok') +end) + +RegisterNUICallback('audioError', function(data, cb) + lib.notify({ + title = 'DJ System', + description = 'Audio Fehler: ' .. (data.error or 'Unbekannter Fehler'), + type = 'error' + }) + cb('ok') +end) + +RegisterNUICallback('songEnded', function(data, cb) + -- Wenn ein Song endet, kann hier Playlist-Logik implementiert werden + if Config.Debug then + print('[DJ System] Song ended') + end + cb('ok') end) -- Functions @@ -62,20 +260,6 @@ function CanUseDJScript() return false end -function GetNearbyDJBooth() - local playerCoords = GetEntityCoords(PlayerPedId()) - - for i, booth in pairs(Config.DJBooths) do - local distance = #(playerCoords - booth.coords) - if distance <= 3.0 then - return booth - end - end - - return nil -end - --- Aktualisierte Client-Funktionen für das neue UI function OpenDJInterface() if not isDJBooth then lib.notify({ @@ -104,546 +288,334 @@ function OpenDJInterface() end) end --- NUI Callbacks für das neue Interface -RegisterNUICallback('djInterfaceClosed', function(data, cb) - SetNuiFocus(false, false) - isUIOpen = false - cb('ok') -end) - -RegisterNUICallback('deckStateChanged', function(data, cb) - print(string.format('[DJ System] Deck %s %s: %s', - data.deck, - data.isPlaying and 'playing' or 'stopped', - data.track and data.track.title or 'No track' - )) - - -- Hier könntest du zusätzliche Logik hinzufügen - -- z.B. Synchronisation mit anderen Spielern - - cb('ok') -end) - - -function OpenYouTubeMenu() - local input = lib.inputDialog('YouTube Song abspielen', { - {type = 'input', label = 'Song Titel', placeholder = 'z.B. Daft Punk - One More Time'}, - {type = 'input', label = 'YouTube URL', placeholder = 'https://www.youtube.com/watch?v=...'} - }) - - if input and input[1] and input[2] then - if not IsValidYouTubeUrl(input[2]) then - lib.notify({ - title = 'DJ System', - description = 'Ungültige YouTube URL!', - type = 'error' - }) - return - end - +function PlayMusicAsDJ(title, url, volume) + if not nearestBooth then lib.notify({ title = 'DJ System', - description = 'YouTube URL wird konvertiert, bitte warten...', - type = 'info' - }) - - PlayMusic(input[1], input[2]) - end -end - -function OpenDirectUrlMenu() - local input = lib.inputDialog('Direkte URL abspielen', { - {type = 'input', label = 'Song Titel', placeholder = 'Titel des Songs'}, - {type = 'input', label = 'Direkte MP3/Audio URL', placeholder = 'https://example.com/song.mp3'} - }) - - if input and input[1] and input[2] then - PlayMusic(input[1], input[2]) - end -end - -function IsValidYouTubeUrl(url) - local patterns = { - "youtube%.com/watch%?v=", - "youtu%.be/", - "youtube%.com/embed/" - } - - for _, pattern in ipairs(patterns) do - if string.match(url, pattern) then - return true - end - end - - return false -end - -function OpenVolumeMenu() - local input = lib.inputDialog('Lautstärke einstellen', { - { - type = 'slider', - label = 'Lautstärke (%)', - description = 'Höhere Lautstärke = größere Reichweite', - default = currentVolume, - min = 0, - max = Config.MaxVolume, - step = 5 - } - }) - - if input and input[1] then - SetVolume(input[1]) - end -end - -function OpenPlaylistMenu() - local options = { - { - title = 'Neue Playlist erstellen', - description = 'Erstelle eine neue Playlist', - icon = 'plus', - onSelect = function() - CreateNewPlaylist() - end - } - } - - for _, playlist in pairs(playlists) do - table.insert(options, { - title = playlist.name, - description = #playlist.songs .. ' Songs', - icon = 'music', - onSelect = function() - OpenPlaylistOptions(playlist) - end - }) - end - - lib.registerContext({ - id = 'playlist_menu', - title = 'Playlist Verwaltung', - menu = 'dj_main_menu', - options = options - }) - - lib.showContext('playlist_menu') -end - -function CreateNewPlaylist() - local input = lib.inputDialog('Neue Playlist', { - {type = 'input', label = 'Playlist Name', placeholder = 'Meine YouTube Playlist'} - }) - - if input and input[1] then - TriggerServerEvent('dj:server:createPlaylist', input[1]) - end -end - -function OpenPlaylistOptions(playlist) - local options = { - { - title = 'Playlist abspielen', - description = 'Spiele alle Songs der Playlist ab', - icon = 'play', - onSelect = function() - PlayPlaylist(playlist) - end - }, - { - title = 'YouTube Song hinzufügen', - description = 'Füge einen YouTube Song zur Playlist hinzu', - icon = 'fab fa-youtube', - onSelect = function() - AddYouTubeSongToPlaylist(playlist) - end - }, - { - title = 'Direkten Song hinzufügen', - description = 'Füge einen Song per direkter URL hinzu', - icon = 'plus', - onSelect = function() - AddDirectSongToPlaylist(playlist) - end - }, - { - title = 'Songs anzeigen', - description = 'Zeige alle Songs in der Playlist', - icon = 'list', - onSelect = function() - ShowPlaylistSongs(playlist) - end - }, - { - title = 'Playlist löschen', - description = 'Lösche diese Playlist', - icon = 'trash', - onSelect = function() - DeletePlaylist(playlist) - end - } - } - - lib.registerContext({ - id = 'playlist_options', - title = playlist.name, - menu = 'playlist_menu', - options = options - }) - - lib.showContext('playlist_options') -end - -function AddYouTubeSongToPlaylist(playlist) - local input = lib.inputDialog('YouTube Song hinzufügen', { - {type = 'input', label = 'Song Titel', placeholder = 'z.B. Avicii - Levels'}, - {type = 'input', label = 'YouTube URL', placeholder = 'https://www.youtube.com/watch?v=...'} - }) - - if input and input[1] and input[2] then - if not IsValidYouTubeUrl(input[2]) then - lib.notify({ - title = 'DJ System', - description = 'Ungültige YouTube URL!', - type = 'error' - }) - return - end - - TriggerServerEvent('dj:server:addSongToPlaylist', playlist.id, { - title = input[1], - url = input[2] - }) - end -end - -function AddDirectSongToPlaylist(playlist) - local input = lib.inputDialog('Direkten Song hinzufügen', { - {type = 'input', label = 'Song Titel', placeholder = 'Titel des Songs'}, - {type = 'input', label = 'Direkte URL', placeholder = 'https://example.com/song.mp3'} - }) - - if input and input[1] and input[2] then - TriggerServerEvent('dj:server:addSongToPlaylist', playlist.id, { - title = input[1], - url = input[2] - }) - end -end - -function PlayMusic(title, url) - if not currentDJBooth then return end - - currentSong = {title = title, url = url} - isPlaying = true - - TriggerServerEvent('dj:server:playMusic', { - title = title, - url = url, - volume = currentVolume, - booth = currentDJBooth, - range = CalculateRange() - }) - - lib.notify({ - title = 'DJ System', - description = 'Lade: ' .. title, - type = 'info' - }) -end - -function StopMusic() - if not isPlaying then - lib.notify({ - title = 'DJ System', - description = 'Es läuft gerade keine Musik!', + description = 'Du musst an einem DJ-Pult stehen!', type = 'error' }) return end - isPlaying = false - currentSong = nil - currentPlaylist = nil - - TriggerServerEvent('dj:server:stopMusic', currentDJBooth) - - lib.notify({ - title = 'DJ System', - description = 'Musik gestoppt', - type = 'success' - }) -end - -function SetVolume(volume) - currentVolume = volume - - if isPlaying then - TriggerServerEvent('dj:server:setVolume', { - volume = volume, - booth = currentDJBooth, - range = CalculateRange() - }) - end - - lib.notify({ - title = 'DJ System', - description = 'Lautstärke auf ' .. volume .. '% gesetzt', - type = 'success' - }) -end - -function CalculateRange() - local baseRange = currentDJBooth.range - local maxRange = currentDJBooth.maxRange - local volumePercent = currentVolume / 100 - - return baseRange + ((maxRange - baseRange) * volumePercent) -end - -function PlayPlaylist(playlist) - if #playlist.songs == 0 then - lib.notify({ - title = 'DJ System', - description = 'Playlist ist leer!', - type = 'error' - }) - return - end - - currentPlaylist = playlist - currentSongIndex = 1 - - local firstSong = playlist.songs[1] - PlayMusic(firstSong.title, firstSong.url) - - lib.notify({ - title = 'DJ System', - description = 'Playlist "' .. playlist.name .. '" wird abgespielt', - type = 'success' - }) -end - -function ShowPlaylistSongs(playlist) - local options = {} - - for i, song in pairs(playlist.songs) do - local songType = IsValidYouTubeUrl(song.url) and "YouTube" or "Direkt" - table.insert(options, { - title = song.title, - description = 'Typ: ' .. songType .. ' | Klicken zum Abspielen', - icon = IsValidYouTubeUrl(song.url) and 'fab fa-youtube' or 'music', - onSelect = function() - PlayMusic(song.title, song.url) - end - }) - end - - if #options == 0 then - table.insert(options, { - title = 'Keine Songs', - description = 'Diese Playlist ist leer', - icon = 'exclamation' - }) - end - - lib.registerContext({ - id = 'playlist_songs', - title = playlist.name .. ' - Songs', - menu = 'playlist_options', - options = options - }) - - lib.showContext('playlist_songs') -end - -function DeletePlaylist(playlist) - local confirm = lib.alertDialog({ - header = 'Playlist löschen', - content = 'Möchtest du die Playlist "' .. playlist.name .. '" wirklich löschen?', - centered = true, - cancel = true - }) - - if confirm == 'confirm' then - TriggerServerEvent('dj:server:deletePlaylist', playlist.id) - end -end - --- Server Events -RegisterNetEvent('dj:client:playMusic', function(data) - SendNUIMessage({ - type = 'playMusic', - url = data.url, - volume = data.volume, - title = data.title - }) - - lib.notify({ - title = 'DJ System', - description = 'Spielt ab: ' .. data.title, - type = 'success' - }) -end) - -RegisterNetEvent('dj:client:stopMusic', function() - SendNUIMessage({ - type = 'stopMusic' - }) -end) - -RegisterNetEvent('dj:client:setVolume', function(volume) - SendNUIMessage({ - type = 'setVolume', - volume = volume - }) -end) - -RegisterNetEvent('dj:client:updatePlaylists', function(data) - playlists = data -end) - -RegisterNetEvent('dj:client:notify', function(message, type) - lib.notify({ - title = 'DJ System', - description = message, - type = type or 'info' - }) -end) - --- Music Range Check -CreateThread(function() - while true do - Wait(1000) - - if isPlaying and currentDJBooth then - local playerCoords = GetEntityCoords(PlayerPedId()) - local distance = #(playerCoords - currentDJBooth.coords) - local range = CalculateRange() - - if distance > range then - SendNUIMessage({ - type = 'fadeOut' - }) - else - SendNUIMessage({ - type = 'fadeIn' - }) - end - end - end -end) - --- Song End Handler -RegisterNUICallback('songEnded', function(data, cb) - if currentPlaylist and currentSongIndex < #currentPlaylist.songs then - -- Nächster Song in Playlist - currentSongIndex = currentSongIndex + 1 - local nextSong = currentPlaylist.songs[currentSongIndex] - PlayMusic(nextSong.title, nextSong.url) - else - -- Playlist beendet - isPlaying = false - currentSong = nil - currentPlaylist = nil - currentSongIndex = 1 - end - cb('ok') -end) - --- Audio Error Handler -RegisterNUICallback('audioError', function(data, cb) - lib.notify({ - title = 'DJ System', - description = 'Audio Fehler: ' .. (data.error or 'Unbekannter Fehler'), - type = 'error' - }) - - -- Reset playing state - isPlaying = false - currentSong = nil - - cb('ok') -end) - --- Song Progress (optional) -RegisterNUICallback('songProgress', function(data, cb) - -- Du kannst hier Song-Progress verarbeiten - -- z.B. für eine Progress-Bar im Menü - cb('ok') -end) - --- Füge diese Funktionen zu deiner client/main.lua hinzu: - -local function cleanYouTubeUrl(url) - -- Entferne Playlist-Parameter und andere Parameter - local patterns = { - "(https://www%.youtube%.com/watch%?v=[^&]+)", - "(https://youtu%.be/[^?]+)" - } - - for _, pattern in ipairs(patterns) do - local cleanUrl = string.match(url, pattern) - if cleanUrl then - return cleanUrl - end - end - - return url -end - -local function extractVideoId(url) - local patterns = { - "youtube%.com/watch%?v=([^&]+)", - "youtu%.be/([^?]+)", - "youtube%.com/embed/([^?]+)" - } - - for _, pattern in ipairs(patterns) do - local videoId = string.match(url, pattern) - if videoId then - return videoId - end - end - - return nil -end - --- Aktualisierte PlayMusic Funktion -function PlayMusic(title, url, volume) - if not title or not url then - lib.notify({ - title = 'DJ System', - description = 'Titel und URL sind erforderlich', - type = 'error' - }) - return - end - - -- Bereinige URL von Playlist-Parametern + -- Bereinige YouTube URL von Playlist-Parametern local cleanUrl = url if string.find(url, "youtube") then cleanUrl = string.gsub(url, "&list=.-$", "") cleanUrl = string.gsub(cleanUrl, "&start_radio=.-$", "") cleanUrl = string.gsub(cleanUrl, "&index=.-$", "") - - lib.notify({ - title = 'DJ System', - description = 'YouTube Video wird gestreamt: ' .. title, - type = 'info' - }) end - print('[DJ System] Streaming: ' .. title .. ' | Clean URL: ' .. cleanUrl) - - -- Sende an Server - TriggerServerEvent('dj:playMusic', title, cleanUrl, volume or 50) - - -- Update lokale Variablen - isPlaying = true - currentSong = { + -- Sende an Server zur Synchronisation + TriggerServerEvent('dj:server:playMusic', { title = title, url = cleanUrl, - volume = volume or 50 + volume = volume or Config.DefaultVolume, + booth = { + name = nearestBooth, + coords = Config.DJBooths[nearestBooth].coords + }, + range = CalculateRange(volume or Config.DefaultVolume) + }) + + -- Lokale Variablen aktualisieren + isPlaying = true + currentVolume = volume or Config.DefaultVolume + currentSong = { + title = title, + url = cleanUrl } + currentBooth = { + name = nearestBooth, + coords = Config.DJBooths[nearestBooth].coords + } +} + +function StopMusicAsDJ() + if not nearestBooth then + lib.notify({ + title = 'DJ System', + description = 'Du musst an einem DJ-Pult stehen!', + type = 'error' + }) + return + end + + -- Sende an Server zur Synchronisation + TriggerServerEvent('dj:server:stopMusic', nearestBooth) + + -- Lokale Variablen aktualisieren + isPlaying = false + currentSong = nil + currentBooth = nil +} + +function SetVolumeAsDJ(volume) + if not nearestBooth then + lib.notify({ + title = 'DJ System', + description = 'Du musst an einem DJ-Pult stehen!', + type = 'error' + }) + return + end + + -- Sende an Server zur Synchronisation + TriggerServerEvent('dj:server:setVolume', { + volume = volume, + booth = { + name = nearestBooth, + coords = Config.DJBooths[nearestBooth].coords + }, + range = CalculateRange(volume) + }) + + -- Lokale Variablen aktualisieren + currentVolume = volume +} + +function CalculateRange(volume) + if not nearestBooth or not Config.DJBooths[nearestBooth] then return 30.0 end + + local booth = Config.DJBooths[nearestBooth] + local baseRange = booth.range or 30.0 + local maxRange = booth.maxRange or 50.0 + local volumePercent = volume / 100 + + return baseRange + ((maxRange - baseRange) * volumePercent) end +-- Regelmäßige Prüfung der Distanz zu DJ-Booths und aktiven Musiken +CreateThread(function() + while true do + Wait(Config.DistanceVolume.updateInterval) + + local playerCoords = GetEntityCoords(PlayerPedId()) + local foundBooth = false + nearestBooth = nil + local nearestDistance = 999999.9 + + -- Prüfe Nähe zu DJ-Booths + for boothName, booth in pairs(Config.DJBooths) do + local distance = #(playerCoords - booth.coords) + + -- Finde das nächste DJ-Booth + if distance < nearestDistance then + nearestDistance = distance + nearestBooth = boothName + end + + -- Prüfe ob Spieler an einem DJ-Booth steht + if distance <= 2.0 then + foundBooth = true + break + end + end + + isDJBooth = foundBooth + + -- Prüfe Distanz zu aktiven Musiken + if not isPlaying and next(activeBooths) then + -- Spieler hört keine Musik, prüfe ob er in Reichweite eines aktiven DJ-Booths ist + for boothName, boothData in pairs(activeBooths) do + local distance = #(playerCoords - boothData.coords) + + if distance <= boothData.range then + -- Berechne Lautstärke basierend auf Distanz + local volumeMultiplier = 1.0 - (distance / boothData.range) + local adjustedVolume = math.floor(boothData.volume * volumeMultiplier) + + -- Spiele Musik ab + SendNUIMessage({ + type = 'playMusic', + url = boothData.url, + volume = adjustedVolume, + title = boothData.title + }) + + currentBooth = { + name = boothName, + coords = boothData.coords + } + isPlaying = true + currentSong = { + title = boothData.title, + url = boothData.url + } + + lib.notify({ + title = 'DJ System', + description = 'Spielt: ' .. boothData.title, + type = 'info' + }) + + break + end + end + elseif isPlaying and currentBooth then + -- Spieler hört Musik, prüfe ob er noch in Reichweite ist + local boothData = activeBooths[currentBooth.name] + + if boothData then + local distance = #(playerCoords - boothData.coords) + + if distance > boothData.range then + -- Spieler hat Reichweite verlassen, stoppe Musik + SendNUIMessage({ + type = 'stopMusic' + }) + + currentBooth = nil + isPlaying = false + currentSong = nil + + lib.notify({ + title = 'DJ System', + description = 'Musik außer Reichweite', + type = 'info' + }) + else + -- Spieler ist noch in Reichweite, passe Lautstärke an + local volumeMultiplier = 1.0 - (distance / boothData.range) + local adjustedVolume = math.floor(boothData.volume * volumeMultiplier) + + SendNUIMessage({ + type = 'setVolume', + volume = adjustedVolume + }) + end + else + -- Booth existiert nicht mehr, stoppe Musik + SendNUIMessage({ + type = 'stopMusic' + }) + + currentBooth = nil + isPlaying = false + currentSong = nil + end + end + end +end) + +-- DJ Booth Blips und Interaktionen +CreateThread(function() + -- Erstelle Blips für DJ-Booths + for boothName, booth in pairs(Config.DJBooths) do + local blip = AddBlipForCoord(booth.coords) + SetBlipSprite(blip, 614) + SetBlipDisplay(blip, 4) + SetBlipScale(blip, 0.7) + SetBlipColour(blip, 47) + SetBlipAsShortRange(blip, true) + BeginTextCommandSetBlipName("STRING") + AddTextComponentString("DJ Booth - " .. boothName) + EndTextCommandSetBlipName(blip) + end + + -- Erstelle Interaktionspunkte für DJ-Booths + for boothName, booth in pairs(Config.DJBooths) do + exports['qb-target']:AddBoxZone("dj_booth_" .. boothName, booth.coords, 1.5, 1.5, { + name = "dj_booth_" .. boothName, + heading = 0, + debugPoly = Config.Debug, + minZ = booth.coords.z - 1.0, + maxZ = booth.coords.z + 1.0 + }, { + options = { + { + type = "client", + event = "dj:client:openDJMenu", + icon = "fas fa-music", + label = "DJ System öffnen", + boothName = boothName, + job = Config.UseJobRestriction and Config.AllowedJobs or nil + } + }, + distance = 2.0 + }) + end +end) + +-- Event für Target-Integration +RegisterNetEvent('dj:client:openDJMenu', function(data) + nearestBooth = data.boothName + isDJBooth = true + OpenDJInterface() +end) + +-- Debug +if Config.Debug then + RegisterCommand('djdebug', function() + print('--- DJ SYSTEM DEBUG ---') + print('isPlaying:', isPlaying) + print('isDJBooth:', isDJBooth) + print('nearestBooth:', nearestBooth) + print('currentBooth:', currentBooth and currentBooth.name or 'nil') + print('currentVolume:', currentVolume) + print('currentSong:', currentSong and currentSong.title or 'nil') + print('activeBooths:') + for boothName, boothData in pairs(activeBooths) do + print(' -', boothName, boothData.title, boothData.volume) + end + end) +end + +-- NUI Callbacks für das DJ-Interface +RegisterNUICallback('loadTrack', function(data, cb) + if data.deck and data.track then + -- Lade Track in Deck + if data.deck == "A" or data.deck == "B" then + -- Hier könntest du zusätzliche Logik hinzufügen + cb({success = true}) + else + cb({success = false, error = "Ungültiges Deck"}) + end + else + cb({success = false, error = "Fehlende Daten"}) + end +end) + +RegisterNUICallback('playTrack', function(data, cb) + if data.deck and data.track then + -- Spiele Track ab + PlayMusicAsDJ(data.track.title, data.track.url, currentVolume) + cb({success = true}) + else + cb({success = false, error = "Fehlende Daten"}) + end +end) + +RegisterNUICallback('getPlaylists', function(data, cb) + -- Hole Playlists vom Server + QBCore.Functions.TriggerCallback('dj:server:getPlaylists', function(playlists) + cb({success = true, playlists = playlists}) + end) +end) + +RegisterNUICallback('createPlaylist', function(data, cb) + if data.name then + -- Erstelle Playlist + TriggerServerEvent('dj:server:createPlaylist', data.name, data.description, data.isPublic) + cb({success = true}) + else + cb({success = false, error = "Fehlender Name"}) + end +end) + +RegisterNUICallback('addToPlaylist', function(data, cb) + if data.playlistId and data.track then + -- Füge Track zu Playlist hinzu + TriggerServerEvent('dj:server:addSongToPlaylist', data.playlistId, data.track) + cb({success = true}) + else + cb({success = false, error = "Fehlende Daten"}) + end +end) + +RegisterNUICallback('getSessionHistory', function(data, cb) + -- Hole Session-Historie vom Server + QBCore.Functions.TriggerCallback('dj:server:getSessionHistory', function(history) + cb({success = true, history = history}) + end, data.limit) +end) diff --git a/resources/[tools]/nordi_dj/config.lua b/resources/[tools]/nordi_dj/config.lua index 5f940d501..bcd67b87b 100644 --- a/resources/[tools]/nordi_dj/config.lua +++ b/resources/[tools]/nordi_dj/config.lua @@ -1,72 +1,70 @@ Config = {} -- Allgemeine Einstellungen -Config.UseJobRestriction = true +Config.UseJobRestriction = true -- true = nur bestimmte Jobs können das Script nutzen Config.AllowedJobs = { 'dj', 'nightclub', 'admin' } -Config.OpenMenuKey = 'F7' +Config.OpenMenuKey = 'F7' -- Taste zum Öffnen des Menüs Config.MaxVolume = 100 Config.DefaultVolume = 50 --- YouTube API Einstellungen -Config.YouTubeAPI = { - enabled = true, - -- Du kannst eine kostenlose API von https://rapidapi.com/ytjar/api/youtube-mp36 bekommen - -- Oder eine andere YouTube to MP3 API verwenden - apiUrl = "https://youtube-mp36.p.rapidapi.com/dl", - apiKey = "DEIN_API_KEY_HIER", -- Ersetze mit deinem API Key - headers = { - ["X-RapidAPI-Key"] = "DEIN_API_KEY_HIER", - ["X-RapidAPI-Host"] = "youtube-mp36.p.rapidapi.com" - } -} - --- Alternative: Lokaler YouTube-DL Server (empfohlen für bessere Performance) -Config.LocalYouTubeConverter = { - enabled = false, -- Setze auf true wenn du einen lokalen Converter verwendest - serverUrl = "http://localhost:3000/convert" -- URL zu deinem lokalen Converter -} - -- DJ Booth Locations Config.DJBooths = { - { - name = "Vanilla Unicorn", + ["Vanilla Unicorn"] = { coords = vector3(120.13, -1281.72, 29.48), - range = 50.0, - maxRange = 100.0 + range = 30.0, + maxRange = 50.0 }, - { - name = "Bahama Mamas", + ["Bahama Mamas"] = { coords = vector3(-1387.08, -618.52, 30.82), - range = 50.0, - maxRange = 100.0 + range = 25.0, + maxRange = 45.0 }, - { - name = "Diamond Casino", + ["Diamond Casino"] = { coords = vector3(1549.78, 252.44, -46.01), - range = 60.0, - maxRange = 120.0 + range = 35.0, + maxRange = 60.0 + }, + ["Galaxy Nightclub"] = { + coords = vector3(-1605.78, -3012.47, -77.79), + range = 40.0, + maxRange = 60.0 + }, + ["Tequi-la-la"] = { + coords = vector3(-564.14, 275.35, 83.02), + range = 25.0, + maxRange = 40.0 } } --- Standard Playlists mit YouTube Links +-- Standard Playlists Config.DefaultPlaylists = { { name = "Party Hits", songs = { {title = "Daft Punk - One More Time", url = "https://www.youtube.com/watch?v=FGBhQbmPwH8"}, - {title = "Calvin Harris - Feel So Close", url = "https://www.youtube.com/watch?v=dGghkjpNCQ8"} + {title = "Avicii - Levels", url = "https://www.youtube.com/watch?v=_ovdm2yX4MA"} } }, { - name = "Chill Music", + name = "Chill Vibes", songs = { {title = "Kygo - Firestone", url = "https://www.youtube.com/watch?v=9Sc-ir2UwGU"}, - {title = "Avicii - Levels", url = "https://www.youtube.com/watch?v=_ovdm2yX4MA"} + {title = "Kygo - Stole The Show", url = "https://www.youtube.com/watch?v=BgfcToAjfdc"} } } } + +-- Distanz-basierte Lautstärke +Config.DistanceVolume = { + enabled = true, + updateInterval = 1000, -- Wie oft die Lautstärke aktualisiert wird (ms) + fadeTime = 1000 -- Wie lange der Fade-Effekt dauert (ms) +} + +-- Debug-Modus +Config.Debug = false diff --git a/resources/[tools]/nordi_dj/html/script.js b/resources/[tools]/nordi_dj/html/script.js index 888e6eadc..fb8458458 100644 --- a/resources/[tools]/nordi_dj/html/script.js +++ b/resources/[tools]/nordi_dj/html/script.js @@ -163,12 +163,6 @@ window.addEventListener('message', function(event) { case 'setVolume': setVolume(data.volume); break; - case 'pauseMusic': - pauseMusic(); - break; - case 'resumeMusic': - resumeMusic(); - break; } }); @@ -352,7 +346,7 @@ function extractYouTubeVideoId(url) { // FiveM benachrichtigen function notifyFiveM(event, data) { - fetch(`https://${GetParentResourceName()}/` + event, { + fetch(`https://${GetParentResourceName()}/${event}`, { method: 'POST', headers: { 'Content-Type': 'application/json' diff --git a/resources/[tools]/nordi_dj/server/main.lua b/resources/[tools]/nordi_dj/server/main.lua index 96f9cb0de..67594ed96 100644 --- a/resources/[tools]/nordi_dj/server/main.lua +++ b/resources/[tools]/nordi_dj/server/main.lua @@ -1,74 +1,7 @@ local QBCore = exports['qb-core']:GetCoreObject() --- YouTube URL Converter -local function ConvertYouTubeUrl(url, callback) - if not string.match(url, "youtube%.com") and not string.match(url, "youtu%.be") then - callback(url) -- Nicht YouTube, direkt zurückgeben - return - end - - -- Extrahiere Video ID - local videoId = ExtractYouTubeVideoId(url) - if not videoId then - callback(nil) - return - end - - if Config.LocalYouTubeConverter.enabled then - -- Verwende lokalen Converter - PerformHttpRequest(Config.LocalYouTubeConverter.serverUrl, function(errorCode, resultData, resultHeaders) - if errorCode == 200 then - local data = json.decode(resultData) - if data and data.url then - callback(data.url) - else - callback(nil) - end - else - callback(nil) - end - end, 'POST', json.encode({videoId = videoId}), {['Content-Type'] = 'application/json'}) - - elseif Config.YouTubeAPI.enabled then - -- Verwende externe API - local apiUrl = Config.YouTubeAPI.apiUrl .. "?id=" .. videoId - - PerformHttpRequest(apiUrl, function(errorCode, resultData, resultHeaders) - if errorCode == 200 then - local data = json.decode(resultData) - if data and data.link then - callback(data.link) - else - callback(nil) - end - else - print("YouTube API Error: " .. errorCode) - callback(nil) - end - end, 'GET', '', Config.YouTubeAPI.headers) - else - callback(nil) - end -end - -function ExtractYouTubeVideoId(url) - -- YouTube URL Patterns - local patterns = { - "youtube%.com/watch%?v=([%w%-_]+)", - "youtube%.com/watch%?.*&v=([%w%-_]+)", - "youtu%.be/([%w%-_]+)", - "youtube%.com/embed/([%w%-_]+)" - } - - for _, pattern in ipairs(patterns) do - local videoId = string.match(url, pattern) - if videoId then - return videoId - end - end - - return nil -end +-- Aktive DJ-Booths +local activeDJBooths = {} -- Database Setup CreateThread(function() @@ -77,7 +10,10 @@ CreateThread(function() id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, owner VARCHAR(50) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + description TEXT DEFAULT NULL, + is_public TINYINT(1) DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ]]) @@ -86,9 +22,15 @@ CreateThread(function() id INT AUTO_INCREMENT PRIMARY KEY, playlist_id INT NOT NULL, title VARCHAR(255) NOT NULL, + artist VARCHAR(255) DEFAULT NULL, url TEXT NOT NULL, - converted_url TEXT NULL, + converted_url TEXT DEFAULT NULL, + duration INT DEFAULT NULL, position INT DEFAULT 0, + song_type VARCHAR(20) DEFAULT 'direct', + thumbnail TEXT DEFAULT NULL, + added_by VARCHAR(50) DEFAULT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (playlist_id) REFERENCES dj_playlists(id) ON DELETE CASCADE ) ]]) @@ -96,181 +38,46 @@ CreateThread(function() MySQL.query([[ CREATE TABLE IF NOT EXISTS dj_url_cache ( id INT AUTO_INCREMENT PRIMARY KEY, - original_url VARCHAR(500) NOT NULL UNIQUE, + original_url VARCHAR(500) NOT NULL, converted_url TEXT NOT NULL, + video_id VARCHAR(50) DEFAULT NULL, + title VARCHAR(255) DEFAULT NULL, + duration INT DEFAULT NULL, + thumbnail TEXT DEFAULT NULL, expires_at TIMESTAMP NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + hit_count INT DEFAULT 1, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_accessed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY (original_url(255)) + ) + ]]) + + MySQL.query([[ + CREATE TABLE IF NOT EXISTS dj_session_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + dj_citizenid VARCHAR(50) NOT NULL, + dj_name VARCHAR(100) NOT NULL, + booth_name VARCHAR(100) NOT NULL, + song_title VARCHAR(255) NOT NULL, + song_url TEXT NOT NULL, + song_type VARCHAR(20) DEFAULT 'direct', + volume INT DEFAULT 50, + duration_played INT DEFAULT NULL, + session_start TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + session_end TIMESTAMP NULL DEFAULT NULL, + listeners_count INT DEFAULT 0 ) ]]) end) --- Events -RegisterNetEvent('dj:server:playMusic', function(data) - local src = source - - -- Prüfe Cache zuerst - MySQL.query('SELECT converted_url FROM dj_url_cache WHERE original_url = ? AND expires_at > NOW()', {data.url}, function(cached) - if cached[1] then - -- Verwende gecachte URL - local musicData = { - title = data.title, - url = cached[1].converted_url, - volume = data.volume, - booth = data.booth, - range = data.range - } - TriggerClientEvent('dj:client:playMusic', -1, musicData) - print(('[DJ System] %s spielt Musik ab (cached): %s'):format(GetPlayerName(src), data.title)) - else - -- Konvertiere URL - ConvertYouTubeUrl(data.url, function(convertedUrl) - if convertedUrl then - -- Cache die URL für 1 Stunde - MySQL.insert('INSERT INTO dj_url_cache (original_url, converted_url, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 1 HOUR)) ON DUPLICATE KEY UPDATE converted_url = VALUES(converted_url), expires_at = VALUES(expires_at)', { - data.url, - convertedUrl - }) - - local musicData = { - title = data.title, - url = convertedUrl, - volume = data.volume, - booth = data.booth, - range = data.range - } - TriggerClientEvent('dj:client:playMusic', -1, musicData) - print(('[DJ System] %s spielt Musik ab (converted): %s'):format(GetPlayerName(src), data.title)) - else - TriggerClientEvent('dj:client:notify', src, 'Fehler beim Konvertieren der YouTube URL!', 'error') - print(('[DJ System] Fehler beim Konvertieren der URL: %s'):format(data.url)) - end - end) - end - end) -end) - -RegisterNetEvent('dj:server:stopMusic', function(booth) - local src = source - TriggerClientEvent('dj:client:stopMusic', -1) - print(('[DJ System] %s hat die Musik gestoppt'):format(GetPlayerName(src))) -end) - -RegisterNetEvent('dj:server:setVolume', function(data) - local src = source - TriggerClientEvent('dj:client:setVolume', -1, data.volume) - print(('[DJ System] %s hat die Lautstärke auf %d%% gesetzt'):format(GetPlayerName(src), data.volume)) -end) - -RegisterNetEvent('dj:server:getPlaylists', function() - local src = source - local Player = QBCore.Functions.GetPlayer(src) - if not Player then return end - - MySQL.query('SELECT * FROM dj_playlists WHERE owner = ?', {Player.PlayerData.citizenid}, function(playlists) - local playlistData = {} - - for _, playlist in pairs(playlists) do - MySQL.query('SELECT * FROM dj_playlist_songs WHERE playlist_id = ? ORDER BY position', {playlist.id}, function(songs) - table.insert(playlistData, { - id = playlist.id, - name = playlist.name, - songs = songs - }) - - if #playlistData == #playlists then - TriggerClientEvent('dj:client:updatePlaylists', src, playlistData) - end - end) - end - - if #playlists == 0 then - TriggerClientEvent('dj:client:updatePlaylists', src, {}) - end - end) -end) - -RegisterNetEvent('dj:server:createPlaylist', function(name) - local src = source - local Player = QBCore.Functions.GetPlayer(src) - if not Player then return end - - MySQL.insert('INSERT INTO dj_playlists (name, owner) VALUES (?, ?)', { - name, - Player.PlayerData.citizenid - }, function(id) - if id then - TriggerClientEvent('dj:client:notify', src, 'Playlist "' .. name .. '" wurde erstellt!', 'success') - TriggerEvent('dj:server:getPlaylists') - else - TriggerClientEvent('dj:client:notify', src, 'Fehler beim Erstellen der Playlist!', 'error') - end - end) -end) - -RegisterNetEvent('dj:server:addSongToPlaylist', function(playlistId, song) - local src = source - local Player = QBCore.Functions.GetPlayer(src) - if not Player then return end - - MySQL.query('SELECT * FROM dj_playlists WHERE id = ? AND owner = ?', { - playlistId, - Player.PlayerData.citizenid - }, function(result) - if result[1] then - MySQL.insert('INSERT INTO dj_playlist_songs (playlist_id, title, url) VALUES (?, ?, ?)', { - playlistId, - song.title, - song.url - }, function(id) - if id then - TriggerClientEvent('dj:client:notify', src, 'Song wurde zur Playlist hinzugefügt!', 'success') - TriggerEvent('dj:server:getPlaylists') - else - TriggerClientEvent('dj:client:notify', src, 'Fehler beim Hinzufügen des Songs!', 'error') - end - end) - else - TriggerClientEvent('dj:client:notify', src, 'Du hast keine Berechtigung für diese Playlist!', 'error') - end - end) -end) - -RegisterNetEvent('dj:server:deletePlaylist', function(playlistId) - local src = source - local Player = QBCore.Functions.GetPlayer(src) - if not Player then return end - - MySQL.query('DELETE FROM dj_playlists WHERE id = ? AND owner = ?', { - playlistId, - Player.PlayerData.citizenid - }, function(affectedRows) - if affectedRows > 0 then - TriggerClientEvent('dj:client:notify', src, 'Playlist wurde gelöscht!', 'success') - TriggerEvent('dj:server:getPlaylists') - else - TriggerClientEvent('dj:client:notify', src, 'Fehler beim Löschen der Playlist!', 'error') - end - end) -end) - --- Cache Cleanup (läuft alle 30 Minuten) -CreateThread(function() - while true do - Wait(1800000) -- 30 Minuten - MySQL.query('DELETE FROM dj_url_cache WHERE expires_at < NOW()') - print('[DJ System] Cache bereinigt') - end -end) - --- Füge diese Funktionen zu deiner server/main.lua hinzu: - +-- YouTube URL Funktionen local function extractYouTubeVideoId(url) - -- Verschiedene YouTube URL Formate unterstützen + -- YouTube URL Patterns local patterns = { - "youtube%.com/watch%?v=([^&]+)", - "youtu%.be/([^?]+)", - "youtube%.com/embed/([^?]+)", - "youtube%.com/v/([^?]+)" + "youtube%.com/watch%?v=([%w%-_]+)", + "youtube%.com/watch%?.*&v=([%w%-_]+)", + "youtu%.be/([%w%-_]+)", + "youtube%.com/embed/([%w%-_]+)" } for _, pattern in ipairs(patterns) do @@ -293,128 +100,476 @@ local function cleanYouTubeUrl(url) return "https://www.youtube.com/watch?v=" .. videoId end --- YouTube zu MP3 Konvertierung (mit yt-dlp oder youtube-dl) -local function convertYouTubeUrl(url, callback) - local videoId = extractYouTubeVideoId(url) - if not videoId then - callback(nil, "Ungültige YouTube URL") - return - end - - -- Prüfe Cache zuerst - MySQL.Async.fetchScalar('SELECT converted_url FROM dj_url_cache WHERE original_url = ? AND expires_at > NOW()', {url}, function(cachedUrl) - if cachedUrl then - print('[DJ System] URL aus Cache geladen: ' .. videoId) - callback(cachedUrl, nil) - - -- Update hit count - MySQL.Async.execute('UPDATE dj_url_cache SET hit_count = hit_count + 1, last_accessed = NOW() WHERE original_url = ?', {url}) - return - end - - -- Konvertiere mit yt-dlp (externe Lösung) - convertWithYtDlp(url, videoId, callback) - end) -end - --- Konvertierung mit yt-dlp (benötigt yt-dlp Installation) -local function convertWithYtDlp(originalUrl, videoId, callback) - local cleanUrl = "https://www.youtube.com/watch?v=" .. videoId - - -- yt-dlp Befehl ausführen - local command = string.format('yt-dlp --get-url --format "bestaudio[ext=m4a]/best[ext=mp4]/best" "%s"', cleanUrl) - - -- Führe Befehl aus (das ist ein vereinfachtes Beispiel) - -- In der Praxis würdest du einen HTTP Request an einen externen Service machen - - -- Für jetzt: Verwende eine alternative Lösung - useAlternativeConversion(originalUrl, videoId, callback) -end - --- Alternative: Verwende einen Online-Service -local function useAlternativeConversion(originalUrl, videoId, callback) - -- Option 1: Verwende YouTube Embed URL (funktioniert manchmal) - local embedUrl = "https://www.youtube.com/embed/" .. videoId .. "?autoplay=1" - - -- Option 2: Verwende einen kostenlosen Konvertierungsservice - -- ACHTUNG: Diese Services können instabil sein! - local convertedUrl = "https://youtube-mp3-download.com/api/convert/" .. videoId - - -- Option 3: Verwende die originale URL und lass den Client damit umgehen - local fallbackUrl = "https://www.youtube.com/watch?v=" .. videoId - - -- Speichere im Cache (24 Stunden gültig) - local expiresAt = os.date('%Y-%m-%d %H:%M:%S', os.time() + 86400) - - MySQL.Async.execute('INSERT INTO dj_url_cache (original_url, converted_url, video_id, expires_at) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE converted_url = VALUES(converted_url), expires_at = VALUES(expires_at)', { - originalUrl, - fallbackUrl, -- Verwende fallback URL - videoId, - expiresAt - }, function(affectedRows) - if affectedRows > 0 then - print('[DJ System] URL in Cache gespeichert: ' .. videoId) - end - - callback(fallbackUrl, nil) - end) -end - --- Aktualisierte PlayMusic Funktion -RegisterServerEvent('dj:playMusic') -AddEventHandler('dj:playMusic', function(title, url, volume) +-- Events +RegisterServerEvent('dj:server:playMusic') +AddEventHandler('dj:server:playMusic', function(data) local src = source local Player = QBCore.Functions.GetPlayer(src) if not Player then return end - -- Bereinige YouTube URL (entferne Playlist-Parameter) - local cleanUrl = url - if string.find(url, "youtube%.com") or string.find(url, "youtu%.be") then - -- Entferne &list= und andere Parameter - cleanUrl = string.gsub(url, "&list=.-$", "") - cleanUrl = string.gsub(cleanUrl, "&start_radio=.-$", "") - cleanUrl = string.gsub(cleanUrl, "&index=.-$", "") - - print('[DJ System] Bereinigte YouTube URL: ' .. cleanUrl) - end + -- Speichere den aktuellen DJ-Status + activeDJBooths[data.booth.name] = { + url = data.url, + title = data.title, + volume = data.volume, + range = data.range, + dj = { + id = Player.PlayerData.citizenid, + name = Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname + }, + coords = data.booth.coords + } - print('[DJ System] Streaming: ' .. title .. ' | URL: ' .. cleanUrl) + -- Sende an alle Spieler + TriggerClientEvent('dj:client:playMusic', -1, data) - -- Sende direkt an alle Clients zum Streamen - TriggerClientEvent('dj:playMusicClient', -1, title, cleanUrl, volume) + -- Log für Admin + print(('[DJ System] %s spielt Musik ab: %s | Booth: %s'):format( + GetPlayerName(src), + data.title, + data.booth.name + )) -- Speichere in Datenbank - local songType = 'direct' - if string.find(cleanUrl, "youtube") then - songType = 'youtube' - end - - MySQL.Async.execute('INSERT INTO dj_session_history (dj_citizenid, dj_name, booth_name, song_title, song_url, song_type, volume, session_start) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', { + MySQL.insert('INSERT INTO dj_session_history (dj_citizenid, dj_name, booth_name, song_title, song_url, song_type, volume, session_start) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', { Player.PlayerData.citizenid, Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname, - 'DJ Booth', - title, - cleanUrl, - songType, - volume + data.booth.name, + data.title, + data.url, + string.match(data.url, "youtube") and 'youtube' or 'direct', + data.volume }) - - TriggerClientEvent('QBCore:Notify', src, 'Streaming: ' .. title, 'success') end) --- Audio Error Handler -RegisterNUICallback('audioError', function(data, cb) +RegisterServerEvent('dj:server:stopMusic') +AddEventHandler('dj:server:stopMusic', function(boothName) local src = source - print('[DJ System] Audio Error: ' .. (data.error or 'Unknown')) - TriggerClientEvent('QBCore:Notify', src, 'Audio Fehler: ' .. (data.error or 'Unbekannt'), 'error') - cb('ok') + -- Entferne DJ-Status + if activeDJBooths[boothName] then + -- Aktualisiere Session-Ende in der Datenbank + local djData = activeDJBooths[boothName] + if djData and djData.dj then + MySQL.update('UPDATE dj_session_history SET session_end = NOW() WHERE dj_citizenid = ? AND booth_name = ? AND session_end IS NULL', { + djData.dj.id, + boothName + }) + end + + activeDJBooths[boothName] = nil + end + + -- Sende an alle Spieler + TriggerClientEvent('dj:client:stopMusic', -1, boothName) + + print(('[DJ System] %s hat die Musik gestoppt in: %s'):format(GetPlayerName(src), boothName)) end) --- Song Ended Handler -RegisterNUICallback('songEnded', function(data, cb) - print('[DJ System] Song ended') - -- Hier könntest du Playlist-Logik hinzufügen - cb('ok') +RegisterServerEvent('dj:server:setVolume') +AddEventHandler('dj:server:setVolume', function(data) + local src = source + + -- Aktualisiere DJ-Status + if activeDJBooths[data.booth.name] then + activeDJBooths[data.booth.name].volume = data.volume + activeDJBooths[data.booth.name].range = data.range + end + + -- Sende an alle Spieler + TriggerClientEvent('dj:client:setVolume', -1, data) + + print(('[DJ System] %s hat die Lautstärke auf %d%% gesetzt in: %s'):format( + GetPlayerName(src), + data.volume, + data.booth.name + )) end) +-- Playlist Management +RegisterServerEvent('dj:server:getPlaylists') +AddEventHandler('dj:server:getPlaylists', function() + local src = source + local Player = QBCore.Functions.GetPlayer(src) + if not Player then return end + + MySQL.query('SELECT * FROM dj_playlists WHERE owner = ? OR is_public = 1', {Player.PlayerData.citizenid}, function(playlists) + local playlistData = {} + + for _, playlist in pairs(playlists) do + MySQL.query('SELECT * FROM dj_playlist_songs WHERE playlist_id = ? ORDER BY position', {playlist.id}, function(songs) + table.insert(playlistData, { + id = playlist.id, + name = playlist.name, + description = playlist.description, + isPublic = playlist.is_public == 1, + isOwner = playlist.owner == Player.PlayerData.citizenid, + songs = songs + }) + + if #playlistData == #playlists then + TriggerClientEvent('dj:client:updatePlaylists', src, playlistData) + end + end) + end + + if #playlists == 0 then + TriggerClientEvent('dj:client:updatePlaylists', src, {}) + end + end) +end) + +RegisterServerEvent('dj:server:createPlaylist') +AddEventHandler('dj:server:createPlaylist', function(name, description, isPublic) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + if not Player then return end + + MySQL.insert('INSERT INTO dj_playlists (name, owner, description, is_public) VALUES (?, ?, ?, ?)', { + name, + Player.PlayerData.citizenid, + description or '', + isPublic and 1 or 0 + }, function(id) + if id then + TriggerClientEvent('QBCore:Notify', src, 'Playlist "' .. name .. '" wurde erstellt!', 'success') + TriggerEvent('dj:server:getPlaylists', src) + else + TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Erstellen der Playlist!', 'error') + end + end) +end) + +RegisterServerEvent('dj:server:addSongToPlaylist') +AddEventHandler('dj:server:addSongToPlaylist', function(playlistId, song) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + if not Player then return end + + -- Prüfe ob Playlist dem Spieler gehört + MySQL.query('SELECT * FROM dj_playlists WHERE id = ? AND (owner = ? OR is_public = 1)', { + playlistId, + Player.PlayerData.citizenid + }, function(result) + if result[1] then + -- Bereinige YouTube URL + local songUrl = song.url + if string.match(songUrl, "youtube") then + local cleanUrl = cleanYouTubeUrl(songUrl) + if cleanUrl then + songUrl = cleanUrl + end + end + + -- Hole höchste Position + MySQL.query('SELECT MAX(position) as max_pos FROM dj_playlist_songs WHERE playlist_id = ?', {playlistId}, function(posResult) + local position = 1 + if posResult[1] and posResult[1].max_pos then + position = posResult[1].max_pos + 1 + end + + -- Füge Song hinzu + MySQL.insert('INSERT INTO dj_playlist_songs (playlist_id, title, artist, url, song_type, position, added_by) VALUES (?, ?, ?, ?, ?, ?, ?)', { + playlistId, + song.title, + song.artist or '', + songUrl, + string.match(songUrl, "youtube") and 'youtube' or 'direct', + position, + Player.PlayerData.citizenid + }, function(id) + if id then + TriggerClientEvent('QBCore:Notify', src, 'Song wurde zur Playlist hinzugefügt!', 'success') + TriggerEvent('dj:server:getPlaylists', src) + else + TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Hinzufügen des Songs!', 'error') + end + end) + end) + else + TriggerClientEvent('QBCore:Notify', src, 'Du hast keine Berechtigung für diese Playlist!', 'error') + end + end) +end) + +RegisterServerEvent('dj:server:removeSongFromPlaylist') +AddEventHandler('dj:server:removeSongFromPlaylist', function(playlistId, songId) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + if not Player then return end + + -- Prüfe ob Playlist dem Spieler gehört + MySQL.query('SELECT * FROM dj_playlists WHERE id = ? AND owner = ?', { + playlistId, + Player.PlayerData.citizenid + }, function(result) + if result[1] then + MySQL.query('DELETE FROM dj_playlist_songs WHERE id = ? AND playlist_id = ?', { + songId, + playlistId + }, function(affectedRows) + if affectedRows > 0 then + TriggerClientEvent('QBCore:Notify', src, 'Song wurde aus der Playlist entfernt!', 'success') + TriggerEvent('dj:server:getPlaylists', src) + else + TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Entfernen des Songs!', 'error') + end + end) + else + TriggerClientEvent('QBCore:Notify', src, 'Du hast keine Berechtigung für diese Playlist!', 'error') + end + end) +end) + +RegisterServerEvent('dj:server:deletePlaylist') +AddEventHandler('dj:server:deletePlaylist', function(playlistId) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + if not Player then return end + + MySQL.query('DELETE FROM dj_playlists WHERE id = ? AND owner = ?', { + playlistId, + Player.PlayerData.citizenid + }, function(affectedRows) + if affectedRows > 0 then + TriggerClientEvent('QBCore:Notify', src, 'Playlist wurde gelöscht!', 'success') + TriggerEvent('dj:server:getPlaylists', src) + else + TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Löschen der Playlist!', 'error') + end + end) +end) + +RegisterServerEvent('dj:server:updatePlaylist') +AddEventHandler('dj:server:updatePlaylist', function(playlistId, name, description, isPublic) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + if not Player then return end + + MySQL.update('UPDATE dj_playlists SET name = ?, description = ?, is_public = ? WHERE id = ? AND owner = ?', { + name, + description or '', + isPublic and 1 or 0, + playlistId, + Player.PlayerData.citizenid + }, function(affectedRows) + if affectedRows > 0 then + TriggerClientEvent('QBCore:Notify', src, 'Playlist wurde aktualisiert!', 'success') + TriggerEvent('dj:server:getPlaylists', src) + else + TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Aktualisieren der Playlist!', 'error') + end + end) +end) + +-- Synchronisation +RegisterServerEvent('dj:server:requestActiveDJs') +AddEventHandler('dj:server:requestActiveDJs', function() + local src = source + TriggerClientEvent('dj:client:receiveActiveDJs', src, activeDJBooths) +end) + +-- Wenn ein Spieler das Spiel verlässt und DJ war, stoppe die Musik +AddEventHandler('playerDropped', function(reason) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + if not Player then return end + + -- Prüfe ob Spieler DJ war + for boothName, boothData in pairs(activeDJBooths) do + if boothData.dj and boothData.dj.id == Player.PlayerData.citizenid then + -- Spieler war DJ, stoppe Musik + + -- Aktualisiere Session-Ende in der Datenbank + MySQL.update('UPDATE dj_session_history SET session_end = NOW() WHERE dj_citizenid = ? AND booth_name = ? AND session_end IS NULL', { + boothData.dj.id, + boothName + }) + + activeDJBooths[boothName] = nil + TriggerClientEvent('dj:client:stopMusic', -1, boothName) + + print(('[DJ System] DJ %s hat das Spiel verlassen, Musik in %s gestoppt'):format( + GetPlayerName(src), + boothName + )) + end + end +end) + +-- Cache Cleanup (läuft alle 30 Minuten) +CreateThread(function() + while true do + Wait(1800000) -- 30 Minuten + MySQL.query('DELETE FROM dj_url_cache WHERE expires_at < NOW()') + print('[DJ System] Cache bereinigt') + end +end) + +-- Initialisiere Standard-Playlists +CreateThread(function() + Wait(5000) -- Warte bis Datenbank bereit ist + + -- Prüfe ob Standard-Playlists existieren + MySQL.query('SELECT * FROM dj_playlists WHERE owner = ?', {'system'}, function(result) + if #result == 0 then + print('[DJ System] Erstelle Standard-Playlists') + + -- Erstelle Standard-Playlists + for _, playlist in pairs(Config.DefaultPlaylists) do + MySQL.insert('INSERT INTO dj_playlists (name, owner, description, is_public) VALUES (?, ?, ?, ?)', { + playlist.name, + 'system', + 'Standard-Playlist', + 1 + }, function(playlistId) + if playlistId then + -- Füge Songs hinzu + for position, song in ipairs(playlist.songs) do + MySQL.insert('INSERT INTO dj_playlist_songs (playlist_id, title, url, song_type, position, added_by) VALUES (?, ?, ?, ?, ?, ?)', { + playlistId, + song.title, + song.url, + string.match(song.url, "youtube") and 'youtube' or 'direct', + position, + 'system' + }) + end + + print('[DJ System] Standard-Playlist erstellt: ' .. playlist.name) + end + end) + end + end + end) +end) + +-- QBCore Callbacks +QBCore.Functions.CreateCallback('dj:server:getActiveDJs', function(source, cb) + cb(activeDJBooths) +end) + +QBCore.Functions.CreateCallback('dj:server:getPlaylists', function(source, cb) + local Player = QBCore.Functions.GetPlayer(source) + if not Player then return cb({}) end + + MySQL.query('SELECT * FROM dj_playlists WHERE owner = ? OR is_public = 1', {Player.PlayerData.citizenid}, function(playlists) + local playlistData = {} + local remaining = #playlists + + if remaining == 0 then + return cb({}) + end + + for _, playlist in pairs(playlists) do + MySQL.query('SELECT * FROM dj_playlist_songs WHERE playlist_id = ? ORDER BY position', {playlist.id}, function(songs) + table.insert(playlistData, { + id = playlist.id, + name = playlist.name, + description = playlist.description, + isPublic = playlist.is_public == 1, + isOwner = playlist.owner == Player.PlayerData.citizenid, + songs = songs + }) + + remaining = remaining - 1 + if remaining == 0 then + cb(playlistData) + end + end) + end + end) +end) + +QBCore.Functions.CreateCallback('dj:server:getSessionHistory', function(source, cb, limit) + local Player = QBCore.Functions.GetPlayer(source) + if not Player then return cb({}) end + + limit = limit or 50 + + MySQL.query('SELECT * FROM dj_session_history WHERE dj_citizenid = ? ORDER BY session_start DESC LIMIT ?', { + Player.PlayerData.citizenid, + limit + }, function(history) + cb(history) + end) +end) + +-- Admin Commands +QBCore.Commands.Add('djstop', 'Stoppe alle DJ-Booths (Admin)', {}, false, function(source, args) + local Player = QBCore.Functions.GetPlayer(source) + if Player.PlayerData.job.name ~= 'admin' and Player.PlayerData.job.name ~= 'police' then + TriggerClientEvent('QBCore:Notify', source, 'Du hast keine Berechtigung für diesen Befehl!', 'error') + return + end + + for boothName, _ in pairs(activeDJBooths) do + -- Aktualisiere Session-Ende in der Datenbank + local djData = activeDJBooths[boothName] + if djData and djData.dj then + MySQL.update('UPDATE dj_session_history SET session_end = NOW() WHERE dj_citizenid = ? AND booth_name = ? AND session_end IS NULL', { + djData.dj.id, + boothName + }) + end + + activeDJBooths[boothName] = nil + TriggerClientEvent('dj:client:stopMusic', -1, boothName) + end + + TriggerClientEvent('QBCore:Notify', source, 'Alle DJ-Booths wurden gestoppt!', 'success') + print('[DJ System] Admin ' .. GetPlayerName(source) .. ' hat alle DJ-Booths gestoppt') +end) + +QBCore.Commands.Add('djstatus', 'Zeige Status aller DJ-Booths (Admin)', {}, false, function(source, args) + local Player = QBCore.Functions.GetPlayer(source) + if Player.PlayerData.job.name ~= 'admin' and Player.PlayerData.job.name ~= 'police' then + TriggerClientEvent('QBCore:Notify', source, 'Du hast keine Berechtigung für diesen Befehl!', 'error') + return + end + + local status = {} + for boothName, boothData in pairs(activeDJBooths) do + table.insert(status, { + booth = boothName, + song = boothData.title, + dj = boothData.dj.name, + volume = boothData.volume + }) + end + + if #status == 0 then + TriggerClientEvent('QBCore:Notify', source, 'Keine aktiven DJ-Booths!', 'info') + else + for _, booth in ipairs(status) do + TriggerClientEvent('QBCore:Notify', source, booth.booth .. ': ' .. booth.song .. ' (DJ: ' .. booth.dj .. ', Vol: ' .. booth.volume .. '%)', 'info') + end + end +end) + +-- Exports +exports('GetActiveDJs', function() + return activeDJBooths +end) + +exports('StopBooth', function(boothName) + if activeDJBooths[boothName] then + -- Aktualisiere Session-Ende in der Datenbank + local djData = activeDJBooths[boothName] + if djData and djData.dj then + MySQL.update('UPDATE dj_session_history SET session_end = NOW() WHERE dj_citizenid = ? AND booth_name = ? AND session_end IS NULL', { + djData.dj.id, + boothName + }) + end + + activeDJBooths[boothName] = nil + TriggerClientEvent('dj:client:stopMusic', -1, boothName) + return true + end + return false +end) + +-- Initialisierung +print('[DJ System] Server gestartet') +