diff --git a/Vocaluxe/Base/CCover.cs b/Vocaluxe/Base/CCover.cs index 7514814c..66964987 100644 --- a/Vocaluxe/Base/CCover.cs +++ b/Vocaluxe/Base/CCover.cs @@ -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 _Covers = new Dictionary(); private static readonly Dictionary _CoverGenerators = new Dictionary(); private static readonly List _CoverThemes = new List(); @@ -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("-", ""); + } + } } -} \ No newline at end of file +} diff --git a/Vocaluxe/Lib/Database/CCoverDB.cs b/Vocaluxe/Lib/Database/CCoverDB.cs index a00e5d36..2db128d1 100644 --- a/Vocaluxe/Lib/Database/CCoverDB.cs +++ b/Vocaluxe/Lib/Database/CCoverDB.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/Vocaluxe/Program.cs b/Vocaluxe/Program.cs index cfa387cb..cd5b8069 100644 --- a/Vocaluxe/Program.cs +++ b/Vocaluxe/Program.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/Vocaluxe/Screens/CScreenOptionsGraphics.cs b/Vocaluxe/Screens/CScreenOptionsGraphics.cs index ca5b9ad3..3dab6ada 100644 --- a/Vocaluxe/Screens/CScreenOptionsGraphics.cs +++ b/Vocaluxe/Screens/CScreenOptionsGraphics.cs @@ -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 } } } -} \ No newline at end of file +} diff --git a/Vocaluxe/Screens/CScreenSong.cs b/Vocaluxe/Screens/CScreenSong.cs index f157cf2a..94098bc9 100644 --- a/Vocaluxe/Screens/CScreenSong.cs +++ b/Vocaluxe/Screens/CScreenSong.cs @@ -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 } -} \ No newline at end of file +}