From 06f40e52adf9da7d0fe80692a226d27ba1f22579 Mon Sep 17 00:00:00 2001 From: Mateusz Skoczek Date: Sun, 3 Mar 2024 23:05:32 +0100 Subject: [PATCH] filename templates added --- .../en-US/FilenameTemplateResources.resw | 141 ++++++++++++++++++ .../Home/Helpers/VideoViewModel.cs | 9 +- .../Home/HomePlaylistViewModel.cs | 7 +- .../Home/HomeVideoViewModel.cs | 9 +- .../VDownload.Core.ViewModels.csproj | 1 + VDownload.Models/Video.cs | 1 + .../CommonConfiguration.cs | 3 + .../Models/FilenameTemplate.cs | 18 +++ .../Models/Tasks.cs | 3 + .../FilenameService.cs | 102 +++++++++++++ ...VDownload.Services.Utility.Filename.csproj | 14 ++ .../TwitchSearchService.cs | 2 + VDownload.sln | 19 +++ VDownload/App.xaml.cs | 3 + VDownload/VDownload.csproj | 1 + VDownload/configuration.json | 30 ++++ 16 files changed, 355 insertions(+), 8 deletions(-) create mode 100644 VDownload.Core/VDownload.Core.Strings/Strings/en-US/FilenameTemplateResources.resw create mode 100644 VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Configuration/Models/FilenameTemplate.cs create mode 100644 VDownload.Services/VDownload.Services.Utility/VDownload.Services.Utility.Filename/FilenameService.cs create mode 100644 VDownload.Services/VDownload.Services.Utility/VDownload.Services.Utility.Filename/VDownload.Services.Utility.Filename.csproj diff --git a/VDownload.Core/VDownload.Core.Strings/Strings/en-US/FilenameTemplateResources.resw b/VDownload.Core/VDownload.Core.Strings/Strings/en-US/FilenameTemplateResources.resw new file mode 100644 index 0000000..cc05822 --- /dev/null +++ b/VDownload.Core/VDownload.Core.Strings/Strings/en-US/FilenameTemplateResources.resw @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {author} = author + + + {date:<format>} = publish date in specified format + + + {duration:<format>} = duration in specified format + + + {id} = id (from url) + + + {source} = source + + + {title} = title + + + {views} = views + + \ No newline at end of file diff --git a/VDownload.Core/VDownload.Core.ViewModels/Home/Helpers/VideoViewModel.cs b/VDownload.Core/VDownload.Core.ViewModels/Home/Helpers/VideoViewModel.cs index 513cb3e..4bf3d75 100644 --- a/VDownload.Core/VDownload.Core.ViewModels/Home/Helpers/VideoViewModel.cs +++ b/VDownload.Core/VDownload.Core.ViewModels/Home/Helpers/VideoViewModel.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using VDownload.Models; using VDownload.Services.Data.Settings; using VDownload.Services.UI.StoragePicker; +using VDownload.Services.Utility.Filename; namespace VDownload.Core.ViewModels.Home.Helpers { @@ -19,6 +20,7 @@ namespace VDownload.Core.ViewModels.Home.Helpers protected readonly ISettingsService _settingsService; protected readonly IStoragePickerService _storagePickerService; + protected readonly IFilenameService _filenameService; #endregion @@ -80,10 +82,11 @@ namespace VDownload.Core.ViewModels.Home.Helpers #region CONSTRUCTORS - public VideoViewModel(Video video, ISettingsService settingsService, IStoragePickerService storagePickerService) + public VideoViewModel(Video video, ISettingsService settingsService, IStoragePickerService storagePickerService, IFilenameService filenameService) { _settingsService = settingsService; _storagePickerService = storagePickerService; + _filenameService = filenameService; Video = video; @@ -100,7 +103,7 @@ namespace VDownload.Core.ViewModels.Home.Helpers TrimStart = TimeSpan.Zero; TrimEnd = Duration; DirectoryPath = _settingsService.Data.Common.Tasks.DefaultOutputDirectory; - Filename = Title.Length > 50 ? Title.Substring(0, 50) : Title; + Filename = _filenameService.CreateFilename(_settingsService.Data.Common.Tasks.FilenameTemplate, video); VideoExtension = _settingsService.Data.Common.Tasks.DefaultVideoExtension; AudioExtension = _settingsService.Data.Common.Tasks.DefaultAudioExtension; } @@ -120,7 +123,7 @@ namespace VDownload.Core.ViewModels.Home.Helpers TrimStart = this.TrimStart, TrimEnd = this.TrimEnd, Directory = this.DirectoryPath, - Filename = string.Join("_", this.Filename.Split(Path.GetInvalidFileNameChars())), + Filename = _filenameService.SanitizeFilename(this.Filename), Extension = (this.MediaType == MediaType.OnlyAudio ? this.AudioExtension.ToString() : this.VideoExtension.ToString()).ToLower(), }; } diff --git a/VDownload.Core/VDownload.Core.ViewModels/Home/HomePlaylistViewModel.cs b/VDownload.Core/VDownload.Core.ViewModels/Home/HomePlaylistViewModel.cs index 921901e..f509d70 100644 --- a/VDownload.Core/VDownload.Core.ViewModels/Home/HomePlaylistViewModel.cs +++ b/VDownload.Core/VDownload.Core.ViewModels/Home/HomePlaylistViewModel.cs @@ -16,6 +16,7 @@ using VDownload.Services.UI.StoragePicker; using VDownload.Sources.Twitch.Configuration.Models; using SimpleToolkit.MVVM; using System.Text.RegularExpressions; +using VDownload.Services.Utility.Filename; namespace VDownload.Core.ViewModels.Home { @@ -27,6 +28,7 @@ namespace VDownload.Core.ViewModels.Home protected readonly ISettingsService _settingsService; protected readonly IStoragePickerService _storagePickerService; + protected readonly IFilenameService _filenameService; #endregion @@ -172,11 +174,12 @@ namespace VDownload.Core.ViewModels.Home #region CONSTRUCTORS - public HomePlaylistViewModel(IDownloadTaskManager tasksManager, ISettingsService settingsService, IStoragePickerService storagePickerService) + public HomePlaylistViewModel(IDownloadTaskManager tasksManager, ISettingsService settingsService, IStoragePickerService storagePickerService, IFilenameService filenameService) { _tasksManager = tasksManager; _settingsService = settingsService; _storagePickerService = storagePickerService; + _filenameService = filenameService; _removedVideos = new List(); @@ -221,7 +224,7 @@ namespace VDownload.Core.ViewModels.Home Videos.Clear(); foreach (Video video in playlist) { - Videos.Add(new VideoViewModel(video, _settingsService, _storagePickerService), true); + Videos.Add(new VideoViewModel(video, _settingsService, _storagePickerService, _filenameService), true); } UpdateFilter(); } diff --git a/VDownload.Core/VDownload.Core.ViewModels/Home/HomeVideoViewModel.cs b/VDownload.Core/VDownload.Core.ViewModels/Home/HomeVideoViewModel.cs index 8fb5a4e..be75bed 100644 --- a/VDownload.Core/VDownload.Core.ViewModels/Home/HomeVideoViewModel.cs +++ b/VDownload.Core/VDownload.Core.ViewModels/Home/HomeVideoViewModel.cs @@ -11,6 +11,7 @@ using VDownload.Core.Tasks; using VDownload.Models; using VDownload.Services.Data.Settings; using VDownload.Services.UI.StoragePicker; +using VDownload.Services.Utility.Filename; namespace VDownload.Core.ViewModels.Home { @@ -22,6 +23,7 @@ namespace VDownload.Core.ViewModels.Home protected readonly ISettingsService _settingsService; protected readonly IStoragePickerService _storagePickerService; + protected readonly IFilenameService _filenameService; #endregion @@ -89,11 +91,12 @@ namespace VDownload.Core.ViewModels.Home #region CONSTRUCTORS - public HomeVideoViewModel(IDownloadTaskManager tasksManager, ISettingsService settingsService, IStoragePickerService storagePickerService) + public HomeVideoViewModel(IDownloadTaskManager tasksManager, ISettingsService settingsService, IStoragePickerService storagePickerService, IFilenameService filenameService) { _tasksManager = tasksManager; _settingsService = settingsService; _storagePickerService = storagePickerService; + _filenameService = filenameService; } #endregion @@ -121,7 +124,7 @@ namespace VDownload.Core.ViewModels.Home TrimStart = TimeSpan.Zero; TrimEnd = Duration; DirectoryPath = _settingsService.Data.Common.Tasks.DefaultOutputDirectory; - Filename = Title.Length > 50 ? Title.Substring(0, 50) : Title; + Filename = _filenameService.CreateFilename(_settingsService.Data.Common.Tasks.FilenameTemplate, video); VideoExtension = _settingsService.Data.Common.Tasks.DefaultVideoExtension; AudioExtension = _settingsService.Data.Common.Tasks.DefaultAudioExtension; } @@ -167,7 +170,7 @@ namespace VDownload.Core.ViewModels.Home TrimStart = this.TrimStart, TrimEnd = this.TrimEnd, Directory = this.DirectoryPath, - Filename = string.Join("_", this.Filename.Split(Path.GetInvalidFileNameChars())), + Filename = _filenameService.SanitizeFilename(this.Filename), Extension = (this.MediaType == MediaType.OnlyAudio ? this.AudioExtension.ToString() : this.VideoExtension.ToString()).ToLower(), }; } diff --git a/VDownload.Core/VDownload.Core.ViewModels/VDownload.Core.ViewModels.csproj b/VDownload.Core/VDownload.Core.ViewModels/VDownload.Core.ViewModels.csproj index b3a0746..bf7da03 100644 --- a/VDownload.Core/VDownload.Core.ViewModels/VDownload.Core.ViewModels.csproj +++ b/VDownload.Core/VDownload.Core.ViewModels/VDownload.Core.ViewModels.csproj @@ -24,6 +24,7 @@ + diff --git a/VDownload.Models/Video.cs b/VDownload.Models/Video.cs index 816e394..348565c 100644 --- a/VDownload.Models/Video.cs +++ b/VDownload.Models/Video.cs @@ -10,6 +10,7 @@ namespace VDownload.Models { #region PROPERTIES + public string Id { get; set; } public string Title { get; set; } public string Author { get; set; } public DateTime PublishDate { get; set; } diff --git a/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Configuration/CommonConfiguration.cs b/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Configuration/CommonConfiguration.cs index 16296f3..c3001d5 100644 --- a/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Configuration/CommonConfiguration.cs +++ b/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Configuration/CommonConfiguration.cs @@ -6,6 +6,9 @@ namespace VDownload.Services.Data.Configuration { public class CommonConfiguration { + [ConfigurationKeyName("filename_templates")] + public IEnumerable FilenameTemplates { get; set; } + [ConfigurationKeyName("path")] public Models.Path Path { get; set; } diff --git a/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Configuration/Models/FilenameTemplate.cs b/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Configuration/Models/FilenameTemplate.cs new file mode 100644 index 0000000..49738dd --- /dev/null +++ b/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Configuration/Models/FilenameTemplate.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VDownload.Services.Data.Configuration.Models +{ + public class FilenameTemplate + { + [ConfigurationKeyName("name")] + public string Name { get; set; } + + [ConfigurationKeyName("wildcard")] + public string Wildcard { get; set; } + } +} diff --git a/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Settings/Models/Tasks.cs b/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Settings/Models/Tasks.cs index 69027ba..dd46c0e 100644 --- a/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Settings/Models/Tasks.cs +++ b/VDownload.Services/VDownload.Services.Data/VDownload.Services.Data.Settings/Models/Tasks.cs @@ -19,6 +19,9 @@ namespace VDownload.Services.Data.Settings.Models [JsonProperty("default_audio_extension")] public AudioExtension DefaultAudioExtension { get; set; } = AudioExtension.MP3; + [JsonProperty("filename_template")] + public string FilenameTemplate { get; set; } = "{title}"; + [JsonProperty("default_output_directory")] public string DefaultOutputDirectory { get; set; } = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); diff --git a/VDownload.Services/VDownload.Services.Utility/VDownload.Services.Utility.Filename/FilenameService.cs b/VDownload.Services/VDownload.Services.Utility/VDownload.Services.Utility.Filename/FilenameService.cs new file mode 100644 index 0000000..f69a246 --- /dev/null +++ b/VDownload.Services/VDownload.Services.Utility/VDownload.Services.Utility.Filename/FilenameService.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using VDownload.Models; +using VDownload.Services.Data.Configuration; +using VDownload.Services.Data.Configuration.Models; + +namespace VDownload.Services.Utility.Filename +{ + public interface IFilenameService + { + string CreateFilename(string template, Video video); + string SanitizeFilename(string filename); + } + + + + public class FilenameService : IFilenameService + { + #region SERVICES + + protected readonly IConfigurationService _configurationService; + + #endregion + + + + #region CONSTRUCTORS + + public FilenameService(IConfigurationService configurationService) + { + _configurationService = configurationService; + } + + #endregion + + + + #region PUBLIC METHODS + + public string CreateFilename(string template, Video video) + { + string filename = template; + foreach (FilenameTemplate templateElement in _configurationService.Common.FilenameTemplates) + { + switch (templateElement.Name) + { + case "id": ReplaceInPlace(ref filename, templateElement.Wildcard, video.Id); break; + case "title": ReplaceInPlace(ref filename, templateElement.Wildcard, video.Title); break; + case "author": ReplaceInPlace(ref filename, templateElement.Wildcard, video.Author); break; + case "views": ReplaceInPlace(ref filename, templateElement.Wildcard, video.Views.ToString()); break; + case "source": ReplaceInPlace(ref filename, templateElement.Wildcard, video.Source.ToString()); break; + case "date": + { + Regex regex = new Regex(templateElement.Wildcard); + foreach (Match match in regex.Matches(filename)) + { + ReplaceInPlace(ref filename, match.Value, video.PublishDate.ToString(match.Groups[1].Value)); + } + break; + } + case "duration": + { + Regex regex = new Regex(templateElement.Wildcard); + foreach (Match match in regex.Matches(filename)) + { + ReplaceInPlace(ref filename, match.Value, video.Duration.ToString(match.Groups[1].Value)); + } + break; + } + default: throw new Exception("Invalid template"); + } + } + filename = SanitizeFilename(filename); + return filename; + } + + public string SanitizeFilename(string filename) + { + char[] invalidChars = System.IO.Path.GetInvalidFileNameChars(); + foreach (char c in invalidChars) + { + filename = filename.Replace(c, '_'); + } + return filename; + } + + #endregion + + + + #region PRIVATE METHODS + + protected void ReplaceInPlace(ref string filename, string oldValue, string newValue) => filename = filename.Replace(oldValue, newValue); + + #endregion + } +} diff --git a/VDownload.Services/VDownload.Services.Utility/VDownload.Services.Utility.Filename/VDownload.Services.Utility.Filename.csproj b/VDownload.Services/VDownload.Services.Utility/VDownload.Services.Utility.Filename/VDownload.Services.Utility.Filename.csproj new file mode 100644 index 0000000..69c8a87 --- /dev/null +++ b/VDownload.Services/VDownload.Services.Utility/VDownload.Services.Utility.Filename/VDownload.Services.Utility.Filename.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/VDownload.Sources/VDownload.Sources.Twitch/VDownload.Sources.Twitch/TwitchSearchService.cs b/VDownload.Sources/VDownload.Sources.Twitch/VDownload.Sources.Twitch/TwitchSearchService.cs index 8d40d3a..1604915 100644 --- a/VDownload.Sources/VDownload.Sources.Twitch/VDownload.Sources.Twitch/TwitchSearchService.cs +++ b/VDownload.Sources/VDownload.Sources.Twitch/VDownload.Sources.Twitch/TwitchSearchService.cs @@ -176,6 +176,7 @@ namespace VDownload.Sources.Twitch } TwitchVod vod = new TwitchVod { + Id = data.Id, Title = data.Title, Description = data.Description, Author = data.UserName, @@ -201,6 +202,7 @@ namespace VDownload.Sources.Twitch TwitchClip clip = new TwitchClip { + Id = data.Id, Title = data.Title, Author = data.BroadcasterName, Creator = data.CreatorName, diff --git a/VDownload.sln b/VDownload.sln index 72527bc..93ba10b 100644 --- a/VDownload.sln +++ b/VDownload.sln @@ -69,6 +69,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VDownload.Core.Tasks", "VDo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VDownload.Services.Utility.FFmpeg", "VDownload.Services\VDownload.Services.Utility\VDownload.Services.Utility.FFmpeg\VDownload.Services.Utility.FFmpeg.csproj", "{A3166F8A-ECAD-4D4B-9BE3-96FEC799B27B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VDownload.Services.Utility.Filename", "VDownload.Services\VDownload.Services.Utility\VDownload.Services.Utility.Filename\VDownload.Services.Utility.Filename.csproj", "{4647EFB5-A206-4F47-976D-BAED11B52579}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -503,6 +505,22 @@ Global {A3166F8A-ECAD-4D4B-9BE3-96FEC799B27B}.Release|x64.Build.0 = Release|Any CPU {A3166F8A-ECAD-4D4B-9BE3-96FEC799B27B}.Release|x86.ActiveCfg = Release|Any CPU {A3166F8A-ECAD-4D4B-9BE3-96FEC799B27B}.Release|x86.Build.0 = Release|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Debug|ARM64.Build.0 = Debug|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Debug|x64.ActiveCfg = Debug|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Debug|x64.Build.0 = Debug|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Debug|x86.ActiveCfg = Debug|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Debug|x86.Build.0 = Debug|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Release|Any CPU.Build.0 = Release|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Release|ARM64.ActiveCfg = Release|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Release|ARM64.Build.0 = Release|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Release|x64.ActiveCfg = Release|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Release|x64.Build.0 = Release|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Release|x86.ActiveCfg = Release|Any CPU + {4647EFB5-A206-4F47-976D-BAED11B52579}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -536,6 +554,7 @@ Global {E470FCE2-DB0D-4771-8C9D-43D8AB85FB80} = {8539067C-9968-4AEB-928C-FEDC43989A79} {3BE998A3-1126-4496-BF60-80D0CEA4D24F} = {8539067C-9968-4AEB-928C-FEDC43989A79} {A3166F8A-ECAD-4D4B-9BE3-96FEC799B27B} = {1020167A-4101-496E-82CF-41B65769DD28} + {4647EFB5-A206-4F47-976D-BAED11B52579} = {1020167A-4101-496E-82CF-41B65769DD28} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9FD7B842-C3E2-4FD0-AD8A-C8E619280AB7} diff --git a/VDownload/App.xaml.cs b/VDownload/App.xaml.cs index ed735f6..2e9b054 100644 --- a/VDownload/App.xaml.cs +++ b/VDownload/App.xaml.cs @@ -16,6 +16,7 @@ using VDownload.Core.Views.Home; using VDownload.Core.Views.Settings; using VDownload.Services.Data.Authentication; using VDownload.Services.Data.Configuration; +using VDownload.Services.Data.Configuration.Models; using VDownload.Services.Data.Settings; using VDownload.Services.UI.Dialogs; using VDownload.Services.UI.DictionaryResources; @@ -25,6 +26,7 @@ using VDownload.Services.UI.StringResources; using VDownload.Services.UI.WebView; using VDownload.Services.Utility.Encryption; using VDownload.Services.Utility.FFmpeg; +using VDownload.Services.Utility.Filename; using VDownload.Services.Utility.HttpClient; using VDownload.Sources; using VDownload.Sources.Twitch; @@ -119,6 +121,7 @@ namespace VDownload services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } protected void BuildSourcesServices(IServiceCollection services) diff --git a/VDownload/VDownload.csproj b/VDownload/VDownload.csproj index e57000c..8a756cf 100644 --- a/VDownload/VDownload.csproj +++ b/VDownload/VDownload.csproj @@ -181,6 +181,7 @@ + diff --git a/VDownload/configuration.json b/VDownload/configuration.json index 83f88b7..1f953b2 100644 --- a/VDownload/configuration.json +++ b/VDownload/configuration.json @@ -1,5 +1,35 @@ { "common": { + "filename_templates": [ + { + "name": "id", + "wildcard": "{id}" + }, + { + "name": "title", + "wildcard": "{title}" + }, + { + "name": "author", + "wildcard": "{author}" + }, + { + "name": "views", + "wildcard": "{views}" + }, + { + "name": "source", + "wildcard": "{source}" + }, + { + "name": "date", + "wildcard": "{date:(.+)}" + }, + { + "name": "duration", + "wildcard": "{duration:(.+)}" + } + ], "processing": { "muxers": [ {