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
        });
示例#2
0
    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);
    }
示例#3
0
        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
            });
示例#4
0
    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]"
        });
示例#5
0
    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);
        }
    }
示例#6
0
        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));
        }
示例#7
0
 internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
示例#8
0
    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());
    }
示例#12
0
    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));
    }
}
示例#13
0
    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()));
    }
示例#14
0
 private record Parameters(Channel Channel, PlayoutItem PlayoutItem);