Browse Source

add collection type search query (#2598)

pull/2599/head
Jason Dove 2 months ago committed by GitHub
parent
commit
dd38ba19ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 4
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  3. 2
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  4. 15
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  5. 2
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  6. 25
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs
  7. 8
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  8. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  9. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs
  10. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  11. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs
  12. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  13. 1
      ErsatzTV.Core/Domain/CollectionType.cs
  14. 1
      ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs
  15. 2
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  16. 7
      ErsatzTV.Core/Scheduling/CollectionKey.cs
  17. 7
      ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs
  18. 5
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  19. 6864
      ErsatzTV.Infrastructure.MySql/Migrations/20251104180803_Add_CollectionTypeSearchQuery.Designer.cs
  20. 51
      ErsatzTV.Infrastructure.MySql/Migrations/20251104180803_Add_CollectionTypeSearchQuery.cs
  21. 9
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  22. 6691
      ErsatzTV.Infrastructure.Sqlite/Migrations/20251104180529_Add_CollectionTypeSearchQuery.Designer.cs
  23. 48
      ErsatzTV.Infrastructure.Sqlite/Migrations/20251104180529_Add_CollectionTypeSearchQuery.cs
  24. 9
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  25. 26
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  26. 7
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

5
CHANGELOG.md

@ -10,6 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add `MediaItem_Resolution` template data (the current `Resolution` variable is the FFmpeg Profile resolution) - Add `MediaItem_Resolution` template data (the current `Resolution` variable is the FFmpeg Profile resolution)
- Add `MediaItem_Start` template data (DateTimeOffset) - Add `MediaItem_Start` template data (DateTimeOffset)
- Add `MediaItem_Stop` template data (DateTimeOffset) - Add `MediaItem_Stop` template data (DateTimeOffset)
- Classic schedules: add collection type `Search Query`
- This allows defining search queries directly on schedule items without creating smart collections beforehand
- As an example, this can be used to filter or combine existing smart collections
- Filter: `smart_collection:"sd movies" AND plot:"christmas"`
- Combine: `smart_collection:"old commercials" AND smart_collection:"nick promos"`
### Fixed ### Fixed
- Fix HLS Direct playback with Jellyfin 10.11 - Fix HLS Direct playback with Jellyfin 10.11

4
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs

@ -17,6 +17,8 @@ public record AddProgramScheduleItem(
int? RerunCollectionId, int? RerunCollectionId,
int? MediaItemId, int? MediaItemId,
int? PlaylistId, int? PlaylistId,
string SearchTitle,
string SearchQuery,
PlaybackOrder PlaybackOrder, PlaybackOrder PlaybackOrder,
MarathonGroupBy MarathonGroupBy, MarathonGroupBy MarathonGroupBy,
bool MarathonShuffleGroups, bool MarathonShuffleGroups,
@ -57,6 +59,8 @@ public record AddProgramScheduleItem(
RerunCollectionId: null, RerunCollectionId: null,
MediaItemId: mediaItemId, MediaItemId: mediaItemId,
PlaylistId: null, PlaylistId: null,
SearchTitle: null,
SearchQuery: null,
PlaybackOrder.Shuffle, PlaybackOrder.Shuffle,
MarathonGroupBy.None, MarathonGroupBy.None,
MarathonShuffleGroups: false, MarathonShuffleGroups: false,

2
ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs

@ -14,6 +14,8 @@ public interface IProgramScheduleItemRequest
int? RerunCollectionId { get; } int? RerunCollectionId { get; }
int? MediaItemId { get; } int? MediaItemId { get; }
int? PlaylistId { get; } int? PlaylistId { get; }
string SearchTitle { get; }
string SearchQuery { get; }
PlayoutMode PlayoutMode { get; } PlayoutMode PlayoutMode { get; }
PlaybackOrder PlaybackOrder { get; } PlaybackOrder PlaybackOrder { get; }
MarathonGroupBy MarathonGroupBy { get; } MarathonGroupBy MarathonGroupBy { get; }

15
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -188,6 +188,13 @@ public abstract class ProgramScheduleItemCommandBase
return BaseError.New("[Playlist] is required for collection type 'Playlist'"); return BaseError.New("[Playlist] is required for collection type 'Playlist'");
} }
break;
case CollectionType.SearchQuery:
if (string.IsNullOrWhiteSpace(item.SearchQuery))
{
return BaseError.New("[SearchQuery] is required for collection type 'SearchQuery'");
}
break; break;
default: default:
return BaseError.New("[CollectionType] is invalid"); return BaseError.New("[CollectionType] is invalid");
@ -216,6 +223,8 @@ public abstract class ProgramScheduleItemCommandBase
RerunCollectionId = item.RerunCollectionId, RerunCollectionId = item.RerunCollectionId,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
SearchTitle = item.SearchTitle,
SearchQuery = item.SearchQuery,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy, MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups, MarathonShuffleGroups = item.MarathonShuffleGroups,
@ -247,6 +256,8 @@ public abstract class ProgramScheduleItemCommandBase
RerunCollectionId = item.RerunCollectionId, RerunCollectionId = item.RerunCollectionId,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
SearchTitle = item.SearchTitle,
SearchQuery = item.SearchQuery,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy, MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups, MarathonShuffleGroups = item.MarathonShuffleGroups,
@ -278,6 +289,8 @@ public abstract class ProgramScheduleItemCommandBase
RerunCollectionId = item.RerunCollectionId, RerunCollectionId = item.RerunCollectionId,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
SearchTitle = item.SearchTitle,
SearchQuery = item.SearchQuery,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy, MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups, MarathonShuffleGroups = item.MarathonShuffleGroups,
@ -311,6 +324,8 @@ public abstract class ProgramScheduleItemCommandBase
RerunCollectionId = item.RerunCollectionId, RerunCollectionId = item.RerunCollectionId,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
SearchTitle = item.SearchTitle,
SearchQuery = item.SearchQuery,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy, MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups, MarathonShuffleGroups = item.MarathonShuffleGroups,

2
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs

@ -17,6 +17,8 @@ public record ReplaceProgramScheduleItem(
int? RerunCollectionId, int? RerunCollectionId,
int? MediaItemId, int? MediaItemId,
int? PlaylistId, int? PlaylistId,
string SearchTitle,
string SearchQuery,
PlaybackOrder PlaybackOrder, PlaybackOrder PlaybackOrder,
MarathonGroupBy MarathonGroupBy, MarathonGroupBy MarathonGroupBy,
bool MarathonShuffleGroups, bool MarathonShuffleGroups,

25
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs

@ -9,25 +9,16 @@ using static ErsatzTV.Application.ProgramSchedules.Mapper;
namespace ErsatzTV.Application.ProgramSchedules; namespace ErsatzTV.Application.ProgramSchedules;
public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase, public class ReplaceProgramScheduleItemsHandler(
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> channel) : ProgramScheduleItemCommandBase,
IRequestHandler<ReplaceProgramScheduleItems, Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>> IRequestHandler<ReplaceProgramScheduleItems, Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public ReplaceProgramScheduleItemsHandler(
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_channel = channel;
}
public async Task<Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>> Handle( public async Task<Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>> Handle(
ReplaceProgramScheduleItems request, ReplaceProgramScheduleItems request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request, cancellationToken); Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request, cancellationToken);
return await validation.Apply(ps => PersistItems(dbContext, request, ps, cancellationToken)); return await validation.Apply(ps => PersistItems(dbContext, request, ps, cancellationToken));
} }
@ -53,7 +44,7 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase
// refresh any playouts that use this schedule // refresh any playouts that use this schedule
foreach (Playout playout in programSchedule.Playouts) foreach (Playout playout in programSchedule.Playouts)
{ {
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh), cancellationToken); await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh), cancellationToken);
} }
return programSchedule.Items.Map(ProjectToViewModel); return programSchedule.Items.Map(ProjectToViewModel);
@ -121,7 +112,8 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase
item.MultiCollectionId, item.MultiCollectionId,
item.SmartCollectionId, item.SmartCollectionId,
item.RerunCollectionId, item.RerunCollectionId,
item.PlaylistId); item.PlaylistId,
item.SearchQuery);
if (keyOrders.TryGetValue(key, out System.Collections.Generic.HashSet<PlaybackOrder> playbackOrders)) if (keyOrders.TryGetValue(key, out System.Collections.Generic.HashSet<PlaybackOrder> playbackOrders))
{ {
@ -147,5 +139,6 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase
int? MultiCollectionId, int? MultiCollectionId,
int? SmartCollectionId, int? SmartCollectionId,
int? RerunCollectionId, int? RerunCollectionId,
int? PlaylistId); int? PlaylistId,
string SearchQuery);
} }

