diff --git a/VkAudioDownloader.CLI/LaunchArgument.cs b/VkAudioDownloader.CLI/LaunchArgument.cs new file mode 100644 index 0000000..cdbba4f --- /dev/null +++ b/VkAudioDownloader.CLI/LaunchArgument.cs @@ -0,0 +1,44 @@ +using System; +using System.Text; +using DTLib.Extensions; + +namespace VkAudioDownloader.CLI; + +public class LaunchArgument +{ + public string[] Aliases; + public string Description; + public string? ParamName; + public Action? Handler; + public Action? HandlerWithArg; + + private LaunchArgument(string[] aliases, string description) + { + Aliases = aliases; + Description = description; + } + + public LaunchArgument(string[] aliases, string description, Action handler) + : this(aliases, description) => Handler = handler; + + public LaunchArgument(string[] aliases, string description, Action handler, string paramName) + : this(aliases, description) + { + HandlerWithArg = handler; + ParamName = paramName; + } + + public StringBuilder AppendHelpInfo(StringBuilder b) + { + b.Append(Aliases[0]); + for (int i = 1; i < Aliases.Length; i++) + b.Append(", ").Append(Aliases[i]); + if (!String.IsNullOrEmpty(ParamName)) + b.Append(" [").Append(ParamName).Append(']'); + b.Append(" - ").Append(Description); + return b; + } + + public override string ToString() => + $"{{{{{Aliases.MergeToString(", ")}}}, Handler: {Handler is null}, HandlerWithArg: {HandlerWithArg is null}}}"; +} \ No newline at end of file diff --git a/VkAudioDownloader.CLI/LaunchArgumentParser.cs b/VkAudioDownloader.CLI/LaunchArgumentParser.cs new file mode 100644 index 0000000..a0607e3 --- /dev/null +++ b/VkAudioDownloader.CLI/LaunchArgumentParser.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace VkAudioDownloader.CLI; + +public class LaunchArgumentParser +{ + private Dictionary argDict = new(); + private List argList = new(); + public bool ExitIfNoArgs = true; + + public class ExitAfterHelpException : Exception + { + internal ExitAfterHelpException() : base("your program can use this exception to exit after displaying help message") + { } + } + + public string CreateHelpMessage() + { + StringBuilder b = new(); + foreach (var arg in argList) + arg.AppendHelpInfo(b).Append('\n'); + b.Remove(b.Length-1, 1); + return b.ToString(); + } + public string CreateHelpArgMessage(string argAlias) + { + StringBuilder b = new(); + var arg = Get(argAlias); + arg.AppendHelpInfo(b); + return b.ToString(); + } + private void HelpHandler() + { + Console.WriteLine(CreateHelpMessage()); + throw new ExitAfterHelpException(); + } + + private void HelpArgHandler(string argAlias) + { + Console.WriteLine(CreateHelpArgMessage(argAlias)); + throw new ExitAfterHelpException(); + } + + + public LaunchArgumentParser() + { + var help = new LaunchArgument(new[] { "h", "help" }, + "shows help message", HelpHandler); + Add(help); + var helpArg = new LaunchArgument( new[]{ "ha", "helparg" }, + "shows help message for particular argument", + HelpArgHandler, "argAlias"); + Add(helpArg); + } + + public LaunchArgumentParser(ICollection arguments) : this() + { + foreach (var arg in arguments) + Add(arg); + } + public LaunchArgumentParser(params LaunchArgument[] arguments) : this() + { + for (var i = 0; i < arguments.Length; i++) + Add(arguments[i]); + } + + public void Add(LaunchArgument arg) + { + argList.Add(arg); + for(int a=0; aprogram launch args + /// argument {args[i]} should have a parameter after it + /// argument hasn't got any handlers + /// happens after help message is displayed + public void ParseAndHandle(string[] args) + { + if (args.Length == 0 && ExitIfNoArgs) + { + HelpHandler(); + } + + for (int i = 0; i < args.Length; i++) + { + LaunchArgument arg = Get(args[i]); + + if (arg.HandlerWithArg is not null) + { + if (i+1 >= args.Length) + throw new Exception($"argument <{args[i]}> should have a parameter after it"); + arg.HandlerWithArg(args[++i]); + } + else if (arg.Handler is not null) + arg.Handler(); + else throw new NullReferenceException($"argument <{args[i]}> hasn't got any handlers"); + } + } +} \ No newline at end of file diff --git a/VkAudioDownloader.CLI/Program.cs b/VkAudioDownloader.CLI/Program.cs index f4fc643..3942187 100644 --- a/VkAudioDownloader.CLI/Program.cs +++ b/VkAudioDownloader.CLI/Program.cs @@ -5,10 +5,13 @@ using System.IO; using DTLib.Extensions; using VkAudioDownloader; using DTLib.Logging.New; +using VkAudioDownloader.CLI; Console.InputEncoding = StringConverter.UTF8; Console.OutputEncoding = StringConverter.UTF8; +LaunchArgumentParser argParser = new LaunchArgumentParser(); + if(!File.Exists("config.dtsod")) { File.Copy("config.dtsod.default", "config.dtsod"); @@ -19,38 +22,48 @@ var config = VkClientConfig.FromDtsod(new DtsodV23(File.ReadAllText("config.dtso var logger = new CompositeLogger(new DefaultLogFormat(true), new ConsoleLogger(), - new FileLogger("logs", "VkAudioDownloaer")); -var _logger = new ContextLogger(logger, "Main"); -_logger.LogDebug("DEBUG LOG ENABLED"); + new FileLogger("logs", "VkAudioDownloaer"), + new FileLogger("logs", "VkAudioDownloaer_debug") { DebugLogEnabled = true}); +var mainLoggerContext = new ContextLogger(logger, "Main"); +mainLoggerContext.LogDebug("DEBUG LOG ENABLED"); try { #if DEBUG + // checking correctness of my aes-128 decryptor on current platform VkAudioDownloader.Helpers.AudioAesDecryptor.TestAes(); #endif - - _logger.LogInfo("initializing api..."); + + mainLoggerContext.LogInfo("initializing api..."); var client = new VkClient(config, logger); await client.ConnectAsync(); - // getting audio from vk - var audios = client.FindAudio("сталинский костюм").ToArray(); - - for (var i = 0; i < audios.Length; i++) + argParser.Add(new LaunchArgument(new []{"s", "search"}, "search audio on vk.com", SearchAudio, "query")); + argParser.ParseAndHandle(args); + + void SearchAudio(string query) { - var a = audios[i]; - Console.WriteLine($"[{i}] {a.AudioToString()}"); + var audios = client.FindAudio(query).ToArray(); + for (var i = 0; i < audios.Length; i++) + { + var a = audios[i]; + Console.WriteLine($"[{i}] {a.AudioToString()}"); + } + Console.Write("choose audio: "); + int ain = Convert.ToInt32(Console.ReadLine()); + var audio = audios[ain]; + Console.WriteLine($"selected {audio.AudioToString()}"); + + string downloadedFile = client.DownloadAudioAsync(audio, "downloads").GetAwaiter().GetResult(); + mainLoggerContext.LogInfo($"audio {audio.AudioToString()} downloaded to {downloadedFile}"); } - - Console.Write("choose audio: "); - int ain = Convert.ToInt32(Console.ReadLine()); - var audio = audios[ain]; - Console.WriteLine($"selected \"{audio.Title}\" -- {audio.Artist} [{TimeSpan.FromSeconds(audio.Duration)}]"); - // downloading parts - string downloadedFile = await client.DownloadAudioAsync(audio, "downloads"); - _logger.LogInfo($"audio {audio.AudioToString()} downloaded to {downloadedFile}"); +} +catch (LaunchArgumentParser.ExitAfterHelpException) +{ + } catch (Exception ex) { - _logger.LogError(ex); + mainLoggerContext.LogError(ex); } +Console.ResetColor(); diff --git a/VkAudioDownloader/Helpers/FFMPegHelper.cs b/VkAudioDownloader/Helpers/FFMPegHelper.cs index 224bf9e..69073a1 100644 --- a/VkAudioDownloader/Helpers/FFMPegHelper.cs +++ b/VkAudioDownloader/Helpers/FFMPegHelper.cs @@ -61,8 +61,7 @@ public class FFMPegHelper opusFile // output }) // ffmpeg prints all log to stderr, because in stdout it ptints encoded file - .WithStandardErrorPipe(PipeTarget.ToDelegate( - msg => _logger.LogWarn(msg))); + .WithStandardErrorPipe(PipeTarget.ToDelegate(StdErrHandle)); tasks[i] = command.ExecuteAsync(); output[i] = opusFile; @@ -72,6 +71,13 @@ public class FFMPegHelper return output; } + protected void StdErrHandle(string msg) + { + if(msg.EndsWith("start time for stream 1 is not set in estimate_timings_from_pts")) + _logger.LogDebug(msg); + else _logger.LogWarn(msg); + } + public async Task Concat(string outfile, string fragmentListFile, string codec="libopus") { _logger.LogDebug($"{fragmentListFile} -> {outfile}"); diff --git a/VkAudioDownloader/VkAudioDownloader.csproj b/VkAudioDownloader/VkAudioDownloader.csproj index 29b2be2..677c81c 100644 --- a/VkAudioDownloader/VkAudioDownloader.csproj +++ b/VkAudioDownloader/VkAudioDownloader.csproj @@ -24,6 +24,6 @@ - + diff --git a/VkAudioDownloader/VkClient.cs b/VkAudioDownloader/VkClient.cs index ad43f3d..f712090 100644 --- a/VkAudioDownloader/VkClient.cs +++ b/VkAudioDownloader/VkClient.cs @@ -24,15 +24,15 @@ public class VkClient : IDisposable public VkApi Api; public VkClientConfig Config; private DTLib.Logging.New.ContextLogger _logger; - private HttpHelper _http; - private FFMPegHelper _ffmpeg; + public HttpHelper Http; + public FFMPegHelper Ffmpeg; public VkClient(VkClientConfig conf, DTLib.Logging.New.ILogger logger) { Config = conf; _logger = new ContextLogger(logger, nameof(VkClient)); - _http = new HttpHelper(); - _ffmpeg = new FFMPegHelper(logger,conf.FFMPegDir); + Http = new HttpHelper(); + Ffmpeg = new FFMPegHelper(logger,conf.FfmpegDir); var services = new ServiceCollection() .Add(new LoggerService(logger)) .AddAudioBypass(); @@ -44,7 +44,7 @@ public class VkClient : IDisposable var authParams = new ApiAuthParams { ApplicationId = Config.AppId, - Settings = Settings.Audio + Settings = Settings.Audio, }; if (Config.Token is not null) { @@ -79,10 +79,11 @@ public class VkClient : IDisposable Count = maxRezults, }); - + ///file name public Task DownloadAudioAsync(Audio audio, string localDir) => DownloadAudioAsync(audio, localDir,TimeSpan.FromHours(1)); + ///file name public async Task DownloadAudioAsync(Audio audio, string localDir, TimeSpan durationLimit) { if (!audio.Url.ToString().StartsWith("http")) @@ -93,16 +94,16 @@ public class VkClient : IDisposable if(File.Exists(outFile)) _logger.LogWarn( $"file {outFile} already exists"); - string m3u8 = await _http.GetStringAsync(audio.Url); + string m3u8 = await Http.GetStringAsync(audio.Url); var parser = new M3U8Parser(); var hls = parser.Parse(audio.Url, m3u8); if (hls.Duration > durationLimit.TotalSeconds) throw new Exception($"duration limit <{durationLimit}> exceeded by track <{audio}> - <{hls.Duration}>"); - await _http.DownloadAsync(hls, fragmentDir); - string[] opusFragments = await _ffmpeg.ToOpus(fragmentDir); - string listFile = _ffmpeg.CreateFragmentList(fragmentDir, opusFragments); - await _ffmpeg.Concat(outFile, listFile); + await Http.DownloadAsync(hls, fragmentDir); + string[] opusFragments = await Ffmpeg.ToOpus(fragmentDir); + string listFile = Ffmpeg.CreateFragmentList(fragmentDir, opusFragments); + await Ffmpeg.Concat(outFile, listFile); Directory.Delete(fragmentDir); return outFile; @@ -113,7 +114,7 @@ public class VkClient : IDisposable { if (_disposed) return; Api.Dispose(); - _http.Dispose(); + Http.Dispose(); _disposed = true; } } diff --git a/VkAudioDownloader/VkClientConfig.cs b/VkAudioDownloader/VkClientConfig.cs index 80bb5db..4da3e85 100644 --- a/VkAudioDownloader/VkClientConfig.cs +++ b/VkAudioDownloader/VkClientConfig.cs @@ -5,7 +5,7 @@ namespace VkAudioDownloader; public class VkClientConfig { /// directory where ffmpeg and ffprobe binaries are stored - public string FFMPegDir; + public string FfmpegDir; /// vk app id from https://vk.com/apps?act=manage public ulong AppId; /// account password @@ -16,27 +16,37 @@ public class VkClientConfig public string? Token; - public VkClientConfig(string ffmPegDir, ulong appId, string? token) + public VkClientConfig(string ffmpegDir, ulong appId, string? token) { - FFMPegDir = ffmPegDir; + FfmpegDir = ffmpegDir; AppId = appId; Token = token; } - public VkClientConfig(string ffmPegDir, ulong appId, string? password, string? login) + public VkClientConfig(string ffmpegDir, ulong appId, string? password, string? login) { - FFMPegDir = ffmPegDir; + FfmpegDir = ffmpegDir; AppId = appId; Password = password; Login = login; } - private VkClientConfig(string ffmPegDir, ulong appId) + private VkClientConfig(string ffmpegDir, ulong appId) { - FFMPegDir = ffmPegDir; + FfmpegDir = ffmpegDir; AppId = appId; } - + + /// + /// { + /// ffmpeg_dir: ""; + /// app_id: 0ul; + /// token: ""; + /// #or + /// login: ""; + /// password: ""; + /// }; + /// public static VkClientConfig FromDtsod(DtsodV23 dtsod) { var config = new VkClientConfig(dtsod["ffmpeg_dir"], dtsod["app_id"]); @@ -56,7 +66,7 @@ public class VkClientConfig { "password", Password ?? null }, { "login", Login ?? null }, { "token", Token ?? null }, - { "ffmpeg_dir", FFMPegDir} + { "ffmpeg_dir", FfmpegDir} }; }