Song server: central song library with on-demand downloads
- VocaluxeSongServer: ASP.NET Core server that scans an UltraStar library and serves the song list (ETag), single files (Range) and zip bundles; optional API key; Dockerfile and Gitea workflow for git.disi.dev/projects/vocaluxe-song-server - Game: config section SongServer (Url, ApiKey). At song loading, the song text files are mirrored into a local cache that is loaded like a song folder; without connection the last library is used - VocaluxeLib: CSongFiles hook for all song file access; media are downloaded on demand (covers in the background, preview audio, everything before singing with progress in the sing screen) - Tests for scanner, API and downloads (incl. resume); AGENTS.md for the fork Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cf2fa796e2
commit
efcc63a640
29 files changed
+1753
-20
No files matched your search
@@ -338,6 +338,41 @@ namespace VocaluxeLib.Songs
|
||||
return Path.Combine(Folder, Video);
|
||||
}
|
||||
|
||||
public string GetCover()
|
||||
{
|
||||
return Cover == "" ? "" : Path.Combine(Folder, Cover);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full paths of the files needed to sing the song: audio (or instrumental/vocals), video, backgrounds and cover.
|
||||
/// </summary>
|
||||
public List<string> GetMediaFiles(bool includeVideo = true)
|
||||
{
|
||||
var files = new List<string> { GetMP3() };
|
||||
if (HasInstrumental())
|
||||
{
|
||||
files.Add(GetInstrumental());
|
||||
}
|
||||
|
||||
if (HasVocals())
|
||||
{
|
||||
files.Add(GetVocals());
|
||||
}
|
||||
|
||||
if (includeVideo && !string.IsNullOrEmpty(Video))
|
||||
{
|
||||
files.Add(GetVideo());
|
||||
}
|
||||
|
||||
files.AddRange(BackgroundFileNames.Where(bg => !string.IsNullOrEmpty(bg)).Select(bg => Path.Combine(Folder, bg)));
|
||||
if (Cover != "")
|
||||
{
|
||||
files.Add(GetCover());
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public void LoadSmallCover()
|
||||
{
|
||||
if (_CoverTextureSmall != null)
|
||||
@@ -347,7 +382,9 @@ namespace VocaluxeLib.Songs
|
||||
|
||||
if (Cover != "")
|
||||
{
|
||||
if (CBase.DataBase.GetCover(Path.Combine(Folder, Cover), ref _CoverTextureSmall, CBase.Config.GetCoverSize()))
|
||||
// Songs from a song server: download the cover first (cover loading runs on background threads).
|
||||
CSongFiles.EnsureLocal(GetCover());
|
||||
if (CBase.DataBase.GetCover(GetCover(), ref _CoverTextureSmall, CBase.Config.GetCoverSize()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#region license
|
||||
// This file is part of Vocaluxe.
|
||||
//
|
||||
// Vocaluxe is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vocaluxe is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vocaluxe. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VocaluxeLib.Songs
|
||||
{
|
||||
/// <summary>
|
||||
/// Access to the files of a song (audio, video, cover, backgrounds).
|
||||
/// Songs from a song server exist locally only as their text file at first; their other files are
|
||||
/// downloaded on demand. Everything that opens a song file should check <see cref="IsLocal" /> and
|
||||
/// call <see cref="RequestAsync" /> (or <see cref="EnsureLocal" /> on background threads) first.
|
||||
/// </summary>
|
||||
public interface ISongFileProvider
|
||||
{
|
||||
/// <summary>True if the file exists locally or can be downloaded.</summary>
|
||||
bool Exists(string path);
|
||||
|
||||
/// <summary>True if the file can be opened now (exists locally and is complete).</summary>
|
||||
bool IsLocal(string path);
|
||||
|
||||
/// <summary>Downloads the files that are not local yet. The task completes when all are local (or failed).</summary>
|
||||
Task RequestAsync(IEnumerable<string> paths);
|
||||
|
||||
/// <summary>Progress of the downloads of these files in [0..1], 1 if all are local.</summary>
|
||||
float GetProgress(IEnumerable<string> paths);
|
||||
}
|
||||
|
||||
/// <summary>The default provider: plain local files.</summary>
|
||||
public class CLocalSongFileProvider : ISongFileProvider
|
||||
{
|
||||
public bool Exists(string path)
|
||||
{
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
public bool IsLocal(string path)
|
||||
{
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
public Task RequestAsync(IEnumerable<string> paths)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public float GetProgress(IEnumerable<string> paths)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CSongFiles
|
||||
{
|
||||
public static ISongFileProvider Provider { get; set; } = new CLocalSongFileProvider();
|
||||
|
||||
public static bool Exists(string path)
|
||||
{
|
||||
return !string.IsNullOrEmpty(path) && Provider.Exists(path);
|
||||
}
|
||||
|
||||
public static bool IsLocal(string path)
|
||||
{
|
||||
return !string.IsNullOrEmpty(path) && Provider.IsLocal(path);
|
||||
}
|
||||
|
||||
public static bool AreLocal(IEnumerable<string> paths)
|
||||
{
|
||||
return paths.All(IsLocal);
|
||||
}
|
||||
|
||||
public static Task RequestAsync(IEnumerable<string> paths)
|
||||
{
|
||||
var missing = paths.Where(p => !string.IsNullOrEmpty(p) && !Provider.IsLocal(p)).Distinct().ToList();
|
||||
return missing.Count == 0 ? Task.CompletedTask : Provider.RequestAsync(missing);
|
||||
}
|
||||
|
||||
/// <summary>Blocks until the file is local. Only for background threads (e.g. cover loading).</summary>
|
||||
public static bool EnsureLocal(string path)
|
||||
{
|
||||
if (IsLocal(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Exists(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RequestAsync(new[] { path }).Wait();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A failed download is reported by the provider; the caller handles the missing file.
|
||||
}
|
||||
|
||||
return IsLocal(path);
|
||||
}
|
||||
|
||||
public static float GetProgress(IEnumerable<string> paths)
|
||||
{
|
||||
return Provider.GetProgress(paths.Where(p => !string.IsNullOrEmpty(p)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,7 @@ namespace VocaluxeLib.Songs
|
||||
break;
|
||||
case "MP3":
|
||||
case "AUDIO":
|
||||
if (File.Exists(Path.Combine(_Song.Folder, value)))
|
||||
if (CSongFiles.Exists(Path.Combine(_Song.Folder, value)))
|
||||
{
|
||||
_Song.Audio = value;
|
||||
headerFlags |= EHeaderFlags.MP3;
|
||||
@@ -207,7 +207,7 @@ namespace VocaluxeLib.Songs
|
||||
|
||||
break;
|
||||
case "INSTRUMENTAL":
|
||||
if (File.Exists(Path.Combine(_Song.Folder, value)))
|
||||
if (CSongFiles.Exists(Path.Combine(_Song.Folder, value)))
|
||||
{
|
||||
_Song.Instrumental = value;
|
||||
headerFlags |= EHeaderFlags.Instrumental;
|
||||
@@ -221,7 +221,7 @@ namespace VocaluxeLib.Songs
|
||||
|
||||
break;
|
||||
case "VOCALS":
|
||||
if (File.Exists(Path.Combine(_Song.Folder, value)))
|
||||
if (CSongFiles.Exists(Path.Combine(_Song.Folder, value)))
|
||||
{
|
||||
_Song.Vocals = value;
|
||||
headerFlags |= EHeaderFlags.Vocals;
|
||||
@@ -298,7 +298,7 @@ namespace VocaluxeLib.Songs
|
||||
|
||||
break;
|
||||
case "COVER":
|
||||
if (File.Exists(Path.Combine(_Song.Folder, value)))
|
||||
if (CSongFiles.Exists(Path.Combine(_Song.Folder, value)))
|
||||
{
|
||||
_Song.Cover = value;
|
||||
}
|
||||
@@ -309,7 +309,7 @@ namespace VocaluxeLib.Songs
|
||||
|
||||
break;
|
||||
case "BACKGROUND":
|
||||
if (File.Exists(Path.Combine(_Song.Folder, value)))
|
||||
if (CSongFiles.Exists(Path.Combine(_Song.Folder, value)))
|
||||
{
|
||||
_Song.BackgroundFileNames.Add(value);
|
||||
}
|
||||
@@ -321,7 +321,7 @@ namespace VocaluxeLib.Songs
|
||||
|
||||
break;
|
||||
case "VIDEO":
|
||||
if (File.Exists(Path.Combine(_Song.Folder, value)))
|
||||
if (CSongFiles.Exists(Path.Combine(_Song.Folder, value)))
|
||||
{
|
||||
_Song.Video = value;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace VocaluxeLib.Utils.Player
|
||||
{
|
||||
var song = _Song;
|
||||
return song != null && !string.IsNullOrEmpty(song.Folder) && !string.IsNullOrEmpty(song.Video) &&
|
||||
File.Exists(Path.Combine(song.Folder, song.Video));
|
||||
CSongFiles.IsLocal(song.GetVideo());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user