Browse Source

add playlist item count and shuffle playlist items (#2407)

* marathon cleanup

* add playlist item count, and shuffle playlist items
pull/2408/head
Jason Dove 4 months ago committed by GitHub
parent
commit
17c7774603
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 1
      ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItem.cs
  3. 1
      ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs
  4. 1
      ErsatzTV.Application/MediaCollections/Mapper.cs
  5. 1
      ErsatzTV.Application/MediaCollections/PlaylistItemViewModel.cs
  6. 139
      ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs
  7. 1
      ErsatzTV.Core/Domain/Collection/PlaylistItem.cs
  8. 54
      ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs
  9. 2
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  10. 6399
      ErsatzTV.Infrastructure.MySql/Migrations/20250912115701_Add_PlaylistItemCount.Designer.cs
  11. 28
      ErsatzTV.Infrastructure.MySql/Migrations/20250912115701_Add_PlaylistItemCount.cs
  12. 5
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  13. 6234
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250912132040_Add_PlaylistItemCount.Designer.cs
  14. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250912132040_Add_PlaylistItemCount.cs
  15. 5
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  16. 18
      ErsatzTV/Pages/PlaylistEditor.razor
  17. 12
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  18. 48
      ErsatzTV/ViewModels/PlaylistItemEditViewModel.cs
  19. 13
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

5
CHANGELOG.md

@ -22,6 +22,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -22,6 +22,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Empty or zero batch size means play all items from each group before advancing
- Any other value means play the specified number of items before advancing to the next group
- Log API requests when `Request Logging Minimum Log Level` is set to `Debug`
- Add `Count` setting to each playlist item
- Previously, when `Play All` was unchecked, this was implicitly 1
- Now, the playlist can play a specific number of items from the collection before moving to the next playlist item
- Classic schedules: add `Shuffle Playlist Items` setting to shuffle the order of playlist items
- Shuffling happens initially (on playout reset), and after all items from the *entire playlist* have been played
### Fixed
- Fix transcoding content with bt709/pc color metadata

1
ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItem.cs

@ -10,5 +10,6 @@ public record ReplacePlaylistItem( @@ -10,5 +10,6 @@ public record ReplacePlaylistItem(
int? SmartCollectionId,
int? MediaItemId,
PlaybackOrder PlaybackOrder,
int? Count,
bool PlayAll,
bool IncludeInProgramGuide);

1
ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs

@ -46,6 +46,7 @@ public class ReplacePlaylistItemsHandler(IDbContextFactory<TvContext> dbContextF @@ -46,6 +46,7 @@ public class ReplacePlaylistItemsHandler(IDbContextFactory<TvContext> dbContextF
SmartCollectionId = item.SmartCollectionId,
MediaItemId = item.MediaItemId,
PlaybackOrder = item.PlaybackOrder,
Count = item.Count,
PlayAll = item.PlayAll,
IncludeInProgramGuide = item.IncludeInProgramGuide
};

1
ErsatzTV.Application/MediaCollections/Mapper.cs

@ -89,6 +89,7 @@ internal static class Mapper @@ -89,6 +89,7 @@ internal static class Mapper
_ => null
},
playlistItem.PlaybackOrder,
playlistItem.Count,
playlistItem.PlayAll,
playlistItem.IncludeInProgramGuide);
}

1
ErsatzTV.Application/MediaCollections/PlaylistItemViewModel.cs

