< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.DefaultLiveTvService
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/DefaultLiveTvService.cs
Line coverage
2%
Covered lines: 11
Uncovered lines: 361
Coverable lines: 372
Total lines: 998
Line coverage: 2.9%
Branch coverage
0%
Covered branches: 0
Total branches: 146
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

File(s)

/srv/git/jellyfin/src/Jellyfin.LiveTv/DefaultLiveTvService.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using Jellyfin.Data.Enums;
 13using Jellyfin.Data.Events;
 14using Jellyfin.Extensions;
 15using Jellyfin.LiveTv.Configuration;
 16using Jellyfin.LiveTv.Timers;
 17using MediaBrowser.Common.Extensions;
 18using MediaBrowser.Controller.Configuration;
 19using MediaBrowser.Controller.Dto;
 20using MediaBrowser.Controller.Entities;
 21using MediaBrowser.Controller.Library;
 22using MediaBrowser.Controller.LiveTv;
 23using MediaBrowser.Model.Dto;
 24using MediaBrowser.Model.LiveTv;
 25using Microsoft.Extensions.Logging;
 26
 27namespace Jellyfin.LiveTv
 28{
 29    public sealed class DefaultLiveTvService : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds
 30    {
 31        public const string ServiceName = "Emby";
 32
 33        private readonly ILogger<DefaultLiveTvService> _logger;
 34        private readonly IServerConfigurationManager _config;
 35        private readonly ITunerHostManager _tunerHostManager;
 36        private readonly IListingsManager _listingsManager;
 37        private readonly IRecordingsManager _recordingsManager;
 38        private readonly ILibraryManager _libraryManager;
 39        private readonly LiveTvDtoService _tvDtoService;
 40        private readonly TimerManager _timerManager;
 41        private readonly SeriesTimerManager _seriesTimerManager;
 42
 43        public DefaultLiveTvService(
 44            ILogger<DefaultLiveTvService> logger,
 45            IServerConfigurationManager config,
 46            ITunerHostManager tunerHostManager,
 47            IListingsManager listingsManager,
 48            IRecordingsManager recordingsManager,
 49            ILibraryManager libraryManager,
 50            LiveTvDtoService tvDtoService,
 51            TimerManager timerManager,
 52            SeriesTimerManager seriesTimerManager)
 53        {
 2254            _logger = logger;
 2255            _config = config;
 2256            _libraryManager = libraryManager;
 2257            _tunerHostManager = tunerHostManager;
 2258            _listingsManager = listingsManager;
 2259            _recordingsManager = recordingsManager;
 2260            _tvDtoService = tvDtoService;
 2261            _timerManager = timerManager;
 2262            _seriesTimerManager = seriesTimerManager;
 63
 2264            _timerManager.TimerFired += OnTimerManagerTimerFired;
 2265        }
 66
 67        public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated;
 68
 69        public event EventHandler<GenericEventArgs<string>> TimerCancelled;
 70
 71        /// <inheritdoc />
 072        public string Name => ServiceName;
 73
 74        /// <inheritdoc />
 075        public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
 76
 77        public async Task RefreshSeriesTimers(CancellationToken cancellationToken)
 78        {
 79            var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
 80
 81            foreach (var timer in seriesTimers)
 82            {
 83                UpdateTimersForSeriesTimer(timer, false, true);
 84            }
 85        }
 86
 87        public async Task RefreshTimers(CancellationToken cancellationToken)
 88        {
 89            var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
 90
 91            var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
 92
 93            foreach (var timer in timers)
 94            {
 95                if (DateTime.UtcNow > timer.EndDate && _recordingsManager.GetActiveRecordingPath(timer.Id) is null)
 96                {
 97                    _timerManager.Delete(timer);
 98                    continue;
 99                }
 100
 101                if (string.IsNullOrWhiteSpace(timer.ProgramId) || string.IsNullOrWhiteSpace(timer.ChannelId))
 102                {
 103                    continue;
 104                }
 105
 106                var program = GetProgramInfoFromCache(timer);
 107                if (program is null)
 108                {
 109                    _timerManager.Delete(timer);
 110                    continue;
 111                }
 112
 113                CopyProgramInfoToTimerInfo(program, timer, tempChannelCache);
 114                _timerManager.Update(timer);
 115            }
 116        }
 117
 118        private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationTo
 119        {
 120            var channels = new List<ChannelInfo>();
 121
 122            foreach (var hostInstance in _tunerHostManager.TunerHosts)
 123            {
 124                try
 125                {
 126                    var tunerChannels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(fa
 127
 128                    channels.AddRange(tunerChannels);
 129                }
 130                catch (Exception ex)
 131                {
 132                    _logger.LogError(ex, "Error getting channels");
 133                }
 134            }
 135
 136            await _listingsManager.AddProviderMetadata(channels, enableCache, cancellationToken).ConfigureAwait(false);
 137
 138            return channels;
 139        }
 140
 141        public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
 142        {
 0143            return GetChannelsAsync(false, cancellationToken);
 144        }
 145
 146        public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
 147        {
 0148            var timers = _timerManager
 0149                .GetAll()
 0150                .Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase))
 0151                .ToList();
 152
 0153            foreach (var timer in timers)
 154            {
 0155                CancelTimerInternal(timer.Id, true, true);
 156            }
 157
 0158            var remove = _seriesTimerManager.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.
 0159            if (remove is not null)
 160            {
 0161                _seriesTimerManager.Delete(remove);
 162            }
 163
 0164            return Task.CompletedTask;
 165        }
 166
 167        private void CancelTimerInternal(string timerId, bool isSeriesCancelled, bool isManualCancellation)
 168        {
 0169            var timer = _timerManager.GetTimer(timerId);
 0170            if (timer is not null)
 171            {
 0172                var statusChanging = timer.Status != RecordingStatus.Cancelled;
 0173                timer.Status = RecordingStatus.Cancelled;
 174
 0175                if (isManualCancellation)
 176                {
 0177                    timer.IsManual = true;
 178                }
 179
 0180                if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled)
 181                {
 0182                    _timerManager.Delete(timer);
 183                }
 184                else
 185                {
 0186                    _timerManager.AddOrUpdate(timer, false);
 187                }
 188
 0189                if (statusChanging && TimerCancelled is not null)
 190                {
 0191                    TimerCancelled(this, new GenericEventArgs<string>(timerId));
 192                }
 193            }
 194
 0195            _recordingsManager.CancelRecording(timerId, timer);
 0196        }
 197
 198        public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
 199        {
 0200            CancelTimerInternal(timerId, false, true);
 0201            return Task.CompletedTask;
 202        }
 203
 204        public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
 205        {
 0206            throw new NotImplementedException();
 207        }
 208
 209        public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
 210        {
 0211            throw new NotImplementedException();
 212        }
 213
 214        public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
 215        {
 0216            var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ?
 0217                null :
 0218                _timerManager.GetTimerByProgramId(info.ProgramId);
 219
 0220            if (existingTimer is not null)
 221            {
 0222                if (existingTimer.Status == RecordingStatus.Cancelled
 0223                    || existingTimer.Status == RecordingStatus.Completed)
 224                {
 0225                    existingTimer.Status = RecordingStatus.New;
 0226                    existingTimer.IsManual = true;
 0227                    _timerManager.Update(existingTimer);
 0228                    return Task.FromResult(existingTimer.Id);
 229                }
 230
 0231                throw new ArgumentException("A scheduled recording already exists for this program.");
 232            }
 233
 0234            info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 235
 0236            LiveTvProgram programInfo = null;
 237
 0238            if (!string.IsNullOrWhiteSpace(info.ProgramId))
 239            {
 0240                programInfo = GetProgramInfoFromCache(info);
 241            }
 242
 0243            if (programInfo is null)
 244            {
 0245                _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramI
 0246                programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate);
 247            }
 248
 0249            if (programInfo is not null)
 250            {
 0251                CopyProgramInfoToTimerInfo(programInfo, info);
 252            }
 253
 0254            info.IsManual = true;
 0255            _timerManager.Add(info);
 256
 0257            TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info));
 258
 0259            return Task.FromResult(info.Id);
 260        }
 261
 262        public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
 263        {
 264            info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 265
 266            // populate info.seriesID
 267            var program = GetProgramInfoFromCache(info.ProgramId);
 268
 269            if (program is not null)
 270            {
 271                info.SeriesId = program.ExternalSeriesId;
 272            }
 273            else
 274            {
 275                throw new InvalidOperationException("SeriesId for program not found");
 276            }
 277
 278            // If any timers have already been manually created, make sure they don't get cancelled
 279            var existingTimers = (await GetTimersAsync(CancellationToken.None).ConfigureAwait(false))
 280                .Where(i =>
 281                {
 282                    if (string.Equals(i.ProgramId, info.ProgramId, StringComparison.OrdinalIgnoreCase) && !string.IsNull
 283                    {
 284                        return true;
 285                    }
 286
 287                    if (string.Equals(i.SeriesId, info.SeriesId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOr
 288                    {
 289                        return true;
 290                    }
 291
 292                    return false;
 293                })
 294                .ToList();
 295
 296            _seriesTimerManager.Add(info);
 297
 298            foreach (var timer in existingTimers)
 299            {
 300                timer.SeriesTimerId = info.Id;
 301                timer.IsManual = true;
 302
 303                _timerManager.AddOrUpdate(timer, false);
 304            }
 305
 306            UpdateTimersForSeriesTimer(info, true, false);
 307
 308            return info.Id;
 309        }
 310
 311        public Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
 312        {
 0313            var instance = _seriesTimerManager.GetAll().FirstOrDefault(i => string.Equals(i.Id, info.Id, StringCompariso
 314
 0315            if (instance is not null)
 316            {
 0317                instance.ChannelId = info.ChannelId;
 0318                instance.Days = info.Days;
 0319                instance.EndDate = info.EndDate;
 0320                instance.IsPostPaddingRequired = info.IsPostPaddingRequired;
 0321                instance.IsPrePaddingRequired = info.IsPrePaddingRequired;
 0322                instance.PostPaddingSeconds = info.PostPaddingSeconds;
 0323                instance.PrePaddingSeconds = info.PrePaddingSeconds;
 0324                instance.Priority = info.Priority;
 0325                instance.RecordAnyChannel = info.RecordAnyChannel;
 0326                instance.RecordAnyTime = info.RecordAnyTime;
 0327                instance.RecordNewOnly = info.RecordNewOnly;
 0328                instance.SkipEpisodesInLibrary = info.SkipEpisodesInLibrary;
 0329                instance.KeepUpTo = info.KeepUpTo;
 0330                instance.KeepUntil = info.KeepUntil;
 0331                instance.StartDate = info.StartDate;
 332
 0333                _seriesTimerManager.Update(instance);
 334
 0335                UpdateTimersForSeriesTimer(instance, true, true);
 336            }
 337
 0338            return Task.CompletedTask;
 339        }
 340
 341        public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken)
 342        {
 0343            var existingTimer = _timerManager.GetTimer(updatedTimer.Id);
 344
 0345            if (existingTimer is null)
 346            {
 0347                throw new ResourceNotFoundException();
 348            }
 349
 350            // Only update if not currently active
 0351            if (_recordingsManager.GetActiveRecordingPath(updatedTimer.Id) is null)
 352            {
 0353                existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
 0354                existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
 0355                existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired;
 0356                existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired;
 357
 0358                _timerManager.Update(existingTimer);
 359            }
 360
 0361            return Task.CompletedTask;
 362        }
 363
 364        private static void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer)
 365        {
 366            // Update the program info but retain the status
 0367            existingTimer.ChannelId = updatedTimer.ChannelId;
 0368            existingTimer.CommunityRating = updatedTimer.CommunityRating;
 0369            existingTimer.EndDate = updatedTimer.EndDate;
 0370            existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber;
 0371            existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle;
 0372            existingTimer.Genres = updatedTimer.Genres;
 0373            existingTimer.IsMovie = updatedTimer.IsMovie;
 0374            existingTimer.IsSeries = updatedTimer.IsSeries;
 0375            existingTimer.Tags = updatedTimer.Tags;
 0376            existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries;
 0377            existingTimer.IsRepeat = updatedTimer.IsRepeat;
 0378            existingTimer.Name = updatedTimer.Name;
 0379            existingTimer.OfficialRating = updatedTimer.OfficialRating;
 0380            existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate;
 0381            existingTimer.Overview = updatedTimer.Overview;
 0382            existingTimer.ProductionYear = updatedTimer.ProductionYear;
 0383            existingTimer.ProgramId = updatedTimer.ProgramId;
 0384            existingTimer.SeasonNumber = updatedTimer.SeasonNumber;
 0385            existingTimer.StartDate = updatedTimer.StartDate;
 0386            existingTimer.ShowId = updatedTimer.ShowId;
 0387            existingTimer.ProviderIds = updatedTimer.ProviderIds;
 0388            existingTimer.SeriesProviderIds = updatedTimer.SeriesProviderIds;
 0389        }
 390
 391        public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
 392        {
 0393            var excludeStatues = new List<RecordingStatus>
 0394            {
 0395                RecordingStatus.Completed
 0396            };
 397
 0398            var timers = _timerManager.GetAll()
 0399                .Where(i => !excludeStatues.Contains(i.Status));
 400
 0401            return Task.FromResult(timers);
 402        }
 403
 404        public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program =
 405        {
 0406            var config = _config.GetLiveTvConfiguration();
 407
 0408            var defaults = new SeriesTimerInfo()
 0409            {
 0410                PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
 0411                PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
 0412                RecordAnyChannel = false,
 0413                RecordAnyTime = true,
 0414                RecordNewOnly = true,
 0415
 0416                Days = new List<DayOfWeek>
 0417                {
 0418                    DayOfWeek.Sunday,
 0419                    DayOfWeek.Monday,
 0420                    DayOfWeek.Tuesday,
 0421                    DayOfWeek.Wednesday,
 0422                    DayOfWeek.Thursday,
 0423                    DayOfWeek.Friday,
 0424                    DayOfWeek.Saturday
 0425                }
 0426            };
 427
 0428            if (program is not null)
 429            {
 0430                defaults.SeriesId = program.SeriesId;
 0431                defaults.ProgramId = program.Id;
 0432                defaults.RecordNewOnly = !program.IsRepeat;
 0433                defaults.Name = program.Name;
 434            }
 435
 0436            defaults.SkipEpisodesInLibrary = defaults.RecordNewOnly;
 0437            defaults.KeepUntil = KeepUntil.UntilDeleted;
 438
 0439            return Task.FromResult(defaults);
 440        }
 441
 442        public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
 443        {
 0444            return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerManager.GetAll());
 445        }
 446
 447        public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime e
 448        {
 449            var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
 450            var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
 451
 452            return await _listingsManager.GetProgramsAsync(channel, startDateUtc, endDateUtc, cancellationToken)
 453                .ConfigureAwait(false);
 454        }
 455
 456        public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationT
 457        {
 0458            throw new NotImplementedException();
 459        }
 460
 461        public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<
 462        {
 463            _logger.LogInformation("Streaming Channel {Id}", channelId);
 464
 465            var result = string.IsNullOrEmpty(streamId) ?
 466                null :
 467                currentLiveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.Ordi
 468
 469            if (result is not null && result.EnableStreamSharing)
 470            {
 471                result.ConsumerCount++;
 472
 473                _logger.LogInformation("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount);
 474
 475                return result;
 476            }
 477
 478            foreach (var hostInstance in _tunerHostManager.TunerHosts)
 479            {
 480                try
 481                {
 482                    result = await hostInstance.GetChannelStream(channelId, streamId, currentLiveStreams, cancellationTo
 483
 484                    var openedMediaSource = result.MediaSource;
 485
 486                    result.OriginalStreamId = streamId;
 487
 488                    _logger.LogInformation("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStre
 489
 490                    return result;
 491                }
 492                catch (FileNotFoundException)
 493                {
 494                }
 495                catch (OperationCanceledException)
 496                {
 497                }
 498            }
 499
 500            throw new ResourceNotFoundException("Tuner not found.");
 501        }
 502
 503        public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancel
 504        {
 505            if (string.IsNullOrWhiteSpace(channelId))
 506            {
 507                throw new ArgumentNullException(nameof(channelId));
 508            }
 509
 510            foreach (var hostInstance in _tunerHostManager.TunerHosts)
 511            {
 512                try
 513                {
 514                    var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).Configur
 515
 516                    if (sources.Count > 0)
 517                    {
 518                        return sources;
 519                    }
 520                }
 521                catch (NotImplementedException)
 522                {
 523                }
 524            }
 525
 526            throw new NotImplementedException();
 527        }
 528
 529        public Task CloseLiveStream(string id, CancellationToken cancellationToken)
 530        {
 0531            return Task.CompletedTask;
 532        }
 533
 534        public Task ResetTuner(string id, CancellationToken cancellationToken)
 535        {
 0536            return Task.CompletedTask;
 537        }
 538
 539        private async void OnTimerManagerTimerFired(object sender, GenericEventArgs<TimerInfo> e)
 540        {
 541            var timer = e.Argument;
 542
 543            _logger.LogInformation("Recording timer fired for {0}.", timer.Name);
 544
 545            try
 546            {
 547                var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds);
 548                if (recordingEndDate <= DateTime.UtcNow)
 549                {
 550                    _logger.LogWarning("Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already
 551                    _timerManager.Delete(timer);
 552                    return;
 553                }
 554
 555                var activeRecordingInfo = new ActiveRecordingInfo
 556                {
 557                    CancellationTokenSource = new CancellationTokenSource(),
 558                    Timer = timer,
 559                    Id = timer.Id
 560                };
 561
 562                if (_recordingsManager.GetActiveRecordingPath(timer.Id) is not null)
 563                {
 564                    _logger.LogInformation("Skipping RecordStream because it's already in progress.");
 565                    return;
 566                }
 567
 568                LiveTvProgram programInfo = null;
 569                if (!string.IsNullOrWhiteSpace(timer.ProgramId))
 570                {
 571                    programInfo = GetProgramInfoFromCache(timer);
 572                }
 573
 574                if (programInfo is null)
 575                {
 576                    _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.Pro
 577                    programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
 578                }
 579
 580                if (programInfo is not null)
 581                {
 582                    CopyProgramInfoToTimerInfo(programInfo, timer);
 583                }
 584
 585                await _recordingsManager.RecordStream(activeRecordingInfo, GetLiveTvChannel(timer), recordingEndDate)
 586                    .ConfigureAwait(false);
 587            }
 588            catch (OperationCanceledException)
 589            {
 590            }
 591            catch (Exception ex)
 592            {
 593                _logger.LogError(ex, "Error recording stream");
 594            }
 595        }
 596
 597        private BaseItem GetLiveTvChannel(TimerInfo timer)
 598        {
 0599            var internalChannelId = _tvDtoService.GetInternalChannelId(Name, timer.ChannelId);
 0600            return _libraryManager.GetItemById(internalChannelId);
 601        }
 602
 603        private LiveTvProgram GetProgramInfoFromCache(string programId)
 604        {
 0605            var query = new InternalItemsQuery
 0606            {
 0607                ItemIds = [_tvDtoService.GetInternalProgramId(programId)],
 0608                Limit = 1,
 0609                DtoOptions = new DtoOptions()
 0610            };
 611
 0612            return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault();
 613        }
 614
 615        private LiveTvProgram GetProgramInfoFromCache(TimerInfo timer)
 616        {
 0617            return GetProgramInfoFromCache(timer.ProgramId);
 618        }
 619
 620        private LiveTvProgram GetProgramInfoFromCache(string channelId, DateTime startDateUtc)
 621        {
 0622            var query = new InternalItemsQuery
 0623            {
 0624                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
 0625                Limit = 1,
 0626                DtoOptions = new DtoOptions(true)
 0627                {
 0628                    EnableImages = false
 0629                },
 0630                MinStartDate = startDateUtc.AddMinutes(-3),
 0631                MaxStartDate = startDateUtc.AddMinutes(3),
 0632                OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }
 0633            };
 634
 0635            if (!string.IsNullOrWhiteSpace(channelId))
 636            {
 0637                query.ChannelIds = [_tvDtoService.GetInternalChannelId(Name, channelId)];
 638            }
 639
 0640            return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault();
 641        }
 642
 643        private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
 644        {
 0645            if (timer.IsManual)
 646            {
 0647                return false;
 648            }
 649
 0650            if (!seriesTimer.RecordAnyTime
 0651                && Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMin
 652            {
 0653                return true;
 654            }
 655
 0656            if (seriesTimer.RecordNewOnly && timer.IsRepeat)
 657            {
 0658                return true;
 659            }
 660
 0661            if (!seriesTimer.RecordAnyChannel
 0662                && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
 663            {
 0664                return true;
 665            }
 666
 0667            return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer);
 668        }
 669
 670        private void HandleDuplicateShowIds(List<TimerInfo> timers)
 671        {
 672            // sort showings by HD channels first, then by startDate, record earliest showing possible
 0673            foreach (var timer in timers.OrderByDescending(t => GetLiveTvChannel(t).IsHD).ThenBy(t => t.StartDate).Skip(
 674            {
 0675                timer.Status = RecordingStatus.Cancelled;
 0676                _timerManager.Update(timer);
 677            }
 0678        }
 679
 680        private void SearchForDuplicateShowIds(IEnumerable<TimerInfo> timers)
 681        {
 0682            var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList();
 683
 0684            foreach (var group in groups)
 685            {
 0686                if (string.IsNullOrWhiteSpace(group.Key))
 687                {
 688                    continue;
 689                }
 690
 0691                var groupTimers = group.ToList();
 692
 0693                if (groupTimers.Count < 2)
 694                {
 695                    continue;
 696                }
 697
 698                // Skip ShowId without SubKey from duplicate removal actions - https://github.com/jellyfin/jellyfin/issu
 0699                if (group.Key.EndsWith("0000", StringComparison.Ordinal))
 700                {
 701                    continue;
 702                }
 703
 0704                HandleDuplicateShowIds(groupTimers);
 705            }
 0706        }
 707
 708        private void UpdateTimersForSeriesTimer(SeriesTimerInfo seriesTimer, bool updateTimerSettings, bool deleteInvali
 709        {
 0710            var allTimers = GetTimersForSeries(seriesTimer).ToList();
 711
 0712            var enabledTimersForSeries = new List<TimerInfo>();
 0713            foreach (var timer in allTimers)
 714            {
 0715                var existingTimer = _timerManager.GetTimer(timer.Id)
 0716                    ?? (string.IsNullOrWhiteSpace(timer.ProgramId)
 0717                        ? null
 0718                        : _timerManager.GetTimerByProgramId(timer.ProgramId));
 719
 0720                if (existingTimer is null)
 721                {
 0722                    if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
 723                    {
 0724                        timer.Status = RecordingStatus.Cancelled;
 725                    }
 726                    else
 727                    {
 0728                        enabledTimersForSeries.Add(timer);
 729                    }
 730
 0731                    _timerManager.Add(timer);
 732
 0733                    TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
 734                }
 735
 736                // Only update if not currently active - test both new timer and existing in case Id's are different
 737                // Id's could be different if the timer was created manually prior to series timer creation
 0738                else if (_recordingsManager.GetActiveRecordingPath(timer.Id) is null
 0739                         && _recordingsManager.GetActiveRecordingPath(existingTimer.Id) is null)
 740                {
 0741                    UpdateExistingTimerWithNewMetadata(existingTimer, timer);
 742
 743                    // Needed by ShouldCancelTimerForSeriesTimer
 0744                    timer.IsManual = existingTimer.IsManual;
 745
 0746                    if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
 747                    {
 0748                        existingTimer.Status = RecordingStatus.Cancelled;
 749                    }
 0750                    else if (!existingTimer.IsManual)
 751                    {
 0752                        existingTimer.Status = RecordingStatus.New;
 753                    }
 754
 0755                    if (existingTimer.Status != RecordingStatus.Cancelled)
 756                    {
 0757                        enabledTimersForSeries.Add(existingTimer);
 758                    }
 759
 0760                    if (updateTimerSettings)
 761                    {
 0762                        existingTimer.KeepUntil = seriesTimer.KeepUntil;
 0763                        existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
 0764                        existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
 0765                        existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
 0766                        existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
 0767                        existingTimer.Priority = seriesTimer.Priority;
 0768                        existingTimer.SeriesTimerId = seriesTimer.Id;
 769                    }
 770
 0771                    existingTimer.SeriesTimerId = seriesTimer.Id;
 0772                    _timerManager.Update(existingTimer);
 773                }
 774            }
 775
 0776            SearchForDuplicateShowIds(enabledTimersForSeries);
 777
 0778            if (deleteInvalidTimers)
 779            {
 0780                var allTimerIds = allTimers
 0781                    .Select(i => i.Id)
 0782                    .ToList();
 783
 0784                var deleteStatuses = new[]
 0785                {
 0786                    RecordingStatus.New
 0787                };
 788
 0789                var deletes = _timerManager.GetAll()
 0790                    .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
 0791                    .Where(i => !allTimerIds.Contains(i.Id, StringComparison.OrdinalIgnoreCase) && i.StartDate > DateTim
 0792                    .Where(i => deleteStatuses.Contains(i.Status))
 0793                    .ToList();
 794
 0795                foreach (var timer in deletes)
 796                {
 0797                    CancelTimerInternal(timer.Id, false, false);
 798                }
 799            }
 0800        }
 801
 802        private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer)
 803        {
 0804            ArgumentNullException.ThrowIfNull(seriesTimer);
 805
 0806            var query = new InternalItemsQuery
 0807            {
 0808                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
 0809                ExternalSeriesId = seriesTimer.SeriesId,
 0810                DtoOptions = new DtoOptions(true)
 0811                {
 0812                    EnableImages = false
 0813                },
 0814                MinEndDate = DateTime.UtcNow
 0815            };
 816
 0817            if (string.IsNullOrEmpty(seriesTimer.SeriesId))
 818            {
 0819                query.Name = seriesTimer.Name;
 820            }
 821
 0822            if (!seriesTimer.RecordAnyChannel)
 823            {
 0824                query.ChannelIds = [_tvDtoService.GetInternalChannelId(Name, seriesTimer.ChannelId)];
 825            }
 826
 0827            var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
 828
 0829            return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().Select(i => CreateTimer(i, seriesTimer, temp
 830        }
 831
 832        private TimerInfo CreateTimer(LiveTvProgram parent, SeriesTimerInfo seriesTimer, Dictionary<Guid, LiveTvChannel>
 833        {
 0834            string channelId = seriesTimer.RecordAnyChannel ? null : seriesTimer.ChannelId;
 835
 0836            if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.IsEmpty())
 837            {
 0838                if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel))
 839                {
 0840                    channel = _libraryManager.GetItemList(
 0841                        new InternalItemsQuery
 0842                        {
 0843                            IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
 0844                            ItemIds = new[] { parent.ChannelId },
 0845                            DtoOptions = new DtoOptions()
 0846                        }).FirstOrDefault() as LiveTvChannel;
 847
 0848                    if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId))
 849                    {
 0850                        tempChannelCache[parent.ChannelId] = channel;
 851                    }
 852                }
 853
 0854                if (channel is not null || tempChannelCache.TryGetValue(parent.ChannelId, out channel))
 855                {
 0856                    channelId = channel.ExternalId;
 857                }
 858            }
 859
 0860            var timer = new TimerInfo
 0861            {
 0862                ChannelId = channelId,
 0863                Id = (seriesTimer.Id + parent.ExternalId).GetMD5().ToString("N", CultureInfo.InvariantCulture),
 0864                StartDate = parent.StartDate,
 0865                EndDate = parent.EndDate.Value,
 0866                ProgramId = parent.ExternalId,
 0867                PrePaddingSeconds = seriesTimer.PrePaddingSeconds,
 0868                PostPaddingSeconds = seriesTimer.PostPaddingSeconds,
 0869                IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired,
 0870                IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired,
 0871                KeepUntil = seriesTimer.KeepUntil,
 0872                Priority = seriesTimer.Priority,
 0873                Name = parent.Name,
 0874                Overview = parent.Overview,
 0875                SeriesId = parent.ExternalSeriesId,
 0876                SeriesTimerId = seriesTimer.Id,
 0877                ShowId = parent.ShowId
 0878            };
 879
 0880            CopyProgramInfoToTimerInfo(parent, timer, tempChannelCache);
 881
 0882            return timer;
 883        }
 884
 885        private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo)
 886        {
 0887            var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
 0888            CopyProgramInfoToTimerInfo(programInfo, timerInfo, tempChannelCache);
 0889        }
 890
 891        private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo, Dictionary<Guid, LiveTvC
 892        {
 0893            string channelId = null;
 894
 0895            if (!programInfo.ChannelId.IsEmpty())
 896            {
 0897                if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel))
 898                {
 0899                    channel = _libraryManager.GetItemList(
 0900                        new InternalItemsQuery
 0901                        {
 0902                            IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
 0903                            ItemIds = new[] { programInfo.ChannelId },
 0904                            DtoOptions = new DtoOptions()
 0905                        }).FirstOrDefault() as LiveTvChannel;
 906
 0907                    if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId))
 908                    {
 0909                        tempChannelCache[programInfo.ChannelId] = channel;
 910                    }
 911                }
 912
 0913                if (channel is not null || tempChannelCache.TryGetValue(programInfo.ChannelId, out channel))
 914                {
 0915                    channelId = channel.ExternalId;
 916                }
 917            }
 918
 0919            timerInfo.Name = programInfo.Name;
 0920            timerInfo.StartDate = programInfo.StartDate;
 0921            timerInfo.EndDate = programInfo.EndDate.Value;
 922
 0923            if (!string.IsNullOrWhiteSpace(channelId))
 924            {
 0925                timerInfo.ChannelId = channelId;
 926            }
 927
 0928            timerInfo.SeasonNumber = programInfo.ParentIndexNumber;
 0929            timerInfo.EpisodeNumber = programInfo.IndexNumber;
 0930            timerInfo.IsMovie = programInfo.IsMovie;
 0931            timerInfo.ProductionYear = programInfo.ProductionYear;
 0932            timerInfo.EpisodeTitle = programInfo.EpisodeTitle;
 0933            timerInfo.OriginalAirDate = programInfo.PremiereDate;
 0934            timerInfo.IsProgramSeries = programInfo.IsSeries;
 935
 0936            timerInfo.IsSeries = programInfo.IsSeries;
 937
 0938            timerInfo.CommunityRating = programInfo.CommunityRating;
 0939            timerInfo.Overview = programInfo.Overview;
 0940            timerInfo.OfficialRating = programInfo.OfficialRating;
 0941            timerInfo.IsRepeat = programInfo.IsRepeat;
 0942            timerInfo.SeriesId = programInfo.ExternalSeriesId;
 0943            timerInfo.ProviderIds = programInfo.ProviderIds;
 0944            timerInfo.Tags = programInfo.Tags;
 945
 0946            var seriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 947
 0948            foreach (var providerId in timerInfo.ProviderIds)
 949            {
 950                const string Search = "Series";
 0951                if (providerId.Key.StartsWith(Search, StringComparison.OrdinalIgnoreCase))
 952                {
 0953                    seriesProviderIds[providerId.Key.Substring(Search.Length)] = providerId.Value;
 954                }
 955            }
 956
 0957            timerInfo.SeriesProviderIds = seriesProviderIds;
 0958        }
 959
 960        private bool IsProgramAlreadyInLibrary(TimerInfo program)
 961        {
 0962            if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.
 963            {
 0964                var seriesIds = _libraryManager.GetItemIds(
 0965                    new InternalItemsQuery
 0966                    {
 0967                        IncludeItemTypes = new[] { BaseItemKind.Series },
 0968                        Name = program.Name
 0969                    }).ToArray();
 970
 0971                if (seriesIds.Length == 0)
 972                {
 0973                    return false;
 974                }
 975
 0976                if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue)
 977                {
 0978                    var result = _libraryManager.GetItemIds(new InternalItemsQuery
 0979                    {
 0980                        IncludeItemTypes = new[] { BaseItemKind.Episode },
 0981                        ParentIndexNumber = program.SeasonNumber.Value,
 0982                        IndexNumber = program.EpisodeNumber.Value,
 0983                        AncestorIds = seriesIds,
 0984                        IsVirtualItem = false,
 0985                        Limit = 1
 0986                    });
 987
 0988                    if (result.Count > 0)
 989                    {
 0990                        return true;
 991                    }
 992                }
 993            }
 994
 0995            return false;
 996        }
 997    }
 998}

