< 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: 489
Coverable lines: 500
Total lines: 1002
Line coverage: 2.2%
Branch coverage
0%
Covered branches: 0
Total branches: 192
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 2.9% (11/372) Branch coverage: 0% (0/146) Total lines: 9994/19/2026 - 12:14:27 AM Line coverage: 2.2% (11/499) Branch coverage: 0% (0/190) Total lines: 9995/6/2026 - 12:15:23 AM Line coverage: 2.2% (11/500) Branch coverage: 0% (0/192) Total lines: 1002 1/23/2026 - 12:11:06 AM Line coverage: 2.9% (11/372) Branch coverage: 0% (0/146) Total lines: 9994/19/2026 - 12:14:27 AM Line coverage: 2.2% (11/499) Branch coverage: 0% (0/190) Total lines: 9995/6/2026 - 12:15:23 AM Line coverage: 2.2% (11/500) Branch coverage: 0% (0/192) Total lines: 1002

Coverage delta

Coverage delta 1 -1

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.Database.Implementations.Enums;
 15using Jellyfin.Extensions;
 16using Jellyfin.LiveTv.Configuration;
 17using Jellyfin.LiveTv.Timers;
 18using MediaBrowser.Common.Extensions;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.Dto;
 21using MediaBrowser.Controller.Entities;
 22using MediaBrowser.Controller.Library;
 23using MediaBrowser.Controller.LiveTv;
 24using MediaBrowser.Model.Dto;
 25using MediaBrowser.Model.LiveTv;
 26using Microsoft.Extensions.Logging;
 27
 28namespace Jellyfin.LiveTv
 29{
 30    public sealed class DefaultLiveTvService : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds
 31    {
 32        public const string ServiceName = "Emby";
 33
 34        private readonly ILogger<DefaultLiveTvService> _logger;
 35        private readonly IServerConfigurationManager _config;
 36        private readonly ITunerHostManager _tunerHostManager;
 37        private readonly IListingsManager _listingsManager;
 38        private readonly IRecordingsManager _recordingsManager;
 39        private readonly ILibraryManager _libraryManager;
 40        private readonly LiveTvDtoService _tvDtoService;
 41        private readonly TimerManager _timerManager;
 42        private readonly SeriesTimerManager _seriesTimerManager;
 43
 44        public DefaultLiveTvService(
 45            ILogger<DefaultLiveTvService> logger,
 46            IServerConfigurationManager config,
 47            ITunerHostManager tunerHostManager,
 48            IListingsManager listingsManager,
 49            IRecordingsManager recordingsManager,
 50            ILibraryManager libraryManager,
 51            LiveTvDtoService tvDtoService,
 52            TimerManager timerManager,
 53            SeriesTimerManager seriesTimerManager)
 54        {
 2155            _logger = logger;
 2156            _config = config;
 2157            _libraryManager = libraryManager;
 2158            _tunerHostManager = tunerHostManager;
 2159            _listingsManager = listingsManager;
 2160            _recordingsManager = recordingsManager;
 2161            _tvDtoService = tvDtoService;
 2162            _timerManager = timerManager;
 2163            _seriesTimerManager = seriesTimerManager;
 64
 2165            _timerManager.TimerFired += OnTimerManagerTimerFired;
 2166        }
 67
 68        public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated;
 69
 70        public event EventHandler<GenericEventArgs<string>> TimerCancelled;
 71
 72        /// <inheritdoc />
 073        public string Name => ServiceName;
 74
 75        /// <inheritdoc />
 076        public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
 77
 78        public async Task RefreshSeriesTimers(CancellationToken cancellationToken)
 79        {
 080            var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
 81
 082            foreach (var timer in seriesTimers)
 83            {
 084                UpdateTimersForSeriesTimer(timer, false, true);
 85            }
 086        }
 87
 88        public async Task RefreshTimers(CancellationToken cancellationToken)
 89        {
 090            var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
 91
 092            var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
 93
 094            foreach (var timer in timers)
 95            {
 096                if (DateTime.UtcNow > timer.EndDate && _recordingsManager.GetActiveRecordingPath(timer.Id) is null)
 97                {
 098                    _timerManager.Delete(timer);
 099                    continue;
 100                }
 101
 0102                if (string.IsNullOrWhiteSpace(timer.ProgramId) || string.IsNullOrWhiteSpace(timer.ChannelId))
 103                {
 104                    continue;
 105                }
 106
 0107                var program = GetProgramInfoFromCache(timer);
 0108                if (program is null)
 109                {
 0110                    _timerManager.Delete(timer);
 0111                    continue;
 112                }
 113
 0114                CopyProgramInfoToTimerInfo(program, timer, tempChannelCache);
 0115                _timerManager.Update(timer);
 116            }
 0117        }
 118
 119        private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationTo
 120        {
 0121            var channels = new List<ChannelInfo>();
 122
 0123            foreach (var hostInstance in _tunerHostManager.TunerHosts)
 124            {
 125                try
 126                {
 0127                    var tunerChannels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(fa
 128
 0129                    channels.AddRange(tunerChannels);
 0130                }
 0131                catch (Exception ex)
 132                {
 0133                    _logger.LogError(ex, "Error getting channels");
 0134                }
 135            }
 136
 0137            await _listingsManager.AddProviderMetadata(channels, enableCache, cancellationToken).ConfigureAwait(false);
 138
 0139            return channels;
 0140        }
 141
 142        public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
 143        {
 0144            return GetChannelsAsync(false, cancellationToken);
 145        }
 146
 147        public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
 148        {
 0149            var timers = _timerManager
 0150                .GetAll()
 0151                .Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase))
 0152                .ToList();
 153
 0154            foreach (var timer in timers)
 155            {
 0156                CancelTimerInternal(timer.Id, true, true);
 157            }
 158
 0159            var remove = _seriesTimerManager.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.
 0160            if (remove is not null)
 161            {
 0162                _seriesTimerManager.Delete(remove);
 163            }
 164
 0165            return Task.CompletedTask;
 166        }
 167
 168        private void CancelTimerInternal(string timerId, bool isSeriesCancelled, bool isManualCancellation)
 169        {
 0170            var timer = _timerManager.GetTimer(timerId);
 0171            if (timer is not null)
 172            {
 0173                var statusChanging = timer.Status != RecordingStatus.Cancelled;
 0174                timer.Status = RecordingStatus.Cancelled;
 175
 0176                if (isManualCancellation)
 177                {
 0178                    timer.IsManual = true;
 179                }
 180
 0181                if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled)
 182                {
 0183                    _timerManager.Delete(timer);
 184                }
 185                else
 186                {
 0187                    _timerManager.AddOrUpdate(timer, false);
 188                }
 189
 0190                if (statusChanging && TimerCancelled is not null)
 191                {
 0192                    TimerCancelled(this, new GenericEventArgs<string>(timerId));
 193                }
 194            }
 195
 0196            _recordingsManager.CancelRecording(timerId, timer);
 0197        }
 198
 199        public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
 200        {
 0201            CancelTimerInternal(timerId, false, true);
 0202            return Task.CompletedTask;
 203        }
 204
 205        public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
 206        {
 0207            throw new NotImplementedException();
 208        }
 209
 210        public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
 211        {
 0212            throw new NotImplementedException();
 213        }
 214
 215        public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
 216        {
 0217            var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ?
 0218                null :
 0219                _timerManager.GetTimerByProgramId(info.ProgramId);
 220
 0221            if (existingTimer is not null)
 222            {
 0223                if (existingTimer.Status == RecordingStatus.Cancelled
 0224                    || existingTimer.Status == RecordingStatus.Completed)
 225                {
 0226                    existingTimer.Status = RecordingStatus.New;
 0227                    existingTimer.IsManual = true;
 0228                    _timerManager.Update(existingTimer);
 0229                    return Task.FromResult(existingTimer.Id);
 230                }
 231
 0232                throw new ArgumentException("A scheduled recording already exists for this program.");
 233            }
 234
 0235            info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 236
 0237            LiveTvProgram programInfo = null;
 238
 0239            if (!string.IsNullOrWhiteSpace(info.ProgramId))
 240            {
 0241                programInfo = GetProgramInfoFromCache(info);
 242            }
 243
 0244            if (programInfo is null)
 245            {
 0246                _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramI
 0247                programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate);
 248            }
 249
 0250            if (programInfo is not null)
 251            {
 0252                CopyProgramInfoToTimerInfo(programInfo, info);
 253            }
 254
 0255            info.IsManual = true;
 0256            _timerManager.Add(info);
 257
 0258            TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info));
 259
 0260            return Task.FromResult(info.Id);
 261        }
 262
 263        public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
 264        {
 0265            info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 266
 267            // populate info.seriesID
 0268            var program = GetProgramInfoFromCache(info.ProgramId);
 269
 0270            if (program is not null)
 271            {
 0272                info.SeriesId = program.ExternalSeriesId;
 273            }
 274            else
 275            {
 0276                throw new InvalidOperationException("SeriesId for program not found");
 277            }
 278
 279            // If any timers have already been manually created, make sure they don't get cancelled
 0280            var existingTimers = (await GetTimersAsync(CancellationToken.None).ConfigureAwait(false))
 0281                .Where(i =>
 0282                {
 0283                    if (string.Equals(i.ProgramId, info.ProgramId, StringComparison.OrdinalIgnoreCase) && !string.IsNull
 0284                    {
 0285                        return true;
 0286                    }
 0287
 0288                    if (string.Equals(i.SeriesId, info.SeriesId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOr
 0289                    {
 0290                        return true;
 0291                    }
 0292
 0293                    return false;
 0294                })
 0295                .ToList();
 296
 0297            _seriesTimerManager.Add(info);
 298
 0299            foreach (var timer in existingTimers)
 300            {
 0301                timer.SeriesTimerId = info.Id;
 0302                timer.IsManual = true;
 303
 0304                _timerManager.AddOrUpdate(timer, false);
 305            }
 306
 0307            UpdateTimersForSeriesTimer(info, true, false);
 308
 0309            return info.Id;
 0310        }
 311
 312        public Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
 313        {
 0314            var instance = _seriesTimerManager.GetAll().FirstOrDefault(i => string.Equals(i.Id, info.Id, StringCompariso
 315
 0316            if (instance is not null)
 317            {
 0318                instance.ChannelId = info.ChannelId;
 0319                instance.Days = info.Days;
 0320                instance.EndDate = info.EndDate;
 0321                instance.IsPostPaddingRequired = info.IsPostPaddingRequired;
 0322                instance.IsPrePaddingRequired = info.IsPrePaddingRequired;
 0323                instance.PostPaddingSeconds = info.PostPaddingSeconds;
 0324                instance.PrePaddingSeconds = info.PrePaddingSeconds;
 0325                instance.Priority = info.Priority;
 0326                instance.RecordAnyChannel = info.RecordAnyChannel;
 0327                instance.RecordAnyTime = info.RecordAnyTime;
 0328                instance.RecordNewOnly = info.RecordNewOnly;
 0329                instance.SkipEpisodesInLibrary = info.SkipEpisodesInLibrary;
 0330                instance.KeepUpTo = info.KeepUpTo;
 0331                instance.KeepUntil = info.KeepUntil;
 0332                instance.StartDate = info.StartDate;
 333
 0334                _seriesTimerManager.Update(instance);
 335
 0336                UpdateTimersForSeriesTimer(instance, true, true);
 337            }
 338
 0339            return Task.CompletedTask;
 340        }
 341
 342        public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken)
 343        {
 0344            var existingTimer = _timerManager.GetTimer(updatedTimer.Id);
 345
 0346            if (existingTimer is null)
 347            {
 0348                throw new ResourceNotFoundException();
 349            }
 350
 351            // Only update if not currently active
 0352            if (_recordingsManager.GetActiveRecordingPath(updatedTimer.Id) is null)
 353            {
 0354                existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
 0355                existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
 0356                existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired;
 0357                existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired;
 358
 0359                _timerManager.Update(existingTimer);
 360            }
 361
 0362            return Task.CompletedTask;
 363        }
 364
 365        private static void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer)
 366        {
 367            // Update the program info but retain the status
 0368            existingTimer.ChannelId = updatedTimer.ChannelId;
 0369            existingTimer.CommunityRating = updatedTimer.CommunityRating;
 0370            existingTimer.EndDate = updatedTimer.EndDate;
 0371            existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber;
 0372            existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle;
 0373            existingTimer.Genres = updatedTimer.Genres;
 0374            existingTimer.IsMovie = updatedTimer.IsMovie;
 0375            existingTimer.IsSeries = updatedTimer.IsSeries;
 0376            existingTimer.Tags = updatedTimer.Tags;
 0377            existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries;
 0378            existingTimer.IsRepeat = updatedTimer.IsRepeat;
 0379            existingTimer.Name = updatedTimer.Name;
 0380            existingTimer.OfficialRating = updatedTimer.OfficialRating;
 0381            existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate;
 0382            existingTimer.Overview = updatedTimer.Overview;
 0383            existingTimer.ProductionYear = updatedTimer.ProductionYear;
 0384            existingTimer.ProgramId = updatedTimer.ProgramId;
 0385            existingTimer.SeasonNumber = updatedTimer.SeasonNumber;
 0386            existingTimer.StartDate = updatedTimer.StartDate;
 0387            existingTimer.ShowId = updatedTimer.ShowId;
 0388            existingTimer.ProviderIds = updatedTimer.ProviderIds;
 0389            existingTimer.SeriesProviderIds = updatedTimer.SeriesProviderIds;
 0390        }
 391
 392        public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
 393        {
 0394            var excludeStatues = new List<RecordingStatus>
 0395            {
 0396                RecordingStatus.Completed
 0397            };
 398
 0399            var timers = _timerManager.GetAll()
 0400                .Where(i => !excludeStatues.Contains(i.Status));
 401
 0402            return Task.FromResult(timers);
 403        }
 404
 405        public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program =
 406        {
 0407            var config = _config.GetLiveTvConfiguration();
 408
 0409            var defaults = new SeriesTimerInfo()
 0410            {
 0411                PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
 0412                PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
 0413                RecordAnyChannel = false,
 0414                RecordAnyTime = true,
 0415                RecordNewOnly = true,
 0416
 0417                Days = new List<DayOfWeek>
 0418                {
 0419                    DayOfWeek.Sunday,
 0420                    DayOfWeek.Monday,
 0421                    DayOfWeek.Tuesday,
 0422                    DayOfWeek.Wednesday,
 0423                    DayOfWeek.Thursday,
 0424                    DayOfWeek.Friday,
 0425                    DayOfWeek.Saturday
 0426                }
 0427            };
 428
 0429            if (program is not null)
 430            {
 0431                defaults.SeriesId = program.SeriesId;
 0432                defaults.ProgramId = program.Id;
 0433                defaults.RecordNewOnly = !program.IsRepeat;
 0434                defaults.Name = program.Name;
 435            }
 436
 0437            defaults.SkipEpisodesInLibrary = defaults.RecordNewOnly;
 0438            defaults.KeepUntil = KeepUntil.UntilDeleted;
 439
 0440            return Task.FromResult(defaults);
 441        }
 442
 443        public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
 444        {
 0445            return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerManager.GetAll());
 446        }
 447
 448        public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime e
 449        {
 0450            var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
 0451            var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
 452
 0453            return await _listingsManager.GetProgramsAsync(channel, startDateUtc, endDateUtc, cancellationToken)
 0454                .ConfigureAwait(false);
 0455        }
 456
 457        public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationT
 458        {
 0459            throw new NotImplementedException();
 460        }
 461
 462        public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<
 463        {
 0464            _logger.LogInformation("Streaming Channel {Id}", channelId);
 465
 0466            var result = string.IsNullOrEmpty(streamId) ?
 0467                null :
 0468                currentLiveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.Ordi
 469
 0470            if (result is not null && result.EnableStreamSharing)
 471            {
 0472                result.ConsumerCount++;
 473
 0474                _logger.LogInformation("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount);
 475
 0476                return result;
 477            }
 478
 0479            foreach (var hostInstance in _tunerHostManager.TunerHosts)
 480            {
 481                try
 482                {
 0483                    result = await hostInstance.GetChannelStream(channelId, streamId, currentLiveStreams, cancellationTo
 484
 0485                    var openedMediaSource = result.MediaSource;
 486
 0487                    result.OriginalStreamId = streamId;
 488
 0489                    _logger.LogInformation("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStre
 490
 0491                    return result;
 492                }
 0493                catch (FileNotFoundException)
 494                {
 0495                }
 0496                catch (OperationCanceledException)
 497                {
 0498                }
 499            }
 500
 0501            throw new ResourceNotFoundException("Tuner not found.");
 0502        }
 503
 504        public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancel
 505        {
 0506            if (string.IsNullOrWhiteSpace(channelId))
 507            {
 0508                throw new ArgumentNullException(nameof(channelId));
 509            }
 510
 0511            foreach (var hostInstance in _tunerHostManager.TunerHosts)
 512            {
 513                try
 514                {
 0515                    var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).Configur
 516
 0517                    if (sources.Count > 0)
 518                    {
 0519                        return sources;
 520                    }
 0521                }
 0522                catch (NotImplementedException)
 523                {
 0524                }
 525            }
 526
 0527            throw new NotImplementedException();
 0528        }
 529
 530        public Task CloseLiveStream(string id, CancellationToken cancellationToken)
 531        {
 0532            return Task.CompletedTask;
 533        }
 534
 535        public Task ResetTuner(string id, CancellationToken cancellationToken)
 536        {
 0537            return Task.CompletedTask;
 538        }
 539
 540        private async void OnTimerManagerTimerFired(object sender, GenericEventArgs<TimerInfo> e)
 541        {
 0542            var timer = e.Argument;
 543
 0544            _logger.LogInformation("Recording timer fired for {0}.", timer.Name);
 545
 546            try
 547            {
 0548                var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds);
 0549                if (recordingEndDate <= DateTime.UtcNow)
 550                {
 0551                    _logger.LogWarning("Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already
 0552                    _timerManager.Delete(timer);
 0553                    return;
 554                }
 555
 0556                var activeRecordingInfo = new ActiveRecordingInfo
 0557                {
 0558                    CancellationTokenSource = new CancellationTokenSource(),
 0559                    Timer = timer,
 0560                    Id = timer.Id
 0561                };
 562
 0563                if (_recordingsManager.GetActiveRecordingPath(timer.Id) is not null)
 564                {
 0565                    _logger.LogInformation("Skipping RecordStream because it's already in progress.");
 0566                    return;
 567                }
 568
 0569                LiveTvProgram programInfo = null;
 0570                if (!string.IsNullOrWhiteSpace(timer.ProgramId))
 571                {
 0572                    programInfo = GetProgramInfoFromCache(timer);
 573                }
 574
 0575                if (programInfo is null)
 576                {
 0577                    _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.Pro
 0578                    programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
 579                }
 580
 0581                if (programInfo is not null)
 582                {
 0583                    CopyProgramInfoToTimerInfo(programInfo, timer);
 584                }
 585
 0586                await _recordingsManager.RecordStream(activeRecordingInfo, GetLiveTvChannel(timer), recordingEndDate)
 0587                    .ConfigureAwait(false);
 0588            }
 0589            catch (OperationCanceledException)
 590            {
 0591            }
 0592            catch (Exception ex)
 593            {
 0594                _logger.LogError(ex, "Error recording stream");
 0595            }
 0596        }
 597
 598        private BaseItem GetLiveTvChannel(TimerInfo timer)
 599        {
 0600            var internalChannelId = _tvDtoService.GetInternalChannelId(Name, timer.ChannelId);
 0601            return _libraryManager.GetItemById(internalChannelId);
 602        }
 603
 604        private LiveTvProgram GetProgramInfoFromCache(string programId)
 605        {
 0606            var query = new InternalItemsQuery
 0607            {
 0608                ItemIds = [_tvDtoService.GetInternalProgramId(programId)],
 0609                Limit = 1,
 0610                DtoOptions = new DtoOptions()
 0611            };
 612
 0613            return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault();
 614        }
 615
 616        private LiveTvProgram GetProgramInfoFromCache(TimerInfo timer)
 617        {
 0618            return GetProgramInfoFromCache(timer.ProgramId);
 619        }
 620
 621        private LiveTvProgram GetProgramInfoFromCache(string channelId, DateTime startDateUtc)
 622        {
 0623            var query = new InternalItemsQuery
 0624            {
 0625                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
 0626                Limit = 1,
 0627                DtoOptions = new DtoOptions(true)
 0628                {
 0629                    EnableImages = false
 0630                },
 0631                MinStartDate = startDateUtc.AddMinutes(-3),
 0632                MaxStartDate = startDateUtc.AddMinutes(3),
 0633                OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }
 0634            };
 635
 0636            if (!string.IsNullOrWhiteSpace(channelId))
 637            {
 0638                query.ChannelIds = [_tvDtoService.GetInternalChannelId(Name, channelId)];
 639            }
 640
 0641            return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault();
 642        }
 643
 644        private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
 645        {
 0646            if (timer.IsManual)
 647            {
 0648                return false;
 649            }
 650
 0651            if (!seriesTimer.RecordAnyTime
 0652                && Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMin
 653            {
 0654                return true;
 655            }
 656
 0657            if (seriesTimer.RecordNewOnly && timer.IsRepeat)
 658            {
 0659                return true;
 660            }
 661
 0662            if (!seriesTimer.RecordAnyChannel
 0663                && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
 664            {
 0665                return true;
 666            }
 667
 0668            return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer);
 669        }
 670
 671        private void HandleDuplicateShowIds(List<TimerInfo> timers)
 672        {
 673            // sort showings by HD channels first, then by startDate, record earliest showing possible
 0674            foreach (var timer in timers.OrderByDescending(t => GetLiveTvChannel(t).IsHD).ThenBy(t => t.StartDate).Skip(
 675            {
 0676                timer.Status = RecordingStatus.Cancelled;
 0677                _timerManager.Update(timer);
 678            }
 0679        }
 680
 681        private void SearchForDuplicateShowIds(IEnumerable<TimerInfo> timers)
 682        {
 0683            var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList();
 684
 0685            foreach (var group in groups)
 686            {
 0687                if (string.IsNullOrWhiteSpace(group.Key))
 688                {
 689                    continue;
 690                }
 691
 0692                var groupTimers = group.ToList();
 693
 0694                if (groupTimers.Count < 2)
 695                {
 696                    continue;
 697                }
 698
 699                // Skip ShowId without SubKey from duplicate removal actions - https://github.com/jellyfin/jellyfin/issu
 0700                if (group.Key.EndsWith("0000", StringComparison.Ordinal))
 701                {
 702                    continue;
 703                }
 704
 0705                HandleDuplicateShowIds(groupTimers);
 706            }
 0707        }
 708
 709        private void UpdateTimersForSeriesTimer(SeriesTimerInfo seriesTimer, bool updateTimerSettings, bool deleteInvali
 710        {
 0711            var allTimers = GetTimersForSeries(seriesTimer).ToList();
 712
 0713            var enabledTimersForSeries = new List<TimerInfo>();
 0714            foreach (var timer in allTimers)
 715            {
 0716                var existingTimer = _timerManager.GetTimer(timer.Id)
 0717                    ?? (string.IsNullOrWhiteSpace(timer.ProgramId)
 0718                        ? null
 0719                        : _timerManager.GetTimerByProgramId(timer.ProgramId));
 720
 0721                if (existingTimer is null)
 722                {
 0723                    if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
 724                    {
 0725                        timer.Status = RecordingStatus.Cancelled;
 726                    }
 727                    else
 728                    {
 0729                        enabledTimersForSeries.Add(timer);
 730                    }
 731
 0732                    _timerManager.Add(timer);
 733
 0734                    TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
 735                }
 736
 737                // Only update if not currently active - test both new timer and existing in case Id's are different
 738                // Id's could be different if the timer was created manually prior to series timer creation
 0739                else if (_recordingsManager.GetActiveRecordingPath(timer.Id) is null
 0740                         && _recordingsManager.GetActiveRecordingPath(existingTimer.Id) is null)
 741                {
 0742                    UpdateExistingTimerWithNewMetadata(existingTimer, timer);
 743
 744                    // Needed by ShouldCancelTimerForSeriesTimer
 0745                    timer.IsManual = existingTimer.IsManual;
 746
 0747                    if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
 748                    {
 0749                        existingTimer.Status = RecordingStatus.Cancelled;
 750                    }
 0751                    else if (!existingTimer.IsManual)
 752                    {
 0753                        existingTimer.Status = RecordingStatus.New;
 754                    }
 755
 0756                    if (existingTimer.Status != RecordingStatus.Cancelled)
 757                    {
 0758                        enabledTimersForSeries.Add(existingTimer);
 759                    }
 760
 0761                    if (updateTimerSettings)
 762                    {
 0763                        existingTimer.KeepUntil = seriesTimer.KeepUntil;
 0764                        existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
 0765                        existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
 0766                        existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
 0767                        existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
 0768                        existingTimer.Priority = seriesTimer.Priority;
 0769                        existingTimer.SeriesTimerId = seriesTimer.Id;
 770                    }
 771
 0772                    existingTimer.SeriesTimerId = seriesTimer.Id;
 0773                    _timerManager.Update(existingTimer);
 774                }
 775            }
 776
 0777            if (seriesTimer.SkipEpisodesInLibrary)
 778            {
 0779                SearchForDuplicateShowIds(enabledTimersForSeries);
 780            }
 781
 0782            if (deleteInvalidTimers)
 783            {
 0784                var allTimerIds = allTimers
 0785                    .Select(i => i.Id)
 0786                    .ToList();
 787
 0788                var deleteStatuses = new[]
 0789                {
 0790                    RecordingStatus.New
 0791                };
 792
 0793                var deletes = _timerManager.GetAll()
 0794                    .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
 0795                    .Where(i => !allTimerIds.Contains(i.Id, StringComparison.OrdinalIgnoreCase) && i.StartDate > DateTim
 0796                    .Where(i => deleteStatuses.Contains(i.Status))
 0797                    .ToList();
 798
 0799                foreach (var timer in deletes)
 800                {
 0801                    CancelTimerInternal(timer.Id, false, false);
 802                }
 803            }
 0804        }
 805
 806        private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer)
 807        {
 0808            ArgumentNullException.ThrowIfNull(seriesTimer);
 809
 0810            var query = new InternalItemsQuery
 0811            {
 0812                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
 0813                ExternalSeriesId = seriesTimer.SeriesId,
 0814                DtoOptions = new DtoOptions(true)
 0815                {
 0816                    EnableImages = false
 0817                },
 0818                MinEndDate = DateTime.UtcNow
 0819            };
 820
 0821            if (string.IsNullOrEmpty(seriesTimer.SeriesId))
 822            {
 0823                query.Name = seriesTimer.Name;
 824            }
 825
 0826            if (!seriesTimer.RecordAnyChannel)
 827            {
 0828                query.ChannelIds = [_tvDtoService.GetInternalChannelId(Name, seriesTimer.ChannelId)];
 829            }
 830
 0831            var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
 832
 0833            return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().Select(i => CreateTimer(i, seriesTimer, temp
 834        }
 835
 836        private TimerInfo CreateTimer(LiveTvProgram parent, SeriesTimerInfo seriesTimer, Dictionary<Guid, LiveTvChannel>
 837        {
 0838            string channelId = seriesTimer.RecordAnyChannel ? null : seriesTimer.ChannelId;
 839
 0840            if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.IsEmpty())
 841            {
 0842                if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel))
 843                {
 0844                    channel = _libraryManager.GetItemList(
 0845                        new InternalItemsQuery
 0846                        {
 0847                            IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
 0848                            ItemIds = new[] { parent.ChannelId },
 0849                            DtoOptions = new DtoOptions()
 0850                        }).FirstOrDefault() as LiveTvChannel;
 851
 0852                    if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId))
 853                    {
 0854                        tempChannelCache[parent.ChannelId] = channel;
 855                    }
 856                }
 857
 0858                if (channel is not null || tempChannelCache.TryGetValue(parent.ChannelId, out channel))
 859                {
 0860                    channelId = channel.ExternalId;
 861                }
 862            }
 863
 0864            var timer = new TimerInfo
 0865            {
 0866                ChannelId = channelId,
 0867                Id = (seriesTimer.Id + parent.ExternalId).GetMD5().ToString("N", CultureInfo.InvariantCulture),
 0868                StartDate = parent.StartDate,
 0869                EndDate = parent.EndDate.Value,
 0870                ProgramId = parent.ExternalId,
 0871                PrePaddingSeconds = seriesTimer.PrePaddingSeconds,
 0872                PostPaddingSeconds = seriesTimer.PostPaddingSeconds,
 0873                IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired,
 0874                IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired,
 0875                KeepUntil = seriesTimer.KeepUntil,
 0876                Priority = seriesTimer.Priority,
 0877                Name = parent.Name,
 0878                Overview = parent.Overview,
 0879                SeriesId = parent.ExternalSeriesId,
 0880                SeriesTimerId = seriesTimer.Id,
 0881                ShowId = parent.ShowId
 0882            };
 883
 0884            CopyProgramInfoToTimerInfo(parent, timer, tempChannelCache);
 885
 0886            return timer;
 887        }
 888
 889        private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo)
 890        {
 0891            var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
 0892            CopyProgramInfoToTimerInfo(programInfo, timerInfo, tempChannelCache);
 0893        }
 894
 895        private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo, Dictionary<Guid, LiveTvC
 896        {
 0897            string channelId = null;
 898
 0899            if (!programInfo.ChannelId.IsEmpty())
 900            {
 0901                if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel))
 902                {
 0903                    channel = _libraryManager.GetItemList(
 0904                        new InternalItemsQuery
 0905                        {
 0906                            IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
 0907                            ItemIds = new[] { programInfo.ChannelId },
 0908                            DtoOptions = new DtoOptions()
 0909                        }).FirstOrDefault() as LiveTvChannel;
 910
 0911                    if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId))
 912                    {
 0913                        tempChannelCache[programInfo.ChannelId] = channel;
 914                    }
 915                }
 916
 0917                if (channel is not null || tempChannelCache.TryGetValue(programInfo.ChannelId, out channel))
 918                {
 0919                    channelId = channel.ExternalId;
 920                }
 921            }
 922
 0923            timerInfo.Name = programInfo.Name;
 0924            timerInfo.StartDate = programInfo.StartDate;
 0925            timerInfo.EndDate = programInfo.EndDate.Value;
 926
 0927            if (!string.IsNullOrWhiteSpace(channelId))
 928            {
 0929                timerInfo.ChannelId = channelId;
 930            }
 931
 0932            timerInfo.SeasonNumber = programInfo.ParentIndexNumber;
 0933            timerInfo.EpisodeNumber = programInfo.IndexNumber;
 0934            timerInfo.IsMovie = programInfo.IsMovie;
 0935            timerInfo.ProductionYear = programInfo.ProductionYear;
 0936            timerInfo.EpisodeTitle = programInfo.EpisodeTitle;
 0937            timerInfo.OriginalAirDate = programInfo.PremiereDate;
 0938            timerInfo.IsProgramSeries = programInfo.IsSeries;
 939
 0940            timerInfo.IsSeries = programInfo.IsSeries;
 941
 0942            timerInfo.CommunityRating = programInfo.CommunityRating;
 0943            timerInfo.Overview = programInfo.Overview;
 0944            timerInfo.OfficialRating = programInfo.OfficialRating;
 0945            timerInfo.IsRepeat = programInfo.IsRepeat;
 0946            timerInfo.SeriesId = programInfo.ExternalSeriesId;
 0947            timerInfo.ProviderIds = programInfo.ProviderIds;
 0948            timerInfo.Tags = programInfo.Tags;
 949
 0950            var seriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 951
 0952            foreach (var providerId in timerInfo.ProviderIds)
 953            {
 954                const string Search = "Series";
 0955                if (providerId.Key.StartsWith(Search, StringComparison.OrdinalIgnoreCase))
 956                {
 0957                    seriesProviderIds[providerId.Key.Substring(Search.Length)] = providerId.Value;
 958                }
 959            }
 960
 0961            timerInfo.SeriesProviderIds = seriesProviderIds;
 0962        }
 963
 964        private bool IsProgramAlreadyInLibrary(TimerInfo program)
 965        {
 0966            if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.
 967            {
 0968                var seriesIds = _libraryManager.GetItemIds(
 0969                    new InternalItemsQuery
 0970                    {
 0971                        IncludeItemTypes = new[] { BaseItemKind.Series },
 0972                        Name = program.Name
 0973                    }).ToArray();
 974
 0975                if (seriesIds.Length == 0)
 976                {
 0977                    return false;
 978                }
 979
 0980                if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue)
 981                {
 0982                    var result = _libraryManager.GetItemIds(new InternalItemsQuery
 0983                    {
 0984                        IncludeItemTypes = new[] { BaseItemKind.Episode },
 0985                        ParentIndexNumber = program.SeasonNumber.Value,
 0986                        IndexNumber = program.EpisodeNumber.Value,
 0987                        AncestorIds = seriesIds,
 0988                        IsVirtualItem = false,
 0989                        Limit = 1
 0990                    });
 991
 0992                    if (result.Count > 0)
 993                    {
 0994                        return true;
 995                    }
 996                }
 997            }
 998
 0999            return false;
 1000        }
 1001    }
 1002}

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()
RefreshSeriesTimers()
RefreshTimers()
GetChannelsAsync()
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)
CreateSeriesTimer()
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)
GetProgramsAsync()
GetChannelStream(System.String,System.String,System.Threading.CancellationToken)
GetChannelStreamWithDirectStreamProvider()
GetChannelStreamMediaSources()
CloseLiveStream(System.String,System.Threading.CancellationToken)
ResetTuner(System.String,System.Threading.CancellationToken)
OnTimerManagerTimerFired()
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)