| | 1 | | #pragma warning disable CS1591 |
| | 2 | |
|
| | 3 | | using System; |
| | 4 | | using System.Collections.Concurrent; |
| | 5 | | using System.Globalization; |
| | 6 | | using System.IO; |
| | 7 | | using System.Linq; |
| | 8 | | using System.Threading; |
| | 9 | | using Jellyfin.Data.Events; |
| | 10 | | using Jellyfin.LiveTv.Recordings; |
| | 11 | | using MediaBrowser.Common.Configuration; |
| | 12 | | using MediaBrowser.Controller.LiveTv; |
| | 13 | | using MediaBrowser.Model.LiveTv; |
| | 14 | | using Microsoft.Extensions.Logging; |
| | 15 | |
|
| | 16 | | namespace Jellyfin.LiveTv.Timers |
| | 17 | | { |
| | 18 | | public class TimerManager : ItemDataProvider<TimerInfo> |
| | 19 | | { |
| 21 | 20 | | private readonly ConcurrentDictionary<string, Timer> _timers = new(StringComparer.OrdinalIgnoreCase); |
| | 21 | |
|
| | 22 | | public TimerManager(ILogger<TimerManager> logger, IConfigurationManager config) |
| 21 | 23 | | : base( |
| 21 | 24 | | logger, |
| 21 | 25 | | Path.Combine(config.CommonApplicationPaths.DataPath, "livetv/timers.json"), |
| 21 | 26 | | (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) |
| | 27 | | { |
| 21 | 28 | | } |
| | 29 | |
|
| | 30 | | public event EventHandler<GenericEventArgs<TimerInfo>>? TimerFired; |
| | 31 | |
|
| | 32 | | public void RestartTimers() |
| | 33 | | { |
| 21 | 34 | | StopTimers(); |
| | 35 | |
|
| 42 | 36 | | foreach (var item in GetAll()) |
| | 37 | | { |
| 0 | 38 | | AddOrUpdateSystemTimer(item); |
| | 39 | | } |
| 21 | 40 | | } |
| | 41 | |
|
| | 42 | | public void StopTimers() |
| | 43 | | { |
| 42 | 44 | | foreach (var pair in _timers.ToList()) |
| | 45 | | { |
| 0 | 46 | | pair.Value.Dispose(); |
| | 47 | | } |
| | 48 | |
|
| 21 | 49 | | _timers.Clear(); |
| 21 | 50 | | } |
| | 51 | |
|
| | 52 | | public override void Delete(TimerInfo item) |
| | 53 | | { |
| 0 | 54 | | base.Delete(item); |
| 0 | 55 | | StopTimer(item); |
| 0 | 56 | | } |
| | 57 | |
|
| | 58 | | public override void Update(TimerInfo item) |
| | 59 | | { |
| 0 | 60 | | base.Update(item); |
| 0 | 61 | | AddOrUpdateSystemTimer(item); |
| 0 | 62 | | } |
| | 63 | |
|
| | 64 | | public void AddOrUpdate(TimerInfo item, bool resetTimer) |
| | 65 | | { |
| 0 | 66 | | if (resetTimer) |
| | 67 | | { |
| 0 | 68 | | AddOrUpdate(item); |
| 0 | 69 | | return; |
| | 70 | | } |
| | 71 | |
|
| 0 | 72 | | base.AddOrUpdate(item); |
| 0 | 73 | | } |
| | 74 | |
|
| | 75 | | public override void AddOrUpdate(TimerInfo item) |
| | 76 | | { |
| 0 | 77 | | base.AddOrUpdate(item); |
| 0 | 78 | | AddOrUpdateSystemTimer(item); |
| 0 | 79 | | } |
| | 80 | |
|
| | 81 | | public override void Add(TimerInfo item) |
| | 82 | | { |
| 0 | 83 | | ArgumentException.ThrowIfNullOrEmpty(item.Id); |
| | 84 | |
|
| 0 | 85 | | base.Add(item); |
| 0 | 86 | | AddOrUpdateSystemTimer(item); |
| 0 | 87 | | } |
| | 88 | |
|
| | 89 | | private void AddOrUpdateSystemTimer(TimerInfo item) |
| | 90 | | { |
| 0 | 91 | | StopTimer(item); |
| | 92 | |
|
| 0 | 93 | | if (item.Status is RecordingStatus.Completed or RecordingStatus.Cancelled) |
| | 94 | | { |
| 0 | 95 | | return; |
| | 96 | | } |
| | 97 | |
|
| 0 | 98 | | var startDate = item.StartDate.AddSeconds(-item.PrePaddingSeconds); |
| 0 | 99 | | var now = DateTime.UtcNow; |
| | 100 | |
|
| 0 | 101 | | if (startDate < now) |
| | 102 | | { |
| 0 | 103 | | TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(item)); |
| 0 | 104 | | return; |
| | 105 | | } |
| | 106 | |
|
| 0 | 107 | | var dueTime = startDate - now; |
| 0 | 108 | | StartTimer(item, dueTime); |
| 0 | 109 | | } |
| | 110 | |
|
| | 111 | | private void StartTimer(TimerInfo item, TimeSpan dueTime) |
| | 112 | | { |
| 0 | 113 | | var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero); |
| | 114 | |
|
| 0 | 115 | | if (_timers.TryAdd(item.Id, timer)) |
| | 116 | | { |
| 0 | 117 | | if (item.IsSeries) |
| | 118 | | { |
| 0 | 119 | | Logger.LogInformation( |
| 0 | 120 | | "Creating recording timer for {Id}, {Name} {SeasonNumber}x{EpisodeNumber:D2} on channel {ChannelId}. |
| 0 | 121 | | item.Id, |
| 0 | 122 | | item.Name, |
| 0 | 123 | | item.SeasonNumber, |
| 0 | 124 | | item.EpisodeNumber, |
| 0 | 125 | | item.ChannelId, |
| 0 | 126 | | dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), |
| 0 | 127 | | item.StartDate); |
| | 128 | | } |
| | 129 | | else |
| | 130 | | { |
| 0 | 131 | | Logger.LogInformation( |
| 0 | 132 | | "Creating recording timer for {Id}, {Name} on channel {ChannelId}. Timer will fire in {Minutes} minu |
| 0 | 133 | | item.Id, |
| 0 | 134 | | item.Name, |
| 0 | 135 | | item.ChannelId, |
| 0 | 136 | | dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), |
| 0 | 137 | | item.StartDate); |
| | 138 | | } |
| | 139 | | } |
| | 140 | | else |
| | 141 | | { |
| 0 | 142 | | timer.Dispose(); |
| 0 | 143 | | Logger.LogWarning("Timer already exists for item {Id}", item.Id); |
| | 144 | | } |
| 0 | 145 | | } |
| | 146 | |
|
| | 147 | | private void StopTimer(TimerInfo item) |
| | 148 | | { |
| 0 | 149 | | if (_timers.TryRemove(item.Id, out var timer)) |
| | 150 | | { |
| 0 | 151 | | timer.Dispose(); |
| | 152 | | } |
| 0 | 153 | | } |
| | 154 | |
|
| | 155 | | private void TimerCallback(object? state) |
| | 156 | | { |
| 0 | 157 | | var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state)); |
| | 158 | |
|
| 0 | 159 | | var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase)); |
| 0 | 160 | | if (timer is not null) |
| | 161 | | { |
| 0 | 162 | | TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(timer)); |
| | 163 | | } |
| 0 | 164 | | } |
| | 165 | |
|
| | 166 | | public TimerInfo? GetTimer(string id) |
| 0 | 167 | | => GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase)); |
| | 168 | |
|
| | 169 | | public TimerInfo? GetTimerByProgramId(string programId) |
| 0 | 170 | | => GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase)); |
| | 171 | | } |
| | 172 | | } |