forked from Simnation/Main
ed
This commit is contained in:
parent
0e29315dd1
commit
e0a3b21c61
7 changed files with 1463 additions and 0 deletions
587
resources/[tools]/nordi_dj/client/main.lua
Normal file
587
resources/[tools]/nordi_dj/client/main.lua
Normal 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)
|
72
resources/[tools]/nordi_dj/config.lua
Normal file
72
resources/[tools]/nordi_dj/config.lua
Normal 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"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
resources/[tools]/nordi_dj/fxmanifest.lua
Normal file
34
resources/[tools]/nordi_dj/fxmanifest.lua
Normal 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'
|
||||||
|
}
|
34
resources/[tools]/nordi_dj/html/index.html
Normal file
34
resources/[tools]/nordi_dj/html/index.html
Normal 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>
|
401
resources/[tools]/nordi_dj/html/script.js
Normal file
401
resources/[tools]/nordi_dj/html/script.js
Normal 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');
|
72
resources/[tools]/nordi_dj/html/style.css
Normal file
72
resources/[tools]/nordi_dj/html/style.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
263
resources/[tools]/nordi_dj/server/main.lua
Normal file
263
resources/[tools]/nordi_dj/server/main.lua
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue