Fans the frozen ETag/If-Match/412 recipe (Block reference, #253) onto the Playlist aggregate: - ReplacePlaylistItems command carries ExpectedVersion; the handler runs CheckVersion as a standalone Either after validation (so a stale write survives as 412, not flattened to 422 by Apply/Join), bumps Version unconditionally before saving, and persists via SaveChangesWithConcurrencyGuard (EF concurrency-token backstop). - PlaylistViewModel carries Version; the items GET sets a strong ETag and the PUT parses If-Match, threads it into the command, and returns the refreshed ETag on success (400 on a malformed If-Match). - Sibling item-adding handlers (AddItemsToPlaylist, AddMovie/Episode/ Season/ShowToPlaylist) bump Version too, since they mutate the same editor-visible item list. - SPA: playlists.ts exposes getPlaylistItemsWithMeta and an If-Match-aware updatePlaylist; PlaylistEditor holds the ETag in a ref, round-trips it on save, and opens a "changed elsewhere" ConfirmDialog on 412 (mirrors BlockEditor). Tests: new ReplacePlaylistItemsHandlerConcurrencyTests (stale/match/ force-write/no-op-bump/racing-save), new PlaylistController tests (ETag on GET items, 400/412/thread-version/force-write on PUT), and a vitest 412-conflict-dialog test for PlaylistsScreen. dotnet test: 1304/1304 green. web: npm run typecheck clean, npm run build clean, vitest 664/664 green. Ref #253 PR2.
124 lines
5.9 KiB
C#
124 lines
5.9 KiB
C#
using ErsatzTV.Application.Tree;
|
|
using ErsatzTV.Core.Api.SmartCollections;
|
|
using ErsatzTV.Core.Domain;
|
|
|
|
namespace ErsatzTV.Application.MediaCollections;
|
|
|
|
internal static class Mapper
|
|
{
|
|
internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) =>
|
|
new(
|
|
CollectionType.Collection,
|
|
collection.Id,
|
|
collection.Name,
|
|
collection.UseCustomPlaybackOrder,
|
|
MediaItemState.Normal);
|
|
|
|
internal static MultiCollectionViewModel ProjectToViewModel(MultiCollection multiCollection) =>
|
|
new(
|
|
multiCollection.Id,
|
|
multiCollection.Name,
|
|
Optional(multiCollection.MultiCollectionItems).Flatten().Map(ProjectToViewModel).ToList(),
|
|
Optional(multiCollection.MultiCollectionSmartItems).Flatten().Map(ProjectToViewModel).ToList());
|
|
|
|
internal static SmartCollectionViewModel ProjectToViewModel(SmartCollection collection) =>
|
|
new(collection.Id, collection.Name, collection.Query);
|
|
|
|
internal static SmartCollectionResponseModel ProjectToResponseModel(SmartCollection collection) =>
|
|
new(collection.Id, collection.Name, collection.Query);
|
|
|
|
internal static RerunCollectionViewModel ProjectToViewModel(RerunCollection collection) =>
|
|
new(
|
|
collection.Id,
|
|
collection.Name,
|
|
collection.CollectionType,
|
|
collection.Collection is not null ? ProjectToViewModel(collection.Collection) : null,
|
|
collection.MultiCollection is not null ? ProjectToViewModel(collection.MultiCollection) : null,
|
|
collection.SmartCollection is not null ? ProjectToViewModel(collection.SmartCollection) : null,
|
|
collection.MediaItem switch
|
|
{
|
|
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
|
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
|
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
|
|
Movie movie => MediaItems.Mapper.ProjectToViewModel(movie),
|
|
Episode episode => MediaItems.Mapper.ProjectToViewModel(episode),
|
|
MusicVideo musicVideo => MediaItems.Mapper.ProjectToViewModel(musicVideo),
|
|
OtherVideo otherVideo => MediaItems.Mapper.ProjectToViewModel(otherVideo),
|
|
Song song => MediaItems.Mapper.ProjectToViewModel(song),
|
|
Image image => MediaItems.Mapper.ProjectToViewModel(image),
|
|
_ => null
|
|
},
|
|
collection.FirstRunPlaybackOrder,
|
|
collection.RerunPlaybackOrder);
|
|
|
|
internal static TraktListViewModel ProjectToViewModel(TraktList traktList) =>
|
|
new(
|
|
traktList.Id,
|
|
traktList.TraktId,
|
|
$"{traktList.User}/{traktList.List}",
|
|
traktList.Name,
|
|
traktList.ItemCount,
|
|
traktList.Items.Count(i => i.MediaItemId.HasValue),
|
|
traktList.AutoRefresh,
|
|
traktList.GeneratePlaylist);
|
|
|
|
private static MultiCollectionItemViewModel ProjectToViewModel(MultiCollectionItem multiCollectionItem) =>
|
|
new(
|
|
multiCollectionItem.MultiCollectionId,
|
|
ProjectToViewModel(multiCollectionItem.Collection),
|
|
multiCollectionItem.ScheduleAsGroup,
|
|
multiCollectionItem.PlaybackOrder);
|
|
|
|
private static MultiCollectionSmartItemViewModel ProjectToViewModel(
|
|
MultiCollectionSmartItem multiCollectionSmartItem) =>
|
|
new(
|
|
multiCollectionSmartItem.MultiCollectionId,
|
|
ProjectToViewModel(multiCollectionSmartItem.SmartCollection),
|
|
multiCollectionSmartItem.ScheduleAsGroup,
|
|
multiCollectionSmartItem.PlaybackOrder);
|
|
|
|
internal static TreeViewModel ProjectToViewModel(List<PlaylistGroup> playlistGroups) =>
|
|
new(
|
|
playlistGroups.Map(bg => new TreeGroupViewModel(
|
|
bg.Id,
|
|
bg.Name,
|
|
bg.Playlists.Map(b => new TreeItemViewModel(b.Id, b.Name, b.IsSystem)).ToList(),
|
|
bg.IsSystem)).ToList());
|
|
|
|
internal static PlaylistGroupViewModel ProjectToViewModel(PlaylistGroup playlistGroup) =>
|
|
new(playlistGroup.Id, playlistGroup.Name, playlistGroup.Playlists.Count, playlistGroup.IsSystem);
|
|
|
|
internal static PlaylistViewModel ProjectToViewModel(Playlist playlist) =>
|
|
new(playlist.Id, playlist.PlaylistGroupId, playlist.Name, playlist.IsSystem, playlist.Version);
|
|
|
|
internal static PlaylistItemViewModel ProjectToViewModel(PlaylistItem playlistItem) =>
|
|
new(
|
|
playlistItem.Id,
|
|
playlistItem.Index,
|
|
playlistItem.CollectionType,
|
|
playlistItem.Collection is not null ? ProjectToViewModel(playlistItem.Collection) : null,
|
|
playlistItem.MultiCollection is not null
|
|
? ProjectToViewModel(playlistItem.MultiCollection)
|
|
: null,
|
|
playlistItem.SmartCollection is not null
|
|
? ProjectToViewModel(playlistItem.SmartCollection)
|
|
: null,
|
|
playlistItem.MediaItem switch
|
|
{
|
|
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
|
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
|
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
|
|
Movie movie => MediaItems.Mapper.ProjectToViewModel(movie),
|
|
Episode episode => MediaItems.Mapper.ProjectToViewModel(episode),
|
|
MusicVideo musicVideo => MediaItems.Mapper.ProjectToViewModel(musicVideo),
|
|
OtherVideo otherVideo => MediaItems.Mapper.ProjectToViewModel(otherVideo),
|
|
Song song => MediaItems.Mapper.ProjectToViewModel(song),
|
|
Image image => MediaItems.Mapper.ProjectToViewModel(image),
|
|
_ => null
|
|
},
|
|
playlistItem.PlaybackOrder,
|
|
playlistItem.Count,
|
|
playlistItem.PlayAll,
|
|
playlistItem.IncludeInProgramGuide);
|
|
}
|