8
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -47,6 +47,8 @@ internal static class Mapper
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
_ => null _ => null
}, },
duration.SearchTitle,
duration.SearchQuery,
duration.PlaybackOrder, duration.PlaybackOrder,
duration.MarathonGroupBy, duration.MarathonGroupBy,
duration.MarathonShuffleGroups, duration.MarathonShuffleGroups,
@ -111,6 +113,8 @@ internal static class Mapper
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
_ => null _ => null
}, },
flood.SearchTitle,
flood.SearchQuery,
flood.PlaybackOrder, flood.PlaybackOrder,
flood.MarathonGroupBy, flood.MarathonGroupBy,
flood.MarathonShuffleGroups, flood.MarathonShuffleGroups,
@ -172,6 +176,8 @@ internal static class Mapper
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
_ => null _ => null
}, },
multiple.SearchTitle,
multiple.SearchQuery,
multiple.PlaybackOrder, multiple.PlaybackOrder,
multiple.MarathonGroupBy, multiple.MarathonGroupBy,
multiple.MarathonShuffleGroups, multiple.MarathonShuffleGroups,
@ -235,6 +241,8 @@ internal static class Mapper
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
_ => null _ => null
}, },
one.SearchTitle,
one.SearchQuery,
one.PlaybackOrder, one.PlaybackOrder,
one.MarathonGroupBy, one.MarathonGroupBy,
one.MarathonShuffleGroups, one.MarathonShuffleGroups,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -23,6 +23,8 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
RerunCollectionViewModel rerunCollection, RerunCollectionViewModel rerunCollection,
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
string searchTitle,
string searchQuery,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
MarathonGroupBy marathonGroupBy, MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups, bool marathonShuffleGroups,
@ -58,6 +60,8 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
rerunCollection, rerunCollection,
playlist, playlist,
mediaItem, mediaItem,
searchTitle,
searchQuery,
playbackOrder, playbackOrder,
marathonGroupBy, marathonGroupBy,
marathonShuffleGroups, marathonShuffleGroups,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -23,6 +23,8 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
RerunCollectionViewModel rerunCollection, RerunCollectionViewModel rerunCollection,
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
string searchTitle,
string searchQuery,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
MarathonGroupBy marathonGroupBy, MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups, bool marathonShuffleGroups,
@ -55,6 +57,8 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
rerunCollection, rerunCollection,
playlist, playlist,
mediaItem, mediaItem,
searchTitle,
searchQuery,
playbackOrder, playbackOrder,
marathonGroupBy, marathonGroupBy,
marathonShuffleGroups, marathonShuffleGroups,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -23,6 +23,8 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
RerunCollectionViewModel rerunCollection, RerunCollectionViewModel rerunCollection,
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
string searchTitle,
string searchQuery,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
MarathonGroupBy marathonGroupBy, MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups, bool marathonShuffleGroups,
@ -57,6 +59,8 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
rerunCollection, rerunCollection,
playlist, playlist,
mediaItem, mediaItem,
searchTitle,
searchQuery,
playbackOrder, playbackOrder,
marathonGroupBy, marathonGroupBy,
marathonShuffleGroups, marathonShuffleGroups,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -23,6 +23,8 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
RerunCollectionViewModel rerunCollection, RerunCollectionViewModel rerunCollection,
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
string searchTitle,
string searchQuery,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
MarathonGroupBy marathonGroupBy, MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups, bool marathonShuffleGroups,
@ -55,6 +57,8 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
rerunCollection, rerunCollection,
playlist, playlist,
mediaItem, mediaItem,
searchTitle,
searchQuery,
playbackOrder, playbackOrder,
marathonGroupBy, marathonGroupBy,
marathonShuffleGroups, marathonShuffleGroups,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -22,6 +22,8 @@ public abstract record ProgramScheduleItemViewModel(
RerunCollectionViewModel RerunCollection, RerunCollectionViewModel RerunCollection,
PlaylistViewModel Playlist, PlaylistViewModel Playlist,
NamedMediaItemViewModel MediaItem, NamedMediaItemViewModel MediaItem,
string SearchTitle,
string SearchQuery,
PlaybackOrder PlaybackOrder, PlaybackOrder PlaybackOrder,
MarathonGroupBy MarathonGroupBy, MarathonGroupBy MarathonGroupBy,
bool MarathonShuffleGroups, bool MarathonShuffleGroups,
@ -55,6 +57,8 @@ public abstract record ProgramScheduleItemViewModel(
MultiCollection?.Name, MultiCollection?.Name,
CollectionType.SmartCollection => CollectionType.SmartCollection =>
SmartCollection?.Name, SmartCollection?.Name,
CollectionType.SearchQuery =>
string.IsNullOrWhiteSpace(SearchTitle) ? SearchQuery : SearchTitle,
CollectionType.Playlist => CollectionType.Playlist =>
Playlist?.Name, Playlist?.Name,
CollectionType.RerunFirstRun or CollectionType.RerunRerun => CollectionType.RerunFirstRun or CollectionType.RerunRerun =>

1
ErsatzTV.Core/Domain/CollectionType.cs

@ -11,6 +11,7 @@ public enum CollectionType
Playlist = 6, Playlist = 6,
RerunFirstRun = 7, RerunFirstRun = 7,
RerunRerun = 8, RerunRerun = 8,
SearchQuery = 9,
Movie = 10, Movie = 10,
Episode = 20, Episode = 20,

1
ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs

@ -29,6 +29,7 @@ public class PlayoutProgramScheduleAnchor
public MediaItem MediaItem { get; set; } public MediaItem MediaItem { get; set; }
public int? PlaylistId { get; set; } public int? PlaylistId { get; set; }
public Playlist Playlist { get; set; } public Playlist Playlist { get; set; }
public string SearchQuery { get; set; }
public string FakeCollectionKey { get; set; } public string FakeCollectionKey { get; set; }
public CollectionEnumeratorState EnumeratorState { get; set; } public CollectionEnumeratorState EnumeratorState { get; set; }
} }

