forked from Simnation/Main
413 lines
15 KiB
Lua
413 lines
15 KiB
Lua
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)
|
|
|
|
-- Füge diese Funktionen zu deiner server/main.lua hinzu:
|
|
|
|
local function extractYouTubeVideoId(url)
|
|
-- Verschiedene YouTube URL Formate unterstützen
|
|
local patterns = {
|
|
"youtube%.com/watch%?v=([^&]+)",
|
|
"youtu%.be/([^?]+)",
|
|
"youtube%.com/embed/([^?]+)",
|
|
"youtube%.com/v/([^?]+)"
|
|
}
|
|
|
|
for _, pattern in ipairs(patterns) do
|
|
local videoId = string.match(url, pattern)
|
|
if videoId then
|
|
return videoId
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function cleanYouTubeUrl(url)
|
|
local videoId = extractYouTubeVideoId(url)
|
|
if not videoId then
|
|
return nil
|
|
end
|
|
|
|
-- Erstelle saubere YouTube URL ohne Playlist-Parameter
|
|
return "https://www.youtube.com/watch?v=" .. videoId
|
|
end
|
|
|
|
-- YouTube zu MP3 Konvertierung (mit yt-dlp oder youtube-dl)
|
|
local function convertYouTubeUrl(url, callback)
|
|
local videoId = extractYouTubeVideoId(url)
|
|
if not videoId then
|
|
callback(nil, "Ungültige YouTube URL")
|
|
return
|
|
end
|
|
|
|
-- Prüfe Cache zuerst
|
|
MySQL.Async.fetchScalar('SELECT converted_url FROM dj_url_cache WHERE original_url = ? AND expires_at > NOW()', {url}, function(cachedUrl)
|
|
if cachedUrl then
|
|
print('[DJ System] URL aus Cache geladen: ' .. videoId)
|
|
callback(cachedUrl, nil)
|
|
|
|
-- Update hit count
|
|
MySQL.Async.execute('UPDATE dj_url_cache SET hit_count = hit_count + 1, last_accessed = NOW() WHERE original_url = ?', {url})
|
|
return
|
|
end
|
|
|
|
-- Konvertiere mit yt-dlp (externe Lösung)
|
|
convertWithYtDlp(url, videoId, callback)
|
|
end)
|
|
end
|
|
|
|
-- Konvertierung mit yt-dlp (benötigt yt-dlp Installation)
|
|
local function convertWithYtDlp(originalUrl, videoId, callback)
|
|
local cleanUrl = "https://www.youtube.com/watch?v=" .. videoId
|
|
|
|
-- yt-dlp Befehl ausführen
|
|
local command = string.format('yt-dlp --get-url --format "bestaudio[ext=m4a]/best[ext=mp4]/best" "%s"', cleanUrl)
|
|
|
|
-- Führe Befehl aus (das ist ein vereinfachtes Beispiel)
|
|
-- In der Praxis würdest du einen HTTP Request an einen externen Service machen
|
|
|
|
-- Für jetzt: Verwende eine alternative Lösung
|
|
useAlternativeConversion(originalUrl, videoId, callback)
|
|
end
|
|
|
|
-- Alternative: Verwende einen Online-Service
|
|
local function useAlternativeConversion(originalUrl, videoId, callback)
|
|
-- Option 1: Verwende YouTube Embed URL (funktioniert manchmal)
|
|
local embedUrl = "https://www.youtube.com/embed/" .. videoId .. "?autoplay=1"
|
|
|
|
-- Option 2: Verwende einen kostenlosen Konvertierungsservice
|
|
-- ACHTUNG: Diese Services können instabil sein!
|
|
local convertedUrl = "https://youtube-mp3-download.com/api/convert/" .. videoId
|
|
|
|
-- Option 3: Verwende die originale URL und lass den Client damit umgehen
|
|
local fallbackUrl = "https://www.youtube.com/watch?v=" .. videoId
|
|
|
|
-- Speichere im Cache (24 Stunden gültig)
|
|
local expiresAt = os.date('%Y-%m-%d %H:%M:%S', os.time() + 86400)
|
|
|
|
MySQL.Async.execute('INSERT INTO dj_url_cache (original_url, converted_url, video_id, expires_at) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE converted_url = VALUES(converted_url), expires_at = VALUES(expires_at)', {
|
|
originalUrl,
|
|
fallbackUrl, -- Verwende fallback URL
|
|
videoId,
|
|
expiresAt
|
|
}, function(affectedRows)
|
|
if affectedRows > 0 then
|
|
print('[DJ System] URL in Cache gespeichert: ' .. videoId)
|
|
end
|
|
|
|
callback(fallbackUrl, nil)
|
|
end)
|
|
end
|
|
|
|
-- Aktualisiere die PlayMusic Funktion
|
|
RegisterServerEvent('dj:playMusic')
|
|
AddEventHandler('dj:playMusic', function(title, url, volume)
|
|
local src = source
|
|
local Player = QBCore.Functions.GetPlayer(src)
|
|
if not Player then return end
|
|
|
|
print('[DJ System] Spiele Musik ab: ' .. title .. ' | URL: ' .. url)
|
|
|
|
-- Prüfe ob es eine YouTube URL ist
|
|
if string.match(url, "youtube%.com") or string.match(url, "youtu%.be") then
|
|
convertYouTubeUrl(url, function(convertedUrl, error)
|
|
if error then
|
|
TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Konvertieren: ' .. error, 'error')
|
|
return
|
|
end
|
|
|
|
if convertedUrl then
|
|
-- Sende konvertierte URL an alle Clients
|
|
TriggerClientEvent('dj:playMusicClient', -1, title, convertedUrl, volume)
|
|
|
|
-- Speichere in Session History
|
|
MySQL.Async.execute('INSERT INTO dj_session_history (dj_citizenid, dj_name, booth_name, song_title, song_url, song_type, volume, session_start) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', {
|
|
Player.PlayerData.citizenid,
|
|
Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname,
|
|
'Unknown', -- Du kannst hier den Booth-Namen hinzufügen
|
|
title,
|
|
url,
|
|
'youtube',
|
|
volume
|
|
})
|
|
else
|
|
TriggerClientEvent('QBCore:Notify', src, 'Konvertierung fehlgeschlagen', 'error')
|
|
end
|
|
end)
|
|
else
|
|
-- Direkte Audio URL
|
|
TriggerClientEvent('dj:playMusicClient', -1, title, url, volume)
|
|
|
|
-- Speichere in Session History
|
|
MySQL.Async.execute('INSERT INTO dj_session_history (dj_citizenid, dj_name, booth_name, song_title, song_url, song_type, volume, session_start) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', {
|
|
Player.PlayerData.citizenid,
|
|
Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname,
|
|
'Unknown',
|
|
title,
|
|
url,
|
|
'direct',
|
|
volume
|
|
})
|
|
end
|
|
end)
|