added play counter and last.fm auth to CLI
This commit is contained in:
parent
2cd93a61aa
commit
1ea964b341
@ -34,6 +34,8 @@ namespace Selector.CLI
|
|||||||
/// Spotify app secret
|
/// Spotify app secret
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClientSecret { get; set; }
|
public string ClientSecret { get; set; }
|
||||||
|
public string LastfmClient { get; set; }
|
||||||
|
public string LastfmSecret { get; set; }
|
||||||
public WatcherOptions WatcherOptions { get; set; } = new();
|
public WatcherOptions WatcherOptions { get; set; } = new();
|
||||||
public DatabaseOptions DatabaseOptions { get; set; } = new();
|
public DatabaseOptions DatabaseOptions { get; set; } = new();
|
||||||
public RedisOptions RedisOptions { get; set; } = new();
|
public RedisOptions RedisOptions { get; set; } = new();
|
||||||
@ -60,6 +62,7 @@ namespace Selector.CLI
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string AccessKey { get; set; }
|
public string AccessKey { get; set; }
|
||||||
public string RefreshKey { get; set; }
|
public string RefreshKey { get; set; }
|
||||||
|
public string LastFmUsername { get; set; }
|
||||||
public int PollPeriod { get; set; } = 5000;
|
public int PollPeriod { get; set; } = 5000;
|
||||||
public WatcherType Type { get; set; } = WatcherType.Player;
|
public WatcherType Type { get; set; } = WatcherType.Player;
|
||||||
public List<Consumers> Consumers { get; set; } = default;
|
public List<Consumers> Consumers { get; set; } = default;
|
||||||
@ -71,7 +74,7 @@ namespace Selector.CLI
|
|||||||
|
|
||||||
enum Consumers
|
enum Consumers
|
||||||
{
|
{
|
||||||
AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher
|
AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher, PlayCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
class DatabaseOptions {
|
class DatabaseOptions {
|
||||||
|
@ -9,6 +9,7 @@ using NLog.Extensions.Logging;
|
|||||||
|
|
||||||
using Selector.Model;
|
using Selector.Model;
|
||||||
using Selector.Cache;
|
using Selector.Cache;
|
||||||
|
using IF.Lastfm.Core.Api;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.CLI
|
namespace Selector.CLI
|
||||||
@ -45,6 +46,18 @@ namespace Selector.CLI
|
|||||||
//services.AddSingleton<IRefreshTokenFactoryProvider, RefreshTokenFactoryProvider>();
|
//services.AddSingleton<IRefreshTokenFactoryProvider, RefreshTokenFactoryProvider>();
|
||||||
services.AddSingleton<IRefreshTokenFactoryProvider, CachingRefreshTokenFactoryProvider>();
|
services.AddSingleton<IRefreshTokenFactoryProvider, CachingRefreshTokenFactoryProvider>();
|
||||||
|
|
||||||
|
if(config.LastfmClient is not null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("> Adding Last.fm credentials...");
|
||||||
|
|
||||||
|
var lastAuth = new LastAuth(config.LastfmClient, config.LastfmSecret);
|
||||||
|
services.AddSingleton<LastAuth>(lastAuth);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("> No Last.fm credentials, skipping init...");
|
||||||
|
}
|
||||||
|
|
||||||
// DB
|
// DB
|
||||||
if (config.DatabaseOptions.Enabled)
|
if (config.DatabaseOptions.Enabled)
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using IF.Lastfm.Core.Api;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@ -25,6 +26,7 @@ namespace Selector.CLI
|
|||||||
private readonly IWatcherFactory WatcherFactory;
|
private readonly IWatcherFactory WatcherFactory;
|
||||||
private readonly IWatcherCollectionFactory WatcherCollectionFactory;
|
private readonly IWatcherCollectionFactory WatcherCollectionFactory;
|
||||||
private readonly IRefreshTokenFactoryProvider SpotifyFactory;
|
private readonly IRefreshTokenFactoryProvider SpotifyFactory;
|
||||||
|
private readonly LastAuth LastAuth;
|
||||||
|
|
||||||
private readonly IDatabaseAsync Cache;
|
private readonly IDatabaseAsync Cache;
|
||||||
private readonly ISubscriber Subscriber;
|
private readonly ISubscriber Subscriber;
|
||||||
@ -37,6 +39,7 @@ namespace Selector.CLI
|
|||||||
IRefreshTokenFactoryProvider spotifyFactory,
|
IRefreshTokenFactoryProvider spotifyFactory,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IOptions<RootOptions> config,
|
IOptions<RootOptions> config,
|
||||||
|
LastAuth lastAuth = null,
|
||||||
IDatabaseAsync cache = null,
|
IDatabaseAsync cache = null,
|
||||||
ISubscriber subscriber = null
|
ISubscriber subscriber = null
|
||||||
) {
|
) {
|
||||||
@ -46,6 +49,7 @@ namespace Selector.CLI
|
|||||||
WatcherFactory = watcherFactory;
|
WatcherFactory = watcherFactory;
|
||||||
WatcherCollectionFactory = watcherCollectionFactory;
|
WatcherCollectionFactory = watcherCollectionFactory;
|
||||||
SpotifyFactory = spotifyFactory;
|
SpotifyFactory = spotifyFactory;
|
||||||
|
LastAuth = lastAuth;
|
||||||
Cache = cache;
|
Cache = cache;
|
||||||
Subscriber = subscriber;
|
Subscriber = subscriber;
|
||||||
|
|
||||||
@ -128,6 +132,22 @@ namespace Selector.CLI
|
|||||||
var pub = new PublisherFactory(Subscriber, LoggerFactory);
|
var pub = new PublisherFactory(Subscriber, LoggerFactory);
|
||||||
consumers.Add(await pub.Get());
|
consumers.Add(await pub.Get());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Consumers.PlayCounter:
|
||||||
|
if(!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername))
|
||||||
|
{
|
||||||
|
if(LastAuth is null) throw new ArgumentNullException("No Last Auth Injected");
|
||||||
|
|
||||||
|
var client = new LastfmClient(LastAuth);
|
||||||
|
|
||||||
|
var playCount = new PlayCounterFactory(LoggerFactory, client: client, creds: new(){ Username = watcherOption.LastFmUsername });
|
||||||
|
consumers.Add(await playCount.Get());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogError("No Last.fm usernmae provided, skipping play counter");
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,9 @@
|
|||||||
{
|
{
|
||||||
"name": "Player Watcher",
|
"name": "Player Watcher",
|
||||||
"type": "player",
|
"type": "player",
|
||||||
|
"lastfmusername": "sarsoo",
|
||||||
"pollperiod": 2000,
|
"pollperiod": 2000,
|
||||||
"consumers": [ "audiofeaturescache", "cachewriter", "publisher" ]
|
"consumers": [ "audiofeaturescache", "cachewriter", "publisher", "playcounter" ]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
49
Selector/Consumers/Factory/PlayCounterFactory.cs
Normal file
49
Selector/Consumers/Factory/PlayCounterFactory.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using IF.Lastfm.Core.Api;
|
||||||
|
|
||||||
|
namespace Selector
|
||||||
|
{
|
||||||
|
public interface IPlayCounterFactory
|
||||||
|
{
|
||||||
|
public Task<IConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayCounterFactory: IPlayCounterFactory {
|
||||||
|
|
||||||
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
|
private readonly LastfmClient Client;
|
||||||
|
private readonly LastFmCredentials Creds;
|
||||||
|
|
||||||
|
public PlayCounterFactory(ILoggerFactory loggerFactory, LastfmClient client = null, LastFmCredentials creds = null)
|
||||||
|
{
|
||||||
|
LoggerFactory = loggerFactory;
|
||||||
|
Client = client;
|
||||||
|
Creds = creds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
|
||||||
|
{
|
||||||
|
var client = fmClient ?? Client;
|
||||||
|
|
||||||
|
if(client is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("No Last.fm client provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PlayCounter(
|
||||||
|
watcher,
|
||||||
|
client.Track,
|
||||||
|
client.Album,
|
||||||
|
client.Artist,
|
||||||
|
client.User,
|
||||||
|
credentials: creds ?? Creds,
|
||||||
|
LoggerFactory.CreateLogger<PlayCounter>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
190
Selector/Consumers/PlayCounter.cs
Normal file
190
Selector/Consumers/PlayCounter.cs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
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;
|
||||||
|
using IF.Lastfm.Core.Api;
|
||||||
|
|
||||||
|
namespace Selector
|
||||||
|
{
|
||||||
|
public class PlayCounter : IConsumer
|
||||||
|
{
|
||||||
|
protected readonly IPlayerWatcher Watcher;
|
||||||
|
protected readonly ITrackApi TrackClient;
|
||||||
|
protected readonly IAlbumApi AlbumClient;
|
||||||
|
protected readonly IArtistApi ArtistClient;
|
||||||
|
protected readonly IUserApi UserClient;
|
||||||
|
protected readonly LastFmCredentials Credentials;
|
||||||
|
protected readonly ILogger<PlayCounter> Logger;
|
||||||
|
|
||||||
|
protected event EventHandler<PlayCount> NewPlayCount;
|
||||||
|
|
||||||
|
public CancellationToken CancelToken { get; set; }
|
||||||
|
|
||||||
|
public AnalysedTrackTimeline Timeline { get; set; } = new();
|
||||||
|
|
||||||
|
public PlayCounter(
|
||||||
|
IPlayerWatcher watcher,
|
||||||
|
ITrackApi trackClient,
|
||||||
|
IAlbumApi albumClient,
|
||||||
|
IArtistApi artistClient,
|
||||||
|
IUserApi userClient,
|
||||||
|
LastFmCredentials credentials = null,
|
||||||
|
ILogger<PlayCounter> logger = null,
|
||||||
|
CancellationToken token = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Watcher = watcher;
|
||||||
|
TrackClient = trackClient;
|
||||||
|
AlbumClient = albumClient;
|
||||||
|
ArtistClient = artistClient;
|
||||||
|
UserClient = userClient;
|
||||||
|
Credentials = credentials;
|
||||||
|
Logger = logger ?? NullLogger<PlayCounter>.Instance;
|
||||||
|
CancelToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Callback(object sender, ListeningChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Current is null) return;
|
||||||
|
|
||||||
|
Task.Run(() => { return AsyncCallback(e); }, CancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AsyncCallback(ListeningChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Current.Item is FullTrack track)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Making Last.fm call");
|
||||||
|
|
||||||
|
var trackInfo = TrackClient.GetInfoAsync(track.Name, track.Artists[0].Name, username: Credentials?.Username);
|
||||||
|
var albumInfo = AlbumClient.GetInfoAsync(track.Album.Name, track.Album.Artists[0].Name, username: Credentials?.Username);
|
||||||
|
var artistInfo = ArtistClient.GetInfoAsync(track.Artists[0].Name);
|
||||||
|
// TODO: Null checking on credentials
|
||||||
|
var userInfo = UserClient.GetInfoAsync(Credentials.Username);
|
||||||
|
|
||||||
|
await Task.WhenAll(new Task[] { trackInfo, albumInfo, artistInfo, userInfo });
|
||||||
|
|
||||||
|
int? trackCount = null, albumCount = null, artistCount = null, userCount = null;
|
||||||
|
|
||||||
|
if (trackInfo.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
if (trackInfo.Result.Success)
|
||||||
|
{
|
||||||
|
trackCount = trackInfo.Result.Content.UserPlayCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"Track info error [{e.Username}] [{trackInfo.Result.Status}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogError(trackInfo.Exception, $"Track info task faulted, [{e.Username}] [{e.Current.DisplayString()}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albumInfo.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
if (albumInfo.Result.Success)
|
||||||
|
{
|
||||||
|
albumCount = albumInfo.Result.Content.UserPlayCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"Album info error [{e.Username}] [{albumInfo.Result.Status}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogError(albumInfo.Exception, $"Album info task faulted, [{e.Username}] [{e.Current.DisplayString()}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Add artist count
|
||||||
|
|
||||||
|
if (userInfo.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
if (userInfo.Result.Success)
|
||||||
|
{
|
||||||
|
userCount = userInfo.Result.Content.Playcount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"User info error [{e.Username}] [{userInfo.Result.Status}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogError(userInfo.Exception, $"User info task faulted, [{e.Username}] [{e.Current.DisplayString()}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug($"Adding Last.fm data [{Credentials.Username}/{e.Username}] [{track.DisplayString()}], track: {trackCount}, album: {albumCount}, artist: {artistCount}, user: {userCount}");
|
||||||
|
|
||||||
|
OnNewPlayCount(new()
|
||||||
|
{
|
||||||
|
Track = trackCount,
|
||||||
|
Album = albumCount,
|
||||||
|
Artist = artistCount,
|
||||||
|
User = userCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnNewPlayCount(PlayCount args)
|
||||||
|
{
|
||||||
|
NewPlayCount?.Invoke(this, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayCount
|
||||||
|
{
|
||||||
|
public int? Track { get; set; }
|
||||||
|
public int? Album { get; set; }
|
||||||
|
public int? Artist { get; set; }
|
||||||
|
public int? User { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LastFmCredentials
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -35,7 +35,7 @@ namespace Selector
|
|||||||
|
|
||||||
public static string DisplayString(this Context context) => $"{context.Type}, {context.Uri}";
|
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 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}dB, Mode {feature.Mode}, Speech {feature.Speechiness}, Tempo {feature.Tempo}BPM, Time Sig. {feature.TimeSignature}, Valence {feature.Valence}";
|
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} dB, Mode {feature.Mode}, Speech {feature.Speechiness}, Tempo {feature.Tempo} BPM, Time Sig. {feature.TimeSignature}, Valence {feature.Valence}";
|
||||||
|
|
||||||
public static string DisplayString(this IEnumerable<SimpleArtist> artists) => string.Join(", ", artists.Select(a => a.DisplayString()));
|
public static string DisplayString(this IEnumerable<SimpleArtist> artists) => string.Join(", ", artists.Select(a => a.DisplayString()));
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user