2
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -27,6 +27,8 @@ public abstract class ProgramScheduleItem
public MultiCollection MultiCollection { get; set; } public MultiCollection MultiCollection { get; set; }
public int? SmartCollectionId { get; set; } public int? SmartCollectionId { get; set; }
public SmartCollection SmartCollection { get; set; } public SmartCollection SmartCollection { get; set; }
public string SearchTitle { get; set; }
public string SearchQuery { get; set; }
public int? RerunCollectionId { get; set; } public int? RerunCollectionId { get; set; }
public RerunCollection RerunCollection { get; set; } public RerunCollection RerunCollection { get; set; }
public int? PlaylistId { get; set; } public int? PlaylistId { get; set; }

7
ErsatzTV.Core/Scheduling/CollectionKey.cs

@ -13,6 +13,7 @@ public class CollectionKey : Record<CollectionKey>
public int? RerunCollectionId { get; set; } public int? RerunCollectionId { get; set; }
public int? MediaItemId { get; set; } public int? MediaItemId { get; set; }
public int? PlaylistId { get; set; } public int? PlaylistId { get; set; }
public string SearchQuery { get; set; }
public string FakeCollectionKey { get; set; } public string FakeCollectionKey { get; set; }
public static CollectionKey ForPlaylistItem(PlaylistItem item) => public static CollectionKey ForPlaylistItem(PlaylistItem item) =>
@ -362,6 +363,12 @@ public class CollectionKey : Record<CollectionKey>
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
FakeCollectionKey = item.FakeCollectionKey FakeCollectionKey = item.FakeCollectionKey
}, },
CollectionType.SearchQuery => new CollectionKey
{
CollectionType = item.CollectionType,
SearchQuery = item.SearchQuery,
FakeCollectionKey = item.FakeCollectionKey
},
CollectionType.FakeCollection => new CollectionKey CollectionType.FakeCollection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,

