#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 . #endregion using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using VocaluxeWeb.Contracts; using VocaluxeWeb.Dispatching; using VocaluxeWeb.Remote; namespace VocaluxeWeb.Services { public sealed class CSongQuery { public string Text; public string Language; public string Genre; public int? Decade; public bool? Duet; public ESongSort Sort; public int Page; public int PageSize = 50; } /// Copy of the game's song list for searching outside the main thread. /// It is refreshed when the game reports a new library version (checked at most every few seconds). public sealed class CSongLibrary { private static readonly TimeSpan _VersionCheckInterval = TimeSpan.FromSeconds(5); private sealed class CEntry { public RemoteSong Song; public SongDto Dto; public string SearchText; public int? Decade; } private readonly IVocaluxeRemote _Remote; private readonly CMainThreadDispatcher _Dispatcher; private readonly SemaphoreSlim _RefreshLock = new SemaphoreSlim(1, 1); private IReadOnlyList _Entries = new CEntry[0]; private Dictionary _ById = new Dictionary(); private int _Version = -1; private DateTime _LastCheck = DateTime.MinValue; public CSongLibrary(IVocaluxeRemote remote, CMainThreadDispatcher dispatcher) { _Remote = remote; _Dispatcher = dispatcher; } public async Task SearchAsync(CSongQuery query) { await _EnsureFreshAsync(); IEnumerable result = _Entries; var words = _Normalize(query.Text).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (words.Length > 0) { result = result.Where(e => words.All(w => e.SearchText.Contains(w))); } if (!string.IsNullOrEmpty(query.Language)) { result = result.Where(e => e.Song.Languages.Contains(query.Language, StringComparer.OrdinalIgnoreCase)); } if (!string.IsNullOrEmpty(query.Genre)) { result = result.Where(e => e.Song.Genres.Contains(query.Genre, StringComparer.OrdinalIgnoreCase)); } if (query.Decade.HasValue) { result = result.Where(e => e.Decade == query.Decade); } if (query.Duet.HasValue) { result = result.Where(e => e.Song.IsDuet == query.Duet.Value); } var sorted = _Sort(result, query.Sort).ToList(); var pageSize = Math.Max(1, Math.Min(query.PageSize, 200)); var page = Math.Max(0, query.Page); return new SongPageDto { Items = sorted.Skip(page * pageSize).Take(pageSize).Select(e => e.Dto).ToList(), Total = sorted.Count, Page = page, PageSize = pageSize }; } public async Task GetFacetsAsync() { await _EnsureFreshAsync(); var entries = _Entries; return new SongFacetsDto { Languages = _Count(entries.SelectMany(e => e.Song.Languages)), Genres = _Count(entries.SelectMany(e => e.Song.Genres)), Decades = entries.Where(e => e.Decade.HasValue) .GroupBy(e => e.Decade.Value) .OrderByDescending(g => g.Key) .Select(g => new FacetValueDto { Value = g.Key.ToString(CultureInfo.InvariantCulture), Count = g.Count() }) .ToList() }; } public async Task GetAsync(int songId) { await _EnsureFreshAsync(); return _ById.TryGetValue(songId, out var entry) ? entry.Song : null; } /// Song for display; songs that are no longer known get a placeholder. public async Task GetDtoAsync(int songId) { await _EnsureFreshAsync(); return _ById.TryGetValue(songId, out var entry) ? entry.Dto : new SongDto { Id = songId, Title = "?", Artist = "?", GameModes = new List(), AudioModes = new List() }; } public async Task CreateStateAsync(RemoteState state) { return new StateDto { Screen = state.Screen, CurrentSong = state.SongId.HasValue ? await GetDtoAsync(state.SongId.Value) : null }; } private async Task _EnsureFreshAsync() { if (DateTime.UtcNow - _LastCheck < _VersionCheckInterval && _Version >= 0) { return; } await _RefreshLock.WaitAsync(); try { if (DateTime.UtcNow - _LastCheck < _VersionCheckInterval && _Version >= 0) { return; } var version = await _Dispatcher.InvokeAsync(() => _Remote.SongLibraryVersion); if (version != _Version) { var songs = await _Dispatcher.InvokeAsync(() => _Remote.GetSongs()); var entries = songs.Select(_CreateEntry).ToList(); _ById = entries.GroupBy(e => e.Song.Id).ToDictionary(g => g.Key, g => g.First()); _Entries = entries; _Version = version; } _LastCheck = DateTime.UtcNow; } finally { _RefreshLock.Release(); } } private static CEntry _CreateEntry(RemoteSong song) { return new CEntry { Song = song, Dto = ToDto(song), SearchText = _Normalize(song.Artist + " " + song.Title + " " + string.Join(" ", song.Editions ?? new string[0])), Decade = _ParseDecade(song.Year) }; } public static SongDto ToDto(RemoteSong song) { return new SongDto { Id = song.Id, Title = song.Title, Artist = song.Artist, Year = string.IsNullOrEmpty(song.Year) ? null : song.Year, Language = song.Languages?.FirstOrDefault(), Genre = song.Genres?.FirstOrDefault(), IsDuet = song.IsDuet, HasCover = !string.IsNullOrEmpty(song.CoverPath), HasAudio = !string.IsNullOrEmpty(song.AudioPath), PreviewStart = song.PreviewStart, GameModes = (song.GameModes ?? new int[0]).Select(m => (ESongGameMode)m).ToList(), AudioModes = (song.AudioModes ?? new int[0]).Select(m => (ESongAudioMode)m).ToList() }; } private static IEnumerable _Sort(IEnumerable entries, ESongSort sort) { var culture = StringComparer.Create(CultureInfo.InvariantCulture, true); switch (sort) { case ESongSort.Title: return entries.OrderBy(e => e.Song.Title, culture).ThenBy(e => e.Song.Artist, culture); case ESongSort.Year: return entries.OrderByDescending(e => e.Song.Year).ThenBy(e => e.Song.Artist, culture); default: return entries.OrderBy(e => e.Song.Artist, culture).ThenBy(e => e.Song.Title, culture); } } private static IList _Count(IEnumerable values) { return values.Where(v => !string.IsNullOrWhiteSpace(v)) .GroupBy(v => v.Trim(), StringComparer.OrdinalIgnoreCase) .OrderByDescending(g => g.Count()) .ThenBy(g => g.Key) .Select(g => new FacetValueDto { Value = g.First().Trim(), Count = g.Count() }) .ToList(); } private static int? _ParseDecade(string year) { if (string.IsNullOrEmpty(year) || year.Length < 4 || !int.TryParse(year.Substring(0, 4), out var value) || value < 1900) { return null; } return value / 10 * 10; } /// Lower case without accents, so "beyonce" finds "Beyoncé". private static string _Normalize(string text) { if (string.IsNullOrEmpty(text)) { return ""; } var builder = new StringBuilder(text.Length); foreach (var c in text.Normalize(NormalizationForm.FormD)) { if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) { builder.Append(char.ToLowerInvariant(c)); } } return builder.ToString(); } } }