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": [
{