7
ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs

@ -52,6 +52,13 @@ public static class MediaItemsForCollection
result.AddRange( result.AddRange(
await mediaCollectionRepository.GetPlaylistItems(collectionKey.PlaylistId ?? 0, cancellationToken)); await mediaCollectionRepository.GetPlaylistItems(collectionKey.PlaylistId ?? 0, cancellationToken));
break; break;
case CollectionType.SearchQuery:
result.AddRange(
await mediaCollectionRepository.GetSmartCollectionItems(
collectionKey.SearchQuery,
string.Empty,
cancellationToken));
break;
case CollectionType.Movie: case CollectionType.Movie:
foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) foreach (int mediaItemId in Optional(collectionKey.MediaItemId))
{ {

5
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -1214,6 +1214,7 @@ public class PlayoutBuilder : IPlayoutBuilder
&& a.RerunCollectionId == collectionKey.RerunCollectionId && a.RerunCollectionId == collectionKey.RerunCollectionId
&& a.MultiCollectionId == collectionKey.MultiCollectionId && a.MultiCollectionId == collectionKey.MultiCollectionId
&& a.PlaylistId == collectionKey.PlaylistId && a.PlaylistId == collectionKey.PlaylistId
&& a.SearchQuery == collectionKey.SearchQuery
&& a.AnchorDate is null); && a.AnchorDate is null);
var maybeEnumeratorState = collectionEnumerators.ToDictionary(e => e.Key, e => e.Value.State); var maybeEnumeratorState = collectionEnumerators.ToDictionary(e => e.Key, e => e.Value.State);
@ -1235,6 +1236,7 @@ public class PlayoutBuilder : IPlayoutBuilder
RerunCollectionId = collectionKey.RerunCollectionId, RerunCollectionId = collectionKey.RerunCollectionId,
MediaItemId = collectionKey.MediaItemId, MediaItemId = collectionKey.MediaItemId,
PlaylistId = collectionKey.PlaylistId, PlaylistId = collectionKey.PlaylistId,
SearchQuery = collectionKey.SearchQuery,
FakeCollectionKey = collectionKey.FakeCollectionKey, FakeCollectionKey = collectionKey.FakeCollectionKey,
EnumeratorState = maybeEnumeratorState[collectionKey] EnumeratorState = maybeEnumeratorState[collectionKey]
}); });
@ -1277,7 +1279,8 @@ public class PlayoutBuilder : IPlayoutBuilder
&& a.SmartCollectionId == collectionKey.SmartCollectionId && a.SmartCollectionId == collectionKey.SmartCollectionId
&& a.RerunCollectionId == collectionKey.RerunCollectionId && a.RerunCollectionId == collectionKey.RerunCollectionId
&& a.MediaItemId == collectionKey.MediaItemId && a.MediaItemId == collectionKey.MediaItemId
&& a.PlaylistId == collectionKey.PlaylistId); && a.PlaylistId == collectionKey.PlaylistId
&& a.SearchQuery == collectionKey.SearchQuery);
CollectionEnumeratorState state = null; CollectionEnumeratorState state = null;

