From 764376ff348c9d23e9eea12b975f919574daee5e Mon Sep 17 00:00:00 2001 From: lorenz | onionware Date: Thu, 12 Dec 2024 15:06:36 +0100 Subject: [PATCH 1/9] LoggingService is ready but needs implementation and file writer --- .../OnionMedia.Core/Classes/LoggerClass.cs | 21 +++++++ OnionMedia/OnionMedia.Core/IoC.cs | 36 ++++++++++-- .../OnionMedia.Core/OnionMedia.Core.csproj | 1 + .../Services/Logging/CentralLoggerService.cs | 56 +++++++++++++++++++ .../Services/Logging/Classes/LogType.cs | 16 ++++++ .../Interfaces/ICentralLoggerService.cs | 12 ++++ .../Logging/Interfaces/ILogWriterService.cs | 7 +++ .../ViewModels/MediaViewModel.cs | 14 ++++- OnionMedia/OnionMedia/App.xaml.cs | 5 ++ OnionMedia/OnionMedia/OnionMedia.csproj | 6 ++ OnionMedia/OnionMedia/ServiceProvider.cs | 35 +++++++++++- .../OnionMedia/Services/LogWriterService.cs | 17 ++++++ 12 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 OnionMedia/OnionMedia.Core/Classes/LoggerClass.cs create mode 100644 OnionMedia/OnionMedia.Core/Services/Logging/CentralLoggerService.cs create mode 100644 OnionMedia/OnionMedia.Core/Services/Logging/Classes/LogType.cs create mode 100644 OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ICentralLoggerService.cs create mode 100644 OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ILogWriterService.cs create mode 100644 OnionMedia/OnionMedia/Services/LogWriterService.cs diff --git a/OnionMedia/OnionMedia.Core/Classes/LoggerClass.cs b/OnionMedia/OnionMedia.Core/Classes/LoggerClass.cs new file mode 100644 index 0000000..f80bf79 --- /dev/null +++ b/OnionMedia/OnionMedia.Core/Classes/LoggerClass.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Logging; +using OnionMedia.Core.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OnionMedia.Core.Classes +{ + public class LoggerClass + { + private readonly ILogger _logger; + + public LoggerClass(ILogger logger) + { + _logger = logger; + } + } +} + diff --git a/OnionMedia/OnionMedia.Core/IoC.cs b/OnionMedia/OnionMedia.Core/IoC.cs index 84901a8..4a18cef 100644 --- a/OnionMedia/OnionMedia.Core/IoC.cs +++ b/OnionMedia/OnionMedia.Core/IoC.cs @@ -10,22 +10,50 @@ */ using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace OnionMedia.Core; public sealed class IoC { public static IoC Default { get; } = new(); + private IServiceProvider iocProvider; + private readonly object lockObject = new(); + + public void InitializeServices(IServiceProvider serviceProvider) + { + if (iocProvider != null) + throw new InvalidOperationException("Service provider has already been initialized."); + + lock (lockObject) + { + iocProvider ??= serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + } - public void InitializeServices(IServiceProvider serviceProvider) => - iocProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + public void InitializeDefault(Action configureServices) + { + var serviceCollection = new ServiceCollection(); + configureServices(serviceCollection); + InitializeServices(serviceCollection.BuildServiceProvider()); + } public T? GetService() where T : class { if (iocProvider == null) throw new Exception("Service provider is not initialized."); - return (T?)iocProvider.GetService(typeof(T)); + return iocProvider.GetService(); + } + + public T GetRequiredService() where T : class + { + if (iocProvider == null) + throw new Exception("Service provider is not initialized."); + + return (T)iocProvider.GetService(typeof(T)) + ?? throw new InvalidOperationException($"Service of type {typeof(T).Name} is not registered."); } - private IServiceProvider iocProvider; } + diff --git a/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj b/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj index 96aa185..9cc5bce 100644 --- a/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj +++ b/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj @@ -9,6 +9,7 @@ + diff --git a/OnionMedia/OnionMedia.Core/Services/Logging/CentralLoggerService.cs b/OnionMedia/OnionMedia.Core/Services/Logging/CentralLoggerService.cs new file mode 100644 index 0000000..d1ca676 --- /dev/null +++ b/OnionMedia/OnionMedia.Core/Services/Logging/CentralLoggerService.cs @@ -0,0 +1,56 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OnionMedia.Core.Services.Logging.Classes; +using OnionMedia.Core.Services.Logging.Interfaces; + +namespace OnionMedia.Core.Services.Logging +{ + public class CentralLoggerService : ICentralLoggerService + { + private readonly ConcurrentQueue _logQueue = new ConcurrentQueue(); + private readonly ILogger _logger; + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + + public CentralLoggerService(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + StartLogging(); + } + + public void StartLogging() + { + Task.Run(() => ProcessLogsAsync(_cancellationTokenSource.Token)); + } + + + public void EnqueueLog(LogType log) + { + if (log == null) throw new ArgumentNullException(nameof(log)); + _logQueue.Enqueue(log); + } + + private async Task ProcessLogsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + if (_logQueue.TryDequeue(out LogType log)) + { + _logger.Log(log.LogPriority, "{Message} at {Time}", log.LogMessage, log.LogTime); + Debug.WriteLine((log.LogPriority, log.LogMessage, log.LogTime)); + } + else + { + await Task.Delay(100, cancellationToken); + } + } + } + + public void StopLogging() + { + _cancellationTokenSource.Cancel(); + } + } +} diff --git a/OnionMedia/OnionMedia.Core/Services/Logging/Classes/LogType.cs b/OnionMedia/OnionMedia.Core/Services/Logging/Classes/LogType.cs new file mode 100644 index 0000000..90f10b0 --- /dev/null +++ b/OnionMedia/OnionMedia.Core/Services/Logging/Classes/LogType.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OnionMedia.Core.Services.Logging.Classes +{ + public class LogType + { + public string LogMessage { get; set; } + public string LogTime { get; set; } + public LogLevel LogPriority { get;set; } + } +} diff --git a/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ICentralLoggerService.cs b/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ICentralLoggerService.cs new file mode 100644 index 0000000..a089292 --- /dev/null +++ b/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ICentralLoggerService.cs @@ -0,0 +1,12 @@ +using OnionMedia.Core.Services.Logging.Classes; +using System.Threading.Tasks; + +namespace OnionMedia.Core.Services.Logging.Interfaces +{ + public interface ICentralLoggerService + { + void EnqueueLog(LogType log); + + void StopLogging(); + } +} diff --git a/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ILogWriterService.cs b/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ILogWriterService.cs new file mode 100644 index 0000000..776ea47 --- /dev/null +++ b/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ILogWriterService.cs @@ -0,0 +1,7 @@ +namespace OnionMedia.Core.Services.Logging.Interfaces +{ + public interface ILogWriterService + { + public void Write(string Path); + } +} \ No newline at end of file diff --git a/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs index cb4bd32..96799fa 100644 --- a/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs +++ b/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs @@ -27,14 +27,26 @@ using OnionMedia.Core.Extensions; using OnionMedia.Core.Models; using OnionMedia.Core.Services; +using Microsoft.Extensions.Logging; +using OnionMedia.Core.Services.Logging.Classes; +using OnionMedia.Core.Services.Logging.Interfaces; +using Microsoft.Extensions.DependencyInjection; namespace OnionMedia.Core.ViewModels { [ObservableObject] public sealed partial class MediaViewModel { - public MediaViewModel(IDialogService dialogService, IDispatcherService dispatcher, IConversionPresetDialog conversionPresetDialog, IFiletagEditorDialog filetagEditorDialog, IToastNotificationService toastNotificationService, ITaskbarProgressService taskbarProgressService) + public MediaViewModel(IDialogService dialogService, IDispatcherService dispatcher, IConversionPresetDialog conversionPresetDialog, IFiletagEditorDialog filetagEditorDialog, IToastNotificationService toastNotificationService, ITaskbarProgressService taskbarProgressService, IServiceProvider serviceprovider) { + var loggerService = serviceprovider.GetRequiredService(); + + loggerService.EnqueueLog(new LogType + { + LogMessage = "Dies ist eine Testnachricht", + LogPriority = LogLevel.Warning, + LogTime = DateTime.Now.ToString("HH:mm:ss") + }); this.dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); this.conversionPresetDialog = conversionPresetDialog ?? throw new ArgumentNullException(nameof(conversionPresetDialog)); this.filetagEditorDialog = filetagEditorDialog ?? throw new ArgumentNullException(nameof(filetagEditorDialog)); diff --git a/OnionMedia/OnionMedia/App.xaml.cs b/OnionMedia/OnionMedia/App.xaml.cs index e8b65ea..e750059 100644 --- a/OnionMedia/OnionMedia/App.xaml.cs +++ b/OnionMedia/OnionMedia/App.xaml.cs @@ -40,6 +40,10 @@ using Microsoft.UI.Windowing; using Microsoft.UI.Composition.SystemBackdrops; using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Windows.Services.Maps; +using OnionMedia.Core.Services.Logging.Classes; +using OnionMedia.Core.Services.Logging.Interfaces; // To learn more about WinUI3, see: https://docs.microsoft.com/windows/apps/winui/winui3/. namespace OnionMedia @@ -57,6 +61,7 @@ public App() Ioc.Default.ConfigureServices(services); IoC.Default.InitializeServices(services); GlobalFFOptions.Configure(options => options.BinaryFolder = IoC.Default.GetService().ExternalBinariesDir); + } private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) diff --git a/OnionMedia/OnionMedia/OnionMedia.csproj b/OnionMedia/OnionMedia/OnionMedia.csproj index 4776b3f..30573b9 100644 --- a/OnionMedia/OnionMedia/OnionMedia.csproj +++ b/OnionMedia/OnionMedia/OnionMedia.csproj @@ -10,6 +10,9 @@ true 10.0.18362.0 + + true + @@ -107,7 +110,10 @@ + + + diff --git a/OnionMedia/OnionMedia/ServiceProvider.cs b/OnionMedia/OnionMedia/ServiceProvider.cs index 31791a4..d3a41e2 100644 --- a/OnionMedia/OnionMedia/ServiceProvider.cs +++ b/OnionMedia/OnionMedia/ServiceProvider.cs @@ -9,17 +9,31 @@ using OnionMedia.Services; using OnionMedia.ViewModels; using OnionMedia.Views; +using Microsoft.Extensions.Logging; +using OnionMedia.Core.Services.Logging; +using OnionMedia.Core.Services.Logging.Interfaces; +using System; +using Microsoft.Extensions.DependencyInjection; namespace OnionMedia; //Services/Activation Handler [ServiceProvider] + +//Logging +[Singleton(typeof(ILoggerFactory), Factory = nameof(CreateLoggerFactory))] +[Singleton(typeof(ILogger<>))] +[Singleton(typeof(ILogger), Factory = nameof(CreateLogger))] +[Singleton(typeof(ICentralLoggerService), typeof(CentralLoggerService))] + + [Singleton(typeof(IThemeSelectorService), typeof(ThemeSelectorService))] [Singleton(typeof(IPageService), typeof(PageService))] [Singleton(typeof(INavigationService), typeof(NavigationService))] [Transient(typeof(INavigationViewService), typeof(NavigationViewService))] [Transient(typeof(ActivationHandler), typeof(DefaultActivationHandler))] [Singleton(typeof(IActivationService), typeof(ActivationService))] + //Core Services [Singleton(typeof(IDataCollectionProvider), typeof(LibraryInfoProvider))] [Singleton(typeof(IDialogService), typeof(DialogService))] @@ -40,6 +54,7 @@ namespace OnionMedia; [Singleton(typeof(IWindowClosingService), typeof(WindowClosingService))] [Singleton(typeof(IPCPower), typeof(WindowsPowerService))] [Singleton(typeof(IFFmpegStartup), typeof(FFmpegStartup))] + //Views and ViewModels [Transient(typeof(ShellViewModel))] [Transient(typeof(ShellPage))] @@ -51,4 +66,22 @@ namespace OnionMedia; [Transient(typeof(SettingsPage))] [Transient(typeof(PlaylistsViewModel))] [Transient(typeof(PlaylistsPage))] -sealed partial class ServiceProvider { } \ No newline at end of file +sealed partial class ServiceProvider +{ + + private static ILogger CreateLogger(IServiceProvider serviceProvider) + { + var factory = serviceProvider.GetRequiredService(); + return factory.CreateLogger(); + } + + private static ILoggerFactory CreateLoggerFactory() + { + return LoggerFactory.Create(builder => + { + builder.AddConsole(); // Konfiguration für Logging, z. B. Console-Logging. + builder.SetMinimumLevel(LogLevel.Information); + }); + } + +} \ No newline at end of file diff --git a/OnionMedia/OnionMedia/Services/LogWriterService.cs b/OnionMedia/OnionMedia/Services/LogWriterService.cs new file mode 100644 index 0000000..9ef984c --- /dev/null +++ b/OnionMedia/OnionMedia/Services/LogWriterService.cs @@ -0,0 +1,17 @@ +using OnionMedia.Core.Services.Logging.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OnionMedia.Services +{ + public class LogWriterService : ILogWriterService + { + public void Write(string Path) + { + throw new NotImplementedException(); + } + } +} From e566c8331fef193ea9d11a028b5e98e5913e2620 Mon Sep 17 00:00:00 2001 From: lorenz | onionware Date: Wed, 29 Jan 2025 18:59:19 +0100 Subject: [PATCH 2/9] EditLogging --- .../Strings/en-us/Resources.resw | 3 + .../Strings/es/Resources.resw | 3 + .../OnionMedia.Core/Classes/LoggerClass.cs | 21 -- .../OnionMedia.Core/Models/AppSettings.cs | 23 ++- .../OnionMedia.Core/OnionMedia.Core.csproj | 5 +- .../Services/Logging/CentralLoggerService.cs | 56 ------ .../Interfaces/ICentralLoggerService.cs | 12 -- .../Logging/Interfaces/ILogWriterService.cs | 7 - .../Dialogs/PlaylistSelectorViewModel.cs | 1 + .../ViewModels/MediaViewModel.cs | 189 +++++++++++++----- .../ViewModels/SettingsViewModel.cs | 9 +- .../ViewModels/YouTubeDownloaderViewModel.cs | 48 ++++- OnionMedia/OnionMedia/App.xaml.cs | 2 - OnionMedia/OnionMedia/OnionMedia.csproj | 10 +- OnionMedia/OnionMedia/ServiceProvider.cs | 59 +++++- .../OnionMedia/Services/SettingsService.cs | 2 + .../Strings/de-de/SettingsPage.resw | 6 + .../Strings/en-us/SettingsPage.resw | 6 + .../OnionMedia/Strings/es/SettingsPage.resw | 6 + .../OnionMedia/Strings/nl/SettingsPage.resw | 8 +- OnionMedia/OnionMedia/Views/SettingsPage.xaml | 14 ++ 21 files changed, 316 insertions(+), 174 deletions(-) delete mode 100644 OnionMedia/OnionMedia.Core/Classes/LoggerClass.cs delete mode 100644 OnionMedia/OnionMedia.Core/Services/Logging/CentralLoggerService.cs delete mode 100644 OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ICentralLoggerService.cs delete mode 100644 OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ILogWriterService.cs diff --git a/OnionMedia/OnionMedia (Package)/Strings/en-us/Resources.resw b/OnionMedia/OnionMedia (Package)/Strings/en-us/Resources.resw index 72fd003..69c1a3a 100644 --- a/OnionMedia/OnionMedia (Package)/Strings/en-us/Resources.resw +++ b/OnionMedia/OnionMedia (Package)/Strings/en-us/Resources.resw @@ -120,4 +120,7 @@ OnionMedia is a free video and audio file converter and downloader. It supports a large number of formats and codecs and is available on GitHub and the Microsoft Store. + + Log Software activity + \ No newline at end of file diff --git a/OnionMedia/OnionMedia (Package)/Strings/es/Resources.resw b/OnionMedia/OnionMedia (Package)/Strings/es/Resources.resw index 056ee47..db05043 100644 --- a/OnionMedia/OnionMedia (Package)/Strings/es/Resources.resw +++ b/OnionMedia/OnionMedia (Package)/Strings/es/Resources.resw @@ -120,4 +120,7 @@ OnionMedia es un convertidor y descargador gratuito de archivos de vídeo y audio. Es compatible con un gran número de formatos y códecs y está disponible en GitHub y en la Microsoft Store. + + Registrar la actividad del software + \ No newline at end of file diff --git a/OnionMedia/OnionMedia.Core/Classes/LoggerClass.cs b/OnionMedia/OnionMedia.Core/Classes/LoggerClass.cs deleted file mode 100644 index f80bf79..0000000 --- a/OnionMedia/OnionMedia.Core/Classes/LoggerClass.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.Extensions.Logging; -using OnionMedia.Core.Services; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OnionMedia.Core.Classes -{ - public class LoggerClass - { - private readonly ILogger _logger; - - public LoggerClass(ILogger logger) - { - _logger = logger; - } - } -} - diff --git a/OnionMedia/OnionMedia.Core/Models/AppSettings.cs b/OnionMedia/OnionMedia.Core/Models/AppSettings.cs index 66bcdb8..b31ff1a 100644 --- a/OnionMedia/OnionMedia.Core/Models/AppSettings.cs +++ b/OnionMedia/OnionMedia.Core/Models/AppSettings.cs @@ -46,6 +46,7 @@ private AppSettings() maxThreadCountForConversion = settingsService.GetSetting("maxThreadCountForConversion") as int? ?? 1; ValidateSettingOrSetToDefault(ref maxThreadCountForConversion, val => val is > 0, 1); + useFixedStoragePaths = settingsService.GetSetting("useFixedStoragePaths") as bool? ?? true; convertedAudioSavePath = settingsService.GetSetting("convertedAudioSavePath") as string ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "OnionMedia", "converted".GetLocalized()); convertedVideoSavePath = settingsService.GetSetting("convertedVideoSavePath") as string ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), "OnionMedia", "converted".GetLocalized()); @@ -64,7 +65,11 @@ private AppSettings() showDonationBanner = settingsService.GetSetting("showDonationBanner") as bool? ?? true; selectedTheme = ParseEnum(settingsService.GetSetting("selectedTheme")); appFlowDirection = ParseEnum(settingsService.GetSetting("appFlowDirection")); - + + + useLogging=settingsService.GetSetting("useLogging") as bool? ?? false; + logPath = settingsService.GetSetting("logPath") as string?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "OnionMedia", "Logs"); ; + var downloadsAudioFormat = settingsService.GetSetting("downloadsAudioFormat"); if (downloadsAudioFormat == null) this.downloadsAudioFormat = AudioConversionFormat.Mp3; @@ -93,6 +98,19 @@ private AppSettings() public static AppFlowDirection[] FlowDirections { get; } = Enum.GetValues().ToArray(); //Settings + + public bool UseLogging + { + get => useLogging.HasValue ? (bool)useLogging:false; + set => SetSetting(ref useLogging, value, "useLogging"); + } + private bool? useLogging; + public string LogPath + { + get => logPath; + set => SetSetting(ref logPath, value, "logPath"); + } + public string logPath; public int SimultaneousOperationCount { get => simultaneousOperationCount.HasValue ? (int)simultaneousOperationCount : default; @@ -367,7 +385,8 @@ public enum PathType ConvertedVideofiles, ConvertedAudiofiles, DownloadedVideofiles, - DownloadedAudiofiles + DownloadedAudiofiles, + LogPath } public enum StartPageType diff --git a/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj b/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj index 9cc5bce..31527b1 100644 --- a/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj +++ b/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj @@ -9,8 +9,9 @@ - - + + + diff --git a/OnionMedia/OnionMedia.Core/Services/Logging/CentralLoggerService.cs b/OnionMedia/OnionMedia.Core/Services/Logging/CentralLoggerService.cs deleted file mode 100644 index d1ca676..0000000 --- a/OnionMedia/OnionMedia.Core/Services/Logging/CentralLoggerService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using OnionMedia.Core.Services.Logging.Classes; -using OnionMedia.Core.Services.Logging.Interfaces; - -namespace OnionMedia.Core.Services.Logging -{ - public class CentralLoggerService : ICentralLoggerService - { - private readonly ConcurrentQueue _logQueue = new ConcurrentQueue(); - private readonly ILogger _logger; - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - public CentralLoggerService(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - StartLogging(); - } - - public void StartLogging() - { - Task.Run(() => ProcessLogsAsync(_cancellationTokenSource.Token)); - } - - - public void EnqueueLog(LogType log) - { - if (log == null) throw new ArgumentNullException(nameof(log)); - _logQueue.Enqueue(log); - } - - private async Task ProcessLogsAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - if (_logQueue.TryDequeue(out LogType log)) - { - _logger.Log(log.LogPriority, "{Message} at {Time}", log.LogMessage, log.LogTime); - Debug.WriteLine((log.LogPriority, log.LogMessage, log.LogTime)); - } - else - { - await Task.Delay(100, cancellationToken); - } - } - } - - public void StopLogging() - { - _cancellationTokenSource.Cancel(); - } - } -} diff --git a/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ICentralLoggerService.cs b/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ICentralLoggerService.cs deleted file mode 100644 index a089292..0000000 --- a/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ICentralLoggerService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using OnionMedia.Core.Services.Logging.Classes; -using System.Threading.Tasks; - -namespace OnionMedia.Core.Services.Logging.Interfaces -{ - public interface ICentralLoggerService - { - void EnqueueLog(LogType log); - - void StopLogging(); - } -} diff --git a/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ILogWriterService.cs b/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ILogWriterService.cs deleted file mode 100644 index 776ea47..0000000 --- a/OnionMedia/OnionMedia.Core/Services/Logging/Interfaces/ILogWriterService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace OnionMedia.Core.Services.Logging.Interfaces -{ - public interface ILogWriterService - { - public void Write(string Path); - } -} \ No newline at end of file diff --git a/OnionMedia/OnionMedia.Core/ViewModels/Dialogs/PlaylistSelectorViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/Dialogs/PlaylistSelectorViewModel.cs index ceb232c..2df056e 100644 --- a/OnionMedia/OnionMedia.Core/ViewModels/Dialogs/PlaylistSelectorViewModel.cs +++ b/OnionMedia/OnionMedia.Core/ViewModels/Dialogs/PlaylistSelectorViewModel.cs @@ -10,6 +10,7 @@ */ using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using OnionMedia.Core.Extensions; using System; diff --git a/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs index 96799fa..598aeeb 100644 --- a/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs +++ b/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs @@ -28,25 +28,16 @@ using OnionMedia.Core.Models; using OnionMedia.Core.Services; using Microsoft.Extensions.Logging; -using OnionMedia.Core.Services.Logging.Classes; -using OnionMedia.Core.Services.Logging.Interfaces; using Microsoft.Extensions.DependencyInjection; - namespace OnionMedia.Core.ViewModels { [ObservableObject] public sealed partial class MediaViewModel { - public MediaViewModel(IDialogService dialogService, IDispatcherService dispatcher, IConversionPresetDialog conversionPresetDialog, IFiletagEditorDialog filetagEditorDialog, IToastNotificationService toastNotificationService, ITaskbarProgressService taskbarProgressService, IServiceProvider serviceprovider) + private readonly ILogger logger; + public MediaViewModel(ILogger _logger, IDialogService dialogService, IDispatcherService dispatcher, IConversionPresetDialog conversionPresetDialog, IFiletagEditorDialog filetagEditorDialog, IToastNotificationService toastNotificationService, ITaskbarProgressService taskbarProgressService) { - var loggerService = serviceprovider.GetRequiredService(); - - loggerService.EnqueueLog(new LogType - { - LogMessage = "Dies ist eine Testnachricht", - LogPriority = LogLevel.Warning, - LogTime = DateTime.Now.ToString("HH:mm:ss") - }); + logger = _logger ?? throw new ArgumentNullException(nameof(_logger)); this.dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); this.conversionPresetDialog = conversionPresetDialog ?? throw new ArgumentNullException(nameof(conversionPresetDialog)); this.filetagEditorDialog = filetagEditorDialog ?? throw new ArgumentNullException(nameof(filetagEditorDialog)); @@ -54,6 +45,11 @@ public MediaViewModel(IDialogService dialogService, IDispatcherService dispatche this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); this.taskbarProgressService = taskbarProgressService; + if (Debugger.IsAttached) + { + logger.LogWarning("App runs currently in debug-mode"); + } + if (this.taskbarProgressService != null) PropertyChanged += (o, e) => { @@ -120,14 +116,26 @@ public MediaViewModel(IDialogService dialogService, IDispatcherService dispatche private void Files_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + logger.LogInformation("Files_CollectionChanged in MediaViewModel"); OnPropertyChanged(nameof(FilesAreEmpty)); OnPropertyChanged(nameof(CanExecuteConversion)); } + private T CheckAndLogNull(T parameter, string parameterName) + { + if (parameter == null) + { + logger.LogError($"ArgumentNullException: {parameterName} is null in MediaViewModel constructor"); + throw new ArgumentNullException(parameterName); + } + return parameter; + } + private void InsertConversionPresetsFromJson() { try { + logger.LogInformation("Trying to read json in InsertConversionPresetsFromJson"); //Try to read the .json file that contains the presets. ConversionPresets.AddRange(JsonSerializer.Deserialize>(File.ReadAllText(conversionPresetsPath))); } @@ -135,10 +143,14 @@ private void InsertConversionPresetsFromJson() { try { + logger.LogInformation("ReTrying to read json in InsertConversionPresetsFromJson"); //If the file is missing or corrupted, try to read the supplied file. ConversionPresets.AddRange(JsonSerializer.Deserialize>(File.ReadAllText(Path.Combine(pathProvider.InstallPath, "Data", "ConversionPresets.json")))); } - catch (Exception) { } //Dont crash when the supplied .json file is missing or corrupted too. + catch (Exception) + { + logger.LogInformation("Json is corrupted or Missing in InsertConversionPresetsFromJson"); + } //Dont crash when the supplied .json file is missing or corrupted too. finally { //Finally create a new .json file for the presets. @@ -146,17 +158,25 @@ private void InsertConversionPresetsFromJson() Directory.CreateDirectory(Path.GetDirectoryName(conversionPresetsPath)); using var sr = File.CreateText(conversionPresetsPath); sr.Write(JsonSerializer.Serialize>(ConversionPresets)); + logger.LogInformation("New json got Written in InsertConversionPresetsFromJson "); + } } } private async void UpdateConversionPresets(object sender, NotifyCollectionChangedEventArgs e) { + + logger.LogInformation("UpdateConversionPresets got called"); + if (!File.Exists(conversionPresetsPath)) { if (!Directory.Exists(Path.GetDirectoryName(conversionPresetsPath))) Directory.CreateDirectory(Path.GetDirectoryName(conversionPresetsPath)); File.Create(conversionPresetsPath); + + logger.LogInformation($"Created file {conversionPresetsPath} in UpdateConversionPresets "); + } List presets = new(((IEnumerable)sender).Where(p => p != null)); @@ -277,11 +297,16 @@ private void ReorderConversionPresets() /// The files to add to the list. public async Task AddFilesAsync(IEnumerable filepaths = null) { + string[] result = filepaths?.ToArray(); if (result == null || result.Length == 0) { result = await dialogService.ShowMultipleFilePickerDialogAsync(DirectoryLocation.Videos); - if (result == null || !result.Any()) return; + if (result == null || !result.Any()) + { + logger.LogInformation($"Created file dialogResult war null in AddFilesAsync"); + return; + }; } int failedCount = 0; @@ -304,6 +329,8 @@ public async Task AddFilesAsync(IEnumerable filepaths = null) AddingFiles = false; if (failedCount > 0) { + logger.LogInformation($"failedCount>0 in AddFilesAsync"); + (string title, string content) dlgContent; if (result.Length == 1) dlgContent = ("fileNotSupported".GetLocalized(dialogResources), "fileNotSupportedText".GetLocalized(dialogResources)); @@ -321,24 +348,43 @@ public async Task AddFilesAsync(IEnumerable filepaths = null) private async Task AddConversionPresetAsync() { ConversionPreset newPreset = await conversionPresetDialog.ShowCustomPresetDialogAsync(ConversionPresets.Select(p => p.Name)); - if (newPreset == null) return; + if (newPreset == null) + { + logger.LogInformation($"newPreset was null in AddConversionPresetAsync"); + + return; + } //Add the new preset and sort the presets (exclude the standard preset [0] from sorting) ConversionPresets.Add(newPreset); ReorderConversionPresets(); SelectedConversionPreset = newPreset; + logger.LogInformation($"newPreset added in AddConversionPresetAsync"); + + } private async Task EditConversionPresetAsync(ConversionPreset conversionPreset) { + if (conversionPreset == null) + { throw new ArgumentNullException(nameof(conversionPreset)); + } + if (!ConversionPresets.Contains(conversionPreset)) + { throw new ArgumentException("ConversionPresets does not contain conversionPreset."); + } ConversionPreset editedPreset = await conversionPresetDialog.ShowCustomPresetDialogAsync(conversionPreset, ConversionPresets.Select(p => p.Name)); - if (editedPreset == null) return; + if (editedPreset == null) + { + logger.LogInformation($"editedPreset was null in EditConversionPresetAsync"); + + return; + } //Rename the preset and sort the presets (exclude the standard preset [0] from sorting) ConversionPresets[ConversionPresets.IndexOf(conversionPreset)] = editedPreset; @@ -348,6 +394,7 @@ private async Task EditConversionPresetAsync(ConversionPreset conversionPreset) private async Task DeleteConversionPresetAsync(ConversionPreset conversionPreset) { + if (conversionPreset == null) throw new ArgumentNullException(nameof(conversionPreset)); @@ -359,7 +406,12 @@ private async Task DeleteConversionPresetAsync(ConversionPreset conversionPreset "delete".GetLocalized(deletePresetDialog), "cancel".GetLocalized(deletePresetDialog), null) ?? false; - if (!deletePreset) return; + if (!deletePreset) + { + logger.LogInformation($"deletePreset was false in EditConversionPresetAsync"); + + return; + } ConversionPresets.Remove(conversionPreset); if (ConversionPresets.Count > 1) @@ -370,6 +422,7 @@ private async Task DeleteConversionPresetAsync(ConversionPreset conversionPreset private async Task ConvertFilesAsync() { + if (SelectedConversionPreset == null) throw new Exception("SelectedConversionPreset is null."); @@ -378,7 +431,12 @@ private async Task ConvertFilesAsync() { //TODO: Check if this path is writable. path = await dialogService.ShowFolderPickerDialogAsync(DirectoryLocation.Videos); - if (path == null) return; + if (path == null) + { + logger.LogInformation($"Path was null in ConvertFilesAsync"); + + return; + } } allCanceled = false; @@ -416,6 +474,8 @@ private async Task ConvertFilesAsync() string outputPath = Path.ChangeExtension(Path.Combine(targetDir, file.MediaFile.FileInfo.Name), file.UseCustomOptions ? file.CustomOptions.Format.Name : SelectedConversionPreset.Format.Name); file.Complete += (o, e) => { + logger.LogInformation($"File Complete in ConvertFilesAsync"); + completed++; lastCompleted = (MediaItemModel)o; if (File.Exists(e?.Output?.Name)) @@ -429,19 +489,27 @@ private async Task ConvertFilesAsync() switch (t.Exception?.InnerException) { default: + logger.LogError($"Exception occured while saving the file. in ConvertFilesAsync"); + Debug.WriteLine("Exception occured while saving the file."); break; case UnauthorizedAccessException: unauthorizedAccessExceptions++; + logger.LogError($"UnauthorizedAccessException {unauthorizedAccessExceptions}. in ConvertFilesAsync"); + break; case DirectoryNotFoundException: directoryNotFoundExceptions++; + logger.LogError($"DirectoriyNotFoundExceptions {directoryNotFoundExceptions}. in ConvertFilesAsync"); + break; case NotEnoughSpaceException: notEnoughSpaceExceptions++; + logger.LogError($"NotEnoughtSpaceExceptions {notEnoughSpaceExceptions}. in ConvertFilesAsync"); + break; } })); @@ -452,6 +520,8 @@ private async Task ConvertFilesAsync() if (AppSettings.Instance.ClearListsAfterOperation) files.ForEach(f => Files.Remove(f), f => Files.Contains(f) && f.ConversionState is FFmpegConversionState.Done); + logger.LogInformation($"ConversionDone. in ConvertFilesAsync"); + Debug.WriteLine("Conversion done"); try @@ -459,48 +529,60 @@ private async Task ConvertFilesAsync() foreach (var dir in Directory.GetDirectories(pathProvider.ConverterTempdir)) { try { Directory.Delete(dir, true); } - catch { /* Dont crash if a directory cant be deleted */ } + catch + { + + logger.LogWarning($"Directorie cant be deleted. in ConvertFilesAsync"); + + /* Dont crash if a directory cant be deleted */ + } } } - catch {Console.WriteLine("Failed to get temporary conversion folders.");} + catch + { + logger.LogWarning($"Failed to get temporary conversion folders. in ConvertFilesAsync"); + + Console.WriteLine("Failed to get temporary conversion folders."); + } try { - if (unauthorizedAccessExceptions + directoryNotFoundExceptions + notEnoughSpaceExceptions > 0) - { - taskbarProgressService?.UpdateState(typeof(MediaViewModel), ProgressBarState.Error); - await GlobalResources.DisplayFileSaveErrorDialog(unauthorizedAccessExceptions, - directoryNotFoundExceptions, notEnoughSpaceExceptions); - } - - taskbarProgressService?.UpdateState(typeof(MediaViewModel), ProgressBarState.None); - if (!AppSettings.Instance.SendMessageAfterConversion) - { - return; - } - - if (completed == 1) - { - await lastCompleted.ShowToastAsync(lastCompletedPath); - } - else if (completed > 1) - { - toastNotificationService.SendConversionsDoneNotification(completed); - } + if (unauthorizedAccessExceptions + directoryNotFoundExceptions + notEnoughSpaceExceptions > 0) + { + taskbarProgressService?.UpdateState(typeof(MediaViewModel), ProgressBarState.Error); + await GlobalResources.DisplayFileSaveErrorDialog(unauthorizedAccessExceptions, + directoryNotFoundExceptions, notEnoughSpaceExceptions); + } + + taskbarProgressService?.UpdateState(typeof(MediaViewModel), ProgressBarState.None); + if (!AppSettings.Instance.SendMessageAfterConversion) + { + return; + } + + if (completed == 1) + { + await lastCompleted.ShowToastAsync(lastCompletedPath); + } + else if (completed > 1) + { + toastNotificationService.SendConversionsDoneNotification(completed); + } } finally { - if (!(allCanceled || files.All(f => f.ConversionState == FFmpegConversionState.Cancelled))) - { - bool errors = files.Any(f => Files.Contains(f) && f.ConversionState is FFmpegConversionState.Failed); - ConversionDone?.Invoke(this, errors); - } + if (!(allCanceled || files.All(f => f.ConversionState == FFmpegConversionState.Cancelled))) + { + bool errors = files.Any(f => Files.Contains(f) && f.ConversionState is FFmpegConversionState.Failed); + ConversionDone?.Invoke(this, errors); + } } } [ICommand] public void SetResolution(Resolution res) { + if (SelectedItem is null) return; SelectedItem.Width = res.Width; SelectedItem.Height = res.Height; @@ -509,17 +591,25 @@ public void SetResolution(Resolution res) private async Task EditTagsAsync() { + TagsEditedFlyoutIsOpen = false; if (SelectedItem == null || !SelectedItem.FileTagsAvailable) return; FileTags newTags = await filetagEditorDialog.ShowTagEditorDialogAsync(SelectedItem.FileTags); - if (newTags == null) return; + if (newTags == null) + { + logger.LogInformation($"newTags was null. in EditTagsAsync"); + + return; + } try { bool result = SelectedItem.ApplyNewTags(newTags); if (!result) { await dialogService.ShowInfoDialogAsync("error".GetLocalized(resources), "tagerrormsg".GetLocalized(resources), "OK"); + logger.LogError($"error occured by ApplyNewTags. in EditTagsAsync"); + return; } @@ -530,11 +620,15 @@ private async Task EditTagsAsync() } catch (FileNotFoundException) { + logger.LogError($"error occured file not found. in EditTagsAsync"); + await dialogService.ShowInfoDialogAsync("fileNotFound".GetLocalized(resources), "fileNotFoundText".GetLocalized(resources), "OK"); Files.Remove(SelectedItem); } catch { + logger.LogError($"error occured file not found. in EditTagsAsync"); + await dialogService.ShowInfoDialogAsync("error".GetLocalized(resources), "changesErrorMsg".GetLocalized(resources), "OK"); } } @@ -543,6 +637,7 @@ private async Task EditTagsAsync() [ICommand] private void CancelAll() { + Files.Where(f => f.ConversionState is FFmpegConversionState.None or FFmpegConversionState.Converting or FFmpegConversionState.Moving) .OrderBy(f => f.ConversionState) .ForEach(f => f.RaiseCancel()); diff --git a/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs index 711e59e..7d77db2 100644 --- a/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs +++ b/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs @@ -19,14 +19,18 @@ using OnionMedia.Core.Models; using OnionMedia.Core.Services; using OnionMedia.Core.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace OnionMedia.Core.ViewModels { [ObservableObject] public sealed partial class SettingsViewModel { - public SettingsViewModel(IUrlService urlService, IDialogService dialogService, IThirdPartyLicenseDialog thirdPartyLicenseDialog, IPathProvider pathProvider, IVersionService versionService) + public ILogger logger; + public SettingsViewModel(ILogger _logger,IUrlService urlService, IDialogService dialogService, IThirdPartyLicenseDialog thirdPartyLicenseDialog, IPathProvider pathProvider, IVersionService versionService) { + logger = _logger ?? throw new ArgumentNullException(nameof(_logger)); this.dialogService = dialogService; this.urlService = urlService; this.thirdPartyLicenseDialog = thirdPartyLicenseDialog; @@ -68,6 +72,9 @@ private async Task ChangePathAsync(PathType pathType) case PathType.DownloadedAudiofiles: AppSettings.Instance.DownloadsAudioSavePath = path; break; + case PathType.LogPath: + AppSettings.Instance.LogPath = path; + break; } } diff --git a/OnionMedia/OnionMedia.Core/ViewModels/YouTubeDownloaderViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/YouTubeDownloaderViewModel.cs index 35fbd74..3880a71 100644 --- a/OnionMedia/OnionMedia.Core/ViewModels/YouTubeDownloaderViewModel.cs +++ b/OnionMedia/OnionMedia.Core/ViewModels/YouTubeDownloaderViewModel.cs @@ -34,14 +34,18 @@ using System.Net; using System.Drawing; using System.Drawing.Imaging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace OnionMedia.Core.ViewModels { [ObservableObject] public sealed partial class YouTubeDownloaderViewModel { - public YouTubeDownloaderViewModel(IDialogService dialogService, IDownloaderDialogService downloaderDialogService, IDispatcherService dispatcher, INetworkStatusService networkStatusService, IToastNotificationService toastNotificationService, IPathProvider pathProvider, ITaskbarProgressService taskbarProgressService, IWindowClosingService windowClosingService, IFiletagEditorDialog filetagDialogService) - { + public ILogger logger; + public YouTubeDownloaderViewModel(ILogger _logger, IDialogService dialogService, IDownloaderDialogService downloaderDialogService, IDispatcherService dispatcher, INetworkStatusService networkStatusService, IToastNotificationService toastNotificationService, IPathProvider pathProvider, ITaskbarProgressService taskbarProgressService, IWindowClosingService windowClosingService, IFiletagEditorDialog filetagDialogService) + { + logger = _logger ?? throw new ArgumentNullException(nameof(_logger)); this.dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); this.downloaderDialogService = downloaderDialogService ?? throw new ArgumentNullException(nameof(downloaderDialogService)); this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); @@ -71,6 +75,7 @@ public YouTubeDownloaderViewModel(IDialogService dialogService, IDownloaderDialo networkAvailable = this.networkStatusService?.IsNetworkConnectionAvailable() ?? true; if (this.networkStatusService != null) this.networkStatusService.ConnectionStateChanged += (o, e) => this.dispatcher.Enqueue(() => NetworkAvailable = e); + } private readonly IDialogService dialogService; @@ -132,6 +137,7 @@ public StreamItemModel SelectedVideo if (selectedVideo != value && value != null) { selectedVideo = value; + logger.LogInformation($"Selected Video changed {value.Video.Url}"); OnPropertyChanged(); } } @@ -158,6 +164,7 @@ public string SelectedQuality sb.Append(selectedQuality); previouslySelected = sb.ToString(); selectedQuality = value; + logger.LogInformation($"Selected quality changed to {value}"); sb.Clear(); } @@ -170,6 +177,8 @@ private async Task FillInfosAsync(string videolink, bool allowPlaylists = true) { if (VideoNotFound) { + //WHAT the Hell?? + //Was solln dass? VideoNotFound = false; VideoNotFound = true; return; @@ -183,7 +192,10 @@ private async Task FillInfosAsync(string videolink, bool allowPlaylists = true) SearchResults.Clear(); if (string.IsNullOrWhiteSpace(videolink)) + { + logger.LogWarning("Video link was Empty"); return; + } bool validUri = Regex.IsMatch(videolink, GlobalResources.URLREGEX); bool isYoutubePlaylist = validUri && allowPlaylists && IsYoutubePlaylist(urlClone); @@ -197,6 +209,7 @@ private async Task FillInfosAsync(string videolink, bool allowPlaylists = true) if (!validUri) { await RefreshResultsAsync(videolink.Clone() as string); + logger.LogWarning("InvalidUri in YouTubeDownloaderViewModel"); return; } @@ -269,6 +282,7 @@ private async Task FillInfosAsync(string videolink, bool allowPlaylists = true) } catch (Exception ex) { + logger.LogError($"{ex} in YouTubeDownloaderViewModel by FillInfosAsync"); ScanVideoCount--; switch (ex) { @@ -420,8 +434,9 @@ private void AddVideos(IEnumerable videos) OnPropertyChanged(nameof(QueueIsNotEmpty)); OnPropertyChanged(nameof(MultipleVideos)); } - catch (InvalidOperationException) + catch (InvalidOperationException) { + logger.LogError("InvalidOperation triggered in YouTubeDownloaderViewModel by AddVideos"); Debug.WriteLine("InvalidOperation triggered"); } finally @@ -450,7 +465,11 @@ private async Task> GetVideosAsync(IEnumerable queue.Release())); } @@ -513,7 +532,7 @@ private async Task RemoveVideoAsync() else SelectedQuality = null; } - catch (InvalidOperationException) { Debug.WriteLine("InvalidOperation triggered"); } + catch (InvalidOperationException) { Debug.WriteLine("InvalidOperation triggered"); logger.LogError("InvalidOperatipnException in YouTubeDownloaderViewModel by RemoveVideoAsync"); } await Task.CompletedTask; } @@ -567,6 +586,7 @@ private async Task DownloadVideosAsync(IList videos, bool getMp switch (t.Exception?.InnerException) { default: + logger.LogInformation("Exception occured while saving file in DonwloadVideoAsync"); Debug.WriteLine("Exception occured while saving the file."); break; @@ -595,7 +615,7 @@ private async Task DownloadVideosAsync(IList videos, bool getMp foreach (var dir in Directory.GetDirectories(pathProvider.DownloaderTempdir)) { try { Directory.Delete(dir, true); } - catch { /* Dont crash if a directory cant be deleted */ } + catch { logger.LogInformation("Directory cant be deleted in DownloadVideosAsync");/* Dont crash if a directory cant be deleted */ } } try @@ -640,7 +660,16 @@ await GlobalResources.DisplayFileSaveErrorDialog(unauthorizedAccessExceptions, if (!(canceledAll || items.All(v => v.DownloadState == DownloadState.IsCancelled))) { bool errors = items.Any(v => videos.Contains(v) && v.Failed); - DownloadDone?.Invoke(this, errors); + logger.LogInformation(errors ? "Errors occured while download":"No errors occured while download"); + if (errors) + { + foreach(var i in items) + { + logger.LogError($"download failed by: {i.Video.Url}"); + } + } + + DownloadDone?.Invoke(this, errors); } } } @@ -667,7 +696,7 @@ private async Task RefreshResultsAsync(string searchTerm) { Debug.WriteLine("No internet connection!"); } - catch (TaskCanceledException) { } + catch (TaskCanceledException) { logger.LogError("TaskCancellationException in RefreshResultsAsync by YoutubeDownloaderViewModel"); } finally { searchProcesses--; } } private int searchProcesses = 0; @@ -682,6 +711,7 @@ private void ClearResults() if (!SearchResults.Any()) return; SearchResults.Clear(); lastSearch = (string.Empty, new Collection()); + logger.LogInformation("ResultsCleared"); } [ICommand] @@ -689,6 +719,7 @@ private void CancelAll() { Videos.ForEach(v => v?.RaiseCancel()); CanceledAll?.Invoke(this, EventArgs.Empty); + logger.LogInformation("CancelALL"); } public event EventHandler CanceledAll; @@ -697,6 +728,7 @@ private void RemoveAll() { CancelAll(); Videos.Clear(); + logger.LogInformation("RemoveALL"); } public int DownloadProgress diff --git a/OnionMedia/OnionMedia/App.xaml.cs b/OnionMedia/OnionMedia/App.xaml.cs index e750059..edc59bd 100644 --- a/OnionMedia/OnionMedia/App.xaml.cs +++ b/OnionMedia/OnionMedia/App.xaml.cs @@ -42,8 +42,6 @@ using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Windows.Services.Maps; -using OnionMedia.Core.Services.Logging.Classes; -using OnionMedia.Core.Services.Logging.Interfaces; // To learn more about WinUI3, see: https://docs.microsoft.com/windows/apps/winui/winui3/. namespace OnionMedia diff --git a/OnionMedia/OnionMedia/OnionMedia.csproj b/OnionMedia/OnionMedia/OnionMedia.csproj index 30573b9..787c413 100644 --- a/OnionMedia/OnionMedia/OnionMedia.csproj +++ b/OnionMedia/OnionMedia/OnionMedia.csproj @@ -111,14 +111,16 @@ - - - + + + - + + + diff --git a/OnionMedia/OnionMedia/ServiceProvider.cs b/OnionMedia/OnionMedia/ServiceProvider.cs index d3a41e2..0b8e07e 100644 --- a/OnionMedia/OnionMedia/ServiceProvider.cs +++ b/OnionMedia/OnionMedia/ServiceProvider.cs @@ -10,10 +10,12 @@ using OnionMedia.ViewModels; using OnionMedia.Views; using Microsoft.Extensions.Logging; -using OnionMedia.Core.Services.Logging; -using OnionMedia.Core.Services.Logging.Interfaces; using System; using Microsoft.Extensions.DependencyInjection; +using Serilog; +using System.IO; +using Windows.Storage; +using System.Threading; namespace OnionMedia; @@ -22,9 +24,10 @@ namespace OnionMedia; //Logging [Singleton(typeof(ILoggerFactory), Factory = nameof(CreateLoggerFactory))] -[Singleton(typeof(ILogger<>))] -[Singleton(typeof(ILogger), Factory = nameof(CreateLogger))] -[Singleton(typeof(ICentralLoggerService), typeof(CentralLoggerService))] +[Singleton(typeof(Serilog.ILogger), Factory = nameof(CreateSerilogLogger))] +[Transient(typeof(ILogger<>), Factory = nameof(CreateLogger))] + + [Singleton(typeof(IThemeSelectorService), typeof(ThemeSelectorService))] @@ -68,20 +71,54 @@ namespace OnionMedia; [Transient(typeof(PlaylistsPage))] sealed partial class ServiceProvider { + private static string logfile = Path.Combine(AppSettings.Instance.LogPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); - private static ILogger CreateLogger(IServiceProvider serviceProvider) + private static Serilog.ILogger CreateSerilogLogger() + { + if (!Directory.Exists("logs")) + { + Directory.CreateDirectory("logs"); + } + CheckAge(); + if (AppSettings.Instance.UseLogging) + { + return new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.File(logfile) + .CreateLogger(); + } + else + { + return new LoggerConfiguration() + .MinimumLevel.Fatal() // There are no fatal log-messages in code so nothing will be loggt. + .CreateLogger(); + } + } + private static ILogger CreateLogger(IServiceProvider serviceProvider) { - var factory = serviceProvider.GetRequiredService(); - return factory.CreateLogger(); + var factory = serviceProvider.GetService(); + return factory.CreateLogger(); } private static ILoggerFactory CreateLoggerFactory() { + var logger = CreateSerilogLogger(); return LoggerFactory.Create(builder => { - builder.AddConsole(); // Konfiguration für Logging, z. B. Console-Logging. - builder.SetMinimumLevel(LogLevel.Information); + builder.AddSerilog(logger); }); } + private static void CheckAge() + { + var files = Directory.EnumerateFiles(AppSettings.Instance.LogPath, "*"); + foreach (var file in files) + { + DateTime lastModified = File.GetLastWriteTime(file); + if ((DateTime.Now - lastModified).TotalDays > 7) + { + File.Delete(file); + } + } + } -} \ No newline at end of file +} diff --git a/OnionMedia/OnionMedia/Services/SettingsService.cs b/OnionMedia/OnionMedia/Services/SettingsService.cs index 63fbbb2..b9dd99a 100644 --- a/OnionMedia/OnionMedia/Services/SettingsService.cs +++ b/OnionMedia/OnionMedia/Services/SettingsService.cs @@ -15,6 +15,8 @@ namespace OnionMedia.Services { + //Why does a programmer code in java? Because he can´t see sharp. rofl + sealed class SettingsService : ISettingsService { public object? GetSetting(string key) diff --git a/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw index 50b8d9b..49285e0 100644 --- a/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw @@ -198,6 +198,9 @@ Downloadgeschwindigkeit limitieren + + Protokoll: + Max. Downloadgeschwindigkeit pro Datei: @@ -234,6 +237,9 @@ Hardwareunterstützte Konvertierung verwenden + + Softwareaktivität Protokollieren + Video oder komplette Playlist bei der Verwendung einer Playlist-URL hinzufügen? diff --git a/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw index c6c0218..1a46233 100644 --- a/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw @@ -198,6 +198,9 @@ Limit download speed + + Log: + Max. download speed per file: @@ -234,6 +237,9 @@ Use hardware accelerated conversion + + Log Software activity + Add video or the playlist with a Playlist-URL? diff --git a/OnionMedia/OnionMedia/Strings/es/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/es/SettingsPage.resw index 370e03e..a768a71 100644 --- a/OnionMedia/OnionMedia/Strings/es/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/es/SettingsPage.resw @@ -198,6 +198,9 @@ Limitar la velocidad de descarga + + Protocolo: + Velocidad máxima de descarga por archivo: @@ -234,6 +237,9 @@ Utilizar la conversión acelerada por hardware + + Registrar la actividad del software + ¿Añadir un vídeo o la lista de reproducción con una Playlist-URL? diff --git a/OnionMedia/OnionMedia/Strings/nl/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/nl/SettingsPage.resw index 65bcf06..6b97c84 100644 --- a/OnionMedia/OnionMedia/Strings/nl/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/nl/SettingsPage.resw @@ -198,6 +198,9 @@ Limiteer de downloadsnelheid + + Log: + Max. downloadsnelheid per bestand: @@ -234,7 +237,10 @@ Gebruik hardwareversnelde conversie + + Software-activiteit loggen + Video of de afspeellijst toevoegen met een afspeellijst-URL? - + \ No newline at end of file diff --git a/OnionMedia/OnionMedia/Views/SettingsPage.xaml b/OnionMedia/OnionMedia/Views/SettingsPage.xaml index 31b869f..ced7c28 100644 --- a/OnionMedia/OnionMedia/Views/SettingsPage.xaml +++ b/OnionMedia/OnionMedia/Views/SettingsPage.xaml @@ -135,6 +135,16 @@ + + + + + + + + From 5f79c4030007da9a7b3aae6d07ebafcd817cbc7f Mon Sep 17 00:00:00 2001 From: lorenz | onionware Date: Wed, 29 Jan 2025 19:23:50 +0100 Subject: [PATCH 3/9] addition --- OnionMedia/OnionMedia/ServiceProvider.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/OnionMedia/OnionMedia/ServiceProvider.cs b/OnionMedia/OnionMedia/ServiceProvider.cs index 0b8e07e..2acfab5 100644 --- a/OnionMedia/OnionMedia/ServiceProvider.cs +++ b/OnionMedia/OnionMedia/ServiceProvider.cs @@ -75,10 +75,9 @@ sealed partial class ServiceProvider private static Serilog.ILogger CreateSerilogLogger() { - if (!Directory.Exists("logs")) - { - Directory.CreateDirectory("logs"); - } + + Directory.CreateDirectory(AppSettings.Instance.LogPath); + CheckAge(); if (AppSettings.Instance.UseLogging) { From 0e33ff5110245c4d8ee950898edb14d45d9bc226 Mon Sep 17 00:00:00 2001 From: Jaden <91136545+onionware-github@users.noreply.github.com> Date: Wed, 29 Jan 2025 19:53:32 +0100 Subject: [PATCH 4/9] Delete OnionMedia/OnionMedia/Services/LogWriterService.cs --- .../OnionMedia/Services/LogWriterService.cs | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 OnionMedia/OnionMedia/Services/LogWriterService.cs diff --git a/OnionMedia/OnionMedia/Services/LogWriterService.cs b/OnionMedia/OnionMedia/Services/LogWriterService.cs deleted file mode 100644 index 9ef984c..0000000 --- a/OnionMedia/OnionMedia/Services/LogWriterService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using OnionMedia.Core.Services.Logging.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OnionMedia.Services -{ - public class LogWriterService : ILogWriterService - { - public void Write(string Path) - { - throw new NotImplementedException(); - } - } -} From 1b8584ab4de47aeaa9e5224523ff7f3648ddf5e1 Mon Sep 17 00:00:00 2001 From: lorenz | onionware Date: Fri, 31 Jan 2025 19:36:13 +0100 Subject: [PATCH 5/9] Added Features: -Log path moved to documents -custom days for deleting logs -open path right from the Settings --- .../OnionMedia.Core/Models/AppSettings.cs | 9 +++- .../Services/Logging/Classes/LogType.cs | 16 ------ .../ViewModels/MediaViewModel.cs | 14 +++++- .../ViewModels/SettingsViewModel.cs | 50 ++++++++++++++++++- OnionMedia/OnionMedia/ServiceProvider.cs | 4 +- .../OnionMedia/Services/LogWriterService.cs | 17 ------- .../Strings/de-de/SettingsPage.resw | 3 ++ .../Strings/en-us/SettingsPage.resw | 3 ++ .../OnionMedia/Strings/es/SettingsPage.resw | 3 ++ .../OnionMedia/Strings/nl/SettingsPage.resw | 3 ++ OnionMedia/OnionMedia/Views/SettingsPage.xaml | 27 ++++++++++ 11 files changed, 111 insertions(+), 38 deletions(-) delete mode 100644 OnionMedia/OnionMedia.Core/Services/Logging/Classes/LogType.cs delete mode 100644 OnionMedia/OnionMedia/Services/LogWriterService.cs diff --git a/OnionMedia/OnionMedia.Core/Models/AppSettings.cs b/OnionMedia/OnionMedia.Core/Models/AppSettings.cs index b31ff1a..3d30bf9 100644 --- a/OnionMedia/OnionMedia.Core/Models/AppSettings.cs +++ b/OnionMedia/OnionMedia.Core/Models/AppSettings.cs @@ -68,7 +68,8 @@ private AppSettings() useLogging=settingsService.GetSetting("useLogging") as bool? ?? false; - logPath = settingsService.GetSetting("logPath") as string?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "OnionMedia", "Logs"); ; + logPath = settingsService.GetSetting("logPath") as string?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "OnionMedia", "Logs"); ; + deleteLogInterval = settingsService.GetSetting("deleteLogInterval") as byte? ?? 7; var downloadsAudioFormat = settingsService.GetSetting("downloadsAudioFormat"); if (downloadsAudioFormat == null) @@ -99,6 +100,12 @@ private AppSettings() //Settings + public byte DeleteLogInterval + { + get => deleteLogInterval; + set => SetSetting(ref deleteLogInterval, value,"deleteLogInterval"); + } + private byte deleteLogInterval; public bool UseLogging { get => useLogging.HasValue ? (bool)useLogging:false; diff --git a/OnionMedia/OnionMedia.Core/Services/Logging/Classes/LogType.cs b/OnionMedia/OnionMedia.Core/Services/Logging/Classes/LogType.cs deleted file mode 100644 index 90f10b0..0000000 --- a/OnionMedia/OnionMedia.Core/Services/Logging/Classes/LogType.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OnionMedia.Core.Services.Logging.Classes -{ - public class LogType - { - public string LogMessage { get; set; } - public string LogTime { get; set; } - public LogLevel LogPriority { get;set; } - } -} diff --git a/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs index 598aeeb..a5c8d8a 100644 --- a/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs +++ b/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs @@ -35,7 +35,7 @@ namespace OnionMedia.Core.ViewModels public sealed partial class MediaViewModel { private readonly ILogger logger; - public MediaViewModel(ILogger _logger, IDialogService dialogService, IDispatcherService dispatcher, IConversionPresetDialog conversionPresetDialog, IFiletagEditorDialog filetagEditorDialog, IToastNotificationService toastNotificationService, ITaskbarProgressService taskbarProgressService) + public MediaViewModel(IVersionService versionService, ILogger _logger, IDialogService dialogService, IDispatcherService dispatcher, IConversionPresetDialog conversionPresetDialog, IFiletagEditorDialog filetagEditorDialog, IToastNotificationService toastNotificationService, ITaskbarProgressService taskbarProgressService) { logger = _logger ?? throw new ArgumentNullException(nameof(_logger)); this.dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); @@ -44,6 +44,9 @@ public MediaViewModel(ILogger _logger, IDialogService dialogServ this.toastNotificationService = toastNotificationService ?? throw new ArgumentNullException(nameof(toastNotificationService)); this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); this.taskbarProgressService = taskbarProgressService; + this.versionService = versionService; + logger.LogDebug(GetVersionDescription()); + if (Debugger.IsAttached) { @@ -105,6 +108,7 @@ public MediaViewModel(ILogger _logger, IDialogService dialogServ }); } + private readonly IVersionService versionService; private readonly IDialogService dialogService; private readonly IConversionPresetDialog conversionPresetDialog; private readonly IFiletagEditorDialog filetagEditorDialog; @@ -114,6 +118,14 @@ public MediaViewModel(ILogger _logger, IDialogService dialogServ private static readonly IPathProvider pathProvider = IoC.Default.GetService() ?? throw new ArgumentNullException(); + + private string GetVersionDescription() + { + var appName = "OnionMedia"; + var version = versionService.GetCurrentVersion(); + + return $"{appName} - {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + } private void Files_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { logger.LogInformation("Files_CollectionChanged in MediaViewModel"); diff --git a/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs index 7d77db2..c787ded 100644 --- a/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs +++ b/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs @@ -21,6 +21,8 @@ using OnionMedia.Core.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Runtime.InteropServices; namespace OnionMedia.Core.ViewModels { @@ -28,7 +30,7 @@ namespace OnionMedia.Core.ViewModels public sealed partial class SettingsViewModel { public ILogger logger; - public SettingsViewModel(ILogger _logger,IUrlService urlService, IDialogService dialogService, IThirdPartyLicenseDialog thirdPartyLicenseDialog, IPathProvider pathProvider, IVersionService versionService) + public SettingsViewModel(ILogger _logger, IUrlService urlService, IDialogService dialogService, IThirdPartyLicenseDialog thirdPartyLicenseDialog, IPathProvider pathProvider, IVersionService versionService) { logger = _logger ?? throw new ArgumentNullException(nameof(_logger)); this.dialogService = dialogService; @@ -78,6 +80,32 @@ private async Task ChangePathAsync(PathType pathType) } } + [ICommand] + private async Task OpenPathAsync(PathType pathType) + { + switch (pathType) + { + case PathType.ConvertedVideofiles: + OpenPath(AppSettings.Instance.ConvertedVideoSavePath); + break; + + case PathType.ConvertedAudiofiles: + OpenPath(AppSettings.Instance.ConvertedAudioSavePath); + break; + + case PathType.DownloadedVideofiles: + OpenPath(AppSettings.Instance.DownloadsVideoSavePath); + break; + + case PathType.DownloadedAudiofiles: + OpenPath(AppSettings.Instance.DownloadsAudioSavePath); + break; + case PathType.LogPath: + OpenPath(AppSettings.Instance.LogPath); + break; + } + } + [ICommand] private async Task ShowLicenseAsync() { @@ -110,5 +138,25 @@ private string GetVersionDescription() return $"{appName} - {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; } + + static void OpenPath(string path) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Process.Start(new ProcessStartInfo("explorer", path) { UseShellExecute = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", path); // xdg is mostly pre-installed. if its not u can just install it with apt, pacman or dnf + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", path); + } + else + { + Debug.WriteLine("Nicht unterstütztes Betriebssystem."); + } + } } } diff --git a/OnionMedia/OnionMedia/ServiceProvider.cs b/OnionMedia/OnionMedia/ServiceProvider.cs index 2acfab5..d24794f 100644 --- a/OnionMedia/OnionMedia/ServiceProvider.cs +++ b/OnionMedia/OnionMedia/ServiceProvider.cs @@ -82,7 +82,7 @@ private static Serilog.ILogger CreateSerilogLogger() if (AppSettings.Instance.UseLogging) { return new LoggerConfiguration() - .MinimumLevel.Information() + .MinimumLevel.Debug() .WriteTo.File(logfile) .CreateLogger(); } @@ -113,7 +113,7 @@ private static void CheckAge() foreach (var file in files) { DateTime lastModified = File.GetLastWriteTime(file); - if ((DateTime.Now - lastModified).TotalDays > 7) + if ((DateTime.Now - lastModified).TotalDays >= AppSettings.Instance.DeleteLogInterval) { File.Delete(file); } diff --git a/OnionMedia/OnionMedia/Services/LogWriterService.cs b/OnionMedia/OnionMedia/Services/LogWriterService.cs deleted file mode 100644 index 9ef984c..0000000 --- a/OnionMedia/OnionMedia/Services/LogWriterService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using OnionMedia.Core.Services.Logging.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OnionMedia.Services -{ - public class LogWriterService : ILogWriterService - { - public void Write(string Path) - { - throw new NotImplementedException(); - } - } -} diff --git a/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw index 49285e0..c81b918 100644 --- a/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw @@ -162,6 +162,9 @@ Media Konverter + + Protokolldatei löschen nach Tagen: + ♥ Spenden ♥ diff --git a/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw index 1a46233..8406087 100644 --- a/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw @@ -162,6 +162,9 @@ Media Converter + + Delete log file after days: + ♥ Donate ♥ diff --git a/OnionMedia/OnionMedia/Strings/es/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/es/SettingsPage.resw index a768a71..af923b8 100644 --- a/OnionMedia/OnionMedia/Strings/es/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/es/SettingsPage.resw @@ -162,6 +162,9 @@ Convertidor + + Eliminar el archivo de registro después de días: + ♥ Donar ♥ diff --git a/OnionMedia/OnionMedia/Strings/nl/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/nl/SettingsPage.resw index 6b97c84..52dab97 100644 --- a/OnionMedia/OnionMedia/Strings/nl/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/nl/SettingsPage.resw @@ -162,6 +162,9 @@ Media Converteerder + + Logbestand na dagen verwijderen: + ♥ Doneer ♥ diff --git a/OnionMedia/OnionMedia/Views/SettingsPage.xaml b/OnionMedia/OnionMedia/Views/SettingsPage.xaml index ced7c28..5188ac9 100644 --- a/OnionMedia/OnionMedia/Views/SettingsPage.xaml +++ b/OnionMedia/OnionMedia/Views/SettingsPage.xaml @@ -96,6 +96,11 @@ + + + From c5358f1e8410559c6aba5b602c1c45667a6332c4 Mon Sep 17 00:00:00 2001 From: lorenz | onionware <77418440+LorenzOnionware@users.noreply.github.com> Date: Mon, 10 Feb 2025 18:52:25 +0100 Subject: [PATCH 6/9] Update OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw Co-authored-by: Jaden <91136545+onionware-github@users.noreply.github.com> --- OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw index c81b918..424abfc 100644 --- a/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/de-de/SettingsPage.resw @@ -241,7 +241,7 @@ Hardwareunterstützte Konvertierung verwenden - Softwareaktivität Protokollieren + Softwareaktivität protokollieren Video oder komplette Playlist bei der Verwendung einer Playlist-URL hinzufügen? From 4fab97611943d81cb1dac3e9d66ebdfecab86475 Mon Sep 17 00:00:00 2001 From: lorenz | onionware <77418440+LorenzOnionware@users.noreply.github.com> Date: Mon, 10 Feb 2025 18:52:37 +0100 Subject: [PATCH 7/9] Update OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw Co-authored-by: Jaden <91136545+onionware-github@users.noreply.github.com> --- OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw b/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw index 8406087..1940bf1 100644 --- a/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw +++ b/OnionMedia/OnionMedia/Strings/en-us/SettingsPage.resw @@ -241,7 +241,7 @@ Use hardware accelerated conversion - Log Software activity + Log software activity Add video or the playlist with a Playlist-URL? From 87c95792d069379b7d32b760fe9af80fb2326b4c Mon Sep 17 00:00:00 2001 From: lorenz | onionware Date: Mon, 10 Feb 2025 18:55:54 +0100 Subject: [PATCH 8/9] Some small changes. Commit the hopefully final logging version within the next days. --- .../ViewModels/MediaViewModel.cs | 19 +----------- OnionMedia/OnionMedia/App.xaml.cs | 3 +- OnionMedia/OnionMedia/ServiceProvider.cs | 5 +-- OnionMedia/OnionMedia/Views/ShellPage.xaml.cs | 31 ++++++++++++++++--- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs index a5c8d8a..f4b98ef 100644 --- a/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs +++ b/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs @@ -35,7 +35,7 @@ namespace OnionMedia.Core.ViewModels public sealed partial class MediaViewModel { private readonly ILogger logger; - public MediaViewModel(IVersionService versionService, ILogger _logger, IDialogService dialogService, IDispatcherService dispatcher, IConversionPresetDialog conversionPresetDialog, IFiletagEditorDialog filetagEditorDialog, IToastNotificationService toastNotificationService, ITaskbarProgressService taskbarProgressService) + public MediaViewModel(ILogger _logger, IDialogService dialogService, IDispatcherService dispatcher, IConversionPresetDialog conversionPresetDialog, IFiletagEditorDialog filetagEditorDialog, IToastNotificationService toastNotificationService, ITaskbarProgressService taskbarProgressService) { logger = _logger ?? throw new ArgumentNullException(nameof(_logger)); this.dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); @@ -44,14 +44,6 @@ public MediaViewModel(IVersionService versionService, ILogger _l this.toastNotificationService = toastNotificationService ?? throw new ArgumentNullException(nameof(toastNotificationService)); this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); this.taskbarProgressService = taskbarProgressService; - this.versionService = versionService; - logger.LogDebug(GetVersionDescription()); - - - if (Debugger.IsAttached) - { - logger.LogWarning("App runs currently in debug-mode"); - } if (this.taskbarProgressService != null) PropertyChanged += (o, e) => @@ -107,8 +99,6 @@ public MediaViewModel(IVersionService versionService, ILogger _l OnPropertyChanged(nameof(SelectedItem)); }); } - - private readonly IVersionService versionService; private readonly IDialogService dialogService; private readonly IConversionPresetDialog conversionPresetDialog; private readonly IFiletagEditorDialog filetagEditorDialog; @@ -119,13 +109,6 @@ public MediaViewModel(IVersionService versionService, ILogger _l private static readonly IPathProvider pathProvider = IoC.Default.GetService() ?? throw new ArgumentNullException(); - private string GetVersionDescription() - { - var appName = "OnionMedia"; - var version = versionService.GetCurrentVersion(); - - return $"{appName} - {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; - } private void Files_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { logger.LogInformation("Files_CollectionChanged in MediaViewModel"); diff --git a/OnionMedia/OnionMedia/App.xaml.cs b/OnionMedia/OnionMedia/App.xaml.cs index edc59bd..48c5119 100644 --- a/OnionMedia/OnionMedia/App.xaml.cs +++ b/OnionMedia/OnionMedia/App.xaml.cs @@ -66,8 +66,7 @@ private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledEx { // TODO WTS: Please log and handle the exception as appropriate to your scenario // For more info see https://docs.microsoft.com/windows/winui/api/microsoft.ui.xaml.unhandledexceptioneventargs - } - + } protected override async void OnLaunched(LaunchActivatedEventArgs args) { base.OnLaunched(args); diff --git a/OnionMedia/OnionMedia/ServiceProvider.cs b/OnionMedia/OnionMedia/ServiceProvider.cs index d24794f..d157947 100644 --- a/OnionMedia/OnionMedia/ServiceProvider.cs +++ b/OnionMedia/OnionMedia/ServiceProvider.cs @@ -71,14 +71,14 @@ namespace OnionMedia; [Transient(typeof(PlaylistsPage))] sealed partial class ServiceProvider { - private static string logfile = Path.Combine(AppSettings.Instance.LogPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); + private static string logfile = Path.Combine(AppSettings.Instance.LogPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}.log"); private static Serilog.ILogger CreateSerilogLogger() { Directory.CreateDirectory(AppSettings.Instance.LogPath); - CheckAge(); + CheckAge(); if (AppSettings.Instance.UseLogging) { return new LoggerConfiguration() @@ -92,6 +92,7 @@ private static Serilog.ILogger CreateSerilogLogger() .MinimumLevel.Fatal() // There are no fatal log-messages in code so nothing will be loggt. .CreateLogger(); } + } private static ILogger CreateLogger(IServiceProvider serviceProvider) { diff --git a/OnionMedia/OnionMedia/Views/ShellPage.xaml.cs b/OnionMedia/OnionMedia/Views/ShellPage.xaml.cs index e27e3b8..665b546 100644 --- a/OnionMedia/OnionMedia/Views/ShellPage.xaml.cs +++ b/OnionMedia/OnionMedia/Views/ShellPage.xaml.cs @@ -26,6 +26,9 @@ using OnionMedia.Core.Services; using OnionMedia.Core.ViewModels; using Microsoft.UI; +using Microsoft.Extensions.Logging; +using OnionMedia.Services; +using System.Diagnostics; namespace OnionMedia.Views { @@ -49,9 +52,13 @@ public sealed partial class ShellPage : Page private PCPowerOption desiredPowerOption; private bool executeOnError; + public ILogger logger; + private readonly IVersionService versionService; - public ShellPage(ShellViewModel viewModel, MediaViewModel mediaViewModel, YouTubeDownloaderViewModel downloaderViewModel, IPCPower pcPowerService) + public ShellPage(IVersionService versionService, ShellViewModel viewModel, MediaViewModel mediaViewModel, YouTubeDownloaderViewModel downloaderViewModel, IPCPower pcPowerService, ILogger _logger) { + this.versionService = versionService; + logger = _logger; ViewModel = viewModel; InitializeComponent(); ViewModel.NavigationService.Frame = shellFrame; @@ -66,17 +73,31 @@ public ShellPage(ShellViewModel viewModel, MediaViewModel mediaViewModel, YouTub private void OnLoaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { - // Keyboard accelerators are added here to avoid showing 'Alt + left' tooltip on the page. - // More info on tracking issue https://github.com/Microsoft/microsoft-ui-xaml/issues/8 - KeyboardAccelerators.Add(_altLeftKeyboardAccelerator); + + logger.LogDebug(GetVersionDescription()); + if (Debugger.IsAttached) + { + logger.LogDebug("App runs currently in debug-mode"); + } + + // Keyboard accelerators are added here to avoid showing 'Alt + left' tooltip on the page. + // More info on tracking issue https://github.com/Microsoft/microsoft-ui-xaml/issues/8 + KeyboardAccelerators.Add(_altLeftKeyboardAccelerator); KeyboardAccelerators.Add(_backKeyboardAccelerator); ConfigureTitleBar(); if (AppSettings.Instance.StartPageType is StartPageType.DownloaderPage || (AppSettings.Instance.StartPageType is StartPageType.LastOpened && navigateToDownloadPage)) shellFrame.Navigate(typeof(YouTubeDownloaderPage), null, new SuppressNavigationTransitionInfo()); } + private string GetVersionDescription() + { + var appName = "OnionMedia"; + var version = versionService.GetCurrentVersion(); - private void ConfigureTitleBar() + return $"{appName} - {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + } + + private void ConfigureTitleBar() { AppTitleBar.Visibility = Microsoft.UI.Xaml.Visibility.Visible; App.MainWindow.ExtendsContentIntoTitleBar = true; From fef3840e7399f76cfcc6bcd4d00b030d54b20ad5 Mon Sep 17 00:00:00 2001 From: lorenz | onionware <77418440+LorenzOnionware@users.noreply.github.com> Date: Tue, 25 Feb 2025 08:50:00 +0100 Subject: [PATCH 9/9] Download log All downlaods are logged now. --- .../Classes/DownloaderMethods.cs | 1440 +++++++++-------- .../OnionMedia.Core/Models/StreamItemModel.cs | 43 +- .../ViewModels/YouTubeDownloaderViewModel.cs | 6 +- 3 files changed, 769 insertions(+), 720 deletions(-) diff --git a/OnionMedia/OnionMedia.Core/Classes/DownloaderMethods.cs b/OnionMedia/OnionMedia.Core/Classes/DownloaderMethods.cs index e88e816..cb3fc28 100644 --- a/OnionMedia/OnionMedia.Core/Classes/DownloaderMethods.cs +++ b/OnionMedia/OnionMedia.Core/Classes/DownloaderMethods.cs @@ -33,714 +33,750 @@ using OnionMedia.Core.Services; using YoutubeExplode.Videos; using OnionMedia.Core.ViewModels.Dialogs; +using Microsoft.Extensions.Logging; namespace OnionMedia.Core.Classes { - public static class DownloaderMethods - { - private static readonly IDispatcherService dispatcher = IoC.Default.GetService() ?? throw new ArgumentNullException(); - private static readonly IPathProvider pathProvider = IoC.Default.GetService() ?? throw new ArgumentNullException(); - - public static readonly YoutubeClient youtube = new(); - - public static readonly YoutubeDL downloadClient = new(5) - { - FFmpegPath = pathProvider.FFmpegPath, - YoutubeDLPath = pathProvider.YtDlPath, - OutputFolder = pathProvider.DownloaderTempdir, - OverwriteFiles = true - }; - - private static string GetHardwareEncodingParameters(FormatData formatData) => AppSettings.Instance.HardwareEncoder switch - { - HardwareEncoder.Nvidia_NVENC => $"-vcodec h264_nvenc {(formatData.VideoBitrate > 0 ? $"-b:v {formatData.VideoBitrate.ToString().Replace(',', '.') + 'k'}" : "-profile:v high -q:v 20 -preset:v slower")}", - HardwareEncoder.Intel_QSV => $"-vcodec h264_qsv {(formatData.VideoBitrate > 0 ? $"-b:v {formatData.VideoBitrate.ToString().Replace(',', '.') + 'k'}" : "-profile:v high -q:v 20 -preset:v slower")}", - HardwareEncoder.AMD_AMF => $"-vcodec h264_amf {(formatData.VideoBitrate > 0 ? $"-b:v {formatData.VideoBitrate.ToString().Replace(',', '.') + 'k'}" : "-profile:v high -q:v 20 -preset:v slower")}", - _ => string.Empty - }; - - public static async Task DownloadStreamAsync(StreamItemModel stream, bool getMP4, string customOutputDirectory = null) - { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - OptionSet ytOptions = new() { RestrictFilenames = true }; - - //Creates a temp directory if it does not already exist. - Directory.CreateDirectory(pathProvider.DownloaderTempdir); - - //Creates a new temp directory for this file - string videotempdir = CreateVideoTempDir(); - - SetProgressToDefault(stream); - stream.CancelEventHandler += CancelDownload; - - var cToken = stream.CancelSource.Token; - bool autoConvertToH264 = AppSettings.Instance.AutoConvertToH264AfterDownload; - - string tempfile = string.Empty; - bool isShortened = false; - Enums.ItemType itemType = Enums.ItemType.video; - - try - { - Debug.WriteLine($"Current State of Progress: {stream.ProgressInfo.Progress}"); - ytOptions.Output = Path.Combine(videotempdir, "%(id)s.%(ext)s"); - - if (stream.CustomTimes) - { - ytOptions.AddCustomOption("--download-sections", $"*{stream.TimeSpanGroup.StartTime.WithoutMilliseconds()}.00-{stream.TimeSpanGroup.EndTime:hh\\:mm\\:ss}.00"); - ytOptions.AddCustomOption("--force-keyframes-at-cuts --compat", "no-direct-merge"); - ytOptions.ExternalDownloaderArgs = String.Empty; - isShortened = true; - } - - //Set download speed limit from AppSettings - if (AppSettings.Instance.LimitDownloadSpeed && AppSettings.Instance.MaxDownloadSpeed > 0) - ytOptions.LimitRate = (long)(AppSettings.Instance.MaxDownloadSpeed * 1000000 / 8); - - //Audiodownload - if (!getMP4) - { - tempfile = await RunAudioDownloadAndConversionAsync(stream, ytOptions, isShortened, TimeSpan.Zero, cToken); - itemType = Enums.ItemType.audio; - } - //Videodownload - else - { - tempfile = await RunVideoDownloadAndConversionAsync(stream, ytOptions, isShortened, autoConvertToH264, TimeSpan.Zero, cToken); - itemType = Enums.ItemType.video; - } - } - catch (Exception ex) - { - stream.Downloading = false; - stream.Converting = false; - Console.WriteLine(ex.Message); - Debug.WriteLine(ex.Message); - switch (ex) - { - default: - Debug.WriteLine(ex.Message); - stream.DownloadState = Enums.DownloadState.IsFailed; - stream.ProgressInfo.IsCancelledOrFailed = true; - return; - - case TaskCanceledException: - Debug.WriteLine("The download has cancelled."); - stream.DownloadState = Enums.DownloadState.IsCancelled; - stream.ProgressInfo.IsCancelledOrFailed = true; - return; - - case HttpRequestException: - Debug.WriteLine("No internet connection."); - stream.DownloadState = Enums.DownloadState.IsFailed; - stream.ProgressInfo.IsCancelledOrFailed = true; - return; - - case SecurityException: - Debug.WriteLine("No access to the temp-path."); - stream.DownloadState = Enums.DownloadState.IsFailed; - stream.ProgressInfo.IsCancelledOrFailed = true; - return; - - case IOException: - Debug.WriteLine("Failed to save video to the temp-path."); - stream.DownloadState = Enums.DownloadState.IsFailed; - stream.ProgressInfo.IsCancelledOrFailed = true; - NotEnoughSpaceException.ThrowIfNotEnoughSpace((IOException)ex); - throw; - } - } - - stream.Converting = false; - Debug.WriteLine($"Current State of Progress: {stream.ProgressInfo.Progress}"); - - try - { - //Writes the tags into the file - //TODO: Fix file corruptions while saving tags - if (Path.GetExtension(tempfile) is ".mp3" or ".mp4" or ".m4a" or ".flac") - { - if (stream.CustomTags is null) - { - SaveTags(tempfile, stream.Video); - } - else - { - SaveTags(tempfile, stream.CustomTags); - } - } - - //Moves the video in the correct directory - stream.Moving = true; - stream.Path = await MoveToDisk(tempfile, stream.Video.Title, itemType, customOutputDirectory, cToken); - - stream.RaiseFinished(); - stream.DownloadState = Enums.DownloadState.IsDone; - } - catch (Exception ex) - { - stream.DownloadState = Enums.DownloadState.IsFailed; - stream.ProgressInfo.IsCancelledOrFailed = true; - Console.WriteLine(ex.Message); - Debug.WriteLine(ex.Message); - switch (ex) - { - default: - Debug.WriteLine(ex.Message); - break; - - case OperationCanceledException: - Debug.WriteLine("Moving operation get canceled."); - stream.DownloadState = Enums.DownloadState.IsCancelled; - break; - - case FileNotFoundException: - Debug.WriteLine("File not found!"); - break; - - case UnauthorizedAccessException: - Debug.WriteLine("No permissions to access the file."); - throw; - - case PathTooLongException: - Debug.WriteLine("Path is too long"); - throw; - - case DirectoryNotFoundException: - Debug.WriteLine("Directory not found!"); - throw; - - case IOException: - Debug.WriteLine("IOException occured."); - NotEnoughSpaceException.ThrowIfNotEnoughSpace((IOException)ex); - throw; - } - } - finally { stream.Moving = false; } - } - - public static async Task DownloadThumbnailAsync(string videoUrl, string filePath, string imageFormat, CancellationToken ct = default) - { - OptionSet options = new() { WriteThumbnail = true, SkipDownload = true, Output = filePath }; - options.AddCustomOption("--convert-thumbnails", imageFormat); - options.AddCustomOption("--ppa", "ThumbnailsConvertor:-q:v 1"); - string newFilename = $"{filePath}.{imageFormat}"; - await downloadClient.RunWithOptions(new[] { videoUrl }, options, ct); - if (File.Exists(newFilename)) - { - File.Move(newFilename, filePath, true); - } - } - - private static void SetProgressToDefault(StreamItemModel stream) - { - stream.ProgressInfo.IsDone = false; - stream.Converting = false; - stream.DownloadState = Enums.DownloadState.IsLoading; - stream.ConversionProgress = 0; - } - - private static string CreateVideoTempDir() - { - string videotempdir; - do videotempdir = Path.Combine(pathProvider.DownloaderTempdir, Path.GetRandomFileName()); - while (Directory.Exists(videotempdir)); - Directory.CreateDirectory(videotempdir); - return videotempdir; - } - - private static async Task DownloadAudioAsync(StreamItemModel stream, OptionSet ytOptions, CancellationToken cToken = default) - { - if (AppSettings.Instance.DownloadsAudioFormat != AudioConversionFormat.Opus) - ytOptions.AddCustomOption("-S", "ext"); - - int downloadCounter = 0; - do - { - downloadCounter++; - if (downloadCounter > 1) - stream.SetProgressToDefault(); - - bool gotArguments = false; - stream.Downloading = true; - RunResult result = await downloadClient.RunAudioDownload(stream.Video.Url, AudioConversionFormat.Best, cToken, stream.DownloadProgress, overrideOptions: ytOptions, output: new Progress(p => - { - Debug.WriteLine(p); - if (!gotArguments) - { - stream.downloadLog.Insert(0, p + '\n'); - gotArguments = true; - } - })); - stream.Downloading = false; - - //Return if the download completed sucessfully. - if (!result.Data.IsNullOrEmpty()) - return result.Data; - - if (AppSettings.Instance.AutoRetryDownload) - Debug.WriteLine("Download failed. Try: " + downloadCounter); - } - while (AppSettings.Instance.AutoRetryDownload && downloadCounter <= AppSettings.Instance.CountOfDownloadRetries); - throw new Exception("Download failed!"); - } - - private static async Task RunAudioDownloadAndConversionAsync(StreamItemModel stream, OptionSet ytOptions, bool isShortened = false, TimeSpan overhead = default, CancellationToken cToken = default) - { - //Download - string tempfile = await DownloadAudioAsync(stream, ytOptions, cToken); - - string format = AppSettings.Instance.DownloadsAudioFormat.ToString().ToLower(); - if (AppSettings.Instance.DownloadsAudioFormat is AudioConversionFormat.Vorbis) - format = "ogg"; - - if (!tempfile.EndsWith(format)) - { - stream.Converting = true; - tempfile = await ConvertAudioAsync(tempfile, format, overhead, stream, cToken); - } - - Debug.WriteLine(tempfile); - stream.ProgressInfo.Progress = 100; - return tempfile; - } - - private static async Task RunVideoDownloadAndConversionAsync(StreamItemModel stream, OptionSet ytOptions, bool isShortened = false, bool autoConvertToH264 = false, TimeSpan overhead = default, CancellationToken cToken = default) - { - DownloadMergeFormat videoMergeFormat = (autoConvertToH264 || isShortened) ? DownloadMergeFormat.Unspecified : DownloadMergeFormat.Mp4; - VideoRecodeFormat videoRecodeFormat = (autoConvertToH264 || isShortened) ? VideoRecodeFormat.None : VideoRecodeFormat.Mp4; - ytOptions.EmbedThumbnail = true; - ytOptions.AddCustomOption("--format-sort", "hdr:SDR"); - bool isFromYoutube = stream.Video.Url.Contains("youtube.com") || stream.Video.Url.Contains("youtu.be"); - - Size originalSize = default; - FormatData formatData = null; - bool removeFormat = false; - - //Don't use DASH/M3U8 in trimmed videos - if (stream.CustomTimes) - { - //formatData = GetBestFormatWithoutDash(stream, out originalSize); - formatData = GetBestTrimmableFormat(stream); - } - else - { - //TODO: Check if that works on weaker PCs with less threads. - //Download multiple fragments at the same time to increase speed. - ytOptions.AddCustomOption("-N", 15); - } - - string? formatString; - if (formatData?.FormatId != null) - formatString = $"{formatData.FormatId}{((formatData.AudioCodec.IsNullOrWhiteSpace() || formatData.AudioCodec == "none") && formatData.AudioBitrate is null or 0 && stream.Video.Formats.Any(f => f.VideoBitrate is null or 0 && f.AudioBitrate > 0) ? "+bestaudio[ext=m4a]" : string.Empty)}"; - else - formatString = stream.QualityLabel.IsNullOrEmpty() ? "bestvideo+bestaudio/best" : stream.Format; - - if (removeFormat) - formatString = null; - - int downloadCounter = 0; - RunResult result; - do - { - downloadCounter++; - if (downloadCounter > 1) - stream.SetProgressToDefault(); - - //Try to encode MP4 hardware-accelerated in the first try - if (downloadCounter == 1 && formatData?.Extension == "mp4" - && AppSettings.Instance.AutoConvertToH264AfterDownload - && AppSettings.Instance.UseHardwareAcceleratedEncoding) - { - ytOptions.ExternalDownloaderArgs = GetHardwareEncodingParameters(formatData); - } - - bool gotArguments = false; - stream.Downloading = true; - result = await downloadClient.RunVideoDownload(stream.Video.Url, formatString, videoMergeFormat, videoRecodeFormat, cToken, stream.DownloadProgress, overrideOptions: ytOptions, output: new Progress(p => - { - Debug.WriteLine(p); - if (!gotArguments) - { - stream.downloadLog.Insert(0, p + '\n'); - gotArguments = true; - } - })); - stream.Downloading = false; - - if (!result.Data.IsNullOrEmpty()) - break; - - if (AppSettings.Instance.FallBackToSoftwareEncoding) - ytOptions.ExternalDownloaderArgs = string.Empty; - - if (AppSettings.Instance.AutoRetryDownload) - Debug.WriteLine("Download failed. Try: " + downloadCounter); - } - while (AppSettings.Instance.AutoRetryDownload && downloadCounter <= AppSettings.Instance.CountOfDownloadRetries); - if (result.Data.IsNullOrEmpty()) - throw new Exception("Download failed!"); - - - string tempfile = result.Data; - Debug.WriteLine(tempfile); - stream.ProgressInfo.Progress = 100; - - //TODO Setting: Allow to recode ALWAYS to H264, even if it's already H264. - var meta = await FFProbe.AnalyseAsync(tempfile); - if ((autoConvertToH264 && meta.PrimaryVideoStream.CodecName != "h264") || Path.GetExtension(tempfile) != ".mp4") - { - stream.Converting = true; - tempfile = await ConvertToMp4Async(tempfile, overhead, default, stream, originalSize, meta, cToken); - } - return tempfile; - } - - private static FormatData GetBestTrimmableFormat(StreamItemModel stream) - { - bool isFromYoutube = stream.Video.Url.Contains("youtube.com") || stream.Video.Url.Contains("youtu.be"); - var validFormats = stream.FormatQualityLabels.Where(f => ((f.Key.Protocol != "http_dash_segments" && !isFromYoutube) || (isFromYoutube && !f.Key.Protocol.Contains("m3u8") && !(f.Key.FormatNote ?? string.Empty).Contains("dash video", StringComparison.OrdinalIgnoreCase))) && f.Key.Extension != "3gp" && f.Key.HDR == "SDR"); - var heightSortedFormats = validFormats.OrderByDescending(f => f.Key.Height); - var extSortedFormats = heightSortedFormats.ThenBy(f => f.Key.Extension == "mp4" ? 0 : 1); - var selectedFormat = stream.QualityLabel.IsNullOrEmpty() - ? extSortedFormats.FirstOrDefault().Key - : extSortedFormats.FirstOrDefault(f => f.Key.Height <= stream.GetVideoHeight).Key; - return selectedFormat; - } - - private static FormatData GetBestFormatWithoutDash(StreamItemModel stream, out Size originalSize) - { - FormatData formatData = null; - originalSize = default; - - //Get the formats that doesn't use DASH - var formats = stream.FormatQualityLabels.Where(f => f.Key.Protocol != "http_dash_segments" && f.Key.Extension != "3gp" && f.Key.HDR == "SDR"); - formatData = formats.LastOrDefault(f => stream.QualityLabel.StartsWith(f.Value.ToString())).Key; - - //Return if a matching format was found. - if (formatData != null) return formatData; - - //Search for the next higher format. - int selectedHeight = int.Parse(stream.QualityLabel.TrimEnd('p')); - var higherFormats = formats.Where(f => f.Value > selectedHeight); - - if (!higherFormats.Any()) return null; - - //Get the higher format that comes closest to the choosen format. - //TODO: Downscale to the selected resolution? (Check for same aspect ratio) - int min = higherFormats.Min(f => f.Value); - formatData = higherFormats.Where(f => f.Value == min).Last().Key; - - //Return the size of the choosen format to downscale it later. - var defaultSize = stream.FormatQualityLabels.LastOrDefault(f => stream.QualityLabel.StartsWith(f.Value.ToString())).Key; - if (defaultSize != null) - originalSize = new Size((int)defaultSize.Width, (int)defaultSize.Height); - - //Return the format. - return formatData; - } - - private static void CancelDownload(object sender, CancellationEventArgs e) - { - if (e.Restart) - throw new DownloadRestartedException(); - e.CancelSource.Cancel(); - } - - //Saves the tags into the file - public static void SaveTags(string filename, VideoData meta) - { - var tagfile = TagLib.File.Create(filename); - - tagfile.Tag.Title = meta.Title; - - if (!meta.Description.IsNullOrEmpty()) - tagfile.Tag.Description = meta.Description; - - if (!meta.Uploader.IsNullOrEmpty()) - tagfile.Tag.Performers = new[] { meta.Uploader }; - - if (meta.UploadDate != null) - tagfile.Tag.Year = (uint)meta.UploadDate.Value.Year; - - tagfile.Save(); - } - - //Saves the tags into the file - public static void SaveTags(string filename, FileTags tags) - { - var tagfile = TagLib.File.Create(filename); - - tagfile.Tag.Title = tags.Title; - - if (!tags.Description.IsNullOrEmpty()) - tagfile.Tag.Description = tags.Description; - - if (!tags.Artist.IsNullOrEmpty()) - tagfile.Tag.Performers = new[] { tags.Artist }; - - tagfile.Tag.Year = tags.Year; - - tagfile.Save(); - } - - /// - /// Converts the downloaded audio file to the selected audio format (AppSettings) and/or removes the overhead a the start. - /// - /// The path of the converted file. - private static async Task ConvertAudioAsync(string inputfile, string format, TimeSpan startTime = default, StreamItemModel stream = null, CancellationToken cancellationToken = default) - { - StringBuilder argBuilder = new(); - argBuilder.Append($"-i \"{inputfile}\" "); - if (!inputfile.EndsWith(format)) - { - argBuilder.Append($"-y -loglevel \"repeat+info\" -movflags \"+faststart\" -vn "); - argBuilder.Append(GetAudioConversionArgs()); - } - argBuilder.Append($"-ss {startTime} "); - - string outputpath = Path.ChangeExtension(inputfile, format); - for (int i = 2; File.Exists(outputpath); i++) - outputpath = Path.ChangeExtension(outputpath, $"_{i}.{format}"); - argBuilder.Append($"\"{outputpath}\""); - - Debug.WriteLine(argBuilder.ToString()); - Engine ffmpeg = new(pathProvider.FFmpegPath); - var meta = await ffmpeg.GetMetaDataAsync(new InputFile(inputfile), cancellationToken); - - if (stream != null) - { - ffmpeg.Complete += async (o, e) => await dispatcher.EnqueueAsync(() => stream.ConversionProgress = 100); - ffmpeg.Error += async (o, e) => await dispatcher.EnqueueAsync(() => stream.ConversionProgress = 0); - ffmpeg.Progress += async (o, e) => await dispatcher.EnqueueAsync(() => stream.ConversionProgress = (int)(e.ProcessedDuration / (meta.Duration - startTime) * 100)); - } - - await ffmpeg.ExecuteAsync(argBuilder.ToString(), cancellationToken); - return outputpath; - } - - /// - /// Converts a file to MP4 (with H264 codec if enabled in settings) and returns the new path - /// - /// The path of the converted file - private static async Task ConvertToMp4Async(string inputfile, TimeSpan startTime = default, TimeSpan endTime = default, StreamItemModel stream = null, Size resolution = default, IMediaAnalysis videoMeta = null, CancellationToken cancellationToken = default) - { - string outputpath = Path.ChangeExtension(inputfile, ".converted.mp4"); - for (int i = 2; File.Exists(outputpath); i++) - outputpath = Path.ChangeExtension(inputfile, $".converted_{i}.mp4"); - - var input = new InputFile(inputfile); - var output = new OutputFile(outputpath); - Engine ffmpeg = new(pathProvider.FFmpegPath); - - var con = new ConversionOptions(); - if (AppSettings.Instance.AutoConvertToH264AfterDownload) - { - if (AppSettings.Instance.UseHardwareAcceleratedEncoding) - { - con.VideoCodec = AppSettings.Instance.HardwareEncoder switch - { - HardwareEncoder.Intel_QSV => VideoCodec.h264_qsv, - HardwareEncoder.AMD_AMF => VideoCodec.h264_amf, - HardwareEncoder.Nvidia_NVENC => VideoCodec.h264_nvenc, - _ => VideoCodec.libx264 - }; - - var meta = videoMeta ?? await FFProbe.AnalyseAsync(inputfile); - con.VideoBitRate = (int)GlobalResources.CalculateVideoBitrate(inputfile, meta) / 1000; - } - else - con.VideoCodec = VideoCodec.libx264; - } - else - con.VideoCodec = VideoCodec.Default; - - StringBuilder extraArgs = new(); - extraArgs.Append($"-ss {startTime}"); - if (endTime != default) - extraArgs.Append($" -to {endTime}"); - if (resolution != default) - extraArgs.Append($" -vf scale={resolution.Width}:{resolution.Height}"); - con.ExtraArguments = extraArgs.ToString(); - - if (!AppSettings.Instance.AutoSelectThreadsForConversion) - con.Threads = AppSettings.Instance.MaxThreadCountForConversion; - - bool error = false; - if (stream != null) - { - ffmpeg.Complete += async (o, e) => await dispatcher.EnqueueAsync(() => stream.ConversionProgress = 100); - ffmpeg.Error += async (o, e) => await dispatcher.EnqueueAsync(() => - { - stream.ConversionProgress = 0; - error = true; - Debug.WriteLine(e.Exception); - }); - ffmpeg.Progress += async (o, e) => await dispatcher.EnqueueAsync(() => - { - var totalDuration = e.TotalDuration - startTime; - if (endTime != default) - totalDuration -= totalDuration - endTime; - - stream.ConversionProgress = (int)(e.ProcessedDuration / totalDuration * 100); - }); - } - - await ffmpeg.ConvertAsync(input, output, con, cancellationToken); - - //Fallback to software-encoding when hardware-encoding doen't work - if (error && con.VideoCodec is VideoCodec.h264_nvenc or VideoCodec.h264_qsv or VideoCodec.h264_amf && AppSettings.Instance.FallBackToSoftwareEncoding) - { - error = false; - con.VideoCodec = VideoCodec.libx264; - con.VideoBitRate = null; - await ffmpeg.ConvertAsync(input, output, con, cancellationToken); - } - - if (error) - stream.DownloadState = Enums.DownloadState.IsFailed; - return outputpath; - } - - - private static async Task MoveToDisk(string tempfile, string videoname, Enums.ItemType itemType, string directory = null, CancellationToken cancellationToken = default) - { - string extension = Path.GetExtension(tempfile); - - if (videoname.IsNullOrEmpty()) - videoname = "Download"; - - string savefilepath = directory ?? (itemType == Enums.ItemType.audio ? AppSettings.Instance.DownloadsAudioSavePath : AppSettings.Instance.DownloadsVideoSavePath); - - Directory.CreateDirectory(savefilepath); - int maxFilenameLength = 250 - (savefilepath.Length + extension.Length); - string filename = Path.Combine(savefilepath, videoname.TrimToFilename(maxFilenameLength) + extension); - - if (!File.Exists(filename)) - await Task.Run(async () => await GlobalResources.MoveFileAsync(tempfile, filename, cancellationToken)); - - else - { - string dir = Path.GetDirectoryName(filename); - string name = Path.GetFileNameWithoutExtension(filename); - - //Increases the number until a possible file name is found - for (int i = 2; File.Exists(filename); i++) - filename = Path.Combine(dir, $"{name}_{i}{extension}"); - - await Task.Run(async () => await GlobalResources.MoveFileAsync(tempfile, filename, cancellationToken)); - } - return new Uri(filename); - } - - public static async Task> GetVideosFromPlaylistAsync(string playlistUrl, CancellationToken cToken = default) - { - //TODO: CHECK FOR VULNERABILITIES! - Process ytdlProc = new(); - ytdlProc.StartInfo.CreateNoWindow = true; - ytdlProc.StartInfo.FileName = pathProvider.YtDlPath; - ytdlProc.StartInfo.RedirectStandardOutput = true; - ytdlProc.StartInfo.Arguments = $"\"{playlistUrl}\" --flat-playlist --dump-json"; - ytdlProc.Start(); - - string output = await ytdlProc.StandardOutput.ReadToEndAsync(); - if (!ytdlProc.HasExited) - await ytdlProc.WaitForExitAsync(cToken); - - List videos = new(); - foreach (var line in output.Split('\n')) - { - cToken.ThrowIfCancellationRequested(); - try { videos.Add(JsonConvert.DeserializeObject(line.Trim())); } - catch { continue; } - } - - return videos.Where(v => v?.DurationAsFloatingNumber != null); - } - - //Returns the resolutions from the videos. - public static IEnumerable GetResolutions(IEnumerable videos) - { - ReadOnlySpan validResolutions = stackalloc[] { 144, 144, 240, 360, 480, 720, 1080, 1440, 2160, 4320, 8640 }; - SortedSet heights = new(); - SortedSet correctHeights = new(); - List resolutions = new(); - - //Add video heights to a SortedSet - foreach (var video in videos.Where(v => v != null)) - if (video.Video.Formats != null) - foreach (var format in video.Video.Formats.Where(f => f != null)) - if (format.Height != null) - heights.Add((int)format.Height); - - //Round up "non-default" resolutions - foreach (int height in heights) - { - for (int i = 1; i < validResolutions.Length; i++) - { - if (height < validResolutions[i] && height > validResolutions[i - 1] || height == validResolutions[i]) - { - correctHeights.Add(validResolutions[i]); - break; - } - else if (height < validResolutions[i]) - { - correctHeights.Add(validResolutions[i - 1]); - break; - } - } - } - - //Converts the heights to string values and adds a "p" at the end. - foreach (int height in correctHeights.Reverse()) - resolutions.Add(height + "p"); - - return resolutions; - } - - public static async IAsyncEnumerable GetSearchResultsAsync(string searchTerm, int maxResults = 20) - { - if (VideoSearchCancelSource.IsCancellationRequested) - VideoSearchCancelSource = new(); - - int counter = 0; - await foreach (var result in youtube.Search.GetVideosAsync(searchTerm, VideoSearchCancelSource.Token)) - { - if (counter == maxResults) - break; - - yield return new SearchItemModel(result); - counter++; - } - } - public static CancellationTokenSource VideoSearchCancelSource { get; private set; } = new(); - - - private static string GetAudioConversionArgs() => AppSettings.Instance.DownloadsAudioFormat switch - { - AudioConversionFormat.Flac => "-acodec flac ", - AudioConversionFormat.Opus => "-acodec libopus ", - AudioConversionFormat.M4a => $"-acodec aac \"-bsf:a\" aac_adtstoasc -aq {GetAudioQuality("aac").ToString().Replace(',', '.')} ", - AudioConversionFormat.Mp3 => $"-acodec libmp3lame -aq {GetAudioQuality("libmp3lame").ToString().Replace(',', '.')} ", - AudioConversionFormat.Vorbis => $"-acodec libvorbis -aq {GetAudioQuality("libvorbis").ToString().Replace(',', '.')} ", - _ => $"-f {AppSettings.Instance.DownloadsAudioFormat.ToString().ToLower()} " - }; - - private static readonly Dictionary audioQualityDict = new() - { - { "libmp3lame", (10, 0) }, - { "libvorbis", (0, 10) }, + public static class DownloaderMethods + { + private static readonly IDispatcherService dispatcher = IoC.Default.GetService() ?? throw new ArgumentNullException(); + private static readonly IPathProvider pathProvider = IoC.Default.GetService() ?? throw new ArgumentNullException(); + + public static readonly YoutubeClient youtube = new(); + + public static readonly YoutubeDL downloadClient = new(5) + { + FFmpegPath = pathProvider.FFmpegPath, + YoutubeDLPath = pathProvider.YtDlPath, + OutputFolder = pathProvider.DownloaderTempdir, + OverwriteFiles = true + }; + + private static string GetHardwareEncodingParameters(FormatData formatData) => AppSettings.Instance.HardwareEncoder switch + { + HardwareEncoder.Nvidia_NVENC => $"-vcodec h264_nvenc {(formatData.VideoBitrate > 0 ? $"-b:v {formatData.VideoBitrate.ToString().Replace(',', '.') + 'k'}" : "-profile:v high -q:v 20 -preset:v slower")}", + HardwareEncoder.Intel_QSV => $"-vcodec h264_qsv {(formatData.VideoBitrate > 0 ? $"-b:v {formatData.VideoBitrate.ToString().Replace(',', '.') + 'k'}" : "-profile:v high -q:v 20 -preset:v slower")}", + HardwareEncoder.AMD_AMF => $"-vcodec h264_amf {(formatData.VideoBitrate > 0 ? $"-b:v {formatData.VideoBitrate.ToString().Replace(',', '.') + 'k'}" : "-profile:v high -q:v 20 -preset:v slower")}", + _ => string.Empty + }; + + public static async Task DownloadStreamAsync(StreamItemModel stream, ILogger logger, bool getMP4, string customOutputDirectory = null) + { + logger.LogDebug("DownloadStreamAsync called"); + if (stream == null) + { + logger.LogError($"Argument null Exception {stream} in DownloadStreamAsync"); + throw new ArgumentNullException(nameof(stream)); + } + + + OptionSet ytOptions = new() { RestrictFilenames = true }; + + //Creates a temp directory if it does not already exist. + Directory.CreateDirectory(pathProvider.DownloaderTempdir); + + //Creates a new temp directory for this file + string videotempdir = CreateVideoTempDir(); + + SetProgressToDefault(stream); + stream.CancelEventHandler += CancelDownload; + + var cToken = stream.CancelSource.Token; + bool autoConvertToH264 = AppSettings.Instance.AutoConvertToH264AfterDownload; + + string tempfile = string.Empty; + bool isShortened = false; + Enums.ItemType itemType = Enums.ItemType.video; + + try + { + logger.LogInformation($"Current State of Progress: {stream.ProgressInfo.Progress}"); + Debug.WriteLine($"Current State of Progress: {stream.ProgressInfo.Progress}"); + ytOptions.Output = Path.Combine(videotempdir, "%(id)s.%(ext)s"); + + if (stream.CustomTimes) + { + ytOptions.AddCustomOption("--download-sections", $"*{stream.TimeSpanGroup.StartTime.WithoutMilliseconds()}.00-{stream.TimeSpanGroup.EndTime:hh\\:mm\\:ss}.00"); + ytOptions.AddCustomOption("--force-keyframes-at-cuts --compat", "no-direct-merge"); + ytOptions.ExternalDownloaderArgs = String.Empty; + isShortened = true; + } + + //Set download speed limit from AppSettings + if (AppSettings.Instance.LimitDownloadSpeed && AppSettings.Instance.MaxDownloadSpeed > 0) + ytOptions.LimitRate = (long)(AppSettings.Instance.MaxDownloadSpeed * 1000000 / 8); + + //Audiodownload + if (!getMP4) + { + tempfile = await RunAudioDownloadAndConversionAsync(stream, logger, ytOptions, isShortened, TimeSpan.Zero, cToken); + itemType = Enums.ItemType.audio; + } + //Videodownload + else + { + tempfile = await RunVideoDownloadAndConversionAsync(stream, logger, ytOptions, isShortened, autoConvertToH264, TimeSpan.Zero, cToken); + itemType = Enums.ItemType.video; + } + } + catch (Exception ex) + { + logger.LogError(ex.Message); + stream.Downloading = false; + stream.Converting = false; + Console.WriteLine(ex.Message); + Debug.WriteLine(ex.Message); + switch (ex) + { + default: + Debug.WriteLine(ex.Message); + stream.DownloadState = Enums.DownloadState.IsFailed; + stream.ProgressInfo.IsCancelledOrFailed = true; + logger.LogWarning($"{stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + + return; + + case TaskCanceledException: + Debug.WriteLine("The download has cancelled."); + stream.DownloadState = Enums.DownloadState.IsCancelled; + stream.ProgressInfo.IsCancelledOrFailed = true; + logger.LogWarning($"{stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + return; + + case HttpRequestException: + Debug.WriteLine("No internet connection."); + stream.DownloadState = Enums.DownloadState.IsFailed; + stream.ProgressInfo.IsCancelledOrFailed = true; + logger.LogWarning($"{stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + return; + + case SecurityException: + Debug.WriteLine("No access to the temp-path."); + stream.DownloadState = Enums.DownloadState.IsFailed; + stream.ProgressInfo.IsCancelledOrFailed = true; + logger.LogWarning($"{stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + return; + + case IOException: + Debug.WriteLine("Failed to save video to the temp-path."); + stream.DownloadState = Enums.DownloadState.IsFailed; + stream.ProgressInfo.IsCancelledOrFailed = true; + logger.LogError("NotEnoughSpace"); + logger.LogWarning($"{stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + NotEnoughSpaceException.ThrowIfNotEnoughSpace((IOException)ex); + throw; + } + } + + stream.Converting = false; + Debug.WriteLine($"Current State of Progress: {stream.ProgressInfo.Progress}"); + logger.LogInformation($"Current State of Progress: {stream.ProgressInfo.Progress}"); + + try + { + //Writes the tags into the file + //TODO: Fix file corruptions while saving tags + if (Path.GetExtension(tempfile) is ".mp3" or ".mp4" or ".m4a" or ".flac") + { + if (stream.CustomTags is null) + { + SaveTags(tempfile, stream.Video); + } + else + { + SaveTags(tempfile, stream.CustomTags); + } + } + + //Moves the video in the correct directory + stream.Moving = true; + stream.Path = await MoveToDisk(tempfile, stream.Video.Title, itemType, customOutputDirectory, cToken); + + stream.RaiseFinished(); + stream.DownloadState = Enums.DownloadState.IsDone; + } + catch (Exception ex) + { + stream.DownloadState = Enums.DownloadState.IsFailed; + stream.ProgressInfo.IsCancelledOrFailed = true; + logger.LogError(ex.Message); + Console.WriteLine(ex.Message); + Debug.WriteLine(ex.Message); + switch (ex) + { + default: + Debug.WriteLine(ex.Message); + break; + + case OperationCanceledException: + Debug.WriteLine("Moving operation get canceled."); + stream.DownloadState = Enums.DownloadState.IsCancelled; + logger.LogError($"Moving operation get Canceled{stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + break; + + case FileNotFoundException: + Debug.WriteLine("File not found!"); + logger.LogError($"File Not Found {stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + break; + + case UnauthorizedAccessException: + Debug.WriteLine("No permissions to access the file."); + logger.LogError($"No permession to access the file {stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + throw; + + case PathTooLongException: + Debug.WriteLine("Path is too long"); + logger.LogError($"path is too long {stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + throw; + + case DirectoryNotFoundException: + Debug.WriteLine("Directory not found!"); + logger.LogError($"Directory not found {stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + throw; + + case IOException: + Debug.WriteLine("IOException occured."); + logger.LogError($"IOException occured {stream.DownloadState}||{stream.DownloadLog}||{stream.Video.Url}"); + NotEnoughSpaceException.ThrowIfNotEnoughSpace((IOException)ex); + throw; + } + } + finally { stream.Moving = false; } + } + + public static async Task DownloadThumbnailAsync(string videoUrl, string filePath, string imageFormat, CancellationToken ct = default) + { + OptionSet options = new() { WriteThumbnail = true, SkipDownload = true, Output = filePath }; + options.AddCustomOption("--convert-thumbnails", imageFormat); + options.AddCustomOption("--ppa", "ThumbnailsConvertor:-q:v 1"); + string newFilename = $"{filePath}.{imageFormat}"; + await downloadClient.RunWithOptions(new[] { videoUrl }, options, ct); + if (File.Exists(newFilename)) + { + File.Move(newFilename, filePath, true); + } + } + + private static void SetProgressToDefault(StreamItemModel stream) + { + stream.ProgressInfo.IsDone = false; + stream.Converting = false; + stream.DownloadState = Enums.DownloadState.IsLoading; + stream.ConversionProgress = 0; + } + + private static string CreateVideoTempDir() + { + string videotempdir; + do videotempdir = Path.Combine(pathProvider.DownloaderTempdir, Path.GetRandomFileName()); + while (Directory.Exists(videotempdir)); + Directory.CreateDirectory(videotempdir); + return videotempdir; + } + + private static async Task DownloadAudioAsync(StreamItemModel stream, ILogger logger, OptionSet ytOptions, CancellationToken cToken = default) + { + if (AppSettings.Instance.DownloadsAudioFormat != AudioConversionFormat.Opus) + ytOptions.AddCustomOption("-S", "ext"); + + int downloadCounter = 0; + do + { + downloadCounter++; + if (downloadCounter > 1) + stream.SetProgressToDefault(); + + bool gotArguments = false; + stream.Downloading = true; + RunResult result = await downloadClient.RunAudioDownload(stream.Video.Url, AudioConversionFormat.Best, cToken, stream.DownloadProgress, overrideOptions: ytOptions, output: new Progress(p => + { + Debug.WriteLine(p); + if (!gotArguments) + { + stream.downloadLog.Insert(0, p + '\n'); + gotArguments = true; + } + })); + stream.Downloading = false; + + //Return if the download completed sucessfully. + if (!result.Data.IsNullOrEmpty()) + return result.Data; + + if (AppSettings.Instance.AutoRetryDownload) + { + logger.LogError($"Download failed in DownloadAudioAsync, try: {downloadCounter}"); + Debug.WriteLine("Download failed. Try: " + downloadCounter); + } + + } + while (AppSettings.Instance.AutoRetryDownload && downloadCounter <= AppSettings.Instance.CountOfDownloadRetries); + logger.LogError("download failed"); + throw new Exception("Download failed!"); + } + + private static async Task RunAudioDownloadAndConversionAsync(StreamItemModel stream, ILogger logger, OptionSet ytOptions, bool isShortened = false, TimeSpan overhead = default, CancellationToken cToken = default) + { + //Download + string tempfile = await DownloadAudioAsync(stream, logger, ytOptions, cToken); + + string format = AppSettings.Instance.DownloadsAudioFormat.ToString().ToLower(); + if (AppSettings.Instance.DownloadsAudioFormat is AudioConversionFormat.Vorbis) + format = "ogg"; + + if (!tempfile.EndsWith(format)) + { + stream.Converting = true; + tempfile = await ConvertAudioAsync(tempfile, format, overhead, stream, cToken); + } + + Debug.WriteLine(tempfile); + stream.ProgressInfo.Progress = 100; + return tempfile; + } + + private static async Task RunVideoDownloadAndConversionAsync(StreamItemModel stream, ILogger logger, OptionSet ytOptions, bool isShortened = false, bool autoConvertToH264 = false, TimeSpan overhead = default, CancellationToken cToken = default) + { + DownloadMergeFormat videoMergeFormat = (autoConvertToH264 || isShortened) ? DownloadMergeFormat.Unspecified : DownloadMergeFormat.Mp4; + VideoRecodeFormat videoRecodeFormat = (autoConvertToH264 || isShortened) ? VideoRecodeFormat.None : VideoRecodeFormat.Mp4; + ytOptions.EmbedThumbnail = true; + ytOptions.AddCustomOption("--format-sort", "hdr:SDR"); + bool isFromYoutube = stream.Video.Url.Contains("youtube.com") || stream.Video.Url.Contains("youtu.be"); + + Size originalSize = default; + FormatData formatData = null; + bool removeFormat = false; + + //Don't use DASH/M3U8 in trimmed videos + if (stream.CustomTimes) + { + //formatData = GetBestFormatWithoutDash(stream, out originalSize); + formatData = GetBestTrimmableFormat(stream); + } + else + { + //TODO: Check if that works on weaker PCs with less threads. + //Download multiple fragments at the same time to increase speed. + ytOptions.AddCustomOption("-N", 15); + } + + string? formatString; + if (formatData?.FormatId != null) + formatString = $"{formatData.FormatId}{((formatData.AudioCodec.IsNullOrWhiteSpace() || formatData.AudioCodec == "none") && formatData.AudioBitrate is null or 0 && stream.Video.Formats.Any(f => f.VideoBitrate is null or 0 && f.AudioBitrate > 0) ? "+bestaudio[ext=m4a]" : string.Empty)}"; + else + formatString = stream.QualityLabel.IsNullOrEmpty() ? "bestvideo+bestaudio/best" : stream.Format; + + if (removeFormat) + formatString = null; + + int downloadCounter = 0; + RunResult result; + do + { + downloadCounter++; + if (downloadCounter > 1) + stream.SetProgressToDefault(); + + //Try to encode MP4 hardware-accelerated in the first try + if (downloadCounter == 1 && formatData?.Extension == "mp4" + && AppSettings.Instance.AutoConvertToH264AfterDownload + && AppSettings.Instance.UseHardwareAcceleratedEncoding) + { + ytOptions.ExternalDownloaderArgs = GetHardwareEncodingParameters(formatData); + } + + bool gotArguments = false; + stream.Downloading = true; + result = await downloadClient.RunVideoDownload(stream.Video.Url, formatString, videoMergeFormat, videoRecodeFormat, cToken, stream.DownloadProgress, overrideOptions: ytOptions, output: new Progress(p => + { + Debug.WriteLine(p); + if (!gotArguments) + { + stream.downloadLog.Insert(0, p + '\n'); + gotArguments = true; + } + })); + stream.Downloading = false; + + if (!result.Data.IsNullOrEmpty()) + break; + + if (AppSettings.Instance.FallBackToSoftwareEncoding) + ytOptions.ExternalDownloaderArgs = string.Empty; + + if (AppSettings.Instance.AutoRetryDownload) + { + logger.LogError("Download failed. Try: " + downloadCounter); + Debug.WriteLine("Download failed. Try: " + downloadCounter); + } + + } + while (AppSettings.Instance.AutoRetryDownload && downloadCounter <= AppSettings.Instance.CountOfDownloadRetries); + if (result.Data.IsNullOrEmpty()) + { + logger.LogError("Download faile!"); + throw new Exception("Download failed!"); + } + + string tempfile = result.Data; + Debug.WriteLine(tempfile); + stream.ProgressInfo.Progress = 100; + + //TODO Setting: Allow to recode ALWAYS to H264, even if it's already H264. + var meta = await FFProbe.AnalyseAsync(tempfile); + if ((autoConvertToH264 && meta.PrimaryVideoStream.CodecName != "h264") || Path.GetExtension(tempfile) != ".mp4") + { + stream.Converting = true; + tempfile = await ConvertToMp4Async(tempfile, logger, overhead, default, stream, originalSize, meta, cToken); + } + return tempfile; + } + + private static FormatData GetBestTrimmableFormat(StreamItemModel stream) + { + bool isFromYoutube = stream.Video.Url.Contains("youtube.com") || stream.Video.Url.Contains("youtu.be"); + var validFormats = stream.FormatQualityLabels.Where(f => ((f.Key.Protocol != "http_dash_segments" && !isFromYoutube) || (isFromYoutube && !f.Key.Protocol.Contains("m3u8") && !(f.Key.FormatNote ?? string.Empty).Contains("dash video", StringComparison.OrdinalIgnoreCase))) && f.Key.Extension != "3gp" && f.Key.HDR == "SDR"); + var heightSortedFormats = validFormats.OrderByDescending(f => f.Key.Height); + var extSortedFormats = heightSortedFormats.ThenBy(f => f.Key.Extension == "mp4" ? 0 : 1); + var selectedFormat = stream.QualityLabel.IsNullOrEmpty() + ? extSortedFormats.FirstOrDefault().Key + : extSortedFormats.FirstOrDefault(f => f.Key.Height <= stream.GetVideoHeight).Key; + return selectedFormat; + } + + private static FormatData GetBestFormatWithoutDash(StreamItemModel stream, out Size originalSize) + { + FormatData formatData = null; + originalSize = default; + + //Get the formats that doesn't use DASH + var formats = stream.FormatQualityLabels.Where(f => f.Key.Protocol != "http_dash_segments" && f.Key.Extension != "3gp" && f.Key.HDR == "SDR"); + formatData = formats.LastOrDefault(f => stream.QualityLabel.StartsWith(f.Value.ToString())).Key; + + //Return if a matching format was found. + if (formatData != null) return formatData; + + //Search for the next higher format. + int selectedHeight = int.Parse(stream.QualityLabel.TrimEnd('p')); + var higherFormats = formats.Where(f => f.Value > selectedHeight); + + if (!higherFormats.Any()) return null; + + //Get the higher format that comes closest to the choosen format. + //TODO: Downscale to the selected resolution? (Check for same aspect ratio) + int min = higherFormats.Min(f => f.Value); + formatData = higherFormats.Where(f => f.Value == min).Last().Key; + + //Return the size of the choosen format to downscale it later. + var defaultSize = stream.FormatQualityLabels.LastOrDefault(f => stream.QualityLabel.StartsWith(f.Value.ToString())).Key; + if (defaultSize != null) + originalSize = new Size((int)defaultSize.Width, (int)defaultSize.Height); + + //Return the format. + return formatData; + } + + private static void CancelDownload(object sender, CancellationEventArgs e) + { + if (e.Restart) + throw new DownloadRestartedException(); + e.CancelSource.Cancel(); + } + + //Saves the tags into the file + public static void SaveTags(string filename, VideoData meta) + { + var tagfile = TagLib.File.Create(filename); + + tagfile.Tag.Title = meta.Title; + + if (!meta.Description.IsNullOrEmpty()) + tagfile.Tag.Description = meta.Description; + + if (!meta.Uploader.IsNullOrEmpty()) + tagfile.Tag.Performers = new[] { meta.Uploader }; + + if (meta.UploadDate != null) + tagfile.Tag.Year = (uint)meta.UploadDate.Value.Year; + + tagfile.Save(); + } + + //Saves the tags into the file + public static void SaveTags(string filename, FileTags tags) + { + var tagfile = TagLib.File.Create(filename); + + tagfile.Tag.Title = tags.Title; + + if (!tags.Description.IsNullOrEmpty()) + tagfile.Tag.Description = tags.Description; + + if (!tags.Artist.IsNullOrEmpty()) + tagfile.Tag.Performers = new[] { tags.Artist }; + + tagfile.Tag.Year = tags.Year; + + tagfile.Save(); + } + + /// + /// Converts the downloaded audio file to the selected audio format (AppSettings) and/or removes the overhead a the start. + /// + /// The path of the converted file. + private static async Task ConvertAudioAsync(string inputfile, string format, TimeSpan startTime = default, StreamItemModel stream = null, CancellationToken cancellationToken = default) + { + StringBuilder argBuilder = new(); + argBuilder.Append($"-i \"{inputfile}\" "); + if (!inputfile.EndsWith(format)) + { + argBuilder.Append($"-y -loglevel \"repeat+info\" -movflags \"+faststart\" -vn "); + argBuilder.Append(GetAudioConversionArgs()); + } + argBuilder.Append($"-ss {startTime} "); + + string outputpath = Path.ChangeExtension(inputfile, format); + for (int i = 2; File.Exists(outputpath); i++) + outputpath = Path.ChangeExtension(outputpath, $"_{i}.{format}"); + argBuilder.Append($"\"{outputpath}\""); + + Debug.WriteLine(argBuilder.ToString()); + Engine ffmpeg = new(pathProvider.FFmpegPath); + var meta = await ffmpeg.GetMetaDataAsync(new InputFile(inputfile), cancellationToken); + + if (stream != null) + { + ffmpeg.Complete += async (o, e) => await dispatcher.EnqueueAsync(() => stream.ConversionProgress = 100); + ffmpeg.Error += async (o, e) => await dispatcher.EnqueueAsync(() => stream.ConversionProgress = 0); + ffmpeg.Progress += async (o, e) => await dispatcher.EnqueueAsync(() => stream.ConversionProgress = (int)(e.ProcessedDuration / (meta.Duration - startTime) * 100)); + } + + await ffmpeg.ExecuteAsync(argBuilder.ToString(), cancellationToken); + return outputpath; + } + + /// + /// Converts a file to MP4 (with H264 codec if enabled in settings) and returns the new path + /// + /// The path of the converted file + private static async Task ConvertToMp4Async(string inputfile,ILogger logger, TimeSpan startTime = default, TimeSpan endTime = default, StreamItemModel stream = null, Size resolution = default, IMediaAnalysis videoMeta = null, CancellationToken cancellationToken = default) + { + string outputpath = Path.ChangeExtension(inputfile, ".converted.mp4"); + for (int i = 2; File.Exists(outputpath); i++) + outputpath = Path.ChangeExtension(inputfile, $".converted_{i}.mp4"); + + var input = new InputFile(inputfile); + var output = new OutputFile(outputpath); + Engine ffmpeg = new(pathProvider.FFmpegPath); + + var con = new ConversionOptions(); + if (AppSettings.Instance.AutoConvertToH264AfterDownload) + { + if (AppSettings.Instance.UseHardwareAcceleratedEncoding) + { + con.VideoCodec = AppSettings.Instance.HardwareEncoder switch + { + HardwareEncoder.Intel_QSV => VideoCodec.h264_qsv, + HardwareEncoder.AMD_AMF => VideoCodec.h264_amf, + HardwareEncoder.Nvidia_NVENC => VideoCodec.h264_nvenc, + _ => VideoCodec.libx264 + }; + + var meta = videoMeta ?? await FFProbe.AnalyseAsync(inputfile); + con.VideoBitRate = (int)GlobalResources.CalculateVideoBitrate(inputfile, meta) / 1000; + } + else + con.VideoCodec = VideoCodec.libx264; + } + else + con.VideoCodec = VideoCodec.Default; + + StringBuilder extraArgs = new(); + extraArgs.Append($"-ss {startTime}"); + if (endTime != default) + extraArgs.Append($" -to {endTime}"); + if (resolution != default) + extraArgs.Append($" -vf scale={resolution.Width}:{resolution.Height}"); + con.ExtraArguments = extraArgs.ToString(); + + if (!AppSettings.Instance.AutoSelectThreadsForConversion) + con.Threads = AppSettings.Instance.MaxThreadCountForConversion; + + bool error = false; + if (stream != null) + { + ffmpeg.Complete += async (o, e) => await dispatcher.EnqueueAsync(() => stream.ConversionProgress = 100); + ffmpeg.Error += async (o, e) => await dispatcher.EnqueueAsync(() => + { + stream.ConversionProgress = 0; + error = true; + Debug.WriteLine(e.Exception); + }); + ffmpeg.Progress += async (o, e) => await dispatcher.EnqueueAsync(() => + { + var totalDuration = e.TotalDuration - startTime; + if (endTime != default) + totalDuration -= totalDuration - endTime; + + stream.ConversionProgress = (int)(e.ProcessedDuration / totalDuration * 100); + }); + } + + await ffmpeg.ConvertAsync(input, output, con, cancellationToken); + + //Fallback to software-encoding when hardware-encoding doen't work + if (error && con.VideoCodec is VideoCodec.h264_nvenc or VideoCodec.h264_qsv or VideoCodec.h264_amf && AppSettings.Instance.FallBackToSoftwareEncoding) + { + logger.LogInformation("fell back to software-encoding"); + error = false; + con.VideoCodec = VideoCodec.libx264; + con.VideoBitRate = null; + await ffmpeg.ConvertAsync(input, output, con, cancellationToken); + } + + if (error) + logger.LogError("ConversionError occured in ConvertToMp4Async"); + stream.DownloadState = Enums.DownloadState.IsFailed; + return outputpath; + } + + + private static async Task MoveToDisk(string tempfile, string videoname, Enums.ItemType itemType, string directory = null, CancellationToken cancellationToken = default) + { + string extension = Path.GetExtension(tempfile); + + if (videoname.IsNullOrEmpty()) + videoname = "Download"; + + string savefilepath = directory ?? (itemType == Enums.ItemType.audio ? AppSettings.Instance.DownloadsAudioSavePath : AppSettings.Instance.DownloadsVideoSavePath); + + Directory.CreateDirectory(savefilepath); + int maxFilenameLength = 250 - (savefilepath.Length + extension.Length); + string filename = Path.Combine(savefilepath, videoname.TrimToFilename(maxFilenameLength) + extension); + + if (!File.Exists(filename)) + await Task.Run(async () => await GlobalResources.MoveFileAsync(tempfile, filename, cancellationToken)); + + else + { + string dir = Path.GetDirectoryName(filename); + string name = Path.GetFileNameWithoutExtension(filename); + + //Increases the number until a possible file name is found + for (int i = 2; File.Exists(filename); i++) + filename = Path.Combine(dir, $"{name}_{i}{extension}"); + + await Task.Run(async () => await GlobalResources.MoveFileAsync(tempfile, filename, cancellationToken)); + } + return new Uri(filename); + } + + public static async Task> GetVideosFromPlaylistAsync(string playlistUrl, CancellationToken cToken = default) + { + //TODO: CHECK FOR VULNERABILITIES! + Process ytdlProc = new(); + ytdlProc.StartInfo.CreateNoWindow = true; + ytdlProc.StartInfo.FileName = pathProvider.YtDlPath; + ytdlProc.StartInfo.RedirectStandardOutput = true; + ytdlProc.StartInfo.Arguments = $"\"{playlistUrl}\" --flat-playlist --dump-json"; + ytdlProc.Start(); + + string output = await ytdlProc.StandardOutput.ReadToEndAsync(); + if (!ytdlProc.HasExited) + await ytdlProc.WaitForExitAsync(cToken); + + List videos = new(); + foreach (var line in output.Split('\n')) + { + cToken.ThrowIfCancellationRequested(); + try { videos.Add(JsonConvert.DeserializeObject(line.Trim())); } + catch { continue; } + } + + return videos.Where(v => v?.DurationAsFloatingNumber != null); + } + + //Returns the resolutions from the videos. + public static IEnumerable GetResolutions(IEnumerable videos) + { + ReadOnlySpan validResolutions = stackalloc[] { 144, 144, 240, 360, 480, 720, 1080, 1440, 2160, 4320, 8640 }; + SortedSet heights = new(); + SortedSet correctHeights = new(); + List resolutions = new(); + + //Add video heights to a SortedSet + foreach (var video in videos.Where(v => v != null)) + if (video.Video.Formats != null) + foreach (var format in video.Video.Formats.Where(f => f != null)) + if (format.Height != null) + heights.Add((int)format.Height); + + //Round up "non-default" resolutions + foreach (int height in heights) + { + for (int i = 1; i < validResolutions.Length; i++) + { + if (height < validResolutions[i] && height > validResolutions[i - 1] || height == validResolutions[i]) + { + correctHeights.Add(validResolutions[i]); + break; + } + else if (height < validResolutions[i]) + { + correctHeights.Add(validResolutions[i - 1]); + break; + } + } + } + + //Converts the heights to string values and adds a "p" at the end. + foreach (int height in correctHeights.Reverse()) + resolutions.Add(height + "p"); + + return resolutions; + } + + public static async IAsyncEnumerable GetSearchResultsAsync(string searchTerm, int maxResults = 20) + { + if (VideoSearchCancelSource.IsCancellationRequested) + VideoSearchCancelSource = new(); + + int counter = 0; + await foreach (var result in youtube.Search.GetVideosAsync(searchTerm, VideoSearchCancelSource.Token)) + { + if (counter == maxResults) + break; + + yield return new SearchItemModel(result); + counter++; + } + } + public static CancellationTokenSource VideoSearchCancelSource { get; private set; } = new(); + + + private static string GetAudioConversionArgs() => AppSettings.Instance.DownloadsAudioFormat switch + { + AudioConversionFormat.Flac => "-acodec flac ", + AudioConversionFormat.Opus => "-acodec libopus ", + AudioConversionFormat.M4a => $"-acodec aac \"-bsf:a\" aac_adtstoasc -aq {GetAudioQuality("aac").ToString().Replace(',', '.')} ", + AudioConversionFormat.Mp3 => $"-acodec libmp3lame -aq {GetAudioQuality("libmp3lame").ToString().Replace(',', '.')} ", + AudioConversionFormat.Vorbis => $"-acodec libvorbis -aq {GetAudioQuality("libvorbis").ToString().Replace(',', '.')} ", + _ => $"-f {AppSettings.Instance.DownloadsAudioFormat.ToString().ToLower()} " + }; + + private static readonly Dictionary audioQualityDict = new() + { + { "libmp3lame", (10, 0) }, + { "libvorbis", (0, 10) }, // FFmpeg's AAC encoder does not have an upper limit for the value of -aq. // Experimentally, with values over 4, bitrate changes were minimal or non-existent { "aac", (0.1f, 4) } - }; + }; - //Preferred quality for audio-conversion (up to 10). - private const float preferredQuality = 5f; + //Preferred quality for audio-conversion (up to 10). + private const float preferredQuality = 5f; - private static float GetAudioQuality(string codec) - => audioQualityDict[codec].Item2 + (audioQualityDict[codec].Item1 - audioQualityDict[codec].Item2) * (preferredQuality / 10); - } + private static float GetAudioQuality(string codec) + => audioQualityDict[codec].Item2 + (audioQualityDict[codec].Item1 - audioQualityDict[codec].Item2) * (preferredQuality / 10); + } } diff --git a/OnionMedia/OnionMedia.Core/Models/StreamItemModel.cs b/OnionMedia/OnionMedia.Core/Models/StreamItemModel.cs index f0a737b..e534e2b 100644 --- a/OnionMedia/OnionMedia.Core/Models/StreamItemModel.cs +++ b/OnionMedia/OnionMedia.Core/Models/StreamItemModel.cs @@ -10,9 +10,11 @@ */ using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.Logging; using OnionMedia.Core.Classes; using OnionMedia.Core.Extensions; using OnionMedia.Core.Services; +using OnionMedia.Core.ViewModels; using System; using System.Collections.Generic; using System.Diagnostics; @@ -30,17 +32,28 @@ namespace OnionMedia.Core.Models [ObservableObject] public partial class StreamItemModel { + + ILogger logger; /// /// Initializes a new StreamItemModel. /// /// The video to get informations from. - public StreamItemModel(RunResult video) + public StreamItemModel(RunResult video, ILogger _logger) { + logger = _logger; if (video.Data == null) + { + logger.LogError("VideoData was null in StreamItemModel"); throw new ArgumentNullException("video.Data is null!"); + } + if (video.Data.IsLive == true) + { + logger.LogError("Tried to Download an Livestream in StreamItemModel"); throw new NotSupportedException("Livestreams cannot be downloaded."); + } + Video = video.Data; if (Video.Thumbnail.IsNullOrEmpty()) @@ -81,13 +94,13 @@ protected void OnProgressChanged(object sender, DownloadProgress e) downloadProgress = e; Match matchResult = null; if (!string.IsNullOrEmpty(e.Data)) - { - matchResult = Regex.Match(e.Data, GlobalResources.FFMPEGTIMEFROMOUTPUTREGEX); - if (!matchResult.Success) - { - downloadLog.AppendLine(e.Data); - } - } + { + matchResult = Regex.Match(e.Data, GlobalResources.FFMPEGTIMEFROMOUTPUTREGEX); + if (!matchResult.Success) + { + downloadLog.AppendLine(e.Data); + } + } if (matchResult?.Success is true) ProgressInfo.Progress = (int)(TimeSpan.Parse(matchResult.Value.Remove(0, 5)) / (TimeSpanGroup.EndTime - TimeSpanGroup.StartTime) * 100); @@ -123,10 +136,10 @@ private async Task UpdateProgressInfoAsync() Debug.WriteLine("Finish update task"); } - /// - /// Contains informations of the video - /// - public VideoData Video { get; } + /// + /// Contains informations of the video + /// + public VideoData Video { get; } public TimeSpan Duration { get; } public string? UploadDate => Video.UploadDate?.ToShortDateString(); public TimeSpanGroup TimeSpanGroup { get; } @@ -219,9 +232,9 @@ public void ShowToast() /// The height for the video to download /// public int GetVideoHeight => Convert.ToInt32(QualityLabel.Remove(QualityLabel.Length - 1)); - /// - /// The format for the video to download - /// + /// + /// The format for the video to download + /// public string Format => $"bestvideo[height<={GetVideoHeight}]+bestaudio[ext=m4a]/best[height<={GetVideoHeight}]/best"; /// diff --git a/OnionMedia/OnionMedia.Core/ViewModels/YouTubeDownloaderViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/YouTubeDownloaderViewModel.cs index 3880a71..691c6f9 100644 --- a/OnionMedia/OnionMedia.Core/ViewModels/YouTubeDownloaderViewModel.cs +++ b/OnionMedia/OnionMedia.Core/ViewModels/YouTubeDownloaderViewModel.cs @@ -245,7 +245,7 @@ private async Task FillInfosAsync(string videolink, bool allowPlaylists = true) return; } - var video = new StreamItemModel(data); + var video = new StreamItemModel(data, logger); video.Video.Url = videolink; if (Videos.Any(v => v.Video.ID == video.Video.ID)) @@ -459,7 +459,7 @@ private async Task> GetVideosAsync(IEnumerable videos, bool getMp finishedCount++; }; - tasks.Add(DownloaderMethods.DownloadStreamAsync(video, getMp4, path).ContinueWith(t => + tasks.Add(DownloaderMethods.DownloadStreamAsync(video, logger,getMp4, path).ContinueWith(t => { queue.Release(); if (t.Exception?.InnerException == null) return;