2022-02-16 23:38:45 +00:00
|
|
|
|
using IF.Lastfm.Core.Api;
|
|
|
|
|
using IF.Lastfm.Core.Objects;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
2022-02-20 21:22:32 +00:00
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
2022-02-16 23:38:45 +00:00
|
|
|
|
using Selector.Model;
|
2022-02-24 00:27:34 +00:00
|
|
|
|
using Selector.Operations;
|
2022-02-16 23:38:45 +00:00
|
|
|
|
using System;
|
2022-02-20 21:22:32 +00:00
|
|
|
|
using System.Collections.Concurrent;
|
2022-02-16 23:38:45 +00:00
|
|
|
|
using System.Collections.Generic;
|
2022-02-18 00:08:42 +00:00
|
|
|
|
using System.Diagnostics;
|
2022-02-16 23:38:45 +00:00
|
|
|
|
using System.Linq;
|
2022-02-18 19:47:11 +00:00
|
|
|
|
using System.Text;
|
2022-02-16 23:38:45 +00:00
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace Selector
|
|
|
|
|
{
|
|
|
|
|
public class ScrobbleSaverConfig
|
|
|
|
|
{
|
|
|
|
|
public ApplicationUser User { get; set; }
|
|
|
|
|
public TimeSpan InterRequestDelay { get; set; }
|
2022-02-20 21:22:32 +00:00
|
|
|
|
public TimeSpan Timeout { get; set; } = new TimeSpan(0, 20, 0);
|
2022-02-16 23:38:45 +00:00
|
|
|
|
public DateTime? From { get; set; }
|
|
|
|
|
public DateTime? To { get; set; }
|
|
|
|
|
public int PageSize { get; set; } = 100;
|
2022-02-18 00:08:42 +00:00
|
|
|
|
public int Retries { get; set; } = 5;
|
2022-02-20 21:22:32 +00:00
|
|
|
|
public int SimultaneousConnections { get; set; } = 3;
|
2022-02-18 00:08:42 +00:00
|
|
|
|
public bool DontAdd { get; set; } = false;
|
|
|
|
|
public bool DontRemove { get; set; } = false;
|
2022-02-16 23:38:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class ScrobbleSaver
|
|
|
|
|
{
|
|
|
|
|
private readonly ILogger<ScrobbleSaver> logger;
|
2022-02-20 21:22:32 +00:00
|
|
|
|
private readonly ILoggerFactory loggerFactory;
|
2022-02-16 23:38:45 +00:00
|
|
|
|
|
|
|
|
|
private readonly IUserApi userClient;
|
|
|
|
|
private readonly ScrobbleSaverConfig config;
|
2022-02-24 00:27:34 +00:00
|
|
|
|
private BatchingOperation<ScrobbleRequest> batchOperation;
|
2022-02-20 21:22:32 +00:00
|
|
|
|
|
2022-02-24 00:27:34 +00:00
|
|
|
|
private readonly IScrobbleRepository scrobbleRepo;
|
2022-02-16 23:38:45 +00:00
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
private readonly object dbLock;
|
|
|
|
|
|
|
|
|
|
public ScrobbleSaver(IUserApi _userClient, ScrobbleSaverConfig _config, IScrobbleRepository _scrobbleRepository, ILogger<ScrobbleSaver> _logger, ILoggerFactory _loggerFactory = null, object _dbLock = null)
|
2022-02-16 23:38:45 +00:00
|
|
|
|
{
|
|
|
|
|
userClient = _userClient;
|
|
|
|
|
config = _config;
|
2022-02-24 00:27:34 +00:00
|
|
|
|
scrobbleRepo = _scrobbleRepository;
|
2022-02-16 23:38:45 +00:00
|
|
|
|
logger = _logger;
|
2022-02-20 21:22:32 +00:00
|
|
|
|
loggerFactory = _loggerFactory;
|
2022-02-26 23:42:29 +00:00
|
|
|
|
|
|
|
|
|
dbLock = _dbLock ?? new();
|
2022-02-16 23:38:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task Execute(CancellationToken token)
|
|
|
|
|
{
|
2022-02-24 00:27:34 +00:00
|
|
|
|
logger.LogInformation("Saving scrobbles for {}/{}", config.User.UserName, config.User.LastFmUsername);
|
2022-02-16 23:38:45 +00:00
|
|
|
|
|
2022-02-20 21:22:32 +00:00
|
|
|
|
var page1 = new ScrobbleRequest(userClient,
|
|
|
|
|
loggerFactory?.CreateLogger<ScrobbleRequest>() ?? NullLogger<ScrobbleRequest>.Instance,
|
|
|
|
|
config.User.UserName,
|
|
|
|
|
1,
|
|
|
|
|
config.PageSize,
|
|
|
|
|
config.From, config.To);
|
2022-02-16 23:38:45 +00:00
|
|
|
|
|
2022-02-20 21:22:32 +00:00
|
|
|
|
await page1.Execute();
|
2022-02-16 23:38:45 +00:00
|
|
|
|
|
2022-02-20 21:22:32 +00:00
|
|
|
|
if (page1.Succeeded)
|
|
|
|
|
{
|
2022-02-18 00:08:42 +00:00
|
|
|
|
if (page1.TotalPages > 1)
|
2022-02-16 23:38:45 +00:00
|
|
|
|
{
|
2022-02-24 00:27:34 +00:00
|
|
|
|
batchOperation = new BatchingOperation<ScrobbleRequest>(
|
|
|
|
|
config.InterRequestDelay,
|
|
|
|
|
config.Timeout,
|
|
|
|
|
config.SimultaneousConnections,
|
|
|
|
|
GetRequestsFromPageNumbers(2, page1.TotalPages)
|
|
|
|
|
);
|
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
await batchOperation.TriggerRequests(token);
|
2022-02-24 00:27:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-24 21:57:26 +00:00
|
|
|
|
IEnumerable<LastTrack> scrobbles;
|
2022-02-24 00:27:34 +00:00
|
|
|
|
if(batchOperation is not null)
|
|
|
|
|
{
|
2022-02-24 21:57:26 +00:00
|
|
|
|
scrobbles = page1.Scrobbles.Union(batchOperation.DoneRequests.SelectMany(r => r.Scrobbles));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
scrobbles = page1.Scrobbles;
|
2022-02-24 00:27:34 +00:00
|
|
|
|
}
|
2022-02-20 21:22:32 +00:00
|
|
|
|
|
2022-02-18 00:08:42 +00:00
|
|
|
|
logger.LogDebug("Ordering and filtering pulled scrobbles");
|
|
|
|
|
|
2022-02-24 23:32:02 +00:00
|
|
|
|
scrobbles = scrobbles.Where(s => !(s.IsNowPlaying is bool playing && playing));
|
|
|
|
|
|
|
|
|
|
IdentifyDuplicates(scrobbles);
|
2022-02-20 21:22:32 +00:00
|
|
|
|
|
2022-02-16 23:38:45 +00:00
|
|
|
|
var nativeScrobbles = scrobbles
|
2022-02-20 21:54:01 +00:00
|
|
|
|
.DistinctBy(s => new { s.TimePlayed?.UtcDateTime, s.Name, s.ArtistName })
|
2022-02-24 21:57:26 +00:00
|
|
|
|
.Select(s => (UserScrobble) s)
|
|
|
|
|
.ToArray();
|
2022-02-18 00:08:42 +00:00
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
logger.LogInformation("Completed network scrobble pulling for {}, pulled {:n0}", config.User.UserName, nativeScrobbles.Length);
|
2022-02-18 00:08:42 +00:00
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
logger.LogDebug("Pulling currently stored scrobbles");
|
2022-02-16 23:38:45 +00:00
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
lock (dbLock)
|
|
|
|
|
{
|
|
|
|
|
var currentScrobbles = scrobbleRepo.GetAll(userId: config.User.Id, from: config.From, to: config.To);
|
2022-02-18 00:08:42 +00:00
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
logger.LogDebug("Identifying difference sets");
|
|
|
|
|
var time = Stopwatch.StartNew();
|
2022-02-18 00:08:42 +00:00
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
(var toAdd, var toRemove) = ScrobbleMatcher.IdentifyDiffs(currentScrobbles, nativeScrobbles);
|
2022-02-18 00:08:42 +00:00
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
time.Stop();
|
|
|
|
|
logger.LogTrace("Finished diffing: {:n}ms", time.ElapsedMilliseconds);
|
|
|
|
|
|
|
|
|
|
var timeDbOps = Stopwatch.StartNew();
|
|
|
|
|
|
|
|
|
|
if (!config.DontAdd)
|
2022-02-24 21:57:26 +00:00
|
|
|
|
{
|
2022-02-26 23:42:29 +00:00
|
|
|
|
foreach (var add in toAdd)
|
|
|
|
|
{
|
|
|
|
|
var scrobble = (UserScrobble)add;
|
|
|
|
|
scrobble.UserId = config.User.Id;
|
|
|
|
|
scrobbleRepo.Add(scrobble);
|
|
|
|
|
}
|
2022-02-24 21:57:26 +00:00
|
|
|
|
}
|
2022-02-26 23:42:29 +00:00
|
|
|
|
else
|
2022-02-24 21:57:26 +00:00
|
|
|
|
{
|
2022-02-26 23:42:29 +00:00
|
|
|
|
logger.LogInformation("Skipping adding of {} scrobbles", toAdd.Count());
|
2022-02-24 21:57:26 +00:00
|
|
|
|
}
|
2022-02-26 23:42:29 +00:00
|
|
|
|
if (!config.DontRemove)
|
|
|
|
|
{
|
|
|
|
|
foreach (var remove in toRemove)
|
|
|
|
|
{
|
|
|
|
|
var scrobble = (UserScrobble)remove;
|
|
|
|
|
scrobble.UserId = config.User.Id;
|
|
|
|
|
scrobbleRepo.Remove(scrobble);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
logger.LogInformation("Skipping removal of {} scrobbles", toRemove.Count());
|
|
|
|
|
}
|
|
|
|
|
_ = scrobbleRepo.Save().Result;
|
2022-02-18 00:08:42 +00:00
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
timeDbOps.Stop();
|
|
|
|
|
logger.LogTrace("DB ops: {:n}ms", timeDbOps.ElapsedMilliseconds);
|
2022-02-18 00:08:42 +00:00
|
|
|
|
|
2022-02-26 23:42:29 +00:00
|
|
|
|
logger.LogInformation("Completed scrobble pulling for {}, +{:n0}, -{:n0}", config.User.UserName, toAdd.Count(), toRemove.Count());
|
|
|
|
|
}
|
2022-02-16 23:38:45 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2022-02-24 00:27:34 +00:00
|
|
|
|
logger.LogError("Failed to pull first scrobble page for {}/{}", config.User.UserName, config.User.LastFmUsername);
|
2022-02-20 21:22:32 +00:00
|
|
|
|
}
|
2022-02-16 23:38:45 +00:00
|
|
|
|
}
|
2022-02-18 19:47:11 +00:00
|
|
|
|
|
2022-02-20 21:22:32 +00:00
|
|
|
|
private IEnumerable<ScrobbleRequest> GetRequestsFromPageNumbers(int start, int totalPages)
|
|
|
|
|
=> Enumerable.Range(start, totalPages - 1)
|
|
|
|
|
.Select(n => new ScrobbleRequest(
|
|
|
|
|
userClient,
|
|
|
|
|
loggerFactory.CreateLogger<ScrobbleRequest>() ?? NullLogger<ScrobbleRequest>.Instance,
|
|
|
|
|
config.User.UserName,
|
|
|
|
|
n,
|
|
|
|
|
config.PageSize,
|
|
|
|
|
config.From,
|
2022-02-24 00:27:34 +00:00
|
|
|
|
config.To,
|
|
|
|
|
config.Retries));
|
2022-02-20 21:22:32 +00:00
|
|
|
|
|
2022-02-18 19:47:11 +00:00
|
|
|
|
private void IdentifyDuplicates(IEnumerable<LastTrack> tracks)
|
|
|
|
|
{
|
|
|
|
|
logger.LogDebug("Identifying duplicates");
|
|
|
|
|
|
|
|
|
|
var duplicates = tracks
|
|
|
|
|
.GroupBy(t => t.TimePlayed?.UtcDateTime)
|
|
|
|
|
.Where(g => g.Count() > 1);
|
|
|
|
|
|
|
|
|
|
foreach(var dupe in duplicates)
|
|
|
|
|
{
|
|
|
|
|
var dupeString = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
foreach(var scrobble in dupe)
|
|
|
|
|
{
|
2022-02-24 00:27:34 +00:00
|
|
|
|
dupeString.Append('(');
|
2022-02-18 19:47:11 +00:00
|
|
|
|
dupeString.Append(scrobble.Name);
|
|
|
|
|
dupeString.Append(", ");
|
|
|
|
|
dupeString.Append(scrobble.AlbumName);
|
|
|
|
|
dupeString.Append(", ");
|
|
|
|
|
dupeString.Append(scrobble.ArtistName);
|
2022-02-24 00:27:34 +00:00
|
|
|
|
dupeString.Append(") ");
|
2022-02-18 19:47:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-24 00:27:34 +00:00
|
|
|
|
logger.LogInformation("Duplicate at {}: {}", dupe.Key, dupeString.ToString());
|
2022-02-18 19:47:11 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2022-02-20 21:22:32 +00:00
|
|
|
|
|
2022-02-24 21:57:26 +00:00
|
|
|
|
private IEnumerable<LastTrack> RemoveNowPlaying(IEnumerable<LastTrack> scrobbles)
|
2022-02-20 21:22:32 +00:00
|
|
|
|
{
|
|
|
|
|
var newestScrobble = scrobbles.FirstOrDefault();
|
|
|
|
|
if (newestScrobble is not null)
|
|
|
|
|
{
|
|
|
|
|
if (newestScrobble.IsNowPlaying is bool playing && playing)
|
|
|
|
|
{
|
2022-02-24 21:57:26 +00:00
|
|
|
|
scrobbles = scrobbles.Skip(1);
|
2022-02-20 21:22:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-24 21:57:26 +00:00
|
|
|
|
return scrobbles;
|
2022-02-20 21:22:32 +00:00
|
|
|
|
}
|
2022-02-16 23:38:45 +00:00
|
|
|
|
}
|
|
|
|
|
}
|