6864
ErsatzTV.Infrastructure.MySql/Migrations/20251104180803_Add_CollectionTypeSearchQuery.Designer.cs generated

File diff suppressed because it is too large Load Diff

51
ErsatzTV.Infrastructure.MySql/Migrations/20251104180803_Add_CollectionTypeSearchQuery.cs

@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_CollectionTypeSearchQuery : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SearchQuery",
table: "ProgramScheduleItem",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "SearchTitle",
table: "ProgramScheduleItem",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "SearchQuery",
table: "PlayoutProgramScheduleAnchor",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SearchQuery",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "SearchTitle",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "SearchQuery",
table: "PlayoutProgramScheduleAnchor");
}
}
}

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

@ -2104,6 +2104,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int?>("RerunCollectionId") b.Property<int?>("RerunCollectionId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("SearchQuery")
.HasColumnType("longtext");
b.Property<int?>("SmartCollectionId") b.Property<int?>("SmartCollectionId")
.HasColumnType("int"); .HasColumnType("int");
@ -2371,6 +2374,12 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int?>("RerunCollectionId") b.Property<int?>("RerunCollectionId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("SearchQuery")
.HasColumnType("longtext");
b.Property<string>("SearchTitle")
.HasColumnType("longtext");
b.Property<int?>("SmartCollectionId") b.Property<int?>("SmartCollectionId")
.HasColumnType("int"); .HasColumnType("int");

6691
ErsatzTV.Infrastructure.Sqlite/Migrations/20251104180529_Add_CollectionTypeSearchQuery.Designer.cs generated

File diff suppressed because it is too large Load Diff

48
ErsatzTV.Infrastructure.Sqlite/Migrations/20251104180529_Add_CollectionTypeSearchQuery.cs

@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_CollectionTypeSearchQuery : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SearchQuery",
table: "ProgramScheduleItem",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SearchTitle",
table: "ProgramScheduleItem",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SearchQuery",
table: "PlayoutProgramScheduleAnchor",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SearchQuery",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "SearchTitle",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "SearchQuery",
table: "PlayoutProgramScheduleAnchor");
}
}
}

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

