add playlist item count and shuffle playlist items (#2407)
* marathon cleanup * add playlist item count, and shuffle playlist items
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -10,5 +10,6 @@ public record ReplacePlaylistItem(
|
||||
int? SmartCollectionId,
|
||||
int? MediaItemId,
|
||||
PlaybackOrder PlaybackOrder,
|
||||
int? Count,
|
||||
bool PlayAll,
|
||||
bool IncludeInProgramGuide);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -89,6 +89,7 @@ internal static class Mapper
|
||||
_ => null
|
||||
},
|
||||
playlistItem.PlaybackOrder,
|
||||
playlistItem.Count,
|
||||
playlistItem.PlayAll,
|
||||
playlistItem.IncludeInProgramGuide);
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ public record PlaylistItemViewModel(
|
||||
SmartCollectionViewModel SmartCollection,
|
||||
NamedMediaItemViewModel MediaItem,
|
||||
PlaybackOrder PlaybackOrder,
|
||||
int? Count,
|
||||
bool PlayAll,
|
||||
bool IncludeInProgramGuide);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
var result = new PlaylistEnumerator
|
||||
{
|
||||
_sortedEnumerators = [],
|
||||
_playAll = [],
|
||||
_idsToIncludeInEPG = [],
|
||||
_shufflePlaylistItems = shufflePlaylistItems,
|
||||
_batchSize = batchSize
|
||||
@@ -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
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
return copy.ToList();
|
||||
}
|
||||
|
||||
private record EnumeratorPlayAllCount(IMediaCollectionEnumerator Enumerator, bool PlayAll, int? Count);
|
||||
}
|
||||
|
||||
@@ -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
Normal file
6399
ErsatzTV.Infrastructure.MySql/Migrations/20250912115701_Add_PlaylistItemCount.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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
Normal file
6234
ErsatzTV.Infrastructure.Sqlite/Migrations/20250912132040_Add_PlaylistItemCount.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
b.Property<int>("CollectionType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IncludeInProgramGuide")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<col/>
|
||||
<col/>
|
||||
<col/>
|
||||
<col/>
|
||||
<col style="width: 240px;"/>
|
||||
</MudHidden>
|
||||
</ColGroup>
|
||||
@@ -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 @@
|
||||
</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 @@
|
||||
}
|
||||
</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)
|
||||
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)
|
||||
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)
|
||||
item.SmartCollection?.Id,
|
||||
item.MediaItem?.MediaItemId,
|
||||
item.PlaybackOrder,
|
||||
item.Count,
|
||||
item.PlayAll,
|
||||
item.IncludeInProgramGuide)).ToList();
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
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>
|
||||
|
||||
@@ -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
|
||||
|
||||
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; }
|
||||
|
||||
|
||||
@@ -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
|
||||
MultipleMode = MultipleMode.Count;
|
||||
}
|
||||
|
||||
if (_playbackOrder is not PlaybackOrder.Marathon)
|
||||
{
|
||||
MarathonGroupBy = MarathonGroupBy.None;
|
||||
MarathonShuffleItems = false;
|
||||
MarathonBatchSize = null;
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(CanFillWithGroups));
|
||||
OnPropertyChanged(nameof(MultipleMode));
|
||||
|
||||
Reference in New Issue
Block a user