LaunchArgumentParser

This commit is contained in:
timerix 2023-01-09 02:16:10 +06:00
parent d650dc19e3
commit 24c1868e6b
7 changed files with 232 additions and 44 deletions

View File

@ -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<string>? 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<string> 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}}}";
}

View File

@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace VkAudioDownloader.CLI;
public class LaunchArgumentParser
{
private Dictionary<string, LaunchArgument> argDict = new();
private List<LaunchArgument> 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<LaunchArgument> 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; a<arg.Aliases.Length; a++)
argDict.Add(arg.Aliases[a], arg);
}
public LaunchArgument Get(string argAlias)
{
// different argument providing patterns
if (!argDict.TryGetValue(argAlias, out var arg) && // arg
!(argAlias.StartsWith("--") && argDict.TryGetValue(argAlias.Substring(2), out arg)) && // --arg
!(argAlias.StartsWith('-') && argDict.TryGetValue(argAlias.Substring(1), out arg)) && // -arg
!(argAlias.StartsWith('/') && argDict.TryGetValue(argAlias.Substring(1), out arg))) // /arg
throw new Exception($"invalid argument: {argAlias}\n{CreateHelpMessage()}");
return arg;
}
/// <param name="args">program launch args</param>
/// <exception cref="Exception">argument {args[i]} should have a parameter after it</exception>
/// <exception cref="NullReferenceException">argument hasn't got any handlers</exception>
/// <exception cref="ExitAfterHelpException">happens after help message is displayed</exception>
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");
}
}
}

View File

@ -5,10 +5,13 @@ using System.IO;
using DTLib.Extensions; using DTLib.Extensions;
using VkAudioDownloader; using VkAudioDownloader;
using DTLib.Logging.New; using DTLib.Logging.New;
using VkAudioDownloader.CLI;
Console.InputEncoding = StringConverter.UTF8; Console.InputEncoding = StringConverter.UTF8;
Console.OutputEncoding = StringConverter.UTF8; Console.OutputEncoding = StringConverter.UTF8;
LaunchArgumentParser argParser = new LaunchArgumentParser();
if(!File.Exists("config.dtsod")) if(!File.Exists("config.dtsod"))
{ {
File.Copy("config.dtsod.default", "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), var logger = new CompositeLogger(new DefaultLogFormat(true),
new ConsoleLogger(), new ConsoleLogger(),
new FileLogger("logs", "VkAudioDownloaer")); new FileLogger("logs", "VkAudioDownloaer"),
var _logger = new ContextLogger(logger, "Main"); new FileLogger("logs", "VkAudioDownloaer_debug") { DebugLogEnabled = true});
_logger.LogDebug("DEBUG LOG ENABLED"); var mainLoggerContext = new ContextLogger(logger, "Main");
mainLoggerContext.LogDebug("DEBUG LOG ENABLED");
try try
{ {
#if DEBUG #if DEBUG
// checking correctness of my aes-128 decryptor on current platform
VkAudioDownloader.Helpers.AudioAesDecryptor.TestAes(); VkAudioDownloader.Helpers.AudioAesDecryptor.TestAes();
#endif #endif
_logger.LogInfo("initializing api..."); mainLoggerContext.LogInfo("initializing api...");
var client = new VkClient(config, logger); var client = new VkClient(config, logger);
await client.ConnectAsync(); await client.ConnectAsync();
// getting audio from vk argParser.Add(new LaunchArgument(new []{"s", "search"}, "search audio on vk.com", SearchAudio, "query"));
var audios = client.FindAudio("сталинский костюм").ToArray(); argParser.ParseAndHandle(args);
void SearchAudio(string query)
{
var audios = client.FindAudio(query).ToArray();
for (var i = 0; i < audios.Length; i++) for (var i = 0; i < audios.Length; i++)
{ {
var a = audios[i]; var a = audios[i];
Console.WriteLine($"[{i}] {a.AudioToString()}"); Console.WriteLine($"[{i}] {a.AudioToString()}");
} }
Console.Write("choose audio: "); Console.Write("choose audio: ");
int ain = Convert.ToInt32(Console.ReadLine()); int ain = Convert.ToInt32(Console.ReadLine());
var audio = audios[ain]; var audio = audios[ain];
Console.WriteLine($"selected \"{audio.Title}\" -- {audio.Artist} [{TimeSpan.FromSeconds(audio.Duration)}]"); Console.WriteLine($"selected {audio.AudioToString()}");
// downloading parts
string downloadedFile = await client.DownloadAudioAsync(audio, "downloads"); string downloadedFile = client.DownloadAudioAsync(audio, "downloads").GetAwaiter().GetResult();
_logger.LogInfo($"audio {audio.AudioToString()} downloaded to {downloadedFile}"); mainLoggerContext.LogInfo($"audio {audio.AudioToString()} downloaded to {downloadedFile}");
}
}
catch (LaunchArgumentParser.ExitAfterHelpException)
{
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex); mainLoggerContext.LogError(ex);
} }
Console.ResetColor();