@ -2009,6 +2009,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int?>("RerunCollectionId") b.Property<int?>("RerunCollectionId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("SearchQuery")
.HasColumnType("TEXT");
b.Property<int?>("SmartCollectionId") b.Property<int?>("SmartCollectionId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -2262,6 +2265,12 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int?>("RerunCollectionId") b.Property<int?>("RerunCollectionId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("SearchQuery")
.HasColumnType("TEXT");
b.Property<string>("SearchTitle")
.HasColumnType("TEXT");
b.Property<int?>("SmartCollectionId") b.Property<int?>("SmartCollectionId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

26
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -195,6 +195,7 @@
<MudSelectItem Value="CollectionType.Artist">Artist</MudSelectItem> <MudSelectItem Value="CollectionType.Artist">Artist</MudSelectItem>
<MudSelectItem Value="CollectionType.MultiCollection">Multi Collection</MudSelectItem> <MudSelectItem Value="CollectionType.MultiCollection">Multi Collection</MudSelectItem>
<MudSelectItem Value="CollectionType.SmartCollection">Smart Collection</MudSelectItem> <MudSelectItem Value="CollectionType.SmartCollection">Smart Collection</MudSelectItem>
<MudSelectItem Value="CollectionType.SearchQuery">Search Query</MudSelectItem>
<MudSelectItem Value="CollectionType.Playlist">Playlist</MudSelectItem> <MudSelectItem Value="CollectionType.Playlist">Playlist</MudSelectItem>
<MudSelectItem Value="CollectionType.RerunFirstRun">Rerun (First Run)</MudSelectItem> <MudSelectItem Value="CollectionType.RerunFirstRun">Rerun (First Run)</MudSelectItem>
<MudSelectItem Value="CollectionType.RerunRerun">Rerun (Rerun)</MudSelectItem> <MudSelectItem Value="CollectionType.RerunRerun">Rerun (Rerun)</MudSelectItem>
@ -338,6 +339,24 @@
</MudStack> </MudStack>
} }
@if (_selectedItem.CollectionType == CollectionType.SearchQuery)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Search Title</MudText>
</div>
<MudTextField @bind-Value="@_selectedItem.SearchTitle"
For="@(() => _selectedItem.SearchTitle)"
HelperText="Optional title that will be displayed in schedule items tables"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Search Query</MudText>
</div>
<MudTextField @bind-Value="@_selectedItem.SearchQuery" For="@(() => _selectedItem.SearchQuery)"/>
</MudStack>
}
@if (_selectedItem.CollectionType == CollectionType.RerunFirstRun) @if (_selectedItem.CollectionType == CollectionType.RerunFirstRun)
{ {
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
@ -389,6 +408,7 @@
break; break;
case CollectionType.Collection: case CollectionType.Collection:
case CollectionType.SmartCollection: case CollectionType.SmartCollection:
case CollectionType.SearchQuery:
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
@ -904,6 +924,8 @@
Collection = item.Collection, Collection = item.Collection,
MultiCollection = item.MultiCollection, MultiCollection = item.MultiCollection,
SmartCollection = item.SmartCollection, SmartCollection = item.SmartCollection,
SearchTitle = item.SearchTitle,
SearchQuery = item.SearchQuery,
RerunCollection = item.RerunCollection, RerunCollection = item.RerunCollection,
Playlist = item.Playlist, Playlist = item.Playlist,
MediaItem = item.MediaItem, MediaItem = item.MediaItem,
@ -980,6 +1002,8 @@
Collection = item.Collection, Collection = item.Collection,
MultiCollection = item.MultiCollection, MultiCollection = item.MultiCollection,
SmartCollection = item.SmartCollection, SmartCollection = item.SmartCollection,
SearchTitle = item.SearchTitle,
SearchQuery = item.SearchQuery,
Playlist = item.Playlist, Playlist = item.Playlist,
MediaItem = item.MediaItem, MediaItem = item.MediaItem,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
@ -1057,6 +1081,8 @@
item.RerunCollection?.Id, item.RerunCollection?.Id,
item.MediaItem?.MediaItemId, item.MediaItem?.MediaItemId,
item.Playlist?.Id, item.Playlist?.Id,
item.SearchTitle,
item.SearchQuery,
item.PlaybackOrder, item.PlaybackOrder,
item.MarathonGroupBy, item.MarathonGroupBy,
item.MarathonShuffleGroups, item.MarathonShuffleGroups,

7
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -62,6 +62,8 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
MultiCollection = null; MultiCollection = null;
MediaItem = null; MediaItem = null;
SmartCollection = null; SmartCollection = null;
SearchTitle = null;
SearchQuery = null;
RerunCollection = null; RerunCollection = null;
if (_collectionType != CollectionType.Playlist && if (_collectionType != CollectionType.Playlist &&
@ -81,6 +83,8 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
OnPropertyChanged(nameof(MultiCollection)); OnPropertyChanged(nameof(MultiCollection));
OnPropertyChanged(nameof(MediaItem)); OnPropertyChanged(nameof(MediaItem));
OnPropertyChanged(nameof(SmartCollection)); OnPropertyChanged(nameof(SmartCollection));
OnPropertyChanged(nameof(SearchTitle));
OnPropertyChanged(nameof(SearchQuery));
OnPropertyChanged(nameof(RerunCollection)); OnPropertyChanged(nameof(RerunCollection));
OnPropertyChanged(nameof(MultiCollection)); OnPropertyChanged(nameof(MultiCollection));
OnPropertyChanged(nameof(PlaybackOrder)); OnPropertyChanged(nameof(PlaybackOrder));
@ -99,6 +103,8 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
public RerunCollectionViewModel RerunCollection { get; set; } public RerunCollectionViewModel RerunCollection { get; set; }
public NamedMediaItemViewModel MediaItem { get; set; } public NamedMediaItemViewModel MediaItem { get; set; }
public PlaylistViewModel Playlist { get; set; } public PlaylistViewModel Playlist { get; set; }
public string SearchTitle { get; set; }
public string SearchQuery { get; set; }
public FillerPresetViewModel PreRollFiller { get; set; } public FillerPresetViewModel PreRollFiller { get; set; }
public FillerPresetViewModel MidRollFiller { get; set; } public FillerPresetViewModel MidRollFiller { get; set; }
public FillerPresetViewModel PostRollFiller { get; set; } public FillerPresetViewModel PostRollFiller { get; set; }
@ -121,6 +127,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
CollectionType.SmartCollection => SmartCollection?.Name, CollectionType.SmartCollection => SmartCollection?.Name,
CollectionType.Playlist => Playlist?.Name, CollectionType.Playlist => Playlist?.Name,
CollectionType.RerunFirstRun or CollectionType.RerunRerun => RerunCollection?.Name, CollectionType.RerunFirstRun or CollectionType.RerunRerun => RerunCollection?.Name,
CollectionType.SearchQuery => string.IsNullOrWhiteSpace(SearchTitle) ? SearchQuery : SearchTitle,
_ => string.Empty _ => string.Empty
}; };

Loading…
Cancel
Save