@ -12,5 +12,6 @@ public record PlaylistItemViewModel( @@ -12,5 +12,6 @@ public record PlaylistItemViewModel(
SmartCollectionViewModel SmartCollection,
NamedMediaItemViewModel MediaItem,
PlaybackOrder PlaybackOrder,
int? Count,
bool PlayAll,
bool IncludeInProgramGuide);

139
ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs

@ -146,6 +146,145 @@ public class PlaylistEnumeratorTests @@ -146,6 +146,145 @@ public class PlaylistEnumeratorTests
items.ShouldBe([10, 20, 30, 31, 10, 21, 30, 31]);
}
[Test]
public async Task Shuffled_Playlist_Should_Honor_PlayAll()
{
// this isn't needed for chronological, so no need to implement anything
IMediaCollectionRepository repo = Substitute.For<IMediaCollectionRepository>();
var playlistItemMap = new Dictionary<PlaylistItem, List<MediaItem>>
{
{
new PlaylistItem
{
Id = 1,
Index = 0,
PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection,
CollectionId = 1
},
[FakeMovie(10)]
},
{
new PlaylistItem
{
Id = 2,
Index = 1,
PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = true,
CollectionType = ProgramScheduleItemCollectionType.Collection,
CollectionId = 2
},
[FakeMovie(20), FakeMovie(21)]
},
{
new PlaylistItem
{
Id = 3,
Index = 2,
PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection,
CollectionId = 3
},
[FakeMovie(30)]
}
};
var state = new CollectionEnumeratorState { Seed = 1 };
PlaylistEnumerator enumerator = await PlaylistEnumerator.Create(
repo,
playlistItemMap,
state,
shufflePlaylistItems: true,
batchSize: Option<int>.None,
CancellationToken.None);
var items = new List<int>();
for (var i = 0; i < 4; i++)
{
items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext();
}
// with seed 1, shuffle order of (1,2,3) is (2,3,1)
// correct playout should be item 2 (all), item 3 (1), item 1 (1)
// which is media items (20, 21), (30), (10)
items.ShouldBe([20, 21, 30, 10]);
}
[Test]
public async Task Shuffled_Playlist_Should_Honor_Custom_Count()
{
// this isn't needed for chronological, so no need to implement anything
IMediaCollectionRepository repo = Substitute.For<IMediaCollectionRepository>();
var playlistItemMap = new Dictionary<PlaylistItem, List<MediaItem>>
{
{
new PlaylistItem
{
Id = 1,
Index = 0,
PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false,
Count = 2,
CollectionType = ProgramScheduleItemCollectionType.Collection,
CollectionId = 1
},
[FakeMovie(10), FakeMovie(11), FakeMovie(12)]
},
{
new PlaylistItem
{
Id = 2,
Index = 1,
PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection,
CollectionId = 2
},
[FakeMovie(20)]
},
{
new PlaylistItem
{
Id = 3,
Index = 2,
PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection,
CollectionId = 3
},
[FakeMovie(30)]
}
};
var state = new CollectionEnumeratorState { Seed = 1 };
PlaylistEnumerator enumerator = await PlaylistEnumerator.Create(
repo,
playlistItemMap,
state,
shufflePlaylistItems: true,
batchSize: Option<int>.None,
CancellationToken.None);
var items = new List<int>();
for (var i = 0; i < 4; i++)
{
items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext();
}
// with seed 1, shuffle order of (1,2,3) is (2,3,1)
// correct playout should be item 2 (1), item 3 (1), item 1 (2)
// which is media items (20), (30), (10, 11)
items.ShouldBe([20, 30, 10, 11]);
}
private static Movie FakeMovie(int id) => new()
{
Id = id,

1
ErsatzTV.Core/Domain/Collection/PlaylistItem.cs

@ -16,6 +16,7 @@ public class PlaylistItem @@ -16,6 +16,7 @@ public class PlaylistItem
public int? SmartCollectionId { get; set; }
public SmartCollection SmartCollection { get; set; }
public PlaybackOrder PlaybackOrder { get; set; }
public int? Count { get; set; }
public bool PlayAll { get; set; }
public bool IncludeInProgramGuide { get; set; }
}

54
ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs

@ -11,10 +11,9 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -11,10 +11,9 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
private readonly System.Collections.Generic.HashSet<int> _remainingMediaItemIds = [];
private System.Collections.Generic.HashSet<int> _allMediaItemIds;
private System.Collections.Generic.HashSet<int> _idsToIncludeInEPG;
private IList<bool> _playAll;
private CloneableRandom _random;
private bool _shufflePlaylistItems;
private List<IMediaCollectionEnumerator> _sortedEnumerators;
private List<EnumeratorPlayAllCount> _sortedEnumerators;
private int _itemsTakenFromCurrent;
private Option<int> _batchSize = Option<int>.None;
@ -24,11 +23,11 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -24,11 +23,11 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
public int CountForRandom => _allMediaItemIds.Count;
public int CountForFiller => _sortedEnumerators.Select((t, i) => _playAll[i] ? t.Count : 1).Sum();
public int CountForFiller => _sortedEnumerators.Select(t => t.PlayAll ? t.Enumerator.Count : 1).Sum();
public ImmutableList<PlaylistEnumeratorCollectionKey> ChildEnumerators { get; private set; }
public bool CurrentEnumeratorPlayAll => _playAll[EnumeratorIndex];
public bool CurrentEnumeratorPlayAll => _sortedEnumerators[EnumeratorIndex].PlayAll;
public int EnumeratorIndex { get; private set; }
@ -39,7 +38,7 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -39,7 +38,7 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
public CollectionEnumeratorState State { get; private set; }
public Option<MediaItem> Current => _sortedEnumerators.Count > 0
? _sortedEnumerators[EnumeratorIndex].Current
? _sortedEnumerators[EnumeratorIndex].Enumerator.Current
: Option<MediaItem>.None;
public Option<bool> CurrentIncludeInProgramGuide
@ -61,19 +60,34 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -61,19 +60,34 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
public void MoveNext()
{
foreach (MediaItem maybeMediaItem in _sortedEnumerators[EnumeratorIndex].Current)
foreach (MediaItem maybeMediaItem in _sortedEnumerators[EnumeratorIndex].Enumerator.Current)
{
_remainingMediaItemIds.Remove(maybeMediaItem.Id);
}
_sortedEnumerators[EnumeratorIndex].MoveNext();
_sortedEnumerators[EnumeratorIndex].Enumerator.MoveNext();
_itemsTakenFromCurrent++;
bool shouldSwitchEnumerator = _batchSize.Match(
// move to the next enumerator if we've hit the batch size
batchSize => _itemsTakenFromCurrent >= batchSize,
// if we aren't playing all, or if we just finished playing all, move to the next enumerator
() => !_playAll[EnumeratorIndex] || _sortedEnumerators[EnumeratorIndex].State.Index == 0);
() =>
{
// if we just finished playing all, move to the next enumerator
if (_sortedEnumerators[EnumeratorIndex].PlayAll)
{
return _sortedEnumerators[EnumeratorIndex].Enumerator.State.Index == 0;
}
// if we have played the desired count, move to the next enumerator
if (_sortedEnumerators[EnumeratorIndex].Count is { } count)
{
return _itemsTakenFromCurrent >= count;
}
// otherwise, always move
return true;
});
if (shouldSwitchEnumerator)
{
@ -82,7 +96,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -82,7 +96,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
}
State.Index += 1;
if (_remainingMediaItemIds.Count == 0 && EnumeratorIndex == 0 && _sortedEnumerators[0].State.Index == 0)
if (_remainingMediaItemIds.Count == 0 && EnumeratorIndex == 0 &&
_sortedEnumerators[0].Enumerator.State.Index == 0)
{
State.Index = 0;
_remainingMediaItemIds.UnionWith(_allMediaItemIds);
@ -109,7 +124,6 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -109,7 +124,6 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
var result = new PlaylistEnumerator
{
_sortedEnumerators = [],
_playAll = [],
_idsToIncludeInEPG = [],
_shufflePlaylistItems = shufflePlaylistItems,
_batchSize = batchSize
@ -135,8 +149,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -135,8 +149,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
var collectionKey = CollectionKey.ForPlaylistItem(playlistItem);
if (enumeratorMap.TryGetValue(collectionKey, out IMediaCollectionEnumerator enumerator))
{
result._sortedEnumerators.Add(enumerator);
result._playAll.Add(playlistItem.PlayAll);
result._sortedEnumerators.Add(
new EnumeratorPlayAllCount(enumerator, playlistItem.PlayAll, playlistItem.Count));
continue;
}
@ -193,8 +207,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -193,8 +207,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
if (enumerator is not null)
{
enumeratorMap.Add(collectionKey, enumerator);
result._sortedEnumerators.Add(enumerator);
result._playAll.Add(playlistItem.PlayAll);
result._sortedEnumerators.Add(
new EnumeratorPlayAllCount(enumerator, playlistItem.PlayAll, playlistItem.Count));
}
}
@ -234,7 +248,7 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -234,7 +248,7 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
}
var childEnumerators = new List<PlaylistEnumeratorCollectionKey>();
foreach (IMediaCollectionEnumerator enumerator in result._sortedEnumerators)
foreach ((IMediaCollectionEnumerator enumerator, _, _) in result._sortedEnumerators)
{
foreach ((CollectionKey collectionKey, _) in enumeratorMap.Find(e => e.Value == enumerator))
{
@ -247,15 +261,15 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -247,15 +261,15 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
return result;
}
private List<IMediaCollectionEnumerator> ShufflePlaylistItems()
private List<EnumeratorPlayAllCount> ShufflePlaylistItems()
{
if (_sortedEnumerators.Count < 3)
{
return _sortedEnumerators;
}
IMediaCollectionEnumerator[] copy = _sortedEnumerators.ToArray();
IMediaCollectionEnumerator last = _sortedEnumerators.Last();
EnumeratorPlayAllCount[] copy = _sortedEnumerators.ToArray();
EnumeratorPlayAllCount last = _sortedEnumerators.Last();
do
{
@ -270,4 +284,6 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -270,4 +284,6 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
return copy.ToList();
}
private record EnumeratorPlayAllCount(IMediaCollectionEnumerator Enumerator, bool PlayAll, int? Count);
}

2
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -1235,7 +1235,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -1235,7 +1235,7 @@ public class PlayoutBuilder : IPlayoutBuilder
_mediaCollectionRepository,
playlistItemMap,
state,
shufflePlaylistItems: false,
marathonShuffleGroups,
batchSize: Option<int>.None,
cancellationToken);
}

