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/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/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/Models/AppSettings.cs b/OnionMedia/OnionMedia.Core/Models/AppSettings.cs
index 66bcdb8..3d30bf9 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,12 @@ 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.MyDocuments), "OnionMedia", "Logs"); ;
+ deleteLogInterval = settingsService.GetSetting("deleteLogInterval") as byte? ?? 7;
+
var downloadsAudioFormat = settingsService.GetSetting("downloadsAudioFormat");
if (downloadsAudioFormat == null)
this.downloadsAudioFormat = AudioConversionFormat.Mp3;
@@ -93,6 +99,25 @@ private AppSettings()
public static AppFlowDirection[] FlowDirections { get; } = Enum.GetValues().ToArray();
//Settings
+
+ public byte DeleteLogInterval
+ {
+ get => deleteLogInterval;
+ set => SetSetting(ref deleteLogInterval, value,"deleteLogInterval");
+ }
+ private byte deleteLogInterval;
+ 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 +392,8 @@ public enum PathType
ConvertedVideofiles,
ConvertedAudiofiles,
DownloadedVideofiles,
- DownloadedAudiofiles
+ DownloadedAudiofiles,
+ LogPath
}
public enum StartPageType
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/OnionMedia.Core.csproj b/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj
index 96aa185..31527b1 100644
--- a/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj
+++ b/OnionMedia/OnionMedia.Core/OnionMedia.Core.csproj
@@ -9,7 +9,9 @@
-
+
+
+
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 cb4bd32..f4b98ef 100644
--- a/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs
+++ b/OnionMedia/OnionMedia.Core/ViewModels/MediaViewModel.cs
@@ -27,14 +27,17 @@
using OnionMedia.Core.Extensions;
using OnionMedia.Core.Models;
using OnionMedia.Core.Services;
-
+using Microsoft.Extensions.Logging;
+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)
+ private readonly ILogger logger;
+ 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));
this.conversionPresetDialog = conversionPresetDialog ?? throw new ArgumentNullException(nameof(conversionPresetDialog));
this.filetagEditorDialog = filetagEditorDialog ?? throw new ArgumentNullException(nameof(filetagEditorDialog));
@@ -96,7 +99,6 @@ public MediaViewModel(IDialogService dialogService, IDispatcherService dispatche
OnPropertyChanged(nameof(SelectedItem));
});
}
-
private readonly IDialogService dialogService;
private readonly IConversionPresetDialog conversionPresetDialog;
private readonly IFiletagEditorDialog filetagEditorDialog;
@@ -106,16 +108,29 @@ public MediaViewModel(IDialogService dialogService, IDispatcherService dispatche
private static readonly IPathProvider pathProvider = IoC.Default.GetService() ?? throw new ArgumentNullException();
+
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)));
}
@@ -123,10 +138,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.
@@ -134,17 +153,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));
@@ -265,11 +292,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;
@@ -292,6 +324,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));
@@ -309,24 +343,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;
@@ -336,6 +389,7 @@ private async Task EditConversionPresetAsync(ConversionPreset conversionPreset)
private async Task DeleteConversionPresetAsync(ConversionPreset conversionPreset)
{
+
if (conversionPreset == null)
throw new ArgumentNullException(nameof(conversionPreset));
@@ -347,7 +401,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)
@@ -358,6 +417,7 @@ private async Task DeleteConversionPresetAsync(ConversionPreset conversionPreset
private async Task ConvertFilesAsync()
{
+
if (SelectedConversionPreset == null)
throw new Exception("SelectedConversionPreset is null.");
@@ -366,7 +426,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;
@@ -404,6 +469,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))
@@ -417,19 +484,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;
}
}));
@@ -440,6 +515,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
@@ -447,48 +524,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;
@@ -497,17 +586,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;
}
@@ -518,11 +615,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");
}
}
@@ -531,6 +632,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..c787ded 100644
--- a/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs
+++ b/OnionMedia/OnionMedia.Core/ViewModels/SettingsViewModel.cs
@@ -19,14 +19,20 @@
using OnionMedia.Core.Models;
using OnionMedia.Core.Services;
using OnionMedia.Core.Extensions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
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 +74,35 @@ private async Task ChangePathAsync(PathType pathType)
case PathType.DownloadedAudiofiles:
AppSettings.Instance.DownloadsAudioSavePath = path;
break;
+ case PathType.LogPath:
+ AppSettings.Instance.LogPath = path;
+ break;
+ }
+ }
+
+ [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;
}
}
@@ -103,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.Core/ViewModels/YouTubeDownloaderViewModel.cs b/OnionMedia/OnionMedia.Core/ViewModels/YouTubeDownloaderViewModel.cs
index 35fbd74..691c6f9 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;
}
@@ -232,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))
@@ -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
@@ -444,13 +459,17 @@ 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;
}
@@ -559,7 +578,7 @@ private async Task DownloadVideosAsync(IList 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;
@@ -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 e8b65ea..48c5119 100644
--- a/OnionMedia/OnionMedia/App.xaml.cs
+++ b/OnionMedia/OnionMedia/App.xaml.cs
@@ -40,6 +40,8 @@
using Microsoft.UI.Windowing;
using Microsoft.UI.Composition.SystemBackdrops;
using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+using Windows.Services.Maps;
// To learn more about WinUI3, see: https://docs.microsoft.com/windows/apps/winui/winui3/.
namespace OnionMedia
@@ -57,14 +59,14 @@ 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)
{
// 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/OnionMedia.csproj b/OnionMedia/OnionMedia/OnionMedia.csproj
index 4776b3f..787c413 100644
--- a/OnionMedia/OnionMedia/OnionMedia.csproj
+++ b/OnionMedia/OnionMedia/OnionMedia.csproj
@@ -10,6 +10,9 @@
true
10.0.18362.0
+
+ true
+
@@ -107,12 +110,17 @@
-
+
+
+
+
-
+
+
+
diff --git a/OnionMedia/OnionMedia/ServiceProvider.cs b/OnionMedia/OnionMedia/ServiceProvider.cs
index 31791a4..d157947 100644
--- a/OnionMedia/OnionMedia/ServiceProvider.cs
+++ b/OnionMedia/OnionMedia/ServiceProvider.cs
@@ -9,17 +9,34 @@
using OnionMedia.Services;
using OnionMedia.ViewModels;
using OnionMedia.Views;
+using Microsoft.Extensions.Logging;
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Serilog;
+using System.IO;
+using Windows.Storage;
+using System.Threading;
namespace OnionMedia;
//Services/Activation Handler
[ServiceProvider]
+
+//Logging
+[Singleton(typeof(ILoggerFactory), Factory = nameof(CreateLoggerFactory))]
+[Singleton(typeof(Serilog.ILogger), Factory = nameof(CreateSerilogLogger))]
+[Transient(typeof(ILogger<>), Factory = nameof(CreateLogger))]
+
+
+
+
[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 +57,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 +69,56 @@ 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 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();
+ if (AppSettings.Instance.UseLogging)
+ {
+ return new LoggerConfiguration()
+ .MinimumLevel.Debug()
+ .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.GetService();
+ return factory.CreateLogger();
+ }
+
+ private static ILoggerFactory CreateLoggerFactory()
+ {
+ var logger = CreateSerilogLogger();
+ return LoggerFactory.Create(builder =>
+ {
+ 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 >= AppSettings.Instance.DeleteLogInterval)
+ {
+ File.Delete(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..424abfc 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 ♥
@@ -198,6 +201,9 @@
Downloadgeschwindigkeit limitieren
+
+ Protokoll:
+
Max. Downloadgeschwindigkeit pro Datei:
@@ -234,6 +240,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..1940bf1 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 ♥
@@ -198,6 +201,9 @@
Limit download speed
+
+ Log:
+
Max. download speed per file:
@@ -234,6 +240,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..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 ♥
@@ -198,6 +201,9 @@
Limitar la velocidad de descarga
+
+ Protocolo:
+
Velocidad máxima de descarga por archivo:
@@ -234,6 +240,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..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 ♥
@@ -198,6 +201,9 @@
Limiteer de downloadsnelheid
+
+ Log:
+
Max. downloadsnelheid per bestand:
@@ -234,7 +240,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..5188ac9 100644
--- a/OnionMedia/OnionMedia/Views/SettingsPage.xaml
+++ b/OnionMedia/OnionMedia/Views/SettingsPage.xaml
@@ -96,6 +96,11 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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;