Merge master into web-player-general-changes
CI/CD / BackEnd (push) Canceled after 0s
CI/CD / WebPlayer (push) Canceled after 0s
CI/CD / BackEnd (pull_request) Successful in 1m23s
CI/CD / WebPlayer (pull_request) Successful in 40s
CI/CD / Deploy (pull_request) Skipped
CI/CD / Deploy (push) Canceled after 0s
CI/CD / BackEnd (push) Canceled after 0s
CI/CD / WebPlayer (push) Canceled after 0s
CI/CD / BackEnd (pull_request) Successful in 1m23s
CI/CD / WebPlayer (pull_request) Successful in 40s
CI/CD / Deploy (pull_request) Skipped
CI/CD / Deploy (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
public record DeleteStaleUncountedListensCommand : ICommand<int>;
|
||||
|
||||
public class DeleteStaleUncountedListensCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<DeleteStaleUncountedListensCommandHandler> logger)
|
||||
: ICommandHandler<DeleteStaleUncountedListensCommand, int>
|
||||
{
|
||||
public async ValueTask<int> Handle(DeleteStaleUncountedListensCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddDays(-ListeningRules.UncountedRetentionDays);
|
||||
|
||||
var stale = await database.ListeningHistories
|
||||
.Where(l => !l.IsCounted && l.ListenedAt < cutoff)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (stale.Count == 0)
|
||||
return 0;
|
||||
|
||||
database.ListeningHistories.RemoveRange(stale);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Deleted {Count} uncounted listens older than {Days} days", stale.Count, ListeningRules.UncountedRetentionDays);
|
||||
return stale.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Musify.Application.Tracks
|
||||
.Include(t => t.Track.Tags)
|
||||
.Where(t => t.UserId == query.UserId)
|
||||
.OrderByDescending(t => t.ListenedAt)
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.Track.ListeningHistories.Count))
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.Track.ListeningHistories.Count(l => l.IsCounted)))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return track == null
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Musify.Application.Tracks
|
||||
{
|
||||
var recentTrackIds = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == query.UserId)
|
||||
.Where(l => l.UserId == query.UserId && l.IsCounted)
|
||||
.GroupBy(l => l.TrackId)
|
||||
.OrderByDescending(g => g.Max(l => l.ListenedAt))
|
||||
.Select(g => g.Key)
|
||||
@@ -29,7 +29,7 @@ namespace Musify.Application.Tracks
|
||||
.Include(t => t.Owner)
|
||||
.Include(t => t.Tags)
|
||||
.Where(t => recentTrackIds.Contains(t.Id))
|
||||
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count })
|
||||
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.ToDictionaryAsync(t => t.Id, cancellationToken);
|
||||
|
||||
return recentTrackIds
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Musify.Application.Tracks
|
||||
.Include(t => t.Owner)
|
||||
.Include(t => t.Tags)
|
||||
.Where(t => t.Id == request.TrackId)
|
||||
.Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count })
|
||||
.Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
|
||||
@@ -5,10 +5,11 @@ using Musify.Application.Tracks.Responses;
|
||||
using X.PagedList;
|
||||
using X.PagedList.EF;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
public record GetTracksQuery(string? Name, int PageNumber, int PageSize)
|
||||
public record GetTracksQuery(string? Name, int PageNumber, int PageSize, Genre? Genre = null)
|
||||
: IQuery<ErrorOr<TracksSearchResponse>>;
|
||||
|
||||
public class GetTracksQueryHandler(IDatabase database)
|
||||
@@ -28,12 +29,15 @@ namespace Musify.Application.Tracks
|
||||
tracksQuery = tracksQuery.Where(t => t.Track.NormalizedTitle.Contains(normalizedName));
|
||||
}
|
||||
|
||||
if (request.Genre is { } genre)
|
||||
tracksQuery = tracksQuery.Where(t => t.Track.Tags.Any(tag => tag.Tag == genre));
|
||||
|
||||
var totalCount = await tracksQuery.CountAsync(cancellationToken);
|
||||
|
||||
var pagedEntities = await tracksQuery
|
||||
.OrderByDescending(t => t.Track.CreatedAt)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count })
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
var items = pagedEntities
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Musify.Application.Tracks
|
||||
|
||||
var pagedEntities = await tracksQuery
|
||||
.OrderBy(t => t.Track.CreatedAt)
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count })
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
public static class ListeningRules
|
||||
{
|
||||
public const double CountedThresholdSeconds = 30;
|
||||
public const double CountedThresholdFraction = 0.5;
|
||||
public const double DurationToleranceSeconds = 2;
|
||||
public const double ClockToleranceSeconds = 5;
|
||||
public const int UncountedRetentionDays = 8;
|
||||
|
||||
public static double CountedThreshold(double durationSeconds) =>
|
||||
Math.Min(CountedThresholdSeconds, durationSeconds * CountedThresholdFraction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
public record RecordListeningProgressCommand(
|
||||
long UserId,
|
||||
Guid ListenId,
|
||||
double PlayedSeconds)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class RecordListeningProgressCommandHandler(IDatabase database)
|
||||
: ICommandHandler<RecordListeningProgressCommand, ErrorOr<Success>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(RecordListeningProgressCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!double.IsFinite(request.PlayedSeconds) || request.PlayedSeconds < 0)
|
||||
return Error.Validation(description: "playedSeconds must be a non-negative number.");
|
||||
|
||||
var listen = await database.ListeningHistories
|
||||
.Include(l => l.Track)
|
||||
.SingleOrDefaultAsync(l => l.Id == request.ListenId && l.UserId == request.UserId, cancellationToken);
|
||||
|
||||
if (listen == null)
|
||||
return Error.NotFound();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var durationCap = listen.Track.DurationSeconds + ListeningRules.DurationToleranceSeconds;
|
||||
var clockCap = (now - listen.ListenedAt).TotalSeconds + ListeningRules.ClockToleranceSeconds;
|
||||
var accepted = Math.Min(request.PlayedSeconds, Math.Min(durationCap, clockCap));
|
||||
|
||||
listen.PlayedSeconds = Math.Max(listen.PlayedSeconds ?? 0, accepted);
|
||||
listen.LastProgressAt = now;
|
||||
listen.IsCounted = listen.IsCounted
|
||||
|| listen.PlayedSeconds >= ListeningRules.CountedThreshold(listen.Track.DurationSeconds);
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,6 @@ namespace Musify.Application.Tracks.Responses
|
||||
public record TrackStreamResponse(
|
||||
string ManifestUrl,
|
||||
string Ticket,
|
||||
int ExpiresInSeconds);
|
||||
int ExpiresInSeconds,
|
||||
Guid? ListenId = null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user