adding playlist watcher
This commit is contained in:
parent
fb88da4337
commit
83e071f5c3
@ -9,6 +9,7 @@ using Selector.Extensions;
|
|||||||
using System;
|
using System;
|
||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using System.CommandLine.Invocation;
|
using System.CommandLine.Invocation;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Selector.CLI
|
namespace Selector.CLI
|
||||||
{
|
{
|
||||||
@ -31,9 +32,14 @@ namespace Selector.CLI
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CreateHostBuilder(Environment.GetCommandLineArgs(), ConfigureDefault, ConfigureDefaultNlog)
|
var host = CreateHostBuilder(Environment.GetCommandLineArgs(),ConfigureDefault, ConfigureDefaultNlog)
|
||||||
.Build()
|
.Build();
|
||||||
.Run();
|
|
||||||
|
var logger = host.Services.GetRequiredService<ILogger<HostCommand>>();
|
||||||
|
var env = host.Services.GetRequiredService<IHostEnvironment>();
|
||||||
|
SetupExceptionHandling(logger, env);
|
||||||
|
|
||||||
|
host.Run();
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch(Exception ex)
|
||||||
{
|
{
|
||||||
@ -44,6 +50,22 @@ namespace Selector.CLI
|
|||||||
return 0;
|
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)
|
public static RootOptions ConfigureOptions(HostBuilderContext context, IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.Configure<RootOptions>(options =>
|
services.Configure<RootOptions>(options =>
|
||||||
|
@ -95,11 +95,17 @@ namespace Selector.CLI
|
|||||||
watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod);
|
watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod);
|
||||||
break;
|
break;
|
||||||
case WatcherType.Playlist:
|
case WatcherType.Playlist:
|
||||||
throw new NotImplementedException("Playlist watchers not implemented");
|
var playlistWatcher = await WatcherFactory.Get<PlaylistWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod) as PlaylistWatcher;
|
||||||
// break;
|
playlistWatcher.config = new() { PlaylistId = watcherOption.PlaylistUri };
|
||||||
|
|
||||||
|
watcher = playlistWatcher;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<IConsumer> consumers = new();
|
List<IConsumer> consumers = new();
|
||||||
|
|
||||||
|
if (watcherOption.Consumers is not null)
|
||||||
|
{
|
||||||
foreach (var consumer in watcherOption.Consumers)
|
foreach (var consumer in watcherOption.Consumers)
|
||||||
{
|
{
|
||||||
switch (consumer)
|
switch (consumer)
|
||||||
@ -132,6 +138,7 @@ namespace Selector.CLI
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watcherCollection.Add(watcher, consumers);
|
watcherCollection.Add(watcher, consumers);
|
||||||
}
|
}
|
||||||
|
@ -31,5 +31,6 @@
|
|||||||
<logger name="*" minlevel="Debug" writeTo="logfile" />
|
<logger name="*" minlevel="Debug" writeTo="logfile" />
|
||||||
<logger name="*" minlevel="Trace" writeTo="tracefile" />
|
<logger name="*" minlevel="Trace" writeTo="tracefile" />
|
||||||
<logger name="Selector.*" minlevel="Debug" writeTo="logconsole" />
|
<logger name="Selector.*" minlevel="Debug" writeTo="logconsole" />
|
||||||
|
<logger name="Microsoft.*" minlevel="Warning" writeTo="logconsole" />
|
||||||
</rules>
|
</rules>
|
||||||
</nlog>
|
</nlog>
|
@ -10,7 +10,7 @@ using StackExchange.Redis;
|
|||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public class CacheWriter : IConsumer
|
public class CacheWriter : IPlayerConsumer
|
||||||
{
|
{
|
||||||
private readonly IPlayerWatcher Watcher;
|
private readonly IPlayerWatcher Watcher;
|
||||||
private readonly IDatabaseAsync Db;
|
private readonly IDatabaseAsync Db;
|
||||||
|
@ -22,7 +22,7 @@ namespace Selector.Cache
|
|||||||
Db = db;
|
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 config = await spotifyFactory.GetConfig();
|
||||||
var client = new SpotifyClient(config);
|
var client = new SpotifyClient(config);
|
||||||
|
@ -9,7 +9,7 @@ using StackExchange.Redis;
|
|||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public interface ICacheWriterFactory {
|
public interface ICacheWriterFactory {
|
||||||
public Task<IConsumer> Get(IPlayerWatcher watcher = null);
|
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheWriterFactory: ICacheWriterFactory {
|
public class CacheWriterFactory: ICacheWriterFactory {
|
||||||
@ -25,9 +25,9 @@ namespace Selector.Cache
|
|||||||
LoggerFactory = loggerFactory;
|
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,
|
watcher,
|
||||||
Cache,
|
Cache,
|
||||||
LoggerFactory.CreateLogger<CacheWriter>()
|
LoggerFactory.CreateLogger<CacheWriter>()
|
||||||
|
@ -28,7 +28,7 @@ namespace Selector.Cache
|
|||||||
Creds = creds;
|
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;
|
var client = fmClient ?? Client;
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ namespace Selector.Cache
|
|||||||
throw new ArgumentNullException("No Last.fm client provided");
|
throw new ArgumentNullException("No Last.fm client provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult<IConsumer>(new PlayCounterCaching(
|
return Task.FromResult<IPlayerConsumer>(new PlayCounterCaching(
|
||||||
watcher,
|
watcher,
|
||||||
client.Track,
|
client.Track,
|
||||||
client.Album,
|
client.Album,
|
||||||
|
@ -9,7 +9,7 @@ using StackExchange.Redis;
|
|||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public interface IPublisherFactory {
|
public interface IPublisherFactory {
|
||||||
public Task<IConsumer> Get(IPlayerWatcher watcher = null);
|
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PublisherFactory: IPublisherFactory {
|
public class PublisherFactory: IPublisherFactory {
|
||||||
@ -25,9 +25,9 @@ namespace Selector.Cache
|
|||||||
LoggerFactory = loggerFactory;
|
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,
|
watcher,
|
||||||
Subscriber,
|
Subscriber,
|
||||||
LoggerFactory.CreateLogger<Publisher>()
|
LoggerFactory.CreateLogger<Publisher>()
|
||||||
|
@ -10,7 +10,7 @@ using StackExchange.Redis;
|
|||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public class Publisher : IConsumer
|
public class Publisher : IPlayerConsumer
|
||||||
{
|
{
|
||||||
private readonly IPlayerWatcher Watcher;
|
private readonly IPlayerWatcher Watcher;
|
||||||
private readonly ISubscriber Subscriber;
|
private readonly ISubscriber Subscriber;
|
||||||
|
@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
|
|
||||||
namespace Selector.Events
|
namespace Selector.Events
|
||||||
{
|
{
|
||||||
public class UserEventFirer : IConsumer
|
public class UserEventFirer : IPlayerConsumer
|
||||||
{
|
{
|
||||||
protected readonly IPlayerWatcher Watcher;
|
protected readonly IPlayerWatcher Watcher;
|
||||||
protected readonly ILogger<UserEventFirer> Logger;
|
protected readonly ILogger<UserEventFirer> Logger;
|
||||||
|
@ -133,5 +133,15 @@ namespace Selector.Tests
|
|||||||
VolumePercent = volume
|
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);
|
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);
|
var config = OptionsHelper.ConfigureOptions(Configuration);
|
||||||
|
|
||||||
services.Configure<SpotifyAppCredentials>(options =>
|
services.Configure<SpotifyAppCredentials>(options =>
|
||||||
|
@ -9,7 +9,7 @@ using SpotifyAPI.Web;
|
|||||||
|
|
||||||
namespace Selector
|
namespace Selector
|
||||||
{
|
{
|
||||||
public class AudioFeatureInjector : IConsumer
|
public class AudioFeatureInjector : IPlayerConsumer
|
||||||
{
|
{
|
||||||
protected readonly IPlayerWatcher Watcher;
|
protected readonly IPlayerWatcher Watcher;
|
||||||
protected readonly ITracksClient TrackClient;
|
protected readonly ITracksClient TrackClient;
|
||||||
|
@ -10,7 +10,7 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
public interface IAudioFeatureInjectorFactory
|
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 {
|
public class AudioFeatureInjectorFactory: IAudioFeatureInjectorFactory {
|
||||||
@ -22,7 +22,7 @@ namespace Selector
|
|||||||
LoggerFactory = loggerFactory;
|
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 config = await spotifyFactory.GetConfig();
|
||||||
var client = new SpotifyClient(config);
|
var client = new SpotifyClient(config);
|
||||||
|
@ -10,7 +10,7 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
public interface IPlayCounterFactory
|
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 {
|
public class PlayCounterFactory: IPlayCounterFactory {
|
||||||
@ -26,7 +26,7 @@ namespace Selector
|
|||||||
Creds = creds;
|
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;
|
var client = fmClient ?? Client;
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ namespace Selector
|
|||||||
throw new ArgumentNullException("No Last.fm client provided");
|
throw new ArgumentNullException("No Last.fm client provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult<IConsumer>(new PlayCounter(
|
return Task.FromResult<IPlayerConsumer>(new PlayCounter(
|
||||||
watcher,
|
watcher,
|
||||||
client.Track,
|
client.Track,
|
||||||
client.Album,
|
client.Album,
|
||||||
|
@ -5,8 +5,18 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
public interface IConsumer
|
public interface IConsumer
|
||||||
{
|
{
|
||||||
public void Callback(object sender, ListeningChangeEventArgs e);
|
|
||||||
public void Subscribe(IWatcher watch = null);
|
public void Subscribe(IWatcher watch = null);
|
||||||
public void Unsubscribe(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
|
namespace Selector
|
||||||
{
|
{
|
||||||
public class PlayCounter : IConsumer
|
public class PlayCounter : IPlayerConsumer
|
||||||
{
|
{
|
||||||
protected readonly IPlayerWatcher Watcher;
|
protected readonly IPlayerWatcher Watcher;
|
||||||
protected readonly ITrackApi TrackClient;
|
protected readonly ITrackApi TrackClient;
|
||||||
|
@ -30,7 +30,7 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WebHook : IConsumer
|
public class WebHook : IPlayerConsumer
|
||||||
{
|
{
|
||||||
protected readonly IPlayerWatcher Watcher;
|
protected readonly IPlayerWatcher Watcher;
|
||||||
protected readonly HttpClient HttpClient;
|
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 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 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 SimpleAlbum album) => $"{album.Name} / {album.Artists?.DisplayString()}";
|
||||||
public static string DisplayString(this SimpleArtist artist) => artist.Name;
|
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 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 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 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="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||||
<PackageReference Include="SpotifyAPI.Web" Version="6.2.2" />
|
<PackageReference Include="SpotifyAPI.Web" Version="6.2.2" />
|
||||||
<PackageReference Include="Inflatable.Lastfm" Version="1.2.0" />
|
<PackageReference Include="Inflatable.Lastfm" Version="1.2.0" />
|
||||||
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="System.Linq.Async" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector
|
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
|
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
|
else
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Type unsupported");
|
throw new ArgumentException("Type unsupported");
|
||||||
|
Loading…
Reference in New Issue
Block a user