diff --git a/README.md b/README.md index 4304eaa..9a7ba36 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,9 @@ MusicTracks = { The standalone `Music` app plays audio only inside the current player's NUI. It never creates a world sound, voice channel, positional event, or 3D-audio state that another player can hear. -Server-owned MP3/OGG tracks live in `frontend/public/music`. Define their stable ID, title, artist, -file, and optional artwork in `sky_phone/config/music.lua`, then run `build_frontend.bat`: +Server-owned MP3/OGG tracks and their optional artwork live directly in +`sky_phone/config/music`. Define their stable ID, title, artist, and paths in +`sky_phone/config/music.lua`: ```lua Tracks = { @@ -34,12 +35,16 @@ Tracks = { Id = "night-drive", Title = "Night Drive", Artist = "Sky Records", - File = "music/night-drive.ogg", - Artwork = "music/night-drive.webp", + File = "config/music/night-drive.ogg", + Artwork = "config/music/night-drive.webp", }, } ``` +No frontend build is needed when tracks change. Restart the resource so FiveM republishes the +files and reloads the track configuration. Keep existing track IDs stable because playlists store +those IDs. + Players can add public YouTube video links to their own library. Metadata is requested through YouTube's oEmbed endpoint on the server, while playback uses the embedded YouTube player only on that player's NUI. Personal songs and playlists are stored per linked iFruit account, or per phone diff --git a/frontend/public/music/README.txt b/frontend/public/music/README.txt deleted file mode 100644 index c535f41..0000000 --- a/frontend/public/music/README.txt +++ /dev/null @@ -1,6 +0,0 @@ -Place server-owned .mp3 and .ogg files in this directory. - -Define each published song in sky_phone/config/music.lua, then run -build_frontend.bat so the files are copied into the deployable NUI. - -Optional artwork can be stored here as .png, .jpg, .jpeg or .webp. diff --git a/sky_phone/config/music.lua b/sky_phone/config/music.lua index de3814a..9756d80 100644 --- a/sky_phone/config/music.lua +++ b/sky_phone/config/music.lua @@ -1,13 +1,13 @@ Config.Music = { - -- Put MP3/OGG files in frontend/public/music and rebuild the frontend. - -- File and Artwork paths are relative to the published NUI root. + -- Put server-owned audio and artwork directly in config/music. + -- Paths must stay inside that directory. Restart the resource after changes. Tracks = { -- { -- Id = "night-drive", -- Title = "Night Drive", -- Artist = "Sky Records", - -- File = "music/night-drive.ogg", - -- Artwork = "music/night-drive.webp", + -- File = "config/music/night-drive.ogg", + -- Artwork = "config/music/night-drive.webp", -- }, }, diff --git a/sky_phone/config/music/README.txt b/sky_phone/config/music/README.txt new file mode 100644 index 0000000..f7b1a83 --- /dev/null +++ b/sky_phone/config/music/README.txt @@ -0,0 +1,11 @@ +Place server-owned .mp3 and .ogg files in this directory. + +Optional artwork can be stored here as .png, .jpg, .jpeg or .webp. Register +each track in ../music.lua using paths such as: + + File = "config/music/night-drive.ogg" + Artwork = "config/music/night-drive.webp" + +Use file names containing only letters, numbers, dots, underscores or hyphens. +Subdirectories are supported. Restart the sky_phone resource after changing +tracks or files; a frontend build is not required. diff --git a/sky_phone/fxmanifest.lua b/sky_phone/fxmanifest.lua index 13891ac..0d1b018 100644 --- a/sky_phone/fxmanifest.lua +++ b/sky_phone/fxmanifest.lua @@ -71,7 +71,7 @@ files { 'source/html/index.html', 'source/html/assets/**', 'source/html/img/**', - 'source/html/music/**', + 'config/music/**', } ui_page 'source/html/index.html' diff --git a/sky_phone/source/server/music.lua b/sky_phone/source/server/music.lua index f3d126a..4899c4c 100644 --- a/sky_phone/source/server/music.lua +++ b/sky_phone/source/server/music.lua @@ -1,6 +1,9 @@ Bridge.Database.AfterMigration("sky_phone", function() local server_tracks = {} local server_tracks_by_id = {} +local music_asset_prefix = "config/music/" +local audio_extensions = { mp3 = true, ogg = true } +local artwork_extensions = { jpeg = true, jpg = true, png = true, webp = true } local function trim(value) return type(value) == "string" and value:match("^%s*(.-)%s*$") or nil @@ -24,6 +27,29 @@ local function optional_text(value) return normalized ~= "" and normalized or nil end +local function normalize_music_asset_path(value, allowed_extensions) + local path = trim(value) + if not path + or path:sub(1, #music_asset_prefix) ~= music_asset_prefix + or not path:match("^[%w%._%-%/]+$") + or path:find("..", 1, true) + or path:find("//", 1, true) + or path:find("\\", 1, true) + then + return nil + end + + local extension = path:match("%.([%w]+)$") + if not extension or not allowed_extensions[extension:lower()] then + return nil + end + return path +end + +local function music_asset_url(path) + return ("https://cfx-nui-%s/%s"):format(GetCurrentResourceName(), path) +end + local function affected_rows(result) if type(result) == "number" then return result @@ -45,25 +71,25 @@ local function normalize_server_tracks() local id = trim(configured.Id) local title = trim(configured.Title) local artist = trim(configured.Artist) - local file = trim(configured.File) - local artwork = trim(configured.Artwork) - local valid_file = file - and file:match("^music/[%w%._%-%/]+%.[mM][pP]3$") - or file and file:match("^music/[%w%._%-%/]+%.[oO][gG][gG]$") + local title_length = text_length(title) + local artist_length = text_length(artist) + local file = normalize_music_asset_path(configured.File, audio_extensions) + local configured_artwork = optional_text(configured.Artwork) + local artwork = configured_artwork + and normalize_music_asset_path(configured_artwork, artwork_extensions) + or nil if not id or not id:match("^[%w%-_]+$") or #id > 48 - or not title - or not text_length(title) - or text_length(title) > 160 - or not artist - or not text_length(artist) - or text_length(artist) > 120 - or not valid_file - or (artwork and not artwork:match("^music/[%w%._%-%/]+%.[pP][nN][gG]$") - and not artwork:match("^music/[%w%._%-%/]+%.[jJ][pP][eE]?[gG]$") - and not artwork:match("^music/[%w%._%-%/]+%.[wW][eE][bB][pP]$")) + or not title_length + or title_length < 1 + or title_length > 160 + or not artist_length + or artist_length < 1 + or artist_length > 120 + or not file + or (configured_artwork and not artwork) or server_tracks_by_id[id] then Bridge.Debug( @@ -78,8 +104,8 @@ local function normalize_server_tracks() source = "server", title = title, artist = artist, - url = file, - artwork = artwork, + url = music_asset_url(file), + artwork = artwork and music_asset_url(artwork) or nil, } server_tracks[#server_tracks + 1] = track server_tracks_by_id[id] = track