Web player general changes #22

Merged
NotS1mp merged 27 commits from web-player-general-changes into master 2026-09-23 22:37:30 +00:00
17 changed files with 1531 additions and 267 deletions
Showing only changes of commit cc7afe4326 - Show all commits
@@ -18,7 +18,8 @@ namespace Musify.Application.Tracks
string Title,
Guid PictureIntentId,
Guid AudioIntentId,
IReadOnlyCollection<Genre> Tags)
IReadOnlyCollection<Genre> Tags,
bool IsExplicit = false)
: ICommand<ErrorOr<TrackApplicationResponse>>;
public class CreateTrackCommandHandler(
@@ -83,6 +84,7 @@ namespace Musify.Application.Tracks
NormalizedTitle = request.Title.ToUpperInvariant(),
OwnerUserId = user.Id,
Owner = user,
IsExplicit = request.IsExplicit,
Pictures = new TrackPictures
{
OriginalName = pictureIntent.ObjectName,
@@ -15,7 +15,8 @@ namespace Musify.Application.Tracks.Responses
DateTime CreatedAt,
DateTime UpdatedAt,
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId,
IReadOnlyCollection<Genre> Tags)
IReadOnlyCollection<Genre> Tags,
bool IsExplicit)
{
public static TrackApplicationResponse FromEntity(Track track, int listensCount) =>
new(
@@ -28,6 +29,7 @@ namespace Musify.Application.Tracks.Responses
track.CreatedAt,
track.UpdatedAt,
track.OwnerUserId,
track.Tags.Select(tag => tag.Tag).ToList());
track.Tags.Select(tag => tag.Tag).ToList(),
track.IsExplicit);
}
}
@@ -31,6 +31,9 @@ namespace Musify.Domain.Entities
[Required]
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
[Required]
public bool IsExplicit { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackIsExplicit : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsExplicit",
table: "Tracks",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsExplicit",
table: "Tracks");
}
}
}
@@ -427,6 +427,9 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.Property<double>("DurationSeconds")
.HasColumnType("double precision");
b.Property<bool>("IsExplicit")
.HasColumnType("boolean");
b.Property<int>("LifeCycleStatus")
.HasColumnType("integer");
@@ -6,4 +6,5 @@ public record CreateTrackRequest(
string Title,
Guid PictureIntentId,
Guid AudioIntentId,
IReadOnlyCollection<Genre> Tags);
IReadOnlyCollection<Genre> Tags,
bool IsExplicit = false);
@@ -147,7 +147,7 @@ public static class TrackEndpoints
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new CreateTrackCommand(currentUser.RequiredId, request.Title, request.PictureIntentId, request.AudioIntentId, request.Tags),
new CreateTrackCommand(currentUser.RequiredId, request.Title, request.PictureIntentId, request.AudioIntentId, request.Tags, request.IsExplicit),
cancellationToken);
return result.ToCreatedResult(track => $"/tracks/{track.Id}");
@@ -17,6 +17,17 @@ namespace Musify.Application.Tests.Shared
Assert.Equal("The Owner", response.Artist);
Assert.Equal(owner.Id, response.OwnerUserId);
Assert.Equal(3, response.ListensCount);
Assert.False(response.IsExplicit);
}
[Fact]
public void FromEntity_CopiesExplicitFlag()
{
var track = TestEntities.Track(TestEntities.User(), isExplicit: true);
var response = TrackApplicationResponse.FromEntity(track, listensCount: 0);
Assert.True(response.IsExplicit);
}
}
}
@@ -56,7 +56,8 @@ namespace Musify.Application.Tests.TestSupport
TrackPictures? pictures = null,
TrackAudio? audio = null,
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
IEnumerable<Genre>? tags = null)
IEnumerable<Genre>? tags = null,
bool isExplicit = false)
{
var track = new Track
{
@@ -64,6 +65,7 @@ namespace Musify.Application.Tests.TestSupport
NormalizedTitle = title.ToUpperInvariant(),
DurationSeconds = durationSeconds,
LifeCycleStatus = lifeCycleStatus,
IsExplicit = isExplicit,
OwnerUserId = owner.Id,
Owner = owner,
Pictures = pictures ?? ProcessedPictures(),
@@ -58,6 +58,26 @@ namespace Musify.Application.Tests.Tracks
Assert.Equal(ProcessingStatus.Pending, stored.Audio.TranscodeStatus);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Handle_StoresExplicitFlag(bool isExplicit)
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop], isExplicit);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(isExplicit, result.Value.IsExplicit);
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(isExplicit, stored.IsExplicit);
}
[Fact]
public async Task Handle_NoTags_ReturnsValidationError()
{
+267 -259
View File
File diff suppressed because it is too large Load Diff
@@ -61,6 +61,7 @@
title={player.current.title}
artist={player.current.artist || '—'}
ownerUserId={player.current.ownerUserId}
explicit={player.current.explicit}
active
/>
</div>
@@ -123,6 +124,7 @@
title={track.title}
subtitle={track.artist || '—'}
subtitleHref={track.ownerUserId}
explicit={track.explicit}
trackId={track.id}
class="p-2"
>
@@ -7,7 +7,7 @@
</script>
<span
class="grid size-4 shrink-0 place-items-center rounded-tag bg-fg-2 text-xs leading-4 font-semibold text-bg {klass}"
class="grid size-4 shrink-0 place-items-center rounded-tag border border-accent/30 bg-accent-tint pt-px text-xs leading-none font-normal text-accent-soft {klass}"
title="Contenido explícito"
aria-label="Contenido explícito">E</span
>
@@ -25,6 +25,7 @@ export const actions: Actions = {
const audio = form.get('audio');
const cover = form.get('cover');
const rawTags = form.getAll('tags').map(String);
const isExplicit = form.get('isExplicit') === 'on';
if (title === '' || title.length > MAX_TITLE) {
return fail(400, { message: 'El título es obligatorio (máx. 100 caracteres).' });
@@ -90,7 +91,8 @@ export const actions: Actions = {
title,
pictureIntentId: urls.pictureIntentId,
audioIntentId: urls.audioIntentId,
tags
tags,
isExplicit
}
});
@@ -13,6 +13,7 @@
import Input from '$lib/components/ui/primitives/Input.svelte';
import Checkbox from '$lib/components/ui/primitives/Checkbox.svelte';
import Button from '$lib/components/ui/primitives/Button.svelte';
import ExplicitBadge from '$lib/components/ui/media/ExplicitBadge.svelte';
import Chip from '$lib/components/ui/primitives/Chip.svelte';
import { TRACK_GENRES, areCompatible, type TrackGenre } from '$lib/data/trackGenres';
@@ -29,6 +30,7 @@
let dragging = $state(false);
let status = $state<Status>('idle');
let acceptedTerms = $state(false);
let isExplicit = $state(false);
let errorMsg = $state('');
let errorDetail = $state('');
let tags = $state<TrackGenre[]>([]);
@@ -101,6 +103,7 @@
coverResetToken += 1;
status = 'idle';
acceptedTerms = false;
isExplicit = false;
errorMsg = '';
errorDetail = '';
tags = [];
@@ -277,6 +280,13 @@
<span class="shrink-0 tabular-nums">{tags.length} elegidos</span>
{/snippet}
</Field>
<Checkbox bind:checked={isExplicit} name="isExplicit">
<span class="inline-flex items-center gap-2">
Contenido explícito
<ExplicitBadge />
</span>
</Checkbox>
</div>
</div>
</Surface>
+2
View File
@@ -195,6 +195,7 @@
title={item.title}
subtitle={item.artist}
subtitleHref={item.ownerUserId}
explicit={item.explicit}
trackId={item.id}
class="animate-enter"
style="--i:{i}"
@@ -252,6 +253,7 @@
title={track.title}
subtitle={track.artist}
subtitleHref={track.ownerUserId}
explicit={track.isExplicit}
>
{#snippet leading()}
<span class="w-5 shrink-0 text-center text-xs text-fg-2 tabular-nums"