adding consumer, audio features injector
This commit is contained in:
parent
b5956ef4a0
commit
9dfad73397
@ -27,6 +27,7 @@ namespace Selector.CLI
|
||||
{
|
||||
public const string Key = "Watcher";
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
public List<WatcherInstanceOptions> Instances { get; set; } = new();
|
||||
}
|
||||
|
||||
@ -39,6 +40,7 @@ namespace Selector.CLI
|
||||
public string RefreshKey { get; set; }
|
||||
public int PollPeriod { get; set; } = 5000;
|
||||
public WatcherType Type { get; set; } = WatcherType.Player;
|
||||
public List<Consumers> Consumers { get; set; } = default;
|
||||
#nullable enable
|
||||
public string? PlaylistUri { get; set; }
|
||||
public string? WatcherCollection { get; set; }
|
||||
@ -49,4 +51,9 @@ namespace Selector.CLI
|
||||
{
|
||||
Player, Playlist
|
||||
}
|
||||
|
||||
enum Consumers
|
||||
{
|
||||
AudioFeatures
|
||||
}
|
||||
}
|
||||
|
@ -20,14 +20,20 @@ namespace Selector.CLI
|
||||
=> Host.CreateDefaultBuilder(args)
|
||||
.ConfigureServices((context, services) => {
|
||||
|
||||
Console.WriteLine("~~~ Selector CLI ~~~");
|
||||
Console.WriteLine("");
|
||||
|
||||
Console.WriteLine("Configuring...");
|
||||
// CONFIG
|
||||
services.Configure<RootOptions>(options => {
|
||||
context.Configuration.GetSection(RootOptions.Key).Bind(options);
|
||||
context.Configuration.GetSection($"{RootOptions.Key}:{WatcherOptions.Key}").Bind(options.WatcherOptions);
|
||||
});
|
||||
|
||||
Console.WriteLine("Adding Services...");
|
||||
// SERVICES
|
||||
services.AddSingleton<IWatcherFactory, WatcherFactory>();
|
||||
services.AddSingleton<IConsumerFactory, AudioFeatureInjectorFactory>();
|
||||
services.AddSingleton<IWatcherCollectionFactory, WatcherCollectionFactory>();
|
||||
// For generating spotify clients
|
||||
services.AddSingleton<IRefreshTokenFactoryProvider, RefreshTokenFactoryProvider>();
|
||||
@ -35,15 +41,25 @@ namespace Selector.CLI
|
||||
switch(context.Configuration.GetValue<EqualityChecker>("selector:equality"))
|
||||
{
|
||||
case EqualityChecker.Uri:
|
||||
Console.WriteLine("Using Uri Equality");
|
||||
services.AddTransient<IEqual, UriEqual>();
|
||||
break;
|
||||
case EqualityChecker.String:
|
||||
Console.WriteLine("Using String Equality");
|
||||
services.AddTransient<IEqual, StringEqual>();
|
||||
break;
|
||||
}
|
||||
|
||||
// HOSTED SERVICES
|
||||
services.AddHostedService<WatcherService>();
|
||||
if(context.Configuration
|
||||
.GetSection($"{RootOptions.Key}:{WatcherOptions.Key}")
|
||||
.Get<WatcherOptions>()
|
||||
.Enabled)
|
||||
{
|
||||
Console.WriteLine("Adding Watcher Service");
|
||||
services.AddHostedService<WatcherService>();
|
||||
}
|
||||
|
||||
})
|
||||
.ConfigureLogging((context, builder) => {
|
||||
builder.ClearProviders();
|
||||
|
@ -4,7 +4,8 @@
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"nativeDebugging": true
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ namespace Selector.CLI
|
||||
private const string ConfigInstanceKey = "localconfig";
|
||||
|
||||
private readonly ILogger<WatcherService> Logger;
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly RootOptions Config;
|
||||
private readonly IWatcherFactory WatcherFactory;
|
||||
private readonly IWatcherCollectionFactory WatcherCollectionFactory;
|
||||
@ -28,10 +29,11 @@ namespace Selector.CLI
|
||||
IWatcherFactory watcherFactory,
|
||||
IWatcherCollectionFactory watcherCollectionFactory,
|
||||
IRefreshTokenFactoryProvider spotifyFactory,
|
||||
ILogger<WatcherService> logger,
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<RootOptions> config
|
||||
) {
|
||||
Logger = logger;
|
||||
Logger = loggerFactory.CreateLogger<WatcherService>();
|
||||
LoggerFactory = loggerFactory;
|
||||
Config = config.Value;
|
||||
WatcherFactory = watcherFactory;
|
||||
WatcherCollectionFactory = watcherCollectionFactory;
|
||||
@ -42,7 +44,7 @@ namespace Selector.CLI
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInformation("Starting up");
|
||||
Logger.LogInformation("Starting watcher service...");
|
||||
|
||||
Logger.LogInformation("Loading config instances...");
|
||||
var watcherIndices = await InitialiseConfigInstances();
|
||||
@ -60,11 +62,11 @@ namespace Selector.CLI
|
||||
var logMsg = new StringBuilder();
|
||||
if (!string.IsNullOrWhiteSpace(watcherOption.Name))
|
||||
{
|
||||
logMsg.Append($"Creating {watcherOption.Name} watcher [{watcherOption.Type}]");
|
||||
logMsg.Append($"Creating [{watcherOption.Name}] watcher [{watcherOption.Type}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
logMsg.Append($"Creating new {watcherOption.Type} watcher");
|
||||
logMsg.Append($"Creating new [{watcherOption.Type}] watcher");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri)) logMsg.Append($" [{ watcherOption.PlaylistUri}]");
|
||||
@ -92,7 +94,19 @@ namespace Selector.CLI
|
||||
break;
|
||||
}
|
||||
|
||||
watcherCollection.Add(watcher);
|
||||
List<IConsumer> consumers = new();
|
||||
foreach(var consumer in watcherOption.Consumers)
|
||||
{
|
||||
switch(consumer)
|
||||
{
|
||||
case Consumers.AudioFeatures:
|
||||
var factory = new AudioFeatureInjectorFactory(LoggerFactory);
|
||||
consumers.Add(await factory.Get(spotifyFactory));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
watcherCollection.Add(watcher, consumers);
|
||||
}
|
||||
|
||||
return indices;
|
||||
@ -104,6 +118,7 @@ namespace Selector.CLI
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInformation($"Starting watcher collection [{index}]");
|
||||
Watchers[index].Start();
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
@ -119,7 +134,7 @@ namespace Selector.CLI
|
||||
|
||||
foreach((var key, var watcher) in Watchers)
|
||||
{
|
||||
Logger.LogInformation($"Stopping watcher collection: {key}");
|
||||
Logger.LogInformation($"Stopping watcher collection [{key}]");
|
||||
watcher.Stop();
|
||||
}
|
||||
|
||||
|
@ -6,16 +6,17 @@
|
||||
"Watcher": {
|
||||
"Instances": [
|
||||
{
|
||||
"name": "player watcher",
|
||||
"name": "Player Watcher",
|
||||
"type": "player",
|
||||
"pollperiod": 1000
|
||||
"pollperiod": 2000,
|
||||
"consumers": [ "audiofeatures" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information"
|
||||
"Default": "Trace"
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,10 @@
|
||||
name="logfile"
|
||||
fileName=".\selector.log"
|
||||
layout="${format}" />
|
||||
<target xsi:type="File"
|
||||
name="tracefile"
|
||||
fileName=".\selector.trace.log"
|
||||
layout="${format}" />
|
||||
<target xsi:type="Console"
|
||||
name="logconsole"
|
||||
layout="${format}" />
|
||||
@ -23,6 +27,9 @@
|
||||
|
||||
<!-- rules to map from logger name to target -->
|
||||
<rules>
|
||||
<logger name="Selector.*" minlevel="Trace" writeTo="logfile,logconsole" />
|
||||
<logger name="*" minlevel="Debug" writeTo="logfile" />
|
||||
<logger name="*" minlevel="Trace" writeTo="tracefile" />
|
||||
|
||||
<logger name="Selector.*" minlevel="Debug" writeTo="logconsole" />
|
||||
</rules>
|
||||
</nlog>
|
103
Selector/Consumers/AudioFeatureInjector.cs
Normal file
103
Selector/Consumers/AudioFeatureInjector.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public class AudioFeatureInjector : IConsumer
|
||||
{
|
||||
private readonly IPlayerWatcher Watcher;
|
||||
private readonly ITracksClient TrackClient;
|
||||
private readonly ILogger<AudioFeatureInjector> Logger;
|
||||
|
||||
public CancellationToken CancelToken { get; set; }
|
||||
|
||||
public AnalysedTrackTimeline Timeline { get; set; } = new();
|
||||
|
||||
public AudioFeatureInjector(
|
||||
IPlayerWatcher watcher,
|
||||
ITracksClient trackClient,
|
||||
ILogger<AudioFeatureInjector> logger = null,
|
||||
CancellationToken token = default
|
||||
){
|
||||
Watcher = watcher;
|
||||
TrackClient = trackClient;
|
||||
Logger = logger ?? NullLogger<AudioFeatureInjector>.Instance;
|
||||
CancelToken = token;
|
||||
}
|
||||
|
||||
public void Callback(object sender, ListeningChangeEventArgs e)
|
||||
{
|
||||
if (e.Current is null) return;
|
||||
|
||||
Task.Run(() => { return AsyncCallback(e); }, CancelToken);
|
||||
}
|
||||
|
||||
private async Task AsyncCallback(ListeningChangeEventArgs e)
|
||||
{
|
||||
if (e.Current.Item is FullTrack track)
|
||||
{
|
||||
Logger.LogTrace("Making Spotify call");
|
||||
var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
|
||||
Logger.LogDebug($"Adding audio features [{track.DisplayString()}]: [{audioFeatures.DisplayString()}]");
|
||||
|
||||
Timeline.Add(AnalysedTrack.From(track, audioFeatures));
|
||||
}
|
||||
else if (e.Current.Item is FullEpisode episode)
|
||||
{
|
||||
Logger.LogDebug($"Ignoring podcast episdoe [{episode.DisplayString()}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"Unknown item pulled from API [{e.Current.Item}]");
|
||||
}
|
||||
}
|
||||
|
||||
public void Subscribe(IWatcher watch = null)
|
||||
{
|
||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||
|
||||
if (watcher is IPlayerWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange += Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||
}
|
||||
}
|
||||
|
||||
public void Unsubscribe(IWatcher watch = null)
|
||||
{
|
||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||
|
||||
if (watcher is IPlayerWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange -= Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AnalysedTrack {
|
||||
public FullTrack Track { get; set; }
|
||||
public TrackAudioFeatures Features { get; set; }
|
||||
|
||||
public static AnalysedTrack From(FullTrack track, TrackAudioFeatures features)
|
||||
{
|
||||
return new AnalysedTrack()
|
||||
{
|
||||
Track = track,
|
||||
Features = features
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
32
Selector/Consumers/AudioFeatureInjectorFactory.cs
Normal file
32
Selector/Consumers/AudioFeatureInjectorFactory.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public class AudioFeatureInjectorFactory: IConsumerFactory {
|
||||
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
|
||||
public AudioFeatureInjectorFactory(ILoggerFactory loggerFactory)
|
||||
{
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public async Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
|
||||
{
|
||||
var config = await spotifyFactory.GetConfig();
|
||||
var client = new SpotifyClient(config);
|
||||
|
||||
return new AudioFeatureInjector(
|
||||
watcher,
|
||||
client.Tracks,
|
||||
LoggerFactory.CreateLogger<AudioFeatureInjector>()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
12
Selector/Consumers/IConsumer.cs
Normal file
12
Selector/Consumers/IConsumer.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public interface IConsumer
|
||||
{
|
||||
public void Callback(object sender, ListeningChangeEventArgs e);
|
||||
public void Subscribe(IWatcher watch = null);
|
||||
public void Unsubscribe(IWatcher watch = null);
|
||||
}
|
||||
}
|
10
Selector/Consumers/IConsumerFactory.cs
Normal file
10
Selector/Consumers/IConsumerFactory.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public interface IConsumerFactory
|
||||
{
|
||||
public Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher);
|
||||
}
|
||||
}
|
@ -5,20 +5,23 @@ using System.Linq;
|
||||
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector.Helpers
|
||||
namespace Selector
|
||||
{
|
||||
public static class SpotifyExtensions
|
||||
{
|
||||
public static string ToString(this FullTrack track) => $"{track.Name} - {track.Album.Name} - {track.Artists}";
|
||||
public static string ToString(this SimpleAlbum album) => $"{album.Name} - {album.Artists}";
|
||||
public static string ToString(this SimpleArtist artist) => $"{artist.Name}";
|
||||
public static string DisplayString(this FullTrack track) => $"{track.Name} / {track.Album.Name} / {track.Artists.DisplayString()}";
|
||||
public static string DisplayString(this SimpleAlbum album) => $"{album.Name} / {album.Artists.DisplayString()}";
|
||||
public static string DisplayString(this SimpleArtist artist) => artist.Name;
|
||||
|
||||
public static string ToString(this FullEpisode ep) => $"{ep.Name} - {ep.Show}";
|
||||
public static string ToString(this SimpleShow show) => $"{show.Name} - {show.Publisher}";
|
||||
public static string DisplayString(this FullEpisode ep) => $"{ep.Name} / {ep.Show.DisplayString()}";
|
||||
public static string DisplayString(this SimpleShow show) => $"{show.Name} / {show.Publisher}";
|
||||
|
||||
public static string ToString(this CurrentlyPlayingContext context) => $"{context.IsPlaying}, {context.Item}, {context.Device}";
|
||||
public static string ToString(this Device device) => $"{device.Id}: {device.Name} {device.VolumePercent}%";
|
||||
|
||||
public static string ToString(this IEnumerable<SimpleArtist> artists) => string.Join("/", artists.Select(a => a.Name));
|
||||
public static string DisplayString(this CurrentlyPlayingContext currentPlaying) => $"{currentPlaying.IsPlaying}, {currentPlaying.Item}, {currentPlaying.Device.DisplayString()}";
|
||||
public static string DisplayString(this Context context) => $"{context.Type}, {context.Uri}";
|
||||
public static string DisplayString(this Device device) => $"{device.Name} ({device.Id}) {device.VolumePercent}%";
|
||||
public static string DisplayString(this TrackAudioFeatures feature) => $"Acou. {feature.Acousticness}, Dance {feature.Danceability}, Energy {feature.Energy}, Instru. {feature.Instrumentalness}, Key {feature.Key}, Live {feature.Liveness}, Loud {feature.Loudness}, Mode {feature.Mode}, Speech {feature.Speechiness}, Tempo {feature.Tempo}, Valence {feature.Valence}";
|
||||
|
||||
public static string DisplayString(this IEnumerable<SimpleArtist> artists) => string.Join(", ", artists.Select(a => a.DisplayString()));
|
||||
}
|
||||
}
|
||||
|
50
Selector/Timeline/AnalysedTrackTimeline.cs
Normal file
50
Selector/Timeline/AnalysedTrackTimeline.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public class AnalysedTrackTimeline
|
||||
: BaseTimeline<AnalysedTrack>, ITrackTimeline<AnalysedTrack>
|
||||
{
|
||||
public IEqual EqualityChecker { get; set; }
|
||||
|
||||
public AnalysedTrack Get(FullTrack track)
|
||||
=> GetAll(track)
|
||||
.LastOrDefault();
|
||||
|
||||
public IEnumerable<AnalysedTrack> GetAll(FullTrack track)
|
||||
=> GetAllTimelineItems(track)
|
||||
.Select(t => t.Item);
|
||||
|
||||
public IEnumerable<TimelineItem<AnalysedTrack>> GetAllTimelineItems(FullTrack track)
|
||||
=> Recent
|
||||
.Where(i => EqualityChecker.IsEqual(i.Item.Track, track));
|
||||
|
||||
public AnalysedTrack Get(SimpleAlbum album)
|
||||
=> GetAll(album)
|
||||
.LastOrDefault();
|
||||
|
||||
public IEnumerable<AnalysedTrack> GetAll(SimpleAlbum album)
|
||||
=> GetAllTimelineItems(album)
|
||||
.Select(t => t.Item);
|
||||
|
||||
public IEnumerable<TimelineItem<AnalysedTrack>> GetAllTimelineItems(SimpleAlbum album)
|
||||
=> Recent
|
||||
.Where(i => EqualityChecker.IsEqual(i.Item.Track.Album, album));
|
||||
|
||||
public AnalysedTrack Get(SimpleArtist artist)
|
||||
=> GetAll(artist)
|
||||
.LastOrDefault();
|
||||
|
||||
public IEnumerable<AnalysedTrack> GetAll(SimpleArtist artist)
|
||||
=> GetAllTimelineItems(artist)
|
||||
.Select(t => t.Item);
|
||||
|
||||
public IEnumerable<TimelineItem<AnalysedTrack>> GetAllTimelineItems(SimpleArtist artist)
|
||||
=> Recent
|
||||
.Where(i => EqualityChecker.IsEqual(i.Item.Track.Artists[0], artist));
|
||||
}
|
||||
}
|
61
Selector/Timeline/BaseTimeline.cs
Normal file
61
Selector/Timeline/BaseTimeline.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public abstract class BaseTimeline<T> : ITimeline<T>, IEnumerable<TimelineItem<T>> where T : class
|
||||
{
|
||||
protected List<TimelineItem<T>> Recent = new();
|
||||
public int Count { get => Recent.Count; }
|
||||
public void Clear() => Recent.Clear();
|
||||
public bool SortOnBackDate { get; set; } = true;
|
||||
|
||||
private int? max = 1000;
|
||||
public int? MaxSize
|
||||
{
|
||||
get => max;
|
||||
set => max = value is null ? value : Math.Max(1, (int) value);
|
||||
}
|
||||
|
||||
public virtual void Add(T item) => Add(item, DateTime.UtcNow);
|
||||
public virtual void Add(T item, DateTime timestamp)
|
||||
{
|
||||
Recent.Add(TimelineItem<T>.From(item, timestamp));
|
||||
|
||||
if (timestamp < Recent.Last().Time && SortOnBackDate)
|
||||
{
|
||||
Sort();
|
||||
}
|
||||
|
||||
CheckSize();
|
||||
}
|
||||
|
||||
public void Sort()
|
||||
{
|
||||
Recent = Recent.OrderBy(i => i.Time).ToList();
|
||||
}
|
||||
|
||||
protected void CheckSize()
|
||||
{
|
||||
if (MaxSize is int maxSize && Count > maxSize)
|
||||
{
|
||||
Recent.RemoveRange(0, Count - maxSize);
|
||||
}
|
||||
}
|
||||
|
||||
public T Get()
|
||||
=> Recent.Last().Item;
|
||||
|
||||
public T Get(DateTime at)
|
||||
=> GetTimelineItem(at).Item;
|
||||
public TimelineItem<T> GetTimelineItem(DateTime at)
|
||||
=> Recent
|
||||
.Where(i => i.Time <= at).LastOrDefault();
|
||||
|
||||
public IEnumerator<TimelineItem<T>> GetEnumerator() => Recent.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
21
Selector/Timeline/Interfaces/ITrackTimeline.cs
Normal file
21
Selector/Timeline/Interfaces/ITrackTimeline.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public interface ITrackTimeline<T>: ITimeline<T>
|
||||
{
|
||||
public T Get(FullTrack track);
|
||||
public IEnumerable<T> GetAll(FullTrack track);
|
||||
public IEnumerable<TimelineItem<T>> GetAllTimelineItems(FullTrack track);
|
||||
|
||||
public T Get(SimpleAlbum album);
|
||||
public IEnumerable<T> GetAll(SimpleAlbum album);
|
||||
public IEnumerable<TimelineItem<T>> GetAllTimelineItems(SimpleAlbum album);
|
||||
|
||||
public T Get(SimpleArtist artist);
|
||||
public IEnumerable<T> GetAll(SimpleArtist artist);
|
||||
public IEnumerable<TimelineItem<T>> GetAllTimelineItems(SimpleArtist artist);
|
||||
}
|
||||
}
|
@ -7,67 +7,11 @@ using SpotifyAPI.Web;
|
||||
namespace Selector
|
||||
{
|
||||
public class PlayerTimeline
|
||||
: ITimeline<CurrentlyPlayingContext>,
|
||||
IEnumerable<TimelineItem<CurrentlyPlayingContext>>
|
||||
: BaseTimeline<CurrentlyPlayingContext>, ITrackTimeline<CurrentlyPlayingContext>
|
||||
{
|
||||
|
||||
private List<TimelineItem<CurrentlyPlayingContext>> recentlyPlayed = new();
|
||||
public IEqual EqualityChecker { get; set; }
|
||||
public bool SortOnBackDate { get; set; } = true;
|
||||
public int Count { get => recentlyPlayed.Count; }
|
||||
|
||||
private int? max = 1000;
|
||||
public int? MaxSize {
|
||||
get => max;
|
||||
set {
|
||||
if(value is null)
|
||||
{
|
||||
max = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
max = Math.Max(1, (int) value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(CurrentlyPlayingContext item) => Add(item, DateHelper.FromUnixMilli(item.Timestamp));
|
||||
public void Add(CurrentlyPlayingContext item, DateTime timestamp)
|
||||
{
|
||||
recentlyPlayed.Add(TimelineItem<CurrentlyPlayingContext>.From(item, timestamp));
|
||||
|
||||
if (timestamp < recentlyPlayed.Last().Time && SortOnBackDate)
|
||||
{
|
||||
Sort();
|
||||
}
|
||||
|
||||
CheckSize();
|
||||
}
|
||||
|
||||
public void Sort()
|
||||
{
|
||||
recentlyPlayed = recentlyPlayed
|
||||
.OrderBy(i => i.Time)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void CheckSize()
|
||||
{
|
||||
if (MaxSize is int maxSize && Count > maxSize) {
|
||||
recentlyPlayed.RemoveRange(0, Count - maxSize);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() => recentlyPlayed.Clear();
|
||||
|
||||
public CurrentlyPlayingContext Get()
|
||||
=> recentlyPlayed.Last().Item;
|
||||
|
||||
public CurrentlyPlayingContext Get(DateTime at)
|
||||
=> GetTimelineItem(at)?.Item;
|
||||
public TimelineItem<CurrentlyPlayingContext> GetTimelineItem(DateTime at)
|
||||
=> recentlyPlayed
|
||||
.Where(i => i.Time <= at).LastOrDefault();
|
||||
public override void Add(CurrentlyPlayingContext item) => Add(item, DateHelper.FromUnixMilli(item.Timestamp));
|
||||
|
||||
public CurrentlyPlayingContext Get(FullTrack track)
|
||||
=> GetAll(track)
|
||||
@ -77,8 +21,8 @@ namespace Selector
|
||||
=> GetAllTimelineItems(track)
|
||||
.Select(t => t.Item);
|
||||
|
||||
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullTrack track)
|
||||
=> recentlyPlayed
|
||||
public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullTrack track)
|
||||
=> Recent
|
||||
.Where(i => i.Item.Item is FullTrack iterTrack
|
||||
&& EqualityChecker.IsEqual(iterTrack, track));
|
||||
|
||||
@ -90,8 +34,8 @@ namespace Selector
|
||||
=> GetAllTimelineItems(ep)
|
||||
.Select(t => t.Item);
|
||||
|
||||
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullEpisode ep)
|
||||
=> recentlyPlayed
|
||||
public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullEpisode ep)
|
||||
=> Recent
|
||||
.Where(i => i.Item.Item is FullEpisode iterEp
|
||||
&& EqualityChecker.IsEqual(iterEp, ep));
|
||||
|
||||
@ -103,8 +47,8 @@ namespace Selector
|
||||
=> GetAllTimelineItems(album)
|
||||
.Select(t => t.Item);
|
||||
|
||||
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleAlbum album)
|
||||
=> recentlyPlayed
|
||||
public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleAlbum album)
|
||||
=> Recent
|
||||
.Where(i => i.Item.Item is FullTrack iterTrack
|
||||
&& EqualityChecker.IsEqual(iterTrack.Album, album));
|
||||
|
||||
@ -116,8 +60,8 @@ namespace Selector
|
||||
=> GetAllTimelineItems(artist)
|
||||
.Select(t => t.Item);
|
||||
|
||||
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleArtist artist)
|
||||
=> recentlyPlayed
|
||||
public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleArtist artist)
|
||||
=> Recent
|
||||
.Where(i => i.Item.Item is FullTrack iterTrack
|
||||
&& EqualityChecker.IsEqual(iterTrack.Artists[0], artist));
|
||||
|
||||
@ -129,8 +73,8 @@ namespace Selector
|
||||
=> GetAllTimelineItems(device)
|
||||
.Select(t => t.Item);
|
||||
|
||||
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Device device)
|
||||
=> recentlyPlayed
|
||||
public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Device device)
|
||||
=> Recent
|
||||
.Where(i => EqualityChecker.IsEqual(i.Item.Device, device));
|
||||
|
||||
public CurrentlyPlayingContext Get(Context context)
|
||||
@ -141,11 +85,8 @@ namespace Selector
|
||||
=> GetAllTimelineItems(context)
|
||||
.Select(t => t.Item);
|
||||
|
||||
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Context context)
|
||||
=> recentlyPlayed
|
||||
public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Context context)
|
||||
=> Recent
|
||||
.Where(i => EqualityChecker.IsEqual(i.Item.Context, context));
|
||||
|
||||
public IEnumerator<TimelineItem<CurrentlyPlayingContext>> GetEnumerator() => recentlyPlayed.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
@ -5,17 +5,29 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public abstract class BaseWatcher: IWatcher
|
||||
{
|
||||
protected readonly ILogger<BaseWatcher> Logger;
|
||||
|
||||
public BaseWatcher(ILogger<BaseWatcher> logger = null)
|
||||
{
|
||||
Logger = logger ?? NullLogger<BaseWatcher>.Instance;
|
||||
}
|
||||
|
||||
public abstract Task WatchOne(CancellationToken token);
|
||||
|
||||
public async Task Watch(CancellationToken cancelToken)
|
||||
{
|
||||
Logger.LogDebug("Starting watcher");
|
||||
while (true) {
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
await WatchOne(cancelToken);
|
||||
Logger.LogTrace($"Finished watch one, delaying {PollPeriod}ms...");
|
||||
await Task.Delay(PollPeriod, cancelToken);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ namespace Selector
|
||||
{
|
||||
public class PlayerWatcher: BaseWatcher, IPlayerWatcher
|
||||
{
|
||||
private readonly ILogger<PlayerWatcher> Logger;
|
||||
new private readonly ILogger<PlayerWatcher> Logger;
|
||||
private readonly IPlayerClient spotifyClient;
|
||||
private readonly IEqual eq;
|
||||
|
||||
@ -26,12 +26,13 @@ namespace Selector
|
||||
public event EventHandler<ListeningChangeEventArgs> PlayingChange;
|
||||
|
||||
public CurrentlyPlayingContext Live { get; private set; }
|
||||
public PlayerTimeline Past { get; private set; }
|
||||
public PlayerTimeline Past { get; set; }
|
||||
|
||||
public PlayerWatcher(IPlayerClient spotifyClient,
|
||||
IEqual equalityChecker,
|
||||
ILogger<PlayerWatcher> logger = null,
|
||||
int pollPeriod = 3000) {
|
||||
int pollPeriod = 3000
|
||||
) : base(logger) {
|
||||
|
||||
this.spotifyClient = spotifyClient;
|
||||
eq = equalityChecker;
|
||||
@ -44,7 +45,9 @@ namespace Selector
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try{
|
||||
Logger.LogTrace("Making Spotify call");
|
||||
var polledCurrent = await spotifyClient.GetCurrentPlayback();
|
||||
Logger.LogTrace($"Received Spotify call [{polledCurrent?.DisplayString()}]");
|
||||
|
||||
if (polledCurrent != null) StoreCurrentPlaying(polledCurrent);
|
||||
|
||||
@ -70,14 +73,14 @@ namespace Selector
|
||||
if(previous is null
|
||||
&& (Live.Item is FullTrack || Live.Item is FullEpisode))
|
||||
{
|
||||
Logger.LogDebug($"Playback started: {Live}");
|
||||
Logger.LogDebug($"Playback started: {Live.DisplayString()}");
|
||||
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live));
|
||||
}
|
||||
// STOPPED PLAYBACK
|
||||
else if((previous.Item is FullTrack || previous.Item is FullEpisode)
|
||||
&& Live is null)
|
||||
{
|
||||
Logger.LogDebug($"Playback stopped: {previous}");
|
||||
Logger.LogDebug($"Playback stopped: {previous.DisplayString()}");
|
||||
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live));
|
||||
}
|
||||
// CONTINUING PLAYBACK
|
||||
@ -88,17 +91,17 @@ namespace Selector
|
||||
&& Live.Item is FullTrack currentTrack)
|
||||
{
|
||||
if(!eq.IsEqual(previousTrack, currentTrack)) {
|
||||
Logger.LogDebug($"Track changed: {previousTrack} -> {currentTrack}");
|
||||
Logger.LogDebug($"Track changed: {previousTrack.DisplayString()} -> {currentTrack.DisplayString()}");
|
||||
OnItemChange(ListeningChangeEventArgs.From(previous, Live));
|
||||
}
|
||||
|
||||
if(!eq.IsEqual(previousTrack.Album, currentTrack.Album)) {
|
||||
Logger.LogDebug($"Album changed: {previousTrack.Album} -> {currentTrack.Album}");
|
||||
Logger.LogDebug($"Album changed: {previousTrack.Album.DisplayString()} -> {currentTrack.Album.DisplayString()}");
|
||||
OnAlbumChange(ListeningChangeEventArgs.From(previous, Live));
|
||||
}
|
||||
|
||||
if(!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0])) {
|
||||
Logger.LogDebug($"Artist changed: {previousTrack.Artists[0]} -> {currentTrack.Artists[0]}");
|
||||
Logger.LogDebug($"Artist changed: {previousTrack.Artists.DisplayString()} -> {currentTrack.Artists.DisplayString()}");
|
||||
OnArtistChange(ListeningChangeEventArgs.From(previous, Live));
|
||||
}
|
||||
}
|
||||
@ -115,7 +118,7 @@ namespace Selector
|
||||
&& Live.Item is FullEpisode currentEp)
|
||||
{
|
||||
if(!eq.IsEqual(previousEp, currentEp)) {
|
||||
Logger.LogDebug($"Podcast changed: {previousEp} -> {currentEp}");
|
||||
Logger.LogDebug($"Podcast changed: {previousEp.DisplayString()} -> {currentEp.DisplayString()}");
|
||||
OnItemChange(ListeningChangeEventArgs.From(previous, Live));
|
||||
}
|
||||
}
|
||||
@ -125,13 +128,13 @@ namespace Selector
|
||||
|
||||
// CONTEXT
|
||||
if(!eq.IsEqual(previous.Context, Live.Context)) {
|
||||
Logger.LogDebug($"Context changed: {previous.Context} -> {Live.Context}");
|
||||
Logger.LogDebug($"Context changed: {previous.Context.DisplayString()} -> {Live.Context.DisplayString()}");
|
||||
OnContextChange(ListeningChangeEventArgs.From(previous, Live));
|
||||
}
|
||||
|
||||
// DEVICE
|
||||
if(!eq.IsEqual(previous?.Device, Live?.Device)) {
|
||||
Logger.LogDebug($"Device changed: {previous?.Device} -> {Live?.Device}");
|
||||
Logger.LogDebug($"Device changed: {previous?.Device.DisplayString()} -> {Live?.Device.DisplayString()}");
|
||||
OnDeviceChange(ListeningChangeEventArgs.From(previous, Live));
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,8 @@ namespace Selector
|
||||
{
|
||||
public bool IsRunning { get; }
|
||||
public void Add(IWatcher watcher);
|
||||
|
||||
public void Add(IWatcher watcher, List<IConsumer> consumers);
|
||||
|
||||
public void Start();
|
||||
public void Stop();
|
||||
}
|
||||
|
@ -34,9 +34,14 @@ namespace Selector
|
||||
|
||||
public void Add(IWatcher watcher)
|
||||
{
|
||||
var context = WatcherContext.From(watcher);
|
||||
Add(watcher, default);
|
||||
}
|
||||
|
||||
public void Add(IWatcher watcher, List<IConsumer> consumers)
|
||||
{
|
||||
var context = WatcherContext.From(watcher, consumers);
|
||||
if (IsRunning) context.Start();
|
||||
|
||||
|
||||
Watchers.Add(context);
|
||||
}
|
||||
|
||||
@ -45,7 +50,7 @@ namespace Selector
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Logger.LogDebug($"Starting {Count} watchers");
|
||||
Logger.LogDebug($"Starting {Count} watcher(s)");
|
||||
foreach(var watcher in Watchers)
|
||||
{
|
||||
watcher.Start();
|
||||
@ -55,7 +60,7 @@ namespace Selector
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
Logger.LogDebug($"Stopping {Count} watchers");
|
||||
Logger.LogDebug($"Stopping {Count} watcher(s)");
|
||||
foreach (var watcher in Watchers)
|
||||
{
|
||||
watcher.Stop();
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -9,6 +10,7 @@ namespace Selector
|
||||
public class WatcherContext: IDisposable
|
||||
{
|
||||
public IWatcher Watcher { get; set; }
|
||||
private List<IConsumer> Consumers { get; set; } = new();
|
||||
public bool IsRunning { get; private set; }
|
||||
public Task Task { get; set; }
|
||||
public CancellationTokenSource TokenSource { get; set; }
|
||||
@ -18,11 +20,30 @@ namespace Selector
|
||||
Watcher = watcher;
|
||||
}
|
||||
|
||||
public WatcherContext(IWatcher watcher, List<IConsumer> consumers)
|
||||
{
|
||||
Watcher = watcher;
|
||||
Consumers = consumers ?? new();
|
||||
}
|
||||
|
||||
public static WatcherContext From(IWatcher watcher)
|
||||
{
|
||||
return new(watcher);
|
||||
}
|
||||
|
||||
public static WatcherContext From(IWatcher watcher, List<IConsumer> consumers)
|
||||
{
|
||||
return new(watcher, consumers);
|
||||
}
|
||||
|
||||
public void AddConsumer(IConsumer consumer)
|
||||
{
|
||||
if (IsRunning)
|
||||
consumer.Subscribe(Watcher);
|
||||
|
||||
Consumers.Add(consumer);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (IsRunning)
|
||||
@ -30,6 +51,9 @@ namespace Selector
|
||||
|
||||
IsRunning = true;
|
||||
TokenSource = new();
|
||||
|
||||
Consumers.ForEach(c => c.Subscribe(Watcher));
|
||||
|
||||
Task = Watcher.Watch(TokenSource.Token);
|
||||
Task.ContinueWith(t =>
|
||||
{
|
||||
@ -39,6 +63,8 @@ namespace Selector
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
Consumers.ForEach(c => c.Unsubscribe(Watcher));
|
||||
|
||||
TokenSource.Cancel();
|
||||
IsRunning = false;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user