Add GET /api/guide returning a per-channel EPG grid (ChannelGuideResponseModel). start defaults to now, end defaults to now + XmltvDaysToBuild. Extract the guide-group/filler-merge projection (ChannelGuideProjector) and programme-metadata resolution (ChannelGuideMetadata) plus the metadata eager-load (IncludeGuideMetadata) out of RefreshChannelDataHandler so the XMLTV cache builder and the JSON guide share one source of truth and cannot drift. XMLTV output is unchanged (ChannelGuideGoldenTests pass without regeneration). The JSON handler mirrors XMLTV semantics: ShowInEpg-only channels, decimal channel ordering, ExternalJson playouts skipped, mirror playout offset applied to copies (never mutating the loaded entities). SubTitle/Category are nullable. refs #102 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72 lines
2.7 KiB
C#
72 lines
2.7 KiB
C#
using ErsatzTV.Core.Domain;
|
|
|
|
namespace ErsatzTV.Application.Channels;
|
|
|
|
/// <summary>
|
|
/// Shared programme-metadata projection for guide output. Both the XMLTV cache builder
|
|
/// (<see cref="RefreshChannelDataHandler" />) and the JSON guide query
|
|
/// (<see cref="GetChannelGuideDataHandler" />) resolve the display title/subtitle/category from a
|
|
/// <see cref="PlayoutItem" /> here so the two representations stay consistent.
|
|
/// </summary>
|
|
public static class ChannelGuideMetadata
|
|
{
|
|
public static string GetTitle(PlayoutItem playoutItem)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
|
|
{
|
|
return playoutItem.CustomTitle;
|
|
}
|
|
|
|
return playoutItem.MediaItem switch
|
|
{
|
|
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title ?? string.Empty)
|
|
.IfNone("[unknown movie]"),
|
|
Episode e => e.Season.Show.ShowMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty)
|
|
.IfNone("[unknown show]"),
|
|
MusicVideo mv => mv.Artist.ArtistMetadata.HeadOrNone().Map(am => am.Title ?? string.Empty)
|
|
.IfNone("[unknown artist]"),
|
|
OtherVideo ov => ov.OtherVideoMetadata.HeadOrNone().Map(vm => vm.Title ?? string.Empty)
|
|
.IfNone("[unknown video]"),
|
|
RemoteStream rs => rs.RemoteStreamMetadata.HeadOrNone().Map(vm => vm.Title ?? string.Empty)
|
|
.IfNone("[unknown remote stream]"),
|
|
_ => "[unknown]"
|
|
};
|
|
}
|
|
|
|
public static string GetSubtitle(PlayoutItem playoutItem)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return playoutItem.MediaItem switch
|
|
{
|
|
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
|
|
em => em.Title ?? string.Empty,
|
|
() => string.Empty),
|
|
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match(
|
|
mvm => mvm.Title ?? string.Empty,
|
|
() => string.Empty),
|
|
Song s => s.SongMetadata.HeadOrNone().Match(
|
|
mvm => mvm.Title ?? string.Empty,
|
|
() => string.Empty),
|
|
_ => string.Empty
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// The primary guide category, mirroring the fixed <c><category></c> the XMLTV templates
|
|
/// emit per media kind (Movie / Series / Music). Media kinds without a fixed category return null.
|
|
/// </summary>
|
|
public static string GetCategory(PlayoutItem playoutItem) =>
|
|
playoutItem.MediaItem switch
|
|
{
|
|
Movie => "Movie",
|
|
Episode => "Series",
|
|
MusicVideo => "Music",
|
|
Song => "Music",
|
|
_ => null
|
|
};
|
|
}
|