Improvements to CoverDB and cover loading (#893)

* Cover handling improvement

* Cover handling improvement

* Cover handling improvement

* Remove CoverDB flag (not need anymore)

* Remove CoverDB flag (not needed anymore)

* Cover handling improvement

* Cover handling improvement

---------

Co-authored-by: Florian Ostertag <flokuep@users.noreply.github.com>
This commit is contained in:
McSchneidarandFlorian Ostertag authored and GitHub committed 2026-08-24 19:28:33 +02:00
1 parent b822281e04
commit ba5a361d42
5 files changed
+323 -114

No files matched your search

+148 -23
View File
@@ -1,4 +1,4 @@
#region license
#region license
// This file is part of Vocaluxe.
//
// Vocaluxe is free software: you can redistribute it and/or modify
@@ -18,8 +18,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using VocaluxeLib;
@@ -34,6 +38,8 @@ namespace Vocaluxe.Base
{
private const string _NoCoverName = "No Cover";
private const string _NoCoverNameAlt = "NoCover";
private const string _CoverCacheFolderName = "CoverCache";
private const int _CoverCacheVersion = 1;
private static readonly Dictionary<string, CTextureRef> _Covers = new Dictionary<string, CTextureRef>();
private static readonly Dictionary<ECoverGeneratorType, CCoverGenerator> _CoverGenerators = new Dictionary<ECoverGeneratorType, CCoverGenerator>();
private static readonly List<SThemeCover> _CoverThemes = new List<SThemeCover>();
@@ -97,35 +103,74 @@ namespace Vocaluxe.Base
public static CTextureRef GenerateCover(string text, ECoverGeneratorType type, CSong firstSong)
{
var texture = Cover(text);
if (texture != NoCover)
lock (_Covers)
{
if (_CoverExists(text))
return _Covers[text];
}
CTextureRef texture;
if (_TryLoadCachedCover(text, type, firstSong, out texture))
{
lock (_Covers)
{
if (!_CoverExists(text))
_Covers.Add(text, texture);
else
{
var existingTexture = _Covers[text];
CDraw.RemoveTexture(ref texture);
texture = existingTexture;
}
}
return texture;
}
texture = CDraw.CopyTexture(NoCover);
Task.Factory.StartNew(() =>
{
_CancelToken.Token.ThrowIfCancellationRequested();
var coverBmp = !_CoverGenerators.ContainsKey(type)
? 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.Cover) : null);
}
var startGeneration = false;
_CancelToken.Token.ThrowIfCancellationRequested();
if (coverBmp != null)
{
CDraw.EnqueueTextureUpdate(texture, coverBmp);
}
_CancelToken.Token.ThrowIfCancellationRequested();
}, _CancelToken.Token);
lock (_Covers)
{
_Covers.Add(text, texture);
if (!_CoverExists(text))
{
_Covers.Add(text, texture);
startGeneration = true;
}
else
{
var existingTexture = _Covers[text];
CDraw.RemoveTexture(ref texture);
texture = existingTexture;
}
}
if (startGeneration)
{
Task.Factory.StartNew(() =>
{
_CancelToken.Token.ThrowIfCancellationRequested();
var firstCoverPath = firstSong != null ? Path.Combine(firstSong.Folder, firstSong.Cover) : null;
var coverBmp = !_CoverGenerators.ContainsKey(type)
? null
: _CoverGenerators[type].GetCover(text, firstCoverPath);
_CancelToken.Token.ThrowIfCancellationRequested();
if (coverBmp == null && _CoverGenerators.ContainsKey(ECoverGeneratorType.Default))
coverBmp = _CoverGenerators[ECoverGeneratorType.Default].GetCover(text, firstCoverPath);
_CancelToken.Token.ThrowIfCancellationRequested();
if (coverBmp != null)
{
_TrySaveCachedCover(text, type, firstSong, coverBmp);
CDraw.EnqueueTextureUpdate(texture, coverBmp);
}
_CancelToken.Token.ThrowIfCancellationRequested();
}, _CancelToken.Token);
}
return texture;
@@ -325,5 +370,85 @@ namespace Vocaluxe.Base
}
}
}
private static bool _TryLoadCachedCover(string text, ECoverGeneratorType type, CSong firstSong, out CTextureRef texture)
{
texture = null;
try
{
var coverTheme = _GetCoverTheme();
var cachePath = _GetCoverCacheFilePath(text, type, firstSong, coverTheme);
if (!File.Exists(cachePath))
return false;
texture = CDraw.AddTexture(cachePath);
return texture != null;
}
catch (Exception e)
{
CLog.Error("Error loading generated cover cache: " + e);
return false;
}
}
private static void _TrySaveCachedCover(string text, ECoverGeneratorType type, CSong firstSong, Bitmap bmp)
{
try
{
var coverTheme = _GetCoverTheme();
var cachePath = _GetCoverCacheFilePath(text, type, firstSong, coverTheme);
var cacheFolder = Path.GetDirectoryName(cachePath);
if (string.IsNullOrEmpty(cacheFolder))
return;
Directory.CreateDirectory(cacheFolder);
var tmpPath = cachePath + ".tmp";
bmp.Save(tmpPath, ImageFormat.Png);
if (File.Exists(cachePath))
File.Delete(cachePath);
File.Move(tmpPath, cachePath);
}
catch (Exception e)
{
CLog.Error("Error writing generated cover cache: " + e);
}
}
private static string _GetCoverCacheFolder(SThemeCover coverTheme)
{
return Path.Combine(CSettings.ProgramFolder, _CoverCacheFolderName, coverTheme.Info.Name);
}
private static string _GetCoverCacheFilePath(string text, ECoverGeneratorType type, CSong firstSong, SThemeCover coverTheme)
{
return Path.Combine(_GetCoverCacheFolder(coverTheme), _GetCoverCacheKey(text, type, firstSong, coverTheme) + ".png");
}
private static string _GetCoverCacheKey(string text, ECoverGeneratorType type, CSong firstSong, SThemeCover coverTheme)
{
var firstCoverPath = firstSong != null ? Path.Combine(firstSong.Folder, firstSong.Cover) : "";
var firstCoverStamp = "";
if (!string.IsNullOrEmpty(firstCoverPath) && File.Exists(firstCoverPath))
firstCoverStamp = File.GetLastWriteTimeUtc(firstCoverPath).Ticks.ToString();
var rawKey = string.Join("|",
_CoverCacheVersion,
coverTheme.Info.Name,
type,
text,
firstCoverPath,
firstCoverStamp
);
using (var sha = SHA256.Create())
{
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(rawKey));
return BitConverter.ToString(hash).Replace("-", "");
}
}
}
}
}
+109 -72
View File
@@ -1,4 +1,4 @@
#region license
#region license
// This file is part of Vocaluxe.
//
// Vocaluxe is free software: you can redistribute it and/or modify
@@ -70,9 +70,7 @@ namespace Vocaluxe.Lib.Database
public bool GetCover(string coverPath, ref CTextureRef tex, int maxSize)
{
if (_Connection == null)
{
return false;
}
if (!File.Exists(coverPath))
{
@@ -84,18 +82,14 @@ namespace Vocaluxe.Lib.Database
{
//Double check here because we may have just closed our connection
if (_Connection == null)
{
return false;
}
using (var command = new SqliteCommand())
{
command.Connection = _Connection;
// If we have an open transaction on this connection, all commands must use it.
if (_TransactionCover != null)
{
command.Transaction = _TransactionCover;
}
command.CommandText = "SELECT id, width, height FROM Cover WHERE [Path] = @path";
command.Parameters.Clear();
@@ -121,7 +115,40 @@ namespace Vocaluxe.Lib.Database
reader.Read();
var data2 = _GetBytes(reader);
reader.Dispose();
tex = CDraw.EnqueueTexture(w, h, data2);
try
{
using (var stream = new MemoryStream(data2))
using (var source = new System.Drawing.Bitmap(stream))
{
var sourceSize = source.GetSize();
System.Drawing.Bitmap bitmap;
if (sourceSize.Width > maxSize || sourceSize.Height > maxSize)
{
var targetSize = CHelper.FitInBounds(
new SRectF(0, 0, maxSize, maxSize, 0),
(float)sourceSize.Width / sourceSize.Height,
EAspect.LetterBox
).SizeI;
bitmap = source.Resize(targetSize);
}
else
{
bitmap = new System.Drawing.Bitmap(source);
}
tex = CDraw.EnqueueTexture(bitmap);
}
}
catch (Exception e)
{
CLog.Error("Can't decode cover from database: " + e);
return false;
}
return true;
}
@@ -129,94 +156,104 @@ namespace Vocaluxe.Lib.Database
command.Parameters.Clear();
command.Parameters.AddWithValue("@id", id);
command.ExecuteNonQuery();
}
if (reader != null)
{
reader.Close();
if (reader != null)
reader.Close();
}
}
}
// At this point we do not have a mathing entry in the CoverDB (either no Data found and deleted or nothing at all)
// We break out of the lock to do the bitmap loading and resizing here to allow multithreaded loading
// At this point we do not have a matching entry in the CoverDB
// (either no Data found and deleted or nothing at all)
// We break out of the lock to do the bitmap loading here to allow multithreaded loading
var origin = CHelper.LoadBitmap(coverPath);
if (origin == null)
{
return false;
}
byte[] data;
int width;
int height;
var size = origin.GetSize();
if (size.Width > maxSize || size.Height > maxSize)
{
size = CHelper.FitInBounds(new SRectF(0, 0, maxSize, maxSize, 0), (float)size.Width / size.Height, EAspect.LetterBox).SizeI;
var tmp = origin.Resize(size);
origin.Dispose();
origin = tmp;
}
byte[] data;
try
{
data = new byte[size.Width * size.Height * 4];
var bmpData = origin.LockBits(origin.GetRect(), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(bmpData.Scan0, data, 0, data.Length);
origin.UnlockBits(bmpData);
}
finally
{
origin.Dispose();
}
tex = CDraw.EnqueueTexture(size.Width, size.Height, data);
lock (_Mutex)
{
//Double check here because we may have just closed our connection
if (_Connection == null)
try
{
data = File.ReadAllBytes(coverPath);
using (var source = CHelper.LoadBitmap(coverPath))
{
if (source == null)
return false;
var sourceSize = source.GetSize();
width = sourceSize.Width;
height = sourceSize.Height;
System.Drawing.Bitmap bitmap;
if (sourceSize.Width > maxSize || sourceSize.Height > maxSize)
{
var targetSize = CHelper.FitInBounds(
new SRectF(0, 0, maxSize, maxSize, 0),
(float)sourceSize.Width / sourceSize.Height,
EAspect.LetterBox
).SizeI;
bitmap = source.Resize(targetSize);
}
else
{
bitmap = new System.Drawing.Bitmap(source);
}
tex = CDraw.EnqueueTexture(bitmap);
}
}
catch (Exception e)
{
CLog.Error("Error loading cover: " + coverPath + " - " + e);
return false;
}
if (_TransactionCover == null)
lock (_Mutex)
{
_TransactionCover = _Connection.BeginTransaction();
}
// Double check here because we may have just closed our connection
if (_Connection == null)
return false;
using (var command = new SqliteCommand())
{
command.Connection = _Connection;
command.Transaction = _TransactionCover;
command.CommandText = "INSERT INTO Cover (Path, width, height) VALUES (@path, @w, @h)";
command.Parameters.Clear();
command.Parameters.AddWithValue("@w", size.Width);
command.Parameters.AddWithValue("@h", size.Height);
command.Parameters.AddWithValue("@path", coverPath);
command.ExecuteNonQuery();
if (_TransactionCover == null)
_TransactionCover = _Connection.BeginTransaction();
command.CommandText = "SELECT id FROM Cover WHERE [Path] = @path";
command.Parameters.Clear();
command.Parameters.AddWithValue("@path", coverPath);
var reader = command.ExecuteReader();
if (reader != null)
using (var command = new SqliteCommand())
{
reader.Read();
command.Connection = _Connection;
command.Transaction = _TransactionCover;
command.CommandText = "INSERT INTO Cover (Path, width, height) VALUES (@path, @w, @h)";
command.Parameters.Clear();
command.Parameters.AddWithValue("@w", width);
command.Parameters.AddWithValue("@h", height);
command.Parameters.AddWithValue("@path", coverPath);
command.ExecuteNonQuery();
command.CommandText = "SELECT id FROM Cover WHERE [Path] = @path";
command.Parameters.Clear();
command.Parameters.AddWithValue("@path", coverPath);
var reader = command.ExecuteReader();
if (reader == null || !reader.Read())
{
reader?.Dispose();
return false;
}
var id = reader.GetInt32(0);
reader.Dispose();
command.CommandText = "INSERT INTO CoverData (CoverId, Data) VALUES (@id, @data)";
command.Parameters.Clear();
command.Parameters.AddWithValue("@id", id);
command.Parameters.AddWithValue("@data", data);
command.ExecuteNonQuery();
return true;
}
}
}
return false;
}
public void CommitCovers()
@@ -275,4 +312,4 @@ namespace Vocaluxe.Lib.Database
return true;
}
}
}
}
+1 -15
View File
@@ -104,20 +104,6 @@ namespace Vocaluxe
// Create data folder
Directory.CreateDirectory(CSettings.DataFolder);
// Delete CoverDB.sqlite if marker exists (cover size changed)
var flagPath = Path.Combine(CSettings.DataFolder, "DeleteCoverDB.flag");
var coverDbPath = Path.Combine(CSettings.DataFolder, CSettings.FileNameCoverDB);
if (File.Exists(flagPath))
{
if (File.Exists(coverDbPath))
{
File.Delete(coverDbPath); // Delete the database
}
File.Delete(flagPath); // Remove the marker so it doesn't repeat
}
// Init Log
CLog.Init(CSettings.FolderNameLogs,
CSettings.FileNameMainLog,
@@ -585,4 +571,4 @@ namespace Vocaluxe
return true;
}
}
}
}
+1 -3
View File
@@ -181,8 +181,6 @@ namespace Vocaluxe.Screens
var newCoverSize = int.Parse(selectedValue);
if (currentCoverSize != newCoverSize)
{
var flagPath = Path.Combine(CSettings.DataFolder, "DeleteCoverDB.flag");
File.Create(flagPath).Dispose();
_Texts[_TextWarningRestart].Visible = true;
_Statics[_StaticWarningRestart].Visible = true;
CConfig.Config.Graphics.CoverSize = newCoverSize;
@@ -207,4 +205,4 @@ namespace Vocaluxe.Screens
}
}
}
}
}
+64 -1
View File
@@ -25,6 +25,7 @@ using VocaluxeLib;
using VocaluxeLib.Menu;
using VocaluxeLib.Menu.SongMenu;
using VocaluxeLib.PartyModes;
using VocaluxeLib.Songs;
namespace Vocaluxe.Screens
{
@@ -970,6 +971,7 @@ namespace Vocaluxe.Screens
_Statics[_StaticShortInfoTop].Visible = false;
_Texts[_TextShortInfoTop].Visible = false;
_PreloadVisibleCovers();
UpdateGame();
}
@@ -1045,6 +1047,67 @@ namespace Vocaluxe.Screens
return true;
}
private void _PreloadVisibleCovers()
{
if (CSongs.IsInCategory)
{
var coverType = CCover._SongSortingToType(_Sso.Sorting.SongSorting);
foreach (var song in CSongs.VisibleSongs)
{
if (song == null)
continue;
if (string.IsNullOrEmpty(song.Cover))
{
var text = _GetGeneratedCoverText(song, coverType);
if (!string.IsNullOrEmpty(text))
CCover.GenerateCover(text, coverType, song);
}
}
}
else
{
foreach (var category in CSongs.Categories)
{
if (category == null || string.IsNullOrEmpty(category.Name))
continue;
var coverType = CCover._SongSortingToType(_Sso.Sorting.SongSorting);
CCover.GenerateCover(category.Name, coverType, null);
}
}
}
private static string _GetGeneratedCoverText(CSong song, ECoverGeneratorType coverType)
{
switch (coverType)
{
case ECoverGeneratorType.Artist:
return song.Artist;
case ECoverGeneratorType.Folder:
return song.FolderName;
case ECoverGeneratorType.Edition:
return song.Editions.Count > 0 ? song.Editions[0] : "";
case ECoverGeneratorType.Genre:
return song.Genres.Count > 0 ? song.Genres[0] : "";
case ECoverGeneratorType.Language:
return song.Languages.Count > 0 ? song.Languages[0] : "";
case ECoverGeneratorType.Year:
return song.Year;
case ECoverGeneratorType.Decade:
return !string.IsNullOrEmpty(song.Year) && song.Year.Length >= 3
? song.Year.Substring(0, 3) + "0s"
: "";
case ECoverGeneratorType.Tags:
return song.Tags.Count > 0 ? song.Tags[0] : "";
case ECoverGeneratorType.Date:
return song.DateAdded.ToString("yyyy-MM-dd");
case ECoverGeneratorType.Letter:
return !string.IsNullOrEmpty(song.Artist) ? song.Artist.Substring(0, 1) : "";
default:
return "";
}
}
public override void OnClose()
{
@@ -2011,4 +2074,4 @@ namespace Vocaluxe.Screens
}
#endregion Playlist Actions
}
}
}