mirror of https://github.com/ErsatzTV/ErsatzTV.git
32 changed files with 298 additions and 69 deletions
@ -0,0 +1,10 @@ |
|||||||
|
namespace ErsatzTV.Core.Search; |
||||||
|
|
||||||
|
public interface ISmartCollectionCache |
||||||
|
{ |
||||||
|
Task Refresh(); |
||||||
|
|
||||||
|
Task<bool> HasCycle(string name); |
||||||
|
|
||||||
|
Task<Option<string>> GetQuery(string name); |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
namespace ErsatzTV.Infrastructure.Search; |
||||||
|
|
||||||
|
public class AdjGraph |
||||||
|
{ |
||||||
|
private readonly List<Edge> _edges = []; |
||||||
|
|
||||||
|
public void Clear() |
||||||
|
{ |
||||||
|
_edges.Clear(); |
||||||
|
} |
||||||
|
|
||||||
|
public void AddEdge(string from, string to) |
||||||
|
{ |
||||||
|
_edges.Add(new Edge(from.ToLowerInvariant(), to.ToLowerInvariant())); |
||||||
|
} |
||||||
|
|
||||||
|
public bool HasCycle(string from) |
||||||
|
{ |
||||||
|
var visited = new System.Collections.Generic.HashSet<string>(); |
||||||
|
var stack = new System.Collections.Generic.HashSet<string>(); |
||||||
|
return HasCycleImpl(from.ToLowerInvariant(), visited, stack); |
||||||
|
} |
||||||
|
|
||||||
|
private bool HasCycleImpl(string node, ISet<string> visited, ISet<string> stack) |
||||||
|
{ |
||||||
|
if (stack.Contains(node)) |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (!visited.Add(node)) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
stack.Add(node); |
||||||
|
|
||||||
|
foreach (Edge edge in _edges.Where(e => e.From == node)) |
||||||
|
{ |
||||||
|
if (HasCycleImpl(edge.To, visited, stack)) |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
stack.Remove(node); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private record Edge(string From, string To); |
||||||
|
} |
@ -0,0 +1,91 @@ |
|||||||
|
using System.Text.RegularExpressions; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Search; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Search; |
||||||
|
|
||||||
|
public sealed class SmartCollectionCache(IDbContextFactory<TvContext> dbContextFactory) |
||||||
|
: ISmartCollectionCache, IDisposable |
||||||
|
{ |
||||||
|
private readonly Dictionary<string, SmartCollectionData> _data = new(StringComparer.OrdinalIgnoreCase); |
||||||
|
private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); |
||||||
|
private readonly AdjGraph _graph = new(); |
||||||
|
|
||||||
|
public async Task Refresh() |
||||||
|
{ |
||||||
|
await _semaphoreSlim.WaitAsync(); |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); |
||||||
|
List<SmartCollection> smartCollections = await dbContext.SmartCollections |
||||||
|
.AsNoTracking() |
||||||
|
.ToListAsync(); |
||||||
|
|
||||||
|
_data.Clear(); |
||||||
|
_graph.Clear(); |
||||||
|
|
||||||
|
foreach (SmartCollection smartCollection in smartCollections) |
||||||
|
{ |
||||||
|
var data = new SmartCollectionData(smartCollection.Query); |
||||||
|
_data.Add(smartCollection.Name, data); |
||||||
|
|
||||||
|
foreach (Match match in SearchQueryParser.SmartCollectionRegex().Matches(smartCollection.Query)) |
||||||
|
{ |
||||||
|
string otherCollectionName = match.Groups[1].Value; |
||||||
|
_graph.AddEdge(smartCollection.Name, otherCollectionName); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
foreach (SmartCollection smartCollection in smartCollections) |
||||||
|
{ |
||||||
|
SmartCollectionData data = _data[smartCollection.Name]; |
||||||
|
data.HasCycle = _graph.HasCycle(smartCollection.Name); |
||||||
|
} |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
_semaphoreSlim.Release(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<bool> HasCycle(string name) |
||||||
|
{ |
||||||
|
await _semaphoreSlim.WaitAsync(); |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
return _data.TryGetValue(name, out SmartCollectionData data) && data.HasCycle; |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
_semaphoreSlim.Release(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<Option<string>> GetQuery(string name) |
||||||
|
{ |
||||||
|
await _semaphoreSlim.WaitAsync(); |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
return _data.TryGetValue(name, out SmartCollectionData data) ? data.Query : Option<string>.None; |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
_semaphoreSlim.Release(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void Dispose() |
||||||
|
{ |
||||||
|
_semaphoreSlim.Dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
private record SmartCollectionData(string Query) |
||||||
|
{ |
||||||
|
public bool HasCycle { get; set; } |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue