From e0a3b21c6134c15a4f3e9f6114fe3c443ff9c30b Mon Sep 17 00:00:00 2001 From: Nordi98 Date: Sun, 3 Aug 2025 16:51:12 +0200 Subject: [PATCH] ed --- resources/[tools]/nordi_dj/client/main.lua | 587 +++++++++++++++++++++ resources/[tools]/nordi_dj/config.lua | 72 +++ resources/[tools]/nordi_dj/fxmanifest.lua | 34 ++ resources/[tools]/nordi_dj/html/index.html | 34 ++ resources/[tools]/nordi_dj/html/script.js | 401 ++++++++++++++ resources/[tools]/nordi_dj/html/style.css | 72 +++ resources/[tools]/nordi_dj/server/main.lua | 263 +++++++++ 7 files changed, 1463 insertions(+) create mode 100644 resources/[tools]/nordi_dj/client/main.lua create mode 100644 resources/[tools]/nordi_dj/config.lua create mode 100644 resources/[tools]/nordi_dj/fxmanifest.lua create mode 100644 resources/[tools]/nordi_dj/html/index.html create mode 100644 resources/[tools]/nordi_dj/html/script.js create mode 100644 resources/[tools]/nordi_dj/html/style.css create mode 100644 resources/[tools]/nordi_dj/server/main.lua diff --git a/resources/[tools]/nordi_dj/client/main.lua b/resources/[tools]/nordi_dj/client/main.lua new file mode 100644 index 000000000..c2c60231a --- /dev/null +++ b/resources/[tools]/nordi_dj/client/main.lua @@ -0,0 +1,587 @@ +local QBCore = exports['qb-core']:GetCoreObject() +local PlayerData = {} +local currentDJBooth = nil +local isPlaying = false +local currentVolume = Config.DefaultVolume +local currentSong = nil +local playlists = {} +local currentPlaylist = nil +local currentSongIndex = 1 + +-- Events +RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() + PlayerData = QBCore.Functions.GetPlayerData() +end) + +RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo) + PlayerData.job = JobInfo +end) + +-- Key Mapping +RegisterKeyMapping('opendj', 'Open DJ Menu', 'keyboard', Config.OpenMenuKey) + +RegisterCommand('opendj', function() + if not CanUseDJScript() then + lib.notify({ + title = 'DJ System', + description = 'Du hast keine Berechtigung das DJ System zu nutzen!', + type = 'error' + }) + return + end + + local nearbyBooth = GetNearbyDJBooth() + if nearbyBooth then + currentDJBooth = nearbyBooth + OpenDJMenu() + else + lib.notify({ + title = 'DJ System', + description = 'Du bist nicht in der Nähe einer DJ Booth!', + type = 'error' + }) + end +end) + +-- Functions +function CanUseDJScript() + if not Config.UseJobRestriction then + return true + end + + if not PlayerData.job then + return false + end + + for _, job in pairs(Config.AllowedJobs) do + if PlayerData.job.name == job then + return true + end + end + + 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 + +function OpenDJMenu() + TriggerServerEvent('dj:server:getPlaylists') + + local options = { + { + title = 'YouTube Song abspielen', + description = 'Spiele einen Song von YouTube ab', + icon = 'fab fa-youtube', + onSelect = function() + OpenYouTubeMenu() + end + }, + { + title = 'Direkte URL abspielen', + description = 'Spiele einen Song von einer direkten URL ab', + icon = 'play', + onSelect = function() + OpenDirectUrlMenu() + end + }, + { + title = 'Musik stoppen', + description = 'Stoppe die aktuelle Musik', + icon = 'stop', + onSelect = function() + StopMusic() + end + }, + { + title = 'Lautstärke ändern', + description = 'Aktuelle Lautstärke: ' .. currentVolume .. '%', + icon = 'volume-up', + onSelect = function() + OpenVolumeMenu() + end + }, + { + title = 'Playlists verwalten', + description = 'Erstelle und verwalte Playlists', + icon = 'list', + onSelect = function() + OpenPlaylistMenu() + end + } + } + + if isPlaying and currentSong then + table.insert(options, 2, { + title = 'Aktueller Song', + description = currentSong.title, + icon = 'music', + disabled = true + }) + end + + lib.registerContext({ + id = 'dj_main_menu', + title = 'DJ System - ' .. currentDJBooth.name, + options = options + }) + + lib.showContext('dj_main_menu') +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 + + 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!', + 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) \ No newline at end of file diff --git a/resources/[tools]/nordi_dj/config.lua b/resources/[tools]/nordi_dj/config.lua new file mode 100644 index 000000000..5f940d501 --- /dev/null +++ b/resources/[tools]/nordi_dj/config.lua @@ -0,0 +1,72 @@ +Config = {} + +-- Allgemeine Einstellungen +Config.UseJobRestriction = true +Config.AllowedJobs = { + 'dj', + 'nightclub', + 'admin' +} + +Config.OpenMenuKey = 'F7' +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", + coords = vector3(120.13, -1281.72, 29.48), + range = 50.0, + maxRange = 100.0 + }, + { + name = "Bahama Mamas", + coords = vector3(-1387.08, -618.52, 30.82), + range = 50.0, + maxRange = 100.0 + }, + { + name = "Diamond Casino", + coords = vector3(1549.78, 252.44, -46.01), + range = 60.0, + maxRange = 120.0 + } +} + +-- Standard Playlists mit YouTube Links +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"} + } + }, + { + name = "Chill Music", + songs = { + {title = "Kygo - Firestone", url = "https://www.youtube.com/watch?v=9Sc-ir2UwGU"}, + {title = "Avicii - Levels", url = "https://www.youtube.com/watch?v=_ovdm2yX4MA"} + } + } +} diff --git a/resources/[tools]/nordi_dj/fxmanifest.lua b/resources/[tools]/nordi_dj/fxmanifest.lua new file mode 100644 index 000000000..865fe6b53 --- /dev/null +++ b/resources/[tools]/nordi_dj/fxmanifest.lua @@ -0,0 +1,34 @@ +fx_version 'cerulean' +game 'gta5' + +author 'YourName' +description 'QBCore DJ Script with YouTube Support' +version '1.0.0' + +shared_scripts { + '@ox_lib/init.lua', + 'config.lua' +} + +client_scripts { + 'client/main.lua' +} + +server_scripts { + '@oxmysql/lib/MySQL.lua', + 'server/main.lua' +} + +ui_page 'html/index.html' + +files { + 'html/index.html', + 'html/style.css', + 'html/script.js' +} + +dependencies { + 'qb-core', + 'ox_lib', + 'oxmysql' +} diff --git a/resources/[tools]/nordi_dj/html/index.html b/resources/[tools]/nordi_dj/html/index.html new file mode 100644 index 000000000..f7aef6b10 --- /dev/null +++ b/resources/[tools]/nordi_dj/html/index.html @@ -0,0 +1,34 @@ + + + + + + DJ System + + + +
+ + + + + + + +
+ + + + diff --git a/resources/[tools]/nordi_dj/html/script.js b/resources/[tools]/nordi_dj/html/script.js new file mode 100644 index 000000000..dd7ca2526 --- /dev/null +++ b/resources/[tools]/nordi_dj/html/script.js @@ -0,0 +1,401 @@ +let audioPlayer = null; +let currentVolume = 50; +let isPlaying = false; +let currentSong = null; +let fadeInterval = null; + +// Initialize when page loads +document.addEventListener('DOMContentLoaded', function() { + audioPlayer = document.getElementById('audio-player'); + setupAudioPlayer(); +}); + +// Setup audio player with event listeners +function setupAudioPlayer() { + if (!audioPlayer) return; + + // Audio Events + audioPlayer.addEventListener('loadstart', function() { + console.log('DJ System: Loading started'); + }); + + audioPlayer.addEventListener('canplay', function() { + console.log('DJ System: Can start playing'); + }); + + audioPlayer.addEventListener('play', function() { + console.log('DJ System: Playback started'); + isPlaying = true; + }); + + audioPlayer.addEventListener('pause', function() { + console.log('DJ System: Playback paused'); + isPlaying = false; + }); + + audioPlayer.addEventListener('ended', function() { + console.log('DJ System: Song ended'); + isPlaying = false; + // Notify FiveM that song ended (for playlist functionality) + fetch(`https://${GetParentResourceName()}/songEnded`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify({}) + }); + }); + + audioPlayer.addEventListener('error', function(e) { + console.error('DJ System: Audio error', e); + isPlaying = false; + // Notify FiveM about the error + fetch(`https://${GetParentResourceName()}/audioError`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify({ + error: 'Audio playback error', + code: audioPlayer.error ? audioPlayer.error.code : 'unknown' + }) + }); + }); + + audioPlayer.addEventListener('loadedmetadata', function() { + console.log('DJ System: Metadata loaded, duration:', audioPlayer.duration); + }); + + audioPlayer.addEventListener('timeupdate', function() { + // Optional: Send progress updates + if (isPlaying && audioPlayer.duration) { + const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100; + // You can use this for progress bars if needed + } + }); +} + +// Main message handler from FiveM +window.addEventListener('message', function(event) { + const data = event.data; + + switch(data.type) { + case 'playMusic': + playMusic(data.url, data.volume, data.title); + break; + case 'stopMusic': + stopMusic(); + break; + case 'setVolume': + setVolume(data.volume); + break; + case 'fadeOut': + fadeOut(); + break; + case 'fadeIn': + fadeIn(); + break; + case 'pauseMusic': + pauseMusic(); + break; + case 'resumeMusic': + resumeMusic(); + break; + } +}); + +// Play music function with YouTube support +async function playMusic(url, volume, title = 'Unknown') { + if (!audioPlayer) { + console.error('DJ System: Audio player not initialized'); + return; + } + + try { + // Stop current music first + stopMusic(); + + console.log('DJ System: Attempting to play:', title, url); + + // Set volume + currentVolume = volume || 50; + audioPlayer.volume = currentVolume / 100; + + // Handle different URL types + let playableUrl = await processUrl(url); + + if (!playableUrl) { + console.error('DJ System: Could not process URL:', url); + notifyError('Could not process audio URL'); + return; + } + + // Set source and play + audioPlayer.src = playableUrl; + audioPlayer.load(); + + // Store current song info + currentSong = { + title: title, + url: url, + playableUrl: playableUrl + }; + + // Attempt to play + const playPromise = audioPlayer.play(); + + if (playPromise !== undefined) { + playPromise + .then(() => { + console.log('DJ System: Successfully started playing:', title); + isPlaying = true; + }) + .catch(error => { + console.error('DJ System: Play failed:', error); + notifyError('Playback failed: ' + error.message); + }); + } + + } catch (error) { + console.error('DJ System: Error in playMusic:', error); + notifyError('Error playing music: ' + error.message); + } +} + +// Process different URL types +async function processUrl(url) { + try { + // Check if it's a YouTube URL + if (isYouTubeUrl(url)) { + console.log('DJ System: Processing YouTube URL'); + return await convertYouTubeUrl(url); + } + + // Check if it's a direct audio URL + if (isDirectAudioUrl(url)) { + console.log('DJ System: Direct audio URL detected'); + return url; + } + + // Try to use URL as-is (might be pre-converted) + console.log('DJ System: Using URL as-is'); + return url; + + } catch (error) { + console.error('DJ System: Error processing URL:', error); + return null; + } +} + +// Check if URL is YouTube +function isYouTubeUrl(url) { + const youtubePatterns = [ + /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, + /youtube\.com\/watch\?.*v=([^&\n?#]+)/ + ]; + + return youtubePatterns.some(pattern => pattern.test(url)); +} + +// Check if URL is direct audio +function isDirectAudioUrl(url) { + const audioExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac']; + const lowerUrl = url.toLowerCase(); + + return audioExtensions.some(ext => lowerUrl.includes(ext)) || + lowerUrl.includes('audio/') || + lowerUrl.includes('stream'); +} + +// Convert YouTube URL (this would be handled server-side in real implementation) +async function convertYouTubeUrl(url) { + try { + // Extract video ID + const videoId = extractYouTubeVideoId(url); + if (!videoId) { + throw new Error('Could not extract YouTube video ID'); + } + + console.log('DJ System: YouTube Video ID:', videoId); + + // In a real implementation, this would call your server-side converter + // For now, we'll return the original URL and let the server handle it + return url; + + } catch (error) { + console.error('DJ System: YouTube conversion error:', error); + return null; + } +} + +// Extract YouTube video ID +function extractYouTubeVideoId(url) { + const patterns = [ + /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, + /youtube\.com\/watch\?.*v=([^&\n?#]+)/ + ]; + + for (let pattern of patterns) { + const match = url.match(pattern); + if (match && match[1]) { + return match[1]; + } + } + + return null; +} + +// Stop music +function stopMusic() { + if (!audioPlayer) return; + + try { + audioPlayer.pause(); + audioPlayer.currentTime = 0; + audioPlayer.src = ''; + isPlaying = false; + currentSong = null; + + // Clear any fade effects + if (fadeInterval) { + clearInterval(fadeInterval); + fadeInterval = null; + } + + console.log('DJ System: Music stopped'); + + } catch (error) { + console.error('DJ System: Error stopping music:', error); + } +} + +// Pause music +function pauseMusic() { + if (!audioPlayer || !isPlaying) return; + + try { + audioPlayer.pause(); + isPlaying = false; + console.log('DJ System: Music paused'); + } catch (error) { + console.error('DJ System: Error pausing music:', error); + } +} + +// Resume music +function resumeMusic() { + if (!audioPlayer || isPlaying) return; + + try { + const playPromise = audioPlayer.play(); + if (playPromise !== undefined) { + playPromise + .then(() => { + isPlaying = true; + console.log('DJ System: Music resumed'); + }) + .catch(error => { + console.error('DJ System: Resume failed:', error); + }); + } + } catch (error) { + console.error('DJ System: Error resuming music:', error); + } +} + +// Set volume +function setVolume(volume) { + if (!audioPlayer) return; + + try { + currentVolume = Math.max(0, Math.min(100, volume)); + audioPlayer.volume = currentVolume / 100; + console.log('DJ System: Volume set to', currentVolume + '%'); + } catch (error) { + console.error('DJ System: Error setting volume:', error); + } +} + +// Fade out effect (when player moves away) +function fadeOut() { + if (!audioPlayer || !isPlaying) return; + + if (fadeInterval) { + clearInterval(fadeInterval); + } + + let currentVol = audioPlayer.volume; + const targetVol = 0; + const fadeStep = 0.05; + + fadeInterval = setInterval(() => { + currentVol -= fadeStep; + if (currentVol <= targetVol) { + currentVol = targetVol; + audioPlayer.volume = currentVol; + clearInterval(fadeInterval); + fadeInterval = null; + } else { + audioPlayer.volume = currentVol; + } + }, 50); +} + +// Fade in effect (when player moves closer) +function fadeIn() { + if (!audioPlayer || !isPlaying) return; + + if (fadeInterval) { + clearInterval(fadeInterval); + } + + let currentVol = audioPlayer.volume; + const targetVol = currentVolume / 100; + const fadeStep = 0.05; + + fadeInterval = setInterval(() => { + currentVol += fadeStep; + if (currentVol >= targetVol) { + currentVol = targetVol; + audioPlayer.volume = currentVol; + clearInterval(fadeInterval); + fadeInterval = null; + } else { + audioPlayer.volume = currentVol; + } + }, 50); +} + +// Notify FiveM about errors +function notifyError(message) { + fetch(`https://${GetParentResourceName()}/audioError`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify({ + error: message + }) + }).catch(err => { + console.error('DJ System: Failed to notify error:', err); + }); +} + +// Get current resource name +function GetParentResourceName() { + return window.location.hostname; +} + +// Debug functions (can be called from browser console) +window.djDebug = { + getCurrentSong: () => currentSong, + getVolume: () => currentVolume, + isPlaying: () => isPlaying, + getAudioPlayer: () => audioPlayer, + testPlay: (url) => playMusic(url, 50, 'Test Song'), + testStop: () => stopMusic(), + testVolume: (vol) => setVolume(vol) +}; + +// Log when script is loaded +console.log('DJ System: Script loaded and ready'); diff --git a/resources/[tools]/nordi_dj/html/style.css b/resources/[tools]/nordi_dj/html/style.css new file mode 100644 index 000000000..fbc090dd9 --- /dev/null +++ b/resources/[tools]/nordi_dj/html/style.css @@ -0,0 +1,72 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: transparent; + font-family: 'Arial', sans-serif; + overflow: hidden; +} + +#music-player { + position: absolute; + top: -9999px; + left: -9999px; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +#audio-player { + width: 100%; + height: auto; +} + +/* Optional progress bar styles */ +#progress-container { + width: 300px; + height: 4px; + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; + overflow: hidden; +} + +#progress-bar { + height: 100%; + background: linear-gradient(90deg, #ff6b6b, #4ecdc4); + width: 0%; + transition: width 0.1s ease; +} + +/* Optional song info styles */ +#song-info { + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 10px; + border-radius: 5px; + font-size: 12px; +} + +#song-title { + font-weight: bold; + margin-right: 10px; +} + +#song-time { + opacity: 0.7; +} + +/* Responsive design */ +@media (max-width: 768px) { + #progress-container { + width: 250px; + } + + #song-info { + font-size: 11px; + padding: 8px; + } +} diff --git a/resources/[tools]/nordi_dj/server/main.lua b/resources/[tools]/nordi_dj/server/main.lua new file mode 100644 index 000000000..6fac67443 --- /dev/null +++ b/resources/[tools]/nordi_dj/server/main.lua @@ -0,0 +1,263 @@ +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 + +-- Database Setup +CreateThread(function() + MySQL.query([[ + CREATE TABLE IF NOT EXISTS dj_playlists ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + owner VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ]]) + + MySQL.query([[ + CREATE TABLE IF NOT EXISTS dj_playlist_songs ( + id INT AUTO_INCREMENT PRIMARY KEY, + playlist_id INT NOT NULL, + title VARCHAR(255) NOT NULL, + url TEXT NOT NULL, + converted_url TEXT NULL, + position INT DEFAULT 0, + FOREIGN KEY (playlist_id) REFERENCES dj_playlists(id) ON DELETE CASCADE + ) + ]]) + + MySQL.query([[ + CREATE TABLE IF NOT EXISTS dj_url_cache ( + id INT AUTO_INCREMENT PRIMARY KEY, + original_url VARCHAR(500) NOT NULL UNIQUE, + converted_url TEXT NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ]]) +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)