Reworked play activity to handle scrobble engine better.

This commit is contained in:
Steven Hildreth 2019-06-09 16:31:02 -05:00
parent 740683e93b
commit 17b542af91
14 changed files with 234 additions and 61 deletions

View file

@ -33,6 +33,8 @@ namespace Roadie.Library.Models
public Image UserThumbnail { get; set; }
public UserTrack UserTrack { get; set; }
public bool IsNowPlaying { get; set; }
public override string ToString()
{
return $"User [{ this.User }], Artist [{ this.Artist }], Release [{ this.Release }], Track [{ this.Track}]";

View file

@ -36,6 +36,16 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic
/// </summary>
public string c { get; set; }
/// <summary>
/// The time (in milliseconds since 1 Jan 1970) at which the song was listened to.
/// </summary>
public string time { get; set; }
/// <summary>
/// Whether this is a "submission" or a "now playing" notification.
/// </summary>
public string submission { get; set; }
/// <summary>
/// <seealso cref="f"/>
/// </summary>

View file

@ -3,6 +3,7 @@ using Roadie.Library.Caching;
using Roadie.Library.Configuration;
using Roadie.Library.MetaData.LastFm;
using Roadie.Library.Models.Users;
using Roadie.Library.Utility;
using System.Threading.Tasks;
using data = Roadie.Library.Data;
@ -16,8 +17,9 @@ namespace Roadie.Library.Scrobble
{
private ILastFmHelper LastFmHelper { get; }
public LastFMScrobbler(IRoadieSettings configuration, ILogger logger, data.IRoadieDbContext dbContext, ICacheManager cacheManager, ILastFmHelper lastFmHelper)
: base(configuration, logger, dbContext, cacheManager)
public LastFMScrobbler(IRoadieSettings configuration, ILogger logger, data.IRoadieDbContext dbContext,
ICacheManager cacheManager, ILastFmHelper lastFmHelper, IHttpContext httpContext)
: base(configuration, logger, dbContext, cacheManager, httpContext)
{
LastFmHelper = lastFmHelper;
}

View file

@ -2,7 +2,9 @@
using Microsoft.Extensions.Logging;
using Roadie.Library.Caching;
using Roadie.Library.Configuration;
using Roadie.Library.Models;
using Roadie.Library.Models.Users;
using Roadie.Library.Utility;
using System;
using System.Diagnostics;
using System.Linq;
@ -13,25 +15,41 @@ namespace Roadie.Library.Scrobble
{
public class RoadieScrobbler : ScrobblerIntegrationBase
{
public RoadieScrobbler(IRoadieSettings configuration, ILogger logger, data.IRoadieDbContext dbContext, ICacheManager cacheManager)
: base(configuration, logger, dbContext, cacheManager)
public RoadieScrobbler(IRoadieSettings configuration, ILogger logger, data.IRoadieDbContext dbContext,
ICacheManager cacheManager, IHttpContext httpContext)
: base(configuration, logger, dbContext, cacheManager, httpContext)
{
}
/// <summary>
/// The user has started playing a track.
/// For Roadie we only add a user play on the full scrobble event, otherwise we get double track play numbers.
/// </summary>
public override async Task<OperationResult<bool>> NowPlaying(User roadieUser, ScrobbleInfo scrobble) => await ScrobbleAction(roadieUser, scrobble, true);
public override async Task<OperationResult<bool>> NowPlaying(User roadieUser, ScrobbleInfo scrobble)
{
return new OperationResult<bool>
{
Data = true,
IsSuccess = true
};
}
/// <summary>
/// The user has played a track.
/// </summary>
public override async Task<OperationResult<bool>> Scrobble(User roadieUser, ScrobbleInfo scrobble) => await ScrobbleAction(roadieUser, scrobble, false);
private async Task<OperationResult<bool>> ScrobbleAction(User roadieUser, ScrobbleInfo scrobble, bool isNowPlaying)
{
public override async Task<OperationResult<bool>> Scrobble(User roadieUser, ScrobbleInfo scrobble)
{
try
{
// If less than half of duration then do nothing
if (scrobble.ElapsedTimeOfTrackPlayed.TotalSeconds < (scrobble.TrackDuration.TotalSeconds / 2))
{
return new OperationResult<bool>
{
Data = true,
IsSuccess = true
};
}
var sw = Stopwatch.StartNew();
var track = this.DbContext.Tracks
.Include(x => x.ReleaseMedia)
@ -52,25 +70,47 @@ namespace Roadie.Library.Scrobble
var success = false;
try
{
// Only create (or update) a user track activity record when the user has played the entire track.
if (!isNowPlaying)
var user = this.DbContext.Users.FirstOrDefault(x => x.RoadieId == roadieUser.UserId);
userTrack = this.DbContext.UserTracks.FirstOrDefault(x => x.UserId == roadieUser.Id && x.TrackId == track.Id);
if (userTrack == null)
{
var user = this.DbContext.Users.FirstOrDefault(x => x.RoadieId == roadieUser.UserId);
userTrack = this.DbContext.UserTracks.FirstOrDefault(x => x.UserId == roadieUser.Id && x.TrackId == track.Id);
if (userTrack == null)
userTrack = new data.UserTrack(now)
{
userTrack = new data.UserTrack(now)
{
UserId = user.Id,
TrackId = track.Id
};
this.DbContext.UserTracks.Add(userTrack);
}
userTrack.LastPlayed = now;
userTrack.PlayedCount = (userTrack.PlayedCount ?? 0) + 1;
this.CacheManager.ClearRegion(user.CacheRegion);
await this.DbContext.SaveChangesAsync();
UserId = user.Id,
TrackId = track.Id
};
this.DbContext.UserTracks.Add(userTrack);
}
userTrack.LastPlayed = now;
userTrack.PlayedCount = (userTrack.PlayedCount ?? 0) + 1;
track.PlayedCount = (track.PlayedCount ?? 0) + 1;
track.LastPlayed = now;
var release = this.DbContext.Releases.Include(x => x.Artist).FirstOrDefault(x => x.RoadieId == track.ReleaseMedia.Release.RoadieId);
release.LastPlayed = now;
release.PlayedCount = (release.PlayedCount ?? 0) + 1;
var artist = this.DbContext.Artists.FirstOrDefault(x => x.RoadieId == release.Artist.RoadieId);
artist.LastPlayed = now;
artist.PlayedCount = (artist.PlayedCount ?? 0) + 1;
data.Artist trackArtist = null;
if (track.ArtistId.HasValue)
{
trackArtist = this.DbContext.Artists.FirstOrDefault(x => x.Id == track.ArtistId);
trackArtist.LastPlayed = now;
trackArtist.PlayedCount = (trackArtist.PlayedCount ?? 0) + 1;
this.CacheManager.ClearRegion(trackArtist.CacheRegion);
}
await this.DbContext.SaveChangesAsync();
this.CacheManager.ClearRegion(track.CacheRegion);
this.CacheManager.ClearRegion(track.ReleaseMedia.Release.CacheRegion);
this.CacheManager.ClearRegion(track.ReleaseMedia.Release.Artist.CacheRegion);
this.CacheManager.ClearRegion(user.CacheRegion);
success = true;
}
catch (Exception ex)
@ -78,7 +118,7 @@ namespace Roadie.Library.Scrobble
this.Logger.LogError(ex, $"Error in Scrobble, Creating UserTrack: User `{ roadieUser}` TrackId [{ track.Id }");
}
sw.Stop();
this.Logger.LogInformation($"RoadieScrobbler: RoadieUser `{ roadieUser }` { (isNowPlaying ? "NowPlaying" : "Scrobble") } `{ scrobble }`");
this.Logger.LogInformation($"RoadieScrobbler: RoadieUser `{ roadieUser }` Scrobble `{ scrobble }`");
return new OperationResult<bool>
{
Data = success,

View file

@ -4,6 +4,7 @@ using Roadie.Library.Configuration;
using Roadie.Library.Encoding;
using Roadie.Library.MetaData.LastFm;
using Roadie.Library.Models.Users;
using Roadie.Library.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
@ -21,18 +22,21 @@ namespace Roadie.Library.Scrobble
private data.IRoadieDbContext DbContext { get; }
private ILogger Logger { get; }
private IHttpEncoder HttpEncoder { get; }
private IHttpContext HttpContext { get; }
private IEnumerable<IScrobblerIntegration> Scrobblers { get; }
public ScrobbleHandler(IRoadieSettings configuration, ILogger<ScrobbleHandler> logger, data.IRoadieDbContext dbContext, ICacheManager cacheManager, IHttpEncoder httpEncoder)
public ScrobbleHandler(IRoadieSettings configuration, ILogger<ScrobbleHandler> logger, data.IRoadieDbContext dbContext,
ICacheManager cacheManager, IHttpEncoder httpEncoder, IHttpContext httpContext)
{
Logger = logger;
Configuration = configuration;
DbContext = dbContext;
HttpEncoder = httpEncoder;
HttpContext = httpContext;
var scrobblers = new List<IScrobblerIntegration>
{
new RoadieScrobbler(configuration, logger, dbContext, cacheManager)
new RoadieScrobbler(configuration, logger, dbContext, cacheManager, httpContext)
};
if (configuration.Integrations.LastFmProviderEnabled)
{

View file

@ -2,6 +2,7 @@
using Roadie.Library.Caching;
using Roadie.Library.Configuration;
using Roadie.Library.Models.Users;
using Roadie.Library.Utility;
using System.Threading.Tasks;
using data = Roadie.Library.Data;
@ -13,13 +14,16 @@ namespace Roadie.Library.Scrobble
protected IRoadieSettings Configuration { get; }
protected data.IRoadieDbContext DbContext { get; }
protected ILogger Logger { get; }
protected IHttpContext HttpContext { get; }
public ScrobblerIntegrationBase(IRoadieSettings configuration, ILogger logger, data.IRoadieDbContext dbContext, ICacheManager cacheManager)
public ScrobblerIntegrationBase(IRoadieSettings configuration, ILogger logger, data.IRoadieDbContext dbContext,
ICacheManager cacheManager, IHttpContext httpContext)
{
Logger = logger;
Configuration = configuration;
DbContext = dbContext;
CacheManager = cacheManager;
HttpContext = httpContext;
}
public abstract Task<OperationResult<bool>> NowPlaying(User roadieUser, ScrobbleInfo scrobble);

View file

@ -40,7 +40,9 @@ namespace Roadie.Library.MetaData.LastFm
{
get
{
return this.Configuration.Integrations.LastFmProviderEnabled;
return this.Configuration.Integrations.LastFmProviderEnabled &&
!string.IsNullOrEmpty(this.Configuration.Integrations.LastFMApiKey) &&
!string.IsNullOrEmpty(this.Configuration.Integrations.LastFmApiSecret);
}
}

View file

@ -2,6 +2,7 @@
using Roadie.Library.Models;
using Roadie.Library.Models.Pagination;
using Roadie.Library.Models.Users;
using Roadie.Library.Scrobble;
using System;
using System.Threading.Tasks;
@ -9,6 +10,8 @@ namespace Roadie.Api.Services
{
public interface IPlayActivityService
{
Task<OperationResult<bool>> NowPlaying(User roadieUser, ScrobbleInfo scrobble);
Task<OperationResult<bool>> Scrobble(User roadieUser, ScrobbleInfo scrobble);
Task<PagedResult<PlayActivityList>> List(PagedRequest request, User roadieUser = null, DateTime? newerThan = null);
}
}

View file

@ -9,6 +9,7 @@ using Roadie.Library.Encoding;
using Roadie.Library.Models;
using Roadie.Library.Models.Pagination;
using Roadie.Library.Models.Users;
using Roadie.Library.Scrobble;
using Roadie.Library.Utility;
using System;
using System.Collections.Generic;
@ -22,6 +23,7 @@ namespace Roadie.Api.Services
{
public class PlayActivityService : ServiceBase, IPlayActivityService
{
protected IScrobbleHandler ScrobblerHandler { get; }
protected IHubContext<PlayActivityHub> PlayActivityHub { get; }
public PlayActivityService(IRoadieSettings configuration,
@ -30,10 +32,12 @@ namespace Roadie.Api.Services
data.IRoadieDbContext dbContext,
ICacheManager cacheManager,
ILogger<PlayActivityService> logger,
IHubContext<PlayActivityHub> playHubContext)
IScrobbleHandler scrobbleHandler,
IHubContext<PlayActivityHub> playActivityHub)
: base(configuration, httpEncoder, dbContext, cacheManager, logger, httpContext)
{
this.PlayActivityHub = playHubContext;
this.PlayActivityHub = playActivityHub;
this.ScrobblerHandler = scrobbleHandler;
}
public Task<Library.Models.Pagination.PagedResult<PlayActivityList>> List(PagedRequest request, User roadieUser = null, DateTime? newerThan = null)
@ -113,5 +117,90 @@ namespace Roadie.Api.Services
}
return Task.FromResult(new Library.Models.Pagination.PagedResult<PlayActivityList>());
}
public async Task<OperationResult<bool>> NowPlaying(User roadieUser, ScrobbleInfo scrobble)
{
var scrobbleResult = await this.ScrobblerHandler.NowPlaying(roadieUser, scrobble);
if (!scrobbleResult.IsSuccess)
{
return scrobbleResult;
}
await PublishPlayActivity(roadieUser, scrobble, true);
return scrobbleResult;
}
public async Task<OperationResult<bool>> Scrobble(User roadieUser, ScrobbleInfo scrobble)
{
var scrobbleResult = await this.ScrobblerHandler.Scrobble(roadieUser, scrobble);
if(!scrobbleResult.IsSuccess)
{
return scrobbleResult;
}
await PublishPlayActivity(roadieUser, scrobble, false);
return scrobbleResult;
}
private async Task PublishPlayActivity(User roadieUser, ScrobbleInfo scrobble, bool isNowPlaying)
{
// Only broadcast if the user is not public and played duration is more than half of duration
if (!roadieUser.IsPrivate &&
scrobble.ElapsedTimeOfTrackPlayed.TotalSeconds > (scrobble.TrackDuration.TotalSeconds / 2))
{
var sw = Stopwatch.StartNew();
var track = this.DbContext.Tracks
.Include(x => x.ReleaseMedia)
.Include(x => x.ReleaseMedia.Release)
.Include(x => x.ReleaseMedia.Release.Artist)
.Include(x => x.TrackArtist)
.FirstOrDefault(x => x.RoadieId == scrobble.TrackId);
var user = this.DbContext.Users.FirstOrDefault(x => x.RoadieId == roadieUser.UserId);
var userTrack = this.DbContext.UserTracks.FirstOrDefault(x => x.UserId == roadieUser.Id && x.TrackId == track.Id);
var pl = new PlayActivityList
{
Artist = new DataToken
{
Text = track.ReleaseMedia.Release.Artist.Name,
Value = track.ReleaseMedia.Release.Artist.RoadieId.ToString()
},
TrackArtist = track.TrackArtist == null ? null : new DataToken
{
Text = track.TrackArtist.Name,
Value = track.TrackArtist.RoadieId.ToString()
},
Release = new DataToken
{
Text = track.ReleaseMedia.Release.Title,
Value = track.ReleaseMedia.Release.RoadieId.ToString()
},
Track = new DataToken
{
Text = track.Title,
Value = track.RoadieId.ToString()
},
User = new DataToken
{
Text = roadieUser.UserName,
Value = roadieUser.UserId.ToString()
},
ArtistThumbnail = this.MakeArtistThumbnailImage(track.TrackArtist != null ? track.TrackArtist.RoadieId : track.ReleaseMedia.Release.Artist.RoadieId),
PlayedDateDateTime = scrobble.TimePlayed,
IsNowPlaying = isNowPlaying,
Rating = track.Rating,
ReleasePlayUrl = $"{ this.HttpContext.BaseUrl }/play/release/{ track.ReleaseMedia.Release.RoadieId}",
ReleaseThumbnail = this.MakeReleaseThumbnailImage(track.ReleaseMedia.Release.RoadieId),
TrackPlayUrl = $"{ this.HttpContext.BaseUrl }/play/track/{ track.RoadieId}.mp3",
UserRating = userTrack?.Rating,
UserThumbnail = this.MakeUserThumbnailImage(roadieUser.UserId)
};
try
{
await this.PlayActivityHub.Clients.All.SendAsync("SendActivity", pl);
}
catch (Exception ex)
{
this.Logger.LogError(ex);
}
}
}
}
}

View file

@ -57,7 +57,7 @@ namespace Roadie.Api.Controllers
return this._currentUser;
}
protected async Task<IActionResult> StreamTrack(Guid id, ITrackService trackService, IScrobbleHandler scrobbleHandler, models.User currentUser = null)
protected async Task<IActionResult> StreamTrack(Guid id, ITrackService trackService, IPlayActivityService playActivityService, models.User currentUser = null)
{
var sw = Stopwatch.StartNew();
var timings = new Dictionary<string, long>();
@ -126,15 +126,7 @@ namespace Roadie.Api.Controllers
TimePlayed = DateTime.UtcNow,
TrackId = id
};
if(!info.Data.IsFullRequest)
{
await scrobbleHandler.NowPlaying(user, scrobble);
}
else
{
await scrobbleHandler.Scrobble(user, scrobble);
}
await playActivityService.NowPlaying(user, scrobble);
sw.Stop();
this.Logger.LogInformation($"StreamTrack ElapsedTime [{ sw.ElapsedMilliseconds }], Timings [{ JsonConvert.SerializeObject(timings) }], StreamInfo `{ info?.Data.ToString() }`");
return new EmptyResult();

View file

@ -22,18 +22,18 @@ namespace Roadie.Api.Controllers
[Authorize]
public class PlayController : EntityControllerBase
{
private IScrobbleHandler ScrobbleHandler { get; }
private IPlayActivityService PlayActivityService { get; }
private IReleaseService ReleaseService { get; }
private ITrackService TrackService { get; }
public PlayController(ITrackService trackService, IReleaseService releaseService, IScrobbleHandler scrobblerHandler,
public PlayController(ITrackService trackService, IReleaseService releaseService, IPlayActivityService playActivityService,
ILoggerFactory logger, ICacheManager cacheManager, UserManager<ApplicationUser> userManager,
IRoadieSettings roadieSettings)
: base(cacheManager, roadieSettings, userManager)
{
this.Logger = logger.CreateLogger("RoadieApi.Controllers.PlayController");
this.TrackService = trackService;
this.ScrobbleHandler = scrobblerHandler;
this.PlayActivityService = playActivityService;
this.ReleaseService = releaseService;
}
@ -76,7 +76,7 @@ namespace Roadie.Api.Controllers
[ProducesResponseType(404)]
public async Task<IActionResult> Scrobble(Guid id, string startedPlaying, bool isRandom)
{
var result = await this.ScrobbleHandler.Scrobble(await this.CurrentUserModel(), new ScrobbleInfo
var result = await this.PlayActivityService.Scrobble(await this.CurrentUserModel(), new ScrobbleInfo
{
TrackId = id,
TimePlayed = SafeParser.ToDateTime(startedPlaying) ?? DateTime.UtcNow,
@ -109,7 +109,7 @@ namespace Roadie.Api.Controllers
{
return StatusCode((int)HttpStatusCode.Unauthorized);
}
return await base.StreamTrack(id, this.TrackService, this.ScrobbleHandler, this.UserModelForUser(user));
return await base.StreamTrack(id, this.TrackService, this.PlayActivityService, this.UserModelForUser(user));
}
}
}

View file

@ -1,7 +1,6 @@
using Mapster;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Roadie.Api.ModelBinding;
@ -12,6 +11,7 @@ using Roadie.Library.Extensions;
using Roadie.Library.Identity;
using Roadie.Library.Models.ThirdPartyApi.Subsonic;
using Roadie.Library.Scrobble;
using Roadie.Library.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
@ -24,7 +24,7 @@ namespace Roadie.Api.Controllers
[ApiController]
public class SubsonicController : EntityControllerBase
{
private IScrobbleHandler ScrobbleHandler { get; }
private IPlayActivityService PlayActivityService { get; }
private IReleaseService ReleaseService { get; }
private ISubsonicService SubsonicService { get; }
@ -35,8 +35,8 @@ namespace Roadie.Api.Controllers
private ITrackService TrackService { get; }
public SubsonicController(ISubsonicService subsonicService, ITrackService trackService, IReleaseService releaseService,
IScrobbleHandler scrobbleHandler, ILoggerFactory logger, ICacheManager cacheManager,
public SubsonicController(ISubsonicService subsonicService, ITrackService trackService, IReleaseService releaseService,
IPlayActivityService playActivityService, ILoggerFactory logger, ICacheManager cacheManager,
UserManager<ApplicationUser> userManager, IRoadieSettings roadieSettings)
: base(cacheManager, roadieSettings, userManager)
{
@ -44,7 +44,7 @@ namespace Roadie.Api.Controllers
this.SubsonicService = subsonicService;
this.TrackService = trackService;
this.ReleaseService = releaseService;
this.ScrobbleHandler = scrobbleHandler;
this.PlayActivityService = playActivityService;
}
[HttpGet("addChatMessage.view")]
@ -130,7 +130,7 @@ namespace Roadie.Api.Controllers
var trackId = request.TrackId;
if (trackId != null)
{
return await base.StreamTrack(trackId.Value, this.TrackService, this.ScrobbleHandler, this.SubsonicUser);
return await base.StreamTrack(trackId.Value, this.TrackService, this.PlayActivityService, this.SubsonicUser);
}
var releaseId = request.ReleaseId;
if (releaseId != null)
@ -635,6 +635,31 @@ namespace Roadie.Api.Controllers
return this.BuildResponse(request, result);
}
[HttpGet("scrobble.view")]
[HttpGet("scrobble")]
[HttpPost("scrobble.view")]
[HttpPost("scrobble")]
[ProducesResponseType(200)]
public async Task<IActionResult> Scrobble(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var timePlayed = string.IsNullOrEmpty(request.time) ? DateTime.UtcNow.AddDays(-1) : SafeParser.ToNumber<long>(request.time).FromUnixTime();
var scrobblerResponse = await this.PlayActivityService.Scrobble(this.SubsonicUser, new ScrobbleInfo
{
TrackId = request.TrackId.Value,
TimePlayed = timePlayed
});
return this.BuildResponse(request, new SubsonicOperationResult<Response>
{
IsSuccess = scrobblerResponse.IsSuccess,
Data = new Response()
});
}
/// <summary>
/// Returns albums, artists and songs matching the given search criteria. Supports paging through the result.
/// </summary>
@ -723,7 +748,7 @@ namespace Roadie.Api.Controllers
{
return NotFound("Invalid TrackId");
}
return await base.StreamTrack(trackId.Value, this.TrackService, this.ScrobbleHandler, this.SubsonicUser);
return await base.StreamTrack(trackId.Value, this.TrackService, this.PlayActivityService, this.SubsonicUser);
}
[HttpGet("unstar.view")]

View file

@ -3,7 +3,7 @@
"Roadie.Api": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"ASPNETCORE_ENVIRONMENT": "Production"
},
"applicationUrl": "http://localhost:5123/"
}

View file

@ -96,17 +96,17 @@
"ApiKeys": [
{
"ApiName": "BingImageSearch",
"Key": "<KEY HERE>"
"Key": ""
},
{
"ApiName": "LastFMApiKey",
"Key": "<KEY HERE>",
"KeySecret": "<SECRET HERE>"
"Key": "",
"KeySecret": ""
},
{
"ApiName": "DiscogsConsumerKey",
"Key": "<KEY HERE>",
"KeySecret": "<SECRET HERE>"
"Key": "",
"KeySecret": ""
}
]
},