private async Task <string> GetPlayoutItemPath(PlayoutItem playoutItem) { MediaVersion version = playoutItem.MediaItem.GetHeadVersion(); MediaFile file = version.MediaFiles.Head(); string path = file.Path; return(playoutItem.MediaItem switch { PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath( plexMovie.LibraryPathId, path), PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath( plexEpisode.LibraryPathId, path), JellyfinMovie jellyfinMovie => await _jellyfinPathReplacementService.GetReplacementJellyfinPath( jellyfinMovie.LibraryPathId, path), JellyfinEpisode jellyfinEpisode => await _jellyfinPathReplacementService.GetReplacementJellyfinPath( jellyfinEpisode.LibraryPathId, path), EmbyMovie embyMovie => await _embyPathReplacementService.GetReplacementEmbyPath( embyMovie.LibraryPathId, path), EmbyEpisode embyEpisode => await _embyPathReplacementService.GetReplacementEmbyPath( embyEpisode.LibraryPathId, path), _ => path });
private long GetIndexForChannel(Channel channel, PlayoutItem playoutItem) { long ticks = playoutItem.Start.Ticks; var key = new ChannelIndexKey(channel.Id); long index; if (_memoryCache.TryGetValue(key, out ChannelIndexRecord channelRecord)) { if (channelRecord.StartTicks == ticks) { index = channelRecord.Index; } else { index = channelRecord.Index + 1; _memoryCache.Set(key, new ChannelIndexRecord(ticks, index), TimeSpan.FromDays(1)); } } else { index = 1; _memoryCache.Set(key, new ChannelIndexRecord(ticks, index), TimeSpan.FromDays(1)); } return(index); }
private async Task <string> GetPlayoutItemPath(PlayoutItem playoutItem) { MediaVersion version = playoutItem.MediaItem switch { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(playoutItem)) }; MediaFile file = version.MediaFiles.Head(); string path = file.Path; return(playoutItem.MediaItem switch { PlexMovie plexMovie => await GetReplacementPlexPath(plexMovie.LibraryPathId, path), PlexEpisode plexEpisode => await GetReplacementPlexPath(plexEpisode.LibraryPathId, path), _ => path });
private 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]"), Song s => s.SongMetadata.HeadOrNone().Map(sm => sm.Artist ?? string.Empty) .IfNone("[unknown artist]"), _ => "[unknown]" });
private async Task <Either <BaseError, Unit> > ExtractAll( TvContext dbContext, ExtractEmbeddedSubtitles request, string ffmpegPath, CancellationToken cancellationToken) { try { DateTime now = DateTime.UtcNow; DateTime until = now.AddHours(1); var playoutIdsToCheck = new List <int>(); // only check the requested playout if subtitles are enabled Option <Playout> requestedPlayout = await dbContext.Playouts .Filter(p => p.Channel.SubtitleMode != ChannelSubtitleMode.None) .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId.IfNone(-1)); playoutIdsToCheck.AddRange(requestedPlayout.Map(p => p.Id)); // check all playouts (that have subtitles enabled) if none were passed if (request.PlayoutId.IsNone) { playoutIdsToCheck = dbContext.Playouts .Filter(p => p.Channel.SubtitleMode != ChannelSubtitleMode.None) .Map(p => p.Id) .ToList(); } if (playoutIdsToCheck.Count == 0) { foreach (int playoutId in request.PlayoutId) { _logger.LogDebug( "Playout {PlayoutId} does not have subtitles enabled; nothing to extract", playoutId); return(Unit.Default); } _logger.LogDebug("No playouts have subtitles enabled; nothing to extract"); return(Unit.Default); } _logger.LogDebug("Checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck); // find all playout items in the next hour List <PlayoutItem> playoutItems = await dbContext.PlayoutItems .Filter(pi => playoutIdsToCheck.Contains(pi.PlayoutId)) .Filter(pi => pi.Finish >= DateTime.UtcNow) .Filter(pi => pi.Start <= until) .ToListAsync(cancellationToken); // TODO: support other media kinds (movies, other videos, etc) var mediaItemIds = playoutItems.Map(pi => pi.MediaItemId).ToList(); // filter for subtitles that need extraction List <int> unextractedMediaItemIds = await GetUnextractedMediaItemIds(dbContext, mediaItemIds, cancellationToken); if (unextractedMediaItemIds.Any()) { _logger.LogDebug( "Found media items {MediaItemIds} with text subtitles to extract for playouts {PlayoutIds}", unextractedMediaItemIds, playoutIdsToCheck); } else { _logger.LogDebug("Found no text subtitles to extract for playouts {PlayoutIds}", playoutIdsToCheck); } // sort by start time var toUpdate = playoutItems .Filter(pi => pi.Finish >= DateTime.UtcNow) .DistinctBy(pi => pi.MediaItemId) .Filter(pi => unextractedMediaItemIds.Contains(pi.MediaItemId)) .OrderBy(pi => pi.StartOffset) .Map(pi => pi.MediaItemId) .ToList(); foreach (int mediaItemId in toUpdate) { if (cancellationToken.IsCancellationRequested) { return(Unit.Default); } PlayoutItem pi = playoutItems.Find(pi => pi.MediaItemId == mediaItemId); _logger.LogDebug("Extracting subtitles for item with start time {StartTime}", pi?.StartOffset); // extract subtitles and fonts for each item and update db await ExtractSubtitles(dbContext, mediaItemId, ffmpegPath, cancellationToken); // await ExtractFonts(dbContext, episodeId, ffmpegPath, cancellationToken); } return(Unit.Default); } catch (TaskCanceledException) { return(Unit.Default); } }
private async Task <Either <BaseError, PlayoutItemWithPath> > ValidatePlayoutItemPath(PlayoutItem playoutItem) { string path = await GetPlayoutItemPath(playoutItem); // TODO: this won't work with url streaming from plex if (_localFileSystem.FileExists(path)) { return(new PlayoutItemWithPath(playoutItem, path)); } return(new PlayoutItemDoesNotExistOnDisk(path)); }
internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
public override Tuple <PlayoutBuilderState, List <PlayoutItem> > Schedule( PlayoutBuilderState playoutBuilderState, Dictionary <CollectionKey, IMediaCollectionEnumerator> collectionEnumerators, ProgramScheduleItemOne scheduleItem, ProgramScheduleItem nextScheduleItem, DateTimeOffset hardStop) { IMediaCollectionEnumerator contentEnumerator = collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)]; foreach (MediaItem mediaItem in contentEnumerator.Current) { // find when we should start this item, based on the current time DateTimeOffset itemStartTime = GetStartTimeAfter( playoutBuilderState, scheduleItem); TimeSpan itemDuration = DurationForMediaItem(mediaItem); List <MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem); var playoutItem = new PlayoutItem { MediaItemId = mediaItem.Id, Start = itemStartTime.UtcDateTime, Finish = itemStartTime.UtcDateTime + itemDuration, InPoint = TimeSpan.Zero, OutPoint = itemDuration, GuideGroup = playoutBuilderState.NextGuideGroup, FillerKind = scheduleItem.GuideMode == GuideMode.Filler ? FillerKind.Tail : FillerKind.None, WatermarkId = scheduleItem.WatermarkId, PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, SubtitleMode = scheduleItem.SubtitleMode }; DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller( collectionEnumerators, scheduleItem, itemStartTime, itemDuration, itemChapters); List <PlayoutItem> playoutItems = AddFiller( playoutBuilderState, collectionEnumerators, scheduleItem, playoutItem, itemChapters); PlayoutBuilderState nextState = playoutBuilderState with { CurrentTime = itemEndTimeWithFiller }; nextState.ScheduleItemsEnumerator.MoveNext(); contentEnumerator.MoveNext(); // LogScheduledItem(scheduleItem, mediaItem, itemStartTime); // only play one item from collection, so always advance to the next item // _logger.LogDebug( // "Advancing to next schedule item after playout mode {PlayoutMode}", // "One"); DateTimeOffset nextItemStart = GetStartTimeAfter(nextState, nextScheduleItem); if (scheduleItem.TailFiller != null) { (nextState, playoutItems) = AddTailFiller( nextState, collectionEnumerators, scheduleItem, playoutItems, nextItemStart); } if (scheduleItem.FallbackFiller != null) { (nextState, playoutItems) = AddFallbackFiller( nextState, collectionEnumerators, scheduleItem, playoutItems, nextItemStart); } nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup }; return(Tuple(nextState, playoutItems)); } return(Tuple(playoutBuilderState, new List <PlayoutItem>())); } }
public override Tuple <PlayoutBuilderState, List <PlayoutItem> > Schedule( PlayoutBuilderState playoutBuilderState, Dictionary <CollectionKey, IMediaCollectionEnumerator> collectionEnumerators, ProgramScheduleItemFlood scheduleItem, ProgramScheduleItem nextScheduleItem, DateTimeOffset hardStop) { var playoutItems = new List <PlayoutItem>(); PlayoutBuilderState nextState = playoutBuilderState; var willFinishInTime = true; IMediaCollectionEnumerator contentEnumerator = collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)]; ProgramScheduleItem peekScheduleItem = nextScheduleItem; while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime) { MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe(); // find when we should start this item, based on the current time DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem); TimeSpan itemDuration = DurationForMediaItem(mediaItem); List <MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem); var playoutItem = new PlayoutItem { MediaItemId = mediaItem.Id, Start = itemStartTime.UtcDateTime, Finish = itemStartTime.UtcDateTime + itemDuration, InPoint = TimeSpan.Zero, OutPoint = itemDuration, GuideGroup = nextState.NextGuideGroup, FillerKind = scheduleItem.GuideMode == GuideMode.Filler ? FillerKind.Tail : FillerKind.None, WatermarkId = scheduleItem.WatermarkId, PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, SubtitleMode = scheduleItem.SubtitleMode }; DateTimeOffset peekScheduleItemStart = peekScheduleItem.StartType == StartType.Fixed ? GetStartTimeAfter(nextState with { InFlood = false }, peekScheduleItem) : DateTimeOffset.MaxValue; DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller( collectionEnumerators, scheduleItem, itemStartTime, itemDuration, itemChapters); // if the next schedule item is supposed to start during this item, // don't schedule this item and just move on willFinishInTime = peekScheduleItemStart < itemStartTime || peekScheduleItemStart >= itemEndTimeWithFiller; if (willFinishInTime) { playoutItems.AddRange( AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters)); // LogScheduledItem(scheduleItem, mediaItem, itemStartTime); DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset); if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1) { _logger.LogWarning( "Filler prediction failure: predicted {PredictedDuration} doesn't match actual {ActualDuration}", itemEndTimeWithFiller, actualEndTime); // _logger.LogWarning("Playout items: {@PlayoutItems}", playoutItems); } nextState = nextState with { CurrentTime = itemEndTimeWithFiller, InFlood = true, NextGuideGroup = nextState.IncrementGuideGroup }; contentEnumerator.MoveNext(); } } // _logger.LogDebug( // "Advancing to next schedule item after playout mode {PlayoutMode}", // "Flood"); nextState = nextState with { InFlood = nextState.CurrentTime >= hardStop, NextGuideGroup = nextState.DecrementGuideGroup }; nextState.ScheduleItemsEnumerator.MoveNext(); ProgramScheduleItem peekItem = nextScheduleItem; DateTimeOffset peekItemStart = GetStartTimeAfter(nextState, peekItem); if (scheduleItem.TailFiller != null) { (nextState, playoutItems) = AddTailFiller( nextState, collectionEnumerators, scheduleItem, playoutItems, peekItemStart); } if (scheduleItem.FallbackFiller != null) { (nextState, playoutItems) = AddFallbackFiller( nextState, collectionEnumerators, scheduleItem, playoutItems, peekItemStart); } nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup }; return(Tuple(nextState, playoutItems)); } }
private async Task <Either <BaseError, PlayoutItemWithPath> > ValidatePlayoutItemPath(PlayoutItem playoutItem) { string path = await GetPlayoutItemPath(playoutItem); if (_localFileSystem.FileExists(path)) { return(new PlayoutItemWithPath(playoutItem, path)); } return(new PlayoutItemDoesNotExistOnDisk(path)); }
private async Task <Either <BaseError, PlayoutItemWithPath> > CheckForFallbackFiller( TvContext dbContext, Channel channel, DateTimeOffset now) { // check for channel fallback Option <FillerPreset> maybeFallback = await dbContext.FillerPresets .SelectOneAsync(w => w.Id, w => w.Id == channel.FallbackFillerId); // then check for global fallback if (maybeFallback.IsNone) { maybeFallback = await dbContext.ConfigElements .GetValue <int>(ConfigElementKey.FFmpegGlobalFallbackFillerId) .BindT(fillerId => dbContext.FillerPresets.SelectOneAsync(w => w.Id, w => w.Id == fillerId)); } foreach (FillerPreset fallbackPreset in maybeFallback) { // turn this into a playout item var collectionKey = CollectionKey.ForFillerPreset(fallbackPreset); List <MediaItem> items = await MediaItemsForCollection.Collect( _mediaCollectionRepository, _televisionRepository, _artistRepository, collectionKey); // TODO: shuffle? does it really matter since we loop anyway MediaItem item = items[new Random().Next(items.Count)]; Option <TimeSpan> maybeDuration = await dbContext.PlayoutItems .Filter(pi => pi.Playout.ChannelId == channel.Id) .Filter(pi => pi.Start > now.UtcDateTime) .OrderBy(pi => pi.Start) .FirstOrDefaultAsync() .Map(Optional) .MapT(pi => pi.StartOffset - now); MediaVersion version = item.GetHeadVersion(); version.MediaFiles = await dbContext.MediaFiles .AsNoTracking() .Filter(mf => mf.MediaVersionId == version.Id) .ToListAsync(); version.Streams = await dbContext.MediaStreams .AsNoTracking() .Filter(ms => ms.MediaVersionId == version.Id) .ToListAsync(); DateTimeOffset finish = maybeDuration.Match( // next playout item exists // loop until it starts now.Add, // no next playout item exists // loop for 5 minutes if less than 30s, otherwise play full item () => version.Duration < TimeSpan.FromSeconds(30) ? now.AddMinutes(5) : now.Add(version.Duration)); var playoutItem = new PlayoutItem { MediaItem = item, MediaItemId = item.Id, Start = now.UtcDateTime, Finish = finish.UtcDateTime, FillerKind = FillerKind.Fallback, InPoint = TimeSpan.Zero, OutPoint = version.Duration }; return(await ValidatePlayoutItemPath(playoutItem)); } return(new UnableToLocatePlayoutItem()); }
public override Tuple <PlayoutBuilderState, List <PlayoutItem> > Schedule( PlayoutBuilderState playoutBuilderState, Dictionary <CollectionKey, IMediaCollectionEnumerator> collectionEnumerators, ProgramScheduleItemDuration scheduleItem, ProgramScheduleItem nextScheduleItem, DateTimeOffset hardStop) { var playoutItems = new List <PlayoutItem>(); PlayoutBuilderState nextState = playoutBuilderState; var willFinishInTime = true; Option <DateTimeOffset> durationUntil = None; IMediaCollectionEnumerator contentEnumerator = collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)]; while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime) { MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe(); // find when we should start this item, based on the current time DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem); // remember when we need to finish this duration item if (nextState.DurationFinish.IsNone) { nextState = nextState with { DurationFinish = itemStartTime + scheduleItem.PlayoutDuration }; durationUntil = nextState.DurationFinish; } TimeSpan itemDuration = DurationForMediaItem(mediaItem); List <MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem); if (itemDuration > scheduleItem.PlayoutDuration) { _logger.LogWarning( "Skipping playout item {Title} with duration {Duration} that is longer than schedule item duration {PlayoutDuration}", PlayoutBuilder.DisplayTitle(mediaItem), itemDuration, scheduleItem.PlayoutDuration); contentEnumerator.MoveNext(); continue; } var playoutItem = new PlayoutItem { MediaItemId = mediaItem.Id, Start = itemStartTime.UtcDateTime, Finish = itemStartTime.UtcDateTime + itemDuration, InPoint = TimeSpan.Zero, OutPoint = itemDuration, GuideGroup = nextState.NextGuideGroup, FillerKind = scheduleItem.GuideMode == GuideMode.Filler ? FillerKind.Tail : FillerKind.None, CustomTitle = scheduleItem.CustomTitle, WatermarkId = scheduleItem.WatermarkId, PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, SubtitleMode = scheduleItem.SubtitleMode }; durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime); DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc); DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller( collectionEnumerators, scheduleItem, itemStartTime, itemDuration, itemChapters); willFinishInTime = itemStartTime > durationFinish || itemEndTimeWithFiller <= durationFinish; if (willFinishInTime) { // LogScheduledItem(scheduleItem, mediaItem, itemStartTime); playoutItems.AddRange( AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters)); nextState = nextState with { CurrentTime = itemEndTimeWithFiller, // only bump guide group if we don't have a custom title NextGuideGroup = string.IsNullOrWhiteSpace(scheduleItem.CustomTitle) ? nextState.IncrementGuideGroup : nextState.NextGuideGroup }; contentEnumerator.MoveNext(); } else { TimeSpan durationBlock = itemEndTimeWithFiller - itemStartTime; if (itemEndTimeWithFiller - itemStartTime > scheduleItem.PlayoutDuration) { _logger.LogWarning( "Unable to schedule duration block of {DurationBlock} which is longer than the configured playout duration {PlayoutDuration}", durationBlock, scheduleItem.PlayoutDuration); } nextState = nextState with { DurationFinish = None }; nextState.ScheduleItemsEnumerator.MoveNext(); } } // this is needed when the duration finish exactly matches the hard stop if (nextState.DurationFinish.IsSome && nextState.CurrentTime == nextState.DurationFinish) { nextState = nextState with { DurationFinish = None }; nextState.ScheduleItemsEnumerator.MoveNext(); } if (playoutItems.Select(pi => pi.GuideGroup).Distinct().Count() != 1) { nextState = nextState with { NextGuideGroup = nextState.DecrementGuideGroup }; } foreach (DateTimeOffset nextItemStart in durationUntil) { switch (scheduleItem.TailMode) { case TailMode.Filler: if (scheduleItem.TailFiller != null) { (nextState, playoutItems) = AddTailFiller( nextState, collectionEnumerators, scheduleItem, playoutItems, nextItemStart); } if (scheduleItem.FallbackFiller != null) { (nextState, playoutItems) = AddFallbackFiller( nextState, collectionEnumerators, scheduleItem, playoutItems, nextItemStart); } nextState = nextState with { CurrentTime = nextItemStart }; break; case TailMode.Offline: if (scheduleItem.FallbackFiller != null) { (nextState, playoutItems) = AddFallbackFiller( nextState, collectionEnumerators, scheduleItem, playoutItems, nextItemStart); } nextState = nextState with { CurrentTime = nextItemStart }; break; } } // clear guide finish on all but the last item var all = playoutItems.Filter(pi => pi.FillerKind == FillerKind.None).ToList(); PlayoutItem last = all.OrderBy(pi => pi.FinishOffset).LastOrDefault(); foreach (PlayoutItem item in all.Filter(pi => pi != last)) { item.GuideFinish = null; } nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup }; return(Tuple(nextState, playoutItems)); } }
public string ToXml() { using var ms = new MemoryStream(); using var xml = XmlWriter.Create(ms); xml.WriteStartDocument(); xml.WriteStartElement("tv"); xml.WriteAttributeString("generator-info-name", "ersatztv"); var sortedChannelItems = new Dictionary <Channel, List <PlayoutItem> >(); foreach (Channel channel in _channels.OrderBy(c => decimal.Parse(c.Number))) { var sortedItems = channel.Playouts.Collect(p => p.Items).OrderBy(x => x.Start).ToList(); sortedChannelItems.Add(channel, sortedItems); if (sortedItems.Any()) { xml.WriteStartElement("channel"); xml.WriteAttributeString("id", $"{channel.Number}.etv"); xml.WriteStartElement("display-name"); xml.WriteAttributeString("lang", "en"); xml.WriteString(channel.Name); xml.WriteEndElement(); // display-name foreach (string category in GetCategories(channel.Categories)) { xml.WriteStartElement("category"); xml.WriteAttributeString("lang", "en"); xml.WriteString(category); xml.WriteEndElement(); // category } xml.WriteStartElement("icon"); string logo = Optional(channel.Artwork).Flatten() .Filter(a => a.ArtworkKind == ArtworkKind.Logo) .HeadOrNone() .Match( artwork => $"{_scheme}://{_host}/iptv/logos/{artwork.Path}.jpg", () => $"{_scheme}://{_host}/iptv/images/ersatztv-500.png"); xml.WriteAttributeString("src", logo); xml.WriteEndElement(); // icon xml.WriteEndElement(); // channel } } foreach ((Channel channel, List <PlayoutItem> sorted) in sortedChannelItems.OrderBy(kvp => kvp.Key.Number)) { var i = 0; while (i < sorted.Count && sorted[i].FillerKind != FillerKind.None && sorted[i].FillerKind != FillerKind.PreRoll) { i++; } while (i < sorted.Count) { PlayoutItem startItem = sorted[i]; int j = i; while (j + 1 < sorted.Count && sorted[j].FillerKind != FillerKind.None) { j++; } PlayoutItem displayItem = sorted[j]; bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle); int finishIndex = i; while (finishIndex + 1 < sorted.Count && sorted[finishIndex + 1].GuideGroup == startItem.GuideGroup) { finishIndex++; } int customShowId = -1; if (displayItem.MediaItem is Episode ep) { customShowId = ep.Season.ShowId; } bool isSameCustomShow = hasCustomTitle; for (int x = j; x <= finishIndex; x++) { isSameCustomShow = isSameCustomShow && sorted[x].MediaItem is Episode e && customShowId == e.Season.ShowId; } PlayoutItem finishItem = sorted[finishIndex]; i = finishIndex; string start = startItem.StartOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty); string stop = displayItem.GuideFinishOffset.HasValue ? displayItem.GuideFinishOffset.Value.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty) : finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty); string title = GetTitle(displayItem); string subtitle = GetSubtitle(displayItem); string description = GetDescription(displayItem); Option <ContentRating> contentRating = GetContentRating(displayItem); xml.WriteStartElement("programme"); xml.WriteAttributeString("start", start); xml.WriteAttributeString("stop", stop); xml.WriteAttributeString("channel", $"{channel.Number}.etv"); xml.WriteStartElement("title"); xml.WriteAttributeString("lang", "en"); xml.WriteString(title); xml.WriteEndElement(); // title if (!string.IsNullOrWhiteSpace(subtitle)) { xml.WriteStartElement("sub-title"); xml.WriteAttributeString("lang", "en"); xml.WriteString(subtitle); xml.WriteEndElement(); // subtitle } if (!isSameCustomShow) { if (!string.IsNullOrWhiteSpace(description)) { xml.WriteStartElement("desc"); xml.WriteAttributeString("lang", "en"); xml.WriteString(description); xml.WriteEndElement(); // desc } } if (!hasCustomTitle && displayItem.MediaItem is Movie movie) { foreach (MovieMetadata metadata in movie.MovieMetadata.HeadOrNone()) { if (metadata.Year.HasValue) { xml.WriteStartElement("date"); xml.WriteString(metadata.Year.Value.ToString()); xml.WriteEndElement(); // date } xml.WriteStartElement("category"); xml.WriteAttributeString("lang", "en"); xml.WriteString("Movie"); xml.WriteEndElement(); // category foreach (Genre genre in Optional(metadata.Genres).Flatten()) { xml.WriteStartElement("category"); xml.WriteAttributeString("lang", "en"); xml.WriteString(genre.Name); xml.WriteEndElement(); // category } string poster = Optional(metadata.Artwork).Flatten() .Filter(a => a.ArtworkKind == ArtworkKind.Poster) .HeadOrNone() .Match(a => GetArtworkUrl(a, ArtworkKind.Poster), () => string.Empty); if (!string.IsNullOrWhiteSpace(poster)) { xml.WriteStartElement("icon"); xml.WriteAttributeString("src", poster); xml.WriteEndElement(); // icon } } } if (!hasCustomTitle && displayItem.MediaItem is MusicVideo musicVideo) { foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone()) { if (metadata.Year.HasValue) { xml.WriteStartElement("date"); xml.WriteString(metadata.Year.Value.ToString()); xml.WriteEndElement(); // date } xml.WriteStartElement("category"); xml.WriteAttributeString("lang", "en"); xml.WriteString("Music"); xml.WriteEndElement(); // category // music video genres foreach (Genre genre in Optional(metadata.Genres).Flatten()) { xml.WriteStartElement("category"); xml.WriteAttributeString("lang", "en"); xml.WriteString(genre.Name); xml.WriteEndElement(); // category } // artist genres Option <ArtistMetadata> maybeMetadata = Optional(musicVideo.Artist?.ArtistMetadata.HeadOrNone()).Flatten(); foreach (ArtistMetadata artistMetadata in maybeMetadata) { foreach (Genre genre in Optional(artistMetadata.Genres).Flatten()) { xml.WriteStartElement("category"); xml.WriteAttributeString("lang", "en"); xml.WriteString(genre.Name); xml.WriteEndElement(); // category } } string thumbnail = Optional(metadata.Artwork).Flatten() .Filter(a => a.ArtworkKind == ArtworkKind.Thumbnail) .HeadOrNone() .Match(a => GetArtworkUrl(a, ArtworkKind.Thumbnail), () => string.Empty); if (!string.IsNullOrWhiteSpace(thumbnail)) { xml.WriteStartElement("icon"); xml.WriteAttributeString("src", thumbnail); xml.WriteEndElement(); // icon } } } if (!hasCustomTitle && displayItem.MediaItem is Song song) { xml.WriteStartElement("category"); xml.WriteAttributeString("lang", "en"); xml.WriteString("Music"); xml.WriteEndElement(); // category foreach (SongMetadata metadata in song.SongMetadata.HeadOrNone()) { string thumbnail = Optional(metadata.Artwork).Flatten() .Filter(a => a.ArtworkKind == ArtworkKind.Thumbnail) .HeadOrNone() .Match(a => GetArtworkUrl(a, ArtworkKind.Thumbnail), () => string.Empty); if (!string.IsNullOrWhiteSpace(thumbnail)) { xml.WriteStartElement("icon"); xml.WriteAttributeString("src", thumbnail); xml.WriteEndElement(); // icon } } } if (displayItem.MediaItem is Episode episode && (!hasCustomTitle || isSameCustomShow)) { Option <ShowMetadata> maybeMetadata = Optional(episode.Season?.Show?.ShowMetadata.HeadOrNone()).Flatten(); foreach (ShowMetadata metadata in maybeMetadata) { xml.WriteStartElement("category"); xml.WriteAttributeString("lang", "en"); xml.WriteString("Series"); xml.WriteEndElement(); // category foreach (Genre genre in Optional(metadata.Genres).Flatten()) { xml.WriteStartElement("category"); xml.WriteAttributeString("lang", "en"); xml.WriteString(genre.Name); xml.WriteEndElement(); // category } string artwork = Optional(metadata.Artwork).Flatten() .Filter(a => a.ArtworkKind == ArtworkKind.Thumbnail) .HeadOrNone() .Match(a => GetArtworkUrl(a, ArtworkKind.Thumbnail), () => string.Empty); // fall back to poster if (string.IsNullOrWhiteSpace(artwork)) { artwork = Optional(metadata.Artwork).Flatten() .Filter(a => a.ArtworkKind == ArtworkKind.Poster) .HeadOrNone() .Match(a => GetArtworkUrl(a, ArtworkKind.Poster), () => string.Empty); } if (!string.IsNullOrWhiteSpace(artwork)) { xml.WriteStartElement("icon"); xml.WriteAttributeString("src", artwork); xml.WriteEndElement(); // icon } } if (!isSameCustomShow) { int s = Optional(episode.Season?.SeasonNumber).IfNone(-1); // TODO: multi-episode? int e = episode.EpisodeMetadata.HeadOrNone().Match(em => em.EpisodeNumber, -1); if (s >= 0 && e > 0) { xml.WriteStartElement("episode-num"); xml.WriteAttributeString("system", "onscreen"); xml.WriteString($"S{s:00}E{e:00}"); xml.WriteEndElement(); // episode-num xml.WriteStartElement("episode-num"); xml.WriteAttributeString("system", "xmltv_ns"); xml.WriteString($"{s - 1}.{e - 1}.0/1"); xml.WriteEndElement(); // episode-num } } } xml.WriteStartElement("previously-shown"); xml.WriteEndElement(); // previously-shown foreach (ContentRating rating in contentRating) { xml.WriteStartElement("rating"); foreach (string system in rating.System) { xml.WriteAttributeString("system", system); } xml.WriteStartElement("value"); xml.WriteString(rating.Value); xml.WriteEndElement(); // value xml.WriteEndElement(); // rating } xml.WriteEndElement(); // programme i++; } } xml.WriteEndElement(); // tv xml.WriteEndDocument(); xml.Flush(); return(Encoding.UTF8.GetString(ms.ToArray())); }
private record Parameters(Channel Channel, PlayoutItem PlayoutItem);