From e13ccfe8547f7f3fbe01dc9ae378bf693f27c4bc Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 8 Feb 2024 12:07:11 -0500 Subject: Move timer services to separate folder --- src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs | 1 + src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs | 163 ------------------- src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs | 29 ---- src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs | 171 -------------------- .../LiveTvServiceCollectionExtensions.cs | 2 +- src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs | 163 +++++++++++++++++++ src/Jellyfin.LiveTv/Timers/SeriesTimerManager.cs | 29 ++++ src/Jellyfin.LiveTv/Timers/TimerManager.cs | 172 +++++++++++++++++++++ 8 files changed, 366 insertions(+), 364 deletions(-) delete mode 100644 src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs delete mode 100644 src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs delete mode 100644 src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs create mode 100644 src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs create mode 100644 src/Jellyfin.LiveTv/Timers/SeriesTimerManager.cs create mode 100644 src/Jellyfin.LiveTv/Timers/TimerManager.cs (limited to 'src') diff --git a/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs index a850ad6eb..48f5cea84 100644 --- a/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs @@ -19,6 +19,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Extensions; using Jellyfin.LiveTv.Configuration; +using Jellyfin.LiveTv.Timers; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; diff --git a/src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs b/src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs deleted file mode 100644 index 547ffeb66..000000000 --- a/src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs +++ /dev/null @@ -1,163 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Text.Json; -using Jellyfin.Extensions.Json; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.LiveTv.EmbyTV -{ - public class ItemDataProvider - where T : class - { - private readonly string _dataPath; - private readonly object _fileDataLock = new object(); - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private T[]? _items; - - public ItemDataProvider( - ILogger logger, - string dataPath, - Func equalityComparer) - { - Logger = logger; - _dataPath = dataPath; - EqualityComparer = equalityComparer; - } - - protected ILogger Logger { get; } - - protected Func EqualityComparer { get; } - - [MemberNotNull(nameof(_items))] - private void EnsureLoaded() - { - if (_items is not null) - { - return; - } - - if (File.Exists(_dataPath)) - { - Logger.LogInformation("Loading live tv data from {Path}", _dataPath); - - try - { - var bytes = File.ReadAllBytes(_dataPath); - _items = JsonSerializer.Deserialize(bytes, _jsonOptions); - if (_items is null) - { - Logger.LogError("Error deserializing {Path}, data was null", _dataPath); - _items = Array.Empty(); - } - - return; - } - catch (JsonException ex) - { - Logger.LogError(ex, "Error deserializing {Path}", _dataPath); - } - } - - _items = Array.Empty(); - } - - private void SaveList() - { - Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath))); - var jsonString = JsonSerializer.Serialize(_items, _jsonOptions); - File.WriteAllText(_dataPath, jsonString); - } - - public IReadOnlyList GetAll() - { - lock (_fileDataLock) - { - EnsureLoaded(); - return (T[])_items.Clone(); - } - } - - public virtual void Update(T item) - { - ArgumentNullException.ThrowIfNull(item); - - lock (_fileDataLock) - { - EnsureLoaded(); - - var index = Array.FindIndex(_items, i => EqualityComparer(i, item)); - if (index == -1) - { - throw new ArgumentException("item not found"); - } - - _items[index] = item; - - SaveList(); - } - } - - public virtual void Add(T item) - { - ArgumentNullException.ThrowIfNull(item); - - lock (_fileDataLock) - { - EnsureLoaded(); - - if (_items.Any(i => EqualityComparer(i, item))) - { - throw new ArgumentException("item already exists", nameof(item)); - } - - int oldLen = _items.Length; - var newList = new T[oldLen + 1]; - _items.CopyTo(newList, 0); - newList[oldLen] = item; - _items = newList; - - SaveList(); - } - } - - public virtual void AddOrUpdate(T item) - { - lock (_fileDataLock) - { - EnsureLoaded(); - - int index = Array.FindIndex(_items, i => EqualityComparer(i, item)); - if (index == -1) - { - int oldLen = _items.Length; - var newList = new T[oldLen + 1]; - _items.CopyTo(newList, 0); - newList[oldLen] = item; - _items = newList; - } - else - { - _items[index] = item; - } - - SaveList(); - } - } - - public virtual void Delete(T item) - { - lock (_fileDataLock) - { - EnsureLoaded(); - _items = _items.Where(i => !EqualityComparer(i, item)).ToArray(); - - SaveList(); - } - } - } -} diff --git a/src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs b/src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs deleted file mode 100644 index 8a3fa7f36..000000000 --- a/src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.LiveTv.EmbyTV -{ - public class SeriesTimerManager : ItemDataProvider - { - public SeriesTimerManager(ILogger logger, IConfigurationManager config) - : base( - logger, - Path.Combine(config.CommonApplicationPaths.DataPath, "livetv/seriestimers.json"), - (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) - { - } - - /// - public override void Add(SeriesTimerInfo item) - { - ArgumentException.ThrowIfNullOrEmpty(item.Id); - - base.Add(item); - } - } -} diff --git a/src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs b/src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs deleted file mode 100644 index 59ffa5d80..000000000 --- a/src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs +++ /dev/null @@ -1,171 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using Jellyfin.Data.Events; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.LiveTv.EmbyTV -{ - public class TimerManager : ItemDataProvider - { - private readonly ConcurrentDictionary _timers = new(StringComparer.OrdinalIgnoreCase); - - public TimerManager(ILogger logger, IConfigurationManager config) - : base( - logger, - Path.Combine(config.CommonApplicationPaths.DataPath, "livetv"), - (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) - { - } - - public event EventHandler>? TimerFired; - - public void RestartTimers() - { - StopTimers(); - - foreach (var item in GetAll()) - { - AddOrUpdateSystemTimer(item); - } - } - - public void StopTimers() - { - foreach (var pair in _timers.ToList()) - { - pair.Value.Dispose(); - } - - _timers.Clear(); - } - - public override void Delete(TimerInfo item) - { - base.Delete(item); - StopTimer(item); - } - - public override void Update(TimerInfo item) - { - base.Update(item); - AddOrUpdateSystemTimer(item); - } - - public void AddOrUpdate(TimerInfo item, bool resetTimer) - { - if (resetTimer) - { - AddOrUpdate(item); - return; - } - - base.AddOrUpdate(item); - } - - public override void AddOrUpdate(TimerInfo item) - { - base.AddOrUpdate(item); - AddOrUpdateSystemTimer(item); - } - - public override void Add(TimerInfo item) - { - ArgumentException.ThrowIfNullOrEmpty(item.Id); - - base.Add(item); - AddOrUpdateSystemTimer(item); - } - - private void AddOrUpdateSystemTimer(TimerInfo item) - { - StopTimer(item); - - if (item.Status is RecordingStatus.Completed or RecordingStatus.Cancelled) - { - return; - } - - var startDate = RecordingHelper.GetStartTime(item); - var now = DateTime.UtcNow; - - if (startDate < now) - { - TimerFired?.Invoke(this, new GenericEventArgs(item)); - return; - } - - var dueTime = startDate - now; - StartTimer(item, dueTime); - } - - private void StartTimer(TimerInfo item, TimeSpan dueTime) - { - var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero); - - if (_timers.TryAdd(item.Id, timer)) - { - if (item.IsSeries) - { - Logger.LogInformation( - "Creating recording timer for {Id}, {Name} {SeasonNumber}x{EpisodeNumber:D2} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", - item.Id, - item.Name, - item.SeasonNumber, - item.EpisodeNumber, - item.ChannelId, - dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), - item.StartDate); - } - else - { - Logger.LogInformation( - "Creating recording timer for {Id}, {Name} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", - item.Id, - item.Name, - item.ChannelId, - dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), - item.StartDate); - } - } - else - { - timer.Dispose(); - Logger.LogWarning("Timer already exists for item {Id}", item.Id); - } - } - - private void StopTimer(TimerInfo item) - { - if (_timers.TryRemove(item.Id, out var timer)) - { - timer.Dispose(); - } - } - - private void TimerCallback(object? state) - { - var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state)); - - var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase)); - if (timer is not null) - { - TimerFired?.Invoke(this, new GenericEventArgs(timer)); - } - } - - public TimerInfo? GetTimer(string id) - => GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase)); - - public TimerInfo? GetTimerByProgramId(string programId) - => GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase)); - } -} diff --git a/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs b/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs index b7ea5f54b..a632827f1 100644 --- a/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs +++ b/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs @@ -1,7 +1,7 @@ using Jellyfin.LiveTv.Channels; -using Jellyfin.LiveTv.EmbyTV; using Jellyfin.LiveTv.Guide; using Jellyfin.LiveTv.Listings; +using Jellyfin.LiveTv.Timers; using Jellyfin.LiveTv.TunerHosts; using Jellyfin.LiveTv.TunerHosts.HdHomerun; using MediaBrowser.Controller.Channels; diff --git a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs new file mode 100644 index 000000000..18e4810a2 --- /dev/null +++ b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs @@ -0,0 +1,163 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.Json; +using Jellyfin.Extensions.Json; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.LiveTv.Timers +{ + public class ItemDataProvider + where T : class + { + private readonly string _dataPath; + private readonly object _fileDataLock = new object(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private T[]? _items; + + public ItemDataProvider( + ILogger logger, + string dataPath, + Func equalityComparer) + { + Logger = logger; + _dataPath = dataPath; + EqualityComparer = equalityComparer; + } + + protected ILogger Logger { get; } + + protected Func EqualityComparer { get; } + + [MemberNotNull(nameof(_items))] + private void EnsureLoaded() + { + if (_items is not null) + { + return; + } + + if (File.Exists(_dataPath)) + { + Logger.LogInformation("Loading live tv data from {Path}", _dataPath); + + try + { + var bytes = File.ReadAllBytes(_dataPath); + _items = JsonSerializer.Deserialize(bytes, _jsonOptions); + if (_items is null) + { + Logger.LogError("Error deserializing {Path}, data was null", _dataPath); + _items = Array.Empty(); + } + + return; + } + catch (JsonException ex) + { + Logger.LogError(ex, "Error deserializing {Path}", _dataPath); + } + } + + _items = Array.Empty(); + } + + private void SaveList() + { + Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath))); + var jsonString = JsonSerializer.Serialize(_items, _jsonOptions); + File.WriteAllText(_dataPath, jsonString); + } + + public IReadOnlyList GetAll() + { + lock (_fileDataLock) + { + EnsureLoaded(); + return (T[])_items.Clone(); + } + } + + public virtual void Update(T item) + { + ArgumentNullException.ThrowIfNull(item); + + lock (_fileDataLock) + { + EnsureLoaded(); + + var index = Array.FindIndex(_items, i => EqualityComparer(i, item)); + if (index == -1) + { + throw new ArgumentException("item not found"); + } + + _items[index] = item; + + SaveList(); + } + } + + public virtual void Add(T item) + { + ArgumentNullException.ThrowIfNull(item); + + lock (_fileDataLock) + { + EnsureLoaded(); + + if (_items.Any(i => EqualityComparer(i, item))) + { + throw new ArgumentException("item already exists", nameof(item)); + } + + int oldLen = _items.Length; + var newList = new T[oldLen + 1]; + _items.CopyTo(newList, 0); + newList[oldLen] = item; + _items = newList; + + SaveList(); + } + } + + public virtual void AddOrUpdate(T item) + { + lock (_fileDataLock) + { + EnsureLoaded(); + + int index = Array.FindIndex(_items, i => EqualityComparer(i, item)); + if (index == -1) + { + int oldLen = _items.Length; + var newList = new T[oldLen + 1]; + _items.CopyTo(newList, 0); + newList[oldLen] = item; + _items = newList; + } + else + { + _items[index] = item; + } + + SaveList(); + } + } + + public virtual void Delete(T item) + { + lock (_fileDataLock) + { + EnsureLoaded(); + _items = _items.Where(i => !EqualityComparer(i, item)).ToArray(); + + SaveList(); + } + } + } +} diff --git a/src/Jellyfin.LiveTv/Timers/SeriesTimerManager.cs b/src/Jellyfin.LiveTv/Timers/SeriesTimerManager.cs new file mode 100644 index 000000000..6e8444ba2 --- /dev/null +++ b/src/Jellyfin.LiveTv/Timers/SeriesTimerManager.cs @@ -0,0 +1,29 @@ +#pragma warning disable CS1591 + +using System; +using System.IO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.LiveTv; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.LiveTv.Timers +{ + public class SeriesTimerManager : ItemDataProvider + { + public SeriesTimerManager(ILogger logger, IConfigurationManager config) + : base( + logger, + Path.Combine(config.CommonApplicationPaths.DataPath, "livetv/seriestimers.json"), + (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) + { + } + + /// + public override void Add(SeriesTimerInfo item) + { + ArgumentException.ThrowIfNullOrEmpty(item.Id); + + base.Add(item); + } + } +} diff --git a/src/Jellyfin.LiveTv/Timers/TimerManager.cs b/src/Jellyfin.LiveTv/Timers/TimerManager.cs new file mode 100644 index 000000000..6bcbd3324 --- /dev/null +++ b/src/Jellyfin.LiveTv/Timers/TimerManager.cs @@ -0,0 +1,172 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using Jellyfin.Data.Events; +using Jellyfin.LiveTv.EmbyTV; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.LiveTv; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.LiveTv.Timers +{ + public class TimerManager : ItemDataProvider + { + private readonly ConcurrentDictionary _timers = new(StringComparer.OrdinalIgnoreCase); + + public TimerManager(ILogger logger, IConfigurationManager config) + : base( + logger, + Path.Combine(config.CommonApplicationPaths.DataPath, "livetv"), + (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) + { + } + + public event EventHandler>? TimerFired; + + public void RestartTimers() + { + StopTimers(); + + foreach (var item in GetAll()) + { + AddOrUpdateSystemTimer(item); + } + } + + public void StopTimers() + { + foreach (var pair in _timers.ToList()) + { + pair.Value.Dispose(); + } + + _timers.Clear(); + } + + public override void Delete(TimerInfo item) + { + base.Delete(item); + StopTimer(item); + } + + public override void Update(TimerInfo item) + { + base.Update(item); + AddOrUpdateSystemTimer(item); + } + + public void AddOrUpdate(TimerInfo item, bool resetTimer) + { + if (resetTimer) + { + AddOrUpdate(item); + return; + } + + base.AddOrUpdate(item); + } + + public override void AddOrUpdate(TimerInfo item) + { + base.AddOrUpdate(item); + AddOrUpdateSystemTimer(item); + } + + public override void Add(TimerInfo item) + { + ArgumentException.ThrowIfNullOrEmpty(item.Id); + + base.Add(item); + AddOrUpdateSystemTimer(item); + } + + private void AddOrUpdateSystemTimer(TimerInfo item) + { + StopTimer(item); + + if (item.Status is RecordingStatus.Completed or RecordingStatus.Cancelled) + { + return; + } + + var startDate = RecordingHelper.GetStartTime(item); + var now = DateTime.UtcNow; + + if (startDate < now) + { + TimerFired?.Invoke(this, new GenericEventArgs(item)); + return; + } + + var dueTime = startDate - now; + StartTimer(item, dueTime); + } + + private void StartTimer(TimerInfo item, TimeSpan dueTime) + { + var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero); + + if (_timers.TryAdd(item.Id, timer)) + { + if (item.IsSeries) + { + Logger.LogInformation( + "Creating recording timer for {Id}, {Name} {SeasonNumber}x{EpisodeNumber:D2} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", + item.Id, + item.Name, + item.SeasonNumber, + item.EpisodeNumber, + item.ChannelId, + dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), + item.StartDate); + } + else + { + Logger.LogInformation( + "Creating recording timer for {Id}, {Name} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", + item.Id, + item.Name, + item.ChannelId, + dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), + item.StartDate); + } + } + else + { + timer.Dispose(); + Logger.LogWarning("Timer already exists for item {Id}", item.Id); + } + } + + private void StopTimer(TimerInfo item) + { + if (_timers.TryRemove(item.Id, out var timer)) + { + timer.Dispose(); + } + } + + private void TimerCallback(object? state) + { + var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state)); + + var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase)); + if (timer is not null) + { + TimerFired?.Invoke(this, new GenericEventArgs(timer)); + } + } + + public TimerInfo? GetTimer(string id) + => GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase)); + + public TimerInfo? GetTimerByProgramId(string programId) + => GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase)); + } +} -- cgit v1.2.3