View File

@ -61,8 +61,7 @@ public class FFMPegHelper
opusFile // output opusFile // output
}) })
// ffmpeg prints all log to stderr, because in stdout it ptints encoded file // ffmpeg prints all log to stderr, because in stdout it ptints encoded file
.WithStandardErrorPipe(PipeTarget.ToDelegate( .WithStandardErrorPipe(PipeTarget.ToDelegate(StdErrHandle));
msg => _logger.LogWarn(msg)));
tasks[i] = command.ExecuteAsync(); tasks[i] = command.ExecuteAsync();
output[i] = opusFile; output[i] = opusFile;
@ -72,6 +71,13 @@ public class FFMPegHelper
return output; 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") public async Task Concat(string outfile, string fragmentListFile, string codec="libopus")
{ {
_logger.LogDebug($"{fragmentListFile} -> {outfile}"); _logger.LogDebug($"{fragmentListFile} -> {outfile}");

View File

@ -24,6 +24,6 @@
<PackageReference Include="VkNet.AudioBypassService" Version="1.7.4" /> <PackageReference Include="VkNet.AudioBypassService" Version="1.7.4" />
<PackageReference Include="VkNet" Version="1.72.0" /> <PackageReference Include="VkNet" Version="1.72.0" />
<PackageReference Include="DTLib.Dtsod" Version="1.0.2" /> <PackageReference Include="DTLib.Dtsod" Version="1.0.2" />
<PackageReference Include="DTLib.Logging" Version="1.0.5" /> <PackageReference Include="DTLib.Logging" Version="1.0.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -24,15 +24,15 @@ public class VkClient : IDisposable
public VkApi Api; public VkApi Api;
public VkClientConfig Config; public VkClientConfig Config;
private DTLib.Logging.New.ContextLogger _logger; private DTLib.Logging.New.ContextLogger _logger;
private HttpHelper _http; public HttpHelper Http;
private FFMPegHelper _ffmpeg; public FFMPegHelper Ffmpeg;
public VkClient(VkClientConfig conf, DTLib.Logging.New.ILogger logger) public VkClient(VkClientConfig conf, DTLib.Logging.New.ILogger logger)
{ {
Config = conf; Config = conf;
_logger = new ContextLogger(logger, nameof(VkClient)); _logger = new ContextLogger(logger, nameof(VkClient));
_http = new HttpHelper(); Http = new HttpHelper();
_ffmpeg = new FFMPegHelper(logger,conf.FFMPegDir); Ffmpeg = new FFMPegHelper(logger,conf.FfmpegDir);
var services = new ServiceCollection() var services = new ServiceCollection()
.Add(new LoggerService<VkApi>(logger)) .Add(new LoggerService<VkApi>(logger))
.AddAudioBypass(); .AddAudioBypass();
@ -44,7 +44,7 @@ public class VkClient : IDisposable
var authParams = new ApiAuthParams var authParams = new ApiAuthParams
{ {
ApplicationId = Config.AppId, ApplicationId = Config.AppId,
Settings = Settings.Audio Settings = Settings.Audio,
}; };
if (Config.Token is not null) if (Config.Token is not null)
{ {
@ -79,10 +79,11 @@ public class VkClient : IDisposable
Count = maxRezults, Count = maxRezults,
}); });
///<returns>file name</returns>
public Task<string> DownloadAudioAsync(Audio audio, string localDir) => public Task<string> DownloadAudioAsync(Audio audio, string localDir) =>
DownloadAudioAsync(audio, localDir,TimeSpan.FromHours(1)); DownloadAudioAsync(audio, localDir,TimeSpan.FromHours(1));
///<returns>file name</returns>
public async Task<string> DownloadAudioAsync(Audio audio, string localDir, TimeSpan durationLimit) public async Task<string> DownloadAudioAsync(Audio audio, string localDir, TimeSpan durationLimit)
{ {
if (!audio.Url.ToString().StartsWith("http")) if (!audio.Url.ToString().StartsWith("http"))
@ -93,16 +94,16 @@ public class VkClient : IDisposable
if(File.Exists(outFile)) if(File.Exists(outFile))
_logger.LogWarn( $"file {outFile} already exists"); _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 parser = new M3U8Parser();
var hls = parser.Parse(audio.Url, m3u8); var hls = parser.Parse(audio.Url, m3u8);
if (hls.Duration > durationLimit.TotalSeconds) if (hls.Duration > durationLimit.TotalSeconds)
throw new Exception($"duration limit <{durationLimit}> exceeded by track <{audio}> - <{hls.Duration}>"); throw new Exception($"duration limit <{durationLimit}> exceeded by track <{audio}> - <{hls.Duration}>");
await _http.DownloadAsync(hls, fragmentDir); await Http.DownloadAsync(hls, fragmentDir);
string[] opusFragments = await _ffmpeg.ToOpus(fragmentDir); string[] opusFragments = await Ffmpeg.ToOpus(fragmentDir);
string listFile = _ffmpeg.CreateFragmentList(fragmentDir, opusFragments); string listFile = Ffmpeg.CreateFragmentList(fragmentDir, opusFragments);
await _ffmpeg.Concat(outFile, listFile); await Ffmpeg.Concat(outFile, listFile);
Directory.Delete(fragmentDir); Directory.Delete(fragmentDir);
return outFile; return outFile;
@ -113,7 +114,7 @@ public class VkClient : IDisposable
{ {
if (_disposed) return; if (_disposed) return;
Api.Dispose(); Api.Dispose();
_http.Dispose(); Http.Dispose();
_disposed = true; _disposed = true;
} }
} }

View File

@ -5,7 +5,7 @@ namespace VkAudioDownloader;
public class VkClientConfig public class VkClientConfig
{ {
/// directory where ffmpeg and ffprobe binaries are stored /// directory where ffmpeg and ffprobe binaries are stored
public string FFMPegDir; public string FfmpegDir;
/// vk app id from https://vk.com/apps?act=manage /// vk app id from https://vk.com/apps?act=manage
public ulong AppId; public ulong AppId;
/// account password /// account password
@ -16,27 +16,37 @@ public class VkClientConfig
public string? Token; 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; AppId = appId;
Token = token; 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; AppId = appId;
Password = password; Password = password;
Login = login; Login = login;
} }
private VkClientConfig(string ffmPegDir, ulong appId) private VkClientConfig(string ffmpegDir, ulong appId)
{ {
FFMPegDir = ffmPegDir; FfmpegDir = ffmpegDir;
AppId = appId; AppId = appId;
} }
/// <summary>
/// {
/// ffmpeg_dir: "";
/// app_id: 0ul;
/// token: "";
/// #or
/// login: "";
/// password: "";
/// };
/// </summary>
public static VkClientConfig FromDtsod(DtsodV23 dtsod) public static VkClientConfig FromDtsod(DtsodV23 dtsod)
{ {
var config = new VkClientConfig(dtsod["ffmpeg_dir"], dtsod["app_id"]); var config = new VkClientConfig(dtsod["ffmpeg_dir"], dtsod["app_id"]);
@ -56,7 +66,7 @@ public class VkClientConfig
{ "password", Password ?? null }, { "password", Password ?? null },
{ "login", Login ?? null }, { "login", Login ?? null },
{ "token", Token ?? null }, { "token", Token ?? null },
{ "ffmpeg_dir", FFMPegDir} { "ffmpeg_dir", FfmpegDir}
}; };
} }