emby-toolbox/EmbyToolbox/ViewModels/VideoInfoViewModel.cs

484 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.IO;
using EmbyToolbox.Models;
using EmbyToolbox.Services;
using Microsoft.Win32;
namespace EmbyToolbox.ViewModels;
public sealed class VideoInfoViewModel : INotifyPropertyChanged
{
private readonly FfprobeService _ffprobeService;
private readonly LoggingService _logging;
private readonly RecentPathService _recentPaths;
private readonly SidecarDiscoveryService _sidecarDiscoveryService;
private readonly VideoInfoSummaryService _summaryService;
private string _selectedFilePath = string.Empty;
private string _analysisStateText = string.Empty;
private string _errorMessage = string.Empty;
private string _formattedJson = string.Empty;
private string _summaryText = string.Empty;
private bool _isBusy;
private bool _isVideoInfoDropHighlight;
private int _selectedSubTabIndex;
public VideoInfoViewModel(
FfprobeService ffprobeService,
LoggingService logging,
RecentPathService recentPaths,
SidecarDiscoveryService sidecarDiscoveryService,
VideoInfoSummaryService summaryService)
{
_ffprobeService = ffprobeService;
_logging = logging;
_recentPaths = recentPaths;
_sidecarDiscoveryService = sidecarDiscoveryService;
_summaryService = summaryService;
SelectFileCommand = new RelayCommand(ExecuteSelectFile);
SelectSummaryFilesCommand = new RelayCommand(ExecuteSelectSummaryFiles);
CopySummaryCommand = new RelayCommand(ExecuteCopySummary, () => !string.IsNullOrWhiteSpace(SummaryText) && SummaryText != "Файл не проанализирован");
SaveSummaryCommand = new RelayCommand(ExecuteSaveSummary, () => !string.IsNullOrWhiteSpace(SummaryText) && SummaryText != "Файл не проанализирован");
}
public ICommand SelectFileCommand { get; }
public ICommand SelectSummaryFilesCommand { get; }
public ICommand CopySummaryCommand { get; }
public ICommand SaveSummaryCommand { get; }
public string SelectedFilePath
{
get => _selectedFilePath;
private set
{
if (_selectedFilePath == value)
{
return;
}
_selectedFilePath = value;
OnPropertyChanged();
}
}
public string AnalysisStateText
{
get => _analysisStateText;
private set
{
if (_analysisStateText == value)
{
return;
}
_analysisStateText = value;
OnPropertyChanged();
}
}
public string ErrorMessage
{
get => _errorMessage;
private set
{
if (_errorMessage == value)
{
return;
}
_errorMessage = value;
OnPropertyChanged();
}
}
public string FormattedJson
{
get => _formattedJson;
private set
{
if (_formattedJson == value)
{
return;
}
_formattedJson = value;
OnPropertyChanged();
}
}
public bool IsBusy
{
get => _isBusy;
private set
{
if (_isBusy == value)
{
return;
}
_isBusy = value;
OnPropertyChanged();
}
}
public int SelectedSubTabIndex
{
get => _selectedSubTabIndex;
set
{
if (_selectedSubTabIndex == value)
{
return;
}
_selectedSubTabIndex = value;
OnPropertyChanged();
}
}
public string SummaryText
{
get => _summaryText;
private set
{
if (_summaryText == value)
{
return;
}
_summaryText = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasSummaryData));
}
}
public bool HasSummaryData => !string.IsNullOrWhiteSpace(_summaryText);
public bool IsVideoInfoDropHighlight
{
get => _isVideoInfoDropHighlight;
internal set
{
if (_isVideoInfoDropHighlight == value)
{
return;
}
_isVideoInfoDropHighlight = value;
OnPropertyChanged();
}
}
/// <summary>На вкладке summary анализирует все поддерживаемые файлы; на detailed — только первый.</summary>
public void ApplyDroppedPathsAndAnalyze(string[]? paths)
{
if (paths is null || paths.Length == 0)
{
return;
}
IsVideoInfoDropHighlight = false;
if (IsBusy)
{
_logging.Warning("video-info (drop): анализ уже выполняется — повторите после завершения", "video-info");
return;
}
_ = SelectedSubTabIndex == 0
? ApplyDroppedSummaryInternalAsync(paths)
: ApplyDroppedDetailedInternalAsync(paths);
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private async void ExecuteSelectFile()
{
var dialog = new OpenFileDialog
{
Title = "Выберите видеофайл",
Filter = SupportedVideoFormats.BuildOpenFileDialogFilter(),
InitialDirectory = _recentPaths.GetInitialDirectory(RecentPathScenario.VideoInfoOpenFile),
};
if (dialog.ShowDialog() != true)
{
return;
}
if (!SupportedVideoFormats.IsSupportedVideoFile(dialog.FileName))
{
_logging.Warning($"video-info: формат не поддерживается: {dialog.FileName}", "video-info");
return;
}
_recentPaths.RememberChosenFiles(RecentPathScenario.VideoInfoOpenFile, [dialog.FileName]);
SelectedFilePath = dialog.FileName;
_logging.Info($"выбран файл: {dialog.FileName}", "video-info");
_logging.Debug("запуск ffprobe", "video-info.ffprobe");
await AnalyzeAsync();
}
private async void ExecuteSelectSummaryFiles()
{
var dialog = new OpenFileDialog
{
Title = "Выберите видеофайлы",
Filter = SupportedVideoFormats.BuildOpenFileDialogFilter(),
InitialDirectory = _recentPaths.GetInitialDirectory(RecentPathScenario.VideoInfoOpenFile),
Multiselect = true
};
if (dialog.ShowDialog() != true || dialog.FileNames.Length == 0)
{
return;
}
_recentPaths.RememberChosenFiles(RecentPathScenario.VideoInfoOpenFile, dialog.FileNames);
await AnalyzeSummaryFilesAsync(dialog.FileNames);
}
private async Task ApplyDroppedDetailedInternalAsync(string[] paths)
{
try
{
foreach (var raw in paths.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
{
string full;
try
{
full = Path.GetFullPath(raw);
}
catch (Exception ex)
{
_logging.Warning($"video-info (drop): путь «{raw}»: {ex.Message}", "video-info");
continue;
}
if (!File.Exists(full))
{
continue;
}
if (!SupportedVideoFormats.IsSupportedVideoFile(full))
{
_logging.Warning($"video-info (drop): формат не поддерживается: {full}", "video-info");
continue;
}
_recentPaths.RememberChosenFiles(RecentPathScenario.VideoInfoOpenFile, [full]);
SelectedFilePath = full;
_logging.Info($"video-info (drop): запуск анализа {full}", "video-info");
await AnalyzeAsync();
return;
}
_logging.Warning("video-info (drop): ни один поддерживаемый видеофайл не найден", "video-info");
}
catch (Exception ex)
{
_logging.Error($"video-info (drop): {ex.Message}", "video-info", ex);
}
}
private async Task AnalyzeAsync()
{
ErrorMessage = string.Empty;
FormattedJson = string.Empty;
RaiseCommandStates();
if (string.IsNullOrWhiteSpace(SelectedFilePath))
{
AnalysisStateText = string.Empty;
return;
}
IsBusy = true;
AnalysisStateText = "Анализ файла...";
var result = await _ffprobeService.AnalyzeAsync(SelectedFilePath);
IsBusy = false;
if (!result.IsSuccess)
{
AnalysisStateText = "Ошибка анализа";
ErrorMessage = result.Error;
_logging.Error($"ffprobe: {result.Error}", "video-info.ffprobe", command: result.Command, stdout: result.StdOut, stderr: result.StdErr);
RaiseCommandStates();
return;
}
try
{
FormattedJson = FormatJsonOrFallback(result.Json);
AnalysisStateText = "Готово";
RaiseCommandStates();
_logging.Info($"ffprobe завершен: {Path.GetFileName(SelectedFilePath)}", "video-info.ffprobe", command: result.Command, stdout: result.StdOut, stderr: result.StdErr);
}
catch (Exception ex)
{
AnalysisStateText = "Ошибка анализа";
ErrorMessage = $"Не удалось разобрать JSON ffprobe: {ex.Message}";
_logging.Error($"parse json: {ex.Message}", "video-info.json", ex);
RaiseCommandStates();
}
}
private async Task ApplyDroppedSummaryInternalAsync(string[] paths)
{
try
{
await AnalyzeSummaryFilesAsync(paths);
}
catch (Exception ex)
{
_logging.Error($"video-info summary (drop): {ex.Message}", "video-info", ex);
}
}
private async Task AnalyzeSummaryFilesAsync(IEnumerable<string> rawPaths)
{
SummaryText = string.Empty;
ErrorMessage = string.Empty;
AnalysisStateText = "Анализ файла...";
var supportedFiles = rawPaths
.Select(
p =>
{
try
{
return Path.GetFullPath(p);
}
catch
{
return string.Empty;
}
})
.Where(p => !string.IsNullOrWhiteSpace(p))
.Where(File.Exists)
.Where(SupportedVideoFormats.IsSupportedVideoFile)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
.ToList();
if (supportedFiles.Count == 0)
{
AnalysisStateText = string.Empty;
return;
}
IsBusy = true;
var all = new List<string>(supportedFiles.Count * 8);
foreach (var file in supportedFiles)
{
var probe = await _ffprobeService.AnalyzeAsync(file);
if (!probe.IsSuccess)
{
all.Add($"{file}{Environment.NewLine}Ошибка анализа: {probe.Error}");
all.Add(string.Empty);
continue;
}
var media = MediaAnalysisParser.TryParse(probe.Json);
if (media is null)
{
all.Add($"{file}{Environment.NewLine}Ошибка разбора ffprobe JSON");
all.Add(string.Empty);
continue;
}
var sidecarResult = await _sidecarDiscoveryService.DiscoverAsync(file, _ffprobeService).ConfigureAwait(true);
var summarySidecars = new SidecarAnalysisResult(file, sidecarResult.Sidecars, sidecarResult.ExternalAudioFiles);
all.Add(file);
all.Add(_summaryService.BuildSummary(media, summarySidecars));
all.Add(string.Empty);
}
IsBusy = false;
SummaryText = string.Join(Environment.NewLine, all).Trim();
AnalysisStateText = "Готово";
RaiseCommandStates();
}
private void ExecuteCopySummary()
{
if (string.IsNullOrWhiteSpace(SummaryText) || SummaryText == "Файл не проанализирован")
{
return;
}
Clipboard.SetText(SummaryText);
_logging.Info("summary скопирован в буфер обмена", "video-info.copy");
}
private void ExecuteSaveSummary()
{
if (string.IsNullOrWhiteSpace(SummaryText) || SummaryText == "Файл не проанализирован")
{
return;
}
var defaultName = string.IsNullOrWhiteSpace(SelectedFilePath)
? "video-summary.txt"
: $"{Path.GetFileNameWithoutExtension(SelectedFilePath)}.summary.txt";
var initialDir = string.IsNullOrWhiteSpace(SelectedFilePath)
? _recentPaths.GetInitialDirectory(RecentPathScenario.SettingsOutputFolder)
: Path.GetDirectoryName(SelectedFilePath) ?? _recentPaths.GetInitialDirectory(RecentPathScenario.SettingsOutputFolder);
var dialog = new SaveFileDialog
{
Title = "Сохранить summary",
Filter = "Text (*.txt)|*.txt|Все файлы|*.*",
FileName = defaultName,
InitialDirectory = initialDir
};
if (dialog.ShowDialog() != true)
{
return;
}
File.WriteAllText(dialog.FileName, SummaryText);
_recentPaths.RememberChosenFolder(
RecentPathScenario.SettingsOutputFolder,
Path.GetDirectoryName(dialog.FileName) ?? dialog.FileName);
_logging.Info($"summary сохранен: {dialog.FileName}", "video-info.save");
}
private void RaiseCommandStates()
{
(CopySummaryCommand as RelayCommand)?.RaiseCanExecuteChanged();
(SaveSummaryCommand as RelayCommand)?.RaiseCanExecuteChanged();
}
private static string FormatJsonOrFallback(string json)
{
try
{
using var doc = JsonDocument.Parse(json);
return JsonSerializer.Serialize(doc.RootElement, new JsonSerializerOptions { WriteIndented = true });
}
catch
{
return json;
}
}
}