Compare commits
6 Commits
49b9b17837
...
b3ae8e65ac
Author | SHA1 | Date | |
---|---|---|---|
b3ae8e65ac | |||
bfc7a0db34 | |||
7e8a9d1e29 | |||
df986e86ee | |||
17b1f464dd | |||
6814ebd72c |
Dockerfile.CLIDockerfile.Web
Selector.AppleMusic
AppleMusicApi.csAppleMusicApiProvider.csAppleTimeline.cs
Consumer
Events.csExceptions
AppleMusicException.csForbiddenException.csRateLimitException.csServiceException.csUnauthorisedException.cs
Extensions
JsonContext.csJwt.csModel
Selector.AppleMusic.csprojWatcher
Selector.CLI
Selector.Cache
Selector.Event
Selector.LastFm
Selector.MAUI
Selector.Model
ApplicationDbContext.csApplicationUser.cs
Extensions
Listen
Migrations
20250329231051_adding_apple_music_user_properties.Designer.cs20250329231051_adding_apple_music_user_properties.cs20250331203003_add_apple_listen.Designer.cs20250331203003_add_apple_listen.csApplicationDbContextModelSnapshot.cs
Scrobble
Selector.Model.csprojSelector.SignalR
Selector.Spotify
ConfigFactory
Consumer
Credentials.csCurrentlyPlayingDTO.csEquality
@ -2,6 +2,7 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base
|
|||||||
|
|
||||||
COPY *.sln .
|
COPY *.sln .
|
||||||
COPY Selector/*.csproj ./Selector/
|
COPY Selector/*.csproj ./Selector/
|
||||||
|
COPY Selector.AppleMusic/*.csproj ./Selector.AppleMusic/
|
||||||
COPY Selector.Cache/*.csproj ./Selector.Cache/
|
COPY Selector.Cache/*.csproj ./Selector.Cache/
|
||||||
COPY Selector.Data/*.csproj ./Selector.Data/
|
COPY Selector.Data/*.csproj ./Selector.Data/
|
||||||
COPY Selector.Event/*.csproj ./Selector.Event/
|
COPY Selector.Event/*.csproj ./Selector.Event/
|
||||||
|
@ -10,6 +10,7 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base
|
|||||||
|
|
||||||
COPY *.sln .
|
COPY *.sln .
|
||||||
COPY Selector/*.csproj ./Selector/
|
COPY Selector/*.csproj ./Selector/
|
||||||
|
COPY Selector.AppleMusic/*.csproj ./Selector.AppleMusic/
|
||||||
COPY Selector.Cache/*.csproj ./Selector.Cache/
|
COPY Selector.Cache/*.csproj ./Selector.Cache/
|
||||||
COPY Selector.Data/*.csproj ./Selector.Data/
|
COPY Selector.Data/*.csproj ./Selector.Data/
|
||||||
COPY Selector.Event/*.csproj ./Selector.Event/
|
COPY Selector.Event/*.csproj ./Selector.Event/
|
||||||
|
55
Selector.AppleMusic/AppleMusicApi.cs
Normal file
55
Selector.AppleMusic/AppleMusicApi.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Selector.AppleMusic.Exceptions;
|
||||||
|
using Selector.AppleMusic.Model;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic;
|
||||||
|
|
||||||
|
public class AppleMusicApi(HttpClient client, string developerToken, string userToken)
|
||||||
|
{
|
||||||
|
private static readonly string _apiBaseUrl = "https://api.music.apple.com/v1";
|
||||||
|
private readonly AppleJsonContext _appleJsonContext = AppleJsonContext.Default;
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> MakeRequest(HttpMethod httpMethod, string requestUri)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(httpMethod, _apiBaseUrl + requestUri);
|
||||||
|
request.Headers.Add("Authorization", "Bearer " + developerToken);
|
||||||
|
request.Headers.Add("Music-User-Token", userToken);
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckResponse(HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new UnauthorisedException { StatusCode = response.StatusCode };
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
throw new ForbiddenException { StatusCode = response.StatusCode };
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
|
{
|
||||||
|
throw new RateLimitException { StatusCode = response.StatusCode };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AppleMusicException { StatusCode = response.StatusCode };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RecentlyPlayedTracksResponse> GetRecentlyPlayedTracks()
|
||||||
|
{
|
||||||
|
var response = await MakeRequest(HttpMethod.Get, "/me/recent/played/tracks");
|
||||||
|
|
||||||
|
CheckResponse(response);
|
||||||
|
|
||||||
|
var parsed = await response.Content.ReadFromJsonAsync(_appleJsonContext.RecentlyPlayedTracksResponse);
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
16
Selector.AppleMusic/AppleMusicApiProvider.cs
Normal file
16
Selector.AppleMusic/AppleMusicApiProvider.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using Selector.Web.Apple;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic;
|
||||||
|
|
||||||
|
public class AppleMusicApiProvider(HttpClient client)
|
||||||
|
{
|
||||||
|
public AppleMusicApi GetApi(string developerKey, string teamId, string keyId, string userKey)
|
||||||
|
{
|
||||||
|
var jwtGenerator = new TokenGenerator(developerKey, teamId, keyId);
|
||||||
|
var developerToken = jwtGenerator.Generate();
|
||||||
|
|
||||||
|
var api = new AppleMusicApi(client, developerToken, userKey);
|
||||||
|
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
}
|
135
Selector.AppleMusic/AppleTimeline.cs
Normal file
135
Selector.AppleMusic/AppleTimeline.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
using Selector.AppleMusic.Model;
|
||||||
|
using Selector.AppleMusic.Watcher;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic;
|
||||||
|
|
||||||
|
public class AppleTimeline : Timeline<AppleMusicCurrentlyPlayingContext>
|
||||||
|
{
|
||||||
|
public List<AppleMusicCurrentlyPlayingContext> Add(IEnumerable<Track> tracks)
|
||||||
|
=> Add(tracks
|
||||||
|
.Select(x => new AppleMusicCurrentlyPlayingContext()
|
||||||
|
{
|
||||||
|
Track = x,
|
||||||
|
FirstSeen = DateTime.UtcNow,
|
||||||
|
}).ToList());
|
||||||
|
|
||||||
|
public List<AppleMusicCurrentlyPlayingContext> Add(List<AppleMusicCurrentlyPlayingContext> items)
|
||||||
|
{
|
||||||
|
var newItems = new List<AppleMusicCurrentlyPlayingContext>();
|
||||||
|
|
||||||
|
if (items == null || !items.Any())
|
||||||
|
{
|
||||||
|
return newItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Recent.Any())
|
||||||
|
{
|
||||||
|
Recent.AddRange(items.Select(x =>
|
||||||
|
TimelineItem<AppleMusicCurrentlyPlayingContext>.From(x, DateTime.UtcNow)));
|
||||||
|
return newItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Recent
|
||||||
|
.TakeLast(items.Count)
|
||||||
|
.Select(x => x.Item)
|
||||||
|
.SequenceEqual(items, new AppleMusicCurrentlyPlayingContextComparer()))
|
||||||
|
{
|
||||||
|
return newItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (found, startIdx) = Loop(items, 0);
|
||||||
|
|
||||||
|
TimelineItem<AppleMusicCurrentlyPlayingContext>? popped = null;
|
||||||
|
if (found == 0)
|
||||||
|
{
|
||||||
|
var (foundOffseted, startIdxOffseted) = Loop(items, 1);
|
||||||
|
|
||||||
|
if (foundOffseted > found)
|
||||||
|
{
|
||||||
|
popped = Recent[^1];
|
||||||
|
Recent.RemoveAt(Recent.Count - 1);
|
||||||
|
|
||||||
|
startIdx = startIdxOffseted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in items.TakeLast(startIdx))
|
||||||
|
{
|
||||||
|
newItems.Add(item);
|
||||||
|
Recent.Add(TimelineItem<AppleMusicCurrentlyPlayingContext>.From(item, DateTime.UtcNow));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popped is not null)
|
||||||
|
{
|
||||||
|
var idx = Recent.FindIndex(x => x.Item.Track.Id == popped.Item.Track.Id);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
newItems.RemoveAt(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckSize();
|
||||||
|
|
||||||
|
return newItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (int, int) Loop(List<AppleMusicCurrentlyPlayingContext> items, int storedOffset)
|
||||||
|
{
|
||||||
|
var stop = false;
|
||||||
|
var found = 0;
|
||||||
|
var startIdx = 0;
|
||||||
|
while (!stop)
|
||||||
|
{
|
||||||
|
found = Loop(items, storedOffset, ref startIdx, ref stop);
|
||||||
|
|
||||||
|
if (!stop) startIdx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (found, startIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Loop(List<AppleMusicCurrentlyPlayingContext> items, int storedOffset, ref int startIdx, ref bool stop)
|
||||||
|
{
|
||||||
|
var found = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
var storedIdx = (Recent.Count - 1) - i - storedOffset;
|
||||||
|
// start from the end, minus this loops index, minus the offset
|
||||||
|
var pulledIdx = (items.Count - 1) - i - startIdx;
|
||||||
|
|
||||||
|
if (pulledIdx < 0)
|
||||||
|
{
|
||||||
|
// ran to the end of new items and none matched the end, add all the new ones
|
||||||
|
stop = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storedIdx < 0)
|
||||||
|
{
|
||||||
|
// all the new stuff matches, we're done and there's nothing new to add
|
||||||
|
stop = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Recent[storedIdx].Item.Track.Id == items[pulledIdx].Track.Id)
|
||||||
|
{
|
||||||
|
// good, keep going
|
||||||
|
found++;
|
||||||
|
if (found >= 3)
|
||||||
|
{
|
||||||
|
stop = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// bad, doesn't match, break and bump stored
|
||||||
|
found = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
5
Selector.AppleMusic/Consumer/IConsumer.cs
Normal file
5
Selector.AppleMusic/Consumer/IConsumer.cs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
namespace Selector.AppleMusic.Consumer;
|
||||||
|
|
||||||
|
public interface IApplePlayerConsumer : IConsumer<AppleListeningChangeEventArgs>
|
||||||
|
{
|
||||||
|
}
|
23
Selector.AppleMusic/Events.cs
Normal file
23
Selector.AppleMusic/Events.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Selector.AppleMusic.Watcher;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic;
|
||||||
|
|
||||||
|
public class AppleListeningChangeEventArgs : ListeningChangeEventArgs
|
||||||
|
{
|
||||||
|
public AppleMusicCurrentlyPlayingContext Previous { get; set; }
|
||||||
|
public AppleMusicCurrentlyPlayingContext Current { get; set; }
|
||||||
|
|
||||||
|
// AppleTimeline Timeline { get; set; }
|
||||||
|
|
||||||
|
public static AppleListeningChangeEventArgs From(AppleMusicCurrentlyPlayingContext previous,
|
||||||
|
AppleMusicCurrentlyPlayingContext current, AppleTimeline timeline, string id = null, string username = null)
|
||||||
|
{
|
||||||
|
return new AppleListeningChangeEventArgs()
|
||||||
|
{
|
||||||
|
Previous = previous,
|
||||||
|
Current = current,
|
||||||
|
// Timeline = timeline,
|
||||||
|
Id = id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
8
Selector.AppleMusic/Exceptions/AppleMusicException.cs
Normal file
8
Selector.AppleMusic/Exceptions/AppleMusicException.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic.Exceptions;
|
||||||
|
|
||||||
|
public class AppleMusicException : Exception
|
||||||
|
{
|
||||||
|
public HttpStatusCode StatusCode { get; set; }
|
||||||
|
}
|
5
Selector.AppleMusic/Exceptions/ForbiddenException.cs
Normal file
5
Selector.AppleMusic/Exceptions/ForbiddenException.cs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
namespace Selector.AppleMusic.Exceptions;
|
||||||
|
|
||||||
|
public class ForbiddenException : AppleMusicException
|
||||||
|
{
|
||||||
|
}
|
5
Selector.AppleMusic/Exceptions/RateLimitException.cs
Normal file
5
Selector.AppleMusic/Exceptions/RateLimitException.cs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
namespace Selector.AppleMusic.Exceptions;
|
||||||
|
|
||||||
|
public class RateLimitException : AppleMusicException
|
||||||
|
{
|
||||||
|
}
|
5
Selector.AppleMusic/Exceptions/ServiceException.cs
Normal file
5
Selector.AppleMusic/Exceptions/ServiceException.cs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
namespace Selector.AppleMusic.Exceptions;
|
||||||
|
|
||||||
|
public class ServiceException : AppleMusicException
|
||||||
|
{
|
||||||
|
}
|
5
Selector.AppleMusic/Exceptions/UnauthorisedException.cs
Normal file
5
Selector.AppleMusic/Exceptions/UnauthorisedException.cs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
namespace Selector.AppleMusic.Exceptions;
|
||||||
|
|
||||||
|
public class UnauthorisedException : AppleMusicException
|
||||||
|
{
|
||||||
|
}
|
15
Selector.AppleMusic/Extensions/ServiceExtensions.cs
Normal file
15
Selector.AppleMusic/Extensions/ServiceExtensions.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Selector.AppleMusic.Watcher;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic.Extensions;
|
||||||
|
|
||||||
|
public static class ServiceExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddAppleMusic(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<AppleMusicApiProvider>()
|
||||||
|
.AddTransient<IAppleMusicWatcherFactory, AppleMusicWatcherFactory>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
14
Selector.AppleMusic/JsonContext.cs
Normal file
14
Selector.AppleMusic/JsonContext.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Selector.AppleMusic.Model;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic;
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(RecentlyPlayedTracksResponse))]
|
||||||
|
[JsonSerializable(typeof(TrackAttributes))]
|
||||||
|
[JsonSerializable(typeof(PlayParams))]
|
||||||
|
[JsonSerializable(typeof(Track))]
|
||||||
|
[JsonSerializable(typeof(AppleListeningChangeEventArgs))]
|
||||||
|
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
|
||||||
|
public partial class AppleJsonContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
119
Selector.AppleMusic/Jwt.cs
Normal file
119
Selector.AppleMusic/Jwt.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// https://github.com/CurtisUpdike/AppleDeveloperToken
|
||||||
|
// MIT License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2023 Curtis Updike
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Selector.Web.Apple;
|
||||||
|
|
||||||
|
internal record AppleAccount(string TeamId, string KeyId, string PrivateKey);
|
||||||
|
|
||||||
|
public class TokenGenerator
|
||||||
|
{
|
||||||
|
private static readonly JwtSecurityTokenHandler _tokenHandler = new();
|
||||||
|
private readonly AppleAccount _account;
|
||||||
|
private int _secondsValid;
|
||||||
|
public int SecondsValid
|
||||||
|
{
|
||||||
|
get { return _secondsValid; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ValidateTime(value);
|
||||||
|
_secondsValid = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenGenerator(string privateKey, string teamId, string keyId, int secondsValid = 15777000)
|
||||||
|
{
|
||||||
|
ValidateTime(secondsValid);
|
||||||
|
_account = new(teamId, keyId, FormatKey(privateKey));
|
||||||
|
_secondsValid = secondsValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Generate()
|
||||||
|
{
|
||||||
|
return GenerateToken(_account, TimeSpan.FromSeconds(SecondsValid));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Generate(int secondsValid)
|
||||||
|
{
|
||||||
|
ValidateTime(secondsValid);
|
||||||
|
return GenerateToken(_account, TimeSpan.FromSeconds(secondsValid));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Generate(TimeSpan timeValid)
|
||||||
|
{
|
||||||
|
ValidateTime(timeValid.Seconds);
|
||||||
|
return GenerateToken(_account, timeValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenerateToken(AppleAccount account, TimeSpan timeValid)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var algorithm = CreateAlgorithm(account.PrivateKey);
|
||||||
|
var signingCredentials = CreateSigningCredentials(account.KeyId, algorithm);
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Issuer = account.TeamId,
|
||||||
|
IssuedAt = now,
|
||||||
|
NotBefore = now,
|
||||||
|
Expires = now.Add(timeValid),
|
||||||
|
SigningCredentials = signingCredentials
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = _tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
|
||||||
|
return _tokenHandler.WriteToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ECDsa CreateAlgorithm(string key)
|
||||||
|
{
|
||||||
|
var algorithm = ECDsa.Create();
|
||||||
|
algorithm.ImportPkcs8PrivateKey(Convert.FromBase64String(key), out _);
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SigningCredentials CreateSigningCredentials(string keyId, ECDsa algorithm)
|
||||||
|
{
|
||||||
|
var key = new ECDsaSecurityKey(algorithm) { KeyId = keyId };
|
||||||
|
return new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateTime(int seconds)
|
||||||
|
{
|
||||||
|
if (seconds > 15777000)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Must be less than 15777000 seconds (6 months).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatKey(string key)
|
||||||
|
{
|
||||||
|
return key.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||||
|
.Replace("-----END PRIVATE KEY-----", "")
|
||||||
|
.Replace("\n", "")
|
||||||
|
.Replace("\r", "");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Selector.AppleMusic.Model;
|
||||||
|
|
||||||
|
public class RecentlyPlayedTracksResponse
|
||||||
|
{
|
||||||
|
public List<Track> Data { get; set; }
|
||||||
|
}
|
44
Selector.AppleMusic/Model/Track.cs
Normal file
44
Selector.AppleMusic/Model/Track.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
namespace Selector.AppleMusic.Model;
|
||||||
|
|
||||||
|
public class TrackAttributes
|
||||||
|
{
|
||||||
|
public string AlbumName { get; set; }
|
||||||
|
public List<string> GenreNames { get; set; }
|
||||||
|
public int TrackNumber { get; set; }
|
||||||
|
public int DurationInMillis { get; set; }
|
||||||
|
public DateTime ReleaseDate { get; set; }
|
||||||
|
|
||||||
|
public string Isrc { get; set; }
|
||||||
|
|
||||||
|
//TODO: Artwork
|
||||||
|
public string ComposerName { get; set; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
public PlayParams PlayParams { get; set; }
|
||||||
|
public int DiscNumber { get; set; }
|
||||||
|
public bool HasLyrics { get; set; }
|
||||||
|
public bool IsAppleDigitalMaster { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
//TODO: previews
|
||||||
|
public string ArtistName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayParams
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Kind { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Track
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Href { get; set; }
|
||||||
|
public TrackAttributes Attributes { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Attributes?.Name} / {Attributes?.AlbumName} / {Attributes?.ArtistName}";
|
||||||
|
}
|
||||||
|
}
|
18
Selector.AppleMusic/Selector.AppleMusic.csproj
Normal file
18
Selector.AppleMusic/Selector.AppleMusic.csproj
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Selector\Selector.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
26
Selector.AppleMusic/Watcher/CurrentlyPlayingContext.cs
Normal file
26
Selector.AppleMusic/Watcher/CurrentlyPlayingContext.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Selector.AppleMusic.Model;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic.Watcher;
|
||||||
|
|
||||||
|
public class AppleMusicCurrentlyPlayingContext
|
||||||
|
{
|
||||||
|
public DateTime FirstSeen { get; set; }
|
||||||
|
public Track Track { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AppleMusicCurrentlyPlayingContextComparer : IEqualityComparer<AppleMusicCurrentlyPlayingContext>
|
||||||
|
{
|
||||||
|
public bool Equals(AppleMusicCurrentlyPlayingContext? x, AppleMusicCurrentlyPlayingContext? y)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(x, y)) return true;
|
||||||
|
if (x is null) return false;
|
||||||
|
if (y is null) return false;
|
||||||
|
if (x.GetType() != y.GetType()) return false;
|
||||||
|
return x.Track.Id.Equals(y.Track.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(AppleMusicCurrentlyPlayingContext obj)
|
||||||
|
{
|
||||||
|
return obj.Track.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
20
Selector.AppleMusic/Watcher/IPlayerWatcher.cs
Normal file
20
Selector.AppleMusic/Watcher/IPlayerWatcher.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Selector.AppleMusic;
|
||||||
|
using Selector.AppleMusic.Watcher;
|
||||||
|
|
||||||
|
namespace Selector
|
||||||
|
{
|
||||||
|
public interface IAppleMusicPlayerWatcher : IWatcher
|
||||||
|
{
|
||||||
|
public event EventHandler<AppleListeningChangeEventArgs> NetworkPoll;
|
||||||
|
public event EventHandler<AppleListeningChangeEventArgs> ItemChange;
|
||||||
|
public event EventHandler<AppleListeningChangeEventArgs> AlbumChange;
|
||||||
|
public event EventHandler<AppleListeningChangeEventArgs> ArtistChange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Last retrieved currently playing
|
||||||
|
/// </summary>
|
||||||
|
public AppleMusicCurrentlyPlayingContext Live { get; }
|
||||||
|
|
||||||
|
public AppleTimeline Past { get; }
|
||||||
|
}
|
||||||
|
}
|
162
Selector.AppleMusic/Watcher/PlayerWatcher.cs
Normal file
162
Selector.AppleMusic/Watcher/PlayerWatcher.cs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.AppleMusic.Exceptions;
|
||||||
|
using Selector.AppleMusic.Model;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic.Watcher;
|
||||||
|
|
||||||
|
public class AppleMusicPlayerWatcher : BaseWatcher, IAppleMusicPlayerWatcher
|
||||||
|
{
|
||||||
|
private new readonly ILogger<AppleMusicPlayerWatcher> Logger;
|
||||||
|
private readonly AppleMusicApi _appleMusicApi;
|
||||||
|
|
||||||
|
public event EventHandler<AppleListeningChangeEventArgs> NetworkPoll;
|
||||||
|
public event EventHandler<AppleListeningChangeEventArgs> ItemChange;
|
||||||
|
public event EventHandler<AppleListeningChangeEventArgs> AlbumChange;
|
||||||
|
public event EventHandler<AppleListeningChangeEventArgs> ArtistChange;
|
||||||
|
|
||||||
|
public AppleMusicCurrentlyPlayingContext? Live { get; private set; }
|
||||||
|
private AppleMusicCurrentlyPlayingContext? Previous { get; set; }
|
||||||
|
public AppleTimeline Past { get; private set; } = new();
|
||||||
|
|
||||||
|
public AppleMusicPlayerWatcher(AppleMusicApi appleMusicClient,
|
||||||
|
ILogger<AppleMusicPlayerWatcher>? logger = null,
|
||||||
|
int pollPeriod = 3000
|
||||||
|
) : base(logger)
|
||||||
|
{
|
||||||
|
_appleMusicApi = appleMusicClient;
|
||||||
|
Logger = logger ?? NullLogger<AppleMusicPlayerWatcher>.Instance;
|
||||||
|
PollPeriod = pollPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task WatchOne(CancellationToken token)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>() { { "user_id", Id } });
|
||||||
|
|
||||||
|
Logger.LogTrace("Making Apple Music call");
|
||||||
|
var polledCurrent = await _appleMusicApi.GetRecentlyPlayedTracks();
|
||||||
|
|
||||||
|
if (polledCurrent is null)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Null response when calling Apple Music API");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polledCurrent.Data is null)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Null track list when calling Apple Music API");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("Received Apple Music call");
|
||||||
|
|
||||||
|
var currentPrevious = Previous;
|
||||||
|
var reversedItems = polledCurrent.Data.ToList();
|
||||||
|
reversedItems.Reverse();
|
||||||
|
var addedItems = Past.Add(reversedItems);
|
||||||
|
|
||||||
|
// swap new item into live and bump existing down to previous
|
||||||
|
Previous = Live;
|
||||||
|
SetLive(polledCurrent);
|
||||||
|
|
||||||
|
OnNetworkPoll(GetEvent());
|
||||||
|
|
||||||
|
if (currentPrevious != null && addedItems.Any())
|
||||||
|
{
|
||||||
|
addedItems.Insert(0, currentPrevious);
|
||||||
|
foreach (var (first, second) in addedItems.Zip(addedItems.Skip(1)))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Track changed: {prevTrack} -> {currentTrack}", first.Track, second.Track);
|
||||||
|
OnItemChange(AppleListeningChangeEventArgs.From(first, second, Past, id: Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (RateLimitException e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "Rate Limit exception");
|
||||||
|
// throw;
|
||||||
|
}
|
||||||
|
catch (ForbiddenException e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "Forbidden exception");
|
||||||
|
// throw;
|
||||||
|
}
|
||||||
|
catch (ServiceException e)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Apple Music internal error");
|
||||||
|
// throw;
|
||||||
|
}
|
||||||
|
catch (UnauthorisedException e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "Unauthorised exception");
|
||||||
|
// throw;
|
||||||
|
}
|
||||||
|
catch (AppleMusicException e)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Apple Music exception ({})", e.StatusCode);
|
||||||
|
// throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetLive(RecentlyPlayedTracksResponse recentlyPlayedTracks)
|
||||||
|
{
|
||||||
|
var lastTrack = recentlyPlayedTracks.Data?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (Live is { Track: not null } && Live.Track.Id == lastTrack?.Id)
|
||||||
|
{
|
||||||
|
Live = new()
|
||||||
|
{
|
||||||
|
Track = Live.Track,
|
||||||
|
FirstSeen = Live.FirstSeen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Live = new()
|
||||||
|
{
|
||||||
|
Track = recentlyPlayedTracks.Data?.FirstOrDefault(),
|
||||||
|
FirstSeen = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task Reset()
|
||||||
|
{
|
||||||
|
Previous = null;
|
||||||
|
Live = null;
|
||||||
|
Past = new();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppleListeningChangeEventArgs GetEvent() =>
|
||||||
|
AppleListeningChangeEventArgs.From(Previous, Live, Past, id: Id);
|
||||||
|
|
||||||
|
#region Event Firers
|
||||||
|
|
||||||
|
private void OnNetworkPoll(AppleListeningChangeEventArgs args)
|
||||||
|
{
|
||||||
|
NetworkPoll?.Invoke(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItemChange(AppleListeningChangeEventArgs args)
|
||||||
|
{
|
||||||
|
ItemChange?.Invoke(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void OnAlbumChange(AppleListeningChangeEventArgs args)
|
||||||
|
{
|
||||||
|
AlbumChange?.Invoke(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void OnArtistChange(AppleListeningChangeEventArgs args)
|
||||||
|
{
|
||||||
|
ArtistChange?.Invoke(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
55
Selector.AppleMusic/Watcher/WatcherFactory.cs
Normal file
55
Selector.AppleMusic/Watcher/WatcherFactory.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
namespace Selector.AppleMusic.Watcher
|
||||||
|
{
|
||||||
|
public interface IAppleMusicWatcherFactory
|
||||||
|
{
|
||||||
|
Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken, string teamId,
|
||||||
|
string keyId, string userToken, string id = null, int pollPeriod = 10000)
|
||||||
|
where T : class, IWatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AppleMusicWatcherFactory : IAppleMusicWatcherFactory
|
||||||
|
{
|
||||||
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
|
private readonly IEqual Equal;
|
||||||
|
|
||||||
|
public AppleMusicWatcherFactory(ILoggerFactory loggerFactory, IEqual equal)
|
||||||
|
{
|
||||||
|
LoggerFactory = loggerFactory;
|
||||||
|
Equal = equal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken,
|
||||||
|
string teamId, string keyId, string userToken, string id = null, int pollPeriod = 10000)
|
||||||
|
where T : class, IWatcher
|
||||||
|
{
|
||||||
|
if (typeof(T).IsAssignableFrom(typeof(AppleMusicPlayerWatcher)))
|
||||||
|
{
|
||||||
|
if (!Magic.Dummy)
|
||||||
|
{
|
||||||
|
var api = appleMusicProvider.GetApi(developerToken, teamId, keyId, userToken);
|
||||||
|
|
||||||
|
return new AppleMusicPlayerWatcher(
|
||||||
|
api,
|
||||||
|
LoggerFactory?.CreateLogger<AppleMusicPlayerWatcher>() ??
|
||||||
|
NullLogger<AppleMusicPlayerWatcher>.Instance,
|
||||||
|
pollPeriod: pollPeriod
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Id = id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Type unsupported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,21 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using System;
|
||||||
|
using System.CommandLine;
|
||||||
|
using System.CommandLine.Invocation;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NLog.Extensions.Logging;
|
using NLog.Extensions.Logging;
|
||||||
|
using Selector.AppleMusic.Extensions;
|
||||||
using Selector.Cache.Extensions;
|
using Selector.Cache.Extensions;
|
||||||
using Selector.CLI.Extensions;
|
using Selector.CLI.Extensions;
|
||||||
using Selector.Events;
|
using Selector.Events;
|
||||||
using Selector.Extensions;
|
using Selector.Extensions;
|
||||||
using System;
|
using Selector.Model.Extensions;
|
||||||
using System.CommandLine;
|
using Selector.Spotify;
|
||||||
using System.CommandLine.Invocation;
|
|
||||||
|
|
||||||
namespace Selector.CLI
|
namespace Selector.CLI
|
||||||
{
|
{
|
||||||
public class HostRootCommand: RootCommand
|
public class HostRootCommand : RootCommand
|
||||||
{
|
{
|
||||||
public HostRootCommand()
|
public HostRootCommand()
|
||||||
{
|
{
|
||||||
@ -31,7 +34,7 @@ namespace Selector.CLI
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var host = CreateHostBuilder(Environment.GetCommandLineArgs(),ConfigureDefault, ConfigureDefaultNlog)
|
var host = CreateHostBuilder(Environment.GetCommandLineArgs(), ConfigureDefault, ConfigureDefaultNlog)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var logger = host.Services.GetRequiredService<ILogger<HostCommand>>();
|
var logger = host.Services.GetRequiredService<ILogger<HostCommand>>();
|
||||||
@ -40,7 +43,7 @@ namespace Selector.CLI
|
|||||||
|
|
||||||
host.Run();
|
host.Run();
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine(ex);
|
Console.WriteLine(ex);
|
||||||
return 1;
|
return 1;
|
||||||
@ -53,7 +56,7 @@ namespace Selector.CLI
|
|||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
|
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
|
||||||
{
|
{
|
||||||
if(e.ExceptionObject is Exception ex)
|
if (e.ExceptionObject is Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Unhandled exception thrown");
|
logger.LogError(ex, "Unhandled exception thrown");
|
||||||
|
|
||||||
@ -96,7 +99,7 @@ namespace Selector.CLI
|
|||||||
Console.WriteLine("> Adding Services...");
|
Console.WriteLine("> Adding Services...");
|
||||||
// SERVICES
|
// SERVICES
|
||||||
services.AddHttpClient()
|
services.AddHttpClient()
|
||||||
.ConfigureDb(config);
|
.ConfigureDb(config);
|
||||||
|
|
||||||
services.AddConsumerFactories();
|
services.AddConsumerFactories();
|
||||||
services.AddCLIConsumerFactories();
|
services.AddCLIConsumerFactories();
|
||||||
@ -107,12 +110,14 @@ namespace Selector.CLI
|
|||||||
}
|
}
|
||||||
|
|
||||||
services.AddWatcher()
|
services.AddWatcher()
|
||||||
.AddEvents()
|
.AddSpotifyWatcher()
|
||||||
.AddSpotify();
|
.AddEvents()
|
||||||
|
.AddSpotify()
|
||||||
|
.AddAppleMusic();
|
||||||
|
|
||||||
services.ConfigureLastFm(config)
|
services.ConfigureLastFm(config)
|
||||||
.ConfigureEqual(config)
|
.ConfigureEqual(config)
|
||||||
.ConfigureJobs(config);
|
.ConfigureJobs(config);
|
||||||
|
|
||||||
if (config.RedisOptions.Enabled)
|
if (config.RedisOptions.Enabled)
|
||||||
{
|
{
|
||||||
@ -121,7 +126,8 @@ namespace Selector.CLI
|
|||||||
|
|
||||||
Console.WriteLine("> Adding cache event maps...");
|
Console.WriteLine("> Adding cache event maps...");
|
||||||
services.AddTransient<IEventMapping, FromPubSub.SpotifyLink>()
|
services.AddTransient<IEventMapping, FromPubSub.SpotifyLink>()
|
||||||
.AddTransient<IEventMapping, FromPubSub.Lastfm>();
|
.AddTransient<IEventMapping, FromPubSub.AppleMusicLink>()
|
||||||
|
.AddTransient<IEventMapping, FromPubSub.Lastfm>();
|
||||||
|
|
||||||
Console.WriteLine("> Adding caching Spotify consumers...");
|
Console.WriteLine("> Adding caching Spotify consumers...");
|
||||||
services.AddCachingSpotify();
|
services.AddCachingSpotify();
|
||||||
@ -147,14 +153,16 @@ namespace Selector.CLI
|
|||||||
public static void ConfigureDefaultNlog(HostBuilderContext context, ILoggingBuilder builder)
|
public static void ConfigureDefaultNlog(HostBuilderContext context, ILoggingBuilder builder)
|
||||||
{
|
{
|
||||||
builder.ClearProviders()
|
builder.ClearProviders()
|
||||||
.SetMinimumLevel(LogLevel.Trace)
|
.SetMinimumLevel(LogLevel.Trace)
|
||||||
.AddNLog(context.Configuration);
|
.AddNLog(context.Configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
static IHostBuilder CreateHostBuilder(string[] args, Action<HostBuilderContext, IServiceCollection> buildServices, Action<HostBuilderContext, ILoggingBuilder> buildLogs)
|
static IHostBuilder CreateHostBuilder(string[] args,
|
||||||
|
Action<HostBuilderContext, IServiceCollection> buildServices,
|
||||||
|
Action<HostBuilderContext, ILoggingBuilder> buildLogs)
|
||||||
=> Host.CreateDefaultBuilder(args)
|
=> Host.CreateDefaultBuilder(args)
|
||||||
.UseSystemd()
|
.UseSystemd()
|
||||||
.ConfigureServices((context, services) => buildServices(context, services))
|
.ConfigureServices((context, services) => buildServices(context, services))
|
||||||
.ConfigureLogging((context, builder) => buildLogs(context, builder));
|
.ConfigureLogging((context, builder) => buildLogs(context, builder));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,33 +1,35 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Selector.Model;
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
|
|
||||||
namespace Selector.CLI.Consumer
|
namespace Selector.CLI.Consumer
|
||||||
{
|
{
|
||||||
public interface IMappingPersisterFactory
|
public interface IMappingPersisterFactory
|
||||||
{
|
{
|
||||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
|
public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MappingPersisterFactory : IMappingPersisterFactory
|
public class MappingPersisterFactory : IMappingPersisterFactory
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
private readonly IServiceScopeFactory ScopeFactory;
|
private readonly IServiceScopeFactory ScopeFactory;
|
||||||
|
|
||||||
public MappingPersisterFactory(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory = null, LastFmCredentials creds = null)
|
public MappingPersisterFactory(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory = null,
|
||||||
|
LastFmCredentials creds = null)
|
||||||
{
|
{
|
||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
ScopeFactory = scopeFactory;
|
ScopeFactory = scopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null)
|
public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null)
|
||||||
{
|
{
|
||||||
return Task.FromResult<IPlayerConsumer>(new MappingPersister(
|
return Task.FromResult<ISpotifyPlayerConsumer>(new MappingPersister(
|
||||||
watcher,
|
watcher,
|
||||||
ScopeFactory,
|
ScopeFactory,
|
||||||
LoggerFactory.CreateLogger<MappingPersister>()
|
LoggerFactory.CreateLogger<MappingPersister>()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,7 +7,10 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.Extensions;
|
||||||
using Selector.Model;
|
using Selector.Model;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector.CLI.Consumer
|
namespace Selector.CLI.Consumer
|
||||||
@ -15,16 +18,16 @@ namespace Selector.CLI.Consumer
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save name -> Spotify URI mappings as new objects come through the watcher without making extra queries of the Spotify API
|
/// Save name -> Spotify URI mappings as new objects come through the watcher without making extra queries of the Spotify API
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MappingPersister: IPlayerConsumer
|
public class MappingPersister : ISpotifyPlayerConsumer
|
||||||
{
|
{
|
||||||
protected readonly IPlayerWatcher Watcher;
|
protected readonly ISpotifyPlayerWatcher Watcher;
|
||||||
protected readonly IServiceScopeFactory ScopeFactory;
|
protected readonly IServiceScopeFactory ScopeFactory;
|
||||||
protected readonly ILogger<MappingPersister> Logger;
|
protected readonly ILogger<MappingPersister> Logger;
|
||||||
|
|
||||||
public CancellationToken CancelToken { get; set; }
|
public CancellationToken CancelToken { get; set; }
|
||||||
|
|
||||||
public MappingPersister(
|
public MappingPersister(
|
||||||
IPlayerWatcher watcher,
|
ISpotifyPlayerWatcher watcher,
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
ILogger<MappingPersister> logger = null,
|
ILogger<MappingPersister> logger = null,
|
||||||
CancellationToken token = default
|
CancellationToken token = default
|
||||||
@ -36,11 +39,12 @@ namespace Selector.CLI.Consumer
|
|||||||
CancelToken = token;
|
CancelToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Callback(object sender, ListeningChangeEventArgs e)
|
public void Callback(object sender, SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Current is null) return;
|
if (e.Current is null) return;
|
||||||
|
|
||||||
Task.Run(async () => {
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AsyncCallback(e);
|
await AsyncCallback(e);
|
||||||
@ -56,16 +60,17 @@ namespace Selector.CLI.Consumer
|
|||||||
}, CancelToken);
|
}, CancelToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AsyncCallback(ListeningChangeEventArgs e)
|
public async Task AsyncCallback(SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
using var serviceScope = ScopeFactory.CreateScope();
|
using var serviceScope = ScopeFactory.CreateScope();
|
||||||
using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id } });
|
using var scope = Logger.BeginScope(new Dictionary<string, object>()
|
||||||
|
{ { "spotify_username", e.SpotifyUsername }, { "id", e.Id } });
|
||||||
|
|
||||||
if (e.Current.Item is FullTrack track)
|
if (e.Current.Item is FullTrack track)
|
||||||
{
|
{
|
||||||
var mappingRepo = serviceScope.ServiceProvider.GetRequiredService<IScrobbleMappingRepository>();
|
var mappingRepo = serviceScope.ServiceProvider.GetRequiredService<IScrobbleMappingRepository>();
|
||||||
|
|
||||||
if(!mappingRepo.GetTracks().Select(t => t.SpotifyUri).Contains(track.Uri))
|
if (!mappingRepo.GetTracks().Select(t => t.SpotifyUri).Contains(track.Uri))
|
||||||
{
|
{
|
||||||
mappingRepo.Add(new TrackLastfmSpotifyMapping()
|
mappingRepo.Add(new TrackLastfmSpotifyMapping()
|
||||||
{
|
{
|
||||||
@ -120,7 +125,7 @@ namespace Selector.CLI.Consumer
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch));
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch));
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange += Callback;
|
watcherCast.ItemChange += Callback;
|
||||||
}
|
}
|
||||||
@ -134,7 +139,7 @@ namespace Selector.CLI.Consumer
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch));
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch));
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange -= Callback;
|
watcherCast.ItemChange -= Callback;
|
||||||
}
|
}
|
||||||
@ -144,5 +149,4 @@ namespace Selector.CLI.Consumer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
|||||||
using IF.Lastfm.Core.Api;
|
using System.Linq;
|
||||||
|
using IF.Lastfm.Core.Api;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Selector.Model;
|
using Selector.Model;
|
||||||
|
using Selector.Spotify.ConfigFactory;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Selector.CLI.Extensions
|
namespace Selector.CLI.Extensions
|
||||||
{
|
{
|
||||||
@ -16,8 +17,8 @@ namespace Selector.CLI.Extensions
|
|||||||
{
|
{
|
||||||
var configBuild = new ConfigurationBuilder();
|
var configBuild = new ConfigurationBuilder();
|
||||||
configBuild.AddJsonFile("appsettings.json", optional: true)
|
configBuild.AddJsonFile("appsettings.json", optional: true)
|
||||||
.AddJsonFile("appsettings.Development.json", optional: true)
|
.AddJsonFile("appsettings.Development.json", optional: true)
|
||||||
.AddJsonFile("appsettings.Production.json", optional: true);
|
.AddJsonFile("appsettings.Production.json", optional: true);
|
||||||
context.Config = configBuild.Build().ConfigureOptions();
|
context.Config = configBuild.Build().ConfigureOptions();
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
@ -28,10 +29,7 @@ namespace Selector.CLI.Extensions
|
|||||||
context.Logger = LoggerFactory.Create(builder =>
|
context.Logger = LoggerFactory.Create(builder =>
|
||||||
{
|
{
|
||||||
//builder.AddConsole(a => a.);
|
//builder.AddConsole(a => a.);
|
||||||
builder.AddSimpleConsole(options =>
|
builder.AddSimpleConsole(options => { options.SingleLine = true; });
|
||||||
{
|
|
||||||
options.SingleLine = true;
|
|
||||||
});
|
|
||||||
builder.SetMinimumLevel(LogLevel.Trace);
|
builder.SetMinimumLevel(LogLevel.Trace);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -46,7 +44,9 @@ namespace Selector.CLI.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.DatabaseConfig = new DbContextOptionsBuilder<ApplicationDbContext>();
|
context.DatabaseConfig = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||||
context.DatabaseConfig.UseNpgsql(string.IsNullOrWhiteSpace(connectionString) ? context.Config.DatabaseOptions.ConnectionString : connectionString);
|
context.DatabaseConfig.UseNpgsql(string.IsNullOrWhiteSpace(connectionString)
|
||||||
|
? context.Config.DatabaseOptions.ConnectionString
|
||||||
|
: connectionString);
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
@ -58,7 +58,8 @@ namespace Selector.CLI.Extensions
|
|||||||
context.WithConfig();
|
context.WithConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
context.LastFmClient = new LastfmClient(new LastAuth(context.Config.LastfmClient, context.Config.LastfmSecret));
|
context.LastFmClient =
|
||||||
|
new LastfmClient(new LastAuth(context.Config.LastfmClient, context.Config.LastfmSecret));
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
@ -72,21 +73,23 @@ namespace Selector.CLI.Extensions
|
|||||||
|
|
||||||
var refreshToken = context.Config.RefreshToken;
|
var refreshToken = context.Config.RefreshToken;
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(refreshToken))
|
if (string.IsNullOrWhiteSpace(refreshToken))
|
||||||
{
|
{
|
||||||
if (context.DatabaseConfig is null)
|
if (context.DatabaseConfig is null)
|
||||||
{
|
{
|
||||||
context.WithDb();
|
context.WithDb();
|
||||||
}
|
}
|
||||||
|
|
||||||
using var db = new ApplicationDbContext(context.DatabaseConfig.Options, NullLogger<ApplicationDbContext>.Instance);
|
using var db = new ApplicationDbContext(context.DatabaseConfig.Options,
|
||||||
|
NullLogger<ApplicationDbContext>.Instance);
|
||||||
|
|
||||||
var user = db.Users.FirstOrDefault(u => u.UserName == "sarsoo");
|
var user = db.Users.FirstOrDefault(u => u.UserName == "sarsoo");
|
||||||
|
|
||||||
refreshToken = user?.SpotifyRefreshToken;
|
refreshToken = user?.SpotifyRefreshToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
var configFactory = new RefreshTokenFactory(context.Config.ClientId, context.Config.ClientSecret, refreshToken);
|
var configFactory =
|
||||||
|
new RefreshTokenFactory(context.Config.ClientId, context.Config.ClientSecret, refreshToken);
|
||||||
|
|
||||||
context.Spotify = new SpotifyClient(configFactory.GetConfig().Result);
|
context.Spotify = new SpotifyClient(configFactory.GetConfig().Result);
|
||||||
|
|
||||||
@ -109,4 +112,4 @@ namespace Selector.CLI.Extensions
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
using Selector.Cache.Extensions;
|
using Selector.Cache.Extensions;
|
||||||
@ -7,7 +8,7 @@ using Selector.CLI.Jobs;
|
|||||||
using Selector.Extensions;
|
using Selector.Extensions;
|
||||||
using Selector.Model;
|
using Selector.Model;
|
||||||
using Selector.Model.Services;
|
using Selector.Model.Services;
|
||||||
using System;
|
using Selector.Spotify.Equality;
|
||||||
|
|
||||||
namespace Selector.CLI.Extensions
|
namespace Selector.CLI.Extensions
|
||||||
{
|
{
|
||||||
@ -41,16 +42,13 @@ namespace Selector.CLI.Extensions
|
|||||||
{
|
{
|
||||||
Console.WriteLine("> Adding Jobs...");
|
Console.WriteLine("> Adding Jobs...");
|
||||||
|
|
||||||
services.AddQuartz(options => {
|
services.AddQuartz(options =>
|
||||||
|
{
|
||||||
options.UseMicrosoftDependencyInjectionJobFactory();
|
options.UseMicrosoftDependencyInjectionJobFactory();
|
||||||
|
|
||||||
options.UseSimpleTypeLoader();
|
options.UseSimpleTypeLoader();
|
||||||
options.UseInMemoryStore();
|
options.UseInMemoryStore();
|
||||||
options.UseDefaultThreadPool(tp =>
|
options.UseDefaultThreadPool(tp => { tp.MaxConcurrency = 5; });
|
||||||
{
|
|
||||||
tp.MaxConcurrency = 5;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (config.JobOptions.Scrobble.Enabled)
|
if (config.JobOptions.Scrobble.Enabled)
|
||||||
{
|
{
|
||||||
@ -68,7 +66,8 @@ namespace Selector.CLI.Extensions
|
|||||||
.WithIdentity("scrobble-watcher-agile-trigger")
|
.WithIdentity("scrobble-watcher-agile-trigger")
|
||||||
.ForJob(scrobbleKey)
|
.ForJob(scrobbleKey)
|
||||||
.StartNow()
|
.StartNow()
|
||||||
.WithSimpleSchedule(x => x.WithInterval(config.JobOptions.Scrobble.InterJobDelay).RepeatForever())
|
.WithSimpleSchedule(x =>
|
||||||
|
x.WithInterval(config.JobOptions.Scrobble.InterJobDelay).RepeatForever())
|
||||||
.WithDescription("Periodic trigger for scrobble watcher")
|
.WithDescription("Periodic trigger for scrobble watcher")
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -86,17 +85,14 @@ namespace Selector.CLI.Extensions
|
|||||||
.WithCronSchedule(config.JobOptions.Scrobble.FullScrobbleCron)
|
.WithCronSchedule(config.JobOptions.Scrobble.FullScrobbleCron)
|
||||||
.WithDescription("Periodic trigger for scrobble watcher")
|
.WithDescription("Periodic trigger for scrobble watcher")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine("> Skipping Scrobble Jobs...");
|
Console.WriteLine("> Skipping Scrobble Jobs...");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddQuartzHostedService(options => {
|
services.AddQuartzHostedService(options => { options.WaitForJobsToComplete = true; });
|
||||||
|
|
||||||
options.WaitForJobsToComplete = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddTransient<ScrobbleWatcherJob>();
|
services.AddTransient<ScrobbleWatcherJob>();
|
||||||
services.AddTransient<IJob, ScrobbleWatcherJob>();
|
services.AddTransient<IJob, ScrobbleWatcherJob>();
|
||||||
@ -115,7 +111,7 @@ namespace Selector.CLI.Extensions
|
|||||||
);
|
);
|
||||||
|
|
||||||
services.AddTransient<IScrobbleRepository, ScrobbleRepository>()
|
services.AddTransient<IScrobbleRepository, ScrobbleRepository>()
|
||||||
.AddTransient<ISpotifyListenRepository, SpotifyListenRepository>();
|
.AddTransient<ISpotifyListenRepository, SpotifyListenRepository>();
|
||||||
|
|
||||||
services.AddTransient<IListenRepository, MetaListenRepository>();
|
services.AddTransient<IListenRepository, MetaListenRepository>();
|
||||||
//services.AddTransient<IListenRepository, SpotifyListenRepository>();
|
//services.AddTransient<IListenRepository, SpotifyListenRepository>();
|
||||||
@ -152,5 +148,5 @@ namespace Selector.CLI.Extensions
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,16 +5,18 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace Selector.CLI
|
namespace Selector.CLI
|
||||||
{
|
{
|
||||||
static class OptionsHelper {
|
static class OptionsHelper
|
||||||
|
{
|
||||||
public static void ConfigureOptions(RootOptions options, IConfiguration config)
|
public static void ConfigureOptions(RootOptions options, IConfiguration config)
|
||||||
{
|
{
|
||||||
config.GetSection(RootOptions.Key).Bind(options);
|
config.GetSection(RootOptions.Key).Bind(options);
|
||||||
config.GetSection(FormatKeys( new[] { RootOptions.Key, WatcherOptions.Key})).Bind(options.WatcherOptions);
|
config.GetSection(FormatKeys(new[] { RootOptions.Key, WatcherOptions.Key })).Bind(options.WatcherOptions);
|
||||||
config.GetSection(FormatKeys( new[] { RootOptions.Key, DatabaseOptions.Key})).Bind(options.DatabaseOptions);
|
config.GetSection(FormatKeys(new[] { RootOptions.Key, DatabaseOptions.Key })).Bind(options.DatabaseOptions);
|
||||||
config.GetSection(FormatKeys( new[] { RootOptions.Key, RedisOptions.Key})).Bind(options.RedisOptions);
|
config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key })).Bind(options.RedisOptions);
|
||||||
config.GetSection(FormatKeys( new[] { RootOptions.Key, JobsOptions.Key})).Bind(options.JobOptions);
|
config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key })).Bind(options.JobOptions);
|
||||||
config.GetSection(FormatKeys( new[] { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key })).Bind(options.JobOptions.Scrobble);
|
config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key }))
|
||||||
}
|
.Bind(options.JobOptions.Scrobble);
|
||||||
|
}
|
||||||
|
|
||||||
public static RootOptions ConfigureOptions(this IConfiguration config)
|
public static RootOptions ConfigureOptions(this IConfiguration config)
|
||||||
{
|
{
|
||||||
@ -29,12 +31,17 @@ namespace Selector.CLI
|
|||||||
{
|
{
|
||||||
var options = config.GetSection(RootOptions.Key).Get<RootOptions>();
|
var options = config.GetSection(RootOptions.Key).Get<RootOptions>();
|
||||||
|
|
||||||
services.Configure<DatabaseOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, DatabaseOptions.Key })));
|
services.Configure<DatabaseOptions>(config.GetSection(FormatKeys(new[]
|
||||||
services.Configure<RedisOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key })));
|
{ RootOptions.Key, DatabaseOptions.Key })));
|
||||||
services.Configure<WatcherOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, WatcherOptions.Key })));
|
services.Configure<RedisOptions>(config.GetSection(FormatKeys(new[]
|
||||||
|
{ RootOptions.Key, RedisOptions.Key })));
|
||||||
|
services.Configure<WatcherOptions>(config.GetSection(FormatKeys(new[]
|
||||||
|
{ RootOptions.Key, WatcherOptions.Key })));
|
||||||
|
|
||||||
services.Configure<JobsOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key })));
|
services.Configure<JobsOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key })));
|
||||||
services.Configure<ScrobbleWatcherJobOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key })));
|
services.Configure<ScrobbleWatcherJobOptions>(config.GetSection(FormatKeys(new[]
|
||||||
|
{ RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key })));
|
||||||
|
services.Configure<AppleMusicOptions>(config.GetSection(AppleMusicOptions._Key));
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
@ -50,14 +57,17 @@ namespace Selector.CLI
|
|||||||
/// Spotify client ID
|
/// Spotify client ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClientId { get; set; }
|
public string ClientId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spotify app secret
|
/// Spotify app secret
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClientSecret { get; set; }
|
public string ClientSecret { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service account refresh token for tool spotify usage
|
/// Service account refresh token for tool spotify usage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string RefreshToken { get; set; }
|
public string RefreshToken { get; set; }
|
||||||
|
|
||||||
public string LastfmClient { get; set; }
|
public string LastfmClient { get; set; }
|
||||||
public string LastfmSecret { get; set; }
|
public string LastfmSecret { get; set; }
|
||||||
public WatcherOptions WatcherOptions { get; set; } = new();
|
public WatcherOptions WatcherOptions { get; set; } = new();
|
||||||
@ -69,7 +79,8 @@ namespace Selector.CLI
|
|||||||
|
|
||||||
public enum EqualityChecker
|
public enum EqualityChecker
|
||||||
{
|
{
|
||||||
Uri, String
|
Uri,
|
||||||
|
String
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WatcherOptions
|
public class WatcherOptions
|
||||||
@ -88,9 +99,10 @@ 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 AppleUserToken { get; set; }
|
||||||
public string LastFmUsername { 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.SpotifyPlayer;
|
||||||
public List<Consumers> Consumers { get; set; } = default;
|
public List<Consumers> Consumers { get; set; } = default;
|
||||||
#nullable enable
|
#nullable enable
|
||||||
public string? PlaylistUri { get; set; }
|
public string? PlaylistUri { get; set; }
|
||||||
@ -100,7 +112,12 @@ namespace Selector.CLI
|
|||||||
|
|
||||||
public enum Consumers
|
public enum Consumers
|
||||||
{
|
{
|
||||||
AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher, PlayCounter, MappingPersister
|
AudioFeatures,
|
||||||
|
AudioFeaturesCache,
|
||||||
|
CacheWriter,
|
||||||
|
Publisher,
|
||||||
|
PlayCounter,
|
||||||
|
MappingPersister
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RedisOptions
|
public class RedisOptions
|
||||||
@ -131,4 +148,15 @@ namespace Selector.CLI
|
|||||||
public int PageSize { get; set; } = 200;
|
public int PageSize { get; set; } = 200;
|
||||||
public int Simultaneous { get; set; } = 3;
|
public int Simultaneous { get; set; } = 3;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public class AppleMusicOptions
|
||||||
|
{
|
||||||
|
public const string _Key = "AppleMusic";
|
||||||
|
|
||||||
|
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string TeamId { get; set; }
|
||||||
|
public string KeyId { get; set; }
|
||||||
|
public TimeSpan? Expiry { get; set; } = null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using System;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
using Selector.Model;
|
|
||||||
using Selector.Operations;
|
|
||||||
using SpotifyAPI.Web;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.Mapping;
|
||||||
|
using Selector.Model;
|
||||||
|
using Selector.Operations;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector
|
||||||
{
|
{
|
||||||
@ -30,7 +30,9 @@ namespace Selector
|
|||||||
private readonly IScrobbleRepository scrobbleRepo;
|
private readonly IScrobbleRepository scrobbleRepo;
|
||||||
private readonly IScrobbleMappingRepository mappingRepo;
|
private readonly IScrobbleMappingRepository mappingRepo;
|
||||||
|
|
||||||
public ScrobbleMapper(ISearchClient _searchClient, ScrobbleMapperConfig _config, IScrobbleRepository _scrobbleRepository, IScrobbleMappingRepository _scrobbleMappingRepository, ILogger<ScrobbleMapper> _logger, ILoggerFactory _loggerFactory = null)
|
public ScrobbleMapper(ISearchClient _searchClient, ScrobbleMapperConfig _config,
|
||||||
|
IScrobbleRepository _scrobbleRepository, IScrobbleMappingRepository _scrobbleMappingRepository,
|
||||||
|
ILogger<ScrobbleMapper> _logger, ILoggerFactory _loggerFactory = null)
|
||||||
{
|
{
|
||||||
searchClient = _searchClient;
|
searchClient = _searchClient;
|
||||||
config = _config;
|
config = _config;
|
||||||
@ -68,9 +70,9 @@ namespace Selector
|
|||||||
.ExceptBy(currentTracks.Select(a => (a.LastfmArtistName, a.LastfmTrackName)), a => a);
|
.ExceptBy(currentTracks.Select(a => (a.LastfmArtistName, a.LastfmTrackName)), a => a);
|
||||||
|
|
||||||
var requests = tracksToPull.Select(a => new ScrobbleTrackMapping(
|
var requests = tracksToPull.Select(a => new ScrobbleTrackMapping(
|
||||||
searchClient,
|
searchClient,
|
||||||
loggerFactory.CreateLogger<ScrobbleTrackMapping>(),
|
loggerFactory.CreateLogger<ScrobbleTrackMapping>(),
|
||||||
a.TrackName, a.ArtistName)
|
a.TrackName, a.ArtistName)
|
||||||
).ToArray();
|
).ToArray();
|
||||||
|
|
||||||
logger.LogInformation("Found {} tracks to map, starting", requests.Length);
|
logger.LogInformation("Found {} tracks to map, starting", requests.Length);
|
||||||
@ -95,11 +97,12 @@ namespace Selector
|
|||||||
if (existingTrackUris.Contains(track.Uri))
|
if (existingTrackUris.Contains(track.Uri))
|
||||||
{
|
{
|
||||||
var artistName = track.Artists.FirstOrDefault()?.Name;
|
var artistName = track.Artists.FirstOrDefault()?.Name;
|
||||||
var duplicates = currentTracks.Where(a => a.LastfmArtistName.Equals(artistName, StringComparison.OrdinalIgnoreCase)
|
var duplicates = currentTracks.Where(a =>
|
||||||
&& a.LastfmTrackName.Equals(track.Name, StringComparison.OrdinalIgnoreCase));
|
a.LastfmArtistName.Equals(artistName, StringComparison.OrdinalIgnoreCase)
|
||||||
logger.LogWarning("Found duplicate Spotify uri ({}), [{}, {}] {}",
|
&& a.LastfmTrackName.Equals(track.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
track.Uri,
|
logger.LogWarning("Found duplicate Spotify uri ({}), [{}, {}] {}",
|
||||||
track.Name,
|
track.Uri,
|
||||||
|
track.Name,
|
||||||
artistName,
|
artistName,
|
||||||
string.Join(", ", duplicates.Select(d => $"{d.LastfmTrackName} {d.LastfmArtistName}"))
|
string.Join(", ", duplicates.Select(d => $"{d.LastfmTrackName} {d.LastfmArtistName}"))
|
||||||
);
|
);
|
||||||
@ -114,7 +117,7 @@ namespace Selector
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!existingAlbumUris.Contains(track.Album.Uri))
|
if (!existingAlbumUris.Contains(track.Album.Uri))
|
||||||
{
|
{
|
||||||
mappingRepo.Add(new AlbumLastfmSpotifyMapping()
|
mappingRepo.Add(new AlbumLastfmSpotifyMapping()
|
||||||
{
|
{
|
||||||
@ -124,7 +127,7 @@ namespace Selector
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach(var artist in track.Artists.UnionBy(track.Album.Artists, a => a.Name))
|
foreach (var artist in track.Artists.UnionBy(track.Album.Artists, a => a.Name))
|
||||||
{
|
{
|
||||||
if (!existingArtistUris.Contains(artist.Uri))
|
if (!existingArtistUris.Contains(artist.Uri))
|
||||||
{
|
{
|
||||||
@ -138,7 +141,7 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BatchingOperation<T> GetOperation<T>(IEnumerable<T> requests) where T: IOperation
|
private BatchingOperation<T> GetOperation<T>(IEnumerable<T> requests) where T : IOperation
|
||||||
=> new (config.InterRequestDelay, config.Timeout, config.SimultaneousConnections, requests);
|
=> new(config.InterRequestDelay, config.Timeout, config.SimultaneousConnections, requests);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,6 +28,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj" />
|
||||||
<ProjectReference Include="..\Selector\Selector.csproj" />
|
<ProjectReference Include="..\Selector\Selector.csproj" />
|
||||||
<ProjectReference Include="..\Selector.Model\Selector.Model.csproj" />
|
<ProjectReference Include="..\Selector.Model\Selector.Model.csproj" />
|
||||||
<ProjectReference Include="..\Selector.Cache\Selector.Cache.csproj" />
|
<ProjectReference Include="..\Selector.Cache\Selector.Cache.csproj" />
|
||||||
@ -58,4 +59,12 @@
|
|||||||
<Folder Include="Consumer\" />
|
<Folder Include="Consumer\" />
|
||||||
<Folder Include="Consumer\Factory\" />
|
<Folder Include="Consumer\Factory\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="appsettings.Development.json">
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -7,13 +8,19 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Selector.AppleMusic;
|
||||||
|
using Selector.AppleMusic.Watcher;
|
||||||
using Selector.Cache;
|
using Selector.Cache;
|
||||||
|
using Selector.CLI.Consumer;
|
||||||
|
using Selector.Events;
|
||||||
using Selector.Model;
|
using Selector.Model;
|
||||||
using Selector.Model.Extensions;
|
using Selector.Model.Extensions;
|
||||||
using Selector.Events;
|
using Selector.Spotify;
|
||||||
using System.Collections.Concurrent;
|
using Selector.Spotify.Consumer;
|
||||||
using Selector.CLI.Consumer;
|
using Selector.Spotify.Consumer.Factory;
|
||||||
|
using Selector.Spotify.FactoryProvider;
|
||||||
|
using Selector.Spotify.Watcher;
|
||||||
|
|
||||||
namespace Selector.CLI
|
namespace Selector.CLI
|
||||||
{
|
{
|
||||||
@ -22,13 +29,16 @@ namespace Selector.CLI
|
|||||||
private const int PollPeriod = 1000;
|
private const int PollPeriod = 1000;
|
||||||
|
|
||||||
private readonly ILogger<DbWatcherService> Logger;
|
private readonly ILogger<DbWatcherService> Logger;
|
||||||
|
private readonly IOptions<AppleMusicOptions> _appleMusicOptions;
|
||||||
private readonly IServiceProvider ServiceProvider;
|
private readonly IServiceProvider ServiceProvider;
|
||||||
private readonly UserEventBus UserEventBus;
|
private readonly UserEventBus UserEventBus;
|
||||||
|
|
||||||
private readonly IWatcherFactory WatcherFactory;
|
private readonly ISpotifyWatcherFactory _spotifyWatcherFactory;
|
||||||
|
private readonly IAppleMusicWatcherFactory _appleWatcherFactory;
|
||||||
private readonly IWatcherCollectionFactory WatcherCollectionFactory;
|
private readonly IWatcherCollectionFactory WatcherCollectionFactory;
|
||||||
private readonly IRefreshTokenFactoryProvider SpotifyFactory;
|
private readonly IRefreshTokenFactoryProvider SpotifyFactory;
|
||||||
|
private readonly AppleMusicApiProvider _appleMusicProvider;
|
||||||
|
|
||||||
private readonly IAudioFeatureInjectorFactory AudioFeatureInjectorFactory;
|
private readonly IAudioFeatureInjectorFactory AudioFeatureInjectorFactory;
|
||||||
private readonly IPlayCounterFactory PlayCounterFactory;
|
private readonly IPlayCounterFactory PlayCounterFactory;
|
||||||
|
|
||||||
@ -42,23 +52,20 @@ namespace Selector.CLI
|
|||||||
private ConcurrentDictionary<string, IWatcherCollection> Watchers { get; set; } = new();
|
private ConcurrentDictionary<string, IWatcherCollection> Watchers { get; set; } = new();
|
||||||
|
|
||||||
public DbWatcherService(
|
public DbWatcherService(
|
||||||
IWatcherFactory watcherFactory,
|
ISpotifyWatcherFactory spotifyWatcherFactory,
|
||||||
|
IAppleMusicWatcherFactory appleWatcherFactory,
|
||||||
IWatcherCollectionFactory watcherCollectionFactory,
|
IWatcherCollectionFactory watcherCollectionFactory,
|
||||||
IRefreshTokenFactoryProvider spotifyFactory,
|
IRefreshTokenFactoryProvider spotifyFactory,
|
||||||
|
AppleMusicApiProvider appleMusicProvider,
|
||||||
IAudioFeatureInjectorFactory audioFeatureInjectorFactory,
|
IAudioFeatureInjectorFactory audioFeatureInjectorFactory,
|
||||||
IPlayCounterFactory playCounterFactory,
|
IPlayCounterFactory playCounterFactory,
|
||||||
|
|
||||||
UserEventBus userEventBus,
|
UserEventBus userEventBus,
|
||||||
|
|
||||||
ILogger<DbWatcherService> logger,
|
ILogger<DbWatcherService> logger,
|
||||||
|
IOptions<AppleMusicOptions> appleMusicOptions,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
|
|
||||||
IPublisherFactory publisherFactory = null,
|
IPublisherFactory publisherFactory = null,
|
||||||
ICacheWriterFactory cacheWriterFactory = null,
|
ICacheWriterFactory cacheWriterFactory = null,
|
||||||
|
|
||||||
IMappingPersisterFactory mappingPersisterFactory = null,
|
IMappingPersisterFactory mappingPersisterFactory = null,
|
||||||
|
|
||||||
IUserEventFirerFactory userEventFirerFactory = null
|
IUserEventFirerFactory userEventFirerFactory = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@ -66,10 +73,13 @@ namespace Selector.CLI
|
|||||||
ServiceProvider = serviceProvider;
|
ServiceProvider = serviceProvider;
|
||||||
UserEventBus = userEventBus;
|
UserEventBus = userEventBus;
|
||||||
|
|
||||||
WatcherFactory = watcherFactory;
|
_spotifyWatcherFactory = spotifyWatcherFactory;
|
||||||
|
_appleWatcherFactory = appleWatcherFactory;
|
||||||
|
_appleMusicOptions = appleMusicOptions;
|
||||||
WatcherCollectionFactory = watcherCollectionFactory;
|
WatcherCollectionFactory = watcherCollectionFactory;
|
||||||
SpotifyFactory = spotifyFactory;
|
SpotifyFactory = spotifyFactory;
|
||||||
|
_appleMusicProvider = appleMusicProvider;
|
||||||
|
|
||||||
AudioFeatureInjectorFactory = audioFeatureInjectorFactory;
|
AudioFeatureInjectorFactory = audioFeatureInjectorFactory;
|
||||||
PlayCounterFactory = playCounterFactory;
|
PlayCounterFactory = playCounterFactory;
|
||||||
|
|
||||||
@ -100,9 +110,18 @@ namespace Selector.CLI
|
|||||||
var indices = new HashSet<string>();
|
var indices = new HashSet<string>();
|
||||||
|
|
||||||
foreach (var dbWatcher in db.Watcher
|
foreach (var dbWatcher in db.Watcher
|
||||||
.Include(w => w.User)
|
.Include(w => w.User)
|
||||||
.Where(w => !string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken)))
|
.Where(w =>
|
||||||
|
((w.Type == WatcherType.SpotifyPlayer || w.Type == WatcherType.SpotifyPlaylist) &&
|
||||||
|
!string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken)) ||
|
||||||
|
(w.Type == WatcherType.AppleMusicPlayer && w.User.AppleMusicLinked)
|
||||||
|
))
|
||||||
{
|
{
|
||||||
|
using var logScope = Logger.BeginScope(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "username", dbWatcher.User.UserName }
|
||||||
|
});
|
||||||
|
|
||||||
var watcherCollectionIdx = dbWatcher.UserId;
|
var watcherCollectionIdx = dbWatcher.UserId;
|
||||||
indices.Add(watcherCollectionIdx);
|
indices.Add(watcherCollectionIdx);
|
||||||
|
|
||||||
@ -123,39 +142,54 @@ namespace Selector.CLI
|
|||||||
|
|
||||||
var watcherCollection = Watchers[watcherCollectionIdx];
|
var watcherCollection = Watchers[watcherCollectionIdx];
|
||||||
|
|
||||||
Logger.LogDebug("Getting Spotify factory");
|
|
||||||
var spotifyFactory = await SpotifyFactory.GetFactory(dbWatcher.User.SpotifyRefreshToken);
|
|
||||||
|
|
||||||
IWatcher watcher = null;
|
IWatcher watcher = null;
|
||||||
List<IConsumer> consumers = new();
|
List<IConsumer> consumers = new();
|
||||||
|
|
||||||
switch (dbWatcher.Type)
|
switch (dbWatcher.Type)
|
||||||
{
|
{
|
||||||
case WatcherType.Player:
|
case WatcherType.SpotifyPlayer:
|
||||||
watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: dbWatcher.UserId, pollPeriod: PollPeriod);
|
Logger.LogDebug("Getting Spotify factory");
|
||||||
|
var spotifyFactory = await SpotifyFactory.GetFactory(dbWatcher.User.SpotifyRefreshToken);
|
||||||
|
|
||||||
|
watcher = await _spotifyWatcherFactory.Get<SpotifyPlayerWatcher>(spotifyFactory,
|
||||||
|
id: dbWatcher.UserId, pollPeriod: PollPeriod);
|
||||||
|
|
||||||
consumers.Add(await AudioFeatureInjectorFactory.Get(spotifyFactory));
|
consumers.Add(await AudioFeatureInjectorFactory.Get(spotifyFactory));
|
||||||
if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.Get());
|
if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.GetSpotify());
|
||||||
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.Get());
|
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetSpotify());
|
||||||
|
|
||||||
if (MappingPersisterFactory is not null && !Magic.Dummy) consumers.Add(await MappingPersisterFactory.Get());
|
if (MappingPersisterFactory is not null && !Magic.Dummy)
|
||||||
|
consumers.Add(await MappingPersisterFactory.Get());
|
||||||
|
|
||||||
if (UserEventFirerFactory is not null) consumers.Add(await UserEventFirerFactory.Get());
|
if (UserEventFirerFactory is not null) consumers.Add(await UserEventFirerFactory.GetSpotify());
|
||||||
|
|
||||||
if (dbWatcher.User.LastFmConnected())
|
if (dbWatcher.User.LastFmConnected())
|
||||||
{
|
{
|
||||||
consumers.Add(await PlayCounterFactory.Get(creds: new() { Username = dbWatcher.User.LastFmUsername }));
|
consumers.Add(await PlayCounterFactory.Get(creds: new()
|
||||||
|
{ Username = dbWatcher.User.LastFmUsername }));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[{username}] No Last.fm username, skipping play counter", dbWatcher.User.UserName);
|
Logger.LogDebug("[{username}] No Last.fm username, skipping play counter",
|
||||||
|
dbWatcher.User.UserName);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WatcherType.Playlist:
|
case WatcherType.SpotifyPlaylist:
|
||||||
throw new NotImplementedException("Playlist watchers not implemented");
|
throw new NotImplementedException("Playlist watchers not implemented");
|
||||||
// break;
|
break;
|
||||||
|
case WatcherType.AppleMusicPlayer:
|
||||||
|
watcher = await _appleWatcherFactory.Get<AppleMusicPlayerWatcher>(_appleMusicProvider,
|
||||||
|
_appleMusicOptions.Value.Key, _appleMusicOptions.Value.TeamId, _appleMusicOptions.Value.KeyId,
|
||||||
|
dbWatcher.User.AppleMusicKey, id: dbWatcher.UserId);
|
||||||
|
|
||||||
|
if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.GetApple());
|
||||||
|
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetApple());
|
||||||
|
|
||||||
|
if (UserEventFirerFactory is not null) consumers.Add(await UserEventFirerFactory.GetApple());
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return watcherCollection.Add(watcher, consumers);
|
return watcherCollection.Add(watcher, consumers);
|
||||||
@ -181,7 +215,7 @@ namespace Selector.CLI
|
|||||||
{
|
{
|
||||||
Logger.LogInformation("Shutting down");
|
Logger.LogInformation("Shutting down");
|
||||||
|
|
||||||
foreach((var key, var watcher) in Watchers)
|
foreach ((var key, var watcher) in Watchers)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Stopping watcher collection [{key}]", key);
|
Logger.LogInformation("Stopping watcher collection [{key}]", key);
|
||||||
watcher.Stop();
|
watcher.Stop();
|
||||||
@ -195,24 +229,27 @@ namespace Selector.CLI
|
|||||||
private void AttachEventBus()
|
private void AttachEventBus()
|
||||||
{
|
{
|
||||||
UserEventBus.SpotifyLinkChange += SpotifyChangeCallback;
|
UserEventBus.SpotifyLinkChange += SpotifyChangeCallback;
|
||||||
|
UserEventBus.AppleLinkChange += AppleMusicChangeCallback;
|
||||||
UserEventBus.LastfmCredChange += LastfmChangeCallback;
|
UserEventBus.LastfmCredChange += LastfmChangeCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DetachEventBus()
|
private void DetachEventBus()
|
||||||
{
|
{
|
||||||
UserEventBus.SpotifyLinkChange -= SpotifyChangeCallback;
|
UserEventBus.SpotifyLinkChange -= SpotifyChangeCallback;
|
||||||
|
UserEventBus.AppleLinkChange -= AppleMusicChangeCallback;
|
||||||
UserEventBus.LastfmCredChange -= LastfmChangeCallback;
|
UserEventBus.LastfmCredChange -= LastfmChangeCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void SpotifyChangeCallback(object sender, SpotifyLinkChange change)
|
public async void SpotifyChangeCallback(object sender, SpotifyLinkChange change)
|
||||||
{
|
{
|
||||||
if(Watchers.ContainsKey(change.UserId))
|
if (Watchers.ContainsKey(change.UserId))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Setting new Spotify link state for [{username}], [{}]", change.UserId, change.NewLinkState);
|
Logger.LogDebug("Setting new Spotify link state for [{username}], [{}]", change.UserId,
|
||||||
|
change.NewLinkState);
|
||||||
|
|
||||||
var watcherCollection = Watchers[change.UserId];
|
var watcherCollection = Watchers[change.UserId];
|
||||||
|
|
||||||
if(change.NewLinkState)
|
if (change.NewLinkState)
|
||||||
{
|
{
|
||||||
watcherCollection.Start();
|
watcherCollection.Start();
|
||||||
}
|
}
|
||||||
@ -227,8 +264,46 @@ namespace Selector.CLI
|
|||||||
var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
|
var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
|
||||||
|
|
||||||
var watcherEnum = db.Watcher
|
var watcherEnum = db.Watcher
|
||||||
.Include(w => w.User)
|
.Include(w => w.User)
|
||||||
.Where(w => w.UserId == change.UserId);
|
.Where(w => w.UserId == change.UserId);
|
||||||
|
|
||||||
|
foreach (var dbWatcher in watcherEnum)
|
||||||
|
{
|
||||||
|
var context = await InitInstance(dbWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
Watchers[change.UserId].Start();
|
||||||
|
|
||||||
|
Logger.LogDebug("Started {} watchers for [{username}]", watcherEnum.Count(), change.UserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void AppleMusicChangeCallback(object sender, AppleMusicLinkChange change)
|
||||||
|
{
|
||||||
|
if (Watchers.ContainsKey(change.UserId))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Setting new Apple Music link state for [{username}], [{}]", change.UserId,
|
||||||
|
change.NewLinkState);
|
||||||
|
|
||||||
|
var watcherCollection = Watchers[change.UserId];
|
||||||
|
|
||||||
|
if (change.NewLinkState)
|
||||||
|
{
|
||||||
|
watcherCollection.Start();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
watcherCollection.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var scope = ServiceProvider.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
|
||||||
|
|
||||||
|
var watcherEnum = db.Watcher
|
||||||
|
.Include(w => w.User)
|
||||||
|
.Where(w => w.UserId == change.UserId);
|
||||||
|
|
||||||
foreach (var dbWatcher in watcherEnum)
|
foreach (var dbWatcher in watcherEnum)
|
||||||
{
|
{
|
||||||
@ -249,9 +324,9 @@ namespace Selector.CLI
|
|||||||
|
|
||||||
var watcherCollection = Watchers[change.UserId];
|
var watcherCollection = Watchers[change.UserId];
|
||||||
|
|
||||||
foreach(var watcher in watcherCollection.Consumers)
|
foreach (var watcher in watcherCollection.Consumers)
|
||||||
{
|
{
|
||||||
if(watcher is PlayCounter counter)
|
if (watcher is PlayCounter counter)
|
||||||
{
|
{
|
||||||
counter.Credentials.Username = change.NewUsername;
|
counter.Credentials.Username = change.NewUsername;
|
||||||
}
|
}
|
||||||
@ -259,9 +334,8 @@ namespace Selector.CLI
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
||||||
Logger.LogDebug("No watchers running for [{username}], skipping Spotify event", change.UserId);
|
Logger.LogDebug("No watchers running for [{username}], skipping Spotify event", change.UserId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,14 +4,18 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Selector.AppleMusic;
|
||||||
|
using Selector.AppleMusic.Watcher;
|
||||||
using Selector.Cache;
|
using Selector.Cache;
|
||||||
using Selector.CLI.Consumer;
|
using Selector.CLI.Consumer;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer.Factory;
|
||||||
|
using Selector.Spotify.FactoryProvider;
|
||||||
|
using Selector.Spotify.Watcher;
|
||||||
|
|
||||||
namespace Selector.CLI
|
namespace Selector.CLI
|
||||||
{
|
{
|
||||||
@ -21,30 +25,38 @@ namespace Selector.CLI
|
|||||||
|
|
||||||
private readonly ILogger<LocalWatcherService> Logger;
|
private readonly ILogger<LocalWatcherService> Logger;
|
||||||
private readonly RootOptions Config;
|
private readonly RootOptions Config;
|
||||||
private readonly IWatcherFactory WatcherFactory;
|
private readonly ISpotifyWatcherFactory _spotifyWatcherFactory;
|
||||||
|
private readonly IAppleMusicWatcherFactory _appleWatcherFactory;
|
||||||
private readonly IWatcherCollectionFactory WatcherCollectionFactory;
|
private readonly IWatcherCollectionFactory WatcherCollectionFactory;
|
||||||
private readonly IRefreshTokenFactoryProvider SpotifyFactory;
|
private readonly IRefreshTokenFactoryProvider SpotifyFactory;
|
||||||
|
private readonly AppleMusicApiProvider _appleMusicApiProvider;
|
||||||
|
private readonly IOptions<AppleMusicOptions> _appleMusicOptions;
|
||||||
|
|
||||||
private readonly IServiceProvider ServiceProvider;
|
private readonly IServiceProvider ServiceProvider;
|
||||||
|
|
||||||
private Dictionary<string, IWatcherCollection> Watchers { get; set; } = new();
|
private Dictionary<string, IWatcherCollection> Watchers { get; set; } = new();
|
||||||
|
|
||||||
public LocalWatcherService(
|
public LocalWatcherService(
|
||||||
IWatcherFactory watcherFactory,
|
ISpotifyWatcherFactory spotifyWatcherFactory,
|
||||||
|
IAppleMusicWatcherFactory appleWatcherFactory,
|
||||||
IWatcherCollectionFactory watcherCollectionFactory,
|
IWatcherCollectionFactory watcherCollectionFactory,
|
||||||
IRefreshTokenFactoryProvider spotifyFactory,
|
IRefreshTokenFactoryProvider spotifyFactory,
|
||||||
|
AppleMusicApiProvider appleMusicApiProvider,
|
||||||
|
IOptions<AppleMusicOptions> appleMusicOptions,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
|
|
||||||
ILogger<LocalWatcherService> logger,
|
ILogger<LocalWatcherService> logger,
|
||||||
IOptions<RootOptions> config
|
IOptions<RootOptions> config
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
Config = config.Value;
|
Config = config.Value;
|
||||||
|
|
||||||
WatcherFactory = watcherFactory;
|
_spotifyWatcherFactory = spotifyWatcherFactory;
|
||||||
|
_appleWatcherFactory = appleWatcherFactory;
|
||||||
WatcherCollectionFactory = watcherCollectionFactory;
|
WatcherCollectionFactory = watcherCollectionFactory;
|
||||||
SpotifyFactory = spotifyFactory;
|
SpotifyFactory = spotifyFactory;
|
||||||
|
_appleMusicApiProvider = appleMusicApiProvider;
|
||||||
|
_appleMusicOptions = appleMusicOptions;
|
||||||
|
|
||||||
ServiceProvider = serviceProvider;
|
ServiceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
@ -75,7 +87,8 @@ namespace Selector.CLI
|
|||||||
logMsg.Append($"Creating new [{watcherOption.Type}] watcher");
|
logMsg.Append($"Creating new [{watcherOption.Type}] watcher");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri)) logMsg.Append($" [{ watcherOption.PlaylistUri}]");
|
if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri))
|
||||||
|
logMsg.Append($" [{watcherOption.PlaylistUri}]");
|
||||||
Logger.LogInformation(logMsg.ToString());
|
Logger.LogInformation(logMsg.ToString());
|
||||||
|
|
||||||
var watcherCollectionIdx = watcherOption.WatcherCollection ?? ConfigInstanceKey;
|
var watcherCollectionIdx = watcherOption.WatcherCollection ?? ConfigInstanceKey;
|
||||||
@ -90,17 +103,27 @@ namespace Selector.CLI
|
|||||||
var spotifyFactory = await SpotifyFactory.GetFactory(watcherOption.RefreshKey);
|
var spotifyFactory = await SpotifyFactory.GetFactory(watcherOption.RefreshKey);
|
||||||
|
|
||||||
IWatcher watcher = null;
|
IWatcher watcher = null;
|
||||||
switch(watcherOption.Type)
|
switch (watcherOption.Type)
|
||||||
{
|
{
|
||||||
case WatcherType.Player:
|
case WatcherType.SpotifyPlayer:
|
||||||
watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod);
|
watcher = await _spotifyWatcherFactory.Get<SpotifyPlayerWatcher>(spotifyFactory,
|
||||||
|
id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod);
|
||||||
break;
|
break;
|
||||||
case WatcherType.Playlist:
|
case WatcherType.SpotifyPlaylist:
|
||||||
var playlistWatcher = await WatcherFactory.Get<PlaylistWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod) as PlaylistWatcher;
|
var playlistWatcher = await _spotifyWatcherFactory.Get<PlaylistWatcher>(spotifyFactory,
|
||||||
|
id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod) as PlaylistWatcher;
|
||||||
playlistWatcher.config = new() { PlaylistId = watcherOption.PlaylistUri };
|
playlistWatcher.config = new() { PlaylistId = watcherOption.PlaylistUri };
|
||||||
|
|
||||||
watcher = playlistWatcher;
|
watcher = playlistWatcher;
|
||||||
break;
|
break;
|
||||||
|
case WatcherType.AppleMusicPlayer:
|
||||||
|
var appleMusicWatcher = await _appleWatcherFactory.Get<AppleMusicPlayerWatcher>(
|
||||||
|
_appleMusicApiProvider, _appleMusicOptions.Value.Key, _appleMusicOptions.Value.TeamId,
|
||||||
|
_appleMusicOptions.Value.KeyId, watcherOption.AppleUserToken,
|
||||||
|
id: watcherOption.Name);
|
||||||
|
|
||||||
|
watcher = appleMusicWatcher;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<IConsumer> consumers = new();
|
List<IConsumer> consumers = new();
|
||||||
@ -112,30 +135,50 @@ namespace Selector.CLI
|
|||||||
switch (consumer)
|
switch (consumer)
|
||||||
{
|
{
|
||||||
case Consumers.AudioFeatures:
|
case Consumers.AudioFeatures:
|
||||||
consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>().Get(spotifyFactory));
|
consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>()
|
||||||
|
.Get(spotifyFactory));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Consumers.AudioFeaturesCache:
|
case Consumers.AudioFeaturesCache:
|
||||||
consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>().Get(spotifyFactory));
|
consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>()
|
||||||
|
.Get(spotifyFactory));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Consumers.CacheWriter:
|
case Consumers.CacheWriter:
|
||||||
consumers.Add(await ServiceProvider.GetService<CacheWriterFactory>().Get());
|
if (watcher is ISpotifyPlayerWatcher or IPlaylistWatcher)
|
||||||
|
{
|
||||||
|
consumers.Add(await ServiceProvider.GetService<CacheWriterFactory>().GetSpotify());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
consumers.Add(await ServiceProvider.GetService<CacheWriterFactory>().GetApple());
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Consumers.Publisher:
|
case Consumers.Publisher:
|
||||||
consumers.Add(await ServiceProvider.GetService<PublisherFactory>().Get());
|
if (watcher is ISpotifyPlayerWatcher or IPlaylistWatcher)
|
||||||
|
{
|
||||||
|
consumers.Add(await ServiceProvider.GetService<PublisherFactory>().GetSpotify());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
consumers.Add(await ServiceProvider.GetService<PublisherFactory>().GetApple());
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Consumers.PlayCounter:
|
case Consumers.PlayCounter:
|
||||||
if (!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername))
|
if (!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername))
|
||||||
{
|
{
|
||||||
consumers.Add(await ServiceProvider.GetService<PlayCounterFactory>().Get(creds: new() { Username = watcherOption.LastFmUsername }));
|
consumers.Add(await ServiceProvider.GetService<PlayCounterFactory>()
|
||||||
|
.Get(creds: new() { Username = watcherOption.LastFmUsername }));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogError("No Last.fm username provided, skipping play counter");
|
Logger.LogError("No Last.fm username provided, skipping play counter");
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Consumers.MappingPersister:
|
case Consumers.MappingPersister:
|
||||||
@ -171,7 +214,7 @@ namespace Selector.CLI
|
|||||||
{
|
{
|
||||||
Logger.LogInformation("Shutting down");
|
Logger.LogInformation("Shutting down");
|
||||||
|
|
||||||
foreach((var key, var watcher) in Watchers)
|
foreach ((var key, var watcher) in Watchers)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Stopping watcher collection [{key}]", key);
|
Logger.LogInformation("Stopping watcher collection [{key}]", key);
|
||||||
watcher.Stop();
|
watcher.Stop();
|
||||||
@ -180,4 +223,4 @@ namespace Selector.CLI
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
93
Selector.Cache/Consumer/AppleMusic/CacheWriterConsumer.cs
Normal file
93
Selector.Cache/Consumer/AppleMusic/CacheWriterConsumer.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.AppleMusic;
|
||||||
|
using Selector.AppleMusic.Consumer;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
|
namespace Selector.Cache
|
||||||
|
{
|
||||||
|
public class AppleCacheWriter : IApplePlayerConsumer
|
||||||
|
{
|
||||||
|
private readonly IAppleMusicPlayerWatcher Watcher;
|
||||||
|
private readonly IDatabaseAsync Db;
|
||||||
|
private readonly ILogger<AppleCacheWriter> Logger;
|
||||||
|
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(20);
|
||||||
|
|
||||||
|
public CancellationToken CancelToken { get; set; }
|
||||||
|
|
||||||
|
public AppleCacheWriter(
|
||||||
|
IAppleMusicPlayerWatcher watcher,
|
||||||
|
IDatabaseAsync db,
|
||||||
|
ILogger<AppleCacheWriter> logger = null,
|
||||||
|
CancellationToken token = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Watcher = watcher;
|
||||||
|
Db = db;
|
||||||
|
Logger = logger ?? NullLogger<AppleCacheWriter>.Instance;
|
||||||
|
CancelToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Callback(object sender, AppleListeningChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Current is null) return;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await AsyncCallback(e);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "Error occured during callback");
|
||||||
|
}
|
||||||
|
}, CancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AsyncCallback(AppleListeningChangeEventArgs e)
|
||||||
|
{
|
||||||
|
// using var scope = Logger.GetListeningEventArgsScope(e);
|
||||||
|
|
||||||
|
var payload = JsonSerializer.Serialize(e, AppleJsonContext.Default.AppleListeningChangeEventArgs);
|
||||||
|
|
||||||
|
Logger.LogTrace("Caching current");
|
||||||
|
|
||||||
|
var resp = await Db.StringSetAsync(Key.CurrentlyPlayingAppleMusic(e.Id), payload, expiry: CacheExpiry);
|
||||||
|
|
||||||
|
Logger.LogDebug("Cached current, {state}", (resp ? "value set" : "value NOT set"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Subscribe(IWatcher watch = null)
|
||||||
|
{
|
||||||
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
|
if (watcher is IAppleMusicPlayerWatcher watcherCastApple)
|
||||||
|
{
|
||||||
|
watcherCastApple.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 IAppleMusicPlayerWatcher watcherCastApple)
|
||||||
|
{
|
||||||
|
watcherCastApple.ItemChange -= Callback;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
Selector.Cache/Consumer/AppleMusic/PublisherConsumer.cs
Normal file
94
Selector.Cache/Consumer/AppleMusic/PublisherConsumer.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.AppleMusic;
|
||||||
|
using Selector.AppleMusic.Consumer;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
|
namespace Selector.Cache.Consumer.AppleMusic
|
||||||
|
{
|
||||||
|
public class ApplePublisher : IApplePlayerConsumer
|
||||||
|
{
|
||||||
|
private readonly IAppleMusicPlayerWatcher Watcher;
|
||||||
|
private readonly ISubscriber Subscriber;
|
||||||
|
private readonly ILogger<ApplePublisher> Logger;
|
||||||
|
|
||||||
|
public CancellationToken CancelToken { get; set; }
|
||||||
|
|
||||||
|
public ApplePublisher(
|
||||||
|
IAppleMusicPlayerWatcher watcher,
|
||||||
|
ISubscriber subscriber,
|
||||||
|
ILogger<ApplePublisher> logger = null,
|
||||||
|
CancellationToken token = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Watcher = watcher;
|
||||||
|
Subscriber = subscriber;
|
||||||
|
Logger = logger ?? NullLogger<ApplePublisher>.Instance;
|
||||||
|
CancelToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Callback(object sender, AppleListeningChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Current is null) return;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await AsyncCallback(e);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "Error occured during callback");
|
||||||
|
}
|
||||||
|
}, CancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AsyncCallback(AppleListeningChangeEventArgs e)
|
||||||
|
{
|
||||||
|
// using var scope = Logger.GetListeningEventArgsScope(e);
|
||||||
|
|
||||||
|
var payload = JsonSerializer.Serialize(e, AppleJsonContext.Default.AppleListeningChangeEventArgs);
|
||||||
|
|
||||||
|
Logger.LogTrace("Publishing current");
|
||||||
|
|
||||||
|
// TODO: currently using spotify username for cache key, use db username
|
||||||
|
var receivers =
|
||||||
|
await Subscriber.PublishAsync(RedisChannel.Literal(Key.CurrentlyPlayingAppleMusic(e.Id)), payload);
|
||||||
|
|
||||||
|
Logger.LogDebug("Published current, {receivers} receivers", receivers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Subscribe(IWatcher watch = null)
|
||||||
|
{
|
||||||
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
|
if (watcher is IAppleMusicPlayerWatcher 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 IAppleMusicPlayerWatcher watcherCast)
|
||||||
|
{
|
||||||
|
watcherCast.ItemChange -= Callback;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +1,30 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.ConfigFactory;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
|
using Selector.Spotify.Consumer.Factory;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public class CachingAudioFeatureInjectorFactory: IAudioFeatureInjectorFactory {
|
public class CachingAudioFeatureInjectorFactory : IAudioFeatureInjectorFactory
|
||||||
|
{
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
private readonly IDatabaseAsync Db;
|
private readonly IDatabaseAsync Db;
|
||||||
|
|
||||||
public CachingAudioFeatureInjectorFactory(
|
public CachingAudioFeatureInjectorFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IDatabaseAsync db
|
IDatabaseAsync db
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
Db = db;
|
Db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
|
public async Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
|
||||||
|
ISpotifyPlayerWatcher watcher = null)
|
||||||
{
|
{
|
||||||
if (!Magic.Dummy)
|
if (!Magic.Dummy)
|
||||||
{
|
{
|
||||||
@ -45,4 +47,4 @@ namespace Selector.Cache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,37 +1,48 @@
|
|||||||
using System;
|
using System.Threading.Tasks;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.AppleMusic.Consumer;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public interface ICacheWriterFactory {
|
public interface ICacheWriterFactory
|
||||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
|
{
|
||||||
|
public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null);
|
||||||
|
public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheWriterFactory: ICacheWriterFactory {
|
public class CacheWriterFactory : ICacheWriterFactory
|
||||||
|
{
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
private readonly IDatabaseAsync Cache;
|
private readonly IDatabaseAsync Cache;
|
||||||
|
|
||||||
public CacheWriterFactory(
|
public CacheWriterFactory(
|
||||||
IDatabaseAsync cache,
|
IDatabaseAsync cache,
|
||||||
ILoggerFactory loggerFactory
|
ILoggerFactory loggerFactory
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
Cache = cache;
|
Cache = cache;
|
||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null)
|
public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null)
|
||||||
{
|
{
|
||||||
return Task.FromResult<IPlayerConsumer>(new CacheWriter(
|
return Task.FromResult<ISpotifyPlayerConsumer>(new SpotifyCacheWriter(
|
||||||
watcher,
|
watcher,
|
||||||
Cache,
|
Cache,
|
||||||
LoggerFactory.CreateLogger<CacheWriter>()
|
LoggerFactory.CreateLogger<SpotifyCacheWriter>()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IApplePlayerConsumer>(new AppleCacheWriter(
|
||||||
|
watcher,
|
||||||
|
Cache,
|
||||||
|
LoggerFactory.CreateLogger<AppleCacheWriter>()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,15 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using StackExchange.Redis;
|
|
||||||
using IF.Lastfm.Core.Api;
|
using IF.Lastfm.Core.Api;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
|
using Selector.Spotify.Consumer.Factory;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public class PlayCounterCachingFactory: IPlayCounterFactory
|
public class PlayCounterCachingFactory : IPlayCounterFactory
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
private readonly IDatabaseAsync Cache;
|
private readonly IDatabaseAsync Cache;
|
||||||
@ -17,9 +17,9 @@ namespace Selector.Cache
|
|||||||
private readonly LastFmCredentials Creds;
|
private readonly LastFmCredentials Creds;
|
||||||
|
|
||||||
public PlayCounterCachingFactory(
|
public PlayCounterCachingFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IDatabaseAsync cache,
|
IDatabaseAsync cache,
|
||||||
LastfmClient client = null,
|
LastfmClient client = null,
|
||||||
LastFmCredentials creds = null)
|
LastFmCredentials creds = null)
|
||||||
{
|
{
|
||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
@ -28,7 +28,8 @@ namespace Selector.Cache
|
|||||||
Creds = creds;
|
Creds = creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
|
public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
|
||||||
|
ISpotifyPlayerWatcher watcher = null)
|
||||||
{
|
{
|
||||||
var client = fmClient ?? Client;
|
var client = fmClient ?? Client;
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ namespace Selector.Cache
|
|||||||
throw new ArgumentNullException("No Last.fm client provided");
|
throw new ArgumentNullException("No Last.fm client provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult<IPlayerConsumer>(new PlayCounterCaching(
|
return Task.FromResult<ISpotifyPlayerConsumer>(new PlayCounterCaching(
|
||||||
watcher,
|
watcher,
|
||||||
client.Track,
|
client.Track,
|
||||||
client.Album,
|
client.Album,
|
||||||
@ -49,4 +50,4 @@ namespace Selector.Cache
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,37 +1,49 @@
|
|||||||
using System;
|
using System.Threading.Tasks;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.AppleMusic.Consumer;
|
||||||
|
using Selector.Cache.Consumer.AppleMusic;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public interface IPublisherFactory {
|
public interface IPublisherFactory
|
||||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
|
{
|
||||||
|
public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null);
|
||||||
|
public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PublisherFactory: IPublisherFactory {
|
public class PublisherFactory : IPublisherFactory
|
||||||
|
{
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
private readonly ISubscriber Subscriber;
|
private readonly ISubscriber Subscriber;
|
||||||
|
|
||||||
public PublisherFactory(
|
public PublisherFactory(
|
||||||
ISubscriber subscriber,
|
ISubscriber subscriber,
|
||||||
ILoggerFactory loggerFactory
|
ILoggerFactory loggerFactory
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
Subscriber = subscriber;
|
Subscriber = subscriber;
|
||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null)
|
public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null)
|
||||||
{
|
{
|
||||||
return Task.FromResult<IPlayerConsumer>(new Publisher(
|
return Task.FromResult<ISpotifyPlayerConsumer>(new SpotifyPublisher(
|
||||||
watcher,
|
watcher,
|
||||||
Subscriber,
|
Subscriber,
|
||||||
LoggerFactory.CreateLogger<Publisher>()
|
LoggerFactory.CreateLogger<SpotifyPublisher>()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IApplePlayerConsumer>(new ApplePublisher(
|
||||||
|
watcher,
|
||||||
|
Subscriber,
|
||||||
|
LoggerFactory.CreateLogger<ApplePublisher>()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.Extensions;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
@ -16,13 +17,13 @@ namespace Selector.Cache
|
|||||||
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(14);
|
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(14);
|
||||||
|
|
||||||
public CachingAudioFeatureInjector(
|
public CachingAudioFeatureInjector(
|
||||||
IPlayerWatcher watcher,
|
ISpotifyPlayerWatcher watcher,
|
||||||
IDatabaseAsync db,
|
IDatabaseAsync db,
|
||||||
ITracksClient trackClient,
|
ITracksClient trackClient,
|
||||||
ILogger<CachingAudioFeatureInjector> logger = null,
|
ILogger<CachingAudioFeatureInjector> logger = null,
|
||||||
CancellationToken token = default
|
CancellationToken token = default
|
||||||
) : base(watcher, trackClient, logger, token) {
|
) : base(watcher, trackClient, logger, token)
|
||||||
|
{
|
||||||
Db = db;
|
Db = db;
|
||||||
|
|
||||||
NewFeature += CacheCallback;
|
NewFeature += CacheCallback;
|
||||||
@ -45,13 +46,14 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
public async Task AsyncCacheCallback(AnalysedTrack e)
|
public async Task AsyncCacheCallback(AnalysedTrack e)
|
||||||
{
|
{
|
||||||
var payload = JsonSerializer.Serialize(e.Features, JsonContext.Default.TrackAudioFeatures);
|
var payload = JsonSerializer.Serialize(e.Features, SpotifyJsonContext.Default.TrackAudioFeatures);
|
||||||
|
|
||||||
Logger.LogTrace("Caching current for [{track}]", e.Track.DisplayString());
|
Logger.LogTrace("Caching current for [{track}]", e.Track.DisplayString());
|
||||||
|
|
||||||
var resp = await Db.StringSetAsync(Key.AudioFeature(e.Track.Id), payload, expiry: CacheExpiry);
|
var resp = await Db.StringSetAsync(Key.AudioFeature(e.Track.Id), payload, expiry: CacheExpiry);
|
||||||
|
|
||||||
Logger.LogDebug("Cached audio feature for [{track}], {state}", e.Track.DisplayString(), (resp ? "value set" : "value NOT set"));
|
Logger.LogDebug("Cached audio feature for [{track}], {state}", e.Track.DisplayString(),
|
||||||
|
(resp ? "value set" : "value NOT set"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,41 +1,44 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.Extensions;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public class CacheWriter : IPlayerConsumer
|
public class SpotifyCacheWriter : ISpotifyPlayerConsumer
|
||||||
{
|
{
|
||||||
private readonly IPlayerWatcher Watcher;
|
private readonly ISpotifyPlayerWatcher Watcher;
|
||||||
private readonly IDatabaseAsync Db;
|
private readonly IDatabaseAsync Db;
|
||||||
private readonly ILogger<CacheWriter> Logger;
|
private readonly ILogger<SpotifyCacheWriter> Logger;
|
||||||
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(20);
|
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(20);
|
||||||
|
|
||||||
public CancellationToken CancelToken { get; set; }
|
public CancellationToken CancelToken { get; set; }
|
||||||
|
|
||||||
public CacheWriter(
|
public SpotifyCacheWriter(
|
||||||
IPlayerWatcher watcher,
|
ISpotifyPlayerWatcher watcher,
|
||||||
IDatabaseAsync db,
|
IDatabaseAsync db,
|
||||||
ILogger<CacheWriter> logger = null,
|
ILogger<SpotifyCacheWriter> logger = null,
|
||||||
CancellationToken token = default
|
CancellationToken token = default
|
||||||
){
|
)
|
||||||
|
{
|
||||||
Watcher = watcher;
|
Watcher = watcher;
|
||||||
Db = db;
|
Db = db;
|
||||||
Logger = logger ?? NullLogger<CacheWriter>.Instance;
|
Logger = logger ?? NullLogger<SpotifyCacheWriter>.Instance;
|
||||||
CancelToken = token;
|
CancelToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Callback(object sender, ListeningChangeEventArgs e)
|
public void Callback(object sender, SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Current is null) return;
|
if (e.Current is null) return;
|
||||||
|
|
||||||
Task.Run(async () => {
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AsyncCallback(e);
|
await AsyncCallback(e);
|
||||||
@ -44,32 +47,31 @@ namespace Selector.Cache
|
|||||||
{
|
{
|
||||||
Logger.LogError(e, "Error occured during callback");
|
Logger.LogError(e, "Error occured during callback");
|
||||||
}
|
}
|
||||||
|
|
||||||
}, CancelToken);
|
}, CancelToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AsyncCallback(ListeningChangeEventArgs e)
|
public async Task AsyncCallback(SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
using var scope = Logger.GetListeningEventArgsScope(e);
|
using var scope = Logger.GetListeningEventArgsScope(e);
|
||||||
|
|
||||||
var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e, JsonContext.Default.CurrentlyPlayingDTO);
|
var payload =
|
||||||
|
JsonSerializer.Serialize((CurrentlyPlayingDTO)e, SpotifyJsonContext.Default.CurrentlyPlayingDTO);
|
||||||
|
|
||||||
Logger.LogTrace("Caching current");
|
Logger.LogTrace("Caching current");
|
||||||
|
|
||||||
var resp = await Db.StringSetAsync(Key.CurrentlyPlaying(e.Id), payload, expiry: CacheExpiry);
|
var resp = await Db.StringSetAsync(Key.CurrentlyPlayingSpotify(e.Id), payload, expiry: CacheExpiry);
|
||||||
|
|
||||||
Logger.LogDebug("Cached current, {state}", (resp ? "value set" : "value NOT set"));
|
Logger.LogDebug("Cached current, {state}", (resp ? "value set" : "value NOT set"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Subscribe(IWatcher watch = null)
|
public void Subscribe(IWatcher watch = null)
|
||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange += Callback;
|
watcherCast.ItemChange += Callback;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||||
@ -80,7 +82,7 @@ namespace Selector.Cache
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange -= Callback;
|
watcherCast.ItemChange -= Callback;
|
||||||
}
|
}
|
||||||
@ -90,4 +92,4 @@ namespace Selector.Cache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,25 +1,23 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using IF.Lastfm.Core.Api;
|
using IF.Lastfm.Core.Api;
|
||||||
using StackExchange.Redis;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.Extensions;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public class PlayCounterCaching: PlayCounter
|
public class PlayCounterCaching : PlayCounter
|
||||||
{
|
{
|
||||||
private readonly IDatabaseAsync Db;
|
private readonly IDatabaseAsync Db;
|
||||||
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(1);
|
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(1);
|
||||||
|
|
||||||
public PlayCounterCaching(
|
public PlayCounterCaching(
|
||||||
IPlayerWatcher watcher,
|
ISpotifyPlayerWatcher watcher,
|
||||||
ITrackApi trackClient,
|
ITrackApi trackClient,
|
||||||
IAlbumApi albumClient,
|
IAlbumApi albumClient,
|
||||||
IArtistApi artistClient,
|
IArtistApi artistClient,
|
||||||
@ -37,7 +35,8 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
public void CacheCallback(object sender, PlayCount e)
|
public void CacheCallback(object sender, PlayCount e)
|
||||||
{
|
{
|
||||||
Task.Run(async () => {
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AsyncCacheCallback(e);
|
await AsyncCacheCallback(e);
|
||||||
@ -51,14 +50,17 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
public async Task AsyncCacheCallback(PlayCount e)
|
public async Task AsyncCacheCallback(PlayCount e)
|
||||||
{
|
{
|
||||||
var track = e.ListeningEvent.Current.Item as FullTrack;
|
var track = e.SpotifyListeningEvent.Current.Item as FullTrack;
|
||||||
Logger.LogTrace("Caching play count for [{track}]", track.DisplayString());
|
Logger.LogTrace("Caching play count for [{track}]", track.DisplayString());
|
||||||
|
|
||||||
var tasks = new Task[]
|
var tasks = new Task[]
|
||||||
{
|
{
|
||||||
Db.StringSetAsync(Key.TrackPlayCount(e.Username, track.Name, track.Artists[0].Name), e.Track, expiry: CacheExpiry),
|
Db.StringSetAsync(Key.TrackPlayCount(e.Username, track.Name, track.Artists[0].Name), e.Track,
|
||||||
Db.StringSetAsync(Key.AlbumPlayCount(e.Username, track.Album.Name, track.Album.Artists[0].Name), e.Album, expiry: CacheExpiry),
|
expiry: CacheExpiry),
|
||||||
Db.StringSetAsync(Key.ArtistPlayCount(e.Username, track.Artists[0].Name), e.Artist, expiry: CacheExpiry),
|
Db.StringSetAsync(Key.AlbumPlayCount(e.Username, track.Album.Name, track.Album.Artists[0].Name),
|
||||||
|
e.Album, expiry: CacheExpiry),
|
||||||
|
Db.StringSetAsync(Key.ArtistPlayCount(e.Username, track.Artists[0].Name), e.Artist,
|
||||||
|
expiry: CacheExpiry),
|
||||||
Db.StringSetAsync(Key.UserPlayCount(e.Username), e.User, expiry: CacheExpiry),
|
Db.StringSetAsync(Key.UserPlayCount(e.Username), e.User, expiry: CacheExpiry),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,4 +69,4 @@ namespace Selector.Cache
|
|||||||
Logger.LogDebug("Cached play count for [{track}]", track.DisplayString());
|
Logger.LogDebug("Cached play count for [{track}]", track.DisplayString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
45
Selector.Cache/Consumer/PublisherConsumer.cs → Selector.Cache/Consumer/Spotify/PublisherConsumer.cs
45
Selector.Cache/Consumer/PublisherConsumer.cs → Selector.Cache/Consumer/Spotify/PublisherConsumer.cs
@ -1,40 +1,43 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.Extensions;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
public class Publisher : IPlayerConsumer
|
public class SpotifyPublisher : ISpotifyPlayerConsumer
|
||||||
{
|
{
|
||||||
private readonly IPlayerWatcher Watcher;
|
private readonly ISpotifyPlayerWatcher Watcher;
|
||||||
private readonly ISubscriber Subscriber;
|
private readonly ISubscriber Subscriber;
|
||||||
private readonly ILogger<Publisher> Logger;
|
private readonly ILogger<SpotifyPublisher> Logger;
|
||||||
|
|
||||||
public CancellationToken CancelToken { get; set; }
|
public CancellationToken CancelToken { get; set; }
|
||||||
|
|
||||||
public Publisher(
|
public SpotifyPublisher(
|
||||||
IPlayerWatcher watcher,
|
ISpotifyPlayerWatcher watcher,
|
||||||
ISubscriber subscriber,
|
ISubscriber subscriber,
|
||||||
ILogger<Publisher> logger = null,
|
ILogger<SpotifyPublisher> logger = null,
|
||||||
CancellationToken token = default
|
CancellationToken token = default
|
||||||
){
|
)
|
||||||
|
{
|
||||||
Watcher = watcher;
|
Watcher = watcher;
|
||||||
Subscriber = subscriber;
|
Subscriber = subscriber;
|
||||||
Logger = logger ?? NullLogger<Publisher>.Instance;
|
Logger = logger ?? NullLogger<SpotifyPublisher>.Instance;
|
||||||
CancelToken = token;
|
CancelToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Callback(object sender, ListeningChangeEventArgs e)
|
public void Callback(object sender, SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Current is null) return;
|
if (e.Current is null) return;
|
||||||
|
|
||||||
Task.Run(async () => {
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AsyncCallback(e);
|
await AsyncCallback(e);
|
||||||
@ -46,16 +49,18 @@ namespace Selector.Cache
|
|||||||
}, CancelToken);
|
}, CancelToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AsyncCallback(ListeningChangeEventArgs e)
|
public async Task AsyncCallback(SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
using var scope = Logger.GetListeningEventArgsScope(e);
|
using var scope = Logger.GetListeningEventArgsScope(e);
|
||||||
|
|
||||||
var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e, JsonContext.Default.CurrentlyPlayingDTO);
|
var payload =
|
||||||
|
JsonSerializer.Serialize((CurrentlyPlayingDTO)e, SpotifyJsonContext.Default.CurrentlyPlayingDTO);
|
||||||
|
|
||||||
Logger.LogTrace("Publishing current");
|
Logger.LogTrace("Publishing current");
|
||||||
|
|
||||||
// TODO: currently using spotify username for cache key, use db username
|
// TODO: currently using spotify username for cache key, use db username
|
||||||
var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.Id), payload);
|
var receivers =
|
||||||
|
await Subscriber.PublishAsync(RedisChannel.Literal(Key.CurrentlyPlayingSpotify(e.Id)), payload);
|
||||||
|
|
||||||
Logger.LogDebug("Published current, {receivers} receivers", receivers);
|
Logger.LogDebug("Published current, {receivers} receivers", receivers);
|
||||||
}
|
}
|
||||||
@ -64,10 +69,10 @@ namespace Selector.Cache
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange += Callback;
|
watcherCast.ItemChange += Callback;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||||
@ -78,7 +83,7 @@ namespace Selector.Cache
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange -= Callback;
|
watcherCast.ItemChange -= Callback;
|
||||||
}
|
}
|
||||||
@ -88,4 +93,4 @@ namespace Selector.Cache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Selector.Spotify.Consumer.Factory;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Cache.Extensions
|
namespace Selector.Cache.Extensions
|
||||||
@ -21,8 +19,10 @@ namespace Selector.Cache.Extensions
|
|||||||
|
|
||||||
var connMulti = ConnectionMultiplexer.Connect(connectionStr);
|
var connMulti = ConnectionMultiplexer.Connect(connectionStr);
|
||||||
services.AddSingleton(connMulti);
|
services.AddSingleton(connMulti);
|
||||||
services.AddTransient<IDatabaseAsync>(services => services.GetService<ConnectionMultiplexer>().GetDatabase());
|
services.AddTransient<IDatabaseAsync>(
|
||||||
services.AddTransient<ISubscriber>(services => services.GetService<ConnectionMultiplexer>().GetSubscriber());
|
services => services.GetService<ConnectionMultiplexer>().GetDatabase());
|
||||||
|
services.AddTransient<ISubscriber>(services =>
|
||||||
|
services.GetService<ConnectionMultiplexer>().GetSubscriber());
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@ -56,4 +56,4 @@ namespace Selector.Cache.Extensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,4 @@
|
|||||||
using System;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
@ -23,6 +20,7 @@ namespace Selector.Cache
|
|||||||
public const string Duration = "DURATION";
|
public const string Duration = "DURATION";
|
||||||
|
|
||||||
public const string SpotifyName = "SPOTIFY";
|
public const string SpotifyName = "SPOTIFY";
|
||||||
|
public const string AppleMusicName = "APPLEMUSIC";
|
||||||
public const string LastfmName = "LASTFM";
|
public const string LastfmName = "LASTFM";
|
||||||
|
|
||||||
public const string WatcherName = "WATCHER";
|
public const string WatcherName = "WATCHER";
|
||||||
@ -32,23 +30,47 @@ namespace Selector.Cache
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">User's database Id (Guid)</param>
|
/// <param name="user">User's database Id (Guid)</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string CurrentlyPlaying(string user) => MajorNamespace(MinorNamespace(UserName, CurrentlyPlayingName), user);
|
public static string CurrentlyPlayingSpotify(string user) =>
|
||||||
public static readonly string AllCurrentlyPlaying = CurrentlyPlaying(All);
|
MajorNamespace(MinorNamespace(UserName, SpotifyName, CurrentlyPlayingName), user);
|
||||||
|
|
||||||
|
public static string CurrentlyPlayingAppleMusic(string user) =>
|
||||||
|
MajorNamespace(MinorNamespace(UserName, AppleMusicName, CurrentlyPlayingName), user);
|
||||||
|
|
||||||
|
public static readonly string AllCurrentlyPlayingSpotify = CurrentlyPlayingSpotify(All);
|
||||||
|
public static readonly string AllCurrentlyPlayingApple = CurrentlyPlayingAppleMusic(All);
|
||||||
|
|
||||||
public static string Track(string trackId) => MajorNamespace(TrackName, trackId);
|
public static string Track(string trackId) => MajorNamespace(TrackName, trackId);
|
||||||
public static readonly string AllTracks = Track(All);
|
public static readonly string AllTracks = Track(All);
|
||||||
|
|
||||||
public static string AudioFeature(string trackId) => MajorNamespace(MinorNamespace(TrackName, AudioFeatureName), trackId);
|
public static string AudioFeature(string trackId) =>
|
||||||
|
MajorNamespace(MinorNamespace(TrackName, AudioFeatureName), trackId);
|
||||||
|
|
||||||
public static readonly string AllAudioFeatures = AudioFeature(All);
|
public static readonly string AllAudioFeatures = AudioFeature(All);
|
||||||
|
|
||||||
public static string TrackPlayCount(string username, string name, string artist) => MajorNamespace(MinorNamespace(TrackName, PlayCountName), artist, name, username);
|
public static string TrackPlayCount(string username, string name, string artist) =>
|
||||||
public static string AlbumPlayCount(string username, string name, string artist) => MajorNamespace(MinorNamespace(AlbumName, PlayCountName), artist, name, username);
|
MajorNamespace(MinorNamespace(TrackName, PlayCountName), artist, name, username);
|
||||||
public static string ArtistPlayCount(string username, string name) => MajorNamespace(MinorNamespace(ArtistName, PlayCountName), name, username);
|
|
||||||
public static string UserPlayCount(string username) => MajorNamespace(MinorNamespace(UserName, PlayCountName), username);
|
public static string AlbumPlayCount(string username, string name, string artist) =>
|
||||||
|
MajorNamespace(MinorNamespace(AlbumName, PlayCountName), artist, name, username);
|
||||||
|
|
||||||
|
public static string ArtistPlayCount(string username, string name) =>
|
||||||
|
MajorNamespace(MinorNamespace(ArtistName, PlayCountName), name, username);
|
||||||
|
|
||||||
|
public static string UserPlayCount(string username) =>
|
||||||
|
MajorNamespace(MinorNamespace(UserName, PlayCountName), username);
|
||||||
|
|
||||||
|
public static string UserSpotify(string username) =>
|
||||||
|
MajorNamespace(MinorNamespace(UserName, SpotifyName), username);
|
||||||
|
|
||||||
|
public static string UserAppleMusic(string username) =>
|
||||||
|
MajorNamespace(MinorNamespace(UserName, AppleMusicName), username);
|
||||||
|
|
||||||
public static string UserSpotify(string username) => MajorNamespace(MinorNamespace(UserName, SpotifyName), username);
|
|
||||||
public static readonly string AllUserSpotify = UserSpotify(All);
|
public static readonly string AllUserSpotify = UserSpotify(All);
|
||||||
public static string UserLastfm(string username) => MajorNamespace(MinorNamespace(UserName, LastfmName), username);
|
public static readonly string AllUserAppleMusic = UserAppleMusic(All);
|
||||||
|
|
||||||
|
public static string UserLastfm(string username) =>
|
||||||
|
MajorNamespace(MinorNamespace(UserName, LastfmName), username);
|
||||||
|
|
||||||
public static readonly string AllUserLastfm = UserLastfm(All);
|
public static readonly string AllUserLastfm = UserLastfm(All);
|
||||||
|
|
||||||
public static string Watcher(int id) => MajorNamespace(WatcherName, id.ToString());
|
public static string Watcher(int id) => MajorNamespace(WatcherName, id.ToString());
|
||||||
@ -63,14 +85,17 @@ namespace Selector.Cache
|
|||||||
public static string[] UnNamespace(string key, params char[] args) => key.Split(args);
|
public static string[] UnNamespace(string key, params char[] args) => key.Split(args);
|
||||||
|
|
||||||
public static string Param(string key) => UnMajorNamespace(key).Skip(1).First();
|
public static string Param(string key) => UnMajorNamespace(key).Skip(1).First();
|
||||||
public static (string, string) ParamPair(string key) {
|
|
||||||
|
public static (string, string) ParamPair(string key)
|
||||||
|
{
|
||||||
var split = UnMajorNamespace(key);
|
var split = UnMajorNamespace(key);
|
||||||
return (split[1], split[2]);
|
return (split[1], split[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (string, string, string) ParamTriplet(string key)
|
public static (string, string, string) ParamTriplet(string key)
|
||||||
{
|
{
|
||||||
var split = UnMajorNamespace(key);
|
var split = UnMajorNamespace(key);
|
||||||
return (split[1], split[2], split[3]);
|
return (split[1], split[2], split[3]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,6 +13,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
|
||||||
|
<ProjectReference Include="..\Selector.Spotify\Selector.Spotify.csproj"/>
|
||||||
<ProjectReference Include="..\Selector\Selector.csproj" />
|
<ProjectReference Include="..\Selector\Selector.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.FactoryProvider;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
@ -22,12 +24,12 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
public async Task<TrackAudioFeatures> Get(string refreshToken, string trackId)
|
public async Task<TrackAudioFeatures> Get(string refreshToken, string trackId)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(trackId)) throw new ArgumentNullException("No track Id provided");
|
if (string.IsNullOrWhiteSpace(trackId)) throw new ArgumentNullException("No track Id provided");
|
||||||
|
|
||||||
var track = await Cache?.StringGetAsync(Key.AudioFeature(trackId));
|
var track = await Cache?.StringGetAsync(Key.AudioFeature(trackId));
|
||||||
if (Cache is null || track == RedisValue.Null)
|
if (Cache is null || track == RedisValue.Null)
|
||||||
{
|
{
|
||||||
if(!string.IsNullOrWhiteSpace(refreshToken) && !Magic.Dummy)
|
if (!string.IsNullOrWhiteSpace(refreshToken) && !Magic.Dummy)
|
||||||
{
|
{
|
||||||
var factory = await SpotifyFactory.GetFactory(refreshToken);
|
var factory = await SpotifyFactory.GetFactory(refreshToken);
|
||||||
var spotifyClient = new SpotifyClient(await factory.GetConfig());
|
var spotifyClient = new SpotifyClient(await factory.GetConfig());
|
||||||
@ -35,17 +37,16 @@ namespace Selector.Cache
|
|||||||
// TODO: Error checking
|
// TODO: Error checking
|
||||||
return await spotifyClient.Tracks.GetAudioFeatures(trackId);
|
return await spotifyClient.Tracks.GetAudioFeatures(trackId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var deserialised = JsonSerializer.Deserialize(track, JsonContext.Default.TrackAudioFeatures);
|
var deserialised = JsonSerializer.Deserialize(track, SpotifyJsonContext.Default.TrackAudioFeatures);
|
||||||
return deserialised;
|
return deserialised;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
@ -21,7 +20,6 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
public DurationPuller(
|
public DurationPuller(
|
||||||
ILogger<DurationPuller> logger,
|
ILogger<DurationPuller> logger,
|
||||||
|
|
||||||
ITracksClient spotifyClient,
|
ITracksClient spotifyClient,
|
||||||
IDatabaseAsync cache = null
|
IDatabaseAsync cache = null
|
||||||
)
|
)
|
||||||
@ -41,7 +39,8 @@ namespace Selector.Cache
|
|||||||
var cachedVal = await Cache?.HashGetAsync(Key.Track(trackId), Key.Duration);
|
var cachedVal = await Cache?.HashGetAsync(Key.Track(trackId), Key.Duration);
|
||||||
if (Cache is null || cachedVal == RedisValue.Null || cachedVal.IsNullOrEmpty)
|
if (Cache is null || cachedVal == RedisValue.Null || cachedVal.IsNullOrEmpty)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
Logger.LogDebug("Missed cache, pulling");
|
Logger.LogDebug("Missed cache, pulling");
|
||||||
|
|
||||||
var info = await SpotifyClient.Get(trackId);
|
var info = await SpotifyClient.Get(trackId);
|
||||||
@ -55,13 +54,14 @@ namespace Selector.Cache
|
|||||||
catch (APIUnauthorizedException e)
|
catch (APIUnauthorizedException e)
|
||||||
{
|
{
|
||||||
Logger.LogError("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
|
Logger.LogError("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
|
||||||
throw e;
|
throw;
|
||||||
}
|
}
|
||||||
catch (APITooManyRequestsException e)
|
catch (APITooManyRequestsException e)
|
||||||
{
|
{
|
||||||
if(_retries <= 3)
|
if (_retries <= 3)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("Too many requests error, retrying ({}): [{message}]", e.RetryAfter, e.Message);
|
Logger.LogWarning("Too many requests error, retrying ({}): [{message}]", e.RetryAfter,
|
||||||
|
e.Message);
|
||||||
_retries++;
|
_retries++;
|
||||||
await Task.Delay(e.RetryAfter);
|
await Task.Delay(e.RetryAfter);
|
||||||
return await Get(uri);
|
return await Get(uri);
|
||||||
@ -69,7 +69,7 @@ namespace Selector.Cache
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogError("Too many requests error, done retrying: [{message}]", e.Message);
|
Logger.LogError("Too many requests error, done retrying: [{message}]", e.Message);
|
||||||
throw e;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (APIException e)
|
catch (APIException e)
|
||||||
@ -84,13 +84,13 @@ namespace Selector.Cache
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogError("API error, done retrying: [{message}]", e.Message);
|
Logger.LogError("API error, done retrying: [{message}]", e.Message);
|
||||||
throw e;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return (int?) cachedVal;
|
return (int?)cachedVal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,17 +111,17 @@ namespace Selector.Cache
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ret[input] = (int) cachedVal;
|
ret[input] = (int)cachedVal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var retries = new List<string>();
|
var retries = new List<string>();
|
||||||
|
|
||||||
foreach(var chunk in toPullFromSpotify.Chunk(50))
|
foreach (var chunk in toPullFromSpotify.Chunk(50))
|
||||||
{
|
{
|
||||||
await PullChunk(chunk, ret);
|
await PullChunk(chunk, ret);
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(500));
|
await Task.Delay(TimeSpan.FromMilliseconds(500));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@ -144,7 +144,7 @@ namespace Selector.Cache
|
|||||||
catch (APIUnauthorizedException e)
|
catch (APIUnauthorizedException e)
|
||||||
{
|
{
|
||||||
Logger.LogError("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
|
Logger.LogError("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
|
||||||
throw e;
|
throw;
|
||||||
}
|
}
|
||||||
catch (APITooManyRequestsException e)
|
catch (APITooManyRequestsException e)
|
||||||
{
|
{
|
||||||
@ -159,7 +159,7 @@ namespace Selector.Cache
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogError("Too many requests error, done retrying: [{message}]", e.Message);
|
Logger.LogError("Too many requests error, done retrying: [{message}]", e.Message);
|
||||||
throw e;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (APIException e)
|
catch (APIException e)
|
||||||
@ -175,9 +175,9 @@ namespace Selector.Cache
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogError("API error, done retrying: [{message}]", e.Message);
|
Logger.LogError("API error, done retrying: [{message}]", e.Message);
|
||||||
throw e;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,13 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using IF.Lastfm.Core.Api;
|
using IF.Lastfm.Core.Api;
|
||||||
using IF.Lastfm.Core.Api.Helpers;
|
using IF.Lastfm.Core.Api.Helpers;
|
||||||
using IF.Lastfm.Core.Objects;
|
using IF.Lastfm.Core.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
@ -24,7 +22,6 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
public PlayCountPuller(
|
public PlayCountPuller(
|
||||||
ILogger<PlayCountPuller> logger,
|
ILogger<PlayCountPuller> logger,
|
||||||
|
|
||||||
ITrackApi trackClient,
|
ITrackApi trackClient,
|
||||||
IAlbumApi albumClient,
|
IAlbumApi albumClient,
|
||||||
IArtistApi artistClient,
|
IArtistApi artistClient,
|
||||||
@ -47,7 +44,7 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
var trackCache = Cache?.StringGetAsync(Key.TrackPlayCount(username, track, artist));
|
var trackCache = Cache?.StringGetAsync(Key.TrackPlayCount(username, track, artist));
|
||||||
var albumCache = Cache?.StringGetAsync(Key.AlbumPlayCount(username, album, albumArtist));
|
var albumCache = Cache?.StringGetAsync(Key.AlbumPlayCount(username, album, albumArtist));
|
||||||
var artistCache = Cache?.StringGetAsync(Key.ArtistPlayCount(username, artist));
|
var artistCache = Cache?.StringGetAsync(Key.ArtistPlayCount(username, artist));
|
||||||
var userCache = Cache?.StringGetAsync(Key.UserPlayCount(username));
|
var userCache = Cache?.StringGetAsync(Key.UserPlayCount(username));
|
||||||
|
|
||||||
var cacheTasks = new Task[] { trackCache, albumCache, artistCache, userCache };
|
var cacheTasks = new Task[] { trackCache, albumCache, artistCache, userCache };
|
||||||
@ -66,7 +63,7 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
if (trackCache is not null && trackCache.IsCompletedSuccessfully && trackCache.Result != RedisValue.Null)
|
if (trackCache is not null && trackCache.IsCompletedSuccessfully && trackCache.Result != RedisValue.Null)
|
||||||
{
|
{
|
||||||
playCount.Track = (int) trackCache.Result;
|
playCount.Track = (int)trackCache.Result;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -75,7 +72,7 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
if (albumCache is not null && albumCache.IsCompletedSuccessfully && albumCache.Result != RedisValue.Null)
|
if (albumCache is not null && albumCache.IsCompletedSuccessfully && albumCache.Result != RedisValue.Null)
|
||||||
{
|
{
|
||||||
playCount.Album = (int) albumCache.Result;
|
playCount.Album = (int)albumCache.Result;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -84,7 +81,7 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
if (artistCache is not null && artistCache.IsCompletedSuccessfully && artistCache.Result != RedisValue.Null)
|
if (artistCache is not null && artistCache.IsCompletedSuccessfully && artistCache.Result != RedisValue.Null)
|
||||||
{
|
{
|
||||||
playCount.Artist = (int) artistCache.Result;
|
playCount.Artist = (int)artistCache.Result;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -93,14 +90,14 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
if (userCache is not null && userCache.IsCompletedSuccessfully && userCache.Result != RedisValue.Null)
|
if (userCache is not null && userCache.IsCompletedSuccessfully && userCache.Result != RedisValue.Null)
|
||||||
{
|
{
|
||||||
playCount.User = (int) userCache.Result;
|
playCount.User = (int)userCache.Result;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
userHttp = UserClient.GetInfoAsync(username);
|
userHttp = UserClient.GetInfoAsync(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(new Task[] {trackHttp, albumHttp, artistHttp, userHttp}.Where(t => t is not null));
|
await Task.WhenAll(new Task[] { trackHttp, albumHttp, artistHttp, userHttp }.Where(t => t is not null));
|
||||||
|
|
||||||
if (trackHttp is not null && trackHttp.IsCompletedSuccessfully)
|
if (trackHttp is not null && trackHttp.IsCompletedSuccessfully)
|
||||||
{
|
{
|
||||||
@ -136,11 +133,12 @@ namespace Selector.Cache
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogDebug("User info error [{username}] [{userHttp.Result.Status}]", username, userHttp.Result.Status);
|
Logger.LogDebug("User info error [{username}] [{userHttp.Result.Status}]", username,
|
||||||
|
userHttp.Result.Status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return playCount;
|
return playCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,13 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Selector.Spotify;
|
||||||
|
|
||||||
namespace Selector.Events
|
namespace Selector.Events
|
||||||
{
|
{
|
||||||
[JsonSerializable(typeof(LastfmChange))]
|
[JsonSerializable(typeof(LastfmChange))]
|
||||||
[JsonSerializable(typeof(SpotifyLinkChange))]
|
[JsonSerializable(typeof(SpotifyLinkChange))]
|
||||||
|
[JsonSerializable(typeof(AppleMusicLinkChange))]
|
||||||
[JsonSerializable(typeof((string, CurrentlyPlayingDTO)))]
|
[JsonSerializable(typeof((string, CurrentlyPlayingDTO)))]
|
||||||
public partial class CacheJsonContext: JsonSerializerContext
|
public partial class CacheJsonContext : JsonSerializerContext
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
98
Selector.Event/CacheMappings/AppleMusicMapping.cs
Normal file
98
Selector.Event/CacheMappings/AppleMusicMapping.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.Cache;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
|
namespace Selector.Events
|
||||||
|
{
|
||||||
|
public class AppleMusicLinkChange
|
||||||
|
{
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public bool PreviousLinkState { get; set; }
|
||||||
|
public bool NewLinkState { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class FromPubSub
|
||||||
|
{
|
||||||
|
public class AppleMusicLink : IEventMapping
|
||||||
|
{
|
||||||
|
private readonly ILogger<AppleMusicLink> Logger;
|
||||||
|
private readonly ISubscriber Subscriber;
|
||||||
|
private readonly UserEventBus UserEvent;
|
||||||
|
|
||||||
|
public AppleMusicLink(ILogger<AppleMusicLink> logger,
|
||||||
|
ISubscriber subscriber,
|
||||||
|
UserEventBus userEvent)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
Subscriber = subscriber;
|
||||||
|
UserEvent = userEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ConstructMapping()
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Forming Apple Music link event mapping FROM cache TO event bus");
|
||||||
|
|
||||||
|
(await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllUserAppleMusic))).OnMessage(message =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = Key.Param(message.Channel);
|
||||||
|
|
||||||
|
var deserialised = JsonSerializer.Deserialize(message.Message,
|
||||||
|
CacheJsonContext.Default.AppleMusicLinkChange);
|
||||||
|
Logger.LogDebug("Received new Apple Music link event for [{userId}]", deserialised.UserId);
|
||||||
|
|
||||||
|
if (!userId.Equals(deserialised.UserId))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId,
|
||||||
|
deserialised.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEvent.OnAppleMusicLinkChange(this, deserialised);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Task Cancelled");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "Error parsing new Apple Music link event");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class ToPubSub
|
||||||
|
{
|
||||||
|
public class AppleMusicLink : IEventMapping
|
||||||
|
{
|
||||||
|
private readonly ILogger<AppleMusicLink> Logger;
|
||||||
|
private readonly ISubscriber Subscriber;
|
||||||
|
private readonly UserEventBus UserEvent;
|
||||||
|
|
||||||
|
public AppleMusicLink(ILogger<AppleMusicLink> logger,
|
||||||
|
ISubscriber subscriber,
|
||||||
|
UserEventBus userEvent)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
Subscriber = subscriber;
|
||||||
|
UserEvent = userEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ConstructMapping()
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Forming Apple Music link event mapping TO cache FROM event bus");
|
||||||
|
|
||||||
|
UserEvent.AppleLinkChange += async (o, e) =>
|
||||||
|
{
|
||||||
|
var payload = JsonSerializer.Serialize(e, CacheJsonContext.Default.AppleMusicLinkChange);
|
||||||
|
await Subscriber.PublishAsync(RedisChannel.Literal(Key.UserAppleMusic(e.UserId)), payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
using Selector.Cache;
|
using Selector.Cache;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Events
|
namespace Selector.Events
|
||||||
{
|
{
|
||||||
@ -35,18 +33,20 @@ namespace Selector.Events
|
|||||||
{
|
{
|
||||||
Logger.LogDebug("Forming Last.fm username event mapping FROM cache TO event bus");
|
Logger.LogDebug("Forming Last.fm username event mapping FROM cache TO event bus");
|
||||||
|
|
||||||
(await Subscriber.SubscribeAsync(Key.AllUserLastfm)).OnMessage(message => {
|
(await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllUserLastfm))).OnMessage(message =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = Key.Param(message.Channel);
|
var userId = Key.Param(message.Channel);
|
||||||
|
|
||||||
var deserialised = JsonSerializer.Deserialize(message.Message, CacheJsonContext.Default.LastfmChange);
|
var deserialised =
|
||||||
|
JsonSerializer.Deserialize(message.Message, CacheJsonContext.Default.LastfmChange);
|
||||||
Logger.LogDebug("Received new Last.fm username event for [{userId}]", deserialised.UserId);
|
Logger.LogDebug("Received new Last.fm username event for [{userId}]", deserialised.UserId);
|
||||||
|
|
||||||
if (!userId.Equals(deserialised.UserId))
|
if (!userId.Equals(deserialised.UserId))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId, deserialised.UserId);
|
Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId,
|
||||||
|
deserialised.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserEvent.OnLastfmCredChange(this, deserialised);
|
UserEvent.OnLastfmCredChange(this, deserialised);
|
||||||
@ -84,7 +84,7 @@ namespace Selector.Events
|
|||||||
UserEvent.LastfmCredChange += async (o, e) =>
|
UserEvent.LastfmCredChange += async (o, e) =>
|
||||||
{
|
{
|
||||||
var payload = JsonSerializer.Serialize(e, CacheJsonContext.Default.LastfmChange);
|
var payload = JsonSerializer.Serialize(e, CacheJsonContext.Default.LastfmChange);
|
||||||
await Subscriber.PublishAsync(Key.UserLastfm(e.UserId), payload);
|
await Subscriber.PublishAsync(RedisChannel.Literal(Key.UserLastfm(e.UserId)), payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.AppleMusic;
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
using Selector.Cache;
|
using Selector.Cache;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Events
|
namespace Selector.Events
|
||||||
{
|
{
|
||||||
@ -28,22 +28,45 @@ namespace Selector.Events
|
|||||||
{
|
{
|
||||||
Logger.LogDebug("Forming now playing event mapping between cache and event bus");
|
Logger.LogDebug("Forming now playing event mapping between cache and event bus");
|
||||||
|
|
||||||
(await Subscriber.SubscribeAsync(Key.AllCurrentlyPlaying)).OnMessage(message => {
|
(await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllCurrentlyPlayingSpotify))).OnMessage(
|
||||||
|
message =>
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var userId = Key.Param(message.Channel);
|
try
|
||||||
|
{
|
||||||
|
var userId = Key.Param(message.Channel);
|
||||||
|
|
||||||
var deserialised = JsonSerializer.Deserialize(message.Message, JsonContext.Default.CurrentlyPlayingDTO);
|
var deserialised =
|
||||||
Logger.LogDebug("Received new currently playing [{username}]", deserialised.Username);
|
JsonSerializer.Deserialize(message.Message,
|
||||||
|
SpotifyJsonContext.Default.CurrentlyPlayingDTO);
|
||||||
|
Logger.LogDebug("Received new Spotify currently playing [{username}]",
|
||||||
|
deserialised.Username);
|
||||||
|
|
||||||
UserEvent.OnCurrentlyPlayingChange(this, deserialised);
|
UserEvent.OnCurrentlyPlayingChangeSpotify(this, deserialised);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "Error parsing new Spotify currently playing [{message}]", message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllCurrentlyPlayingApple))).OnMessage(
|
||||||
|
message =>
|
||||||
{
|
{
|
||||||
Logger.LogError(e, "Error parsing new currently playing [{message}]", message);
|
try
|
||||||
}
|
{
|
||||||
});
|
var userId = Key.Param(message.Channel);
|
||||||
|
|
||||||
|
var deserialised = JsonSerializer.Deserialize(message.Message,
|
||||||
|
AppleJsonContext.Default.AppleListeningChangeEventArgs);
|
||||||
|
Logger.LogDebug("Received new Apple Music currently playing");
|
||||||
|
|
||||||
|
UserEvent.OnCurrentlyPlayingChangeApple(this, deserialised);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "Error parsing new Apple Music currently playing [{message}]", message);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,10 +92,16 @@ namespace Selector.Events
|
|||||||
{
|
{
|
||||||
Logger.LogDebug("Forming now playing event mapping TO cache FROM event bus");
|
Logger.LogDebug("Forming now playing event mapping TO cache FROM event bus");
|
||||||
|
|
||||||
UserEvent.CurrentlyPlaying += async (o, e) =>
|
UserEvent.CurrentlyPlayingSpotify += async (o, e) =>
|
||||||
{
|
{
|
||||||
var payload = JsonSerializer.Serialize(e, JsonContext.Default.CurrentlyPlayingDTO);
|
var payload = JsonSerializer.Serialize(e, SpotifyJsonContext.Default.CurrentlyPlayingDTO);
|
||||||
await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.UserId), payload);
|
await Subscriber.PublishAsync(RedisChannel.Literal(Key.CurrentlyPlayingSpotify(e.UserId)), payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
UserEvent.CurrentlyPlayingApple += async (o, e) =>
|
||||||
|
{
|
||||||
|
var payload = JsonSerializer.Serialize(e, AppleJsonContext.Default.AppleListeningChangeEventArgs);
|
||||||
|
await Subscriber.PublishAsync(RedisChannel.Literal(Key.CurrentlyPlayingAppleMusic(e.Id)), payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
using Selector.Cache;
|
using Selector.Cache;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Events
|
namespace Selector.Events
|
||||||
{
|
{
|
||||||
@ -35,18 +33,20 @@ namespace Selector.Events
|
|||||||
{
|
{
|
||||||
Logger.LogDebug("Forming Spotify link event mapping FROM cache TO event bus");
|
Logger.LogDebug("Forming Spotify link event mapping FROM cache TO event bus");
|
||||||
|
|
||||||
(await Subscriber.SubscribeAsync(Key.AllUserSpotify)).OnMessage(message => {
|
(await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllUserSpotify))).OnMessage(message =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = Key.Param(message.Channel);
|
var userId = Key.Param(message.Channel);
|
||||||
|
|
||||||
var deserialised = JsonSerializer.Deserialize(message.Message, CacheJsonContext.Default.SpotifyLinkChange);
|
var deserialised = JsonSerializer.Deserialize(message.Message,
|
||||||
|
CacheJsonContext.Default.SpotifyLinkChange);
|
||||||
Logger.LogDebug("Received new Spotify link event for [{userId}]", deserialised.UserId);
|
Logger.LogDebug("Received new Spotify link event for [{userId}]", deserialised.UserId);
|
||||||
|
|
||||||
if (!userId.Equals(deserialised.UserId))
|
if (!userId.Equals(deserialised.UserId))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId, deserialised.UserId);
|
Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId,
|
||||||
|
deserialised.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserEvent.OnSpotifyLinkChange(this, deserialised);
|
UserEvent.OnSpotifyLinkChange(this, deserialised);
|
||||||
@ -88,7 +88,7 @@ namespace Selector.Events
|
|||||||
UserEvent.SpotifyLinkChange += async (o, e) =>
|
UserEvent.SpotifyLinkChange += async (o, e) =>
|
||||||
{
|
{
|
||||||
var payload = JsonSerializer.Serialize(e, CacheJsonContext.Default.SpotifyLinkChange);
|
var payload = JsonSerializer.Serialize(e, CacheJsonContext.Default.SpotifyLinkChange);
|
||||||
await Subscriber.PublishAsync(Key.UserSpotify(e.UserId), payload);
|
await Subscriber.PublishAsync(RedisChannel.Literal(Key.UserSpotify(e.UserId)), payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -1,35 +1,38 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.AppleMusic;
|
||||||
|
using Selector.AppleMusic.Consumer;
|
||||||
|
|
||||||
namespace Selector.Events
|
namespace Selector.Events
|
||||||
{
|
{
|
||||||
public class UserEventFirer : IPlayerConsumer
|
public class AppleUserEventFirer : IApplePlayerConsumer
|
||||||
{
|
{
|
||||||
protected readonly IPlayerWatcher Watcher;
|
protected readonly IAppleMusicPlayerWatcher Watcher;
|
||||||
protected readonly ILogger<UserEventFirer> Logger;
|
protected readonly ILogger<AppleUserEventFirer> Logger;
|
||||||
|
|
||||||
protected readonly UserEventBus UserEvent;
|
protected readonly UserEventBus UserEvent;
|
||||||
|
|
||||||
public CancellationToken CancelToken { get; set; }
|
public CancellationToken CancelToken { get; set; }
|
||||||
|
|
||||||
public UserEventFirer(
|
public AppleUserEventFirer(
|
||||||
IPlayerWatcher watcher,
|
IAppleMusicPlayerWatcher watcher,
|
||||||
UserEventBus userEvent,
|
UserEventBus userEvent,
|
||||||
ILogger<UserEventFirer> logger = null,
|
ILogger<AppleUserEventFirer> logger = null,
|
||||||
CancellationToken token = default
|
CancellationToken token = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Watcher = watcher;
|
Watcher = watcher;
|
||||||
UserEvent = userEvent;
|
UserEvent = userEvent;
|
||||||
Logger = logger ?? NullLogger<UserEventFirer>.Instance;
|
Logger = logger ?? NullLogger<AppleUserEventFirer>.Instance;
|
||||||
CancelToken = token;
|
CancelToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Callback(object sender, ListeningChangeEventArgs e)
|
public void Callback(object sender, AppleListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Current is null) return;
|
if (e.Current is null) return;
|
||||||
|
|
||||||
Task.Run(async () => {
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AsyncCallback(e);
|
await AsyncCallback(e);
|
||||||
@ -41,11 +44,11 @@ namespace Selector.Events
|
|||||||
}, CancelToken);
|
}, CancelToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AsyncCallback(ListeningChangeEventArgs e)
|
public Task AsyncCallback(AppleListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Firing now playing event on user bus [{username}/{userId}]", e.SpotifyUsername, e.Id);
|
Logger.LogDebug("Firing Apple now playing event on user bus [{userId}]", e.Id);
|
||||||
|
|
||||||
UserEvent.OnCurrentlyPlayingChange(this, (CurrentlyPlayingDTO) e);
|
UserEvent.OnCurrentlyPlayingChangeApple(this, e);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@ -54,7 +57,7 @@ namespace Selector.Events
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is IAppleMusicPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange += Callback;
|
watcherCast.ItemChange += Callback;
|
||||||
}
|
}
|
||||||
@ -68,7 +71,7 @@ namespace Selector.Events
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is IAppleMusicPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange -= Callback;
|
watcherCast.ItemChange -= Callback;
|
||||||
}
|
}
|
||||||
@ -78,4 +81,4 @@ namespace Selector.Events
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
85
Selector.Event/Consumers/SpotifyUserEventFirer.cs
Normal file
85
Selector.Event/Consumers/SpotifyUserEventFirer.cs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
|
|
||||||
|
namespace Selector.Events
|
||||||
|
{
|
||||||
|
public class SpotifyUserEventFirer : ISpotifyPlayerConsumer
|
||||||
|
{
|
||||||
|
protected readonly ISpotifyPlayerWatcher Watcher;
|
||||||
|
protected readonly ILogger<SpotifyUserEventFirer> Logger;
|
||||||
|
|
||||||
|
protected readonly UserEventBus UserEvent;
|
||||||
|
|
||||||
|
public CancellationToken CancelToken { get; set; }
|
||||||
|
|
||||||
|
public SpotifyUserEventFirer(
|
||||||
|
ISpotifyPlayerWatcher watcher,
|
||||||
|
UserEventBus userEvent,
|
||||||
|
ILogger<SpotifyUserEventFirer> logger = null,
|
||||||
|
CancellationToken token = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Watcher = watcher;
|
||||||
|
UserEvent = userEvent;
|
||||||
|
Logger = logger ?? NullLogger<SpotifyUserEventFirer>.Instance;
|
||||||
|
CancelToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Callback(object sender, SpotifyListeningChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Current is null) return;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await AsyncCallback(e);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "Error occured during callback");
|
||||||
|
}
|
||||||
|
}, CancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task AsyncCallback(SpotifyListeningChangeEventArgs e)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Firing Spotify now playing event on user bus [{username}/{userId}]", e.SpotifyUsername,
|
||||||
|
e.Id);
|
||||||
|
|
||||||
|
UserEvent.OnCurrentlyPlayingChangeSpotify(this, (CurrentlyPlayingDTO)e);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Subscribe(IWatcher watch = null)
|
||||||
|
{
|
||||||
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
|
if (watcher is ISpotifyPlayerWatcher 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 ISpotifyPlayerWatcher watcherCast)
|
||||||
|
{
|
||||||
|
watcherCast.ItemChange -= Callback;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,15 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.Spotify;
|
||||||
|
|
||||||
namespace Selector.Events
|
namespace Selector.Events
|
||||||
{
|
{
|
||||||
public interface IUserEventFirerFactory
|
public interface IUserEventFirerFactory
|
||||||
{
|
{
|
||||||
public Task<UserEventFirer> Get(IPlayerWatcher watcher = null);
|
public Task<SpotifyUserEventFirer> GetSpotify(ISpotifyPlayerWatcher watcher = null);
|
||||||
|
public Task<AppleUserEventFirer> GetApple(IAppleMusicPlayerWatcher watcher = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserEventFirerFactory: IUserEventFirerFactory
|
public class UserEventFirerFactory : IUserEventFirerFactory
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
private readonly UserEventBus UserEvent;
|
private readonly UserEventBus UserEvent;
|
||||||
@ -18,13 +20,22 @@ namespace Selector.Events
|
|||||||
UserEvent = userEvent;
|
UserEvent = userEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<UserEventFirer> Get(IPlayerWatcher watcher = null)
|
public Task<SpotifyUserEventFirer> GetSpotify(ISpotifyPlayerWatcher watcher = null)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new UserEventFirer(
|
return Task.FromResult(new SpotifyUserEventFirer(
|
||||||
watcher,
|
watcher,
|
||||||
UserEvent,
|
UserEvent,
|
||||||
LoggerFactory.CreateLogger<UserEventFirer>()
|
LoggerFactory.CreateLogger<SpotifyUserEventFirer>()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<AppleUserEventFirer> GetApple(IAppleMusicPlayerWatcher watcher = null)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new AppleUserEventFirer(
|
||||||
|
watcher,
|
||||||
|
UserEvent,
|
||||||
|
LoggerFactory.CreateLogger<AppleUserEventFirer>()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,6 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
|
||||||
<ProjectReference Include="..\Selector\Selector.csproj" />
|
<ProjectReference Include="..\Selector\Selector.csproj" />
|
||||||
<ProjectReference Include="..\Selector.Model\Selector.Model.csproj" />
|
<ProjectReference Include="..\Selector.Model\Selector.Model.csproj" />
|
||||||
<ProjectReference Include="..\Selector.Cache\Selector.Cache.csproj" />
|
<ProjectReference Include="..\Selector.Cache\Selector.Cache.csproj" />
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Selector.AppleMusic;
|
||||||
using Selector.Model;
|
using Selector.Model;
|
||||||
|
using Selector.Spotify;
|
||||||
|
|
||||||
namespace Selector.Events
|
namespace Selector.Events
|
||||||
{
|
{
|
||||||
public class UserEventBus: IEventBus
|
public class UserEventBus : IEventBus
|
||||||
{
|
{
|
||||||
private readonly ILogger<UserEventBus> Logger;
|
private readonly ILogger<UserEventBus> Logger;
|
||||||
|
|
||||||
public event EventHandler<ApplicationUser> UserChange;
|
public event EventHandler<ApplicationUser> UserChange;
|
||||||
public event EventHandler<SpotifyLinkChange> SpotifyLinkChange;
|
public event EventHandler<SpotifyLinkChange> SpotifyLinkChange;
|
||||||
|
public event EventHandler<AppleMusicLinkChange> AppleLinkChange;
|
||||||
public event EventHandler<LastfmChange> LastfmCredChange;
|
public event EventHandler<LastfmChange> LastfmCredChange;
|
||||||
|
|
||||||
public event EventHandler<CurrentlyPlayingDTO> CurrentlyPlaying;
|
public event EventHandler<CurrentlyPlayingDTO> CurrentlyPlayingSpotify;
|
||||||
|
public event EventHandler<AppleListeningChangeEventArgs> CurrentlyPlayingApple;
|
||||||
|
|
||||||
public UserEventBus(ILogger<UserEventBus> logger)
|
public UserEventBus(ILogger<UserEventBus> logger)
|
||||||
{
|
{
|
||||||
@ -31,16 +34,28 @@ namespace Selector.Events
|
|||||||
SpotifyLinkChange?.Invoke(sender, args);
|
SpotifyLinkChange?.Invoke(sender, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnAppleMusicLinkChange(object sender, AppleMusicLinkChange args)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Firing user Apple Music event [{usernamne}]", args?.UserId);
|
||||||
|
AppleLinkChange?.Invoke(sender, args);
|
||||||
|
}
|
||||||
|
|
||||||
public void OnLastfmCredChange(object sender, LastfmChange args)
|
public void OnLastfmCredChange(object sender, LastfmChange args)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("Firing user Last.fm event [{usernamne}]", args?.UserId);
|
Logger.LogTrace("Firing user Last.fm event [{usernamne}]", args?.UserId);
|
||||||
LastfmCredChange?.Invoke(sender, args);
|
LastfmCredChange?.Invoke(sender, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnCurrentlyPlayingChange(object sender, CurrentlyPlayingDTO args)
|
public void OnCurrentlyPlayingChangeSpotify(object sender, CurrentlyPlayingDTO args)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("Firing currently playing event [{usernamne}/{userId}]", args?.Username, args.UserId);
|
Logger.LogTrace("Firing currently playing event [{usernamne}/{userId}]", args?.Username, args.UserId);
|
||||||
CurrentlyPlaying?.Invoke(sender, args);
|
CurrentlyPlayingSpotify?.Invoke(sender, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnCurrentlyPlayingChangeApple(object sender, AppleListeningChangeEventArgs args)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Firing currently playing event");
|
||||||
|
CurrentlyPlayingApple?.Invoke(sender, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
28
Selector.LastFm/Extensions/ServiceExtensions.cs
Normal file
28
Selector.LastFm/Extensions/ServiceExtensions.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using IF.Lastfm.Core.Api;
|
||||||
|
using IF.Lastfm.Core.Scrobblers;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Selector.Extensions;
|
||||||
|
|
||||||
|
public static class ServiceExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddLastFm(this IServiceCollection services, string client, string secret)
|
||||||
|
{
|
||||||
|
services.AddTransient(sp => new LastAuth(client, secret));
|
||||||
|
services.AddTransient(sp => new LastfmClient(sp.GetService<LastAuth>()));
|
||||||
|
|
||||||
|
services.AddTransient<ITrackApi>(sp => sp.GetService<LastfmClient>().Track);
|
||||||
|
services.AddTransient<IAlbumApi>(sp => sp.GetService<LastfmClient>().Album);
|
||||||
|
services.AddTransient<IArtistApi>(sp => sp.GetService<LastfmClient>().Artist);
|
||||||
|
|
||||||
|
services.AddTransient<IUserApi>(sp => sp.GetService<LastfmClient>().User);
|
||||||
|
|
||||||
|
services.AddTransient<IChartApi>(sp => sp.GetService<LastfmClient>().Chart);
|
||||||
|
services.AddTransient<ILibraryApi>(sp => sp.GetService<LastfmClient>().Library);
|
||||||
|
services.AddTransient<ITagApi>(sp => sp.GetService<LastfmClient>().Tag);
|
||||||
|
|
||||||
|
services.AddTransient<IScrobbler, MemoryScrobbler>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
10
Selector/Scrobble/Mapping/ScrobbleAlbumMapping.cs → Selector.LastFm/Mapping/ScrobbleAlbumMapping.cs
10
Selector/Scrobble/Mapping/ScrobbleAlbumMapping.cs → Selector.LastFm/Mapping/ScrobbleAlbumMapping.cs
@ -1,10 +1,7 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Mapping
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public class ScrobbleAlbumMapping : ScrobbleMapping
|
public class ScrobbleAlbumMapping : ScrobbleMapping
|
||||||
@ -12,7 +9,8 @@ namespace Selector
|
|||||||
public string AlbumName { get; set; }
|
public string AlbumName { get; set; }
|
||||||
public string ArtistName { get; set; }
|
public string ArtistName { get; set; }
|
||||||
|
|
||||||
public ScrobbleAlbumMapping(ISearchClient _searchClient, ILogger<ScrobbleAlbumMapping> _logger, string albumName, string artistName) : base(_searchClient, _logger)
|
public ScrobbleAlbumMapping(ISearchClient _searchClient, ILogger<ScrobbleAlbumMapping> _logger,
|
||||||
|
string albumName, string artistName) : base(_searchClient, _logger)
|
||||||
{
|
{
|
||||||
AlbumName = albumName;
|
AlbumName = albumName;
|
||||||
ArtistName = artistName;
|
ArtistName = artistName;
|
||||||
@ -38,4 +36,4 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,19 +1,15 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Mapping
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public class ScrobbleArtistMapping : ScrobbleMapping
|
public class ScrobbleArtistMapping : ScrobbleMapping
|
||||||
{
|
{
|
||||||
public string ArtistName { get; set; }
|
public string ArtistName { get; set; }
|
||||||
|
|
||||||
public ScrobbleArtistMapping(ISearchClient _searchClient, ILogger<ScrobbleArtistMapping> _logger, string artistName) : base(_searchClient, _logger)
|
public ScrobbleArtistMapping(ISearchClient _searchClient, ILogger<ScrobbleArtistMapping> _logger,
|
||||||
|
string artistName) : base(_searchClient, _logger)
|
||||||
{
|
{
|
||||||
ArtistName = artistName;
|
ArtistName = artistName;
|
||||||
}
|
}
|
||||||
@ -37,4 +33,4 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,14 +1,15 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using System.Diagnostics;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Selector.Operations;
|
using Selector.Operations;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Mapping
|
||||||
{
|
{
|
||||||
public enum LastfmObject{
|
public enum LastfmObject
|
||||||
Track, Album, Artist
|
{
|
||||||
|
Track,
|
||||||
|
Album,
|
||||||
|
Artist
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -45,7 +46,7 @@ namespace Selector
|
|||||||
logger.LogInformation("Mapping Last.fm {} ({}) to Spotify", Query, QueryType);
|
logger.LogInformation("Mapping Last.fm {} ({}) to Spotify", Query, QueryType);
|
||||||
|
|
||||||
var netTime = Stopwatch.StartNew();
|
var netTime = Stopwatch.StartNew();
|
||||||
currentTask = searchClient.Item(new (QueryType, Query));
|
currentTask = searchClient.Item(new(QueryType, Query));
|
||||||
currentTask.ContinueWith(async t =>
|
currentTask.ContinueWith(async t =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -76,7 +77,8 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.LogError(e, "Error while mapping Last.fm {} ({}) to Spotify on attempt {}", Query, QueryType, Attempts);
|
logger.LogError(e, "Error while mapping Last.fm {} ({}) to Spotify on attempt {}", Query, QueryType,
|
||||||
|
Attempts);
|
||||||
Succeeded = false;
|
Succeeded = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -93,4 +95,4 @@ namespace Selector
|
|||||||
Success?.Invoke(this, new EventArgs());
|
Success?.Invoke(this, new EventArgs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
16
Selector/Scrobble/Mapping/ScrobbleTrackMapping.cs → Selector.LastFm/Mapping/ScrobbleTrackMapping.cs
16
Selector/Scrobble/Mapping/ScrobbleTrackMapping.cs → Selector.LastFm/Mapping/ScrobbleTrackMapping.cs
@ -1,12 +1,7 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Mapping
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public class ScrobbleTrackMapping : ScrobbleMapping
|
public class ScrobbleTrackMapping : ScrobbleMapping
|
||||||
@ -14,7 +9,8 @@ namespace Selector
|
|||||||
public string TrackName { get; set; }
|
public string TrackName { get; set; }
|
||||||
public string ArtistName { get; set; }
|
public string ArtistName { get; set; }
|
||||||
|
|
||||||
public ScrobbleTrackMapping(ISearchClient _searchClient, ILogger<ScrobbleTrackMapping> _logger, string trackName, string artistName) : base(_searchClient, _logger)
|
public ScrobbleTrackMapping(ISearchClient _searchClient, ILogger<ScrobbleTrackMapping> _logger,
|
||||||
|
string trackName, string artistName) : base(_searchClient, _logger)
|
||||||
{
|
{
|
||||||
TrackName = trackName;
|
TrackName = trackName;
|
||||||
ArtistName = artistName;
|
ArtistName = artistName;
|
||||||
@ -32,12 +28,12 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
var topResult = response.Result.Tracks.Items.FirstOrDefault();
|
var topResult = response.Result.Tracks.Items.FirstOrDefault();
|
||||||
|
|
||||||
if(topResult is not null
|
if (topResult is not null
|
||||||
&& topResult.Name.Equals(TrackName, StringComparison.InvariantCultureIgnoreCase)
|
&& topResult.Name.Equals(TrackName, StringComparison.InvariantCultureIgnoreCase)
|
||||||
&& topResult.Artists.First().Name.Equals(ArtistName, StringComparison.InvariantCultureIgnoreCase))
|
&& topResult.Artists.First().Name.Equals(ArtistName, StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
result = topResult;
|
result = topResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,19 +1,21 @@
|
|||||||
using System;
|
using IF.Lastfm.Core.Objects;
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector
|
||||||
{
|
{
|
||||||
public class Scrobble: IListen
|
public class Scrobble : IListen
|
||||||
{
|
{
|
||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
public string TrackName { get; set; }
|
public string? TrackName { get; set; }
|
||||||
public string AlbumName { get; set; }
|
public string? AlbumName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Not populated by default from the service, where not the same as <see cref="ArtistName"/> these have been manually entered
|
/// Not populated by default from the service, where not the same as <see cref="ArtistName"/> these have been manually entered
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AlbumArtistName { get; set; }
|
public string? AlbumArtistName { get; set; }
|
||||||
public string ArtistName { get; set; }
|
|
||||||
|
public string? ArtistName { get; set; }
|
||||||
public static explicit operator Scrobble(IF.Lastfm.Core.Objects.LastTrack track) => new()
|
|
||||||
|
public static explicit operator Scrobble(LastTrack track) => new()
|
||||||
{
|
{
|
||||||
Timestamp = track.TimePlayed?.UtcDateTime ?? DateTime.MinValue,
|
Timestamp = track.TimePlayed?.UtcDateTime ?? DateTime.MinValue,
|
||||||
|
|
||||||
@ -24,4 +26,4 @@ namespace Selector
|
|||||||
|
|
||||||
public override string ToString() => $"({Timestamp}) {TrackName}, {AlbumName}, {ArtistName}";
|
public override string ToString() => $"({Timestamp}) {TrackName}, {AlbumName}, {ArtistName}";
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,4 @@
|
|||||||
using System;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector
|
||||||
{
|
{
|
||||||
@ -13,7 +10,8 @@ namespace Selector
|
|||||||
public static bool MatchTime(IListen nativeScrobble, IListen serviceScrobble)
|
public static bool MatchTime(IListen nativeScrobble, IListen serviceScrobble)
|
||||||
=> serviceScrobble.Timestamp.Equals(nativeScrobble.Timestamp);
|
=> serviceScrobble.Timestamp.Equals(nativeScrobble.Timestamp);
|
||||||
|
|
||||||
public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffs(IEnumerable<IListen> existing, IEnumerable<IListen> toApply, bool matchContents = true)
|
public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffs(IEnumerable<IListen> existing,
|
||||||
|
IEnumerable<IListen> toApply, bool matchContents = true)
|
||||||
{
|
{
|
||||||
existing = existing.OrderBy(s => s.Timestamp);
|
existing = existing.OrderBy(s => s.Timestamp);
|
||||||
toApply = toApply.OrderBy(s => s.Timestamp);
|
toApply = toApply.OrderBy(s => s.Timestamp);
|
||||||
@ -96,7 +94,8 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffsContains(IEnumerable<IListen> existing, IEnumerable<IListen> toApply)
|
public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffsContains(IEnumerable<IListen> existing,
|
||||||
|
IEnumerable<IListen> toApply)
|
||||||
{
|
{
|
||||||
var toAdd = toApply.Where(s => !existing.Contains(s, new ListenComp()));
|
var toAdd = toApply.Where(s => !existing.Contains(s, new ListenComp()));
|
||||||
var toRemove = existing.Where(s => !toApply.Contains(s, new ListenComp()));
|
var toRemove = existing.Where(s => !toApply.Contains(s, new ListenComp()));
|
||||||
@ -111,4 +110,4 @@ namespace Selector
|
|||||||
public int GetHashCode([DisallowNull] IListen obj) => obj.Timestamp.GetHashCode();
|
public int GetHashCode([DisallowNull] IListen obj) => obj.Timestamp.GetHashCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,13 +1,9 @@
|
|||||||
using IF.Lastfm.Core.Api;
|
using System.Diagnostics;
|
||||||
|
using IF.Lastfm.Core.Api;
|
||||||
using IF.Lastfm.Core.Api.Helpers;
|
using IF.Lastfm.Core.Api.Helpers;
|
||||||
using IF.Lastfm.Core.Objects;
|
using IF.Lastfm.Core.Objects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Selector.Operations;
|
using Selector.Operations;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector
|
||||||
{
|
{
|
||||||
@ -34,7 +30,8 @@ namespace Selector
|
|||||||
private TaskCompletionSource AggregateTaskSource { get; set; } = new();
|
private TaskCompletionSource AggregateTaskSource { get; set; } = new();
|
||||||
public Task Task => AggregateTaskSource.Task;
|
public Task Task => AggregateTaskSource.Task;
|
||||||
|
|
||||||
public ScrobbleRequest(IUserApi _userClient, ILogger<ScrobbleRequest> _logger, string _username, int _pageNumber, int _pageSize, DateTime? _from, DateTime? _to, int maxRetries = 5)
|
public ScrobbleRequest(IUserApi _userClient, ILogger<ScrobbleRequest> _logger, string _username,
|
||||||
|
int _pageNumber, int _pageSize, DateTime? _from, DateTime? _to, int maxRetries = 5)
|
||||||
{
|
{
|
||||||
userClient = _userClient;
|
userClient = _userClient;
|
||||||
logger = _logger;
|
logger = _logger;
|
||||||
@ -50,15 +47,24 @@ namespace Selector
|
|||||||
|
|
||||||
public Task Execute()
|
public Task Execute()
|
||||||
{
|
{
|
||||||
using var scope = logger.BeginScope(new Dictionary<string, object>() { { "username", username }, { "page_number", pageNumber }, { "page_size", pageSize }, { "from", from }, { "to", to } });
|
using var scope = logger.BeginScope(new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "username", username }, { "page_number", pageNumber }, { "page_size", pageSize }, { "from", from },
|
||||||
|
{ "to", to }
|
||||||
|
});
|
||||||
|
|
||||||
logger.LogInformation("Starting request");
|
logger.LogInformation("Starting request");
|
||||||
|
|
||||||
var netTime = Stopwatch.StartNew();
|
var netTime = Stopwatch.StartNew();
|
||||||
currentTask = userClient.GetRecentScrobbles(username, pagenumber: pageNumber, count: pageSize, from: from, to: to);
|
currentTask =
|
||||||
|
userClient.GetRecentScrobbles(username, pagenumber: pageNumber, count: pageSize, from: from, to: to);
|
||||||
currentTask.ContinueWith(async t =>
|
currentTask.ContinueWith(async t =>
|
||||||
{
|
{
|
||||||
using var scope = logger.BeginScope(new Dictionary<string, object>() { { "username", username }, { "page_number", pageNumber }, { "page_size", pageSize }, { "from", from }, { "to", to } });
|
using var scope = logger.BeginScope(new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "username", username }, { "page_number", pageNumber }, { "page_size", pageSize },
|
||||||
|
{ "from", from }, { "to", to }
|
||||||
|
});
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -81,12 +87,14 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
if (Attempts < MaxAttempts)
|
if (Attempts < MaxAttempts)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Request failed: {}, retrying ({} of {})", result.Status, Attempts + 1, MaxAttempts);
|
logger.LogDebug("Request failed: {}, retrying ({} of {})", result.Status, Attempts + 1,
|
||||||
|
MaxAttempts);
|
||||||
await Execute();
|
await Execute();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug("Request failed: {}, max retries exceeded {}, not retrying", result.Status, MaxAttempts);
|
logger.LogDebug("Request failed: {}, max retries exceeded {}, not retrying",
|
||||||
|
result.Status, MaxAttempts);
|
||||||
AggregateTaskSource.SetCanceled();
|
AggregateTaskSource.SetCanceled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,7 +105,7 @@ namespace Selector
|
|||||||
AggregateTaskSource.SetException(t.Exception);
|
AggregateTaskSource.SetException(t.Exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.LogError(e, "Error while making scrobble request on attempt {}", Attempts);
|
logger.LogError(e, "Error while making scrobble request on attempt {}", Attempts);
|
||||||
Succeeded = false;
|
Succeeded = false;
|
||||||
@ -113,4 +121,4 @@ namespace Selector
|
|||||||
Success?.Invoke(this, new EventArgs());
|
Success?.Invoke(this, new EventArgs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
20
Selector.LastFm/Selector.LastFm.csproj
Normal file
20
Selector.LastFm/Selector.LastFm.csproj
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/>
|
||||||
|
<PackageReference Include="SpotifyAPI.Web" Version="7.2.1"/>
|
||||||
|
<PackageReference Include="Inflatable.Lastfm" Version="1.2.0"/>
|
||||||
|
<PackageReference Include="System.Linq.Async" Version="6.0.1"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Selector\Selector.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -10,17 +10,16 @@ public partial class App : Application
|
|||||||
private readonly ILogger<App> logger;
|
private readonly ILogger<App> logger;
|
||||||
|
|
||||||
public App(NowHubClient nowClient, ILogger<App> logger)
|
public App(NowHubClient nowClient, ILogger<App> logger)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
MainPage = new MainPage();
|
|
||||||
this.nowClient = nowClient;
|
this.nowClient = nowClient;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Window CreateWindow(IActivationState activationState)
|
protected override Window CreateWindow(IActivationState activationState)
|
||||||
{
|
{
|
||||||
Window window = base.CreateWindow(activationState);
|
Window window = new Window(new MainPage());
|
||||||
|
|
||||||
window.Resumed += async (s, e) =>
|
window.Resumed += async (s, e) =>
|
||||||
{
|
{
|
||||||
@ -36,16 +35,13 @@ public partial class App : Application
|
|||||||
await nowClient.OnConnected();
|
await nowClient.OnConnected();
|
||||||
|
|
||||||
logger.LogInformation("Hubs reconnected");
|
logger.LogInformation("Hubs reconnected");
|
||||||
|
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error while reconnecting hubs");
|
logger.LogError(ex, "Error while reconnecting hubs");
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +64,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<CodesignKey>iPhone Developer</CodesignKey>
|
<CodesignKey>iPhone Developer</CodesignKey>
|
||||||
|
<MtouchDebug>true</MtouchDebug>
|
||||||
|
<IOSDebugOverWiFi>true</IOSDebugOverWiFi>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
<CodesignKey>iPhone Developer</CodesignKey>
|
<CodesignKey>iPhone Developer</CodesignKey>
|
||||||
@ -83,6 +85,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.14"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.14"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||||
<PackageReference Include="System.Net.Http.Json" Version="9.0.0" />
|
<PackageReference Include="System.Net.Http.Json" Version="9.0.0" />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@using SpotifyAPI.Web;
|
@using Selector.Spotify.Consumer
|
||||||
|
@using SpotifyAPI.Web
|
||||||
@if (Count is not null) {
|
@if (Count is not null) {
|
||||||
|
|
||||||
<div class="card info-card">
|
<div class="card info-card">
|
||||||
|
@ -65,6 +65,14 @@
|
|||||||
width: 21px;
|
width: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.apple-logo {
|
||||||
|
width: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-logo img {
|
||||||
|
width: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
.lastfm-logo {
|
.lastfm-logo {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace Selector.Model
|
namespace Selector.Model
|
||||||
{
|
{
|
||||||
|
|
||||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
||||||
{
|
{
|
||||||
private readonly ILogger<ApplicationDbContext> Logger;
|
private readonly ILogger<ApplicationDbContext> Logger;
|
||||||
@ -25,9 +20,10 @@ namespace Selector.Model
|
|||||||
public DbSet<ArtistLastfmSpotifyMapping> ArtistMapping { get; set; }
|
public DbSet<ArtistLastfmSpotifyMapping> ArtistMapping { get; set; }
|
||||||
|
|
||||||
public DbSet<SpotifyListen> SpotifyListen { get; set; }
|
public DbSet<SpotifyListen> SpotifyListen { get; set; }
|
||||||
|
public DbSet<AppleMusicListen> AppleMusicListen { get; set; }
|
||||||
|
|
||||||
public ApplicationDbContext(
|
public ApplicationDbContext(
|
||||||
DbContextOptions<ApplicationDbContext> options,
|
DbContextOptions<ApplicationDbContext> options,
|
||||||
ILogger<ApplicationDbContext> logger
|
ILogger<ApplicationDbContext> logger
|
||||||
) : base(options)
|
) : base(options)
|
||||||
{
|
{
|
||||||
@ -36,14 +32,14 @@ namespace Selector.Model
|
|||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.HasCollation("case_insensitive", locale: "en-u-ks-primary", provider: "icu", deterministic: false);
|
modelBuilder.HasCollation("case_insensitive", locale: "en-u-ks-primary", provider: "icu",
|
||||||
|
deterministic: false);
|
||||||
|
|
||||||
modelBuilder.Entity<ApplicationUser>()
|
modelBuilder.Entity<ApplicationUser>()
|
||||||
.Property(u => u.SpotifyIsLinked)
|
.Property(u => u.SpotifyIsLinked)
|
||||||
@ -107,20 +103,32 @@ namespace Selector.Model
|
|||||||
//modelBuilder.Entity<SpotifyListen>()
|
//modelBuilder.Entity<SpotifyListen>()
|
||||||
// .HasIndex(x => new { x.UserId, x.ArtistName, x.TrackName });
|
// .HasIndex(x => new { x.UserId, x.ArtistName, x.TrackName });
|
||||||
|
|
||||||
|
modelBuilder.Entity<AppleMusicListen>().HasKey(s => s.Id);
|
||||||
|
modelBuilder.Entity<AppleMusicListen>()
|
||||||
|
.Property(s => s.TrackName)
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
modelBuilder.Entity<AppleMusicListen>()
|
||||||
|
.Property(s => s.AlbumName)
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
modelBuilder.Entity<AppleMusicListen>()
|
||||||
|
.Property(s => s.ArtistName)
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
SeedData.Seed(modelBuilder);
|
SeedData.Seed(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreatePlayerWatcher(string userId)
|
public void CreatePlayerWatcher(string userId)
|
||||||
{
|
{
|
||||||
if(Watcher.Any(w => w.UserId == userId && w.Type == WatcherType.Player))
|
if (Watcher.Any(w => w.UserId == userId && w.Type == WatcherType.SpotifyPlayer))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("Trying to create more than one player watcher for user [{id}]", userId);
|
Logger.LogWarning("Trying to create more than one player watcher for user [{id}]", userId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Watcher.Add(new Watcher {
|
Watcher.Add(new Watcher
|
||||||
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
Type = WatcherType.Player
|
Type = WatcherType.SpotifyPlayer
|
||||||
});
|
});
|
||||||
|
|
||||||
SaveChanges();
|
SaveChanges();
|
||||||
@ -129,17 +137,18 @@ namespace Selector.Model
|
|||||||
|
|
||||||
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
|
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
|
||||||
{
|
{
|
||||||
private static string GetPath(string env) => $"{@Directory.GetCurrentDirectory()}/../Selector.Web/appsettings.{env}.json";
|
private static string GetPath(string env) =>
|
||||||
|
$"{@Directory.GetCurrentDirectory()}/../Selector.Web/appsettings.{env}.json";
|
||||||
|
|
||||||
public ApplicationDbContext CreateDbContext(string[] args)
|
public ApplicationDbContext CreateDbContext(string[] args)
|
||||||
{
|
{
|
||||||
string configFile;
|
string configFile;
|
||||||
|
|
||||||
if(File.Exists(GetPath("Development")))
|
if (File.Exists(GetPath("Development")))
|
||||||
{
|
{
|
||||||
configFile = GetPath("Development");
|
configFile = GetPath("Development");
|
||||||
}
|
}
|
||||||
else if(File.Exists(GetPath("Production")))
|
else if (File.Exists(GetPath("Production")))
|
||||||
{
|
{
|
||||||
configFile = GetPath("Production");
|
configFile = GetPath("Production");
|
||||||
}
|
}
|
||||||
@ -155,7 +164,7 @@ namespace Selector.Model
|
|||||||
|
|
||||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||||
builder.UseNpgsql(configuration.GetConnectionString("Default"));
|
builder.UseNpgsql(configuration.GetConnectionString("Default"));
|
||||||
|
|
||||||
return new ApplicationDbContext(builder.Options, NullLogger<ApplicationDbContext>.Instance);
|
return new ApplicationDbContext(builder.Options, NullLogger<ApplicationDbContext>.Instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,12 @@ namespace Selector.Model
|
|||||||
public string SpotifyAccessToken { get; set; }
|
public string SpotifyAccessToken { get; set; }
|
||||||
public string SpotifyRefreshToken { get; set; }
|
public string SpotifyRefreshToken { get; set; }
|
||||||
|
|
||||||
|
[PersonalData]
|
||||||
|
public bool AppleMusicLinked { get; set; }
|
||||||
|
public string AppleMusicKey { get; set; }
|
||||||
|
[PersonalData]
|
||||||
|
public DateTime AppleMusicLastRefresh { get; set; }
|
||||||
|
|
||||||
[PersonalData]
|
[PersonalData]
|
||||||
public string LastFmUsername { get; set; }
|
public string LastFmUsername { get; set; }
|
||||||
[PersonalData]
|
[PersonalData]
|
||||||
@ -39,6 +45,10 @@ namespace Selector.Model
|
|||||||
public string SpotifyAccessToken { get; set; }
|
public string SpotifyAccessToken { get; set; }
|
||||||
public string SpotifyRefreshToken { get; set; }
|
public string SpotifyRefreshToken { get; set; }
|
||||||
|
|
||||||
|
public bool AppleMusicLinked { get; set; }
|
||||||
|
public string AppleMusicKey { get; set; }
|
||||||
|
public DateTime AppleMusicLastRefresh { get; set; }
|
||||||
|
|
||||||
public string LastFmUsername { get; set; }
|
public string LastFmUsername { get; set; }
|
||||||
|
|
||||||
public static explicit operator ApplicationUserDTO(ApplicationUser user) => new() {
|
public static explicit operator ApplicationUserDTO(ApplicationUser user) => new() {
|
||||||
@ -54,6 +64,10 @@ namespace Selector.Model
|
|||||||
SpotifyAccessToken = user.SpotifyAccessToken,
|
SpotifyAccessToken = user.SpotifyAccessToken,
|
||||||
SpotifyRefreshToken = user.SpotifyRefreshToken,
|
SpotifyRefreshToken = user.SpotifyRefreshToken,
|
||||||
|
|
||||||
|
AppleMusicLinked = user.AppleMusicLinked,
|
||||||
|
AppleMusicKey = user.AppleMusicKey,
|
||||||
|
AppleMusicLastRefresh = user.AppleMusicLastRefresh,
|
||||||
|
|
||||||
LastFmUsername = user.LastFmUsername
|
LastFmUsername = user.LastFmUsername
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,20 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Selector.Cache;
|
using Selector.Cache;
|
||||||
using Selector.Model.Authorisation;
|
using Selector.Model.Authorisation;
|
||||||
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Watcher;
|
||||||
|
|
||||||
namespace Selector.Model.Extensions
|
namespace Selector.Model.Extensions
|
||||||
{
|
{
|
||||||
public static class ServiceExtensions
|
public static class ServiceExtensions
|
||||||
{
|
{
|
||||||
|
public static IServiceCollection AddSpotifyWatcher(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<ISpotifyWatcherFactory, SpotifyWatcherFactory>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
public static void AddAuthorisationHandlers(this IServiceCollection services)
|
public static void AddAuthorisationHandlers(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IAuthorizationHandler, WatcherIsOwnerAuthHandler>();
|
services.AddScoped<IAuthorizationHandler, WatcherIsOwnerAuthHandler>();
|
||||||
@ -23,4 +32,4 @@ namespace Selector.Model.Extensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
26
Selector.Model/Listen/AppleMusicListen.cs
Normal file
26
Selector.Model/Listen/AppleMusicListen.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Selector.AppleMusic.Watcher;
|
||||||
|
|
||||||
|
namespace Selector.Model;
|
||||||
|
|
||||||
|
public class AppleMusicListen : Listen, IUserListen
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string TrackId { get; set; }
|
||||||
|
public string Isrc { get; set; }
|
||||||
|
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public ApplicationUser User { get; set; }
|
||||||
|
|
||||||
|
public static explicit operator AppleMusicListen(AppleMusicCurrentlyPlayingContext track) => new()
|
||||||
|
{
|
||||||
|
Timestamp = track.FirstSeen,
|
||||||
|
|
||||||
|
TrackId = track.Track.Id,
|
||||||
|
Isrc = track.Track.Attributes.Isrc,
|
||||||
|
|
||||||
|
TrackName = track.Track.Attributes.Name,
|
||||||
|
AlbumName = track.Track.Attributes.AlbumName,
|
||||||
|
ArtistName = track.Track.Attributes.ArtistName,
|
||||||
|
};
|
||||||
|
}
|
500
Selector.Model/Migrations/20250329231051_adding_apple_music_user_properties.Designer.cs
generated
Normal file
500
Selector.Model/Migrations/20250329231051_adding_apple_music_user_properties.Designer.cs
generated
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Selector.Model;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Selector.Model.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20250329231051_adding_apple_music_user_properties")]
|
||||||
|
partial class adding_apple_music_user_properties
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "en-u-ks-primary,en-u-ks-primary,icu,False")
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = "00c64c0a-3387-4933-9575-83443fa9092b",
|
||||||
|
Name = "Admin",
|
||||||
|
NormalizedName = "ADMIN"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.AlbumLastfmSpotifyMapping", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SpotifyUri")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmAlbumName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.HasKey("SpotifyUri");
|
||||||
|
|
||||||
|
b.ToTable("AlbumMapping");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("AppleMusicKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AppleMusicLastRefresh")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("AppleMusicLinked")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("LastFmUsername")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("SaveScrobbles")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SpotifyAccessToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("SpotifyIsLinked")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SpotifyLastRefresh")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("SpotifyRefreshToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("SpotifyTokenExpiry")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.ArtistLastfmSpotifyMapping", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SpotifyUri")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.HasKey("SpotifyUri");
|
||||||
|
|
||||||
|
b.ToTable("ArtistMapping");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.SpotifyListen", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AlbumName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("ArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<int?>("PlayedDuration")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TrackName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("TrackUri")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("SpotifyListen");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.TrackLastfmSpotifyMapping", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SpotifyUri")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmTrackName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.HasKey("SpotifyUri");
|
||||||
|
|
||||||
|
b.ToTable("TrackMapping");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.UserScrobble", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AlbumArtistName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("AlbumName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("ArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TrackName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Scrobble");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.Watcher", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Watcher");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.SpotifyListen", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.UserScrobble", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", "User")
|
||||||
|
.WithMany("Scrobbles")
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.Watcher", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", "User")
|
||||||
|
.WithMany("Watchers")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Scrobbles");
|
||||||
|
|
||||||
|
b.Navigation("Watchers");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Selector.Model.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class adding_apple_music_user_properties : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "AppleMusicKey",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "AppleMusicLastRefresh",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AppleMusicLinked",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AppleMusicKey",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AppleMusicLastRefresh",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AppleMusicLinked",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
548
Selector.Model/Migrations/20250331203003_add_apple_listen.Designer.cs
generated
Normal file
548
Selector.Model/Migrations/20250331203003_add_apple_listen.Designer.cs
generated
Normal file
@ -0,0 +1,548 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Selector.Model;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Selector.Model.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20250331203003_add_apple_listen")]
|
||||||
|
partial class add_apple_listen
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "en-u-ks-primary,en-u-ks-primary,icu,False")
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = "00c64c0a-3387-4933-9575-83443fa9092b",
|
||||||
|
Name = "Admin",
|
||||||
|
NormalizedName = "ADMIN"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.AlbumLastfmSpotifyMapping", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SpotifyUri")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmAlbumName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.HasKey("SpotifyUri");
|
||||||
|
|
||||||
|
b.ToTable("AlbumMapping");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.AppleMusicListen", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AlbumName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("ArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("Isrc")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("TrackName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AppleMusicListen");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("AppleMusicKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AppleMusicLastRefresh")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("AppleMusicLinked")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("LastFmUsername")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("SaveScrobbles")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SpotifyAccessToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("SpotifyIsLinked")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SpotifyLastRefresh")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("SpotifyRefreshToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("SpotifyTokenExpiry")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.ArtistLastfmSpotifyMapping", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SpotifyUri")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.HasKey("SpotifyUri");
|
||||||
|
|
||||||
|
b.ToTable("ArtistMapping");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.SpotifyListen", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AlbumName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("ArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<int?>("PlayedDuration")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TrackName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("TrackUri")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("SpotifyListen");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.TrackLastfmSpotifyMapping", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SpotifyUri")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("LastfmTrackName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.HasKey("SpotifyUri");
|
||||||
|
|
||||||
|
b.ToTable("TrackMapping");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.UserScrobble", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AlbumArtistName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("AlbumName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("ArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TrackName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Scrobble");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.Watcher", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Watcher");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.AppleMusicListen", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.SpotifyListen", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.UserScrobble", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", "User")
|
||||||
|
.WithMany("Scrobbles")
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.Watcher", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", "User")
|
||||||
|
.WithMany("Watchers")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Scrobbles");
|
||||||
|
|
||||||
|
b.Navigation("Watchers");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
Selector.Model/Migrations/20250331203003_add_apple_listen.cs
Normal file
52
Selector.Model/Migrations/20250331203003_add_apple_listen.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Selector.Model.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class add_apple_listen : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AppleMusicListen",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
TrackId = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Isrc = table.Column<string>(type: "text", nullable: true),
|
||||||
|
UserId = table.Column<string>(type: "text", nullable: true),
|
||||||
|
TrackName = table.Column<string>(type: "text", nullable: true, collation: "case_insensitive"),
|
||||||
|
AlbumName = table.Column<string>(type: "text", nullable: true, collation: "case_insensitive"),
|
||||||
|
ArtistName = table.Column<string>(type: "text", nullable: true, collation: "case_insensitive"),
|
||||||
|
Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AppleMusicListen", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AppleMusicListen_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AppleMusicListen_UserId",
|
||||||
|
table: "AppleMusicListen",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AppleMusicListen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -181,6 +181,45 @@ namespace Selector.Model.Migrations
|
|||||||
b.ToTable("AlbumMapping");
|
b.ToTable("AlbumMapping");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.AppleMusicListen", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AlbumName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("ArtistName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("Isrc")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TrackId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("TrackName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.UseCollation("case_insensitive");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AppleMusicListen");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Selector.Model.ApplicationUser", b =>
|
modelBuilder.Entity("Selector.Model.ApplicationUser", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@ -189,6 +228,15 @@ namespace Selector.Model.Migrations
|
|||||||
b.Property<int>("AccessFailedCount")
|
b.Property<int>("AccessFailedCount")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("AppleMusicKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AppleMusicLastRefresh")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("AppleMusicLinked")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
b.Property<string>("ConcurrencyStamp")
|
||||||
.IsConcurrencyToken()
|
.IsConcurrencyToken()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@ -447,6 +495,15 @@ namespace Selector.Model.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Selector.Model.AppleMusicListen", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Selector.Model.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Selector.Model.SpotifyListen", b =>
|
modelBuilder.Entity("Selector.Model.SpotifyListen", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Selector.Model.ApplicationUser", "User")
|
b.HasOne("Selector.Model.ApplicationUser", "User")
|
||||||
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Selector.Model;
|
using Selector.Model;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
|
|
||||||
namespace Selector.Cache
|
namespace Selector.Cache
|
||||||
{
|
{
|
||||||
@ -27,7 +28,8 @@ namespace Selector.Cache
|
|||||||
|
|
||||||
var userScrobbleCount = ScrobbleRepository.Count(username: username);
|
var userScrobbleCount = ScrobbleRepository.Count(username: username);
|
||||||
|
|
||||||
var artistScrobbles = ScrobbleRepository.GetAll(username: username, artistName: artist, tracking: false, orderTime: true).ToArray();
|
var artistScrobbles = ScrobbleRepository
|
||||||
|
.GetAll(username: username, artistName: artist, tracking: false, orderTime: true).ToArray();
|
||||||
var albumScrobbles = artistScrobbles.Where(
|
var albumScrobbles = artistScrobbles.Where(
|
||||||
s => s.AlbumName.Equals(album, StringComparison.CurrentCultureIgnoreCase)).ToArray();
|
s => s.AlbumName.Equals(album, StringComparison.CurrentCultureIgnoreCase)).ToArray();
|
||||||
var trackScrobbles = artistScrobbles.Where(
|
var trackScrobbles = artistScrobbles.Where(
|
||||||
@ -67,4 +69,4 @@ namespace Selector.Cache
|
|||||||
return Task.FromResult(playCount);
|
return Task.FromResult(playCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
|
||||||
|
<ProjectReference Include="..\Selector.LastFm\Selector.LastFm.csproj"/>
|
||||||
|
<ProjectReference Include="..\Selector.Spotify\Selector.Spotify.csproj"/>
|
||||||
<ProjectReference Include="..\Selector\Selector.csproj" />
|
<ProjectReference Include="..\Selector\Selector.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@ -30,7 +33,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Migrations\" />
|
<Folder Include="Migrations\" />
|
||||||
<Folder Include="Listen\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Selector.SignalR;
|
namespace Selector.SignalR;
|
||||||
|
|
||||||
@ -20,5 +20,4 @@ public interface INowPlayingHub
|
|||||||
Task SendFacts(string track, string artist, string album, string albumArtist);
|
Task SendFacts(string track, string artist, string album, string albumArtist);
|
||||||
Task SendNewPlaying();
|
Task SendNewPlaying();
|
||||||
Task SendPlayCount(string track, string artist, string album, string albumArtist);
|
Task SendPlayCount(string track, string artist, string album, string albumArtist);
|
||||||
}
|
}
|
||||||
|
|
@ -1,41 +1,42 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging;
|
using Selector.Spotify;
|
||||||
|
using Selector.Spotify.Consumer;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector.SignalR;
|
namespace Selector.SignalR;
|
||||||
|
|
||||||
public class NowHubCache
|
public class NowHubCache
|
||||||
{
|
{
|
||||||
private readonly NowHubClient _connection;
|
private readonly NowHubClient _connection;
|
||||||
private readonly ILogger<NowHubCache> logger;
|
private readonly ILogger<NowHubCache> logger;
|
||||||
|
|
||||||
public TrackAudioFeatures LastFeature { get; private set; }
|
public TrackAudioFeatures LastFeature { get; private set; }
|
||||||
public List<Card> LastCards { get; private set; } = new();
|
public List<Card> LastCards { get; private set; } = new();
|
||||||
private readonly object updateLock = new();
|
private readonly object updateLock = new();
|
||||||
|
|
||||||
private readonly object bindingLock = new();
|
private readonly object bindingLock = new();
|
||||||
private bool isBound = false;
|
private bool isBound = false;
|
||||||
|
|
||||||
public PlayCount LastPlayCount { get; private set; }
|
public PlayCount LastPlayCount { get; private set; }
|
||||||
public CurrentlyPlayingDTO LastPlaying { get; private set; }
|
public CurrentlyPlayingDTO LastPlaying { get; private set; }
|
||||||
|
|
||||||
public event EventHandler NewAudioFeature;
|
public event EventHandler NewAudioFeature;
|
||||||
public event EventHandler NewCard;
|
public event EventHandler NewCard;
|
||||||
public event EventHandler NewPlayCount;
|
public event EventHandler NewPlayCount;
|
||||||
public event EventHandler NewNowPlaying;
|
public event EventHandler NewNowPlaying;
|
||||||
|
|
||||||
public NowHubCache(NowHubClient connection, ILogger<NowHubCache> logger)
|
public NowHubCache(NowHubClient connection, ILogger<NowHubCache> logger)
|
||||||
{
|
{
|
||||||
_connection = connection;
|
_connection = connection;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BindClient()
|
public void BindClient()
|
||||||
{
|
{
|
||||||
lock(bindingLock)
|
lock (bindingLock)
|
||||||
{
|
{
|
||||||
if(!isBound)
|
if (!isBound)
|
||||||
{
|
{
|
||||||
_connection.OnNewAudioFeature(af =>
|
_connection.OnNewAudioFeature(af =>
|
||||||
{
|
{
|
||||||
lock (updateLock)
|
lock (updateLock)
|
||||||
@ -108,7 +109,6 @@ public class NowHubCache
|
|||||||
|
|
||||||
isBound = true;
|
isBound = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
|||||||
using System;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
using System.Threading.Tasks;
|
using Selector.Spotify;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Selector.Spotify.Consumer;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector.SignalR;
|
namespace Selector.SignalR;
|
||||||
|
|
||||||
public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable
|
public class NowHubClient : BaseSignalRClient, INowPlayingHub, IDisposable
|
||||||
{
|
{
|
||||||
private List<IDisposable> NewPlayingCallbacks = new();
|
private List<IDisposable> NewPlayingCallbacks = new();
|
||||||
private List<IDisposable> NewAudioFeatureCallbacks = new();
|
private List<IDisposable> NewAudioFeatureCallbacks = new();
|
||||||
@ -13,9 +13,9 @@ public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable
|
|||||||
private List<IDisposable> NewCardCallbacks = new();
|
private List<IDisposable> NewCardCallbacks = new();
|
||||||
private bool disposedValue;
|
private bool disposedValue;
|
||||||
|
|
||||||
public NowHubClient(string token = null): base("nowhub", token)
|
public NowHubClient(string token = null) : base("nowhub", token)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnNewPlaying(Action<CurrentlyPlayingDTO> action)
|
public void OnNewPlaying(Action<CurrentlyPlayingDTO> action)
|
||||||
{
|
{
|
||||||
@ -93,10 +93,10 @@ public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable
|
|||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
foreach(var callback in NewPlayingCallbacks
|
foreach (var callback in NewPlayingCallbacks
|
||||||
.Concat(NewAudioFeatureCallbacks)
|
.Concat(NewAudioFeatureCallbacks)
|
||||||
.Concat(NewPlayCountCallbacks)
|
.Concat(NewPlayCountCallbacks)
|
||||||
.Concat(NewCardCallbacks))
|
.Concat(NewCardCallbacks))
|
||||||
{
|
{
|
||||||
callback.Dispose();
|
callback.Dispose();
|
||||||
}
|
}
|
||||||
@ -114,5 +114,4 @@ public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable
|
|||||||
Dispose(disposing: true);
|
Dispose(disposing: true);
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Selector.Spotify\Selector.Spotify.csproj"/>
|
||||||
<ProjectReference Include="..\Selector\Selector.csproj" />
|
<ProjectReference Include="..\Selector\Selector.csproj" />
|
||||||
<!-- <ProjectReference Include="..\Selector.Model\Selector.Model.csproj" /> -->
|
<!-- <ProjectReference Include="..\Selector.Model\Selector.Model.csproj" /> -->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
using System.Threading.Tasks;
|
using SpotifyAPI.Web;
|
||||||
using SpotifyAPI.Web;
|
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Spotify.ConfigFactory
|
||||||
{
|
{
|
||||||
public interface ISpotifyConfigFactory
|
public interface ISpotifyConfigFactory
|
||||||
{
|
{
|
||||||
public Task<SpotifyClientConfig> GetConfig();
|
public Task<SpotifyClientConfig> GetConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,6 @@
|
|||||||
using System.Threading.Tasks;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
using SpotifyAPI.Web;
|
namespace Selector.Spotify.ConfigFactory
|
||||||
|
|
||||||
|
|
||||||
namespace Selector
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get config from a refresh token
|
/// Get config from a refresh token
|
||||||
@ -14,7 +11,8 @@ namespace Selector
|
|||||||
private string ClientSecret { get; set; }
|
private string ClientSecret { get; set; }
|
||||||
private string RefreshToken { get; set; }
|
private string RefreshToken { get; set; }
|
||||||
|
|
||||||
public RefreshTokenFactory(string clientId, string clientSecret, string refreshToken) {
|
public RefreshTokenFactory(string clientId, string clientSecret, string refreshToken)
|
||||||
|
{
|
||||||
ClientId = clientId;
|
ClientId = clientId;
|
||||||
ClientSecret = clientSecret;
|
ClientSecret = clientSecret;
|
||||||
RefreshToken = refreshToken;
|
RefreshToken = refreshToken;
|
||||||
@ -27,7 +25,8 @@ namespace Selector
|
|||||||
|
|
||||||
var config = SpotifyClientConfig
|
var config = SpotifyClientConfig
|
||||||
.CreateDefault()
|
.CreateDefault()
|
||||||
.WithAuthenticator(new AuthorizationCodeAuthenticator(ClientId, ClientSecret, new(){
|
.WithAuthenticator(new AuthorizationCodeAuthenticator(ClientId, ClientSecret, new()
|
||||||
|
{
|
||||||
AccessToken = refreshed.AccessToken,
|
AccessToken = refreshed.AccessToken,
|
||||||
TokenType = refreshed.TokenType,
|
TokenType = refreshed.TokenType,
|
||||||
ExpiresIn = refreshed.ExpiresIn,
|
ExpiresIn = refreshed.ExpiresIn,
|
||||||
@ -39,4 +38,4 @@ namespace Selector
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,17 +1,14 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.Extensions;
|
||||||
|
using Selector.Spotify.Timeline;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Spotify.Consumer
|
||||||
{
|
{
|
||||||
public class AudioFeatureInjector : IPlayerConsumer
|
public class AudioFeatureInjector : ISpotifyPlayerConsumer
|
||||||
{
|
{
|
||||||
protected readonly IPlayerWatcher Watcher;
|
protected readonly ISpotifyPlayerWatcher Watcher;
|
||||||
protected readonly ITracksClient TrackClient;
|
protected readonly ITracksClient TrackClient;
|
||||||
protected readonly ILogger<AudioFeatureInjector> Logger;
|
protected readonly ILogger<AudioFeatureInjector> Logger;
|
||||||
|
|
||||||
@ -22,22 +19,24 @@ namespace Selector
|
|||||||
public AnalysedTrackTimeline Timeline { get; set; } = new();
|
public AnalysedTrackTimeline Timeline { get; set; } = new();
|
||||||
|
|
||||||
public AudioFeatureInjector(
|
public AudioFeatureInjector(
|
||||||
IPlayerWatcher watcher,
|
ISpotifyPlayerWatcher watcher,
|
||||||
ITracksClient trackClient,
|
ITracksClient trackClient,
|
||||||
ILogger<AudioFeatureInjector> logger = null,
|
ILogger<AudioFeatureInjector> logger = null,
|
||||||
CancellationToken token = default
|
CancellationToken token = default
|
||||||
){
|
)
|
||||||
|
{
|
||||||
Watcher = watcher;
|
Watcher = watcher;
|
||||||
TrackClient = trackClient;
|
TrackClient = trackClient;
|
||||||
Logger = logger ?? NullLogger<AudioFeatureInjector>.Instance;
|
Logger = logger ?? NullLogger<AudioFeatureInjector>.Instance;
|
||||||
CancelToken = token;
|
CancelToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Callback(object sender, ListeningChangeEventArgs e)
|
public void Callback(object sender, SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Current is null) return;
|
if (e.Current is null) return;
|
||||||
|
|
||||||
Task.Run(async () => {
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AsyncCallback(e);
|
await AsyncCallback(e);
|
||||||
@ -49,7 +48,7 @@ namespace Selector
|
|||||||
}, CancelToken);
|
}, CancelToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task AsyncCallback(ListeningChangeEventArgs e)
|
public virtual async Task AsyncCallback(SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
using var scope = Logger.GetListeningEventArgsScope(e);
|
using var scope = Logger.GetListeningEventArgsScope(e);
|
||||||
|
|
||||||
@ -57,10 +56,12 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(track.Id)) return;
|
if (string.IsNullOrWhiteSpace(track.Id)) return;
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
Logger.LogTrace("Making Spotify call");
|
Logger.LogTrace("Making Spotify call");
|
||||||
var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
|
var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
|
||||||
Logger.LogDebug("Adding audio features [{track}]: [{audio_features}]", track.DisplayString(), audioFeatures.DisplayString());
|
Logger.LogDebug("Adding audio features [{track}]: [{audio_features}]", track.DisplayString(),
|
||||||
|
audioFeatures.DisplayString());
|
||||||
|
|
||||||
var analysedTrack = AnalysedTrack.From(track, audioFeatures);
|
var analysedTrack = AnalysedTrack.From(track, audioFeatures);
|
||||||
|
|
||||||
@ -103,10 +104,10 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange += Callback;
|
watcherCast.ItemChange += Callback;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||||
@ -117,7 +118,7 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange -= Callback;
|
watcherCast.ItemChange -= Callback;
|
||||||
}
|
}
|
||||||
@ -129,11 +130,12 @@ namespace Selector
|
|||||||
|
|
||||||
protected virtual void OnNewFeature(AnalysedTrack args)
|
protected virtual void OnNewFeature(AnalysedTrack args)
|
||||||
{
|
{
|
||||||
NewFeature?.Invoke(this, args);
|
NewFeature?.Invoke(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AnalysedTrack {
|
public class AnalysedTrack
|
||||||
|
{
|
||||||
public FullTrack Track { get; set; }
|
public FullTrack Track { get; set; }
|
||||||
public TrackAudioFeatures Features { get; set; }
|
public TrackAudioFeatures Features { get; set; }
|
||||||
|
|
||||||
@ -146,4 +148,4 @@ namespace Selector
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,13 +1,8 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Generic;
|
using Selector.Extensions;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Spotify.Consumer
|
||||||
{
|
{
|
||||||
public class DummyAudioFeatureInjector : AudioFeatureInjector
|
public class DummyAudioFeatureInjector : AudioFeatureInjector
|
||||||
{
|
{
|
||||||
@ -30,18 +25,18 @@ namespace Selector
|
|||||||
Valence = 0.5f,
|
Valence = 0.5f,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private int _contextIdx = 0;
|
private int _contextIdx = 0;
|
||||||
|
|
||||||
private DateTime _lastNext = DateTime.UtcNow;
|
private DateTime _lastNext = DateTime.UtcNow;
|
||||||
private TimeSpan _contextLifespan = TimeSpan.FromSeconds(30);
|
private TimeSpan _contextLifespan = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
public DummyAudioFeatureInjector(
|
public DummyAudioFeatureInjector(
|
||||||
IPlayerWatcher watcher,
|
ISpotifyPlayerWatcher watcher,
|
||||||
ILogger<DummyAudioFeatureInjector> logger = null,
|
ILogger<DummyAudioFeatureInjector> logger = null,
|
||||||
CancellationToken token = default
|
CancellationToken token = default
|
||||||
): base (watcher, null, logger, token)
|
) : base(watcher, null, logger, token)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldCycle() => DateTime.UtcNow - _lastNext > _contextLifespan;
|
private bool ShouldCycle() => DateTime.UtcNow - _lastNext > _contextLifespan;
|
||||||
@ -65,7 +60,7 @@ namespace Selector
|
|||||||
return _features[_contextIdx];
|
return _features[_contextIdx];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task AsyncCallback(ListeningChangeEventArgs e)
|
public override Task AsyncCallback(SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
using var scope = Logger.GetListeningEventArgsScope(e);
|
using var scope = Logger.GetListeningEventArgsScope(e);
|
||||||
|
|
||||||
@ -98,4 +93,4 @@ namespace Selector
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,20 +1,17 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Generic;
|
using Selector.Spotify.ConfigFactory;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Spotify.Consumer.Factory
|
||||||
{
|
{
|
||||||
public interface IAudioFeatureInjectorFactory
|
public interface IAudioFeatureInjectorFactory
|
||||||
{
|
{
|
||||||
public Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null);
|
public Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
|
||||||
|
ISpotifyPlayerWatcher watcher = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AudioFeatureInjectorFactory: IAudioFeatureInjectorFactory {
|
|
||||||
|
|
||||||
|
public class AudioFeatureInjectorFactory : IAudioFeatureInjectorFactory
|
||||||
|
{
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
|
|
||||||
public AudioFeatureInjectorFactory(ILoggerFactory loggerFactory)
|
public AudioFeatureInjectorFactory(ILoggerFactory loggerFactory)
|
||||||
@ -22,7 +19,8 @@ namespace Selector
|
|||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
|
public async Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
|
||||||
|
ISpotifyPlayerWatcher watcher = null)
|
||||||
{
|
{
|
||||||
if (!Magic.Dummy)
|
if (!Magic.Dummy)
|
||||||
{
|
{
|
||||||
@ -44,4 +42,4 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,41 +1,39 @@
|
|||||||
using System;
|
using IF.Lastfm.Core.Api;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
using IF.Lastfm.Core.Api;
|
namespace Selector.Spotify.Consumer.Factory
|
||||||
|
|
||||||
namespace Selector
|
|
||||||
{
|
{
|
||||||
public interface IPlayCounterFactory
|
public interface IPlayCounterFactory
|
||||||
{
|
{
|
||||||
public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null);
|
public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
|
||||||
|
ISpotifyPlayerWatcher watcher = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PlayCounterFactory: IPlayCounterFactory {
|
|
||||||
|
|
||||||
|
public class PlayCounterFactory : IPlayCounterFactory
|
||||||
|
{
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
private readonly LastfmClient Client;
|
private readonly LastfmClient Client;
|
||||||
private readonly LastFmCredentials Creds;
|
private readonly LastFmCredentials Creds;
|
||||||
|
|
||||||
public PlayCounterFactory(ILoggerFactory loggerFactory, LastfmClient client = null, LastFmCredentials creds = null)
|
public PlayCounterFactory(ILoggerFactory loggerFactory, LastfmClient client = null,
|
||||||
|
LastFmCredentials creds = null)
|
||||||
{
|
{
|
||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
Client = client;
|
Client = client;
|
||||||
Creds = creds;
|
Creds = creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
|
public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
|
||||||
|
ISpotifyPlayerWatcher watcher = null)
|
||||||
{
|
{
|
||||||
var client = fmClient ?? Client;
|
var client = fmClient ?? Client;
|
||||||
|
|
||||||
if(client is null)
|
if (client is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException("No Last.fm client provided");
|
throw new ArgumentNullException("No Last.fm client provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult<IPlayerConsumer>(new PlayCounter(
|
return Task.FromResult<ISpotifyPlayerConsumer>(new PlayCounter(
|
||||||
watcher,
|
watcher,
|
||||||
client.Track,
|
client.Track,
|
||||||
client.Album,
|
client.Album,
|
||||||
@ -46,4 +44,4 @@ namespace Selector
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
20
Selector/Consumers/Factory/WebHookFactory.cs → Selector.Spotify/Consumer/Factory/WebHookFactory.cs
20
Selector/Consumers/Factory/WebHookFactory.cs → Selector.Spotify/Consumer/Factory/WebHookFactory.cs
@ -1,19 +1,13 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using System.Net.Http;
|
namespace Selector.Spotify.Consumer.Factory
|
||||||
|
|
||||||
namespace Selector
|
|
||||||
{
|
{
|
||||||
public interface IWebHookFactory
|
public interface IWebHookFactory
|
||||||
{
|
{
|
||||||
public Task<WebHook> Get(WebHookConfig config, IPlayerWatcher watcher = null);
|
public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher watcher = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WebHookFactory: IWebHookFactory
|
public class WebHookFactory : IWebHookFactory
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
private readonly HttpClient Http;
|
private readonly HttpClient Http;
|
||||||
@ -24,7 +18,7 @@ namespace Selector
|
|||||||
Http = httpClient;
|
Http = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<WebHook> Get(WebHookConfig config, IPlayerWatcher watcher = null)
|
public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher watcher = null)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new WebHook(
|
return Task.FromResult(new WebHook(
|
||||||
watcher,
|
watcher,
|
||||||
@ -34,4 +28,4 @@ namespace Selector
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
9
Selector.Spotify/Consumer/IConsumer.cs
Normal file
9
Selector.Spotify/Consumer/IConsumer.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Selector.Spotify.Consumer;
|
||||||
|
|
||||||
|
public interface ISpotifyPlayerConsumer : IConsumer<SpotifyListeningChangeEventArgs>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPlaylistConsumer : IConsumer<PlaylistChangeEventArgs>
|
||||||
|
{
|
||||||
|
}
|
@ -1,21 +1,15 @@
|
|||||||
using System;
|
using IF.Lastfm.Core.Api;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.Extensions;
|
||||||
|
using Selector.Spotify.Timeline;
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using IF.Lastfm.Core.Api;
|
|
||||||
using IF.Lastfm.Core.Objects;
|
|
||||||
using IF.Lastfm.Core.Api.Helpers;
|
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Spotify.Consumer
|
||||||
{
|
{
|
||||||
public class PlayCounter : IPlayerConsumer
|
public class PlayCounter : ISpotifyPlayerConsumer
|
||||||
{
|
{
|
||||||
protected readonly IPlayerWatcher Watcher;
|
protected readonly ISpotifyPlayerWatcher Watcher;
|
||||||
protected readonly ITrackApi TrackClient;
|
protected readonly ITrackApi TrackClient;
|
||||||
protected readonly IAlbumApi AlbumClient;
|
protected readonly IAlbumApi AlbumClient;
|
||||||
protected readonly IArtistApi ArtistClient;
|
protected readonly IArtistApi ArtistClient;
|
||||||
@ -30,7 +24,7 @@ namespace Selector
|
|||||||
public AnalysedTrackTimeline Timeline { get; set; } = new();
|
public AnalysedTrackTimeline Timeline { get; set; } = new();
|
||||||
|
|
||||||
public PlayCounter(
|
public PlayCounter(
|
||||||
IPlayerWatcher watcher,
|
ISpotifyPlayerWatcher watcher,
|
||||||
ITrackApi trackClient,
|
ITrackApi trackClient,
|
||||||
IAlbumApi albumClient,
|
IAlbumApi albumClient,
|
||||||
IArtistApi artistClient,
|
IArtistApi artistClient,
|
||||||
@ -50,11 +44,12 @@ namespace Selector
|
|||||||
CancelToken = token;
|
CancelToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Callback(object sender, ListeningChangeEventArgs e)
|
public void Callback(object sender, SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Current is null) return;
|
if (e.Current is null) return;
|
||||||
|
|
||||||
Task.Run(async () => {
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AsyncCallback(e);
|
await AsyncCallback(e);
|
||||||
@ -66,9 +61,10 @@ namespace Selector
|
|||||||
}, CancelToken);
|
}, CancelToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AsyncCallback(ListeningChangeEventArgs e)
|
public async Task AsyncCallback(SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "username", Credentials.Username } });
|
using var scope = Logger.BeginScope(new Dictionary<string, object>()
|
||||||
|
{ { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "username", Credentials.Username } });
|
||||||
|
|
||||||
if (Credentials is null || string.IsNullOrWhiteSpace(Credentials.Username))
|
if (Credentials is null || string.IsNullOrWhiteSpace(Credentials.Username))
|
||||||
{
|
{
|
||||||
@ -78,12 +74,15 @@ namespace Selector
|
|||||||
|
|
||||||
if (e.Current.Item is FullTrack track)
|
if (e.Current.Item is FullTrack track)
|
||||||
{
|
{
|
||||||
using var trackScope = Logger.BeginScope(new Dictionary<string, object>() { { "track", track.DisplayString() } });
|
using var trackScope = Logger.BeginScope(new Dictionary<string, object>()
|
||||||
|
{ { "track", track.DisplayString() } });
|
||||||
|
|
||||||
Logger.LogTrace("Making Last.fm call");
|
Logger.LogTrace("Making Last.fm call");
|
||||||
|
|
||||||
var trackInfo = TrackClient.GetInfoAsync(track.Name, track.Artists[0].Name, username: Credentials?.Username);
|
var trackInfo =
|
||||||
var albumInfo = AlbumClient.GetInfoAsync(track.Album.Artists[0].Name, track.Album.Name, username: Credentials?.Username);
|
TrackClient.GetInfoAsync(track.Name, track.Artists[0].Name, username: Credentials?.Username);
|
||||||
|
var albumInfo = AlbumClient.GetInfoAsync(track.Album.Artists[0].Name, track.Album.Name,
|
||||||
|
username: Credentials?.Username);
|
||||||
var artistInfo = ArtistClient.GetInfoAsync(track.Artists[0].Name);
|
var artistInfo = ArtistClient.GetInfoAsync(track.Artists[0].Name);
|
||||||
var userInfo = UserClient.GetInfoAsync(Credentials.Username);
|
var userInfo = UserClient.GetInfoAsync(Credentials.Username);
|
||||||
|
|
||||||
@ -104,7 +103,8 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogError(trackInfo.Exception, "Track info task faulted, [{context}]", e.Current.DisplayString());
|
Logger.LogError(trackInfo.Exception, "Track info task faulted, [{context}]",
|
||||||
|
e.Current.DisplayString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (albumInfo.IsCompletedSuccessfully)
|
if (albumInfo.IsCompletedSuccessfully)
|
||||||
@ -120,7 +120,8 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogError(albumInfo.Exception, "Album info task faulted, [{context}]", e.Current.DisplayString());
|
Logger.LogError(albumInfo.Exception, "Album info task faulted, [{context}]",
|
||||||
|
e.Current.DisplayString());
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Add artist count
|
//TODO: Add artist count
|
||||||
@ -138,10 +139,13 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogError(userInfo.Exception, "User info task faulted, [{context}]", e.Current.DisplayString());
|
Logger.LogError(userInfo.Exception, "User info task faulted, [{context}]",
|
||||||
|
e.Current.DisplayString());
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug("Adding Last.fm data [{username}], track: {track_count}, album: {album_count}, artist: {artist_count}, user: {user_count}", Credentials.Username, trackCount, albumCount, artistCount, userCount);
|
Logger.LogDebug(
|
||||||
|
"Adding Last.fm data [{username}], track: {track_count}, album: {album_count}, artist: {artist_count}, user: {user_count}",
|
||||||
|
Credentials.Username, trackCount, albumCount, artistCount, userCount);
|
||||||
|
|
||||||
PlayCount playCount = new()
|
PlayCount playCount = new()
|
||||||
{
|
{
|
||||||
@ -149,7 +153,7 @@ namespace Selector
|
|||||||
Album = albumCount,
|
Album = albumCount,
|
||||||
Artist = artistCount,
|
Artist = artistCount,
|
||||||
User = userCount,
|
User = userCount,
|
||||||
ListeningEvent = e
|
SpotifyListeningEvent = e
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(Credentials.Username))
|
if (!string.IsNullOrWhiteSpace(Credentials.Username))
|
||||||
@ -175,7 +179,7 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange += Callback;
|
watcherCast.ItemChange += Callback;
|
||||||
}
|
}
|
||||||
@ -189,7 +193,7 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange -= Callback;
|
watcherCast.ItemChange -= Callback;
|
||||||
}
|
}
|
||||||
@ -215,11 +219,11 @@ namespace Selector
|
|||||||
public IEnumerable<CountSample> TrackCountData { get; set; }
|
public IEnumerable<CountSample> TrackCountData { get; set; }
|
||||||
public IEnumerable<CountSample> AlbumCountData { get; set; }
|
public IEnumerable<CountSample> AlbumCountData { get; set; }
|
||||||
public IEnumerable<CountSample> ArtistCountData { get; set; }
|
public IEnumerable<CountSample> ArtistCountData { get; set; }
|
||||||
public ListeningChangeEventArgs ListeningEvent { get; set; }
|
public SpotifyListeningChangeEventArgs SpotifyListeningEvent { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LastFmCredentials
|
public class LastFmCredentials
|
||||||
{
|
{
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,25 +1,19 @@
|
|||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Selector.Spotify.Timeline;
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Spotify.Consumer
|
||||||
{
|
{
|
||||||
public class WebHookConfig
|
public class WebHookConfig
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public IEnumerable<Predicate<ListeningChangeEventArgs>> Predicates { get; set; }
|
public IEnumerable<Predicate<SpotifyListeningChangeEventArgs>> Predicates { get; set; }
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
public HttpContent Content { get; set; }
|
public HttpContent Content { get; set; }
|
||||||
|
|
||||||
public bool ShouldRequest(ListeningChangeEventArgs e)
|
public bool ShouldRequest(SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if(Predicates is not null)
|
if (Predicates is not null)
|
||||||
{
|
{
|
||||||
return Predicates.Select(p => p(e)).Aggregate((a, b) => a && b);
|
return Predicates.Select(p => p(e)).Aggregate((a, b) => a && b);
|
||||||
}
|
}
|
||||||
@ -30,9 +24,9 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WebHook : IPlayerConsumer
|
public class WebHook : ISpotifyPlayerConsumer
|
||||||
{
|
{
|
||||||
protected readonly IPlayerWatcher Watcher;
|
protected readonly ISpotifyPlayerWatcher Watcher;
|
||||||
protected readonly HttpClient HttpClient;
|
protected readonly HttpClient HttpClient;
|
||||||
protected readonly ILogger<WebHook> Logger;
|
protected readonly ILogger<WebHook> Logger;
|
||||||
|
|
||||||
@ -47,7 +41,7 @@ namespace Selector
|
|||||||
public AnalysedTrackTimeline Timeline { get; set; } = new();
|
public AnalysedTrackTimeline Timeline { get; set; } = new();
|
||||||
|
|
||||||
public WebHook(
|
public WebHook(
|
||||||
IPlayerWatcher watcher,
|
ISpotifyPlayerWatcher watcher,
|
||||||
HttpClient httpClient,
|
HttpClient httpClient,
|
||||||
WebHookConfig config,
|
WebHookConfig config,
|
||||||
ILogger<WebHook> logger = null,
|
ILogger<WebHook> logger = null,
|
||||||
@ -61,11 +55,12 @@ namespace Selector
|
|||||||
CancelToken = token;
|
CancelToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Callback(object sender, ListeningChangeEventArgs e)
|
public void Callback(object sender, SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Current is null) return;
|
if (e.Current is null) return;
|
||||||
|
|
||||||
Task.Run(async () => {
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AsyncCallback(e);
|
await AsyncCallback(e);
|
||||||
@ -77,9 +72,13 @@ namespace Selector
|
|||||||
}, CancelToken);
|
}, CancelToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AsyncCallback(ListeningChangeEventArgs e)
|
public async Task AsyncCallback(SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "name", Config.Name }, { "url", Config.Url } });
|
using var scope = Logger.BeginScope(new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "name", Config.Name },
|
||||||
|
{ "url", Config.Url }
|
||||||
|
});
|
||||||
|
|
||||||
if (Config.ShouldRequest(e))
|
if (Config.ShouldRequest(e))
|
||||||
{
|
{
|
||||||
@ -101,7 +100,7 @@ namespace Selector
|
|||||||
OnFailedRequest(new EventArgs());
|
OnFailedRequest(new EventArgs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Exception occured during request");
|
Logger.LogError(ex, "Exception occured during request");
|
||||||
}
|
}
|
||||||
@ -120,7 +119,7 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange += Callback;
|
watcherCast.ItemChange += Callback;
|
||||||
}
|
}
|
||||||
@ -134,7 +133,7 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||||
|
|
||||||
if (watcher is IPlayerWatcher watcherCast)
|
if (watcher is ISpotifyPlayerWatcher watcherCast)
|
||||||
{
|
{
|
||||||
watcherCast.ItemChange -= Callback;
|
watcherCast.ItemChange -= Callback;
|
||||||
}
|
}
|
||||||
@ -159,4 +158,4 @@ namespace Selector
|
|||||||
FailedRequest?.Invoke(this, args);
|
FailedRequest?.Invoke(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
17
Selector.Spotify/Credentials.cs
Normal file
17
Selector.Spotify/Credentials.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace Selector.Spotify;
|
||||||
|
|
||||||
|
public class SpotifyAppCredentials
|
||||||
|
{
|
||||||
|
public string ClientId { get; set; }
|
||||||
|
public string ClientSecret { get; set; }
|
||||||
|
|
||||||
|
public SpotifyAppCredentials()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpotifyAppCredentials(string clientId, string clientSecret)
|
||||||
|
{
|
||||||
|
ClientId = clientId;
|
||||||
|
ClientSecret = clientSecret;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
using System;
|
using Selector.Extensions;
|
||||||
|
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector {
|
namespace Selector.Spotify
|
||||||
|
{
|
||||||
public class CurrentlyPlayingDTO {
|
public class CurrentlyPlayingDTO
|
||||||
|
{
|
||||||
public CurrentlyPlayingContextDTO Context { get; set; }
|
public CurrentlyPlayingContextDTO Context { get; set; }
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
public string UserId { get; set; }
|
public string UserId { get; set; }
|
||||||
@ -12,9 +12,9 @@ namespace Selector {
|
|||||||
public FullTrack Track { get; set; }
|
public FullTrack Track { get; set; }
|
||||||
public FullEpisode Episode { get; set; }
|
public FullEpisode Episode { get; set; }
|
||||||
|
|
||||||
public static explicit operator CurrentlyPlayingDTO(ListeningChangeEventArgs e)
|
public static explicit operator CurrentlyPlayingDTO(SpotifyListeningChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if(e.Current.Item is FullTrack track)
|
if (e.Current.Item is FullTrack track)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
@ -52,13 +52,14 @@ namespace Selector {
|
|||||||
public long Timestamp { get; set; }
|
public long Timestamp { get; set; }
|
||||||
public int ProgressMs { get; set; }
|
public int ProgressMs { get; set; }
|
||||||
public bool IsPlaying { get; set; }
|
public bool IsPlaying { get; set; }
|
||||||
|
|
||||||
public string CurrentlyPlayingType { get; set; }
|
public string CurrentlyPlayingType { get; set; }
|
||||||
public Actions Actions { get; set; }
|
public Actions Actions { get; set; }
|
||||||
|
|
||||||
public static implicit operator CurrentlyPlayingContextDTO(CurrentlyPlayingContext context)
|
public static implicit operator CurrentlyPlayingContextDTO(CurrentlyPlayingContext context)
|
||||||
{
|
{
|
||||||
return new CurrentlyPlayingContextDTO {
|
return new CurrentlyPlayingContextDTO
|
||||||
|
{
|
||||||
Device = context.Device,
|
Device = context.Device,
|
||||||
RepeatState = context.RepeatState,
|
RepeatState = context.RepeatState,
|
||||||
ShuffleState = context.ShuffleState,
|
ShuffleState = context.ShuffleState,
|
@ -1,11 +1,10 @@
|
|||||||
using System;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Collections.Generic;
|
using Selector.Extensions;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector.Equality
|
namespace Selector.Spotify.Equality
|
||||||
{
|
{
|
||||||
public class PlayableItemEqualityComparer: IEqualityComparer<PlaylistTrack<IPlayableItem>>
|
public class PlayableItemEqualityComparer : IEqualityComparer<PlaylistTrack<IPlayableItem>>
|
||||||
{
|
{
|
||||||
public bool Equals(PlaylistTrack<IPlayableItem> x, PlaylistTrack<IPlayableItem> y)
|
public bool Equals(PlaylistTrack<IPlayableItem> x, PlaylistTrack<IPlayableItem> y)
|
||||||
{
|
{
|
||||||
@ -17,5 +16,4 @@ namespace Selector.Equality
|
|||||||
return obj.GetUri().GetHashCode();
|
return obj.GetUri().GetHashCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,8 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
namespace Selector
|
namespace Selector.Spotify.Equality
|
||||||
{
|
{
|
||||||
|
public abstract class NoHashCode<T> : EqualityComparer<T>
|
||||||
public abstract class NoHashCode<T>: EqualityComparer<T>
|
|
||||||
{
|
{
|
||||||
public override int GetHashCode(T obj)
|
public override int GetHashCode(T obj)
|
||||||
{
|
{
|
||||||
@ -16,79 +12,89 @@ namespace Selector
|
|||||||
|
|
||||||
public class FullTrackStringComparer : NoHashCode<FullTrack>
|
public class FullTrackStringComparer : NoHashCode<FullTrack>
|
||||||
{
|
{
|
||||||
public override bool Equals(FullTrack track1, FullTrack track2) => FullTrackStringComparer.IsEqual(track1, track2);
|
public override bool Equals(FullTrack track1, FullTrack track2) => IsEqual(track1, track2);
|
||||||
|
|
||||||
public static bool IsEqual(FullTrack track1, FullTrack track2) => track1.Name == track2.Name
|
public static bool IsEqual(FullTrack track1, FullTrack track2) => track1.Name == track2.Name
|
||||||
&& Enumerable.SequenceEqual(track1.Artists.Select(a => a.Name), track2.Artists.Select(a => a.Name))
|
&& Enumerable.SequenceEqual(
|
||||||
&& SimpleAlbumStringComparer.IsEqual(track1.Album, track2.Album);
|
track1.Artists.Select(a => a.Name),
|
||||||
|
track2.Artists.Select(a => a.Name))
|
||||||
|
&& SimpleAlbumStringComparer.IsEqual(
|
||||||
|
track1.Album, track2.Album);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FullEpisodeStringComparer : NoHashCode<FullEpisode>
|
public class FullEpisodeStringComparer : NoHashCode<FullEpisode>
|
||||||
{
|
{
|
||||||
public override bool Equals(FullEpisode ep1, FullEpisode ep2) => FullEpisodeStringComparer.IsEqual(ep1, ep2);
|
public override bool Equals(FullEpisode ep1, FullEpisode ep2) => IsEqual(ep1, ep2);
|
||||||
|
|
||||||
public static bool IsEqual(FullEpisode ep1, FullEpisode ep2) => ep1.Name == ep2.Name
|
public static bool IsEqual(FullEpisode ep1, FullEpisode ep2) => ep1.Name == ep2.Name
|
||||||
&& SimpleShowStringComparer.IsEqual(ep1.Show, ep2.Show);
|
&& SimpleShowStringComparer.IsEqual(ep1.Show,
|
||||||
|
ep2.Show);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FullAlbumStringComparer : NoHashCode<FullAlbum>
|
public class FullAlbumStringComparer : NoHashCode<FullAlbum>
|
||||||
{
|
{
|
||||||
public override bool Equals(FullAlbum album1, FullAlbum album2) => FullAlbumStringComparer.IsEqual(album1, album2);
|
public override bool Equals(FullAlbum album1, FullAlbum album2) => IsEqual(album1, album2);
|
||||||
|
|
||||||
public static bool IsEqual(FullAlbum album1, FullAlbum album2) => album1.Name == album2.Name
|
public static bool IsEqual(FullAlbum album1, FullAlbum album2) => album1.Name == album2.Name
|
||||||
&& Enumerable.SequenceEqual(album1.Artists.Select(a => a.Name), album2.Artists.Select(a => a.Name));
|
&& Enumerable.SequenceEqual(
|
||||||
|
album1.Artists.Select(a => a.Name),
|
||||||
|
album2.Artists.Select(a => a.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FullShowStringComparer : NoHashCode<FullShow>
|
public class FullShowStringComparer : NoHashCode<FullShow>
|
||||||
{
|
{
|
||||||
public override bool Equals(FullShow show1, FullShow show2) => FullShowStringComparer.IsEqual(show1, show2);
|
public override bool Equals(FullShow show1, FullShow show2) => IsEqual(show1, show2);
|
||||||
|
|
||||||
public static bool IsEqual(FullShow show1, FullShow show2) => show1.Name == show2.Name
|
public static bool IsEqual(FullShow show1, FullShow show2) => show1.Name == show2.Name
|
||||||
&& show1.Publisher == show2.Publisher;
|
&& show1.Publisher == show2.Publisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FullArtistStringComparer : NoHashCode<FullArtist>
|
public class FullArtistStringComparer : NoHashCode<FullArtist>
|
||||||
{
|
{
|
||||||
public override bool Equals(FullArtist artist1, FullArtist artist2) => FullArtistStringComparer.IsEqual(artist1, artist2);
|
public override bool Equals(FullArtist artist1, FullArtist artist2) => IsEqual(artist1, artist2);
|
||||||
|
|
||||||
public static bool IsEqual(FullArtist artist1, FullArtist artist2) => artist1.Name == artist2.Name;
|
public static bool IsEqual(FullArtist artist1, FullArtist artist2) => artist1.Name == artist2.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SimpleTrackStringComparer : NoHashCode<SimpleTrack>
|
public class SimpleTrackStringComparer : NoHashCode<SimpleTrack>
|
||||||
{
|
{
|
||||||
public override bool Equals(SimpleTrack track1, SimpleTrack track2) => SimpleTrackStringComparer.IsEqual(track1, track2);
|
public override bool Equals(SimpleTrack track1, SimpleTrack track2) => IsEqual(track1, track2);
|
||||||
|
|
||||||
public static bool IsEqual(SimpleTrack track1, SimpleTrack track2) => track1.Name == track2.Name
|
public static bool IsEqual(SimpleTrack track1, SimpleTrack track2) => track1.Name == track2.Name
|
||||||
&& Enumerable.SequenceEqual(track1.Artists.Select(a => a.Name), track2.Artists.Select(a => a.Name));
|
&& Enumerable.SequenceEqual(
|
||||||
|
track1.Artists.Select(a => a.Name),
|
||||||
|
track2.Artists.Select(a => a.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SimpleEpisodeStringComparer : NoHashCode<SimpleEpisode>
|
public class SimpleEpisodeStringComparer : NoHashCode<SimpleEpisode>
|
||||||
{
|
{
|
||||||
public override bool Equals(SimpleEpisode ep1, SimpleEpisode ep2) => SimpleEpisodeStringComparer.IsEqual(ep1, ep2);
|
public override bool Equals(SimpleEpisode ep1, SimpleEpisode ep2) => IsEqual(ep1, ep2);
|
||||||
|
|
||||||
public static bool IsEqual(SimpleEpisode ep1, SimpleEpisode ep2) => ep1.Name == ep2.Name;
|
public static bool IsEqual(SimpleEpisode ep1, SimpleEpisode ep2) => ep1.Name == ep2.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SimpleAlbumStringComparer : NoHashCode<SimpleAlbum>
|
public class SimpleAlbumStringComparer : NoHashCode<SimpleAlbum>
|
||||||
{
|
{
|
||||||
public override bool Equals(SimpleAlbum album1, SimpleAlbum album2) => SimpleAlbumStringComparer.IsEqual(album1, album2);
|
public override bool Equals(SimpleAlbum album1, SimpleAlbum album2) => IsEqual(album1, album2);
|
||||||
|
|
||||||
public static bool IsEqual(SimpleAlbum album1, SimpleAlbum album2) => album1.Name == album2.Name
|
public static bool IsEqual(SimpleAlbum album1, SimpleAlbum album2) => album1.Name == album2.Name
|
||||||
&& Enumerable.SequenceEqual(album1.Artists.Select(a => a.Name), album2.Artists.Select(a => a.Name));
|
&& Enumerable.SequenceEqual(
|
||||||
|
album1.Artists.Select(a => a.Name),
|
||||||
|
album2.Artists.Select(a => a.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SimpleShowStringComparer : NoHashCode<SimpleShow>
|
public class SimpleShowStringComparer : NoHashCode<SimpleShow>
|
||||||
{
|
{
|
||||||
public override bool Equals(SimpleShow show1, SimpleShow show2) => SimpleShowStringComparer.IsEqual(show1, show2);
|
public override bool Equals(SimpleShow show1, SimpleShow show2) => IsEqual(show1, show2);
|
||||||
|
|
||||||
public static bool IsEqual(SimpleShow show1, SimpleShow show2) => show1.Name == show2.Name
|
public static bool IsEqual(SimpleShow show1, SimpleShow show2) => show1.Name == show2.Name
|
||||||
&& show1.Publisher == show2.Publisher;
|
&& show1.Publisher == show2.Publisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SimpleArtistStringComparer : NoHashCode<SimpleArtist>
|
public class SimpleArtistStringComparer : NoHashCode<SimpleArtist>
|
||||||
{
|
{
|
||||||
public override bool Equals(SimpleArtist artist1, SimpleArtist artist2) => SimpleArtistStringComparer.IsEqual(artist1, artist2);
|
public override bool Equals(SimpleArtist artist1, SimpleArtist artist2) => IsEqual(artist1, artist2);
|
||||||
|
|
||||||
public static bool IsEqual(SimpleArtist artist1, SimpleArtist artist2) => artist1.Name == artist2.Name;
|
public static bool IsEqual(SimpleArtist artist1, SimpleArtist artist2) => artist1.Name == artist2.Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user