adding last.fm spotify mappings from watcher event
This commit is contained in:
parent
1ac02d14a8
commit
5084f2bd07
@ -100,6 +100,7 @@ namespace Selector.CLI
|
||||
.ConfigureDb(config);
|
||||
|
||||
services.AddConsumerFactories();
|
||||
services.AddCLIConsumerFactories();
|
||||
if (config.RedisOptions.Enabled)
|
||||
{
|
||||
Console.WriteLine("> Adding caching consumers...");
|
||||
|
33
Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs
Normal file
33
Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Selector.Model;
|
||||
|
||||
namespace Selector.CLI.Consumer
|
||||
{
|
||||
public interface IMappingPersisterFactory
|
||||
{
|
||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
|
||||
}
|
||||
|
||||
public class MappingPersisterFactory : IMappingPersisterFactory
|
||||
{
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly IServiceScopeFactory ScopeFactory;
|
||||
|
||||
public MappingPersisterFactory(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory = null, LastFmCredentials creds = null)
|
||||
{
|
||||
LoggerFactory = loggerFactory;
|
||||
ScopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null)
|
||||
{
|
||||
return Task.FromResult<IPlayerConsumer>(new MappingPersister(
|
||||
watcher,
|
||||
ScopeFactory,
|
||||
LoggerFactory.CreateLogger<MappingPersister>()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
148
Selector.CLI/Consumer/MappingPersister.cs
Normal file
148
Selector.CLI/Consumer/MappingPersister.cs
Normal file
@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Selector.Model;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector.CLI.Consumer
|
||||
{
|
||||
/// <summary>
|
||||
/// Save name -> Spotify URI mappings as new objects come through the watcher without making extra queries of the Spotify API
|
||||
/// </summary>
|
||||
public class MappingPersister: IPlayerConsumer
|
||||
{
|
||||
protected readonly IPlayerWatcher Watcher;
|
||||
protected readonly IServiceScopeFactory ScopeFactory;
|
||||
protected readonly ILogger<MappingPersister> Logger;
|
||||
|
||||
public CancellationToken CancelToken { get; set; }
|
||||
|
||||
public MappingPersister(
|
||||
IPlayerWatcher watcher,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<MappingPersister> logger = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
Watcher = watcher;
|
||||
ScopeFactory = scopeFactory;
|
||||
Logger = logger ?? NullLogger<MappingPersister>.Instance;
|
||||
CancelToken = token;
|
||||
}
|
||||
|
||||
public void Callback(object sender, ListeningChangeEventArgs e)
|
||||
{
|
||||
if (e.Current is null) return;
|
||||
|
||||
Task.Run(async () => {
|
||||
try
|
||||
{
|
||||
await AsyncCallback(e);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
Logger.LogWarning("Failed to update database, likely a duplicate Spotify URI");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "Error occured during callback");
|
||||
}
|
||||
}, CancelToken);
|
||||
}
|
||||
|
||||
public async Task AsyncCallback(ListeningChangeEventArgs e)
|
||||
{
|
||||
using var serviceScope = ScopeFactory.CreateScope();
|
||||
using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id } });
|
||||
|
||||
if (e.Current.Item is FullTrack track)
|
||||
{
|
||||
var mappingRepo = serviceScope.ServiceProvider.GetRequiredService<IScrobbleMappingRepository>();
|
||||
|
||||
if(!mappingRepo.GetTracks().Select(t => t.SpotifyUri).Contains(track.Uri))
|
||||
{
|
||||
mappingRepo.Add(new TrackLastfmSpotifyMapping()
|
||||
{
|
||||
SpotifyUri = track.Uri,
|
||||
LastfmTrackName = track.Name,
|
||||
LastfmArtistName = track.Artists.FirstOrDefault()?.Name
|
||||
});
|
||||
}
|
||||
|
||||
if (!mappingRepo.GetAlbums().Select(t => t.SpotifyUri).Contains(track.Album.Uri))
|
||||
{
|
||||
mappingRepo.Add(new AlbumLastfmSpotifyMapping()
|
||||
{
|
||||
SpotifyUri = track.Album.Uri,
|
||||
LastfmAlbumName = track.Album.Name,
|
||||
LastfmArtistName = track.Album.Artists.FirstOrDefault()?.Name
|
||||
});
|
||||
}
|
||||
|
||||
var artistUris = mappingRepo.GetArtists().Select(t => t.SpotifyUri).ToArray();
|
||||
foreach (var artist in track.Artists)
|
||||
{
|
||||
if (!artistUris.Contains(artist.Uri))
|
||||
{
|
||||
mappingRepo.Add(new ArtistLastfmSpotifyMapping()
|
||||
{
|
||||
SpotifyUri = artist.Uri,
|
||||
LastfmArtistName = artist.Name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mappingRepo.Save();
|
||||
|
||||
Logger.LogDebug("Adding Spotify <-> Last.fm mapping [{username}]", e.SpotifyUsername);
|
||||
}
|
||||
else if (e.Current.Item is FullEpisode episode)
|
||||
{
|
||||
Logger.LogDebug("Ignoring podcast episdoe [{episode}]", episode.DisplayString());
|
||||
}
|
||||
else if (e.Current.Item is null)
|
||||
{
|
||||
Logger.LogDebug("Skipping play count pulling for null item [{context}]", e.Current.DisplayString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError("Unknown item pulled from API [{item}]", 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Quartz;
|
||||
using Selector.Cache.Extensions;
|
||||
using Selector.CLI.Consumer;
|
||||
using Selector.CLI.Jobs;
|
||||
using Selector.Extensions;
|
||||
using Selector.Model;
|
||||
@ -142,5 +143,13 @@ namespace Selector.CLI.Extensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCLIConsumerFactories(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IMappingPersisterFactory, MappingPersisterFactory>();
|
||||
services.AddTransient<MappingPersisterFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ namespace Selector.CLI
|
||||
|
||||
public enum Consumers
|
||||
{
|
||||
AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher, PlayCounter
|
||||
AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher, PlayCounter, MappingPersister
|
||||
}
|
||||
|
||||
public class RedisOptions
|
||||
|
@ -34,6 +34,10 @@
|
||||
<ProjectReference Include="..\Selector.Event\Selector.Event.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Consumer\" />
|
||||
<None Remove="Consumer\Factory\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
@ -49,4 +53,8 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Consumer\" />
|
||||
<Folder Include="Consumer\Factory\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -13,6 +13,7 @@ using Selector.Model;
|
||||
using Selector.Model.Extensions;
|
||||
using Selector.Events;
|
||||
using System.Collections.Concurrent;
|
||||
using Selector.CLI.Consumer;
|
||||
|
||||
namespace Selector.CLI
|
||||
{
|
||||
@ -35,6 +36,9 @@ namespace Selector.CLI
|
||||
|
||||
private readonly IPublisherFactory PublisherFactory;
|
||||
private readonly ICacheWriterFactory CacheWriterFactory;
|
||||
|
||||
private readonly IMappingPersisterFactory MappingPersisterFactory;
|
||||
|
||||
private ConcurrentDictionary<string, IWatcherCollection> Watchers { get; set; } = new();
|
||||
|
||||
public DbWatcherService(
|
||||
@ -53,6 +57,8 @@ namespace Selector.CLI
|
||||
IPublisherFactory publisherFactory = null,
|
||||
ICacheWriterFactory cacheWriterFactory = null,
|
||||
|
||||
IMappingPersisterFactory mappingPersisterFactory = null,
|
||||
|
||||
IUserEventFirerFactory userEventFirerFactory = null
|
||||
)
|
||||
{
|
||||
@ -71,6 +77,8 @@ namespace Selector.CLI
|
||||
|
||||
PublisherFactory = publisherFactory;
|
||||
CacheWriterFactory = cacheWriterFactory;
|
||||
|
||||
MappingPersisterFactory = mappingPersisterFactory;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
@ -130,6 +138,8 @@ namespace Selector.CLI
|
||||
if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.Get());
|
||||
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.Get());
|
||||
|
||||
if (MappingPersisterFactory is not null) consumers.Add(await MappingPersisterFactory.Get());
|
||||
|
||||
if (UserEventFirerFactory is not null) consumers.Add(await UserEventFirerFactory.Get());
|
||||
|
||||
if (dbWatcher.User.LastFmConnected())
|
||||
|
@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Selector.Cache;
|
||||
using Selector.CLI.Consumer;
|
||||
|
||||
namespace Selector.CLI
|
||||
{
|
||||
@ -136,6 +137,10 @@ namespace Selector.CLI
|
||||
Logger.LogError("No Last.fm username provided, skipping play counter");
|
||||
}
|
||||
break;
|
||||
|
||||
case Consumers.MappingPersister:
|
||||
consumers.Add(await ServiceProvider.GetService<MappingPersisterFactory>().Get());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,19 @@
|
||||
"ClientSecret": "",
|
||||
"Equality": "uri",
|
||||
"Watcher": {
|
||||
"localenabled": false,
|
||||
"LocalEnabled": false,
|
||||
"Instances": [
|
||||
// {
|
||||
// "Name": "test watcher",
|
||||
// "type": "playlist",
|
||||
// "PlaylistUri": "spotify:playlist:4o5IArXmDeJByESaUJoEFS",
|
||||
// "pollperiod": 2000
|
||||
// },
|
||||
{
|
||||
"type": "player",
|
||||
"lastfmusername": "sarsoo",
|
||||
"pollperiod": 2000,
|
||||
"consumers": [ "audiofeaturescache", "cachewriter", "publisher", "playcounter" ]
|
||||
"consumers": [ "audiofeaturescache", "cachewriter", "publisher", "playcounter", "mappingpersister" ]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public class ScrobbleAlbumMapping : ScrobbleMapping
|
||||
{
|
||||
public string AlbumName { get; set; }
|
||||
@ -18,6 +19,7 @@ namespace Selector
|
||||
}
|
||||
|
||||
private SimpleAlbum result;
|
||||
public SimpleAlbum Album => result;
|
||||
public override object Result => result;
|
||||
|
||||
public override string Query => $"{AlbumName} {ArtistName}";
|
||||
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public class ScrobbleArtistMapping : ScrobbleMapping
|
||||
{
|
||||
public string ArtistName { get; set; }
|
||||
@ -18,6 +19,7 @@ namespace Selector
|
||||
}
|
||||
|
||||
private FullArtist result;
|
||||
public FullArtist Artist => result;
|
||||
public override object Result => result;
|
||||
|
||||
public override string Query => ArtistName;
|
||||
|
@ -11,6 +11,9 @@ namespace Selector
|
||||
Track, Album, Artist
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map Last.fm resources to Spotify resources using the Spotify search endpoint before saving mappings to database
|
||||
/// </summary>
|
||||
public abstract class ScrobbleMapping : IOperation
|
||||
{
|
||||
private readonly ILogger<ScrobbleMapping> logger;
|
||||
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public class ScrobbleTrackMapping : ScrobbleMapping
|
||||
{
|
||||
public string TrackName { get; set; }
|
||||
@ -20,6 +21,7 @@ namespace Selector
|
||||
}
|
||||
|
||||
private FullTrack result;
|
||||
public FullTrack Track => result;
|
||||
public override object Result => result;
|
||||
|
||||
public override string Query => $"{TrackName} {ArtistName}";
|
||||
|
Loading…
Reference in New Issue
Block a user