adding playlist watcher
This commit is contained in:
parent
fb88da4337
commit
83e071f5c3
@ -9,6 +9,7 @@ using Selector.Extensions;
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Selector.CLI
|
||||
{
|
||||
@ -31,9 +32,14 @@ namespace Selector.CLI
|
||||
{
|
||||
try
|
||||
{
|
||||
CreateHostBuilder(Environment.GetCommandLineArgs(), ConfigureDefault, ConfigureDefaultNlog)
|
||||
.Build()
|
||||
.Run();
|
||||
var host = CreateHostBuilder(Environment.GetCommandLineArgs(),ConfigureDefault, ConfigureDefaultNlog)
|
||||
.Build();
|
||||
|
||||
var logger = host.Services.GetRequiredService<ILogger<HostCommand>>();
|
||||
var env = host.Services.GetRequiredService<IHostEnvironment>();
|
||||
SetupExceptionHandling(logger, env);
|
||||
|
||||
host.Run();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@ -44,6 +50,22 @@ namespace Selector.CLI
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void SetupExceptionHandling(ILogger logger, IHostEnvironment env)
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += (obj, e) =>
|
||||
{
|
||||
if(e.ExceptionObject is Exception ex)
|
||||
{
|
||||
logger.LogError(ex as Exception, "Unhandled exception thrown");
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static RootOptions ConfigureOptions(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
services.Configure<RootOptions>(options =>
|
||||
|
@ -95,41 +95,48 @@ namespace Selector.CLI
|
||||
watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod);
|
||||
break;
|
||||
case WatcherType.Playlist:
|
||||
throw new NotImplementedException("Playlist watchers not implemented");
|
||||
// break;
|
||||
var playlistWatcher = await WatcherFactory.Get<PlaylistWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod) as PlaylistWatcher;
|
||||
playlistWatcher.config = new() { PlaylistId = watcherOption.PlaylistUri };
|
||||
|
||||
watcher = playlistWatcher;
|
||||
break;
|
||||
}
|
||||
|
||||
List<IConsumer> consumers = new();
|
||||
foreach(var consumer in watcherOption.Consumers)
|
||||
|
||||
if (watcherOption.Consumers is not null)
|
||||
{
|
||||
switch(consumer)
|
||||
foreach (var consumer in watcherOption.Consumers)
|
||||
{
|
||||
case Consumers.AudioFeatures:
|
||||
consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>().Get(spotifyFactory));
|
||||
break;
|
||||
switch (consumer)
|
||||
{
|
||||
case Consumers.AudioFeatures:
|
||||
consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>().Get(spotifyFactory));
|
||||
break;
|
||||
|
||||
case Consumers.AudioFeaturesCache:
|
||||
consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>().Get(spotifyFactory));
|
||||
break;
|
||||
case Consumers.AudioFeaturesCache:
|
||||
consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>().Get(spotifyFactory));
|
||||
break;
|
||||
|
||||
case Consumers.CacheWriter:
|
||||
consumers.Add(await ServiceProvider.GetService<CacheWriterFactory>().Get());
|
||||
break;
|
||||
case Consumers.CacheWriter:
|
||||
consumers.Add(await ServiceProvider.GetService<CacheWriterFactory>().Get());
|
||||
break;
|
||||
|
||||
case Consumers.Publisher:
|
||||
consumers.Add(await ServiceProvider.GetService<PublisherFactory>().Get());
|
||||
break;
|
||||
case Consumers.Publisher:
|
||||
consumers.Add(await ServiceProvider.GetService<PublisherFactory>().Get());
|
||||
break;
|
||||
|
||||
case Consumers.PlayCounter:
|
||||
if(!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername))
|
||||
{
|
||||
consumers.Add(await ServiceProvider.GetService<PlayCounterFactory>().Get(creds: new() { Username = watcherOption.LastFmUsername }));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError("No Last.fm username provided, skipping play counter");
|
||||
}
|
||||
break;
|
||||
case Consumers.PlayCounter:
|
||||
if (!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername))
|
||||
{
|
||||
consumers.Add(await ServiceProvider.GetService<PlayCounterFactory>().Get(creds: new() { Username = watcherOption.LastFmUsername }));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError("No Last.fm username provided, skipping play counter");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,5 +31,6 @@
|
||||
<logger name="*" minlevel="Debug" writeTo="logfile" />
|
||||
<logger name="*" minlevel="Trace" writeTo="tracefile" />
|
||||
<logger name="Selector.*" minlevel="Debug" writeTo="logconsole" />
|
||||
<logger name="Microsoft.*" minlevel="Warning" writeTo="logconsole" />
|
||||
</rules>
|
||||
</nlog>
|
@ -10,7 +10,7 @@ using StackExchange.Redis;
|
||||
|
||||
namespace Selector.Cache
|
||||
{
|
||||
public class CacheWriter : IConsumer
|
||||
public class CacheWriter : IPlayerConsumer
|
||||
{
|
||||
private readonly IPlayerWatcher Watcher;
|
||||
private readonly IDatabaseAsync Db;
|
||||
|
@ -22,7 +22,7 @@ namespace Selector.Cache
|
||||
Db = db;
|
||||
}
|
||||
|
||||
public async Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
|
||||
public async Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
|
||||
{
|
||||
var config = await spotifyFactory.GetConfig();
|
||||
var client = new SpotifyClient(config);
|
||||
|
@ -9,7 +9,7 @@ using StackExchange.Redis;
|
||||
namespace Selector.Cache
|
||||
{
|
||||
public interface ICacheWriterFactory {
|
||||
public Task<IConsumer> Get(IPlayerWatcher watcher = null);
|
||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
|
||||
}
|
||||
|
||||
public class CacheWriterFactory: ICacheWriterFactory {
|
||||
@ -25,9 +25,9 @@ namespace Selector.Cache
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public Task<IConsumer> Get(IPlayerWatcher watcher = null)
|
||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null)
|
||||
{
|
||||
return Task.FromResult<IConsumer>(new CacheWriter(
|
||||
return Task.FromResult<IPlayerConsumer>(new CacheWriter(
|
||||
watcher,
|
||||
Cache,
|
||||
LoggerFactory.CreateLogger<CacheWriter>()
|
||||
|
@ -28,7 +28,7 @@ namespace Selector.Cache
|
||||
Creds = creds;
|
||||
}
|
||||
|
||||
public Task<IConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
|
||||
public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
|
||||
{
|
||||
var client = fmClient ?? Client;
|
||||
|
||||
@ -37,7 +37,7 @@ namespace Selector.Cache
|
||||
throw new ArgumentNullException("No Last.fm client provided");
|
||||
}
|
||||
|
||||
return Task.FromResult<IConsumer>(new PlayCounterCaching(
|
||||
return Task.FromResult<IPlayerConsumer>(new PlayCounterCaching(
|
||||
watcher,
|
||||
client.Track,
|
||||
client.Album,
|
||||
|
@ -9,7 +9,7 @@ using StackExchange.Redis;
|
||||
namespace Selector.Cache
|
||||
{
|
||||
public interface IPublisherFactory {
|
||||
public Task<IConsumer> Get(IPlayerWatcher watcher = null);
|
||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
|
||||
}
|
||||
|
||||
public class PublisherFactory: IPublisherFactory {
|
||||
@ -25,9 +25,9 @@ namespace Selector.Cache
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public Task<IConsumer> Get(IPlayerWatcher watcher = null)
|
||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null)
|
||||
{
|
||||
return Task.FromResult<IConsumer>(new Publisher(
|
||||
return Task.FromResult<IPlayerConsumer>(new Publisher(
|
||||
watcher,
|
||||
Subscriber,
|
||||
LoggerFactory.CreateLogger<Publisher>()
|
||||
|
@ -10,7 +10,7 @@ using StackExchange.Redis;
|
||||
|
||||
namespace Selector.Cache
|
||||
{
|
||||
public class Publisher : IConsumer
|
||||
public class Publisher : IPlayerConsumer
|
||||
{
|
||||
private readonly IPlayerWatcher Watcher;
|
||||
private readonly ISubscriber Subscriber;
|
||||
|
@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Selector.Events
|
||||
{
|
||||
public class UserEventFirer : IConsumer
|
||||
public class UserEventFirer : IPlayerConsumer
|
||||
{
|
||||
protected readonly IPlayerWatcher Watcher;
|
||||
protected readonly ILogger<UserEventFirer> Logger;
|
||||
|
@ -133,5 +133,15 @@ namespace Selector.Tests
|
||||
VolumePercent = volume
|
||||
};
|
||||
}
|
||||
|
||||
public static FullPlaylist FullPlaylist(string name, string id = null, string snapshotId = null)
|
||||
{
|
||||
return new FullPlaylist()
|
||||
{
|
||||
Name = name,
|
||||
Id = id ?? name,
|
||||
SnapshotId = snapshotId ?? id ?? name
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
99
Selector.Tests/Watcher/PlaylistWatcher.cs
Normal file
99
Selector.Tests/Watcher/PlaylistWatcher.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using FluentAssertions;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Selector.Tests
|
||||
{
|
||||
public class PlaylistWatcherTests
|
||||
{
|
||||
public static IEnumerable<object[]> CurrentPlaylistData =>
|
||||
new List<object[]>
|
||||
{
|
||||
new object[] { new List<FullPlaylist>(){
|
||||
Helper.FullPlaylist("playlist1"),
|
||||
Helper.FullPlaylist("playlist1"),
|
||||
Helper.FullPlaylist("playlist1"),
|
||||
Helper.FullPlaylist("playlist1")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CurrentPlaylistData))]
|
||||
public async void CurrentPlaylist(List<FullPlaylist> playing)
|
||||
{
|
||||
var playlistDequeue = new Queue<FullPlaylist>(playing);
|
||||
|
||||
var spotMock = new Mock<ISpotifyClient>();
|
||||
|
||||
spotMock.Setup(s => s.Playlists.Get(It.IsAny<string>()).Result).Returns(playlistDequeue.Dequeue);
|
||||
|
||||
var config = new PlaylistWatcherConfig() { PlaylistId = "spotify:playlist:test" };
|
||||
var watcher = new PlaylistWatcher(config, spotMock.Object);
|
||||
|
||||
for (var i = 0; i < playing.Count; i++)
|
||||
{
|
||||
await watcher.WatchOne();
|
||||
watcher.Live.Should().Be(playing[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> EventsData =>
|
||||
new List<object[]>
|
||||
{
|
||||
// NO CHANGING
|
||||
new object[] { new List<FullPlaylist>(){
|
||||
Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"),
|
||||
Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"),
|
||||
Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"),
|
||||
},
|
||||
// to raise
|
||||
new List<string>(){ },
|
||||
// to not raise
|
||||
new List<string>(){ "SnapshotChange" }
|
||||
},
|
||||
// CHANGING SNAPSHOT
|
||||
new object[] { new List<FullPlaylist>(){
|
||||
Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"),
|
||||
Helper.FullPlaylist("Playlist", snapshotId: "snapshot2"),
|
||||
Helper.FullPlaylist("Playlist", snapshotId: "snapshot2"),
|
||||
},
|
||||
// to raise
|
||||
new List<string>(){ "SnapshotChange" },
|
||||
// to not raise
|
||||
new List<string>(){ }
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(EventsData))]
|
||||
public async void Events(List<FullPlaylist> playing, List<string> toRaise, List<string> toNotRaise)
|
||||
{
|
||||
var playlistDequeue = new Queue<FullPlaylist>(playing);
|
||||
|
||||
var spotMock = new Mock<ISpotifyClient>();
|
||||
|
||||
spotMock.Setup(s => s.Playlists.Get(It.IsAny<string>()).Result).Returns(playlistDequeue.Dequeue);
|
||||
|
||||
var config = new PlaylistWatcherConfig() { PlaylistId = "spotify:playlist:test" };
|
||||
var watcher = new PlaylistWatcher(config, spotMock.Object);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
@ -38,6 +38,15 @@ namespace Selector.Web
|
||||
{
|
||||
OptionsHelper.ConfigureOptions(options, Configuration);
|
||||
});
|
||||
services.Configure<RedisOptions>(options =>
|
||||
{
|
||||
Configuration.GetSection(string.Join(':', RootOptions.Key, RedisOptions.Key)).Bind(options);
|
||||
});
|
||||
services.Configure<NowPlayingOptions>(options =>
|
||||
{
|
||||
Configuration.GetSection(string.Join(':', RootOptions.Key, NowPlayingOptions.Key)).Bind(options);
|
||||
});
|
||||
|
||||
var config = OptionsHelper.ConfigureOptions(Configuration);
|
||||
|
||||
services.Configure<SpotifyAppCredentials>(options =>
|
||||
|
@ -9,7 +9,7 @@ using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public class AudioFeatureInjector : IConsumer
|
||||
public class AudioFeatureInjector : IPlayerConsumer
|
||||
{
|
||||
protected readonly IPlayerWatcher Watcher;
|
||||
protected readonly ITracksClient TrackClient;
|
||||
|
@ -10,7 +10,7 @@ namespace Selector
|
||||
{
|
||||
public interface IAudioFeatureInjectorFactory
|
||||
{
|
||||
public Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null);
|
||||
public Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null);
|
||||
}
|
||||
|
||||
public class AudioFeatureInjectorFactory: IAudioFeatureInjectorFactory {
|
||||
@ -22,7 +22,7 @@ namespace Selector
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public async Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
|
||||
public async Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
|
||||
{
|
||||
var config = await spotifyFactory.GetConfig();
|
||||
var client = new SpotifyClient(config);
|
||||
|
@ -10,7 +10,7 @@ namespace Selector
|
||||
{
|
||||
public interface IPlayCounterFactory
|
||||
{
|
||||
public Task<IConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null);
|
||||
public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null);
|
||||
}
|
||||
|
||||
public class PlayCounterFactory: IPlayCounterFactory {
|
||||
@ -26,7 +26,7 @@ namespace Selector
|
||||
Creds = creds;
|
||||
}
|
||||
|
||||
public Task<IConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
|
||||
public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
|
||||
{
|
||||
var client = fmClient ?? Client;
|
||||
|
||||
@ -35,7 +35,7 @@ namespace Selector
|
||||
throw new ArgumentNullException("No Last.fm client provided");
|
||||
}
|
||||
|
||||
return Task.FromResult<IConsumer>(new PlayCounter(
|
||||
return Task.FromResult<IPlayerConsumer>(new PlayCounter(
|
||||
watcher,
|
||||
client.Track,
|
||||
client.Album,
|
||||
|
@ -5,8 +5,18 @@ namespace Selector
|
||||
{
|
||||
public interface IConsumer
|
||||
{
|
||||
public void Callback(object sender, ListeningChangeEventArgs e);
|
||||
public void Subscribe(IWatcher watch = null);
|
||||
public void Unsubscribe(IWatcher watch = null);
|
||||
}
|
||||
|
||||
public interface IConsumer<T>: IConsumer
|
||||
{
|
||||
public void Callback(object sender, T e);
|
||||
}
|
||||
|
||||
public interface IPlayerConsumer: IConsumer<ListeningChangeEventArgs>
|
||||
{ }
|
||||
|
||||
public interface IPlaylistConsumer : IConsumer<PlaylistChangeEventArgs>
|
||||
{ }
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ using IF.Lastfm.Core.Api.Helpers;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public class PlayCounter : IConsumer
|
||||
public class PlayCounter : IPlayerConsumer
|
||||
{
|
||||
protected readonly IPlayerWatcher Watcher;
|
||||
protected readonly ITrackApi TrackClient;
|
||||
|
@ -30,7 +30,7 @@ namespace Selector
|
||||
}
|
||||
}
|
||||
|
||||
public class WebHook : IConsumer
|
||||
public class WebHook : IPlayerConsumer
|
||||
{
|
||||
protected readonly IPlayerWatcher Watcher;
|
||||
protected readonly HttpClient HttpClient;
|
||||
|
21
Selector/Equality/PlayableItemEqualityComparer.cs
Normal file
21
Selector/Equality/PlayableItemEqualityComparer.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector.Equality
|
||||
{
|
||||
public class PlayableItemEqualityComparer: IEqualityComparer<PlaylistTrack<IPlayableItem>>
|
||||
{
|
||||
public bool Equals(PlaylistTrack<IPlayableItem> x, PlaylistTrack<IPlayableItem> y)
|
||||
{
|
||||
return x.GetUri().Equals(y.GetUri());
|
||||
}
|
||||
|
||||
public int GetHashCode([DisallowNull] PlaylistTrack<IPlayableItem> obj)
|
||||
{
|
||||
return obj.GetUri().GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ namespace Selector
|
||||
{
|
||||
public static class SpotifyExtensions
|
||||
{
|
||||
public static string DisplayString(this FullPlaylist playlist) => $"{playlist.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;
|
||||
@ -48,5 +50,37 @@ namespace Selector
|
||||
public static bool IsSpokenWord(this TrackAudioFeatures feature) => feature.Speechiness > 0.66f;
|
||||
public static bool IsSpeechAndMusic(this TrackAudioFeatures feature) => feature.Speechiness is >= 0.33f and <= 0.66f;
|
||||
public static bool IsNotSpeech(this TrackAudioFeatures feature) => feature.Speechiness < 0.33f;
|
||||
|
||||
public static string GetUri(this IPlayableItem y)
|
||||
{
|
||||
if (y is FullTrack track)
|
||||
{
|
||||
return track.Uri;
|
||||
}
|
||||
else if (y is FullEpisode episode)
|
||||
{
|
||||
return episode.Uri;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(nameof(y));
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetUri(this PlaylistTrack<IPlayableItem> y)
|
||||
{
|
||||
if (y.Track is FullTrack track)
|
||||
{
|
||||
return track.Uri;
|
||||
}
|
||||
else if (y.Track is FullEpisode episode)
|
||||
{
|
||||
return episode.Uri;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(nameof(y.Track));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageReference Include="SpotifyAPI.Web" Version="6.2.2" />
|
||||
<PackageReference Include="Inflatable.Lastfm" Version="1.2.0" />
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="System.Linq.Async" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector
|
||||
@ -29,4 +30,38 @@ namespace Selector
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class PlaylistChangeEventArgs : EventArgs
|
||||
{
|
||||
public FullPlaylist Previous { get; set; }
|
||||
public FullPlaylist Current { get; set; }
|
||||
/// <summary>
|
||||
/// Spotify Username
|
||||
/// </summary>
|
||||
public string SpotifyUsername { get; set; }
|
||||
/// <summary>
|
||||
/// String Id for watcher, used to hold user Db Id
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string Id { get; set; }
|
||||
Timeline<FullPlaylist> Timeline { get; set; }
|
||||
ICollection<PlaylistTrack<IPlayableItem>> CurrentTracks { get; set; }
|
||||
ICollection<PlaylistTrack<IPlayableItem>> AddedTracks { get; set; }
|
||||
ICollection<PlaylistTrack<IPlayableItem>> RemovedTracks { get; set; }
|
||||
|
||||
public static PlaylistChangeEventArgs From(FullPlaylist previous, FullPlaylist current, Timeline<FullPlaylist> timeline, ICollection<PlaylistTrack<IPlayableItem>> tracks = null, ICollection<PlaylistTrack<IPlayableItem>> addedTracks = null, ICollection<PlaylistTrack<IPlayableItem>> removedTracks = null, string id = null, string username = null)
|
||||
{
|
||||
return new PlaylistChangeEventArgs()
|
||||
{
|
||||
Previous = previous,
|
||||
Current = current,
|
||||
Timeline = timeline,
|
||||
CurrentTracks = tracks,
|
||||
AddedTracks = addedTracks,
|
||||
RemovedTracks = removedTracks,
|
||||
Id = id,
|
||||
SpotifyUsername = username
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
Selector/Watcher/Interfaces/IPlaylistWatcher.cs
Normal file
20
Selector/Watcher/Interfaces/IPlaylistWatcher.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public interface IPlaylistWatcher: IWatcher
|
||||
{
|
||||
public event EventHandler<PlaylistChangeEventArgs> NetworkPoll;
|
||||
public event EventHandler<PlaylistChangeEventArgs> SnapshotChange;
|
||||
|
||||
public event EventHandler<PlaylistChangeEventArgs> TracksAdded;
|
||||
public event EventHandler<PlaylistChangeEventArgs> TracksRemoved;
|
||||
|
||||
public event EventHandler<PlaylistChangeEventArgs> NameChanged;
|
||||
public event EventHandler<PlaylistChangeEventArgs> DescriptionChanged;
|
||||
|
||||
public FullPlaylist Live { get; }
|
||||
public Timeline<FullPlaylist> Past { get; }
|
||||
}
|
||||
}
|
256
Selector/Watcher/PlaylistWatcher.cs
Normal file
256
Selector/Watcher/PlaylistWatcher.cs
Normal file
@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System.Linq;
|
||||
using Selector.Equality;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public class PlaylistWatcherConfig
|
||||
{
|
||||
public string PlaylistId { get; set; }
|
||||
public bool PullTracks { get; set; } = true;
|
||||
}
|
||||
|
||||
public class PlaylistWatcher: BaseWatcher, IPlaylistWatcher
|
||||
{
|
||||
new private readonly ILogger<PlaylistWatcher> Logger;
|
||||
private readonly ISpotifyClient spotifyClient;
|
||||
|
||||
public PlaylistWatcherConfig config { get; set; }
|
||||
|
||||
public event EventHandler<PlaylistChangeEventArgs> NetworkPoll;
|
||||
public event EventHandler<PlaylistChangeEventArgs> SnapshotChange;
|
||||
|
||||
public event EventHandler<PlaylistChangeEventArgs> TracksAdded;
|
||||
public event EventHandler<PlaylistChangeEventArgs> TracksRemoved;
|
||||
|
||||
public event EventHandler<PlaylistChangeEventArgs> NameChanged;
|
||||
public event EventHandler<PlaylistChangeEventArgs> DescriptionChanged;
|
||||
|
||||
public FullPlaylist Live { get; private set; }
|
||||
private List<PlaylistTrack<IPlayableItem>> CurrentTracks { get; set; }
|
||||
private ICollection<PlaylistTrack<IPlayableItem>> LastAddedTracks { get; set; }
|
||||
private ICollection<PlaylistTrack<IPlayableItem>> LastRemovedTracks { get; set; }
|
||||
|
||||
private FullPlaylist Previous { get; set; }
|
||||
public Timeline<FullPlaylist> Past { get; set; } = new();
|
||||
|
||||
private IEqualityComparer<PlaylistTrack<IPlayableItem>> EqualityComparer = new PlayableItemEqualityComparer();
|
||||
|
||||
public PlaylistWatcher(PlaylistWatcherConfig config,
|
||||
ISpotifyClient spotifyClient,
|
||||
ILogger<PlaylistWatcher> logger = null,
|
||||
int pollPeriod = 3000
|
||||
) : base(logger) {
|
||||
|
||||
this.spotifyClient = spotifyClient;
|
||||
this.config = config;
|
||||
Logger = logger ?? NullLogger<PlaylistWatcher>.Instance;
|
||||
PollPeriod = pollPeriod;
|
||||
}
|
||||
|
||||
public override Task Reset()
|
||||
{
|
||||
Previous = null;
|
||||
Live = null;
|
||||
Past = new();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task WatchOne(CancellationToken token = default)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
using var logScope = Logger.BeginScope(new Dictionary<string, object> { { "playlist_id", config.PlaylistId } });
|
||||
|
||||
try{
|
||||
string id;
|
||||
|
||||
if(config.PlaylistId.Contains(':'))
|
||||
{
|
||||
id = config.PlaylistId.Split(':').Last();
|
||||
}
|
||||
else
|
||||
{
|
||||
id = config.PlaylistId;
|
||||
}
|
||||
|
||||
Logger.LogTrace("Making Spotify call");
|
||||
var polledCurrent = await spotifyClient.Playlists.Get(id);
|
||||
Logger.LogTrace("Received Spotify call [{context}]", polledCurrent?.DisplayString());
|
||||
|
||||
if (polledCurrent != null) StoreCurrentPlaying(polledCurrent);
|
||||
|
||||
// swap new item into live and bump existing down to previous
|
||||
Previous = Live;
|
||||
Live = polledCurrent;
|
||||
|
||||
OnNetworkPoll(GetEvent());
|
||||
|
||||
await CheckSnapshot();
|
||||
CheckStringValues();
|
||||
}
|
||||
catch(APIUnauthorizedException e)
|
||||
{
|
||||
Logger.LogDebug($"Unauthorised error: [{e.Message}] (should be refreshed and retried?)");
|
||||
//throw e;
|
||||
}
|
||||
catch(APITooManyRequestsException e)
|
||||
{
|
||||
Logger.LogDebug($"Too many requests error: [{e.Message}]");
|
||||
await Task.Delay(e.RetryAfter, token);
|
||||
// throw e;
|
||||
}
|
||||
catch(APIException e)
|
||||
{
|
||||
Logger.LogDebug($"API error: [{e.Message}]");
|
||||
// throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckSnapshot()
|
||||
{
|
||||
switch(Previous, Live)
|
||||
{
|
||||
case (null, not null): // gone null
|
||||
await PageLiveTracks();
|
||||
break;
|
||||
case (not null, null): // went non-null
|
||||
break;
|
||||
case (not null, not null): // continuing non-null
|
||||
|
||||
if (Live.SnapshotId != Previous.SnapshotId)
|
||||
{
|
||||
Logger.LogDebug("Snapshot Id changed: {previous} -> {current}", Previous.SnapshotId, Live.SnapshotId);
|
||||
await PageLiveTracks();
|
||||
OnSnapshotChange(GetEvent());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PageLiveTracks()
|
||||
{
|
||||
if (config.PullTracks)
|
||||
{
|
||||
Logger.LogDebug("Paging current tracks");
|
||||
|
||||
var newCurrentTracks = await spotifyClient.Paginate(Live.Tracks).ToListAsync();
|
||||
|
||||
Logger.LogTrace("Completed paging current tracks");
|
||||
|
||||
if (CurrentTracks is not null)
|
||||
{
|
||||
Logger.LogDebug("Identifying diffs");
|
||||
|
||||
LastAddedTracks = newCurrentTracks.Except(CurrentTracks, EqualityComparer).ToArray();
|
||||
LastRemovedTracks = CurrentTracks.Except(newCurrentTracks, EqualityComparer).ToArray();
|
||||
|
||||
if (LastAddedTracks.Count > 0)
|
||||
{
|
||||
OnTracksAdded(GetEvent());
|
||||
}
|
||||
|
||||
if (LastRemovedTracks.Count > 0)
|
||||
{
|
||||
OnTracksRemoved(GetEvent());
|
||||
}
|
||||
|
||||
Logger.LogTrace("Completed identifying diffs");
|
||||
}
|
||||
|
||||
CurrentTracks = newCurrentTracks;
|
||||
}
|
||||
}
|
||||
|
||||
private PlaylistChangeEventArgs GetEvent() => PlaylistChangeEventArgs.From(Previous, Live, Past, tracks: CurrentTracks, addedTracks: LastAddedTracks, removedTracks: LastRemovedTracks, id: Id, username: SpotifyUsername);
|
||||
|
||||
private void CheckStringValues()
|
||||
{
|
||||
switch (Previous, Live)
|
||||
{
|
||||
case (null, not null): // gone null
|
||||
break;
|
||||
case (not null, null): // went non-null
|
||||
break;
|
||||
case (not null, not null): // continuing non-null
|
||||
|
||||
if (!Live.Name.Equals(Previous.Name))
|
||||
{
|
||||
Logger.LogDebug("Name changed: {previous} -> {current}", Previous.SnapshotId, Live.SnapshotId);
|
||||
OnNameChanged(GetEvent());
|
||||
}
|
||||
|
||||
if (!Live.Description.Equals(Previous.Description))
|
||||
{
|
||||
Logger.LogDebug("Description changed: {previous} -> {current}", Previous.SnapshotId, Live.SnapshotId);
|
||||
OnDescriptionChanged(GetEvent());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store currently playing in last plays. Determine whether new list or appending required
|
||||
/// </summary>
|
||||
/// <param name="current">New currently playing to store</param>
|
||||
private void StoreCurrentPlaying(FullPlaylist current)
|
||||
{
|
||||
Past?.Add(current);
|
||||
}
|
||||
|
||||
#region Event Firers
|
||||
protected virtual void OnNetworkPoll(PlaylistChangeEventArgs args)
|
||||
{
|
||||
Logger.LogTrace("Firing network poll event");
|
||||
|
||||
NetworkPoll?.Invoke(this, args);
|
||||
}
|
||||
|
||||
protected virtual void OnSnapshotChange(PlaylistChangeEventArgs args)
|
||||
{
|
||||
Logger.LogTrace("Firing snapshot change event");
|
||||
|
||||
SnapshotChange?.Invoke(this, args);
|
||||
}
|
||||
|
||||
protected virtual void OnTracksAdded(PlaylistChangeEventArgs args)
|
||||
{
|
||||
Logger.LogTrace("Firing tracks added event");
|
||||
|
||||
TracksAdded?.Invoke(this, args);
|
||||
}
|
||||
|
||||
protected virtual void OnTracksRemoved(PlaylistChangeEventArgs args)
|
||||
{
|
||||
Logger.LogTrace("Firing tracks removed event");
|
||||
|
||||
TracksRemoved?.Invoke(this, args);
|
||||
}
|
||||
|
||||
protected virtual void OnNameChanged(PlaylistChangeEventArgs args)
|
||||
{
|
||||
Logger.LogTrace("Firing name changed event");
|
||||
|
||||
NameChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
protected virtual void OnDescriptionChanged(PlaylistChangeEventArgs args)
|
||||
{
|
||||
Logger.LogTrace("Firing description changed event");
|
||||
|
||||
DescriptionChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -40,10 +40,25 @@ namespace Selector
|
||||
Id = id
|
||||
};
|
||||
}
|
||||
//else if (typeof(T).IsAssignableFrom(typeof(PlaylistWatcher)))
|
||||
//{
|
||||
else if (typeof(T).IsAssignableFrom(typeof(PlaylistWatcher)))
|
||||
{
|
||||
var config = await spotifyFactory.GetConfig();
|
||||
var client = new SpotifyClient(config);
|
||||
|
||||
//}
|
||||
// TODO: catch spotify exceptions
|
||||
var user = await client.UserProfile.Current();
|
||||
|
||||
return new PlaylistWatcher(
|
||||
new(),
|
||||
client,
|
||||
LoggerFactory?.CreateLogger<PlaylistWatcher>() ?? NullLogger<PlaylistWatcher>.Instance,
|
||||
pollPeriod: pollPeriod
|
||||
)
|
||||
{
|
||||
SpotifyUsername = user.DisplayName,
|
||||
Id = id
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Type unsupported");
|
||||
|
Loading…
Reference in New Issue
Block a user