6399
ErsatzTV.Infrastructure.MySql/Migrations/20250912115701_Add_PlaylistItemCount.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.MySql/Migrations/20250912115701_Add_PlaylistItemCount.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_PlaylistItemCount : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Count",
table: "PlaylistItem",
type: "int",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Count",
table: "PlaylistItem");
}
}
}

5
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
@ -1777,6 +1777,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -1777,6 +1777,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("CollectionType")
.HasColumnType("int");
b.Property<int?>("Count")
.HasColumnType("int");
b.Property<bool>("IncludeInProgramGuide")
.HasColumnType("tinyint(1)");

6234
ErsatzTV.Infrastructure.Sqlite/Migrations/20250912132040_Add_PlaylistItemCount.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20250912132040_Add_PlaylistItemCount.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlaylistItemCount : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Count",
table: "PlaylistItem",
type: "INTEGER",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Count",
table: "PlaylistItem");
}
}
}

5
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.8");
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{
@ -1690,6 +1690,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1690,6 +1690,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("CollectionType")
.HasColumnType("INTEGER");
b.Property<int?>("Count")
.HasColumnType("INTEGER");
b.Property<bool>("IncludeInProgramGuide")
.HasColumnType("INTEGER");

18
ErsatzTV/Pages/PlaylistEditor.razor

@ -53,6 +53,7 @@ @@ -53,6 +53,7 @@
<col/>
<col/>
<col/>
<col/>
<col style="width: 240px;"/>
</MudHidden>
</ColGroup>
@ -60,6 +61,7 @@ @@ -60,6 +61,7 @@
<MudTh>Item Type</MudTh>
<MudTh>Item</MudTh>
<MudTh>Playback Order</MudTh>
<MudTh>Count</MudTh>
<MudTh>Play All</MudTh>
<MudTh>Show In EPG</MudTh>
<MudTh/>
@ -77,7 +79,12 @@ @@ -77,7 +79,12 @@
</MudTd>
<MudTd DataLabel="Playback Order">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@(context.PlaybackOrder > 0 ? context.PlaybackOrder : "")
@(context.PlaybackOrder > 0 ? context.PlaybackOrder : string.Empty)
</MudText>
</MudTd>
<MudTd DataLabel="Count">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@(context.Count is not null ? context.Count : (context.PlayAll ? string.Empty : "1"))
</MudText>
</MudTd>
<MudTd DataLabel="Play All">
@ -298,6 +305,12 @@ @@ -298,6 +305,12 @@
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Count</MudText>
</div>
<MudTextField @bind-Value="@_selectedItem.Count" For="@(() => _selectedItem.Count)"/>
</MudStack>
}
else if (_previewItems is not null)
{
@ -466,6 +479,7 @@ else if (_previewItems is not null) @@ -466,6 +479,7 @@ else if (_previewItems is not null)
SmartCollection = item.SmartCollection,
MediaItem = item.MediaItem,
PlaybackOrder = item.PlaybackOrder,
Count = item.Count,
PlayAll = item.PlayAll,
IncludeInProgramGuide = item.IncludeInProgramGuide
};
@ -494,6 +508,7 @@ else if (_previewItems is not null) @@ -494,6 +508,7 @@ else if (_previewItems is not null)
MultiCollection = item.MultiCollection,
SmartCollection = item.SmartCollection,
MediaItem = item.MediaItem,
Count = item.Count,
PlayAll = item.PlayAll,
IncludeInProgramGuide = item.IncludeInProgramGuide
};
@ -552,6 +567,7 @@ else if (_previewItems is not null) @@ -552,6 +567,7 @@ else if (_previewItems is not null)
item.SmartCollection?.Id,
item.MediaItem?.MediaItemId,
item.PlaybackOrder,
item.Count,
item.PlayAll,
item.IncludeInProgramGuide)).ToList();

12
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -365,6 +365,7 @@ @@ -365,6 +365,7 @@
<MudSelectItem Value="PlaybackOrder.MultiEpisodeShuffle">Multi-Episode Shuffle</MudSelectItem>
break;
case ProgramScheduleItemCollectionType.Playlist:
<MudSelectItem Value="PlaybackOrder.None">Playlist</MudSelectItem>
break;
default:
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
@ -412,6 +413,17 @@ @@ -412,6 +413,17 @@
HelperText="How many items to play from each group before advancing; empty or zero will play all items"/>
</MudStack>
}
else if (_selectedItem.CollectionType is ProgramScheduleItemCollectionType.Playlist)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Shuffle Playlist Items</MudText>
</div>
<MudCheckBox @bind-Value="_selectedItem.MarathonShuffleGroups"
For="@(() => _selectedItem.MarathonShuffleGroups)"
Dense="true"/>
</MudStack>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Playout Mode</MudText>

48
ErsatzTV/ViewModels/PlaylistItemEditViewModel.cs

@ -9,6 +9,8 @@ namespace ErsatzTV.ViewModels; @@ -9,6 +9,8 @@ namespace ErsatzTV.ViewModels;
public class PlaylistItemEditViewModel : INotifyPropertyChanged
{
private ProgramScheduleItemCollectionType _collectionType;
private int? _count;
private bool _playAll;
public int Id { get; set; }
public int Index { get; set; }
@ -81,7 +83,51 @@ public class PlaylistItemEditViewModel : INotifyPropertyChanged @@ -81,7 +83,51 @@ public class PlaylistItemEditViewModel : INotifyPropertyChanged
public PlaybackOrder PlaybackOrder { get; set; }
public bool PlayAll { get; set; }
public int? Count
{
get => _count;
set
{
if (value == _count)
{
return;
}
_count = value;
OnPropertyChanged();
if (_count is not null)
{
_playAll = false;
OnPropertyChanged(nameof(PlayAll));
}
}
}
public bool PlayAll
{
get => _playAll;
set
{
if (value == _playAll)
{
return;
}
_playAll = value;
OnPropertyChanged();
if (_playAll)
{
_count = null;
}
else
{
_count ??= 1;
}
OnPropertyChanged(nameof(Count));
}
}
public bool IncludeInProgramGuide { get; set; }

13
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -69,11 +69,17 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -69,11 +69,17 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
MultipleMode = MultipleMode.Count;
}
if (_collectionType == ProgramScheduleItemCollectionType.Playlist)
{
PlaybackOrder = PlaybackOrder.None;
}
OnPropertyChanged(nameof(Collection));
OnPropertyChanged(nameof(MultiCollection));
OnPropertyChanged(nameof(MediaItem));
OnPropertyChanged(nameof(SmartCollection));
OnPropertyChanged(nameof(MultiCollection));
OnPropertyChanged(nameof(PlaybackOrder));
}
if (_collectionType == ProgramScheduleItemCollectionType.MultiCollection)
@ -129,6 +135,13 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -129,6 +135,13 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
MultipleMode = MultipleMode.Count;
}
if (_playbackOrder is not PlaybackOrder.Marathon)
{
MarathonGroupBy = MarathonGroupBy.None;
MarathonShuffleItems = false;
MarathonBatchSize = null;
}
OnPropertyChanged();
OnPropertyChanged(nameof(CanFillWithGroups));
OnPropertyChanged(nameof(MultipleMode));

Loading…
Cancel
Save