mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 09:59:06 -07:00
Add IRecordingsManager service
This commit is contained in:
parent
7baf2d6c6b
commit
0370167b8d
@ -630,7 +630,7 @@ namespace Emby.Server.Implementations
|
||||
BaseItem.FileSystem = Resolve<IFileSystem>();
|
||||
BaseItem.UserDataManager = Resolve<IUserDataManager>();
|
||||
BaseItem.ChannelManager = Resolve<IChannelManager>();
|
||||
Video.LiveTvManager = Resolve<ILiveTvManager>();
|
||||
Video.RecordingsManager = Resolve<IRecordingsManager>();
|
||||
Folder.UserViewManager = Resolve<IUserViewManager>();
|
||||
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
|
||||
UserView.CollectionManager = Resolve<ICollectionManager>();
|
||||
|
@ -47,6 +47,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IRecordingsManager _recordingsManager;
|
||||
|
||||
private readonly IApplicationHost _appHost;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
@ -62,6 +63,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
IItemRepository itemRepo,
|
||||
IImageProcessor imageProcessor,
|
||||
IProviderManager providerManager,
|
||||
IRecordingsManager recordingsManager,
|
||||
IApplicationHost appHost,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
Lazy<ILiveTvManager> livetvManagerFactory,
|
||||
@ -74,6 +76,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
_itemRepo = itemRepo;
|
||||
_imageProcessor = imageProcessor;
|
||||
_providerManager = providerManager;
|
||||
_recordingsManager = recordingsManager;
|
||||
_appHost = appHost;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_livetvManagerFactory = livetvManagerFactory;
|
||||
@ -256,8 +259,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.Etag = item.GetEtag(user);
|
||||
}
|
||||
|
||||
var liveTvManager = LivetvManager;
|
||||
var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path);
|
||||
var activeRecording = _recordingsManager.GetActiveRecordingInfo(item.Path);
|
||||
if (activeRecording is not null)
|
||||
{
|
||||
dto.Type = BaseItemKind.Recording;
|
||||
@ -270,7 +272,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.Name = dto.SeriesName;
|
||||
}
|
||||
|
||||
liveTvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
|
||||
LivetvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
|
||||
}
|
||||
|
||||
return dto;
|
||||
|
@ -46,6 +46,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
private readonly IGuideManager _guideManager;
|
||||
private readonly ITunerHostManager _tunerHostManager;
|
||||
private readonly IListingsManager _listingsManager;
|
||||
private readonly IRecordingsManager _recordingsManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@ -61,6 +62,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <param name="guideManager">Instance of the <see cref="IGuideManager"/> interface.</param>
|
||||
/// <param name="tunerHostManager">Instance of the <see cref="ITunerHostManager"/> interface.</param>
|
||||
/// <param name="listingsManager">Instance of the <see cref="IListingsManager"/> interface.</param>
|
||||
/// <param name="recordingsManager">Instance of the <see cref="IRecordingsManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
@ -73,6 +75,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
IGuideManager guideManager,
|
||||
ITunerHostManager tunerHostManager,
|
||||
IListingsManager listingsManager,
|
||||
IRecordingsManager recordingsManager,
|
||||
IUserManager userManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILibraryManager libraryManager,
|
||||
@ -85,6 +88,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
_guideManager = guideManager;
|
||||
_tunerHostManager = tunerHostManager;
|
||||
_listingsManager = listingsManager;
|
||||
_recordingsManager = recordingsManager;
|
||||
_userManager = userManager;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_libraryManager = libraryManager;
|
||||
@ -1140,8 +1144,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
[ProducesVideoFile]
|
||||
public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId)
|
||||
{
|
||||
var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
|
||||
|
||||
var path = _recordingsManager.GetActiveRecordingPath(recordingId);
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return NotFound();
|
||||
|
@ -171,7 +171,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
[JsonIgnore]
|
||||
public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0;
|
||||
|
||||
public static ILiveTvManager LiveTvManager { get; set; }
|
||||
public static IRecordingsManager RecordingsManager { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override SourceType SourceType
|
||||
@ -334,7 +334,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
protected override bool IsActiveRecording()
|
||||
{
|
||||
return LiveTvManager.GetActiveRecordingInfo(Path) is not null;
|
||||
return RecordingsManager.GetActiveRecordingInfo(Path) is not null;
|
||||
}
|
||||
|
||||
public override bool CanDelete()
|
||||
|
@ -245,10 +245,6 @@ namespace MediaBrowser.Controller.LiveTv
|
||||
/// <param name="user">The user.</param>
|
||||
void AddChannelInfo(IReadOnlyCollection<(BaseItemDto ItemDto, LiveTvChannel Channel)> items, DtoOptions options, User user);
|
||||
|
||||
string GetEmbyTvActiveRecordingPath(string id);
|
||||
|
||||
ActiveRecordingInfo GetActiveRecordingInfo(string path);
|
||||
|
||||
void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null);
|
||||
|
||||
Task<BaseItem[]> GetRecordingFoldersAsync(User user);
|
||||
|
55
MediaBrowser.Controller/LiveTv/IRecordingsManager.cs
Normal file
55
MediaBrowser.Controller/LiveTv/IRecordingsManager.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.LiveTv;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for managing LiveTV recordings.
|
||||
/// </summary>
|
||||
public interface IRecordingsManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path for the provided timer id.
|
||||
/// </summary>
|
||||
/// <param name="id">The timer id.</param>
|
||||
/// <returns>The recording path, or <c>null</c> if none exists.</returns>
|
||||
string? GetActiveRecordingPath(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the information for an active recording.
|
||||
/// </summary>
|
||||
/// <param name="path">The recording path.</param>
|
||||
/// <returns>The <see cref="ActiveRecordingInfo"/>, or <c>null</c> if none exists.</returns>
|
||||
ActiveRecordingInfo? GetActiveRecordingInfo(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recording folders.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="VirtualFolderInfo"/> for each recording folder.</returns>
|
||||
IEnumerable<VirtualFolderInfo> GetRecordingFolders();
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the recording folders all exist, and removes unused folders.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
Task CreateRecordingFolders();
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the recording with the provided timer id, if one is active.
|
||||
/// </summary>
|
||||
/// <param name="timerId">The timer id.</param>
|
||||
/// <param name="timer">The timer.</param>
|
||||
void CancelRecording(string timerId, TimerInfo? timer);
|
||||
|
||||
/// <summary>
|
||||
/// Records a stream.
|
||||
/// </summary>
|
||||
/// <param name="recordingInfo">The recording info.</param>
|
||||
/// <param name="channel">The channel associated with the recording timer.</param>
|
||||
/// <param name="recordingEndDate">The time to stop recording.</param>
|
||||
/// <returns>Task representing the recording process.</returns>
|
||||
Task RecordStream(ActiveRecordingInfo recordingInfo, BaseItem channel, DateTime recordingEndDate);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.LiveTv.Timers;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
@ -12,19 +11,26 @@ namespace Jellyfin.LiveTv.EmbyTV;
|
||||
/// </summary>
|
||||
public sealed class LiveTvHost : IHostedService
|
||||
{
|
||||
private readonly EmbyTV _service;
|
||||
private readonly IRecordingsManager _recordingsManager;
|
||||
private readonly TimerManager _timerManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LiveTvHost"/> class.
|
||||
/// </summary>
|
||||
/// <param name="services">The available <see cref="ILiveTvService"/>s.</param>
|
||||
public LiveTvHost(IEnumerable<ILiveTvService> services)
|
||||
/// <param name="recordingsManager">The <see cref="IRecordingsManager"/>.</param>
|
||||
/// <param name="timerManager">The <see cref="TimerManager"/>.</param>
|
||||
public LiveTvHost(IRecordingsManager recordingsManager, TimerManager timerManager)
|
||||
{
|
||||
_service = services.OfType<EmbyTV>().First();
|
||||
_recordingsManager = recordingsManager;
|
||||
_timerManager = timerManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken) => _service.Start();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_timerManager.RestartTimers();
|
||||
return _recordingsManager.CreateRecordingFolders();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
@ -28,12 +28,14 @@ public static class LiveTvServiceCollectionExtensions
|
||||
services.AddSingleton<TimerManager>();
|
||||
services.AddSingleton<SeriesTimerManager>();
|
||||
services.AddSingleton<RecordingsMetadataManager>();
|
||||
|
||||
services.AddSingleton<ILiveTvManager, LiveTvManager>();
|
||||
services.AddSingleton<IChannelManager, ChannelManager>();
|
||||
services.AddSingleton<IStreamHelper, StreamHelper>();
|
||||
services.AddSingleton<ITunerHostManager, TunerHostManager>();
|
||||
services.AddSingleton<IListingsManager, ListingsManager>();
|
||||
services.AddSingleton<IGuideManager, GuideManager>();
|
||||
services.AddSingleton<IRecordingsManager, RecordingsManager>();
|
||||
|
||||
services.AddSingleton<ILiveTvService, EmbyTV.EmbyTV>();
|
||||
services.AddSingleton<ITunerHost, HdHomerunHost>();
|
||||
|
@ -34,6 +34,7 @@ public class GuideManager : IGuideManager
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
private readonly ITunerHostManager _tunerHostManager;
|
||||
private readonly IRecordingsManager _recordingsManager;
|
||||
private readonly LiveTvDtoService _tvDtoService;
|
||||
|
||||
/// <summary>
|
||||
@ -46,6 +47,7 @@ public class GuideManager : IGuideManager
|
||||
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
|
||||
/// <param name="liveTvManager">The <see cref="ILiveTvManager"/>.</param>
|
||||
/// <param name="tunerHostManager">The <see cref="ITunerHostManager"/>.</param>
|
||||
/// <param name="recordingsManager">The <see cref="IRecordingsManager"/>.</param>
|
||||
/// <param name="tvDtoService">The <see cref="LiveTvDtoService"/>.</param>
|
||||
public GuideManager(
|
||||
ILogger<GuideManager> logger,
|
||||
@ -55,6 +57,7 @@ public class GuideManager : IGuideManager
|
||||
ILibraryManager libraryManager,
|
||||
ILiveTvManager liveTvManager,
|
||||
ITunerHostManager tunerHostManager,
|
||||
IRecordingsManager recordingsManager,
|
||||
LiveTvDtoService tvDtoService)
|
||||
{
|
||||
_logger = logger;
|
||||
@ -64,6 +67,7 @@ public class GuideManager : IGuideManager
|
||||
_libraryManager = libraryManager;
|
||||
_liveTvManager = liveTvManager;
|
||||
_tunerHostManager = tunerHostManager;
|
||||
_recordingsManager = recordingsManager;
|
||||
_tvDtoService = tvDtoService;
|
||||
}
|
||||
|
||||
@ -85,7 +89,7 @@ public class GuideManager : IGuideManager
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(progress);
|
||||
|
||||
await EmbyTV.EmbyTV.Current.CreateRecordingFolders().ConfigureAwait(false);
|
||||
await _recordingsManager.CreateRecordingFolders().ConfigureAwait(false);
|
||||
|
||||
await _tunerHostManager.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
@ -43,6 +43,7 @@ namespace Jellyfin.LiveTv
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IChannelManager _channelManager;
|
||||
private readonly IRecordingsManager _recordingsManager;
|
||||
private readonly LiveTvDtoService _tvDtoService;
|
||||
private readonly ILiveTvService[] _services;
|
||||
|
||||
@ -55,6 +56,7 @@ namespace Jellyfin.LiveTv
|
||||
ILibraryManager libraryManager,
|
||||
ILocalizationManager localization,
|
||||
IChannelManager channelManager,
|
||||
IRecordingsManager recordingsManager,
|
||||
LiveTvDtoService liveTvDtoService,
|
||||
IEnumerable<ILiveTvService> services)
|
||||
{
|
||||
@ -67,6 +69,7 @@ namespace Jellyfin.LiveTv
|
||||
_userDataManager = userDataManager;
|
||||
_channelManager = channelManager;
|
||||
_tvDtoService = liveTvDtoService;
|
||||
_recordingsManager = recordingsManager;
|
||||
_services = services.ToArray();
|
||||
|
||||
var defaultService = _services.OfType<EmbyTV.EmbyTV>().First();
|
||||
@ -88,11 +91,6 @@ namespace Jellyfin.LiveTv
|
||||
/// <value>The services.</value>
|
||||
public IReadOnlyList<ILiveTvService> Services => _services;
|
||||
|
||||
public string GetEmbyTvActiveRecordingPath(string id)
|
||||
{
|
||||
return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
|
||||
}
|
||||
|
||||
private void OnEmbyTvTimerCancelled(object sender, GenericEventArgs<string> e)
|
||||
{
|
||||
var timerId = e.Argument;
|
||||
@ -765,18 +763,13 @@ namespace Jellyfin.LiveTv
|
||||
return AddRecordingInfo(programTuples, CancellationToken.None);
|
||||
}
|
||||
|
||||
public ActiveRecordingInfo GetActiveRecordingInfo(string path)
|
||||
{
|
||||
return EmbyTV.EmbyTV.Current.GetActiveRecordingInfo(path);
|
||||
}
|
||||
|
||||
public void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null)
|
||||
{
|
||||
var service = EmbyTV.EmbyTV.Current;
|
||||
|
||||
var info = activeRecordingInfo.Timer;
|
||||
|
||||
var channel = string.IsNullOrWhiteSpace(info.ChannelId) ? null : _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(service.Name, info.ChannelId));
|
||||
var channel = string.IsNullOrWhiteSpace(info.ChannelId)
|
||||
? null
|
||||
: _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(EmbyTV.EmbyTV.ServiceName, info.ChannelId));
|
||||
|
||||
dto.SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId)
|
||||
? null
|
||||
@ -1461,7 +1454,7 @@ namespace Jellyfin.LiveTv
|
||||
|
||||
private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels)
|
||||
{
|
||||
var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
|
||||
var folders = _recordingsManager.GetRecordingFolders()
|
||||
.SelectMany(i => i.Locations)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(i => _libraryManager.FindByPath(i, true))
|
||||
|
@ -24,13 +24,15 @@ namespace Jellyfin.LiveTv
|
||||
private const char StreamIdDelimiter = '_';
|
||||
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
private readonly IRecordingsManager _recordingsManager;
|
||||
private readonly ILogger<LiveTvMediaSourceProvider> _logger;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, ILogger<LiveTvMediaSourceProvider> logger, IMediaSourceManager mediaSourceManager, IServerApplicationHost appHost)
|
||||
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IRecordingsManager recordingsManager, ILogger<LiveTvMediaSourceProvider> logger, IMediaSourceManager mediaSourceManager, IServerApplicationHost appHost)
|
||||
{
|
||||
_liveTvManager = liveTvManager;
|
||||
_recordingsManager = recordingsManager;
|
||||
_logger = logger;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_appHost = appHost;
|
||||
@ -40,7 +42,7 @@ namespace Jellyfin.LiveTv
|
||||
{
|
||||
if (item.SourceType == SourceType.LiveTV)
|
||||
{
|
||||
var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path);
|
||||
var activeRecordingInfo = _recordingsManager.GetActiveRecordingInfo(item.Path);
|
||||
|
||||
if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo is not null)
|
||||
{
|
||||
|
849
src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
Normal file
849
src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
Normal file
@ -0,0 +1,849 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.LiveTv.Configuration;
|
||||
using Jellyfin.LiveTv.EmbyTV;
|
||||
using Jellyfin.LiveTv.IO;
|
||||
using Jellyfin.LiveTv.Timers;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.LiveTv.Recordings;
|
||||
|
||||
/// <inheritdoc cref="IRecordingsManager" />
|
||||
public sealed class RecordingsManager : IRecordingsManager, IDisposable
|
||||
{
|
||||
private readonly ILogger<RecordingsManager> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILibraryMonitor _libraryMonitor;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IStreamHelper _streamHelper;
|
||||
private readonly TimerManager _timerManager;
|
||||
private readonly SeriesTimerManager _seriesTimerManager;
|
||||
private readonly RecordingsMetadataManager _recordingsMetadataManager;
|
||||
|
||||
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly AsyncNonKeyedLocker _recordingDeleteSemaphore = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecordingsManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
|
||||
/// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
|
||||
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
|
||||
/// <param name="libraryMonitor">The <see cref="ILibraryMonitor"/>.</param>
|
||||
/// <param name="providerManager">The <see cref="IProviderManager"/>.</param>
|
||||
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
|
||||
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
|
||||
/// <param name="streamHelper">The <see cref="IStreamHelper"/>.</param>
|
||||
/// <param name="timerManager">The <see cref="TimerManager"/>.</param>
|
||||
/// <param name="seriesTimerManager">The <see cref="SeriesTimerManager"/>.</param>
|
||||
/// <param name="recordingsMetadataManager">The <see cref="RecordingsMetadataManager"/>.</param>
|
||||
public RecordingsManager(
|
||||
ILogger<RecordingsManager> logger,
|
||||
IServerConfigurationManager config,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
ILibraryMonitor libraryMonitor,
|
||||
IProviderManager providerManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IStreamHelper streamHelper,
|
||||
TimerManager timerManager,
|
||||
SeriesTimerManager seriesTimerManager,
|
||||
RecordingsMetadataManager recordingsMetadataManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_fileSystem = fileSystem;
|
||||
_libraryManager = libraryManager;
|
||||
_libraryMonitor = libraryMonitor;
|
||||
_providerManager = providerManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_streamHelper = streamHelper;
|
||||
_timerManager = timerManager;
|
||||
_seriesTimerManager = seriesTimerManager;
|
||||
_recordingsMetadataManager = recordingsMetadataManager;
|
||||
|
||||
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
||||
}
|
||||
|
||||
private string DefaultRecordingPath
|
||||
{
|
||||
get
|
||||
{
|
||||
var path = _config.GetLiveTvConfiguration().RecordingPath;
|
||||
|
||||
return string.IsNullOrWhiteSpace(path)
|
||||
? Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv", "recordings")
|
||||
: path;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? GetActiveRecordingPath(string id)
|
||||
=> _activeRecordings.GetValueOrDefault(id)?.Path;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ActiveRecordingInfo? GetActiveRecordingInfo(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || _activeRecordings.IsEmpty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var (_, recordingInfo) in _activeRecordings)
|
||||
{
|
||||
if (string.Equals(recordingInfo.Path, path, StringComparison.Ordinal)
|
||||
&& !recordingInfo.CancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
return recordingInfo.Timer.Status == RecordingStatus.InProgress ? recordingInfo : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<VirtualFolderInfo> GetRecordingFolders()
|
||||
{
|
||||
if (Directory.Exists(DefaultRecordingPath))
|
||||
{
|
||||
yield return new VirtualFolderInfo
|
||||
{
|
||||
Locations = [DefaultRecordingPath],
|
||||
Name = "Recordings"
|
||||
};
|
||||
}
|
||||
|
||||
var customPath = _config.GetLiveTvConfiguration().MovieRecordingPath;
|
||||
if (!string.IsNullOrWhiteSpace(customPath)
|
||||
&& !string.Equals(customPath, DefaultRecordingPath, StringComparison.OrdinalIgnoreCase)
|
||||
&& Directory.Exists(customPath))
|
||||
{
|
||||
yield return new VirtualFolderInfo
|
||||
{
|
||||
Locations = [customPath],
|
||||
Name = "Recorded Movies",
|
||||
CollectionType = CollectionTypeOptions.Movies
|
||||
};
|
||||
}
|
||||
|
||||
customPath = _config.GetLiveTvConfiguration().SeriesRecordingPath;
|
||||
if (!string.IsNullOrWhiteSpace(customPath)
|
||||
&& !string.Equals(customPath, DefaultRecordingPath, StringComparison.OrdinalIgnoreCase)
|
||||
&& Directory.Exists(customPath))
|
||||
{
|
||||
yield return new VirtualFolderInfo
|
||||
{
|
||||
Locations = [customPath],
|
||||
Name = "Recorded Shows",
|
||||
CollectionType = CollectionTypeOptions.TvShows
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CreateRecordingFolders()
|
||||
{
|
||||
try
|
||||
{
|
||||
var recordingFolders = GetRecordingFolders().ToArray();
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders();
|
||||
|
||||
var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
|
||||
|
||||
var pathsAdded = new List<string>();
|
||||
|
||||
foreach (var recordingFolder in recordingFolders)
|
||||
{
|
||||
var pathsToCreate = recordingFolder.Locations
|
||||
.Where(i => !allExistingPaths.Any(p => _fileSystem.AreEqual(p, i)))
|
||||
.ToList();
|
||||
|
||||
if (pathsToCreate.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo(i)).ToArray();
|
||||
var libraryOptions = new LibraryOptions
|
||||
{
|
||||
PathInfos = mediaPathInfos
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await _libraryManager
|
||||
.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, libraryOptions, true)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating virtual folder");
|
||||
}
|
||||
|
||||
pathsAdded.AddRange(pathsToCreate);
|
||||
}
|
||||
|
||||
var config = _config.GetLiveTvConfiguration();
|
||||
|
||||
var pathsToRemove = config.MediaLocationsCreated
|
||||
.Except(recordingFolders.SelectMany(i => i.Locations))
|
||||
.ToList();
|
||||
|
||||
if (pathsAdded.Count > 0 || pathsToRemove.Count > 0)
|
||||
{
|
||||
pathsAdded.InsertRange(0, config.MediaLocationsCreated);
|
||||
config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
_config.SaveConfiguration("livetv", config);
|
||||
}
|
||||
|
||||
foreach (var path in pathsToRemove)
|
||||
{
|
||||
await RemovePathFromLibraryAsync(path).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating recording folders");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemovePathFromLibraryAsync(string path)
|
||||
{
|
||||
_logger.LogDebug("Removing path from library: {0}", path);
|
||||
|
||||
var requiresRefresh = false;
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders();
|
||||
|
||||
foreach (var virtualFolder in virtualFolders)
|
||||
{
|
||||
if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (virtualFolder.Locations.Length == 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error removing virtual folder");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
_libraryManager.RemoveMediaPath(virtualFolder.Name, path);
|
||||
requiresRefresh = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error removing media path");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requiresRefresh)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CancelRecording(string timerId, TimerInfo? timer)
|
||||
{
|
||||
if (_activeRecordings.TryGetValue(timerId, out var activeRecordingInfo))
|
||||
{
|
||||
activeRecordingInfo.Timer = timer;
|
||||
activeRecordingInfo.CancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RecordStream(ActiveRecordingInfo recordingInfo, BaseItem channel, DateTime recordingEndDate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(recordingInfo);
|
||||
ArgumentNullException.ThrowIfNull(channel);
|
||||
|
||||
var timer = recordingInfo.Timer;
|
||||
var remoteMetadata = await FetchInternetMetadata(timer, CancellationToken.None).ConfigureAwait(false);
|
||||
var recordingPath = GetRecordingPath(timer, remoteMetadata, out var seriesPath);
|
||||
|
||||
string? liveStreamId = null;
|
||||
RecordingStatus recordingStatus;
|
||||
try
|
||||
{
|
||||
var allMediaSources = await _mediaSourceManager
|
||||
.GetPlaybackMediaSources(channel, null, true, false, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var mediaStreamInfo = allMediaSources[0];
|
||||
IDirectStreamProvider? directStreamProvider = null;
|
||||
if (mediaStreamInfo.RequiresOpening)
|
||||
{
|
||||
var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal(
|
||||
new LiveStreamRequest
|
||||
{
|
||||
ItemId = channel.Id,
|
||||
OpenToken = mediaStreamInfo.OpenToken
|
||||
},
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
mediaStreamInfo = liveStreamResponse.Item1.MediaSource;
|
||||
liveStreamId = mediaStreamInfo.LiveStreamId;
|
||||
directStreamProvider = liveStreamResponse.Item2;
|
||||
}
|
||||
|
||||
using var recorder = GetRecorder(mediaStreamInfo);
|
||||
|
||||
recordingPath = recorder.GetOutputPath(mediaStreamInfo, recordingPath);
|
||||
recordingPath = EnsureFileUnique(recordingPath, timer.Id);
|
||||
|
||||
_libraryMonitor.ReportFileSystemChangeBeginning(recordingPath);
|
||||
|
||||
var duration = recordingEndDate - DateTime.UtcNow;
|
||||
|
||||
_logger.LogInformation("Beginning recording. Will record for {Duration} minutes.", duration.TotalMinutes);
|
||||
_logger.LogInformation("Writing file to: {Path}", recordingPath);
|
||||
|
||||
async void OnStarted()
|
||||
{
|
||||
recordingInfo.Path = recordingPath;
|
||||
_activeRecordings.TryAdd(timer.Id, recordingInfo);
|
||||
|
||||
timer.Status = RecordingStatus.InProgress;
|
||||
_timerManager.AddOrUpdate(timer, false);
|
||||
|
||||
await _recordingsMetadataManager.SaveRecordingMetadata(timer, recordingPath, seriesPath).ConfigureAwait(false);
|
||||
await CreateRecordingFolders().ConfigureAwait(false);
|
||||
|
||||
TriggerRefresh(recordingPath);
|
||||
await EnforceKeepUpTo(timer, seriesPath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await recorder.Record(
|
||||
directStreamProvider,
|
||||
mediaStreamInfo,
|
||||
recordingPath,
|
||||
duration,
|
||||
OnStarted,
|
||||
recordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
recordingStatus = RecordingStatus.Completed;
|
||||
_logger.LogInformation("Recording completed: {RecordPath}", recordingPath);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Recording stopped: {RecordPath}", recordingPath);
|
||||
recordingStatus = RecordingStatus.Completed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error recording to {RecordPath}", recordingPath);
|
||||
recordingStatus = RecordingStatus.Error;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(liveStreamId))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error closing live stream");
|
||||
}
|
||||
}
|
||||
|
||||
DeleteFileIfEmpty(recordingPath);
|
||||
TriggerRefresh(recordingPath);
|
||||
_libraryMonitor.ReportFileSystemChangeComplete(recordingPath, false);
|
||||
_activeRecordings.TryRemove(timer.Id, out _);
|
||||
|
||||
if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate && timer.RetryCount < 10)
|
||||
{
|
||||
const int RetryIntervalSeconds = 60;
|
||||
_logger.LogInformation("Retrying recording in {0} seconds.", RetryIntervalSeconds);
|
||||
|
||||
timer.Status = RecordingStatus.New;
|
||||
timer.PrePaddingSeconds = 0;
|
||||
timer.StartDate = DateTime.UtcNow.AddSeconds(RetryIntervalSeconds);
|
||||
timer.RetryCount++;
|
||||
_timerManager.AddOrUpdate(timer);
|
||||
}
|
||||
else if (File.Exists(recordingPath))
|
||||
{
|
||||
timer.RecordingPath = recordingPath;
|
||||
timer.Status = RecordingStatus.Completed;
|
||||
_timerManager.AddOrUpdate(timer, false);
|
||||
PostProcessRecording(recordingPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_timerManager.Delete(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_recordingDeleteSemaphore.Dispose();
|
||||
|
||||
foreach (var pair in _activeRecordings.ToList())
|
||||
{
|
||||
pair.Value.CancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private async void OnNamedConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs e)
|
||||
{
|
||||
if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await CreateRecordingFolders().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<RemoteSearchResult?> FetchInternetMetadata(TimerInfo timer, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!timer.IsSeries || timer.SeriesProviderIds.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var query = new RemoteSearchQuery<SeriesInfo>
|
||||
{
|
||||
SearchInfo = new SeriesInfo
|
||||
{
|
||||
ProviderIds = timer.SeriesProviderIds,
|
||||
Name = timer.Name,
|
||||
MetadataCountryCode = _config.Configuration.MetadataCountryCode,
|
||||
MetadataLanguage = _config.Configuration.PreferredMetadataLanguage
|
||||
}
|
||||
};
|
||||
|
||||
var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return results.FirstOrDefault();
|
||||
}
|
||||
|
||||
private string GetRecordingPath(TimerInfo timer, RemoteSearchResult? metadata, out string? seriesPath)
|
||||
{
|
||||
var recordingPath = DefaultRecordingPath;
|
||||
var config = _config.GetLiveTvConfiguration();
|
||||
seriesPath = null;
|
||||
|
||||
if (timer.IsProgramSeries)
|
||||
{
|
||||
var customRecordingPath = config.SeriesRecordingPath;
|
||||
var allowSubfolder = true;
|
||||
if (!string.IsNullOrWhiteSpace(customRecordingPath))
|
||||
{
|
||||
allowSubfolder = string.Equals(customRecordingPath, recordingPath, StringComparison.OrdinalIgnoreCase);
|
||||
recordingPath = customRecordingPath;
|
||||
}
|
||||
|
||||
if (allowSubfolder && config.EnableRecordingSubfolders)
|
||||
{
|
||||
recordingPath = Path.Combine(recordingPath, "Series");
|
||||
}
|
||||
|
||||
// trim trailing period from the folder name
|
||||
var folderName = _fileSystem.GetValidFilename(timer.Name).Trim().TrimEnd('.').Trim();
|
||||
|
||||
if (metadata is not null && metadata.ProductionYear.HasValue)
|
||||
{
|
||||
folderName += " (" + metadata.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
|
||||
}
|
||||
|
||||
// Can't use the year here in the folder name because it is the year of the episode, not the series.
|
||||
recordingPath = Path.Combine(recordingPath, folderName);
|
||||
|
||||
seriesPath = recordingPath;
|
||||
|
||||
if (timer.SeasonNumber.HasValue)
|
||||
{
|
||||
folderName = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Season {0}",
|
||||
timer.SeasonNumber.Value);
|
||||
recordingPath = Path.Combine(recordingPath, folderName);
|
||||
}
|
||||
}
|
||||
else if (timer.IsMovie)
|
||||
{
|
||||
var customRecordingPath = config.MovieRecordingPath;
|
||||
var allowSubfolder = true;
|
||||
if (!string.IsNullOrWhiteSpace(customRecordingPath))
|
||||
{
|
||||
allowSubfolder = string.Equals(customRecordingPath, recordingPath, StringComparison.OrdinalIgnoreCase);
|
||||
recordingPath = customRecordingPath;
|
||||
}
|
||||
|
||||
if (allowSubfolder && config.EnableRecordingSubfolders)
|
||||
{
|
||||
recordingPath = Path.Combine(recordingPath, "Movies");
|
||||
}
|
||||
|
||||
var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
|
||||
if (timer.ProductionYear.HasValue)
|
||||
{
|
||||
folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
|
||||
}
|
||||
|
||||
// trim trailing period from the folder name
|
||||
folderName = folderName.TrimEnd('.').Trim();
|
||||
|
||||
recordingPath = Path.Combine(recordingPath, folderName);
|
||||
}
|
||||
else if (timer.IsKids)
|
||||
{
|
||||
if (config.EnableRecordingSubfolders)
|
||||
{
|
||||
recordingPath = Path.Combine(recordingPath, "Kids");
|
||||
}
|
||||
|
||||
var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
|
||||
if (timer.ProductionYear.HasValue)
|
||||
{
|
||||
folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
|
||||
}
|
||||
|
||||
// trim trailing period from the folder name
|
||||
folderName = folderName.TrimEnd('.').Trim();
|
||||
|
||||
recordingPath = Path.Combine(recordingPath, folderName);
|
||||
}
|
||||
else if (timer.IsSports)
|
||||
{
|
||||
if (config.EnableRecordingSubfolders)
|
||||
{
|
||||
recordingPath = Path.Combine(recordingPath, "Sports");
|
||||
}
|
||||
|
||||
recordingPath = Path.Combine(recordingPath, _fileSystem.GetValidFilename(timer.Name).Trim());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (config.EnableRecordingSubfolders)
|
||||
{
|
||||
recordingPath = Path.Combine(recordingPath, "Other");
|
||||
}
|
||||
|
||||
recordingPath = Path.Combine(recordingPath, _fileSystem.GetValidFilename(timer.Name).Trim());
|
||||
}
|
||||
|
||||
var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer)).Trim() + ".ts";
|
||||
|
||||
return Path.Combine(recordingPath, recordingFileName);
|
||||
}
|
||||
|
||||
private void DeleteFileIfEmpty(string path)
|
||||
{
|
||||
var file = _fileSystem.GetFileInfo(path);
|
||||
|
||||
if (file.Exists && file.Length == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting 0-byte failed recording file {Path}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerRefresh(string path)
|
||||
{
|
||||
_logger.LogInformation("Triggering refresh on {Path}", path);
|
||||
|
||||
var item = GetAffectedBaseItem(Path.GetDirectoryName(path));
|
||||
if (item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Refreshing recording parent {Path}", item.Path);
|
||||
_providerManager.QueueRefresh(
|
||||
item.Id,
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
RefreshPaths =
|
||||
[
|
||||
path,
|
||||
Path.GetDirectoryName(path),
|
||||
Path.GetDirectoryName(Path.GetDirectoryName(path))
|
||||
]
|
||||
},
|
||||
RefreshPriority.High);
|
||||
}
|
||||
|
||||
private BaseItem? GetAffectedBaseItem(string? path)
|
||||
{
|
||||
BaseItem? item = null;
|
||||
var parentPath = Path.GetDirectoryName(path);
|
||||
while (item is null && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
item = _libraryManager.FindByPath(path, null);
|
||||
path = Path.GetDirectoryName(path);
|
||||
}
|
||||
|
||||
if (item is not null
|
||||
&& item.GetType() == typeof(Folder)
|
||||
&& string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var parentItem = item.GetParent();
|
||||
if (parentItem is not null && parentItem is not AggregateFolder)
|
||||
{
|
||||
item = parentItem;
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private async Task EnforceKeepUpTo(TimerInfo timer, string? seriesPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(timer.SeriesTimerId)
|
||||
|| string.IsNullOrWhiteSpace(seriesPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var seriesTimerId = timer.SeriesTimerId;
|
||||
var seriesTimer = _seriesTimerManager.GetAll()
|
||||
.FirstOrDefault(i => string.Equals(i.Id, seriesTimerId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (seriesTimer is null || seriesTimer.KeepUpTo <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (await _recordingDeleteSemaphore.LockAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var timersToDelete = _timerManager.GetAll()
|
||||
.Where(timerInfo => timerInfo.Status == RecordingStatus.Completed
|
||||
&& !string.IsNullOrWhiteSpace(timerInfo.RecordingPath)
|
||||
&& string.Equals(timerInfo.SeriesTimerId, seriesTimerId, StringComparison.OrdinalIgnoreCase)
|
||||
&& File.Exists(timerInfo.RecordingPath))
|
||||
.OrderByDescending(i => i.EndDate)
|
||||
.Skip(seriesTimer.KeepUpTo - 1)
|
||||
.ToList();
|
||||
|
||||
DeleteLibraryItemsForTimers(timersToDelete);
|
||||
|
||||
if (_libraryManager.FindByPath(seriesPath, true) is not Folder librarySeries)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var episodesToDelete = librarySeries.GetItemList(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
|
||||
IsVirtualItem = false,
|
||||
IsFolder = false,
|
||||
Recursive = true,
|
||||
DtoOptions = new DtoOptions(true)
|
||||
})
|
||||
.Where(i => i.IsFileProtocol && File.Exists(i.Path))
|
||||
.Skip(seriesTimer.KeepUpTo - 1);
|
||||
|
||||
foreach (var item in episodesToDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
_libraryManager.DeleteItem(
|
||||
item,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = true
|
||||
},
|
||||
true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting item");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteLibraryItemsForTimers(List<TimerInfo> timers)
|
||||
{
|
||||
foreach (var timer in timers)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DeleteLibraryItemForTimer(timer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting recording");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteLibraryItemForTimer(TimerInfo timer)
|
||||
{
|
||||
var libraryItem = _libraryManager.FindByPath(timer.RecordingPath, false);
|
||||
if (libraryItem is not null)
|
||||
{
|
||||
_libraryManager.DeleteItem(
|
||||
libraryItem,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = true
|
||||
},
|
||||
true);
|
||||
}
|
||||
else if (File.Exists(timer.RecordingPath))
|
||||
{
|
||||
_fileSystem.DeleteFile(timer.RecordingPath);
|
||||
}
|
||||
|
||||
_timerManager.Delete(timer);
|
||||
}
|
||||
|
||||
private string EnsureFileUnique(string path, string timerId)
|
||||
{
|
||||
var parent = Path.GetDirectoryName(path)!;
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
var index = 1;
|
||||
while (File.Exists(path) || _activeRecordings.Any(i
|
||||
=> string.Equals(i.Value.Path, path, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(i.Value.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
name += " - " + index.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
path = Path.ChangeExtension(Path.Combine(parent, name), extension);
|
||||
index++;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private IRecorder GetRecorder(MediaSourceInfo mediaSource)
|
||||
{
|
||||
if (mediaSource.RequiresLooping
|
||||
|| !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase)
|
||||
|| (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
|
||||
{
|
||||
return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _config);
|
||||
}
|
||||
|
||||
return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
|
||||
}
|
||||
|
||||
private void PostProcessRecording(string path)
|
||||
{
|
||||
var options = _config.GetLiveTvConfiguration();
|
||||
if (string.IsNullOrWhiteSpace(options.RecordingPostProcessor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = options.RecordingPostProcessorArguments
|
||||
.Replace("{path}", path, StringComparison.OrdinalIgnoreCase),
|
||||
CreateNoWindow = true,
|
||||
ErrorDialog = false,
|
||||
FileName = options.RecordingPostProcessor,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
_logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Exited += OnProcessExited;
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error running recording post processor");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProcessExited(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is Process process)
|
||||
{
|
||||
using (process)
|
||||
{
|
||||
_logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ public class AudioResolverTests
|
||||
public AudioResolverTests()
|
||||
{
|
||||
// prep BaseItem and Video for calls made that expect managers
|
||||
Video.LiveTvManager = Mock.Of<ILiveTvManager>();
|
||||
Video.RecordingsManager = Mock.Of<IRecordingsManager>();
|
||||
|
||||
var applicationPaths = new Mock<IServerApplicationPaths>().Object;
|
||||
var serverConfig = new Mock<IServerConfigurationManager>();
|
||||
|
@ -37,7 +37,7 @@ public class MediaInfoResolverTests
|
||||
public MediaInfoResolverTests()
|
||||
{
|
||||
// prep BaseItem and Video for calls made that expect managers
|
||||
Video.LiveTvManager = Mock.Of<ILiveTvManager>();
|
||||
Video.RecordingsManager = Mock.Of<IRecordingsManager>();
|
||||
|
||||
var applicationPaths = new Mock<IServerApplicationPaths>().Object;
|
||||
var serverConfig = new Mock<IServerConfigurationManager>();
|
||||
|
@ -26,7 +26,7 @@ public class SubtitleResolverTests
|
||||
public SubtitleResolverTests()
|
||||
{
|
||||
// prep BaseItem and Video for calls made that expect managers
|
||||
Video.LiveTvManager = Mock.Of<ILiveTvManager>();
|
||||
Video.RecordingsManager = Mock.Of<IRecordingsManager>();
|
||||
|
||||
var applicationPaths = new Mock<IServerApplicationPaths>().Object;
|
||||
var serverConfig = new Mock<IServerConfigurationManager>();
|
||||
|
Loading…
Reference in New Issue
Block a user