add optional ffmpeg reports (#81)
* log full exceptions in plex tv api client * add optional ffmpeg reports
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
@@ -70,12 +71,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
|
|
||||||
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
|
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
|
||||||
{
|
{
|
||||||
Option<ConfigElement> ffmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath);
|
await _configElementRepository.Get(ConfigElementKey.FFmpegPath).Match(
|
||||||
Option<ConfigElement> ffprobePath = await _configElementRepository.Get(ConfigElementKey.FFprobePath);
|
|
||||||
Option<ConfigElement> defaultFFmpegProfileId =
|
|
||||||
await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId);
|
|
||||||
|
|
||||||
ffmpegPath.Match(
|
|
||||||
ce =>
|
ce =>
|
||||||
{
|
{
|
||||||
ce.Value = request.Settings.FFmpegPath;
|
ce.Value = request.Settings.FFmpegPath;
|
||||||
@@ -88,7 +84,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
_configElementRepository.Add(ce);
|
_configElementRepository.Add(ce);
|
||||||
});
|
});
|
||||||
|
|
||||||
ffprobePath.Match(
|
await _configElementRepository.Get(ConfigElementKey.FFprobePath).Match(
|
||||||
ce =>
|
ce =>
|
||||||
{
|
{
|
||||||
ce.Value = request.Settings.FFprobePath;
|
ce.Value = request.Settings.FFprobePath;
|
||||||
@@ -101,7 +97,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
_configElementRepository.Add(ce);
|
_configElementRepository.Add(ce);
|
||||||
});
|
});
|
||||||
|
|
||||||
defaultFFmpegProfileId.Match(
|
await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId).Match(
|
||||||
ce =>
|
ce =>
|
||||||
{
|
{
|
||||||
ce.Value = request.Settings.DefaultFFmpegProfileId.ToString();
|
ce.Value = request.Settings.DefaultFFmpegProfileId.ToString();
|
||||||
@@ -117,6 +113,27 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
_configElementRepository.Add(ce);
|
_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;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
public string FFmpegPath { get; set; }
|
public string FFmpegPath { get; set; }
|
||||||
public string FFprobePath { get; set; }
|
public string FFprobePath { get; set; }
|
||||||
public int DefaultFFmpegProfileId { get; set; }
|
public int DefaultFFmpegProfileId { get; set; }
|
||||||
|
public bool SaveReports { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,15 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
|||||||
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
|
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
|
||||||
Option<int> defaultFFmpegProfileId =
|
Option<int> defaultFFmpegProfileId =
|
||||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
|
||||||
|
Option<bool> saveReports =
|
||||||
|
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
|
||||||
|
|
||||||
return new FFmpegSettingsViewModel
|
return new FFmpegSettingsViewModel
|
||||||
{
|
{
|
||||||
FFmpegPath = ffmpegPath.IfNone(string.Empty),
|
FFmpegPath = ffmpegPath.IfNone(string.Empty),
|
||||||
FFprobePath = ffprobePath.IfNone(string.Empty),
|
FFprobePath = ffprobePath.IfNone(string.Empty),
|
||||||
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0)
|
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0),
|
||||||
|
SaveReports = saveReports.IfNone(false)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -23,6 +23,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
private readonly ILocalFileSystem _localFileSystem;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
|
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
|
||||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
private readonly IPlayoutRepository _playoutRepository;
|
private readonly IPlayoutRepository _playoutRepository;
|
||||||
|
|
||||||
public GetPlayoutItemProcessByChannelNumberHandler(
|
public GetPlayoutItemProcessByChannelNumberHandler(
|
||||||
@@ -35,6 +36,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
|
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
|
||||||
: base(channelRepository, configElementRepository)
|
: base(channelRepository, configElementRepository)
|
||||||
{
|
{
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
_playoutRepository = playoutRepository;
|
_playoutRepository = playoutRepository;
|
||||||
_mediaSourceRepository = mediaSourceRepository;
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
_ffmpegProcessService = ffmpegProcessService;
|
_ffmpegProcessService = ffmpegProcessService;
|
||||||
@@ -54,7 +56,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
.BindT(ValidatePlayoutItemPath);
|
.BindT(ValidatePlayoutItemPath);
|
||||||
|
|
||||||
return await maybePlayoutItem.Match(
|
return await maybePlayoutItem.Match(
|
||||||
playoutItemWithPath =>
|
async playoutItemWithPath =>
|
||||||
{
|
{
|
||||||
MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem switch
|
MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem switch
|
||||||
{
|
{
|
||||||
@@ -63,14 +65,18 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
_ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath))
|
_ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool saveReports = await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||||
|
.Map(result => result.IfNone(false));
|
||||||
|
|
||||||
return Right<BaseError, Process>(
|
return Right<BaseError, Process>(
|
||||||
_ffmpegProcessService.ForPlayoutItem(
|
_ffmpegProcessService.ForPlayoutItem(
|
||||||
ffmpegPath,
|
ffmpegPath,
|
||||||
|
saveReports,
|
||||||
channel,
|
channel,
|
||||||
version,
|
version,
|
||||||
playoutItemWithPath.Path,
|
playoutItemWithPath.Path,
|
||||||
playoutItemWithPath.PlayoutItem.StartOffset,
|
playoutItemWithPath.PlayoutItem.StartOffset,
|
||||||
now)).AsTask();
|
now));
|
||||||
},
|
},
|
||||||
async error =>
|
async error =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,5 +10,6 @@
|
|||||||
public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path");
|
public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path");
|
||||||
public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id");
|
public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id");
|
||||||
public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id");
|
public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id");
|
||||||
|
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
@@ -39,9 +40,14 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
|
|
||||||
private readonly List<string> _arguments = new();
|
private readonly List<string> _arguments = new();
|
||||||
private readonly string _ffmpegPath;
|
private readonly string _ffmpegPath;
|
||||||
|
private readonly bool _saveReports;
|
||||||
private FFmpegComplexFilterBuilder _complexFilterBuilder = new();
|
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)
|
public FFmpegProcessBuilder WithThreads(int threads)
|
||||||
{
|
{
|
||||||
@@ -365,6 +371,12 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
StandardOutputEncoding = Encoding.UTF8
|
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)
|
foreach (string argument in _arguments)
|
||||||
{
|
{
|
||||||
startInfo.ArgumentList.Add(argument);
|
startInfo.ArgumentList.Add(argument);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
|
|
||||||
public Process ForPlayoutItem(
|
public Process ForPlayoutItem(
|
||||||
string ffmpegPath,
|
string ffmpegPath,
|
||||||
|
bool saveReports,
|
||||||
Channel channel,
|
Channel channel,
|
||||||
MediaVersion version,
|
MediaVersion version,
|
||||||
string path,
|
string path,
|
||||||
@@ -28,7 +29,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
start,
|
start,
|
||||||
now);
|
now);
|
||||||
|
|
||||||
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath)
|
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports)
|
||||||
.WithThreads(playbackSettings.ThreadCount)
|
.WithThreads(playbackSettings.ThreadCount)
|
||||||
.WithHardwareAcceleration(playbackSettings.HardwareAcceleration)
|
.WithHardwareAcceleration(playbackSettings.HardwareAcceleration)
|
||||||
.WithQuiet()
|
.WithQuiet()
|
||||||
@@ -91,7 +92,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
|
|
||||||
IDisplaySize desiredResolution = channel.FFmpegProfile.Resolution;
|
IDisplaySize desiredResolution = channel.FFmpegProfile.Resolution;
|
||||||
|
|
||||||
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath)
|
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false)
|
||||||
.WithThreads(1)
|
.WithThreads(1)
|
||||||
.WithQuiet()
|
.WithQuiet()
|
||||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||||
@@ -114,7 +115,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
{
|
{
|
||||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
|
||||||
|
|
||||||
return new FFmpegProcessBuilder(ffmpegPath)
|
return new FFmpegProcessBuilder(ffmpegPath, false)
|
||||||
.WithThreads(1)
|
.WithThreads(1)
|
||||||
.WithQuiet()
|
.WithQuiet()
|
||||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ namespace ErsatzTV.Core
|
|||||||
|
|
||||||
public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json");
|
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 ArtworkCacheFolder = Path.Combine(AppDataFolder, "cache", "artwork");
|
||||||
|
|
||||||
public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters");
|
public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Plex;
|
|||||||
using ErsatzTV.Core.Plex;
|
using ErsatzTV.Core.Plex;
|
||||||
using ErsatzTV.Infrastructure.Plex.Models;
|
using ErsatzTV.Infrastructure.Plex.Models;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Refit;
|
using Refit;
|
||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Plex
|
namespace ErsatzTV.Infrastructure.Plex
|
||||||
@@ -15,14 +16,16 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
public class PlexTvApiClient : IPlexTvApiClient
|
public class PlexTvApiClient : IPlexTvApiClient
|
||||||
{
|
{
|
||||||
private const string AppName = "ErsatzTV";
|
private const string AppName = "ErsatzTV";
|
||||||
|
private readonly ILogger<PlexTvApiClient> _logger;
|
||||||
private readonly IPlexSecretStore _plexSecretStore;
|
private readonly IPlexSecretStore _plexSecretStore;
|
||||||
|
|
||||||
private readonly IPlexTvApi _plexTvApi;
|
private readonly IPlexTvApi _plexTvApi;
|
||||||
|
|
||||||
public PlexTvApiClient(IPlexTvApi plexTvApi, IPlexSecretStore plexSecretStore)
|
public PlexTvApiClient(IPlexTvApi plexTvApi, IPlexSecretStore plexSecretStore, ILogger<PlexTvApiClient> logger)
|
||||||
{
|
{
|
||||||
_plexTvApi = plexTvApi;
|
_plexTvApi = plexTvApi;
|
||||||
_plexSecretStore = plexSecretStore;
|
_plexSecretStore = plexSecretStore;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Either<BaseError, List<PlexMediaSource>>> GetServers()
|
public async Task<Either<BaseError, List<PlexMediaSource>>> GetServers()
|
||||||
@@ -85,11 +88,12 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
{
|
{
|
||||||
await _plexSecretStore.DeleteAll();
|
await _plexSecretStore.DeleteAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
return BaseError.New(apiException.Message);
|
return BaseError.New(apiException.Message);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(ex, "Error getting plex servers");
|
||||||
return BaseError.New(ex.Message);
|
return BaseError.New(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +108,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(ex, "Error starting plex pin flow");
|
||||||
return BaseError.New(ex.Message);
|
return BaseError.New(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,9 +135,9 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// ignored
|
_logger.LogError(ex, "Error completing plex pin flow");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -29,6 +29,12 @@
|
|||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
</MudElement>
|
</MudElement>
|
||||||
|
<MudElement HtmlTag="div" Class="mt-3">
|
||||||
|
<MudSwitch T="bool"
|
||||||
|
Label="Save troubleshooting reports to disk"
|
||||||
|
Color="Color.Primary"
|
||||||
|
@bind-Checked="@_ffmpegSettings.SaveReports" />
|
||||||
|
</MudElement>
|
||||||
</MudForm>
|
</MudForm>
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
<MudCardActions>
|
<MudCardActions>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
@* @page "/media/sources" *@
|
|
||||||
@* @page "/media/sources/{PanelIndex:int}" *@
|
|
||||||
@* @inject NavigationManager NavigationManager *@
|
|
||||||
@* *@
|
|
||||||
@* <MudContainer MaxWidth="MaxWidth.ExtraLarge"> *@
|
|
||||||
@* <MudTabs @ref="_tabs" Elevation="1"> *@
|
|
||||||
@* <MudTabPanel Text="Local" OnClick="@(() => NavigationManager.NavigateTo("/media/sources/0"))"> *@
|
|
||||||
@* <LocalMediaSources></LocalMediaSources> *@
|
|
||||||
@* </MudTabPanel> *@
|
|
||||||
@* <MudTabPanel Text="Plex" OnClick="@(() => NavigationManager.NavigateTo("/media/sources/1"))"> *@
|
|
||||||
@* <PlexMediaSources></PlexMediaSources> *@
|
|
||||||
@* </MudTabPanel> *@
|
|
||||||
@* </MudTabs> *@
|
|
||||||
@* </MudContainer> *@
|
|
||||||
@* *@
|
|
||||||
@* @code { *@
|
|
||||||
@* *@
|
|
||||||
@* [Parameter] *@
|
|
||||||
@* public int PanelIndex { get; set; } *@
|
|
||||||
@* *@
|
|
||||||
@* private MudTabs _tabs; *@
|
|
||||||
@* *@
|
|
||||||
@* protected override void OnAfterRender(bool firstRender) => _tabs.ActivatePanel(PanelIndex); *@
|
|
||||||
@* *@
|
|
||||||
@* } *@
|
|
||||||
Reference in New Issue
Block a user