You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jellyfin/src/Jellyfin.LiveTv/DefaultLiveTvService.cs

999 lines
38 KiB

#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Extensions;
using Jellyfin.LiveTv.Configuration;
using Jellyfin.LiveTv.Timers;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
namespace Jellyfin.LiveTv
{
public sealed class DefaultLiveTvService : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds
{
public const string ServiceName = "Emby";
private readonly ILogger<DefaultLiveTvService> _logger;
private readonly IServerConfigurationManager _config;
private readonly ITunerHostManager _tunerHostManager;
private readonly IListingsManager _listingsManager;
private readonly IRecordingsManager _recordingsManager;
private readonly ILibraryManager _libraryManager;
private readonly LiveTvDtoService _tvDtoService;
private readonly TimerManager _timerManager;
private readonly SeriesTimerManager _seriesTimerManager;
public DefaultLiveTvService(
ILogger<DefaultLiveTvService> logger,
IServerConfigurationManager config,
ITunerHostManager tunerHostManager,
IListingsManager listingsManager,
IRecordingsManager recordingsManager,
ILibraryManager libraryManager,
LiveTvDtoService tvDtoService,
TimerManager timerManager,
SeriesTimerManager seriesTimerManager)
{
_logger = logger;
_config = config;
_libraryManager = libraryManager;
_tunerHostManager = tunerHostManager;
_listingsManager = listingsManager;
_recordingsManager = recordingsManager;
_tvDtoService = tvDtoService;
_timerManager = timerManager;
_seriesTimerManager = seriesTimerManager;
_timerManager.TimerFired += OnTimerManagerTimerFired;
}
public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated;
public event EventHandler<GenericEventArgs<string>> TimerCancelled;
/// <inheritdoc />
public string Name => ServiceName;
/// <inheritdoc />
public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
public async Task RefreshSeriesTimers(CancellationToken cancellationToken)
{
var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
foreach (var timer in seriesTimers)
{
UpdateTimersForSeriesTimer(timer, false, true);
}
}
public async Task RefreshTimers(CancellationToken cancellationToken)
{
var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
foreach (var timer in timers)
{
if (DateTime.UtcNow > timer.EndDate && _recordingsManager.GetActiveRecordingPath(timer.Id) is null)
{
_timerManager.Delete(timer);
continue;
}
if (string.IsNullOrWhiteSpace(timer.ProgramId) || string.IsNullOrWhiteSpace(timer.ChannelId))
{
continue;
}
var program = GetProgramInfoFromCache(timer);
if (program is null)
{
_timerManager.Delete(timer);
continue;
}
CopyProgramInfoToTimerInfo(program, timer, tempChannelCache);
_timerManager.Update(timer);
}
}
private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
{
var channels = new List<ChannelInfo>();
foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
var tunerChannels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false);
channels.AddRange(tunerChannels);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting channels");
}
}
await _listingsManager.AddProviderMetadata(channels, enableCache, cancellationToken).ConfigureAwait(false);
return channels;
}
public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
{
return GetChannelsAsync(false, cancellationToken);
}
public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
{
var timers = _timerManager
.GetAll()
.Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var timer in timers)
{
CancelTimerInternal(timer.Id, true, true);
}
var remove = _seriesTimerManager.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
if (remove is not null)
{
_seriesTimerManager.Delete(remove);
}
return Task.CompletedTask;
}
private void CancelTimerInternal(string timerId, bool isSeriesCancelled, bool isManualCancellation)
{
var timer = _timerManager.GetTimer(timerId);
if (timer is not null)
{
var statusChanging = timer.Status != RecordingStatus.Cancelled;
timer.Status = RecordingStatus.Cancelled;
if (isManualCancellation)
{
timer.IsManual = true;
}
if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled)
{
_timerManager.Delete(timer);
}
else
{
_timerManager.AddOrUpdate(timer, false);
}
if (statusChanging && TimerCancelled is not null)
{
TimerCancelled(this, new GenericEventArgs<string>(timerId));
}
}
_recordingsManager.CancelRecording(timerId, timer);
}
public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
{
CancelTimerInternal(timerId, false, true);
return Task.CompletedTask;
}
public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
{
var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ?
null :
_timerManager.GetTimerByProgramId(info.ProgramId);
if (existingTimer is not null)
{
if (existingTimer.Status == RecordingStatus.Cancelled
|| existingTimer.Status == RecordingStatus.Completed)
{
existingTimer.Status = RecordingStatus.New;
existingTimer.IsManual = true;
_timerManager.Update(existingTimer);
return Task.FromResult(existingTimer.Id);
}
throw new ArgumentException("A scheduled recording already exists for this program.");
}
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
LiveTvProgram programInfo = null;
if (!string.IsNullOrWhiteSpace(info.ProgramId))
{
programInfo = GetProgramInfoFromCache(info);
}
if (programInfo is null)
{
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId);
programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate);
}
if (programInfo is not null)
{
CopyProgramInfoToTimerInfo(programInfo, info);
}
info.IsManual = true;
_timerManager.Add(info);
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info));
return Task.FromResult(info.Id);
}
public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
{
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
// populate info.seriesID
var program = GetProgramInfoFromCache(info.ProgramId);
if (program is not null)
{
info.SeriesId = program.ExternalSeriesId;
}
else
{
throw new InvalidOperationException("SeriesId for program not found");
}
// If any timers have already been manually created, make sure they don't get cancelled
var existingTimers = (await GetTimersAsync(CancellationToken.None).ConfigureAwait(false))
.Where(i =>
{
if (string.Equals(i.ProgramId, info.ProgramId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.ProgramId))
{
return true;
}
if (string.Equals(i.SeriesId, info.SeriesId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.SeriesId))
{
return true;
}
return false;
})
.ToList();
_seriesTimerManager.Add(info);
foreach (var timer in existingTimers)
{
timer.SeriesTimerId = info.Id;
timer.IsManual = true;
_timerManager.AddOrUpdate(timer, false);
}
UpdateTimersForSeriesTimer(info, true, false);
return info.Id;
}
public Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
var instance = _seriesTimerManager.GetAll().FirstOrDefault(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
if (instance is not null)
{
instance.ChannelId = info.ChannelId;
instance.Days = info.Days;
instance.EndDate = info.EndDate;
instance.IsPostPaddingRequired = info.IsPostPaddingRequired;
instance.IsPrePaddingRequired = info.IsPrePaddingRequired;
instance.PostPaddingSeconds = info.PostPaddingSeconds;
instance.PrePaddingSeconds = info.PrePaddingSeconds;
instance.Priority = info.Priority;
instance.RecordAnyChannel = info.RecordAnyChannel;
instance.RecordAnyTime = info.RecordAnyTime;
instance.RecordNewOnly = info.RecordNewOnly;
instance.SkipEpisodesInLibrary = info.SkipEpisodesInLibrary;
instance.KeepUpTo = info.KeepUpTo;
instance.KeepUntil = info.KeepUntil;
instance.StartDate = info.StartDate;
_seriesTimerManager.Update(instance);
UpdateTimersForSeriesTimer(instance, true, true);
}
return Task.CompletedTask;
}
public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken)
{
var existingTimer = _timerManager.GetTimer(updatedTimer.Id);
if (existingTimer is null)
{
throw new ResourceNotFoundException();
}
// Only update if not currently active
if (_recordingsManager.GetActiveRecordingPath(updatedTimer.Id) is null)
{
existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired;
existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired;
_timerManager.Update(existingTimer);
}
return Task.CompletedTask;
}
private static void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer)
{
// Update the program info but retain the status
existingTimer.ChannelId = updatedTimer.ChannelId;
existingTimer.CommunityRating = updatedTimer.CommunityRating;
existingTimer.EndDate = updatedTimer.EndDate;
existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber;
existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle;
existingTimer.Genres = updatedTimer.Genres;
existingTimer.IsMovie = updatedTimer.IsMovie;
existingTimer.IsSeries = updatedTimer.IsSeries;
existingTimer.Tags = updatedTimer.Tags;
existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries;
existingTimer.IsRepeat = updatedTimer.IsRepeat;
existingTimer.Name = updatedTimer.Name;
existingTimer.OfficialRating = updatedTimer.OfficialRating;
existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate;
existingTimer.Overview = updatedTimer.Overview;
existingTimer.ProductionYear = updatedTimer.ProductionYear;
existingTimer.ProgramId = updatedTimer.ProgramId;
existingTimer.SeasonNumber = updatedTimer.SeasonNumber;
existingTimer.StartDate = updatedTimer.StartDate;
existingTimer.ShowId = updatedTimer.ShowId;
existingTimer.ProviderIds = updatedTimer.ProviderIds;
existingTimer.SeriesProviderIds = updatedTimer.SeriesProviderIds;
}
public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
{
var excludeStatues = new List<RecordingStatus>
{
RecordingStatus.Completed
};
var timers = _timerManager.GetAll()
.Where(i => !excludeStatues.Contains(i.Status));
return Task.FromResult(timers);
}
public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
{
var config = _config.GetLiveTvConfiguration();
var defaults = new SeriesTimerInfo()
{
PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
RecordAnyChannel = false,
RecordAnyTime = true,
RecordNewOnly = true,
Days = new List<DayOfWeek>
{
DayOfWeek.Sunday,
DayOfWeek.Monday,
DayOfWeek.Tuesday,
DayOfWeek.Wednesday,
DayOfWeek.Thursday,
DayOfWeek.Friday,
DayOfWeek.Saturday
}
};
if (program is not null)
{
defaults.SeriesId = program.SeriesId;
defaults.ProgramId = program.Id;
defaults.RecordNewOnly = !program.IsRepeat;
defaults.Name = program.Name;
}
defaults.SkipEpisodesInLibrary = defaults.RecordNewOnly;
defaults.KeepUntil = KeepUntil.UntilDeleted;
return Task.FromResult(defaults);
}
public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
{
return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerManager.GetAll());
}
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
{
var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
return await _listingsManager.GetProgramsAsync(channel, startDateUtc, endDateUtc, cancellationToken)
.ConfigureAwait(false);
}
public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
_logger.LogInformation("Streaming Channel {Id}", channelId);
var result = string.IsNullOrEmpty(streamId) ?
null :
currentLiveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase));
if (result is not null && result.EnableStreamSharing)
{
result.ConsumerCount++;
_logger.LogInformation("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount);
return result;
}
foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
result = await hostInstance.GetChannelStream(channelId, streamId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
var openedMediaSource = result.MediaSource;
result.OriginalStreamId = streamId;
_logger.LogInformation("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}", streamId, openedMediaSource.Id, openedMediaSource.LiveStreamId);
return result;
}
catch (FileNotFoundException)
{
}
catch (OperationCanceledException)
{
}
}
throw new ResourceNotFoundException("Tuner not found.");
}
public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(channelId))
{
throw new ArgumentNullException(nameof(channelId));
}
foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).ConfigureAwait(false);
if (sources.Count > 0)
{
return sources;
}
}
catch (NotImplementedException)
{
}
}
throw new NotImplementedException();
}
public Task CloseLiveStream(string id, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task ResetTuner(string id, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private async void OnTimerManagerTimerFired(object sender, GenericEventArgs<TimerInfo> e)
{
var timer = e.Argument;
_logger.LogInformation("Recording timer fired for {0}.", timer.Name);
try
{
var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds);
if (recordingEndDate <= DateTime.UtcNow)
{
_logger.LogWarning("Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
_timerManager.Delete(timer);
return;
}
var activeRecordingInfo = new ActiveRecordingInfo
{
CancellationTokenSource = new CancellationTokenSource(),
Timer = timer,
Id = timer.Id
};
if (_recordingsManager.GetActiveRecordingPath(timer.Id) is not null)
{
_logger.LogInformation("Skipping RecordStream because it's already in progress.");
return;
}
LiveTvProgram programInfo = null;
if (!string.IsNullOrWhiteSpace(timer.ProgramId))
{
programInfo = GetProgramInfoFromCache(timer);
}
if (programInfo is null)
{
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
}
if (programInfo is not null)
{
CopyProgramInfoToTimerInfo(programInfo, timer);
}
await _recordingsManager.RecordStream(activeRecordingInfo, GetLiveTvChannel(timer), recordingEndDate)
.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
_logger.LogError(ex, "Error recording stream");
}
}
private BaseItem GetLiveTvChannel(TimerInfo timer)
{
var internalChannelId = _tvDtoService.GetInternalChannelId(Name, timer.ChannelId);
return _libraryManager.GetItemById(internalChannelId);
}
private LiveTvProgram GetProgramInfoFromCache(string programId)
{
var query = new InternalItemsQuery
{
ItemIds = [_tvDtoService.GetInternalProgramId(programId)],
Limit = 1,
DtoOptions = new DtoOptions()
};
return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault();
}
private LiveTvProgram GetProgramInfoFromCache(TimerInfo timer)
{
return GetProgramInfoFromCache(timer.ProgramId);
}
private LiveTvProgram GetProgramInfoFromCache(string channelId, DateTime startDateUtc)
{
var query = new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
Limit = 1,
DtoOptions = new DtoOptions(true)
{
EnableImages = false
},
MinStartDate = startDateUtc.AddMinutes(-3),
MaxStartDate = startDateUtc.AddMinutes(3),
OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }
};
if (!string.IsNullOrWhiteSpace(channelId))
{
query.ChannelIds = [_tvDtoService.GetInternalChannelId(Name, channelId)];
}
return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault();
}
private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
{
if (timer.IsManual)
{
return false;
}
if (!seriesTimer.RecordAnyTime
&& Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks)
{
return true;
}
if (seriesTimer.RecordNewOnly && timer.IsRepeat)
{
return true;
}
if (!seriesTimer.RecordAnyChannel
&& !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer);
}
private void HandleDuplicateShowIds(List<TimerInfo> timers)
{
// sort showings by HD channels first, then by startDate, record earliest showing possible
foreach (var timer in timers.OrderByDescending(t => GetLiveTvChannel(t).IsHD).ThenBy(t => t.StartDate).Skip(1))
{
timer.Status = RecordingStatus.Cancelled;
_timerManager.Update(timer);
}
}
private void SearchForDuplicateShowIds(IEnumerable<TimerInfo> timers)
{
var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList();
foreach (var group in groups)
{
if (string.IsNullOrWhiteSpace(group.Key))
{
continue;
}
var groupTimers = group.ToList();
if (groupTimers.Count < 2)
{
continue;
}
// Skip ShowId without SubKey from duplicate removal actions - https://github.com/jellyfin/jellyfin/issues/5856
if (group.Key.EndsWith("0000", StringComparison.Ordinal))
{
continue;
}
HandleDuplicateShowIds(groupTimers);
}
}
private void UpdateTimersForSeriesTimer(SeriesTimerInfo seriesTimer, bool updateTimerSettings, bool deleteInvalidTimers)
{
var allTimers = GetTimersForSeries(seriesTimer).ToList();
var enabledTimersForSeries = new List<TimerInfo>();
foreach (var timer in allTimers)
{
var existingTimer = _timerManager.GetTimer(timer.Id)
?? (string.IsNullOrWhiteSpace(timer.ProgramId)
? null
: _timerManager.GetTimerByProgramId(timer.ProgramId));
if (existingTimer is null)
{
if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
{
timer.Status = RecordingStatus.Cancelled;
}
else
{
enabledTimersForSeries.Add(timer);
}
_timerManager.Add(timer);
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
}
// Only update if not currently active - test both new timer and existing in case Id's are different
// Id's could be different if the timer was created manually prior to series timer creation
else if (_recordingsManager.GetActiveRecordingPath(timer.Id) is null
&& _recordingsManager.GetActiveRecordingPath(existingTimer.Id) is null)
{
UpdateExistingTimerWithNewMetadata(existingTimer, timer);
// Needed by ShouldCancelTimerForSeriesTimer
timer.IsManual = existingTimer.IsManual;
if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
{
existingTimer.Status = RecordingStatus.Cancelled;
}
else if (!existingTimer.IsManual)
{
existingTimer.Status = RecordingStatus.New;
}
if (existingTimer.Status != RecordingStatus.Cancelled)
{
enabledTimersForSeries.Add(existingTimer);
}
if (updateTimerSettings)
{
existingTimer.KeepUntil = seriesTimer.KeepUntil;
existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
existingTimer.Priority = seriesTimer.Priority;
existingTimer.SeriesTimerId = seriesTimer.Id;
}
existingTimer.SeriesTimerId = seriesTimer.Id;
_timerManager.Update(existingTimer);
}
}
SearchForDuplicateShowIds(enabledTimersForSeries);
if (deleteInvalidTimers)
{
var allTimerIds = allTimers
.Select(i => i.Id)
.ToList();
var deleteStatuses = new[]
{
RecordingStatus.New
};
var deletes = _timerManager.GetAll()
.Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
.Where(i => !allTimerIds.Contains(i.Id, StringComparison.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
.Where(i => deleteStatuses.Contains(i.Status))
.ToList();
foreach (var timer in deletes)
{
CancelTimerInternal(timer.Id, false, false);
}
}
}
private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer)
{
ArgumentNullException.ThrowIfNull(seriesTimer);
var query = new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
ExternalSeriesId = seriesTimer.SeriesId,
DtoOptions = new DtoOptions(true)
{
EnableImages = false
},
MinEndDate = DateTime.UtcNow
};
if (string.IsNullOrEmpty(seriesTimer.SeriesId))
{
query.Name = seriesTimer.Name;
}
if (!seriesTimer.RecordAnyChannel)
{
query.ChannelIds = [_tvDtoService.GetInternalChannelId(Name, seriesTimer.ChannelId)];
}
var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().Select(i => CreateTimer(i, seriesTimer, tempChannelCache));
}
private TimerInfo CreateTimer(LiveTvProgram parent, SeriesTimerInfo seriesTimer, Dictionary<Guid, LiveTvChannel> tempChannelCache)
{
string channelId = seriesTimer.RecordAnyChannel ? null : seriesTimer.ChannelId;
if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.IsEmpty())
{
if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel))
{
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
ItemIds = new[] { parent.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId))
{
tempChannelCache[parent.ChannelId] = channel;
}
}
if (channel is not null || tempChannelCache.TryGetValue(parent.ChannelId, out channel))
{
channelId = channel.ExternalId;
}
}
var timer = new TimerInfo
{
ChannelId = channelId,
Id = (seriesTimer.Id + parent.ExternalId).GetMD5().ToString("N", CultureInfo.InvariantCulture),
StartDate = parent.StartDate,
EndDate = parent.EndDate.Value,
ProgramId = parent.ExternalId,
PrePaddingSeconds = seriesTimer.PrePaddingSeconds,
PostPaddingSeconds = seriesTimer.PostPaddingSeconds,
IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired,
IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired,
KeepUntil = seriesTimer.KeepUntil,
Priority = seriesTimer.Priority,
Name = parent.Name,
Overview = parent.Overview,
SeriesId = parent.ExternalSeriesId,
SeriesTimerId = seriesTimer.Id,
ShowId = parent.ShowId
};
CopyProgramInfoToTimerInfo(parent, timer, tempChannelCache);
return timer;
}
private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo)
{
var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
CopyProgramInfoToTimerInfo(programInfo, timerInfo, tempChannelCache);
}
private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo, Dictionary<Guid, LiveTvChannel> tempChannelCache)
{
string channelId = null;
if (!programInfo.ChannelId.IsEmpty())
{
if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel))
{
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
ItemIds = new[] { programInfo.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId))
{
tempChannelCache[programInfo.ChannelId] = channel;
}
}
if (channel is not null || tempChannelCache.TryGetValue(programInfo.ChannelId, out channel))
{
channelId = channel.ExternalId;
}
}
timerInfo.Name = programInfo.Name;
timerInfo.StartDate = programInfo.StartDate;
timerInfo.EndDate = programInfo.EndDate.Value;
if (!string.IsNullOrWhiteSpace(channelId))
{
timerInfo.ChannelId = channelId;
}
timerInfo.SeasonNumber = programInfo.ParentIndexNumber;
timerInfo.EpisodeNumber = programInfo.IndexNumber;
timerInfo.IsMovie = programInfo.IsMovie;
timerInfo.ProductionYear = programInfo.ProductionYear;
timerInfo.EpisodeTitle = programInfo.EpisodeTitle;
timerInfo.OriginalAirDate = programInfo.PremiereDate;
timerInfo.IsProgramSeries = programInfo.IsSeries;
timerInfo.IsSeries = programInfo.IsSeries;
timerInfo.CommunityRating = programInfo.CommunityRating;
timerInfo.Overview = programInfo.Overview;
timerInfo.OfficialRating = programInfo.OfficialRating;
timerInfo.IsRepeat = programInfo.IsRepeat;
timerInfo.SeriesId = programInfo.ExternalSeriesId;
timerInfo.ProviderIds = programInfo.ProviderIds;
timerInfo.Tags = programInfo.Tags;
var seriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var providerId in timerInfo.ProviderIds)
{
const string Search = "Series";
if (providerId.Key.StartsWith(Search, StringComparison.OrdinalIgnoreCase))
{
seriesProviderIds[providerId.Key.Substring(Search.Length)] = providerId.Value;
}
}
timerInfo.SeriesProviderIds = seriesProviderIds;
}
private bool IsProgramAlreadyInLibrary(TimerInfo program)
{
if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
{
var seriesIds = _libraryManager.GetItemIds(
new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Series },
Name = program.Name
}).ToArray();
if (seriesIds.Length == 0)
{
return false;
}
if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue)
{
var result = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Episode },
ParentIndexNumber = program.SeasonNumber.Value,
IndexNumber = program.EpisodeNumber.Value,
AncestorIds = seriesIds,
IsVirtualItem = false,
Limit = 1
});
if (result.Count > 0)
{
return true;
}
}
}
return false;
}
}
}