From 1bba340cf8d16c38fe3d46d046c7acb0e1fe2e07 Mon Sep 17 00:00:00 2001 From: Quentin Bray Date: Tue, 23 Jun 2026 21:04:59 +0200 Subject: [PATCH] Better handle multiple tags respecting US format (#878) * Better handle multiple tags respecting US format Based on unversioned, V1 & V2 of US formats : https://github.com/UltraStar-Deluxe/format Should address #623 Creator now takes multiple values Maintain backward compatibility * Update file path properties to match US formats --- Vocaluxe/Base/CCover.cs | 4 +- Vocaluxe/Base/CSongFilter.cs | 54 ++++---------- Vocaluxe/Base/Server/CVocaluxeServer.cs | 4 +- Vocaluxe/Screens/CScreenSing.cs | 4 +- VocaluxeLib/Menu/SongMenu/CSongMenuList.cs | 2 +- .../Menu/SongMenu/CSongMenuTileBoard.cs | 2 +- VocaluxeLib/Songs/CSong.cs | 72 +++++++++---------- VocaluxeLib/Songs/CSongLoader.cs | 70 ++++++------------ VocaluxeLib/Songs/CSongWriter.cs | 39 +++++----- VocaluxeLib/Utils/Player/CSongPlayer.cs | 6 +- 10 files changed, 97 insertions(+), 160 deletions(-) diff --git a/Vocaluxe/Base/CCover.cs b/Vocaluxe/Base/CCover.cs index 8c4a1993..7514814c 100644 --- a/Vocaluxe/Base/CCover.cs +++ b/Vocaluxe/Base/CCover.cs @@ -108,11 +108,11 @@ namespace Vocaluxe.Base { _CancelToken.Token.ThrowIfCancellationRequested(); var coverBmp = !_CoverGenerators.ContainsKey(type) - ? null : _CoverGenerators[type].GetCover(text, firstSong != null ? Path.Combine(firstSong.Folder, firstSong.CoverFileName) : null); + ? null : _CoverGenerators[type].GetCover(text, firstSong != null ? Path.Combine(firstSong.Folder, firstSong.Cover) : null); _CancelToken.Token.ThrowIfCancellationRequested(); if (coverBmp == null && _CoverGenerators.ContainsKey(ECoverGeneratorType.Default)) { - coverBmp = _CoverGenerators[ECoverGeneratorType.Default].GetCover(text, firstSong != null ? Path.Combine(firstSong.Folder, firstSong.CoverFileName) : null); + coverBmp = _CoverGenerators[ECoverGeneratorType.Default].GetCover(text, firstSong != null ? Path.Combine(firstSong.Folder, firstSong.Cover) : null); } _CancelToken.Token.ThrowIfCancellationRequested(); diff --git a/Vocaluxe/Base/CSongFilter.cs b/Vocaluxe/Base/CSongFilter.cs index 033a5389..d36914d1 100644 --- a/Vocaluxe/Base/CSongFilter.cs +++ b/Vocaluxe/Base/CSongFilter.cs @@ -104,7 +104,7 @@ namespace Vocaluxe.Base String searchForLanguage = null; // l: String searchForCreator = null; // c: String searchForEdition = null; // e: - String searchForTags = null; // #: + String searchForTag = null; // #: String searchForAlbum = null; // al: String searchForFileName = null; // fi: String searchForFolderName = null; // fo: @@ -146,7 +146,7 @@ namespace Vocaluxe.Base searchForEdition = searchToken.Substring(2); break; case "#:": - searchForTags = searchToken.Substring(2); + searchForTag = searchToken.Substring(2); break; default: foundIt = false; @@ -230,11 +230,6 @@ namespace Vocaluxe.Base _FilteredSongs.Add(song); } - if (searchForCreator != null && song.Creator.ToUpper().Contains(searchForCreator)) - { - _FilteredSongs.Add(song); - } - if (searchForFileName != null && song.FileName.ToUpper().Contains(searchForFileName)) { _FilteredSongs.Add(song); @@ -245,48 +240,29 @@ namespace Vocaluxe.Base _FilteredSongs.Add(song); } - if (searchForGenre != null) + if (searchForCreator != null && song.Creators.Any(t => t.ToUpper().Contains(searchForCreator))) { - foreach (var genre in song.Genres) - { - if (genre.ToUpper().Contains(searchForGenre)) - { - _FilteredSongs.Add(song); - } - } + _FilteredSongs.Add(song); } - if (searchForLanguage != null) + if (searchForGenre != null && song.Genres.Any(t => t.ToUpper().Contains(searchForGenre))) { - foreach (var language in song.Languages) - { - if (language.ToUpper().Contains(searchForLanguage)) - { - _FilteredSongs.Add(song); - } - } + _FilteredSongs.Add(song); } - if (searchForEdition != null) + if (searchForLanguage != null && song.Languages.Any(t => t.ToUpper().Contains(searchForLanguage))) { - foreach (var edition in song.Editions) - { - if (edition.ToUpper().Contains(searchForEdition)) - { - _FilteredSongs.Add(song); - } - } + _FilteredSongs.Add(song); } - if (searchForTags != null) + if (searchForEdition != null && song.Editions.Any(t => t.ToUpper().Contains(searchForEdition))) { - foreach (var tag in song.Tags) - { - if (tag.ToUpper().Contains(searchForTags)) - { - _FilteredSongs.Add(song); - } - } + _FilteredSongs.Add(song); + } + + if (searchForTag != null && song.Tags.Any(t => t.ToUpper().Contains(searchForTag))) + { + _FilteredSongs.Add(song); } if (searchForYear != null) diff --git a/Vocaluxe/Base/Server/CVocaluxeServer.cs b/Vocaluxe/Base/Server/CVocaluxeServer.cs index fa304cc7..992c0a86 100644 --- a/Vocaluxe/Base/Server/CVocaluxeServer.cs +++ b/Vocaluxe/Base/Server/CVocaluxeServer.cs @@ -707,13 +707,13 @@ namespace Vocaluxe.Base.Server result.SongId = song.Id; if (includeCover) { - if (song.CoverFileName == "") + if (song.Cover == "") { result.Cover = new CBase64Image(_CreateDelayedImage("Website\\img\\noCover.png")); } else { - result.Cover = new CBase64Image(_CreateDelayedImage(song.Folder + "\\" + song.CoverFileName)); + result.Cover = new CBase64Image(_CreateDelayedImage(song.Folder + "\\" + song.Cover)); } } } diff --git a/Vocaluxe/Screens/CScreenSing.cs b/Vocaluxe/Screens/CScreenSing.cs index 40acac6e..93c02510 100644 --- a/Vocaluxe/Screens/CScreenSing.cs +++ b/Vocaluxe/Screens/CScreenSing.cs @@ -1591,9 +1591,9 @@ namespace Vocaluxe.Screens } _VideoAspect = song.VideoAspect; - if (!string.IsNullOrEmpty(song.VideoFileName)) + if (!string.IsNullOrEmpty(song.Video)) { - _CurrentVideo = CVideo.Load(Path.Combine(song.Folder, song.VideoFileName)); + _CurrentVideo = CVideo.Load(Path.Combine(song.Folder, song.Video)); CVideo.Skip(_CurrentVideo, song.Start, song.VideoGap); } diff --git a/VocaluxeLib/Menu/SongMenu/CSongMenuList.cs b/VocaluxeLib/Menu/SongMenu/CSongMenuList.cs index 299946a9..5aae9e87 100644 --- a/VocaluxeLib/Menu/SongMenu/CSongMenuList.cs +++ b/VocaluxeLib/Menu/SongMenu/CSongMenuList.cs @@ -305,7 +305,7 @@ namespace VocaluxeLib.Menu.SongMenu _SongYear.Text = song.Year; _DuetIcon.Visible = song.IsDuet; _RapIcon.Visible = song.IsRap; - _VideoIcon.Visible = song.VideoFileName != ""; + _VideoIcon.Visible = song.Video != ""; _MedleyCalcIcon.Visible = song.Medley.Source == EDataSource.Calculated; _MedleyTagIcon.Visible = song.Medley.Source == EDataSource.Tag; _InstrumentalIcon.Visible = song.HasInstrumental(); diff --git a/VocaluxeLib/Menu/SongMenu/CSongMenuTileBoard.cs b/VocaluxeLib/Menu/SongMenu/CSongMenuTileBoard.cs index e28e7831..4ea5c755 100644 --- a/VocaluxeLib/Menu/SongMenu/CSongMenuTileBoard.cs +++ b/VocaluxeLib/Menu/SongMenu/CSongMenuTileBoard.cs @@ -279,7 +279,7 @@ namespace VocaluxeLib.Menu.SongMenu _SongYear.Text = song.Year; _DuetIcon.Visible = song.IsDuet; _RapIcon.Visible = song.IsRap; - _VideoIcon.Visible = song.VideoFileName != ""; + _VideoIcon.Visible = song.Video != ""; _MedleyCalcIcon.Visible = song.Medley.Source == EDataSource.Calculated; _MedleyTagIcon.Visible = song.Medley.Source == EDataSource.Tag; _InstrumentalIcon.Visible = song.HasInstrumental(); diff --git a/VocaluxeLib/Songs/CSong.cs b/VocaluxeLib/Songs/CSong.cs index 4190cd54..3ec56e6a 100644 --- a/VocaluxeLib/Songs/CSong.cs +++ b/VocaluxeLib/Songs/CSong.cs @@ -98,12 +98,12 @@ namespace VocaluxeLib.Songs public string FileName = string.Empty; public bool Relative; - public string MP3FileName = string.Empty; - public string InstrumentalFileName = string.Empty; - public string VocalsFileName = string.Empty; - public string CoverFileName = string.Empty; - public readonly List BackgroundFileNames = new List(); - public string VideoFileName = string.Empty; + public string Audio = string.Empty; + public string Instrumental = string.Empty; + public string Vocals = string.Empty; + public string Cover = string.Empty; + public readonly List BackgroundFileNames = new(); + public string Video = string.Empty; public EAspect VideoAspect = EAspect.Automatic; @@ -120,14 +120,11 @@ namespace VocaluxeLib.Songs return _CoverTextureSmall; } - - set { _CoverTextureSmall = value; } } public CTextureRef CoverTextureBig { - get { return _CoverTextureBig ?? _CoverTextureSmall; } - set { _CoverTextureBig = value; } + get => _CoverTextureBig ?? _CoverTextureSmall; } public string Title = string.Empty; @@ -136,11 +133,10 @@ namespace VocaluxeLib.Songs public string TitleSorting = string.Empty; public string ArtistSorting = string.Empty; - public string Creator = ""; public string Version = ""; public string Length = ""; //Length set in song file, SHOULD match actual song length but is more a hint public string Source = ""; - public readonly List UnknownTags = new List(); + public readonly List UnknownTags = new(); /// /// Start of the song in s (s in txt) @@ -168,27 +164,24 @@ namespace VocaluxeLib.Songs private readonly bool _Visible = true; private readonly int _CatIndex = -1; private readonly bool _Selected; - public bool IsDuet - { - get { return Notes.VoiceCount > 1; } - } + public bool IsDuet => Notes.VoiceCount > 1; public bool IsRap = false; - public readonly List Editions = new List(); - public readonly List Genres = new List(); - public readonly List Tags = new List(); + public readonly List Creators = new(); + public readonly List Editions = new(); + public readonly List Genres = new(); + public readonly List Tags = new(); + public readonly List Languages = new(); public string Album = ""; public string Year = ""; - public readonly List Languages = new List(); - public int DataBaseSongId = -1; public DateTime DateAdded = DateTime.Today; public int NumPlayed; public int NumPlayedSession; // Notes - public readonly CNotes Notes = new CNotes(); + public readonly CNotes Notes = new(); public IList AvailableGameModes { @@ -241,12 +234,12 @@ namespace VocaluxeLib.Songs FileName = song.FileName; Relative = song.Relative; - MP3FileName = song.MP3FileName; - InstrumentalFileName = song.InstrumentalFileName; - VocalsFileName = song.VocalsFileName; - CoverFileName = song.CoverFileName; + Audio = song.Audio; + Instrumental = song.Instrumental; + Vocals = song.Vocals; + Cover = song.Cover; BackgroundFileNames = song.BackgroundFileNames; - VideoFileName = song.VideoFileName; + Video = song.Video; VideoAspect = song.VideoAspect; NotesLoaded = song.NotesLoaded; @@ -256,7 +249,6 @@ namespace VocaluxeLib.Songs ArtistSorting = song.ArtistSorting; TitleSorting = song.TitleSorting; - Creator = song.Creator; Version = song.Version; Length = song.Length; Source = song.Source; @@ -276,14 +268,14 @@ namespace VocaluxeLib.Songs _CatIndex = song._CatIndex; _Selected = song._Selected; + Creators = new List(song.Creators); Editions = new List(song.Editions); Genres = new List(song.Genres); Tags = new List(song.Tags); + Languages = new List(song.Languages); Album = song.Album; Year = song.Year; - Languages = new List(song.Languages); - DataBaseSongId = song.DataBaseSongId; DateAdded = song.DateAdded; NumPlayed = song.NumPlayed; @@ -318,32 +310,32 @@ namespace VocaluxeLib.Songs public string GetMP3() { - return Path.Combine(Folder, MP3FileName); + return Path.Combine(Folder, Audio); } public string GetInstrumental() { - return Path.Combine(Folder, InstrumentalFileName); + return Path.Combine(Folder, Instrumental); } public bool HasInstrumental() { - return !string.IsNullOrEmpty(InstrumentalFileName); + return !string.IsNullOrEmpty(Instrumental); } public string GetVocals() { - return Path.Combine(Folder, VocalsFileName); + return Path.Combine(Folder, Vocals); } public bool HasVocals() { - return !string.IsNullOrEmpty(VocalsFileName); + return !string.IsNullOrEmpty(Vocals); } public string GetVideo() { - return Path.Combine(Folder, VideoFileName); + return Path.Combine(Folder, Video); } public void LoadSmallCover() @@ -353,9 +345,9 @@ namespace VocaluxeLib.Songs return; } - if (CoverFileName != "") + if (Cover != "") { - if (CBase.DataBase.GetCover(Path.Combine(Folder, CoverFileName), ref _CoverTextureSmall, CBase.Config.GetCoverSize())) + if (CBase.DataBase.GetCover(Path.Combine(Folder, Cover), ref _CoverTextureSmall, CBase.Config.GetCoverSize())) { return; } @@ -366,7 +358,7 @@ namespace VocaluxeLib.Songs private void _CheckFiles() { - if (CoverFileName == "") + if (Cover == "") { var files = CHelper.ListImageFiles(Folder); foreach (var file in files) @@ -374,7 +366,7 @@ namespace VocaluxeLib.Songs if (file.ContainsIgnoreCase("[CO]") && (file.ContainsIgnoreCase(Title) || file.ContainsIgnoreCase(Artist))) { - CoverFileName = file; + Cover = file; } } } diff --git a/VocaluxeLib/Songs/CSongLoader.cs b/VocaluxeLib/Songs/CSongLoader.cs index bb1bd175..2735e823 100644 --- a/VocaluxeLib/Songs/CSongLoader.cs +++ b/VocaluxeLib/Songs/CSongLoader.cs @@ -16,6 +16,7 @@ #endregion using System; +using System.Collections.Generic; using System.IO; using System.Linq; using VocaluxeLib.Log; @@ -180,11 +181,6 @@ namespace VocaluxeLib.Songs case "ARTIST-ON-SORTING": _Song.ArtistSorting = value; break; - case "CREATOR": - case "AUTHOR": - case "AUTOR": - _Song.Creator = value; - break; case "VERSION": _Song.Version = value; break; @@ -200,7 +196,7 @@ namespace VocaluxeLib.Songs case "AUDIO": if (File.Exists(Path.Combine(_Song.Folder, value))) { - _Song.MP3FileName = value; + _Song.Audio = value; headerFlags |= EHeaderFlags.MP3; } else @@ -213,7 +209,7 @@ namespace VocaluxeLib.Songs case "INSTRUMENTAL": if (File.Exists(Path.Combine(_Song.Folder, value))) { - _Song.InstrumentalFileName = value; + _Song.Instrumental = value; headerFlags |= EHeaderFlags.Instrumental; } else @@ -227,7 +223,7 @@ namespace VocaluxeLib.Songs case "VOCALS": if (File.Exists(Path.Combine(_Song.Folder, value))) { - _Song.VocalsFileName = value; + _Song.Vocals = value; headerFlags |= EHeaderFlags.Vocals; } else @@ -249,39 +245,23 @@ namespace VocaluxeLib.Songs CLog.CSongLog.Warning("[{SongFileName}] Invalid BPM value: {Value}", CLog.Params(_Song.FileName, value)); } + break; + case "CREATOR": + case "AUTHOR": + case "AUTOR": + _Song.Creators.AddRange(_GetMultipleHeaderValues(value)); break; case "EDITION": - if (value.Length > 1) - { - _Song.Editions.Add(value); - } - else - { - CLog.CSongLog.Warning("[{SongFileName}] Invalid edition: {Value}", CLog.Params(_Song.FileName, value)); - } - + _Song.Editions.AddRange(_GetMultipleHeaderValues(value)); break; case "GENRE": - if (value.Length > 1) - { - _Song.Genres.Add(value); - } - else - { - CLog.CSongLog.Warning("[{SongFileName}] Invalid genre: {Value}", CLog.Params(_Song.FileName, value)); - } - + _Song.Genres.AddRange(_GetMultipleHeaderValues(value)); break; case "TAGS": - if (value.Length > 1) - { - _Song.Tags.AddRange(value.Split(',')); - } - else - { - CLog.CSongLog.Warning("[{SongFileName}] Invalid tags: {Value}", CLog.Params(_Song.FileName, value)); - } - + _Song.Tags.AddRange(_GetMultipleHeaderValues(value)); + break; + case "LANGUAGE": + _Song.Languages.AddRange(_GetMultipleHeaderValues(value).Select(_UnifyLanguage)); break; case "ALBUM": _Song.Album = value; @@ -297,17 +277,6 @@ namespace VocaluxeLib.Songs CLog.CSongLog.Warning("[{SongFileName}] Invalid year: {Value}", CLog.Params(_Song.FileName, value)); } - break; - case "LANGUAGE": - if (value.Length > 1) - { - _Song.Languages.Add(_UnifyLanguage(value)); - } - else - { - CLog.CSongLog.Warning("[{SongFileName}] Invalid language: {Value}", CLog.Params(_Song.FileName, value)); - } - break; case "COMMENT": if (!string.IsNullOrEmpty(_Song._Comment)) @@ -331,7 +300,7 @@ namespace VocaluxeLib.Songs case "COVER": if (File.Exists(Path.Combine(_Song.Folder, value))) { - _Song.CoverFileName = value; + _Song.Cover = value; } else { @@ -354,7 +323,7 @@ namespace VocaluxeLib.Songs case "VIDEO": if (File.Exists(Path.Combine(_Song.Folder, value))) { - _Song.VideoFileName = value; + _Song.Video = value; } else { @@ -597,6 +566,11 @@ namespace VocaluxeLib.Songs return true; } + private IEnumerable _GetMultipleHeaderValues(string value) + { + return value.Split(',').Select(v => v.Trim()).Where(v => !string.IsNullOrEmpty(v)); + } + private static string _UnifyLanguage(string lang) { if (lang != "") diff --git a/VocaluxeLib/Songs/CSongWriter.cs b/VocaluxeLib/Songs/CSongWriter.cs index df40b29a..2ec21099 100644 --- a/VocaluxeLib/Songs/CSongWriter.cs +++ b/VocaluxeLib/Songs/CSongWriter.cs @@ -66,17 +66,13 @@ namespace VocaluxeLib.Songs } } - private void _WriteHeaderEntrys(string id, ICollection value) + private void _WriteHeaderEntries(string id, ICollection value) { - if (value == null || value.Count <= 0) + if (value is not { Count: > 0 }) { return; } - - foreach (var val in value) - { - _WriteHeaderEntry(id, val); - } + _WriteHeaderEntry(id, string.Join(",", value)); } private void _WriteHeader() @@ -86,16 +82,10 @@ namespace VocaluxeLib.Songs _WriteHeaderEntry("ENCODING", _Song.Encoding.GetEncodingName()); } - _WriteHeaderEntry("CREATOR", _Song.Creator); _WriteHeaderEntry("VERSION", _Song.Version); _WriteHeaderEntry("LENGTH", _Song.Length); _WriteHeaderEntry("SOURCE", _Song.Source); - if (!string.IsNullOrEmpty(_Song._Comment)) - { - var comment = _Song._Comment.Replace("\r\n", "\n").Replace('\r', '\n'); - char[] splitChar = { '\n' }; - _WriteHeaderEntrys("COMMENT", comment.Split(splitChar)); - } + _WriteHeaderEntry("COMMENT", _Song._Comment); _WriteHeaderEntry("TITLE", _Song.Title); _WriteHeaderEntry("ARTIST", _Song.Artist); @@ -109,16 +99,21 @@ namespace VocaluxeLib.Songs _WriteHeaderEntry("ARTIST-ON-SORTING", _Song.ArtistSorting); } - _WriteHeaderEntrys("EDITION", _Song.Editions); - _WriteHeaderEntrys("GENRE", _Song.Genres); - _WriteHeaderEntry("TAGS", String.Join(",", _Song.Tags.ToArray())); - _WriteHeaderEntrys("LANGUAGE", _Song.Languages); + _WriteHeaderEntries("CREATOR", _Song.Creators); + _WriteHeaderEntries("EDITION", _Song.Editions); + _WriteHeaderEntries("GENRE", _Song.Genres); + _WriteHeaderEntries("TAGS", _Song.Tags); + _WriteHeaderEntries("LANGUAGE", _Song.Languages); _WriteHeaderEntry("ALBUM", _Song.Album); _WriteHeaderEntry("YEAR", _Song.Year); - _WriteHeaderEntry("MP3", _Song.MP3FileName); - _WriteHeaderEntry("COVER", _Song.CoverFileName); - _WriteHeaderEntrys("BACKGROUND", _Song.BackgroundFileNames); - _WriteHeaderEntry("VIDEO", _Song.VideoFileName); + _WriteHeaderEntry("MP3", _Song.Audio); + _WriteHeaderEntry("COVER", _Song.Cover); + // This is a custom behaviour, normally multiple backgrounds is not supported in official US format + foreach (var songBackgroundFileName in _Song.BackgroundFileNames) + { + _WriteHeaderEntry("BACKGROUND", songBackgroundFileName); + } + _WriteHeaderEntry("VIDEO", _Song.Video); _WriteHeaderEntry("VIDEOGAP", _Song.VideoGap); if (_Song.VideoAspect != EAspect.Crop) { diff --git a/VocaluxeLib/Utils/Player/CSongPlayer.cs b/VocaluxeLib/Utils/Player/CSongPlayer.cs index f5aee6bf..a2868b9e 100644 --- a/VocaluxeLib/Utils/Player/CSongPlayer.cs +++ b/VocaluxeLib/Utils/Player/CSongPlayer.cs @@ -60,8 +60,8 @@ namespace VocaluxeLib.Utils.Player { get { - return _Song != null && !string.IsNullOrEmpty(_Song.Folder) && !string.IsNullOrEmpty(_Song.VideoFileName) && - File.Exists(Path.Combine(_Song.Folder, _Song.VideoFileName)); + return _Song != null && !string.IsNullOrEmpty(_Song.Folder) && !string.IsNullOrEmpty(_Song.Video) && + File.Exists(Path.Combine(_Song.Folder, _Song.Video)); } } @@ -134,7 +134,7 @@ namespace VocaluxeLib.Utils.Player return; } - var videoFilePath = Path.Combine(_Song.Folder, _Song.VideoFileName); + var videoFilePath = Path.Combine(_Song.Folder, _Song.Video); _Video = CBase.Video.Load(videoFilePath); if (_Video == null) {