initial net work
This commit is contained in:
parent
63b1df88ec
commit
f26c378b06
9
Selector.Net/BaseNode.cs
Normal file
9
Selector.Net/BaseNode.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Net;
|
||||||
|
|
||||||
|
public class BaseNode<T> : INode<T>
|
||||||
|
{
|
||||||
|
public T Id { get; set; }
|
||||||
|
}
|
||||||
|
|
19
Selector.Net/BaseSinkSource.cs
Normal file
19
Selector.Net/BaseSinkSource.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Net;
|
||||||
|
|
||||||
|
public abstract class BaseSinkSource<TNodeId, TObj> : BaseSingleSink<TNodeId, TObj>, ISource<TNodeId>
|
||||||
|
{
|
||||||
|
protected Emit<TNodeId> EmitHandler { get; set; }
|
||||||
|
|
||||||
|
public void ReceiveHandler(Emit<TNodeId> handler)
|
||||||
|
{
|
||||||
|
EmitHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Emit(object obj)
|
||||||
|
{
|
||||||
|
EmitHandler(this, obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
135
Selector.Net/Graph.cs
Normal file
135
Selector.Net/Graph.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
using System;
|
||||||
|
using QuikGraph;
|
||||||
|
|
||||||
|
namespace Selector.Net
|
||||||
|
{
|
||||||
|
public class Graph<TNodeId> : IGraph<TNodeId>
|
||||||
|
{
|
||||||
|
protected AdjacencyGraph<INode<TNodeId>, SEdge<INode<TNodeId>>> graph { get; set; }
|
||||||
|
private readonly object graphLock = new object();
|
||||||
|
|
||||||
|
public Graph()
|
||||||
|
{
|
||||||
|
graph = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<INode<TNodeId>> Nodes => graph.Vertices;
|
||||||
|
|
||||||
|
public Task AddEdge(INode<TNodeId> from, INode<TNodeId> to)
|
||||||
|
{
|
||||||
|
lock(graphLock)
|
||||||
|
{
|
||||||
|
graph.AddVerticesAndEdge(new SEdge<INode<TNodeId>>(from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from is ISource<TNodeId> fromSource)
|
||||||
|
{
|
||||||
|
fromSource.ReceiveHandler(SourceHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to is ISource<TNodeId> toSource)
|
||||||
|
{
|
||||||
|
toSource.ReceiveHandler(SourceHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task AddNode(INode<TNodeId> node)
|
||||||
|
{
|
||||||
|
lock (graphLock)
|
||||||
|
{
|
||||||
|
graph.AddVertex(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node is ISource<TNodeId> source)
|
||||||
|
{
|
||||||
|
source.ReceiveHandler(SourceHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SourceHandler(ISource<TNodeId> sender, object obj,
|
||||||
|
IEnumerable<TNodeId> nodeWhitelist = null, IEnumerable<TNodeId> nodeBlacklist = null)
|
||||||
|
{
|
||||||
|
if(nodeWhitelist is not null && nodeBlacklist is not null && nodeWhitelist.Any() && nodeBlacklist.Any())
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot provide whitelist and blacklist, at most one");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graph.TryGetOutEdges(sender, out var edges))
|
||||||
|
{
|
||||||
|
foreach (var edge in edges)
|
||||||
|
{
|
||||||
|
if (edge.Target is ISink<TNodeId> sink)
|
||||||
|
{
|
||||||
|
if(nodeWhitelist is not null && nodeWhitelist.Any())
|
||||||
|
{
|
||||||
|
if(nodeWhitelist.Contains(sink.Id))
|
||||||
|
{
|
||||||
|
await sink.Consume(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (nodeBlacklist is not null && nodeBlacklist.Any())
|
||||||
|
{
|
||||||
|
if (!nodeBlacklist.Contains(sink.Id))
|
||||||
|
{
|
||||||
|
await sink.Consume(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await sink.Consume(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ISink<TNodeId>> GetSinks()
|
||||||
|
{
|
||||||
|
foreach (var node in graph.Vertices)
|
||||||
|
{
|
||||||
|
if (node is ISink<TNodeId> sink)
|
||||||
|
{
|
||||||
|
yield return sink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ISink<TNodeId, TSink>> GetSinks<TSink>()
|
||||||
|
{
|
||||||
|
foreach (var node in graph.Vertices)
|
||||||
|
{
|
||||||
|
if (node is ISink<TNodeId, TSink> sink)
|
||||||
|
{
|
||||||
|
yield return sink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ISource<TNodeId>> GetSources()
|
||||||
|
{
|
||||||
|
foreach (var node in graph.Vertices)
|
||||||
|
{
|
||||||
|
if (node is ISource<TNodeId> source)
|
||||||
|
{
|
||||||
|
yield return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Sink(string topic, object obj)
|
||||||
|
{
|
||||||
|
foreach (var sink in GetSinks())
|
||||||
|
{
|
||||||
|
if (sink.Topics.Contains(topic))
|
||||||
|
{
|
||||||
|
await sink.Consume(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
21
Selector.Net/Interfaces/IGraph.cs
Normal file
21
Selector.Net/Interfaces/IGraph.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using QuikGraph;
|
||||||
|
|
||||||
|
namespace Selector.Net
|
||||||
|
{
|
||||||
|
public interface IGraph<TNodeId>
|
||||||
|
{
|
||||||
|
IEnumerable<INode<TNodeId>> Nodes { get; }
|
||||||
|
|
||||||
|
Task AddEdge(INode<TNodeId> from, INode<TNodeId> to);
|
||||||
|
Task AddNode(INode<TNodeId> node);
|
||||||
|
|
||||||
|
Task Sink(string topic, object obj);
|
||||||
|
|
||||||
|
IEnumerable<ISource<TNodeId>> GetSources();
|
||||||
|
|
||||||
|
IEnumerable<ISink<TNodeId>> GetSinks();
|
||||||
|
IEnumerable<ISink<TNodeId, TSink>> GetSinks<TSink>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
10
Selector.Net/Interfaces/INode.cs
Normal file
10
Selector.Net/Interfaces/INode.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Net
|
||||||
|
{
|
||||||
|
public interface INode<T>
|
||||||
|
{
|
||||||
|
public T Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
25
Selector.Net/Interfaces/ISink.cs
Normal file
25
Selector.Net/Interfaces/ISink.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
namespace Selector.Net
|
||||||
|
{
|
||||||
|
public interface ISink<TNodeId> : INode<TNodeId>
|
||||||
|
{
|
||||||
|
IEnumerable<string> Topics { get; set; }
|
||||||
|
|
||||||
|
Task Consume(object obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Not a node, just callback handler
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TObj"></typeparam>
|
||||||
|
public interface ITypeSink<TObj>
|
||||||
|
{
|
||||||
|
Task ConsumeType(TObj obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ISink<TNodeId, TObj> : ISink<TNodeId>, ITypeSink<TObj>
|
||||||
|
{
|
||||||
|
//Task ConsumeType(TObj obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
17
Selector.Net/Interfaces/ISource.cs
Normal file
17
Selector.Net/Interfaces/ISource.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
namespace Selector.Net
|
||||||
|
{
|
||||||
|
public delegate void Emit<T>(ISource<T> sender, object obj, IEnumerable<T> nodeWhitelist = null, IEnumerable<T> nodeBlacklist = null);
|
||||||
|
public delegate void Emit<TNodeId, TObj>(ISource<TNodeId, TObj> sender, object obj, IEnumerable<T> nodeWhitelist = null, IEnumerable<T> nodeBlacklist = null);
|
||||||
|
|
||||||
|
public interface ISource<TNodeId> : INode<TNodeId>
|
||||||
|
{
|
||||||
|
void ReceiveHandler(Emit<TNodeId> handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ISource<TNodeId, TObj> : INode<TNodeId>
|
||||||
|
{
|
||||||
|
void ReceiveHandler(Emit<TNodeId, TObj> handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
56
Selector.Net/Playlist/Added.cs
Normal file
56
Selector.Net/Playlist/Added.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
|
namespace Selector.Net.Playlist;
|
||||||
|
|
||||||
|
public enum AddedType
|
||||||
|
{
|
||||||
|
Since, Before
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class Added<TNodeId> : BaseSinkSource<TNodeId, PlaylistChangeEventArgs>
|
||||||
|
{
|
||||||
|
public DateTime Threshold { get; set; }
|
||||||
|
public bool IncludeNull { get; set; }
|
||||||
|
|
||||||
|
public AddedType Operator { get; set; } = AddedType.Since;
|
||||||
|
|
||||||
|
private IEnumerable<PlaylistTrack<IPlayableItem>> Filter(IEnumerable<PlaylistTrack<IPlayableItem>> tracks)
|
||||||
|
{
|
||||||
|
return tracks.Where(t =>
|
||||||
|
{
|
||||||
|
if (t.AddedAt is not null)
|
||||||
|
{
|
||||||
|
switch (Operator)
|
||||||
|
{
|
||||||
|
case AddedType.Since:
|
||||||
|
return t.AddedAt.Value > Threshold;
|
||||||
|
case AddedType.Before:
|
||||||
|
return t.AddedAt.Value < Threshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (IncludeNull)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ConsumeType(PlaylistChangeEventArgs obj)
|
||||||
|
{
|
||||||
|
//obj.CurrentTracks = Filter(obj.CurrentTracks);
|
||||||
|
obj.AddedTracks = Filter(obj.AddedTracks);
|
||||||
|
obj.RemovedTracks = Filter(obj.RemovedTracks);
|
||||||
|
|
||||||
|
Emit(obj);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
60
Selector.Net/Playlist/Aggregator.cs
Normal file
60
Selector.Net/Playlist/Aggregator.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
|
namespace Selector.Net.Playlist
|
||||||
|
{
|
||||||
|
public class AggregatorConfig
|
||||||
|
{
|
||||||
|
public string PlaylistId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Aggregator<TNodeId>: BaseSingleSink<TNodeId, PlaylistChangeEventArgs>
|
||||||
|
{
|
||||||
|
private readonly ILogger<Aggregator<TNodeId>> Logger;
|
||||||
|
private readonly ISpotifyClient SpotifyClient;
|
||||||
|
private readonly ICurrentItemListResolver ItemResolver;
|
||||||
|
private readonly AggregatorConfig Config;
|
||||||
|
|
||||||
|
public Aggregator(ISpotifyClient spotifyClient, ICurrentItemListResolver itemResolver, AggregatorConfig config, ILogger<Aggregator<TNodeId>> logger)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
SpotifyClient = spotifyClient;
|
||||||
|
ItemResolver = itemResolver;
|
||||||
|
Config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ConsumeType(PlaylistChangeEventArgs obj)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var addedTracks = obj.AddedTracks.ToList();
|
||||||
|
var removedTracks = obj.AddedTracks.ToList();
|
||||||
|
var removedTrackURIs = obj.AddedTracks.ToList();
|
||||||
|
|
||||||
|
var currentTracks = await ItemResolver.GetCurrentItems().ConfigureAwait(false);
|
||||||
|
|
||||||
|
currentTracks = currentTracks.Where(t => addedTracks);
|
||||||
|
currentTracks = currentTracks.Concat(addedTracks);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (APIUnauthorizedException e)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
|
||||||
|
//throw e;
|
||||||
|
}
|
||||||
|
catch (APITooManyRequestsException e)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Too many requests error: [{message}]", e.Message);
|
||||||
|
// throw e;
|
||||||
|
}
|
||||||
|
catch (APIException e)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("API error: [{message}]", e.Message);
|
||||||
|
// throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
54
Selector.Net/Playlist/Applier.cs
Normal file
54
Selector.Net/Playlist/Applier.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
|
namespace Selector.Net.Playlist
|
||||||
|
{
|
||||||
|
public class ApplierConfig {
|
||||||
|
public string PlaylistId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Applier<TNodeId>: BaseSingleSink<TNodeId, PlaylistChangeEventArgs>
|
||||||
|
{
|
||||||
|
private readonly ILogger<Applier<TNodeId>> Logger;
|
||||||
|
private readonly ISpotifyClient SpotifyClient;
|
||||||
|
private readonly ApplierConfig Config;
|
||||||
|
|
||||||
|
private FullPlaylist Playlist { get; set; }
|
||||||
|
private IEnumerable<PlaylistTrack<IPlayableItem>> Items { get; set; }
|
||||||
|
|
||||||
|
public Applier(ISpotifyClient spotifyClient, ApplierConfig config, ILogger<Applier<TNodeId>> logger)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
SpotifyClient = spotifyClient;
|
||||||
|
Config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ConsumeType(PlaylistChangeEventArgs obj)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tracks = obj.CurrentTracks.ToList();
|
||||||
|
var trackUris = tracks.Select(t => t.GetUri()).ToList();
|
||||||
|
|
||||||
|
await SpotifyClient.Playlists.ReplaceItems(Config.PlaylistId, new PlaylistReplaceItemsRequest(trackUris));
|
||||||
|
}
|
||||||
|
catch (APIUnauthorizedException e)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
|
||||||
|
//throw e;
|
||||||
|
}
|
||||||
|
catch (APITooManyRequestsException e)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Too many requests error: [{message}]", e.Message);
|
||||||
|
// throw e;
|
||||||
|
}
|
||||||
|
catch (APIException e)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("API error: [{message}]", e.Message);
|
||||||
|
// throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
11
Selector.Net/Playlist/CurrentTrackResolver.cs
Normal file
11
Selector.Net/Playlist/CurrentTrackResolver.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
|
namespace Selector.Net.Playlist
|
||||||
|
{
|
||||||
|
public interface ICurrentItemListResolver
|
||||||
|
{
|
||||||
|
Task<IEnumerable<PlaylistTrack<IPlayableItem>>> GetCurrentItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
26
Selector.Net/Playlist/ItemFilter.cs
Normal file
26
Selector.Net/Playlist/ItemFilter.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
|
namespace Selector.Net.Playlist;
|
||||||
|
|
||||||
|
public class ItemFilter<TNodeId> : BaseSinkSource<TNodeId, PlaylistChangeEventArgs>
|
||||||
|
{
|
||||||
|
public Func<PlaylistTrack<IPlayableItem>, bool> Func { get; set; }
|
||||||
|
|
||||||
|
public ItemFilter(Func<PlaylistTrack<IPlayableItem>, bool> func)
|
||||||
|
{
|
||||||
|
Func = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ConsumeType(PlaylistChangeEventArgs obj)
|
||||||
|
{
|
||||||
|
//obj.CurrentTracks = obj.CurrentTracks.Where(Func);
|
||||||
|
obj.AddedTracks = obj.AddedTracks.Where(Func);
|
||||||
|
obj.RemovedTracks = obj.RemovedTracks.Where(Func);
|
||||||
|
|
||||||
|
Emit(obj);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
26
Selector.Net/Playlist/ItemMutator.cs
Normal file
26
Selector.Net/Playlist/ItemMutator.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
|
namespace Selector.Net.Playlist;
|
||||||
|
|
||||||
|
public class ItemMutator<TNodeId> : BaseSinkSource<TNodeId, PlaylistChangeEventArgs>
|
||||||
|
{
|
||||||
|
public Func<PlaylistTrack<IPlayableItem>, PlaylistTrack<IPlayableItem>> Func { get; set; }
|
||||||
|
|
||||||
|
public ItemMutator(Func<PlaylistTrack<IPlayableItem>, PlaylistTrack<IPlayableItem>> func)
|
||||||
|
{
|
||||||
|
Func = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ConsumeType(PlaylistChangeEventArgs obj)
|
||||||
|
{
|
||||||
|
//obj.CurrentTracks = obj.CurrentTracks.Select(Func);
|
||||||
|
obj.AddedTracks = obj.AddedTracks.Select(Func);
|
||||||
|
obj.RemovedTracks = obj.RemovedTracks.Select(Func);
|
||||||
|
|
||||||
|
Emit(obj);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
68
Selector.Net/Playlist/PlaylistFilter.cs
Normal file
68
Selector.Net/Playlist/PlaylistFilter.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Net.Playlist
|
||||||
|
{
|
||||||
|
public class PlaylistFilterConfig
|
||||||
|
{
|
||||||
|
public IEnumerable<string> NameWhiteList { get; set; }
|
||||||
|
public IEnumerable<string> NameBlackList { get; set; }
|
||||||
|
public IEnumerable<string> UriWhiteList { get; set; }
|
||||||
|
public IEnumerable<string> UriBlackList { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlaylistFilter<TNodeId> : BaseSinkSource<TNodeId, PlaylistChangeEventArgs>
|
||||||
|
{
|
||||||
|
public IEnumerable<string> NameWhiteList { get; set; }
|
||||||
|
public IEnumerable<string> NameBlackList { get; set; }
|
||||||
|
public IEnumerable<string> UriWhiteList { get; set; }
|
||||||
|
public IEnumerable<string> UriBlackList { get; set; }
|
||||||
|
|
||||||
|
public PlaylistFilter() { }
|
||||||
|
|
||||||
|
public PlaylistFilter(PlaylistFilterConfig config)
|
||||||
|
{
|
||||||
|
NameWhiteList = config.NameWhiteList;
|
||||||
|
NameBlackList = config.NameBlackList;
|
||||||
|
UriWhiteList = config.UriWhiteList;
|
||||||
|
UriBlackList = config.UriBlackList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ConsumeType(PlaylistChangeEventArgs obj)
|
||||||
|
{
|
||||||
|
if (NameWhiteList is not null && NameWhiteList.Any())
|
||||||
|
{
|
||||||
|
if (NameWhiteList.Contains(obj.Current.Name))
|
||||||
|
{
|
||||||
|
Emit(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NameBlackList is not null && NameBlackList.Any())
|
||||||
|
{
|
||||||
|
if (!NameBlackList.Contains(obj.Current.Name))
|
||||||
|
{
|
||||||
|
Emit(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UriWhiteList is not null && UriWhiteList.Any())
|
||||||
|
{
|
||||||
|
if (UriWhiteList.Contains(obj.Current.Name))
|
||||||
|
{
|
||||||
|
Emit(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UriBlackList is not null && UriBlackList.Any())
|
||||||
|
{
|
||||||
|
if (!UriBlackList.Contains(obj.Current.Name))
|
||||||
|
{
|
||||||
|
Emit(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
40
Selector.Net/Playlist/TypeFilter.cs
Normal file
40
Selector.Net/Playlist/TypeFilter.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
|
namespace Selector.Net.Playlist;
|
||||||
|
|
||||||
|
public enum PlayableItemType
|
||||||
|
{
|
||||||
|
Track, Episode
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TypeFilter<TNodeId> : BaseSinkSource<TNodeId, IEnumerable<PlaylistTrack<IPlayableItem>>>
|
||||||
|
{
|
||||||
|
public PlayableItemType FilterType { get; set; }
|
||||||
|
|
||||||
|
public TypeFilter(PlayableItemType filterType)
|
||||||
|
{
|
||||||
|
FilterType = filterType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ConsumeType(IEnumerable<PlaylistTrack<IPlayableItem>> tracks)
|
||||||
|
{
|
||||||
|
switch (FilterType)
|
||||||
|
{
|
||||||
|
case PlayableItemType.Track:
|
||||||
|
|
||||||
|
Emit(tracks.Where(i => i.Track is FullTrack));
|
||||||
|
|
||||||
|
break;
|
||||||
|
case PlayableItemType.Episode:
|
||||||
|
|
||||||
|
Emit(tracks.Where(i => i.Track is FullEpisode));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
18
Selector.Net/PlaylistGraph.cs
Normal file
18
Selector.Net/PlaylistGraph.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
namespace Selector.Net.Playlist;
|
||||||
|
|
||||||
|
public class PlaylistGraph<TNodeId>
|
||||||
|
{
|
||||||
|
private IGraph<TNodeId> graph { get; set; }
|
||||||
|
|
||||||
|
public PlaylistGraph(PlaylistFilterConfig filterConfig)
|
||||||
|
{
|
||||||
|
graph = new Graph<TNodeId>();
|
||||||
|
|
||||||
|
var entryFilter = new PlaylistFilter<TNodeId>(filterConfig)
|
||||||
|
{
|
||||||
|
Topics = new[] { "track-entry" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
18
Selector.Net/Repeater.cs
Normal file
18
Selector.Net/Repeater.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Net;
|
||||||
|
|
||||||
|
public class Repeater<TNodeId>: BaseSource<TNodeId>, ISink<TNodeId>
|
||||||
|
{
|
||||||
|
public IEnumerable<string> Topics { get; set; }
|
||||||
|
|
||||||
|
private Type[] _types = Array.Empty<Type>();
|
||||||
|
public IEnumerable<Type> Types => _types;
|
||||||
|
|
||||||
|
public Task Consume(object obj)
|
||||||
|
{
|
||||||
|
Emit(obj);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
32
Selector.Net/Selector.Net.csproj
Normal file
32
Selector.Net/Selector.Net.csproj
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="QuikGraph" />
|
||||||
|
<None Remove="Interfaces\" />
|
||||||
|
<None Remove="Source\" />
|
||||||
|
<None Remove="Sink\" />
|
||||||
|
<None Remove="Playlist\" />
|
||||||
|
<None Remove="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
|
<None Remove="System.Linq.Async" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="QuikGraph" Version="2.3.0" />
|
||||||
|
<PackageReference Include="SpotifyAPI.Web" Version="6.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
||||||
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Interfaces\" />
|
||||||
|
<Folder Include="Source\" />
|
||||||
|
<Folder Include="Sink\" />
|
||||||
|
<Folder Include="Playlist\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Selector\Selector.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
30
Selector.Net/Sink/BaseMultiSink.cs
Normal file
30
Selector.Net/Sink/BaseMultiSink.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Net;
|
||||||
|
|
||||||
|
//public abstract class BaseMultiSink<TNodeId, TObj>: BaseNode<TNodeId>, ISink<TNodeId>
|
||||||
|
//{
|
||||||
|
// public IEnumerable<string> Topics { get; }
|
||||||
|
|
||||||
|
// public IDictionary<Type, Action<object>> Callbacks { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
// public Task Consume(object obj)
|
||||||
|
// {
|
||||||
|
// var objType = obj.GetType();
|
||||||
|
|
||||||
|
// if(Callbacks.ContainsKey(objType))
|
||||||
|
// {
|
||||||
|
// var callback = Callbacks[objType];
|
||||||
|
|
||||||
|
// var objCast = (TObj)obj;
|
||||||
|
|
||||||
|
// return ConsumeType(objCast);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// throw new ArgumentException("Not of acceptable payload type");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
27
Selector.Net/Sink/BaseSingleSink.cs
Normal file
27
Selector.Net/Sink/BaseSingleSink.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Net;
|
||||||
|
|
||||||
|
public abstract class BaseSingleSink<TNodeId, TObj>: BaseNode<TNodeId>, ISink<TNodeId, TObj>
|
||||||
|
{
|
||||||
|
public IEnumerable<string> Topics { get; set; }
|
||||||
|
|
||||||
|
public Task Consume(object obj)
|
||||||
|
{
|
||||||
|
var type = GetType();
|
||||||
|
var genericArgs = type.GetGenericArguments();
|
||||||
|
var objType = obj.GetType();
|
||||||
|
|
||||||
|
if (objType.IsAssignableTo(genericArgs[1]))
|
||||||
|
{
|
||||||
|
var objCast = (TObj)obj;
|
||||||
|
|
||||||
|
return ConsumeType(objCast);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task ConsumeType(TObj obj);
|
||||||
|
}
|
||||||
|
|
28
Selector.Net/Sink/DebugSink.cs
Normal file
28
Selector.Net/Sink/DebugSink.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Selector.Net;
|
||||||
|
|
||||||
|
public class DebugSink<TNodeId>: ISink<TNodeId>
|
||||||
|
{
|
||||||
|
public IEnumerable<string> Topics { get; set; }
|
||||||
|
public TNodeId Id { get; set; }
|
||||||
|
|
||||||
|
public ILogger<DebugSink<TNodeId>> Logger { get; set; }
|
||||||
|
|
||||||
|
public Task Consume(object obj)
|
||||||
|
{
|
||||||
|
var type = obj.GetType();
|
||||||
|
|
||||||
|
if (Logger is not null)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("{} Received, {}", type, obj);
|
||||||
|
}else
|
||||||
|
{
|
||||||
|
Console.WriteLine("{0} Received, {1}", type, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
12
Selector.Net/Sink/EmptySink.cs
Normal file
12
Selector.Net/Sink/EmptySink.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Net;
|
||||||
|
|
||||||
|
public class EmptySink<TNodeId, TObj>: BaseSingleSink<TNodeId, TObj>
|
||||||
|
{
|
||||||
|
public override Task ConsumeType(TObj obj)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
19
Selector.Net/Source/BaseSource.cs
Normal file
19
Selector.Net/Source/BaseSource.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Net;
|
||||||
|
|
||||||
|
public abstract class BaseSource<TNodeId>: BaseNode<TNodeId>, ISource<TNodeId>
|
||||||
|
{
|
||||||
|
protected Emit<TNodeId> EmitHandler { get; set; }
|
||||||
|
|
||||||
|
public void ReceiveHandler(Emit<TNodeId> handler)
|
||||||
|
{
|
||||||
|
EmitHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Emit(object obj)
|
||||||
|
{
|
||||||
|
EmitHandler(this, obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
10
Selector.Net/Source/TriggerSource.cs
Normal file
10
Selector.Net/Source/TriggerSource.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Selector.Net;
|
||||||
|
|
||||||
|
public class TriggerSource<T> : BaseSource<T>
|
||||||
|
{
|
||||||
|
public void Trigger(T obj)
|
||||||
|
{
|
||||||
|
Emit(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
127
Selector.Tests/Net/Graph.cs
Normal file
127
Selector.Tests/Net/Graph.cs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using QuikGraph;
|
||||||
|
using Selector.Net;
|
||||||
|
using Xunit;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace Selector.Tests.Net
|
||||||
|
{
|
||||||
|
public class GraphTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Network()
|
||||||
|
{
|
||||||
|
var net = new Graph<string>();
|
||||||
|
|
||||||
|
var trigger = new TriggerSource<string>()
|
||||||
|
{
|
||||||
|
Id = "trigger"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sink = new EmptySink<string, object>()
|
||||||
|
{
|
||||||
|
Id = "sink"
|
||||||
|
};
|
||||||
|
|
||||||
|
net.AddEdge(
|
||||||
|
trigger,
|
||||||
|
sink
|
||||||
|
);
|
||||||
|
|
||||||
|
trigger.Trigger("payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SourceReceivesPayload()
|
||||||
|
{
|
||||||
|
var net = new Graph<string>();
|
||||||
|
|
||||||
|
var trigger = new TriggerSource<string>()
|
||||||
|
{
|
||||||
|
Id = "trigger"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sink = new Mock<ISink<string>>();
|
||||||
|
|
||||||
|
net.AddEdge(
|
||||||
|
trigger,
|
||||||
|
sink.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
var payload = "payload";
|
||||||
|
|
||||||
|
trigger.Trigger(payload);
|
||||||
|
|
||||||
|
sink.Verify(a => a.Consume(payload), Times.Once());
|
||||||
|
sink.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SourceReceivesGraphPayload()
|
||||||
|
{
|
||||||
|
var net = new Graph<string>();
|
||||||
|
|
||||||
|
var sink = new Mock<ISink<string>>();
|
||||||
|
sink.Setup(a => a.Topics).Returns(new[] {"topic1"});
|
||||||
|
|
||||||
|
net.AddNode(
|
||||||
|
sink.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
var payload = "payload";
|
||||||
|
|
||||||
|
net.Sink("topic1", payload);
|
||||||
|
|
||||||
|
sink.Verify(a => a.Consume(payload), Times.Once());
|
||||||
|
sink.Verify(a => a.Topics, Times.Once());
|
||||||
|
sink.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SourceReceivesRepeatedPayload()
|
||||||
|
{
|
||||||
|
var net = new Graph<string>();
|
||||||
|
|
||||||
|
var trigger = new TriggerSource<string>()
|
||||||
|
{
|
||||||
|
Id = "trigger"
|
||||||
|
};
|
||||||
|
|
||||||
|
var repeater = new Repeater<string>();
|
||||||
|
var repeater2 = new Repeater<string>();
|
||||||
|
var repeater3 = new Repeater<string>();
|
||||||
|
|
||||||
|
var sink = new Mock<ISink<string>>();
|
||||||
|
|
||||||
|
net.AddEdge(
|
||||||
|
trigger,
|
||||||
|
repeater
|
||||||
|
);
|
||||||
|
|
||||||
|
net.AddEdge(
|
||||||
|
repeater,
|
||||||
|
repeater2
|
||||||
|
);
|
||||||
|
|
||||||
|
net.AddEdge(
|
||||||
|
repeater2,
|
||||||
|
repeater3
|
||||||
|
);
|
||||||
|
|
||||||
|
net.AddEdge(
|
||||||
|
repeater3,
|
||||||
|
sink.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
var payload = "payload";
|
||||||
|
|
||||||
|
trigger.Trigger(payload);
|
||||||
|
|
||||||
|
sink.Verify(a => a.Consume(payload), Times.Once());
|
||||||
|
sink.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Selector\Selector.csproj" />
|
<ProjectReference Include="..\Selector\Selector.csproj" />
|
||||||
|
<ProjectReference Include="..\Selector.Net\Selector.Net.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Net\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Net\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -9,233 +9,232 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit.Sdk;
|
using Xunit.Sdk;
|
||||||
|
|
||||||
namespace Selector.Tests
|
namespace Selector.Tests;
|
||||||
|
|
||||||
|
public class PlayerWatcherTests
|
||||||
{
|
{
|
||||||
public class PlayerWatcherTests
|
public static IEnumerable<object[]> NowPlayingData =>
|
||||||
|
new List<object[]>
|
||||||
{
|
{
|
||||||
public static IEnumerable<object[]> NowPlayingData =>
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
new List<object[]>
|
Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1")),
|
||||||
{
|
Helper.CurrentPlayback(Helper.FullTrack("track2", "album2", "artist2")),
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
Helper.CurrentPlayback(Helper.FullTrack("track3", "album3", "artist3")),
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1")),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("track2", "album2", "artist2")),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("track3", "album3", "artist3")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[MemberData(nameof(NowPlayingData))]
|
|
||||||
public async void NowPlaying(List<CurrentlyPlayingContext> playing)
|
|
||||||
{
|
|
||||||
var playingQueue = new Queue<CurrentlyPlayingContext>(playing);
|
|
||||||
|
|
||||||
var spotMock = new Mock<IPlayerClient>();
|
|
||||||
var eq = new UriEqual();
|
|
||||||
|
|
||||||
spotMock.Setup(s => s.GetCurrentPlayback().Result).Returns(playingQueue.Dequeue);
|
|
||||||
|
|
||||||
var watcher = new PlayerWatcher(spotMock.Object, eq);
|
|
||||||
|
|
||||||
for (var i = 0; i < playing.Count; i++)
|
|
||||||
{
|
|
||||||
await watcher.WatchOne();
|
|
||||||
watcher.Live.Should().Be(playing[i]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public static IEnumerable<object[]> EventsData =>
|
[Theory]
|
||||||
new List<object[]>
|
[MemberData(nameof(NowPlayingData))]
|
||||||
|
public async void NowPlaying(List<CurrentlyPlayingContext> playing)
|
||||||
|
{
|
||||||
|
var playingQueue = new Queue<CurrentlyPlayingContext>(playing);
|
||||||
|
|
||||||
|
var spotMock = new Mock<IPlayerClient>();
|
||||||
|
var eq = new UriEqual();
|
||||||
|
|
||||||
|
spotMock.Setup(s => s.GetCurrentPlayback().Result).Returns(playingQueue.Dequeue);
|
||||||
|
|
||||||
|
var watcher = new PlayerWatcher(spotMock.Object, eq);
|
||||||
|
|
||||||
|
for (var i = 0; i < playing.Count; i++)
|
||||||
{
|
{
|
||||||
// NO CHANGING
|
await watcher.WatchOne();
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
watcher.Live.Should().Be(playing[i]);
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"),
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "ItemChange", "ContextChange", "PlayingChange", "DeviceChange", "VolumeChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange", "ArtistChange" }
|
|
||||||
},
|
|
||||||
// TRACK CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("trackchange1", "album1", "artist1")),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("trackchange2", "album1", "artist1"))
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "DeviceChange", "VolumeChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange", "ArtistChange" }
|
|
||||||
},
|
|
||||||
// ALBUM CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album1", "artist1")),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album2", "artist1"))
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "AlbumChange", "DeviceChange", "VolumeChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "ArtistChange" }
|
|
||||||
},
|
|
||||||
// ARTIST CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist1")),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist2"))
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "ArtistChange", "DeviceChange", "VolumeChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange" }
|
|
||||||
},
|
|
||||||
// CONTEXT CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context1"),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context2")
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "PlayingChange", "ItemChange", "ContextChange", "DeviceChange", "VolumeChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange", "ArtistChange" }
|
|
||||||
},
|
|
||||||
// PLAYING CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: true, context: "context1"),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: false, context: "context1")
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange", "ArtistChange" }
|
|
||||||
},
|
|
||||||
// PLAYING CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: false, context: "context1"),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: true, context: "context1")
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange", "ArtistChange" }
|
|
||||||
},
|
|
||||||
// CONTENT CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1"),
|
|
||||||
Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2")
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange", "ArtistChange" }
|
|
||||||
},
|
|
||||||
// CONTENT CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2"),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1")
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange", "ArtistChange" }
|
|
||||||
},
|
|
||||||
// DEVICE CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev1")),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev2"))
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange" }
|
|
||||||
},
|
|
||||||
// VOLUME CHANGE
|
|
||||||
new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 50)),
|
|
||||||
Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 60))
|
|
||||||
},
|
|
||||||
// to raise
|
|
||||||
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
|
|
||||||
// to not raise
|
|
||||||
new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange" }
|
|
||||||
},
|
|
||||||
// // STARTED PLAYBACK
|
|
||||||
// new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
// null,
|
|
||||||
// Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1")
|
|
||||||
// },
|
|
||||||
// // to raise
|
|
||||||
// new List<string>(){ "PlayingChange" },
|
|
||||||
// // to not raise
|
|
||||||
// new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
|
|
||||||
// },
|
|
||||||
// // STARTED PLAYBACK
|
|
||||||
// new object[] { new List<CurrentlyPlayingContext>(){
|
|
||||||
// Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"),
|
|
||||||
// null
|
|
||||||
// },
|
|
||||||
// // to raise
|
|
||||||
// new List<string>(){ "PlayingChange" },
|
|
||||||
// // to not raise
|
|
||||||
// new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[MemberData(nameof(EventsData))]
|
|
||||||
public async void Events(List<CurrentlyPlayingContext> playing, List<string> toRaise, List<string> toNotRaise)
|
|
||||||
{
|
|
||||||
var playingQueue = new Queue<CurrentlyPlayingContext>(playing);
|
|
||||||
|
|
||||||
var spotMock = new Mock<IPlayerClient>();
|
|
||||||
var eq = new UriEqual();
|
|
||||||
|
|
||||||
spotMock.Setup(
|
|
||||||
s => s.GetCurrentPlayback().Result
|
|
||||||
).Returns(playingQueue.Dequeue);
|
|
||||||
|
|
||||||
var watcher = new PlayerWatcher(spotMock.Object, eq);
|
|
||||||
using var monitoredWatcher = watcher.Monitor();
|
|
||||||
|
|
||||||
for (var i = 0; i < playing.Count; i++)
|
|
||||||
{
|
|
||||||
await watcher.WatchOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
toRaise.ForEach(r => monitoredWatcher.Should().Raise(r).WithSender(watcher));
|
|
||||||
toNotRaise.ForEach(r => monitoredWatcher.Should().NotRaise(r));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(1000, 3500, 4)]
|
|
||||||
[InlineData(500, 3800, 8)]
|
|
||||||
[InlineData(100, 250, 3)]
|
|
||||||
public async void Watch(int pollPeriod, int execTime, int numberOfCalls)
|
|
||||||
{
|
|
||||||
var spotMock = new Mock<IPlayerClient>();
|
|
||||||
var eq = new UriEqual();
|
|
||||||
var watch = new PlayerWatcher(spotMock.Object, eq)
|
|
||||||
{
|
|
||||||
PollPeriod = pollPeriod
|
|
||||||
};
|
|
||||||
|
|
||||||
var tokenSource = new CancellationTokenSource();
|
|
||||||
var task = watch.Watch(tokenSource.Token);
|
|
||||||
|
|
||||||
await Task.Delay(execTime);
|
|
||||||
tokenSource.Cancel();
|
|
||||||
|
|
||||||
spotMock.Verify(s => s.GetCurrentPlayback(), Times.Exactly(numberOfCalls));
|
|
||||||
}
|
|
||||||
|
|
||||||
// [Fact]
|
|
||||||
// public async void Auth()
|
|
||||||
// {
|
|
||||||
// var spot = new SpotifyClient("");
|
|
||||||
// var eq = new UriEqual();
|
|
||||||
// var watch = new PlayerWatcher(spot.Player, eq);
|
|
||||||
|
|
||||||
// var token = new CancellationTokenSource();
|
|
||||||
// await watch.Watch(token.Token);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> EventsData =>
|
||||||
|
new List<object[]>
|
||||||
|
{
|
||||||
|
// NO CHANGING
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"),
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "ItemChange", "ContextChange", "PlayingChange", "DeviceChange", "VolumeChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange", "ArtistChange" }
|
||||||
|
},
|
||||||
|
// TRACK CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("trackchange1", "album1", "artist1")),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("trackchange2", "album1", "artist1"))
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "DeviceChange", "VolumeChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange", "ArtistChange" }
|
||||||
|
},
|
||||||
|
// ALBUM CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album1", "artist1")),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album2", "artist1"))
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "AlbumChange", "DeviceChange", "VolumeChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "ArtistChange" }
|
||||||
|
},
|
||||||
|
// ARTIST CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist1")),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist2"))
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "ArtistChange", "DeviceChange", "VolumeChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange" }
|
||||||
|
},
|
||||||
|
// CONTEXT CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context1"),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context2")
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "PlayingChange", "ItemChange", "ContextChange", "DeviceChange", "VolumeChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange", "ArtistChange" }
|
||||||
|
},
|
||||||
|
// PLAYING CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: true, context: "context1"),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: false, context: "context1")
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange", "ArtistChange" }
|
||||||
|
},
|
||||||
|
// PLAYING CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: false, context: "context1"),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: true, context: "context1")
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange", "ArtistChange" }
|
||||||
|
},
|
||||||
|
// CONTENT CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1"),
|
||||||
|
Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2")
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange", "ArtistChange" }
|
||||||
|
},
|
||||||
|
// CONTENT CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2"),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1")
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange", "ArtistChange" }
|
||||||
|
},
|
||||||
|
// DEVICE CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev1")),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev2"))
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange" }
|
||||||
|
},
|
||||||
|
// VOLUME CHANGE
|
||||||
|
new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 50)),
|
||||||
|
Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 60))
|
||||||
|
},
|
||||||
|
// to raise
|
||||||
|
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
|
||||||
|
// to not raise
|
||||||
|
new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange" }
|
||||||
|
},
|
||||||
|
// // STARTED PLAYBACK
|
||||||
|
// new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
// null,
|
||||||
|
// Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1")
|
||||||
|
// },
|
||||||
|
// // to raise
|
||||||
|
// new List<string>(){ "PlayingChange" },
|
||||||
|
// // to not raise
|
||||||
|
// new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
|
||||||
|
// },
|
||||||
|
// // STARTED PLAYBACK
|
||||||
|
// new object[] { new List<CurrentlyPlayingContext>(){
|
||||||
|
// Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"),
|
||||||
|
// null
|
||||||
|
// },
|
||||||
|
// // to raise
|
||||||
|
// new List<string>(){ "PlayingChange" },
|
||||||
|
// // to not raise
|
||||||
|
// new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(EventsData))]
|
||||||
|
public async void Events(List<CurrentlyPlayingContext> playing, List<string> toRaise, List<string> toNotRaise)
|
||||||
|
{
|
||||||
|
var playingQueue = new Queue<CurrentlyPlayingContext>(playing);
|
||||||
|
|
||||||
|
var spotMock = new Mock<IPlayerClient>();
|
||||||
|
var eq = new UriEqual();
|
||||||
|
|
||||||
|
spotMock.Setup(
|
||||||
|
s => s.GetCurrentPlayback().Result
|
||||||
|
).Returns(playingQueue.Dequeue);
|
||||||
|
|
||||||
|
var watcher = new PlayerWatcher(spotMock.Object, eq);
|
||||||
|
using var monitoredWatcher = watcher.Monitor();
|
||||||
|
|
||||||
|
for (var i = 0; i < playing.Count; i++)
|
||||||
|
{
|
||||||
|
await watcher.WatchOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
toRaise.ForEach(r => monitoredWatcher.Should().Raise(r).WithSender(watcher));
|
||||||
|
toNotRaise.ForEach(r => monitoredWatcher.Should().NotRaise(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1000, 3500, 4)]
|
||||||
|
[InlineData(500, 3800, 8)]
|
||||||
|
[InlineData(100, 250, 3)]
|
||||||
|
public async void Watch(int pollPeriod, int execTime, int numberOfCalls)
|
||||||
|
{
|
||||||
|
var spotMock = new Mock<IPlayerClient>();
|
||||||
|
var eq = new UriEqual();
|
||||||
|
var watch = new PlayerWatcher(spotMock.Object, eq)
|
||||||
|
{
|
||||||
|
PollPeriod = pollPeriod
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenSource = new CancellationTokenSource();
|
||||||
|
var task = watch.Watch(tokenSource.Token);
|
||||||
|
|
||||||
|
await Task.Delay(execTime);
|
||||||
|
tokenSource.Cancel();
|
||||||
|
|
||||||
|
spotMock.Verify(s => s.GetCurrentPlayback(), Times.Exactly(numberOfCalls));
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Fact]
|
||||||
|
// public async void Auth()
|
||||||
|
// {
|
||||||
|
// var spot = new SpotifyClient("");
|
||||||
|
// var eq = new UriEqual();
|
||||||
|
// var watch = new PlayerWatcher(spot.Player, eq);
|
||||||
|
|
||||||
|
// var token = new CancellationTokenSource();
|
||||||
|
// await watch.Watch(token.Token);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Selector.Cache", "Selector.
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.Event", "Selector.Event\Selector.Event.csproj", "{C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.Event", "Selector.Event\Selector.Event.csproj", "{C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.Net", "Selector.Net\Selector.Net.csproj", "{825F16A4-DB67-4CC4-A4D0-8326D297E227}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -58,6 +60,10 @@ Global
|
|||||||
{C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{825F16A4-DB67-4CC4-A4D0-8326D297E227}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{825F16A4-DB67-4CC4-A4D0-8326D297E227}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{825F16A4-DB67-4CC4-A4D0-8326D297E227}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{825F16A4-DB67-4CC4-A4D0-8326D297E227}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
Loading…
Reference in New Issue
Block a user