Methods/Properties

.ctor(Microsoft.Extensions.Logging.ILogger`1<Jellyfin.LiveTv.DefaultLiveTvService>,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.LiveTv.ITunerHostManager,MediaBrowser.Controller.LiveTv.IListingsManager,MediaBrowser.Controller.LiveTv.IRecordingsManager,MediaBrowser.Controller.Library.ILibraryManager,Jellyfin.LiveTv.LiveTvDtoService,Jellyfin.LiveTv.Timers.TimerManager,Jellyfin.LiveTv.Timers.SeriesTimerManager)
get_Name()
get_HomePageUrl()
GetChannelsAsync(System.Threading.CancellationToken)
CancelSeriesTimerAsync(System.String,System.Threading.CancellationToken)
CancelTimerInternal(System.String,System.Boolean,System.Boolean)
CancelTimerAsync(System.String,System.Threading.CancellationToken)
CreateSeriesTimerAsync(MediaBrowser.Controller.LiveTv.SeriesTimerInfo,System.Threading.CancellationToken)
CreateTimerAsync(MediaBrowser.Controller.LiveTv.TimerInfo,System.Threading.CancellationToken)
CreateTimer(MediaBrowser.Controller.LiveTv.TimerInfo,System.Threading.CancellationToken)
UpdateSeriesTimerAsync(MediaBrowser.Controller.LiveTv.SeriesTimerInfo,System.Threading.CancellationToken)
UpdateTimerAsync(MediaBrowser.Controller.LiveTv.TimerInfo,System.Threading.CancellationToken)
UpdateExistingTimerWithNewMetadata(MediaBrowser.Controller.LiveTv.TimerInfo,MediaBrowser.Controller.LiveTv.TimerInfo)
GetTimersAsync(System.Threading.CancellationToken)
GetNewTimerDefaultsAsync(System.Threading.CancellationToken,MediaBrowser.Controller.LiveTv.ProgramInfo)
GetSeriesTimersAsync(System.Threading.CancellationToken)
GetChannelStream(System.String,System.String,System.Threading.CancellationToken)
CloseLiveStream(System.String,System.Threading.CancellationToken)
ResetTuner(System.String,System.Threading.CancellationToken)
GetLiveTvChannel(MediaBrowser.Controller.LiveTv.TimerInfo)
GetProgramInfoFromCache(System.String)
GetProgramInfoFromCache(MediaBrowser.Controller.LiveTv.TimerInfo)
GetProgramInfoFromCache(System.String,System.DateTime)
ShouldCancelTimerForSeriesTimer(MediaBrowser.Controller.LiveTv.SeriesTimerInfo,MediaBrowser.Controller.LiveTv.TimerInfo)
HandleDuplicateShowIds(System.Collections.Generic.List`1<MediaBrowser.Controller.LiveTv.TimerInfo>)
SearchForDuplicateShowIds(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.LiveTv.TimerInfo>)
UpdateTimersForSeriesTimer(MediaBrowser.Controller.LiveTv.SeriesTimerInfo,System.Boolean,System.Boolean)
GetTimersForSeries(MediaBrowser.Controller.LiveTv.SeriesTimerInfo)
CreateTimer(MediaBrowser.Controller.LiveTv.LiveTvProgram,MediaBrowser.Controller.LiveTv.SeriesTimerInfo,System.Collections.Generic.Dictionary`2<System.Guid,MediaBrowser.Controller.LiveTv.LiveTvChannel>)
CopyProgramInfoToTimerInfo(MediaBrowser.Controller.LiveTv.LiveTvProgram,MediaBrowser.Controller.LiveTv.TimerInfo)
CopyProgramInfoToTimerInfo(MediaBrowser.Controller.LiveTv.LiveTvProgram,MediaBrowser.Controller.LiveTv.TimerInfo,System.Collections.Generic.Dictionary`2<System.Guid,MediaBrowser.Controller.LiveTv.LiveTvChannel>)
IsProgramAlreadyInLibrary(MediaBrowser.Controller.LiveTv.TimerInfo)