xiaohongshu
xiaohongshu
Implement playlist track reordering with move operations and position management (project)
Full skill instructions
Implement track reordering within playlists using move operations with stable position management.
Playlist reordering allows users to change the order of tracks via move operations. Each move specifies a source position and target position.
Location: src/NovaTuneApp/NovaTuneApp.ApiService/Models/Playlists/
namespace NovaTuneApp.ApiService.Models.Playlists;
/// <summary>
/// Represents a single move operation in a reorder request.
/// </summary>
/// <param name="From">Current position of the track (0-based)</param>
/// <param name="To">Target position for the track (0-based)</param>
public record MoveOperation(int From, int To);
/// <summary>
/// Request to reorder tracks within a playlist.
/// </summary>
/// <param name="Moves">List of move operations (applied sequentially)</param>
public record ReorderRequest(IReadOnlyList<MoveOperation> Moves);
Location: src/NovaTuneApp/NovaTuneApp.ApiService/Validators/
using FluentValidation;
namespace NovaTuneApp.ApiService.Validators;
public class ReorderRequestValidator : AbstractValidator<ReorderRequest>
{
public ReorderRequestValidator(IOptions<PlaylistOptions> options)
{
RuleFor(x => x.Moves)
.NotEmpty()
.WithMessage("At least one move operation is required")
.Must(m => m.Count <= options.Value.MaxMovesPerReorderRequest)
.WithMessage($"Maximum {options.Value.MaxMovesPerReorderRequest} moves per request");
RuleForEach(x => x.Moves)
.ChildRules(move =>
{
move.RuleFor(m => m.From)
.GreaterThanOrEqualTo(0)
.WithMessage("From position must be non-negative");
move.RuleFor(m => m.To)
.GreaterThanOrEqualTo(0)
.WithMessage("To position must be non-negative");
});
}
}
Location: src/NovaTuneApp/NovaTuneApp.ApiService/Services/PlaylistService.cs
public async Task<PlaylistDetails> ReorderTracksAsync(
string playlistId,
string userId,
ReorderRequest request,
CancellationToken ct = default)
{
var playlist = await _session.LoadAsync<Playlist>($"Playlists/{playlistId}", ct);
if (playlist is null)
throw new PlaylistNotFoundException(playlistId);
if (playlist.UserId != userId)
throw new PlaylistAccessDeniedException(playlistId);
if (playlist.Tracks.Count == 0)
throw new InvalidOperationException("Cannot reorder empty playlist");
// Validate all positions before applying any moves
foreach (var move in request.Moves)
{
if (move.From < 0 || move.From >= playlist.Tracks.Count)
throw new InvalidPositionException(move.From, playlist.Tracks.Count);
if (move.To < 0 || move.To >= playlist.Tracks.Count)
throw new InvalidPositionException(move.To, playlist.Tracks.Count);
}
// Sort tracks by position to work with a proper list
var tracks = playlist.Tracks.OrderBy(t => t.Position).ToList();
// Apply moves sequentially
foreach (var move in request.Moves)
{
if (move.From == move.To)
continue; // No-op move
var track = tracks[move.From];
tracks.RemoveAt(move.From);
tracks.Insert(move.To, track);
}
// Reassign positions to maintain contiguous 0-based indices
for (var i = 0; i < tracks.Count; i++)
{
tracks[i].Position = i;
}
playlist.Tracks = tracks;
playlist.UpdatedAt = DateTimeOffset.UtcNow;
await _session.SaveChangesAsync(ct);
_logger.LogInformation(
"Reordered {MoveCount} tracks in playlist {PlaylistId} for user {UserId}",
request.Moves.Count, playlistId, userId);
return await MapToDetailsAsync(playlist, ct);
}
Location: src/NovaTuneApp/NovaTuneApp.ApiService/Infrastructure/Exceptions/
namespace NovaTuneApp.ApiService.Infrastructure.Exceptions;
/// <summary>
/// Thrown when a position is out of valid range.
/// </summary>
public class InvalidPositionException : Exception
{
public int Position { get; }
public int MaxPosition { get; }
public InvalidPositionException(int position, int trackCount)
: base($"Position {position} is out of range. Valid range: 0 to {trackCount - 1}")
{
Position = position;
MaxPosition = trackCount - 1;
}
}
Location: src/NovaTuneApp/NovaTuneApp.ApiService/Endpoints/PlaylistEndpoints.cs
group.MapPost("/{playlistId}/reorder", HandleReorderTracks)
.WithName("ReorderPlaylistTracks")
.WithSummary("Reorder tracks within a playlist")
.Produces<PlaylistDetails>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status409Conflict)
.RequireRateLimiting("playlist-reorder");
private static async Task<IResult> HandleReorderTracks(
[FromRoute] string playlistId,
[FromBody] ReorderRequest request,
[FromServices] IPlaylistService playlistService,
[FromServices] IValidator<ReorderRequest> validator,
ClaimsPrincipal user,
CancellationToken ct)
{
if (!Ulid.TryParse(playlistId, out _))
{
return Results.Problem(
title: "Invalid playlist ID",
detail: "Playlist ID must be a valid ULID.",
statusCode: StatusCodes.Status400BadRequest,
type: "https://novatune.dev/errors/invalid-playlist-id");
}
var validationResult = await validator.ValidateAsync(request, ct);
if (!validationResult.IsValid)
{
return Results.ValidationProblem(validationResult.ToDictionary());
}
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier)!;
try
{
var playlist = await playlistService.ReorderTracksAsync(
playlistId, userId, request, ct);
return Results.Ok(playlist);
}
catch (PlaylistNotFoundException)
{
return Results.Problem(
title: "Playlist not found",
statusCode: StatusCodes.Status404NotFound,
type: "https://novatune.dev/errors/playlist-not-found");
}
catch (PlaylistAccessDeniedException)
{
return Results.Problem(
title: "Access denied",
statusCode: StatusCodes.Status403Forbidden,
type: "https://novatune.dev/errors/forbidden");
}
catch (InvalidPositionException ex)
{
return Results.Problem(
title: "Invalid position",
detail: ex.Message,
statusCode: StatusCodes.Status400BadRequest,
type: "https://novatune.dev/errors/invalid-position",
extensions: new Dictionary<string, object?>
{
["position"] = ex.Position,
["maxPosition"] = ex.MaxPosition
});
}
catch (ConcurrencyException)
{
return Results.Problem(
title: "Concurrent modification",
detail: "The playlist was modified by another request. Please retry.",
statusCode: StatusCodes.Status409Conflict,
type: "https://novatune.dev/errors/concurrency-conflict");
}
}
POST /playlists/01HXK.../reorder
{
"moves": [
{ "from": 5, "to": 0 },
{ "from": 10, "to": 3 }
]
}
{
"playlistId": "01HXK...",
"name": "My Playlist",
"trackCount": 20,
"tracks": {
"items": [
{ "position": 0, "trackId": "01HXL...", "title": "Moved Track" },
{ "position": 1, "trackId": "01HXM...", "title": "Second Track" }
],
"hasMore": true
}
}
{
"type": "https://novatune.dev/errors/invalid-position",
"title": "Invalid position",
"status": 400,
"detail": "Position 25 is out of range. Valid range: 0 to 19",
"position": 25,
"maxPosition": 19
}
Moves are applied sequentially, which means:
This allows complex reorderings with predictable results.
Before: [A, B, C, D, E] (positions 0-4)
Move: { "from": 4, "to": 0 }
After: [E, A, B, C, D] (positions 0-4)
Before: [A, B, C, D, E]
Moves: [{ "from": 0, "to": 4 }, { "from": 4, "to": 0 }]
After Move 1: [B, C, D, E, A]
After Move 2: [A, B, C, D, E] (back to original - this is NOT a swap)
For a true swap, use: [{ "from": 0, "to": 4 }, { "from": 3, "to": 0 }]
For simpler UX, consider also exposing a single-move endpoint:
group.MapPost("/{playlistId}/tracks/{position:int}/move", HandleMoveTrack)
.WithName("MovePlaylistTrack")
.WithSummary("Move a single track to a new position");
Request: POST /playlists/01HXK.../tracks/5/move?to=0
| Rule | Error |
|---|---|
| Moves array not empty | 400 Bad Request |
| Max 50 moves per request | 400 Bad Request |
| From position in valid range | 400 Bad Request |
| To position in valid range | 400 Bad Request |
| Playlist exists | 404 Not Found |
| User owns playlist | 403 Forbidden |
doc/implementation/stage-6/09-api-reorder-tracks.mddoc/implementation/stage-6/10-service-interface.mddoc/implementation/stage-6/18-test-strategy.mdxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience