From 66c8496707391b9e7cefd29b7c4e63e8cb1502b4 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:04:00 -0600 Subject: [PATCH] add marathon group by director --- CHANGELOG.md | 3 ++ ErsatzTV.Core/Domain/MarathonGroupBy.cs | 3 +- .../Scheduling/Engine/MarathonHelper.cs | 42 +++++++++++++++++++ .../Repositories/MediaCollectionRepository.cs | 12 ++++++ ErsatzTV/Pages/ScheduleItemsEditor.razor | 1 + 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 931d23b02..dec484bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add health check to warn about incompatible ffmpeg due to missing filters - This is directly applicable to homebrew `ffmpeg` on MacOS, which is no longer compatible with ErsatzTV - `ffmpeg@7` or `ffmpeg-full` should be used instead +- Add `Marathon Group By` option `Director` + - This groups the *first* director on Movies, Episodes, Music Videos and Other Videos + - This is supported in classic schedules and sequential schedules ### Changed - Disable automatic artwork database cleanup diff --git a/ErsatzTV.Core/Domain/MarathonGroupBy.cs b/ErsatzTV.Core/Domain/MarathonGroupBy.cs index 2c8f1ddc7..10cfc7af3 100644 --- a/ErsatzTV.Core/Domain/MarathonGroupBy.cs +++ b/ErsatzTV.Core/Domain/MarathonGroupBy.cs @@ -6,5 +6,6 @@ public enum MarathonGroupBy Show = 1, Season = 2, Artist = 3, - Album = 4 + Album = 4, + Director = 5 } diff --git a/ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs b/ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs index 20844a456..e7975327c 100644 --- a/ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs +++ b/ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs @@ -39,6 +39,10 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository groups.AddRange(mediaItems.GroupBy(MediaItemKeyByAlbum)); itemPlaybackOrder = marathonShuffleItems ? PlaybackOrder.Shuffle : PlaybackOrder.Chronological; break; + case MarathonGroupBy.Director: + groups.AddRange(mediaItems.GroupBy(MediaItemKeyByDirector)); + itemPlaybackOrder = marathonShuffleItems ? PlaybackOrder.Shuffle : PlaybackOrder.Chronological; + break; default: return Option.None; } @@ -111,6 +115,11 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository { groups.AddRange(allMediaItems.GroupBy(MediaItemKeyByAlbum)); } + // group by (first) director + else if (string.Equals(groupBy, "director", StringComparison.OrdinalIgnoreCase)) + { + groups.AddRange(allMediaItems.GroupBy(MediaItemKeyByDirector)); + } Dictionary> itemMap = []; @@ -189,6 +198,39 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository _ => new GroupKey(CollectionType.Collection, 0, null, null, null) }; + private static GroupKey MediaItemKeyByDirector(MediaItem mediaItem) => + mediaItem switch + { + Movie m => new GroupKey( + CollectionType.Collection, + m.MovieMetadata.HeadOrNone().Map(mm => FirstDirectorHashCode(mm.Directors)).IfNone(0), + null, + null, + null), + Episode e => new GroupKey( + CollectionType.Collection, + e.EpisodeMetadata.HeadOrNone().Map(em => FirstDirectorHashCode(em.Directors)).IfNone(0), + null, + null, + null), + MusicVideo mv => new GroupKey( + CollectionType.Collection, + mv.MusicVideoMetadata.HeadOrNone().Map(mvm => FirstDirectorHashCode(mvm.Directors)).IfNone(0), + null, + null, + null), + OtherVideo ov => new GroupKey( + CollectionType.Collection, + ov.OtherVideoMetadata.HeadOrNone().Map(ovm => FirstDirectorHashCode(ovm.Directors)).IfNone(0), + null, + null, + null), + _ => new GroupKey(CollectionType.Collection, 0, null, null, null) + }; + + private static int FirstDirectorHashCode(List directors) => + directors.HeadOrNone().Select(director => director.Name.GetStableHashCode()).FirstOrDefault(); + private static PlaylistItem GroupToPlaylistItem( int index, bool playAllItems, diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 6330850f0..c1ead51a9 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -1229,9 +1229,12 @@ public class MediaCollectionRepository : IMediaCollectionRepository private static Task> GetMovieItems(TvContext dbContext, IEnumerable movieIds) => dbContext.Movies + .TagWithCallSite() .AsNoTracking() .Include(m => m.MovieMetadata) .ThenInclude(mm => mm.Subtitles) + .Include(m => m.MovieMetadata) + .ThenInclude(mm => mm.Directors) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(m => m.MediaVersions) @@ -1291,6 +1294,7 @@ public class MediaCollectionRepository : IMediaCollectionRepository private static Task> GetMusicVideoItems(TvContext dbContext, IEnumerable musicVideoIds) => dbContext.MusicVideos + .TagWithCallSite() .AsNoTracking() .Include(m => m.Artist) .ThenInclude(a => a.ArtistMetadata) @@ -1298,6 +1302,8 @@ public class MediaCollectionRepository : IMediaCollectionRepository .ThenInclude(mvm => mvm.Subtitles) .Include(m => m.MusicVideoMetadata) .ThenInclude(mvm => mvm.Artists) + .Include(m => m.MusicVideoMetadata) + .ThenInclude(mvm => mvm.Directors) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(m => m.MediaVersions) @@ -1318,9 +1324,12 @@ public class MediaCollectionRepository : IMediaCollectionRepository private static Task> GetOtherVideoItems(TvContext dbContext, IEnumerable otherVideoIds) => dbContext.OtherVideos + .TagWithCallSite() .AsNoTracking() .Include(m => m.OtherVideoMetadata) .ThenInclude(ovm => ovm.Subtitles) + .Include(m => m.OtherVideoMetadata) + .ThenInclude(ovm => ovm.Directors) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(m => m.MediaVersions) @@ -1492,9 +1501,12 @@ public class MediaCollectionRepository : IMediaCollectionRepository private static Task> GetEpisodeItems(TvContext dbContext, IEnumerable episodeIds) => dbContext.Episodes + .TagWithCallSite() .AsNoTracking() .Include(e => e.EpisodeMetadata) .ThenInclude(em => em.Subtitles) + .Include(e => e.EpisodeMetadata) + .ThenInclude(em => em.Directors) .Include(e => e.MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(m => m.MediaVersions) diff --git a/ErsatzTV/Pages/ScheduleItemsEditor.razor b/ErsatzTV/Pages/ScheduleItemsEditor.razor index ab1a6398f..dff1d384f 100644 --- a/ErsatzTV/Pages/ScheduleItemsEditor.razor +++ b/ErsatzTV/Pages/ScheduleItemsEditor.razor @@ -450,6 +450,7 @@ Artist Season Show + Director