diff --git a/.gitignore b/.gitignore index add57be..f400df3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +.idea/ \ No newline at end of file diff --git a/.idea/.idea.VkAudioDownloader/.idea/.gitignore b/.idea/.idea.VkAudioDownloader/.idea/.gitignore deleted file mode 100644 index d67a4ef..0000000 --- a/.idea/.idea.VkAudioDownloader/.idea/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/contentModel.xml -/.idea.VkAudioDownloader.iml -/modules.xml -/projectSettingsUpdater.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.idea.VkAudioDownloader/.idea/encodings.xml b/.idea/.idea.VkAudioDownloader/.idea/encodings.xml deleted file mode 100644 index 97626ba..0000000 --- a/.idea/.idea.VkAudioDownloader/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/.idea.VkAudioDownloader/.idea/indexLayout.xml b/.idea/.idea.VkAudioDownloader/.idea/indexLayout.xml deleted file mode 100644 index f5a863a..0000000 --- a/.idea/.idea.VkAudioDownloader/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/.idea.VkAudioDownloader/.idea/misc.xml b/.idea/.idea.VkAudioDownloader/.idea/misc.xml deleted file mode 100644 index f98d778..0000000 --- a/.idea/.idea.VkAudioDownloader/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.VkAudioDownloader/.idea/vcs.xml b/.idea/.idea.VkAudioDownloader/.idea/vcs.xml deleted file mode 100644 index cfebfd3..0000000 --- a/.idea/.idea.VkAudioDownloader/.idea/vcs.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/DTLib b/DTLib index f6d045a..c701637 160000 --- a/DTLib +++ b/DTLib @@ -1 +1 @@ -Subproject commit f6d045ae2d97691d67bfa82a26e9c100bb3215a1 +Subproject commit c7016371a53efa266453e0ab4aacaae4fe260e9f diff --git a/VkAudioDownloader.CLI/Program.cs b/VkAudioDownloader.CLI/Program.cs index 74b8de9..c04bde1 100644 --- a/VkAudioDownloader.CLI/Program.cs +++ b/VkAudioDownloader.CLI/Program.cs @@ -1,11 +1,30 @@ -using DTLib.Dtsod; +using System; +using System.Linq; +using System.Net.Http; +using DTLib.Dtsod; +using DTLib.Filesystem; using VkAudioDownloader; +using DTLib.Logging.New; +using VkAudioDownloader.VkM3U8; -var client = new VkClient(VkClientConfig.FromDtsod(new DtsodV23(File.ReadAllText("config.dtsod")))); + +if(!File.Exists("config.dtsod")) + File.Copy("config.dtsod.default","config.dtsod"); + +var logger = new CompositeLogger(new DefaultLogFormat(true), + new ConsoleLogger(), + new FileLogger("logs", "VkAudioDownloaer")); +var client = new VkClient( + VkClientConfig.FromDtsod(new DtsodV23(File.ReadAllText("config.dtsod"))), + logger); +logger.Log("main", LogSeverity.Info, "initializing api..."); +logger.DebugLogEnabled = true; client.Connect(); -Console.WriteLine(client.Api.Token); -var audios = client.FindAudio("моя оборона"); -foreach (var a in audios) -{ - Console.WriteLine(a.Title); -} +var audio = client.FindAudio("гражданская оборона", 1).First(); +Console.WriteLine($"{audio.Title} -- {audio.Artist} [{TimeSpan.FromSeconds(audio.Duration)}]"); +var Http = new HttpClient(); +var m3u8 = await Http.GetStringAsync(audio.Url); +Console.WriteLine("downloaded m3u8 playlist:\n" + m3u8); +var parser = new M3U8Parser(); +var HLSPlaylist = parser.Parse(audio.Url, m3u8); +Console.WriteLine(HLSPlaylist); diff --git a/VkAudioDownloader.CLI/VkAudioDownloader.CLI.csproj b/VkAudioDownloader.CLI/VkAudioDownloader.CLI.csproj index 30f41bc..3a34438 100644 --- a/VkAudioDownloader.CLI/VkAudioDownloader.CLI.csproj +++ b/VkAudioDownloader.CLI/VkAudioDownloader.CLI.csproj @@ -4,7 +4,7 @@ Exe net6.0 10 - enable + disable enable diff --git a/VkAudioDownloader.sln b/VkAudioDownloader.sln index f47fb9a..61ecc0d 100644 --- a/VkAudioDownloader.sln +++ b/VkAudioDownloader.sln @@ -14,6 +14,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VkNet.Generators", "vknet\V EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VkNet.AudioBypassService", "VkNet.AudioBypass\VkNet.AudioBypassService\VkNet.AudioBypassService.csproj", "{5650A6DD-602D-49FF-A0B0-DB59BD63EACE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sulution_items", "sulution_items", "{2CBCAE99-53A3-4ADE-A08B-5755EC471878}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + .gitmodules = .gitmodules + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DTLib.Logging", "DTLib\DTLib.Logging\DTLib.Logging.csproj", "{A087B535-371A-4A7E-883E-B5B290567E9A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,5 +56,9 @@ Global {5650A6DD-602D-49FF-A0B0-DB59BD63EACE}.Debug|Any CPU.Build.0 = Debug|Any CPU {5650A6DD-602D-49FF-A0B0-DB59BD63EACE}.Release|Any CPU.ActiveCfg = Release|Any CPU {5650A6DD-602D-49FF-A0B0-DB59BD63EACE}.Release|Any CPU.Build.0 = Release|Any CPU + {A087B535-371A-4A7E-883E-B5B290567E9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A087B535-371A-4A7E-883E-B5B290567E9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A087B535-371A-4A7E-883E-B5B290567E9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A087B535-371A-4A7E-883E-B5B290567E9A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/VkAudioDownloader/VkAudioDownloader.csproj b/VkAudioDownloader/VkAudioDownloader.csproj index c8414fa..a6281c0 100644 --- a/VkAudioDownloader/VkAudioDownloader.csproj +++ b/VkAudioDownloader/VkAudioDownloader.csproj @@ -3,12 +3,14 @@ net6.0 10 - enable + disable enable + true + diff --git a/VkAudioDownloader/VkClient.cs b/VkAudioDownloader/VkClient.cs index 24e5c6b..0326e61 100644 --- a/VkAudioDownloader/VkClient.cs +++ b/VkAudioDownloader/VkClient.cs @@ -1,6 +1,6 @@ -global using DTLib; -global using DTLib.Extensions; -using DTLib.Logging; +using System; +using System.IO; +using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using VkNet; @@ -9,8 +9,10 @@ using VkNet.Model; using VkNet.Model.RequestParams; using VkNet.Utils; using VkNet.Model.Attachments; -using VkNet.AudioBypassService; using VkNet.AudioBypassService.Extensions; +using DTLib.Logging.DependencyInjection; +using DTLib.Logging.New; +using ILogger = DTLib.Logging.New.ILogger; namespace VkAudioDownloader; @@ -20,15 +22,16 @@ public class VkClient : IDisposable { public VkApi Api; public VkClientConfig Config; - - public VkClient(VkClientConfig conf) + private ILogger _logger; + + public VkClient(VkClientConfig conf, ILogger logger) { Config = conf; - var services = new ServiceCollection(); - //services.AddSingleton(); - services.AddAudioBypass(); + _logger = logger; + var services = new ServiceCollection() + .Add(new LoggerService(logger)) + .AddAudioBypass(); Api = new VkApi(services); - } public void Connect() @@ -40,12 +43,12 @@ public class VkClient : IDisposable }; if (Config.Token is not null) { - Console.WriteLine("authorizing by token"); + _logger.Log(nameof(VkClient),LogSeverity.Info,"authorizing by token"); authParams.AccessToken = Config.Token; } else { - Console.WriteLine("authorizing by login and password"); + _logger.Log(nameof(VkClient),LogSeverity.Info,"authorizing by login and password"); authParams.Login = Config.Login; authParams.Password = Config.Password; } diff --git a/VkAudioDownloader/VkM3U8/AudioDecryptor.cs b/VkAudioDownloader/VkM3U8/AudioDecryptor.cs new file mode 100644 index 0000000..f115d28 --- /dev/null +++ b/VkAudioDownloader/VkM3U8/AudioDecryptor.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using System.Security.Cryptography; + +namespace VkAudioDownloader.VkM3U8; + +public class AudioDecryptor : IDisposable +{ + private Aes Aes; + private ICryptoTransform Decryptor; + + AudioDecryptor() + { + Aes=Aes.Create(); + Aes.KeySize = 128; + Aes.Mode = CipherMode.CBC; + Aes.IV = new byte[4] { 0, 0, 0, 0 }; + Aes.Padding = PaddingMode.Zeros; + Decryptor = Aes.CreateDecryptor(); + } + + public Stream Decrypt(Stream fragment) + => new CryptoStream(fragment, Decryptor, CryptoStreamMode.Read); + + private bool _disposed; + public void Dispose() + { + if(_disposed) return; + Aes.Dispose(); + Decryptor.Dispose(); + _disposed = true; + } + + ~AudioDecryptor() => Dispose(); +} \ No newline at end of file diff --git a/VkAudioDownloader/VkM3U8/HLSFragment.cs b/VkAudioDownloader/VkM3U8/HLSFragment.cs new file mode 100644 index 0000000..342435c --- /dev/null +++ b/VkAudioDownloader/VkM3U8/HLSFragment.cs @@ -0,0 +1,9 @@ +namespace VkAudioDownloader.VkM3U8; + +public record struct HLSFragment +{ + public string Name; + // public int Duration; + public bool Encrypted; + public string? EncryptionKeyUrl; +} \ No newline at end of file diff --git a/VkAudioDownloader/VkM3U8/HLSPlaylist.cs b/VkAudioDownloader/VkM3U8/HLSPlaylist.cs new file mode 100644 index 0000000..aeeca9d --- /dev/null +++ b/VkAudioDownloader/VkM3U8/HLSPlaylist.cs @@ -0,0 +1,25 @@ +namespace VkAudioDownloader.VkM3U8; + +public class HLSPlaylist +{ + + public HLSFragment[] Fragments { get; internal set; } + + /// content duration in seconds + public int Duration { get; internal set; } + + /// url before index.m3u8 + public string BaseUrl { get; internal set; } + + internal HLSPlaylist(HLSFragment[] fragments, int duration, string baseUrl) + { + Fragments = fragments; + Duration = duration; + BaseUrl = baseUrl; + } + + public override string ToString() => + $"BaseUrl: {BaseUrl}\n" + + $"Duration: {Duration}\n" + + $"Fragments: HLSFragment[{Fragments.Length}]"; +} \ No newline at end of file diff --git a/VkAudioDownloader/VkM3U8/M3U8Parser.cs b/VkAudioDownloader/VkM3U8/M3U8Parser.cs new file mode 100644 index 0000000..e9a0a31 --- /dev/null +++ b/VkAudioDownloader/VkM3U8/M3U8Parser.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; + +namespace VkAudioDownloader.VkM3U8; + +public class M3U8Parser +{ + private string _m3u8=""; + private int _pos; + private List _fragments = new(); + private int _playlistDuration = 0; + private HLSFragment _currentFragment = default; + + // parses m3u8 playlist and resets state + public HLSPlaylist Parse(Uri m3u8Url, string m3u8Content) + { + _m3u8 = m3u8Content; + var line = NextLine(); + while (!line.IsEmpty) + { + if (line.Contains('#')) + ParseHashTag(line); + else + { + _currentFragment.Name = line.ToString(); + _fragments.Add(_currentFragment); + _currentFragment = default; + } + + line = NextLine(); + } + + string urlStr = m3u8Url.ToString(); + var rezult = new HLSPlaylist( + _fragments.ToArray(), + _playlistDuration, + urlStr.Remove(urlStr.LastIndexOf('/')+1)); + Clear(); + return rezult; + } + + ReadOnlySpan NextLine() + { + int pos = _pos; + int index = _m3u8.IndexOf('\n', pos); + if (index == -1) + index = _m3u8.Length - _pos; + if (index == 0) + return ReadOnlySpan.Empty; + _pos = index+1; + if (_m3u8[index - 1] == '\r') + index--; // skip /r + var line = _m3u8.AsSpan(pos, index - pos); + return line; + } + + private void ParseHashTag(ReadOnlySpan line) + { + if(line.StartsWith("EXT-X-TARGETDURATION:")) + _playlistDuration=Int32.Parse(line.After(':')); + else if (line.StartsWith("#EXT-X-KEY:METHOD=")) + { + var method = line.After("#EXT-X-KEY:METHOD="); + + if (method.ToString() == "NONE") + { + _currentFragment.Encrypted = false; + return; + } + + var alg = method.Before(','); + if (alg.ToString() != "AES-128") + throw new Exception($"unknown encryption algorythm: {method}"); + + var keyUrl=method.After("URI=\"").Before('\"'); + if (!keyUrl.StartsWith("http")) + throw new Exception($"key uri is not url: {keyUrl}"); + + // AES-128 which AudioDecryptor can decrypt + _currentFragment.Encrypted = true; + _currentFragment.EncryptionKeyUrl = keyUrl.ToString(); + } + } + + private void Clear() + { + _m3u8 = ""; + _pos=0; + _fragments.Clear(); + _currentFragment = default; + _playlistDuration = 0; + } +} \ No newline at end of file diff --git a/VkAudioDownloader/VkM3U8/SpanHelper.cs b/VkAudioDownloader/VkM3U8/SpanHelper.cs new file mode 100644 index 0000000..648f33e --- /dev/null +++ b/VkAudioDownloader/VkM3U8/SpanHelper.cs @@ -0,0 +1,39 @@ +using System; + +namespace VkAudioDownloader.VkM3U8; + +internal static class SpanHelper +{ + public static ReadOnlySpan After(this ReadOnlySpan span, char c) + { + var index = span.IndexOf(c); + if (index == -1) + throw new Exception($"char <{c}> not found in span <{span}>"); + return span.Slice(index+1); + } + + public static ReadOnlySpan After(this ReadOnlySpan span, ReadOnlySpan s) + { + var index = span.IndexOf(s); + if (index == -1) + throw new Exception($"span <{s}> not found in span <{span}>"); + return span.Slice(index+s.Length); + } + + + public static ReadOnlySpan Before(this ReadOnlySpan span, char c) + { + var index = span.IndexOf(c); + if (index == -1) + throw new Exception($"char <{c}> not found in span <{span}>"); + return span.Slice(0,index); + } + + public static ReadOnlySpan Before(this ReadOnlySpan span, ReadOnlySpan s) + { + var index = span.IndexOf(s); + if (index == -1) + throw new Exception($"span <{s}> not found in span <{span}>"); + return span.Slice(0,index); + } +} \ No newline at end of file