1
0
Fork 0
forked from Simnation/Main
This commit is contained in:
Nordi98 2025-08-03 16:51:12 +02:00
parent 0e29315dd1
commit e0a3b21c61
7 changed files with 1463 additions and 0 deletions

View file

@ -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)

View file

@ -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"}
}
}
}

View file

@ -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'
}

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DJ System</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="music-player">
<audio
id="audio-player"
preload="auto"
crossorigin="anonymous"
controls="false"
style="display: none;">
Dein Browser unterstützt das Audio-Element nicht.
</audio>
<!-- Optional: Progress indicator (hidden by default) -->
<div id="progress-container" style="display: none;">
<div id="progress-bar"></div>
</div>
<!-- Optional: Song info display (hidden by default) -->
<div id="song-info" style="display: none;">
<span id="song-title"></span>
<span id="song-time"></span>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

View file

@ -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');

View file

@ -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;
}
}

View file

@ -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)