diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs index e1486dbec..c2468cb6a 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.IO; using System.Threading; using System.Threading.Tasks; using ErsatzTV.Core; @@ -70,12 +71,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands private async Task ApplyUpdate(UpdateFFmpegSettings request) { - Option ffmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath); - Option ffprobePath = await _configElementRepository.Get(ConfigElementKey.FFprobePath); - Option defaultFFmpegProfileId = - await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId); - - ffmpegPath.Match( + await _configElementRepository.Get(ConfigElementKey.FFmpegPath).Match( ce => { ce.Value = request.Settings.FFmpegPath; @@ -88,7 +84,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands _configElementRepository.Add(ce); }); - ffprobePath.Match( + await _configElementRepository.Get(ConfigElementKey.FFprobePath).Match( ce => { ce.Value = request.Settings.FFprobePath; @@ -101,7 +97,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands _configElementRepository.Add(ce); }); - defaultFFmpegProfileId.Match( + await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId).Match( ce => { ce.Value = request.Settings.DefaultFFmpegProfileId.ToString(); @@ -117,6 +113,27 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands _configElementRepository.Add(ce); }); + await _configElementRepository.Get(ConfigElementKey.FFmpegSaveReports).Match( + ce => + { + ce.Value = request.Settings.SaveReports.ToString(); + _configElementRepository.Update(ce); + }, + () => + { + var ce = new ConfigElement + { + Key = ConfigElementKey.FFmpegSaveReports.Key, + Value = request.Settings.SaveReports.ToString() + }; + _configElementRepository.Add(ce); + }); + + if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder)) + { + Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder); + } + return Unit.Default; } } diff --git a/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs b/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs index 6bf3f9cdd..16a72bd0a 100644 --- a/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs +++ b/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs @@ -5,5 +5,6 @@ public string FFmpegPath { get; set; } public string FFprobePath { get; set; } public int DefaultFFmpegProfileId { get; set; } + public bool SaveReports { get; set; } } } diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs index 0037ae66a..496afe82e 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs @@ -22,12 +22,15 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries Option ffprobePath = await _configElementRepository.GetValue(ConfigElementKey.FFprobePath); Option defaultFFmpegProfileId = await _configElementRepository.GetValue(ConfigElementKey.FFmpegDefaultProfileId); + Option saveReports = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegSaveReports); return new FFmpegSettingsViewModel { FFmpegPath = ffmpegPath.IfNone(string.Empty), FFprobePath = ffprobePath.IfNone(string.Empty), - DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0) + DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0), + SaveReports = saveReports.IfNone(false) }; } } diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index a40054dbf..1a0dba983 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -23,6 +23,7 @@ namespace ErsatzTV.Application.Streaming.Queries private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IConfigElementRepository _configElementRepository; private readonly IPlayoutRepository _playoutRepository; public GetPlayoutItemProcessByChannelNumberHandler( @@ -35,6 +36,7 @@ namespace ErsatzTV.Application.Streaming.Queries ILogger logger) : base(channelRepository, configElementRepository) { + _configElementRepository = configElementRepository; _playoutRepository = playoutRepository; _mediaSourceRepository = mediaSourceRepository; _ffmpegProcessService = ffmpegProcessService; @@ -54,7 +56,7 @@ namespace ErsatzTV.Application.Streaming.Queries .BindT(ValidatePlayoutItemPath); return await maybePlayoutItem.Match( - playoutItemWithPath => + async playoutItemWithPath => { MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem switch { @@ -63,14 +65,18 @@ namespace ErsatzTV.Application.Streaming.Queries _ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath)) }; + bool saveReports = await _configElementRepository.GetValue(ConfigElementKey.FFmpegSaveReports) + .Map(result => result.IfNone(false)); + return Right( _ffmpegProcessService.ForPlayoutItem( ffmpegPath, + saveReports, channel, version, playoutItemWithPath.Path, playoutItemWithPath.PlayoutItem.StartOffset, - now)).AsTask(); + now)); }, async error => { diff --git a/ErsatzTV.Core/Domain/ConfigElementKey.cs b/ErsatzTV.Core/Domain/ConfigElementKey.cs index 286bbefe1..7b397baf0 100644 --- a/ErsatzTV.Core/Domain/ConfigElementKey.cs +++ b/ErsatzTV.Core/Domain/ConfigElementKey.cs @@ -10,5 +10,6 @@ public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path"); public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id"); public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id"); + public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports"); } } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs index 23cf4c3f0..8d3d531ce 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs @@ -21,6 +21,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Text; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.FFmpeg; @@ -39,9 +40,14 @@ namespace ErsatzTV.Core.FFmpeg private readonly List _arguments = new(); private readonly string _ffmpegPath; + private readonly bool _saveReports; private FFmpegComplexFilterBuilder _complexFilterBuilder = new(); - public FFmpegProcessBuilder(string ffmpegPath) => _ffmpegPath = ffmpegPath; + public FFmpegProcessBuilder(string ffmpegPath, bool saveReports) + { + _ffmpegPath = ffmpegPath; + _saveReports = saveReports; + } public FFmpegProcessBuilder WithThreads(int threads) { @@ -365,6 +371,12 @@ namespace ErsatzTV.Core.FFmpeg StandardOutputEncoding = Encoding.UTF8 }; + if (_saveReports) + { + string fileName = Path.Combine(FileSystemLayout.FFmpegReportsFolder, "%p-%t.log"); + startInfo.EnvironmentVariables.Add("FFREPORT", $"file={fileName}:level=32"); + } + foreach (string argument in _arguments) { startInfo.ArgumentList.Add(argument); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index 5d6aeb774..0b2febdef 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -15,6 +15,7 @@ namespace ErsatzTV.Core.FFmpeg public Process ForPlayoutItem( string ffmpegPath, + bool saveReports, Channel channel, MediaVersion version, string path, @@ -28,7 +29,7 @@ namespace ErsatzTV.Core.FFmpeg start, now); - FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath) + FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports) .WithThreads(playbackSettings.ThreadCount) .WithHardwareAcceleration(playbackSettings.HardwareAcceleration) .WithQuiet() @@ -91,7 +92,7 @@ namespace ErsatzTV.Core.FFmpeg IDisplaySize desiredResolution = channel.FFmpegProfile.Resolution; - FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath) + FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false) .WithThreads(1) .WithQuiet() .WithFormatFlags(playbackSettings.FormatFlags) @@ -114,7 +115,7 @@ namespace ErsatzTV.Core.FFmpeg { FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings; - return new FFmpegProcessBuilder(ffmpegPath) + return new FFmpegProcessBuilder(ffmpegPath, false) .WithThreads(1) .WithQuiet() .WithFormatFlags(playbackSettings.FormatFlags) diff --git a/ErsatzTV.Core/FileSystemLayout.cs b/ErsatzTV.Core/FileSystemLayout.cs index 7c31ae6fb..f1525d5dc 100644 --- a/ErsatzTV.Core/FileSystemLayout.cs +++ b/ErsatzTV.Core/FileSystemLayout.cs @@ -19,6 +19,8 @@ namespace ErsatzTV.Core public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json"); + public static readonly string FFmpegReportsFolder = Path.Combine(AppDataFolder, "ffmpeg-reports"); + public static readonly string ArtworkCacheFolder = Path.Combine(AppDataFolder, "cache", "artwork"); public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters"); diff --git a/ErsatzTV.Infrastructure/Plex/PlexTvApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexTvApiClient.cs index c68cad1fc..615f2b7dd 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexTvApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexTvApiClient.cs @@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Plex; using ErsatzTV.Infrastructure.Plex.Models; using LanguageExt; +using Microsoft.Extensions.Logging; using Refit; namespace ErsatzTV.Infrastructure.Plex @@ -15,14 +16,16 @@ namespace ErsatzTV.Infrastructure.Plex public class PlexTvApiClient : IPlexTvApiClient { private const string AppName = "ErsatzTV"; + private readonly ILogger _logger; private readonly IPlexSecretStore _plexSecretStore; private readonly IPlexTvApi _plexTvApi; - public PlexTvApiClient(IPlexTvApi plexTvApi, IPlexSecretStore plexSecretStore) + public PlexTvApiClient(IPlexTvApi plexTvApi, IPlexSecretStore plexSecretStore, ILogger logger) { _plexTvApi = plexTvApi; _plexSecretStore = plexSecretStore; + _logger = logger; } public async Task>> GetServers() @@ -85,11 +88,12 @@ namespace ErsatzTV.Infrastructure.Plex { await _plexSecretStore.DeleteAll(); } - + return BaseError.New(apiException.Message); } catch (Exception ex) { + _logger.LogError(ex, "Error getting plex servers"); return BaseError.New(ex.Message); } } @@ -104,6 +108,7 @@ namespace ErsatzTV.Infrastructure.Plex } catch (Exception ex) { + _logger.LogError(ex, "Error starting plex pin flow"); return BaseError.New(ex.Message); } } @@ -130,9 +135,9 @@ namespace ErsatzTV.Infrastructure.Plex return true; } } - catch (Exception) + catch (Exception ex) { - // ignored + _logger.LogError(ex, "Error completing plex pin flow"); } return false; diff --git a/ErsatzTV/Pages/FFmpeg.razor b/ErsatzTV/Pages/FFmpeg.razor index 793fb0f53..73210d9bb 100644 --- a/ErsatzTV/Pages/FFmpeg.razor +++ b/ErsatzTV/Pages/FFmpeg.razor @@ -29,6 +29,12 @@ } + + + diff --git a/ErsatzTV/Pages/MediaSources.razor b/ErsatzTV/Pages/MediaSources.razor deleted file mode 100644 index 7a24157d8..000000000 --- a/ErsatzTV/Pages/MediaSources.razor +++ /dev/null @@ -1,25 +0,0 @@ -@* @page "/media/sources" *@ -@* @page "/media/sources/{PanelIndex:int}" *@ -@* @inject NavigationManager NavigationManager *@ -@* *@ -@* *@ -@* *@ -@* *@ -@* *@ -@* *@ -@* *@ -@* *@ -@* *@ -@* *@ -@* *@ -@* *@ -@* @code { *@ -@* *@ -@* [Parameter] *@ -@* public int PanelIndex { get; set; } *@ -@* *@ -@* private MudTabs _tabs; *@ -@* *@ -@* protected override void OnAfterRender(bool firstRender) => _tabs.ActivatePanel(PanelIndex); *@ -@* *@ -@* } *@ \ No newline at end of file