Merged c# repo to xdm repo

master
subhra74 2021-12-01 06:10:14 +01:00
parent a0b6377b6c
commit e72966b1cf
814 changed files with 61039 additions and 56309 deletions

355
.gitignore vendored
View File

@ -1,5 +1,350 @@
app/.classpath
app/.project
app/.settings/*
app/target/*
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/

351
app/.gitignore vendored
View File

@ -1 +1,350 @@
/target/
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/

View File

@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Linq;
using TraceLog;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Util;
namespace BrowserMonitoring
{
internal static class BrowserMessageHandler
{
internal static void Handle(IApp app, RawBrowserMessageEnvelop envelop)
{
//Log.Debug("Type: " + envelop.MessageType);
if (envelop.MessageType == "videoIds")
{
foreach (var item in envelop.VideoIds)
{
app.AddVideoDownload(item);
}
return;
}
if (envelop.MessageType == "clear")
{
app.ClearVideoList();
return;
}
if (envelop.MessageType == "sync")
{
return;
}
var rawMessage = envelop.Message;
if (rawMessage == null)
{
Log.Debug("Raw message is null");
return;
};
switch (envelop.MessageType)
{
case "download":
{
var message = Parse(rawMessage);
if (!(Helpers.IsBlockedHost(message.Url) || Helpers.IsCompressedJSorCSS(message.Url)))
{
app.AddDownload(message);
}
break;
}
case "video":
{
var message = Parse(rawMessage);
var contentType = message.GetResponseHeaderFirstValue("Content-Type");
if (VideoUrlHelper.IsYtFormat(contentType))
{
VideoUrlHelper.ProcessPostYtFormats(message, app);
}
//if (VideoUrlHelper.IsFBFormat(contentType, message.Url))
//{
// VideoUrlHelper.ProcessPostFBFormats(message, app);
//}
if (VideoUrlHelper.IsHLS(contentType))
{
VideoUrlHelper.ProcessHLSVideo(message, app);
}
if (VideoUrlHelper.IsDASH(contentType))
{
VideoUrlHelper.ProcessDashVideo(message, app);
}
if (!VideoUrlHelper.ProcessYtDashSegment(message, app))
{
if (VideoUrlHelper.IsNormalVideo(contentType, message.Url, message.GetContentLength()))
{
VideoUrlHelper.ProcessNormalVideo(message, app);
}
}
break;
}
}
}
private static string GetFileName(string text)
{
try
{
return Uri.UnescapeDataString(text);
}
catch { }
return text;
}
internal static Message Parse(RawBrowserMessage rawMessage)
{
var message = new Message
{
File = GetFileName(rawMessage.File),
Url = rawMessage.Url,
RequestMethod = rawMessage.Method ?? "GET",
RequestBody = rawMessage.RequestBody
};
var cookies = new List<string>();
if (rawMessage.RequestHeaders != null && rawMessage.RequestHeaders.Count > 0)
{
foreach (var key in rawMessage.RequestHeaders.Keys)
{
if (string.IsNullOrEmpty(key)) continue;
if (key.Equals("cookie", StringComparison.InvariantCultureIgnoreCase))
{
cookies.AddRange(rawMessage.RequestHeaders[key]);
}
var invalidHeader = IsBlockedHeader(key);
if (key.Equals("content-type", StringComparison.InvariantCultureIgnoreCase) &&
!string.IsNullOrEmpty(rawMessage.RequestBody))
{
invalidHeader = false;
}
if (!invalidHeader)
{
message.RequestHeaders.Add(key, rawMessage.RequestHeaders[key]);
}
}
}
var cookieSet = new HashSet<string>();
foreach (var cookie in cookies)
{
foreach (var item in cookie.Split(';'))
{
var value = item.Trim();
if (!string.IsNullOrEmpty(value))
{
cookieSet.Add(value);
}
}
}
if (rawMessage.ResponseHeaders != null && rawMessage.ResponseHeaders.Count > 0)
{
foreach (var key in rawMessage.ResponseHeaders.Keys)
{
if (!string.IsNullOrEmpty(key))
{
message.ResponseHeaders.Add(key, rawMessage.ResponseHeaders[key]);
}
}
}
if (rawMessage.Cookies != null && rawMessage.Cookies.Count > 0)
{
foreach (var key in rawMessage.Cookies.Keys)
{
if (!string.IsNullOrEmpty(key))
{
cookieSet.Add(key + "=" + rawMessage.Cookies[key]);
}
}
}
if (cookieSet.Count > 0)
{
message.Cookies.Add("Cookie", Helpers.MakeCookieString(cookieSet));
}
if (!message.RequestHeaders.ContainsKey("User-Agent") && message.ResponseHeaders.ContainsKey("realUA"))
{
message.ResponseHeaders["User-Agent"] = message.ResponseHeaders["realUA"];
}
return message;
}
private static bool IsBlockedHeader(string header) =>
blockedHeaders.Any(blockedHeader => (header?.ToLowerInvariant() ?? string.Empty).StartsWith(blockedHeader));
private static string[] blockedHeaders = { "accept", "if", "authorization", "proxy", "connection", "expect", "te",
"upgrade", "range", "transfer-encoding", "content-type", "content-length","content-encoding" ,"accept-encoding"};
}
}

View File

@ -0,0 +1,20 @@
using XDM.Core.Lib.Common;
namespace BrowserMonitoring
{
public static class BrowserMonitor
{
public static void RunHttpIpcHandler(IApp app)
{
var handler = new IpcHttpHandler(app);
handler.StartHttpIpcChannel();
}
public static NativeMessagingHostHandler RunNativeHostHandler(IApp app)
{
var handler = new NativeMessagingHostHandler(app);
handler.StartPipedChannel();
return handler;
}
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net3.5;net4.5;net4.7.2;net5.0</TargetFrameworks>
<Platforms>AnyCPU;x86</Platforms>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net5.0' ">
<ProjectReference Include="..\NetFX.Polyfill\NetFX.Polyfill.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HttpServer\HttpServer.csproj" />
<ProjectReference Include="..\XDM_CoreFx\XDM.Core.csproj" />
<ProjectReference Include="..\TraceLog\TraceLog.csproj" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net3.5' ">
<PackageReference Include="System.Buffers" Version="4.5.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,135 @@
using System;
using System.Text;
using System.Net;
using System.Linq;
using Newtonsoft.Json;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Util;
using HttpServer;
using System.Threading;
namespace BrowserMonitoring
{
public class IpcHttpHandler
{
private IApp app;
private NanoServer server;
public IpcHttpHandler(IApp app)
{
this.app = app;
server = new NanoServer(IPAddress.Loopback, 9614);
server.RequestReceived += (sender, args) =>
{
HandleRequest(args.RequestContext);
};
}
public void StartHttpIpcChannel()
{
new Thread(() =>
{
server.Start();
}).Start();
}
public void HandleRequest(RequestContext context)
{
if (context.RequestPath == "/204")
{
context.ResponseStatus = new ResponseStatus
{
StatusCode = 204,
StatusMessage = "No Content"
};
context.AddResponseHeader("Cache-Control", "max-age=0, no-cache, must-revalidate");
context.SendResponse();
return;
}
try
{
switch (context.RequestPath)
{
case "/download":
Console.WriteLine(Encoding.UTF8.GetString(context.RequestBody!));
var message = Message.ParseMessage(Encoding.UTF8.GetString(context.RequestBody!));
if (!(Helpers.IsBlockedHost(message.Url) || Helpers.IsCompressedJSorCSS(message.Url)))
{
app.AddDownload(message);
}
break;
case "/video":
Console.WriteLine(Encoding.UTF8.GetString(context.RequestBody!));
var message2 = Message.ParseMessage(Encoding.UTF8.GetString(context.RequestBody!));
var contentType = message2.GetResponseHeaderFirstValue("Content-Type")?.ToLowerInvariant() ?? string.Empty;
if (VideoUrlHelper.IsHLS(contentType))
{
VideoUrlHelper.ProcessHLSVideo(message2, app);
}
if (VideoUrlHelper.IsDASH(contentType))
{
VideoUrlHelper.ProcessDashVideo(message2, app);
}
if (!VideoUrlHelper.ProcessYtDashSegment(message2, app))
{
if (contentType != null && !(contentType.Contains("f4f") ||
contentType.Contains("m4s") ||
contentType.Contains("mp2t") || message2.Url.Contains("abst") ||
message2.Url.Contains("f4x") || message2.Url.Contains(".fbcdn")
|| message2.Url.Contains("http://127.0.0.1:9614")))
{
VideoUrlHelper.ProcessNormalVideo(message2, app);
}
}
break;
case "/item":
foreach (var item in Encoding.UTF8.GetString(context.RequestBody!).Split(new char[] { '\r', '\n' }))
{
app.AddVideoDownload(item);
}
break;
case "/clear":
app.ClearVideoList();
break;
}
}
finally
{
SendSyncResponse(context);
}
}
private void SendSyncResponse(RequestContext context)
{
var resp = new
{
enabled = Config.Instance.IsBrowserMonitoringEnabled,
blockedHosts = new string[0],
videoUrls = new string[0],
fileExts = Config.Instance.FileExtensions,
vidExts = Config.Instance.VideoExtensions,
vidList = app.GetVideoList().Select(a => new
{
id = a.ID,
text = a.File,
info = a.DisplayName
}).ToList(),
mimeList = new string[] { "video", "audio", "mpegurl", "f4m", "m3u8", "dash" }
};
var json = JsonConvert.SerializeObject(resp);
context.ResponseStatus = new ResponseStatus
{
StatusCode = 200,
StatusMessage = "OK"
};
context.AddResponseHeader("Content-Type", "application/json");
context.AddResponseHeader("Cache-Control", "max-age=0, no-cache, must-revalidate");
context.ResponseBody = Encoding.UTF8.GetBytes(json);
context.SendResponse();
}
}
}

View File

@ -0,0 +1,245 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Pipes;
using System.IO;
using XDM.Core.Lib.Common;
using System.Threading;
#if NET35
using NetFX.Polyfill;
#else
using System.Collections.Concurrent;
#endif
using TraceLog;
namespace BrowserMonitoring
{
public class NativeMessagingHostHandler : IDisposable
{
private int MaxPipeInstance = 254;
private readonly string PipeName = "XDM_Ipc_Browser_Monitoring_Pipe";
private List<NamedPipeServerStream> inPipes = new();
private Dictionary<NamedPipeServerStream, NamedPipeClientStream> inOutMap = new();
private readonly IApp app;
private readonly Mutex globalMutex;
private readonly BlockingCollection<byte[]> Messages = new();
private Thread WriterThread;
public NativeMessagingHostHandler(IApp app)
{
this.app = app;
try
{
using var mutex = Mutex.OpenExisting(@"Global\XDM_Active_Instance");
throw new InstanceAlreadyRunningException(@"XDM instance already running, Mutex exists 'Global\XDM_Active_Instance'");
}
catch (Exception ex)
{
Log.Debug(ex, "Exception in NativeMessagingHostHandler ctor");
if (ex is InstanceAlreadyRunningException) throw;
}
globalMutex = new Mutex(true, @"Global\XDM_Active_Instance");
}
public void BroadcastConfig()
{
var bytes = GetSyncBytes();
Messages.Add(bytes);
}
public void StartPipedChannel()
{
WriterThread = new Thread(() =>
{
while (true)
{
//Log.Debug("Total messages to be sent to native host: " + Messages.Count);
var bytes = Messages.Take();
foreach (var key in inOutMap.Keys)
{
//Log.Debug("Sending message to native host");
try
{
var outpipe = inOutMap[key];
WriteMessage(outpipe, bytes);
//Log.Debug("Send message to native host successfully");
}
catch (Exception ex)
{
Log.Debug(ex, "Send message to native host failed");
}
}
}
});
WriterThread.Start();
new Thread(() =>
{
try
{
if (inPipes.Count == MaxPipeInstance)
{
Log.Debug("Max pipe count of " + MaxPipeInstance + " is reached");
return;
}
var inPipe =
new NamedPipeServerStream(PipeName,
PipeDirection.In, NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Byte, PipeOptions.WriteThrough);
inPipes.Add(inPipe);
var first = true;
while (true)
{
Log.Debug("Waiting for native host pipe...");
inPipe.WaitForConnection();
Log.Debug("Pipe request received");
if (first)
{
Log.Debug("Creating one more additional pipe");
StartPipedChannel();
first = false;
}
try
{
ConsumePipe(inPipe);
}
catch (Exception e)
{
inPipe.Disconnect();
Log.Debug(e, "Error in message exchange");
}
Log.Debug("Terminated message exchange, will reuse the pipe");
}
}
catch (Exception ex)
{
Log.Debug(ex, "Error in message exchange flow");
}
}).Start();
}
private void ConsumePipe(NamedPipeServerStream inPipe)
{
try
{
Log.Debug("Initiate message handshake");
var clientPipeName = Encoding.UTF8.GetString(ReadMessageBytes(inPipe));
Log.Debug("Client pipe: " + clientPipeName);
var outPipe = new NamedPipeClientStream(".", clientPipeName, PipeDirection.Out);
outPipe.Connect();
SendConfig(outPipe);
inOutMap[inPipe] = outPipe;
Log.Debug("Message handshake completed");
while (true)
{
var text = ReadMessageBytes(inPipe);
using var ms = new MemoryStream(text);
using var br = new BinaryReader(ms);
// Log.Debug("{Text}", text);
var envelop = RawBrowserMessageEnvelop.Deserialize(br);
BrowserMessageHandler.Handle(app, envelop);
}
}
finally
{
try
{
NamedPipeClientStream? op = null;
lock (this)
{
if (inOutMap.TryGetValue(inPipe, out op))
{
inOutMap.Remove(inPipe);
}
}
op?.Close();
op?.Dispose();
}
catch { }
}
}
private void SendConfig(Stream pipe)
{
var bytes = GetSyncBytes();
WriteMessage(pipe, bytes);
}
private static void ReadFully(Stream stream, byte[] buf, int bytesToRead)
{
var rem = bytesToRead;
var index = 0;
while (rem > 0)
{
var c = stream.Read(buf, index, rem);
if (c == 0) throw new IOException("Unexpected EOF");
index += c;
rem -= c;
}
}
private static byte[] ReadMessageBytes(Stream pipe)
{
var b4 = new byte[4];
ReadFully(pipe, b4, 4);
var syncLength = BitConverter.ToInt32(b4, 0);
var bytes = new byte[syncLength];
ReadFully(pipe, bytes, syncLength);
return bytes;
}
private void WriteMessage(Stream pipe, string message)
{
var msgBytes = Encoding.UTF8.GetBytes(message);
WriteMessage(pipe, msgBytes);
}
private static void WriteMessage(Stream pipe, byte[] msgBytes)
{
var bytes = BitConverter.GetBytes(msgBytes.Length);
pipe.Write(bytes, 0, bytes.Length);
pipe.Write(msgBytes, 0, msgBytes.Length);
pipe.Flush();
}
public void Dispose()
{
foreach (var pipe in inPipes)
{
try { pipe.Disconnect(); } catch { }
try { pipe.Dispose(); } catch { }
}
}
private byte[] GetSyncBytes()
{
var msg = new SyncMessage()
{
Enabled = Config.Instance.IsBrowserMonitoringEnabled,
BlockedHosts = Config.Instance.BlockedHosts,
VideoUrls = new string[0],
FileExts = Config.Instance.FileExtensions,
VidExts = Config.Instance.VideoExtensions,
VidList = app.GetVideoList(false).Select(a => new VideoItem
{
Id = a.ID,
Text = a.File,
Info = a.DisplayName
}).ToList(),
MimeList = new string[] { "video", "audio", "mpegurl", "f4m", "m3u8", "dash" },
BlockedMimeList = new string[] { "text/javascript", "application/javascript", "text/css", "text/html" },
VideoUrlsWithPostReq = new string[] { "ubei/v1/player?key=", "ubei/v1/next?key=" }
};
return msg.Serialize();
}
}
public class InstanceAlreadyRunningException : Exception
{
public InstanceAlreadyRunningException(string message) : base(message)
{
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace BrowserMonitoring
{
public class RawBrowserMessage
{
public string File { get; set; }
public string Url { get; set; }
public string Method { get; set; }
public string RequestBody { get; set; }
public Dictionary<string, List<string>> RequestHeaders { get; set; }
public Dictionary<string, List<string>> ResponseHeaders { get; set; }
public Dictionary<string, string> Cookies { get; set; }
}
}

View File

@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.IO;
using XDM.Core.Lib.Util;
namespace BrowserMonitoring
{
public struct RawBrowserMessageEnvelop
{
public string MessageType { get; set; }
public RawBrowserMessage Message { get; set; }
public string[] VideoIds { get; set; }
public void Serialize(BinaryWriter w)
{
RawBrowserMessageEnvelopSerializerV1.Serialize(this, w);
}
public static RawBrowserMessageEnvelop Deserialize(BinaryReader r)
{
var version = r.ReadInt32();
if (version == 1)
{
return RawBrowserMessageEnvelopSerializerV1.Deserialize(r);
}
throw new InvalidDataException($"Version ${version} not supported.");
}
}
internal static class RawBrowserMessageEnvelopSerializerV1
{
public static void Serialize(RawBrowserMessageEnvelop e, BinaryWriter w)
{
w.Write(1);
w.Write(e.MessageType);
w.Write(e.Message != null);
if (e.Message != null)
{
w.Write(e.Message.Url ?? string.Empty);
w.Write(e.Message.File ?? string.Empty);
w.Write(e.Message.Method ?? string.Empty);
w.Write(e.Message.RequestBody ?? string.Empty);
Helpers.WriteStateHeaders(e.Message.RequestHeaders, w);
Helpers.WriteStateHeaders(e.Message.ResponseHeaders, w);
Helpers.WriteStateCookies(e.Message.Cookies, w);
}
var count = e.VideoIds?.Length ?? 0;
w.Write(count);
if (e.VideoIds != null && e.VideoIds.Length > 0)
{
foreach (var item in e.VideoIds)
{
w.Write(item);
}
}
}
public static RawBrowserMessageEnvelop Deserialize(BinaryReader r)
{
var e = new RawBrowserMessageEnvelop { };
e.MessageType = Helpers.ReadString(r);
if (r.ReadBoolean())
{
e.Message = new();
e.Message.Url = Helpers.ReadString(r);
e.Message.File = Helpers.ReadString(r);
e.Message.Method = Helpers.ReadString(r);
e.Message.RequestBody = Helpers.ReadString(r);
Helpers.ReadStateHeaders(r, out Dictionary<string, List<string>> dict1);
Helpers.ReadStateHeaders(r, out Dictionary<string, List<string>> dict2);
Helpers.ReadStateCookies(r, out Dictionary<string, string> dict3);
e.Message.RequestHeaders = dict1;
e.Message.ResponseHeaders = dict2;
e.Message.Cookies = dict3;
}
var count = r.ReadInt32();
e.VideoIds = new string[count];
for (int i = 0; i < count; i++)
{
e.VideoIds[i] = r.ReadString();
}
return e;
}
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using XDM.Core.Lib.Util;
namespace BrowserMonitoring
{
public class SyncMessage
{
public bool Enabled { get; set; }
public string[] BlockedHosts { get; set; } = new string[0];
public string[] VideoUrls { get; set; } = new string[0];
public string[] FileExts { get; set; } = new string[0];
public string[] VidExts { get; set; } = new string[0];
public List<VideoItem> VidList { get; set; } = new List<VideoItem>(0);
public string[] MimeList { get; set; } = new string[0];
public string[] BlockedMimeList { get; set; } = new string[0];
public string[] VideoUrlsWithPostReq { get; set; } = new string[0];
public byte[] Serialize()
{
using var ms = new MemoryStream();
using var w = new BinaryWriter(ms);
w.Write(this.Enabled);
WriteStringArray(w, BlockedHosts);
WriteStringArray(w, VideoUrls);
WriteStringArray(w, FileExts);
WriteStringArray(w, VidExts);
WriteStringArray(w, MimeList);
WriteStringArray(w, BlockedMimeList);
WriteStringArray(w, VideoUrlsWithPostReq);
w.Write(VidList.Count);
foreach (var item in VidList)
{
w.Write(item.Id);
w.Write(item.Info);
w.Write(item.Text);
}
w.Close();
ms.Close();
return ms.ToArray();
}
public static SyncMessage Deserialize(byte[] bytes)
{
using var ms = new MemoryStream(bytes);
using var r = new BinaryReader(ms);
var msg = new SyncMessage();
msg.Enabled = r.ReadBoolean();
msg.BlockedHosts = ReadStringArray(r);
msg.VideoUrls = ReadStringArray(r);
msg.FileExts = ReadStringArray(r);
msg.VidExts = ReadStringArray(r);
msg.MimeList = ReadStringArray(r);
msg.BlockedMimeList = ReadStringArray(r);
msg.VideoUrlsWithPostReq = ReadStringArray(r);
var c = r.ReadInt32();
msg.VidList = new(c);
for (int i = 0; i < c; i++)
{
msg.VidList.Add(new VideoItem
{
Id = Helpers.ReadString(r),
Info = Helpers.ReadString(r),
Text = Helpers.ReadString(r)
});
}
return msg;
}
private static void WriteStringArray(BinaryWriter w, string[] arr)
{
var c = arr?.Length ?? 0;
w.Write(c);
if (arr != null && arr.Length > 0)
{
foreach (var item in arr)
{
w.Write(item ?? string.Empty);
}
}
}
private static string[] ReadStringArray(BinaryReader r)
{
var c = r.ReadInt32();
var arr = new string[c];
for (int i = 0; i < c; i++)
{
arr[i] = r.ReadString();
}
return arr;
}
}
public struct VideoItem
{
public string Id { get; set; }
public string Text { get; set; }
public string Info { get; set; }
}
}

View File

@ -0,0 +1,817 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Util;
//using XDM.Core.Lib.Downloader.YT.Dash;
using XDM.Core.Lib.Common.Segmented;
using System.IO;
using System.Net;
using XDM.Core.Lib.Common.Segmented;
using MediaParser.Hls;
using XDM.Core.Lib.Common.Hls;
using MediaParser.Dash;
using XDM.Core.Lib.Common.Dash;
using MediaParser.YouTube;
using System.Security.Cryptography;
using XDM.Core.Lib.Common;
using TraceLog;
using XDM.Core.Lib.Clients.Http;
#if !NET5_0_OR_GREATER
using NetFX.Polyfill;
#endif
namespace BrowserMonitoring
{
static class VideoUrlHelper
{
private static object lockObject = new object();
private static DashInfo lastVid;
private static List<DashInfo> videoQueue = new();
private static List<DashInfo> audioQueue = new();
private static Dictionary<string, DateTime> referersToSkip = new(); //Skip the video requests whose referer hash is present in below dict
//as they were triggered by HLS or DASH
internal static bool IsNormalVideo(string contentType, string url, long size)
{
if (size > 0 && size < Config.Instance.MinVideoSize * 1024)
{
return false;
}
return (contentType != null && !(contentType.Contains("f4f") ||
contentType.Contains("m4s") ||
contentType.Contains("mp2t") || url.Contains("abst") ||
url.Contains("f4x") || url.Contains(".fbcdn")
|| url.Contains("http://127.0.0.1:9614")));
}
internal static void ProcessPostYtFormats(Message message, IApp app)
{
//var file = message.File ?? Helpers.GetFileName(new Uri(message.Url));
var manifest = DownloadManifest(message);
if (manifest == null)
{
Log.Debug("Failed to download youtube manifest: " + message.Url);
return;
}
//var manifestText = File.ReadAllText(manifest);
//Log.Debug("Text: {text}", manifestText);
try
{
var (DualVideoItems, VideoItems) = YoutubeDataFormatParser.GetFormats(manifest);
Log.Debug("DualVideoItems: " + DualVideoItems.Count + " VideoItems: " + VideoItems.Count);
message.RequestHeaders.Remove("Content-Type");
if (DualVideoItems != null && DualVideoItems.Count > 0)
{
lock (app)
{
var list = new List<(DualSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)>();
foreach (var item in DualVideoItems)
{
var fileExt = item.MediaContainer;
var mediaItem = new DualSourceHTTPDownloadInfo
{
Uri1 = item.VideoUrl,
Uri2 = item.AudioUrl,
Headers1 = message.RequestHeaders,
Headers2 = message.RequestHeaders,
File = Helpers.SanitizeFileName(item.Title) + "." + fileExt,
Cookies1 = message.Cookies,
Cookies2 = message.Cookies
};
var size = item.Size > 0 ? Helpers.FormatSize(item.Size) : string.Empty;
var displayInfo = new StreamingVideoDisplayInfo
{
Quality = $"[{fileExt.ToUpperInvariant()}] {size} {item.FormatDescription}",
Size = item.Size
};
//var displayText = $"[{fileExt.ToUpperInvariant()}] {size} {item.FormatDescription}";
list.Add((Info: mediaItem, DisplayInfo: displayInfo));
//app.AddVideoNotification(displayText, mediaItem);
}
app.AddVideoNotifications(list);
}
}
if (VideoItems != null && VideoItems.Count > 0)
{
lock (app)
{
var list = new List<(SingleSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)>();
foreach (var item in VideoItems)
{
var fileExt = item.MediaContainer;
var mediaItem = new SingleSourceHTTPDownloadInfo
{
Uri = item.MediaUrl,
Headers = message.RequestHeaders,
File = Helpers.SanitizeFileName(item.Title) + "." + fileExt,
Cookies = message.Cookies
};
var size = item.Size > 0 ? Helpers.FormatSize(item.Size) : string.Empty;
var displayText = $"[{fileExt.ToUpperInvariant()}] {size} {item.FormatDescription}";
var displayInfo = new StreamingVideoDisplayInfo
{
Quality = $"[{fileExt.ToUpperInvariant()}] {size} {item.FormatDescription}",
Size = item.Size
};
list.Add((Info: mediaItem, DisplayInfo: displayInfo));
}
app.AddVideoNotifications(list);
}
}
}
catch (Exception ex)
{
Log.Debug(ex, "Failed to parse youtube manifest");
}
}
internal static void ProcessDashVideo(Message message, IApp app)
{
var file = message.File ?? Helpers.GetFileName(new Uri(message.Url));
Log.Debug("Downloading MPD manifest: " + message.Url);
AddToSkippedRefererList(message.GetRequestHeaderFirstValue("Referer"));
var manifest = DownloadManifest(message);
if (manifest == null) { return; }
var manifestText = File.ReadAllText(manifest);
Log.Debug("MPD playlist");
var mediaEntries = MpdParser.Parse(manifestText.Split('\n'), message.Url);
if (mediaEntries.Count < 1) return;
Log.Debug("Manifest contains: " + mediaEntries.Count);
var count = 0;
foreach (var plc in mediaEntries)
{
foreach ((Representation video, Representation audio) in plc)
{
//prefix added for multi period
var prefix = (count == 0 ? "" : count.ToString() + " ");
if (video != null && audio != null)
{
if (video.Segments.Count == 1 && audio.Segments.Count == 1)
{
Log.Debug("DASH manifest contains 1 audio and 1 video, making it DualSourceHTTPDownload");
var fileExt = (((video.MimeType + "").Contains("mp4") && (audio.MimeType + "").Contains("mp4")) ? "mp4" : "mkv");
var mediaItem = new DualSourceHTTPDownloadInfo
{
Uri1 = video.Segments[0].ToString(),
Uri2 = audio.Segments[0].ToString(),
Headers1 = message.RequestHeaders,
Headers2 = message.RequestHeaders,
File = prefix + Helpers.SanitizeFileName(file) + "." + fileExt,
Cookies1 = message.Cookies,
Cookies2 = message.Cookies
};
var displayText = $"[{fileExt.ToUpperInvariant()}] {GetQualityString(video, audio)}";
Log.Debug("Display text dash: " + displayText);
app.AddVideoNotification(new StreamingVideoDisplayInfo
{
Quality = displayText
}, mediaItem);
}
else
{
var fileExt = (((video.MimeType + "").Contains("mp4") && (audio.MimeType + "").Contains("mp4")) ? "mp4" : "mkv");
var mediaItem = new MultiSourceDASHDownloadInfo
{
VideoSegments = video.Segments,
AudioSegments = audio.Segments,
Headers = message.RequestHeaders,
File = prefix + Helpers.SanitizeFileName(file) + "." + fileExt,
Cookies = message.Cookies,
Duration = Math.Max(video.Duration, audio.Duration),
Url = message.Url,
VideoMimeType = video.MimeType,
AudioMimeType = audio.MimeType
};
var displayText = $"[{fileExt.ToUpperInvariant()}] {GetQualityString(video, audio)}";
Log.Debug("Display text hls: " + displayText);
app.AddVideoNotification(new StreamingVideoDisplayInfo
{
Quality = displayText
}, mediaItem);
}
}
else if (video != null)
{
Log.Debug("DASH manifest contains no audio and 1 video, making it SingleSourceHTTPDownload");
AddSingleItem(video, message, app, prefix, false, file);
}
else if (audio != null)
{
Log.Debug("DASH manifest contains 1 audio and no video, making it SingleSourceHTTPDownload");
AddSingleItem(audio, message, app, prefix, true, file);
}
else
{
Log.Debug("No audio or video in dash mpd");
}
}
count++;
}
}
private static void AddSingleItem(Representation item, Message message, IApp app, string prefix, bool audio, string file)
{
var fileExt = (item.MimeType + "").Contains("mp4") ? "mp4" : "mkv";
var mediaItem = new SingleSourceHTTPDownloadInfo
{
Uri = item.Segments[0].ToString(),
Headers = message.RequestHeaders,
File = prefix + Helpers.SanitizeFileName(file) + "." + fileExt,
Cookies = message.Cookies
};
var quality = audio ? GetQualityString(null, item) : GetQualityString(item, null);
var displayText = $"[{fileExt.ToUpperInvariant()}] {quality}";
Log.Debug("Display text hls: " + displayText);
app.AddVideoNotification(new StreamingVideoDisplayInfo
{
Quality = displayText
}, mediaItem);
}
private static string GetQualityString(Representation video, Representation audio)
{
string GetVideoResolution(Representation video)
{
return video.Height > 0 ? video.Height + "p " : "";
}
string GetAudioLanguage(Representation audio)
{
return audio.Language != null && audio.Language != "und" ? audio.Language + " " : "";
}
string GetBandwidth(params Representation[] args)
{
var sum = 0L;
foreach (var arg in args)
{
if (arg != null && arg.Bandwidth > 0) sum += arg.Bandwidth;
}
return sum > 0 ? (sum / 1024) + " Kbps " : "";
}
var text = new StringBuilder();
if (video != null && audio != null)
{
text.Append(GetVideoResolution(video) + GetBandwidth(video, audio) + " " + GetAudioLanguage(audio));
}
else if (video != null)
{
text.Append(GetVideoResolution(video) + GetBandwidth(video, audio));
}
else if (audio != null)
{
text.Append(GetAudioLanguage(audio) + GetBandwidth(video, audio));
}
return text.ToString();
}
internal static void ProcessHLSVideo(Message message, IApp app)
{
Log.Debug("Downloading HLS manifest: " + message.Url);
AddToSkippedRefererList(message.GetRequestHeaderFirstValue("Referer"));
var manifest = DownloadManifest(message);
if (manifest != null)
{
var manifestText = File.ReadAllText(manifest);
if (manifestText.Contains(HlsParser.EXT_X_STREAM_INF))
{
Log.Debug("Master playlist: " + message.Url);
var playlists = HlsParser.ParseMasterPlaylist(manifestText.Split('\n'), message.Url);
if (playlists != null && playlists.Count > 0)
{
Log.Debug("Master playlist contains: " + playlists.Count);
foreach (var plc in playlists)
{
var type = (plc.AudioPlaylist != null && plc.VideoPlaylist != null ? "MP4" : "TS");
var video = new MultiSourceHLSDownloadInfo
{
VideoUri = plc.VideoPlaylist?.ToString(),
AudioUri = plc.AudioPlaylist?.ToString(),
Headers = message.RequestHeaders,
File = Helpers.SanitizeFileName(message.File ?? (Helpers.GetFileName(new Uri(message.Url)))) +
(plc.AudioPlaylist != null && plc.VideoPlaylist != null ?
"." + type.ToLowerInvariant() : "." + type.ToLowerInvariant()),
Cookies = message.Cookies
};
var displayText = $"{type} {plc.Quality}";
Log.Debug("Display text hls: " + plc.Quality);
app.AddVideoNotification(new StreamingVideoDisplayInfo
{
Quality = displayText
}, video);
}
}
}
else
{
if (manifestText.Contains(HlsParser.EXT_X_I_FRAMES_ONLY))
{
Log.Debug("Skipping EXT_X_I_FRAMES_ONLY: " + message.Url);
return;
}
Log.Debug("Not Master playlist");
var mediaPlaylist = HlsParser.ParseMediaSegments(manifestText.Split('\n'), message.Url);
if (mediaPlaylist == null) return;
var file = Helpers.GetFileName(mediaPlaylist.MediaSegments.Last().Url);
var container = Helpers.GuessContainerFormatFromSegmentExtension(Path.GetExtension(file));
var video = new MultiSourceHLSDownloadInfo
{
VideoUri = message.Url,
Headers = message.RequestHeaders,
File = Helpers.SanitizeFileName(message.File ?? Helpers.GetFileName(new Uri(message.Url))) + ".ts",
Cookies = message.Cookies,
};
var displayText = $"[{container}]";
app.AddVideoNotification(new StreamingVideoDisplayInfo
{
Quality = displayText
}, video);
}
}
}
public static bool ProcessYtDashSegment(Message message, IApp app)
{
try
{
var url = new Uri(message.Url);
if (!(url.Host.Contains("youtube.com") || url.Host.Contains("googlevideo.com")))
{
return false;
}
var contentType = message.GetResponseHeaderValue("Content-Type")?[0]?.ToLowerInvariant();
if (!(contentType != null && (contentType.Contains("audio/") ||
contentType.Contains("video/") ||
contentType.Contains("application/octet"))))
{
return false;
}
var lowUrl = message.Url.ToLowerInvariant();
if (!(lowUrl.Contains("videoplayback") && lowUrl.Contains("itag")))
{
return false;
}
(var path, var query, _) = Helpers.ParseKeyValuePair(message.Url, '?');
string[] arr = query.Split('&');
var yt_url = new StringBuilder();
yt_url.Append(path + "?");
int itag = 0;
long clen = 0;
String id = "";
String mime = "";
for (int i = 0; i < arr.Length; i++)
{
var str = arr[i];
(var key, var val, var success) = Helpers.ParseKeyValuePair(str, '=');
if (!success)
{
continue;
}
if (key.StartsWith("range"))
{
continue;
}
if (key == "itag")
{
itag = Int32.Parse(val);
}
if (key == "clen")
{
clen = Int64.Parse(val);
}
if (key.StartsWith("mime"))
{
mime = Uri.UnescapeDataString(val);
}
if (str.StartsWith("id"))
{
id = val;
}
yt_url.Append(str);
if (i < arr.Length - 1)
{
yt_url.Append('&');
}
}
if (itag != 0 && IsNormalVideo(itag))
{
return false;
}
var info = new DashInfo()
{
Url = yt_url.ToString(),
Length = clen,
IsVideo = mime.StartsWith("video"),
ITag = itag,
ID = id,
Mime = mime,
Headers = message.RequestHeaders,
Cookies = message.Cookies
};
if (AddToQueue(info))
{
var di = GetDASHPair(info);
if (di == null)
{
return true;
}
var video = new DualSourceHTTPDownloadInfo
{
Uri1 = di.Url,
Uri2 = info.Url,
Headers1 = di.Headers,
Headers2 = info.Headers,
File = Helpers.SanitizeFileName(message.File ?? Helpers.GetFileName(new Uri(message.Url))) + ".mkv",
Cookies1 = di.Cookies,
Cookies2 = info.Cookies,
ContentLength = di.Length + info.Length
};
var size = di.Length + info.Length;
Log.Debug("Itag: " + info.ITag + " " + di.ITag);
var quality = Itags.GetValueOrDefault(info.IsVideo ? info.ITag : di.ITag, "MKV");
var displayText = $"[{quality}] {(size > 0 ? Helpers.FormatSize(size) : string.Empty)}";
app.AddVideoNotification(new StreamingVideoDisplayInfo
{
Quality = displayText,
Size = size
}, video);
}
return true;
}
catch { }
return false;
}
internal static void ProcessNormalVideo(Message message2, IApp app)
{
if (IsMediaFragment(message2.GetRequestHeaderFirstValue("Referer")))
{
Log.Debug($"Skipping url:{message2.Url} as it seems a media fragment");
return;
}
var file = (message2.File ?? Helpers.GetFileName(new Uri(message2.Url)));
var type = message2.GetResponseHeaderFirstValue("Content-Type")?.ToLowerInvariant() ?? string.Empty;
var len = message2.GetContentLength();
if (string.IsNullOrEmpty(file))
{
file = Helpers.GetFileName(new Uri(message2.Url));
}
string ext;
if (type.Contains("video/mp4"))
{
ext = "mp4";
}
else if (type.Contains("video/x-flv"))
{
ext = "flv";
}
else if (type.Contains("video/webm"))
{
ext = "mkv";
}
else if (type.Contains("matroska") || type.Contains("mkv"))
{
ext = "mkv";
}
else if (type.Equals("audio/mpeg") || type.Contains("audio/mp3"))
{
ext = "mp3";
}
else if (type.Contains("audio/aac"))
{
ext = "aac";
}
else if (type.Contains("audio/mp4"))
{
ext = "m4a";
}
else
{
return;
}
var video = new SingleSourceHTTPDownloadInfo
{
Uri = message2.Url,
Headers = message2.RequestHeaders,
File = Helpers.SanitizeFileName(file) + "." + ext,
Cookies = message2.Cookies,
ContentLength = len
};
var size = long.Parse(message2.GetResponseHeaderFirstValue("Content-Length"));
var displayText = $"[{ext.ToUpperInvariant()}] {(size > 0 ? Helpers.FormatSize(size) : string.Empty)}";
app.AddVideoNotification(new StreamingVideoDisplayInfo
{
Quality = displayText,
Size = size
}, video); ;
}
public static bool IsNormalVideo(int itag)
{
return ((itag > 4 && itag < 79) || (itag > 81 && itag < 86) || (itag > 99 && itag < 103));
}
private static bool AddIfNew(DashInfo info, List<DashInfo> list)
{
for (int i = list.Count - 1; i >= 0; i--)
{
var di = list[i];
if (di.Length == info.Length && di.ID == info.ID)
{
return false;
}
}
list.Add(info);
return true;
}
internal static bool IsHLS(string contentType)
{
foreach (var key in new string[] { "mpegurl", ".m3u8", "m3u8" })
{
if (contentType.Contains(key))
{
return true;
}
}
return false;
}
internal static bool IsDASH(string contentType)
{
foreach (var key in new string[] { "dash" })
{
if (contentType.Contains(key))
{
return true;
}
}
return false;
}
internal static bool IsYtFormat(string contentType)
{
foreach (var key in new string[] { "application/json" })
{
if (contentType.ToLowerInvariant().Contains(key))
{
return true;
}
}
return false;
}
public static bool AddToQueue(DashInfo info)
{
lock (lockObject)
{
if (videoQueue.Count > 32)
{
videoQueue.RemoveAt(0);
}
if (audioQueue.Count > 32)
{
audioQueue.RemoveAt(0);
}
if (info.IsVideo)
{
return AddIfNew(info, videoQueue);
}
else
{
return AddIfNew(info, audioQueue);
}
}
}
public static DashInfo GetDASHPair(DashInfo info)
{
lock (lockObject)
{
if (info.IsVideo)
{
if (audioQueue.Count < 1)
return null;
for (int i = audioQueue.Count - 1; i >= 0; i--)
{
var di = audioQueue[i];
if (di.ID == info.ID)
{
return di;
}
}
}
else
{
if (videoQueue.Count < 1)
return null;
for (int i = videoQueue.Count - 1; i >= 0; i--)
{
var di = videoQueue[i];
if (di.ID == info.ID)
{
if (lastVid?.Length == di.Length)
{
return null;
}
lastVid = di;
return di;
}
}
}
return null;
}
}
internal static string? DownloadManifest(Message message)
{
try
{
using var http = HttpClientFactory.NewHttpClient(null);
http.Timeout = TimeSpan.FromSeconds(Config.Instance.NetworkTimeout);
var acceptHeaderAdded = false;
var headers = new Dictionary<string, List<string>>();
var cookies = new Dictionary<string, string>();
foreach (var header in message.RequestHeaders)
{
headers.Add(header.Key, header.Value);
if (header.Key.ToLowerInvariant() == "accept")
{
acceptHeaderAdded = true;
}
}
if (acceptHeaderAdded)
{
headers.Add("Accept", new List<string> { "*/*" });
}
foreach (var cookie in message.Cookies)
{
cookies.Add(cookie.Key, cookie.Value);
}
byte[]? body = null;
if (message.RequestBody != null)
{
body = Convert.FromBase64String(message.RequestBody);
}
var request = "POST" == message.RequestMethod ?
http.CreatePostRequest(new Uri(message.Url), headers, cookies, null, body) :
http.CreateGetRequest(new Uri(message.Url), headers, cookies, null);
using var response = http.Send(request);
Log.Debug("Downloading manifest response: " + response.StatusCode);
var path = Path.GetTempFileName();
using var fs = new FileStream(path, FileMode.Create);
response.GetResponseStream().CopyTo(fs);
Log.Debug("Downloaded manifest: " + message.Url + " -> " + path);
return path;
}
catch (Exception e) { Log.Debug(e, "Error"); }
return null;
}
private static bool IsMediaFragment(string referer)
{
if (string.IsNullOrEmpty(referer)) return false;
var sha1 = ComputeHash(referer);
lock (referersToSkip)
{
if (referersToSkip.ContainsKey(sha1))
{
referersToSkip[sha1] = DateTime.Now;
return true;
}
}
return false;
}
private static void AddToSkippedRefererList(string referer)
{
if (string.IsNullOrEmpty(referer)) return;
lock (referersToSkip)
{
referersToSkip[ComputeHash(referer)] = DateTime.Now;
}
}
private static string ComputeHash(string input)
{
using var sha1 = new SHA1Managed();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input));
return string.Concat(hash.Select(b => b.ToString("x2")));
}
public static readonly Dictionary<Int32, string> Itags = new()
{
[5] = "240p",
[133] = "240p",
[6] = "270p",
[134] = "360p",
[135] = "480p",
[136] = "720p",
[264] = "1440p",
[137] = "1080p",
[266] = "2160p",
[139] = "Low bitrate",
[140] = "Med bitrate",
[13] = "Small",
[141] = "Hi bitrate",
[271] = "1440p",
[272] = "2160p",
[17] = "144p",
[18] = "360p",
[22] = "720p",
[278] = "144p",
[160] = "144p",
[34] = "360p",
[35] = "480p",
[36] = "240p",
[37] = "1080p",
[38] = "1080p",
[167] = "360p",
[168] = "480p",
[169] = "720p",
[170] = "1080p",
[298] = "720p",
[43] = "360p",
[171] = "Med bitrate",
[299] = "2160p",
[44] = "480p",
[172] = "Hi bitrate",
[45] = "720p",
[46] = "1080p",
[302] = "720p",
[303] = "1080p",
[308] = "1440p",
[313] = "2160p",
[59] = "480p",
[315] = "2160p",
[78] = "480p",
[82] = "360p 3D",
[83] = "480p 3D",
[84] = "720p 3D",
[85] = "1080p 3D",
[218] = "480p",
[219] = "480p",
[100] = "360p 3D",
[101] = "480p 3D",
[102] = "720p 3D",
[242] = "240p",
[243] = "360p",
[244] = "480p",
[245] = "480p",
[246] = "480p",
[247] = "720p",
[248] = "1080p"
};
}
class DashInfo
{
public string Url;
public long Length;
public bool IsVideo;
public string ID;
public int ITag;
public string Mime;
public Dictionary<string, List<string>> Headers;
public Dictionary<string, string> Cookies;
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net4.5.2</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.AppContext" Version="4.3.0" />
<PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="System.Diagnostics.Tools" Version="4.3.0" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageReference Include="System.Runtime.Numerics" Version="4.3.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal class IsExternalInit { }
}

View File

@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System.Numerics.Hashing
{
internal static class HashHelpers
{
public static int Combine(int h1, int h2)
{
// RyuJIT optimizes this to use the ROL instruction
// Related GitHub pull request: https://github.com/dotnet/coreclr/pull/1830
uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
return ((int)rol5 + h1) ^ h2;
}
}
}

View File

@ -0,0 +1,157 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace System
{
/// <summary>Represent a type can be used to index a collection either from the start or the end.</summary>
/// <remarks>
/// Index is used by the C# compiler to support the new index syntax
/// <code>
/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ;
/// int lastElement = someArray[^1]; // lastElement = 5
/// </code>
/// </remarks>
public readonly struct Index : IEquatable<Index>
{
private readonly int _value;
/// <summary>Construct an Index using a value and indicating if the index is from the start or from the end.</summary>
/// <param name="value">The index value. it has to be zero or positive number.</param>
/// <param name="fromEnd">Indicating if the index is from the start or from the end.</param>
/// <remarks>
/// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Index(int value, bool fromEnd = false)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("ThrowHelper.ThrowValueArgumentOutOfRange_NeedNonNegNumException();");
}
if (fromEnd)
_value = ~value;
else
_value = value;
}
// The following private constructors mainly created for perf reason to avoid the checks
private Index(int value)
{
_value = value;
}
/// <summary>Create an Index pointing at first element.</summary>
public static Index Start => new Index(0);
/// <summary>Create an Index pointing at beyond last element.</summary>
public static Index End => new Index(~0);
/// <summary>Create an Index from the start at the position indicated by the value.</summary>
/// <param name="value">The index value from the start.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Index FromStart(int value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("ThrowHelper.ThrowValueArgumentOutOfRange_NeedNonNegNumException");
}
return new Index(value);
}
/// <summary>Create an Index from the end at the position indicated by the value.</summary>
/// <param name="value">The index value from the end.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Index FromEnd(int value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("ThrowHelper.ThrowValueArgumentOutOfRange_NeedNonNegNumException();");
}
return new Index(~value);
}
/// <summary>Returns the index value.</summary>
public int Value
{
get
{
if (_value < 0)
return ~_value;
else
return _value;
}
}
/// <summary>Indicates whether the index is from the start or the end.</summary>
public bool IsFromEnd => _value < 0;
/// <summary>Calculate the offset from the start using the giving collection length.</summary>
/// <param name="length">The length of the collection that the Index will be used with. length has to be a positive value</param>
/// <remarks>
/// For performance reason, we don't validate the input length parameter and the returned offset value against negative values.
/// we don't validate either the returned offset is greater than the input length.
/// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and
/// then used to index a collection will get out of range exception which will be same affect as the validation.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetOffset(int length)
{
int offset = _value;
if (IsFromEnd)
{
// offset = length - (~value)
// offset = length + (~(~value) + 1)
// offset = length + value + 1
offset += length + 1;
}
return offset;
}
/// <summary>Indicates whether the current Index object is equal to another object of the same type.</summary>
/// <param name="value">An object to compare with this object</param>
public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value;
/// <summary>Indicates whether the current Index object is equal to another Index object.</summary>
/// <param name="other">An object to compare with this object</param>
public bool Equals(Index other) => _value == other._value;
/// <summary>Returns the hash code for this instance.</summary>
public override int GetHashCode() => _value;
/// <summary>Converts integer number to an Index.</summary>
public static implicit operator Index(int value) => FromStart(value);
/// <summary>Converts the value of the current Index object to its equivalent string representation.</summary>
public override string ToString()
{
if (IsFromEnd)
return ToStringFromEnd();
return ((uint)Value).ToString();
}
private string ToStringFromEnd()
{
#if (!NETSTANDARD2_0 && !NETFRAMEWORK)
Span<char> span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value
bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten);
Debug.Assert(formatted);
span[0] = '^';
return new string(span.Slice(0, charsWritten + 1));
#else
return '^' + Value.ToString();
#endif
}
}
}

View File

@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
namespace System.Collections.Generic
{
// Provides the Create factory method for KeyValuePair<TKey, TValue>.
public static class KeyValuePair
{
// Creates a new KeyValuePair<TKey, TValue> from the given values.
public static KeyValuePair<TKey, TValue> Create<TKey, TValue>(TKey key, TValue value)
{
return new KeyValuePair<TKey, TValue>(key, value);
}
/// <summary>
/// Used by KeyValuePair.ToString to reduce generic code
/// </summary>
internal static string PairToString(object? key, object? value)
{
var s = new StringBuilder();
s.Append('[');
if (key != null)
{
s.Append(key.ToString());
}
s.Append(", ");
if (value != null)
{
s.Append(value.ToString());
}
s.Append(']');
return s.ToString();
}
}
//// A KeyValuePair holds a key and a value from a dictionary.
//// It is used by the IEnumerable<T> implementation for both IDictionary<TKey, TValue>
//// and IReadOnlyDictionary<TKey, TValue>.
//[Serializable]
//[System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
//public readonly struct KeyValuePair<TKey, TValue>
//{
// [DebuggerBrowsable(DebuggerBrowsableState.Never)]
// private readonly TKey key; // Do not rename (binary serialization)
// [DebuggerBrowsable(DebuggerBrowsableState.Never)]
// private readonly TValue value; // Do not rename (binary serialization)
// public KeyValuePair(TKey key, TValue value)
// {
// this.key = key;
// this.value = value;
// }
// public TKey Key => key;
// public TValue Value => value;
// public override string ToString()
// {
// return KeyValuePair.PairToString(Key, Value);
// }
// [EditorBrowsable(EditorBrowsableState.Never)]
// public void Deconstruct(out TKey key, out TValue value)
// {
// key = Key;
// value = Value;
// }
//}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
#if NETFRAMEWORK || NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis
{
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
public sealed class NotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue)
{
ReturnValue = returnValue;
}
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
}
#endif

View File

@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
namespace System.Runtime.InteropServices
{
public readonly struct OSPlatform : IEquatable<OSPlatform>
{
public static OSPlatform FreeBSD { get; } = new OSPlatform("FREEBSD");
public static OSPlatform Linux { get; } = new OSPlatform("LINUX");
public static OSPlatform OSX { get; } = new OSPlatform("OSX");
public static OSPlatform Windows { get; } = new OSPlatform("WINDOWS");
internal string Name { get; }
private OSPlatform(string osPlatform)
{
if (osPlatform == null) throw new ArgumentNullException(nameof(osPlatform));
if (osPlatform.Length == 0) throw new ArgumentException("SR.Argument_EmptyValue", nameof(osPlatform));
Name = osPlatform;
}
/// <summary>
/// Creates a new OSPlatform instance.
/// </summary>
/// <remarks>If you plan to call this method frequently, please consider caching its result.</remarks>
public static OSPlatform Create(string osPlatform)
{
return new OSPlatform(osPlatform);
}
public bool Equals(OSPlatform other)
{
return Equals(other.Name);
}
internal bool Equals(string? other)
{
return string.Equals(Name, other, StringComparison.OrdinalIgnoreCase);
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is OSPlatform osPlatform && Equals(osPlatform);
}
public override int GetHashCode()
{
return Name == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(Name);
}
public override string ToString()
{
return Name ?? string.Empty;
}
public static bool operator ==(OSPlatform left, OSPlatform right)
{
return left.Equals(right);
}
public static bool operator !=(OSPlatform left, OSPlatform right)
{
return !(left == right);
}
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CoreFx.Polyfill
{
public static class ProcessStartInfoHelper
{
public static string ArgumentListToArgsString(IList<string> arguments)
{
var stringBuilder = new StringBuilder();
foreach (var argument in arguments)
{
AppendArgument(stringBuilder, argument);
}
return stringBuilder.ToString();
}
public static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length != 0)
{
stringBuilder.Append(' ');
}
// Parsing rules for non-argv[0] arguments:
// - Backslash is a normal character except followed by a quote.
// - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote
// - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote
// - Parsing stops at first whitespace outside of quoted region.
// - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode.
if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument))
{
// Simple case - no quoting or changes needed.
stringBuilder.Append(argument);
}
else
{
stringBuilder.Append(Quote);
int idx = 0;
while (idx < argument.Length)
{
char c = argument[idx++];
if (c == Backslash)
{
int numBackSlash = 1;
while (idx < argument.Length && argument[idx] == Backslash)
{
idx++;
numBackSlash++;
}
if (idx == argument.Length)
{
// We'll emit an end quote after this so must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2);
}
else if (argument[idx] == Quote)
{
// Backslashes will be followed by a quote. Must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2 + 1);
stringBuilder.Append(Quote);
idx++;
}
else
{
// Backslash will not be followed by a quote, so emit as normal characters.
stringBuilder.Append(Backslash, numBackSlash);
}
continue;
}
if (c == Quote)
{
// Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed
// by another quote (which parses differently pre-2008 vs. post-2008.)
stringBuilder.Append(Backslash);
stringBuilder.Append(Quote);
continue;
}
stringBuilder.Append(c);
}
stringBuilder.Append(Quote);
}
}
private static bool ContainsNoWhitespaceOrQuotes(string s)
{
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (char.IsWhiteSpace(c) || c == Quote)
{
return false;
}
}
return true;
}
private const char Quote = '\"';
private const char Backslash = '\\';
}
}

View File

@ -0,0 +1,133 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics.Hashing;
using System.Runtime.CompilerServices;
namespace System
{
/// <summary>Represent a range has start and end indexes.</summary>
/// <remarks>
/// Range is used by the C# compiler to support the range syntax.
/// <code>
/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 };
/// int[] subArray1 = someArray[0..2]; // { 1, 2 }
/// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 }
/// </code>
/// </remarks>
public readonly struct Range : IEquatable<Range>
{
/// <summary>Represent the inclusive start index of the Range.</summary>
public Index Start { get; }
/// <summary>Represent the exclusive end index of the Range.</summary>
public Index End { get; }
/// <summary>Construct a Range object using the start and end indexes.</summary>
/// <param name="start">Represent the inclusive start index of the range.</param>
/// <param name="end">Represent the exclusive end index of the range.</param>
public Range(Index start, Index end)
{
Start = start;
End = end;
}
/// <summary>Indicates whether the current Range object is equal to another object of the same type.</summary>
/// <param name="value">An object to compare with this object</param>
public override bool Equals([NotNullWhen(true)] object? value) =>
value is Range r &&
r.Start.Equals(Start) &&
r.End.Equals(End);
/// <summary>Indicates whether the current Range object is equal to another Range object.</summary>
/// <param name="other">An object to compare with this object</param>
public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End);
/// <summary>Returns the hash code for this instance.</summary>
public override int GetHashCode()
{
#if (!NETSTANDARD2_0 && !NETFRAMEWORK)
return HashCode.Combine(Start.GetHashCode(), End.GetHashCode());
#else
return HashHelpers.Combine(Start.GetHashCode(), End.GetHashCode());
#endif
}
/// <summary>Converts the value of the current Range object to its equivalent string representation.</summary>
public override string ToString()
{
#if (!NETSTANDARD2_0 && !NETFRAMEWORK)
Span<char> span = stackalloc char[2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint
int pos = 0;
if (Start.IsFromEnd)
{
span[0] = '^';
pos = 1;
}
bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten);
Debug.Assert(formatted);
pos += charsWritten;
span[pos++] = '.';
span[pos++] = '.';
if (End.IsFromEnd)
{
span[pos++] = '^';
}
formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten);
Debug.Assert(formatted);
pos += charsWritten;
return new string(span.Slice(0, pos));
#else
return Start.ToString() + ".." + End.ToString();
#endif
}
/// <summary>Create a Range object starting from start index to the end of the collection.</summary>
public static Range StartAt(Index start) => new Range(start, Index.End);
/// <summary>Create a Range object starting from first element in the collection to the end Index.</summary>
public static Range EndAt(Index end) => new Range(Index.Start, end);
/// <summary>Create a Range object starting from first element to the end.</summary>
public static Range All => new Range(Index.Start, Index.End);
/// <summary>Calculate the start offset and length of range object using a collection length.</summary>
/// <param name="length">The length of the collection that the range will be used with. length has to be a positive value.</param>
/// <remarks>
/// For performance reason, we don't validate the input length parameter against negative values.
/// It is expected Range will be used with collections which always have non negative length/count.
/// We validate the range is inside the length scope though.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public (int Offset, int Length) GetOffsetAndLength(int length)
{
int start;
Index startIndex = Start;
if (startIndex.IsFromEnd)
start = length - startIndex.Value;
else
start = startIndex.Value;
int end;
Index endIndex = End;
if (endIndex.IsFromEnd)
end = length - endIndex.Value;
else
end = endIndex.Value;
if ((uint)end > (uint)length || (uint)start > (uint)end)
{
throw new ArgumentOutOfRangeException("ExceptionArgument.length");
}
return (start, end - start);
}
}
}

View File

@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
namespace System.Runtime.InteropServices
{
public static partial class RuntimeInformation
{
/// <summary>
/// Indicates whether the current application is running on the specified platform.
/// </summary>
public static bool IsOSPlatform(OSPlatform osPlatform) => IsOSPlatform(osPlatform.Name);
/// <summary>
/// Indicates whether the current application is running on the specified platform.
/// </summary>
/// <param name="platform">Case-insensitive platform name. Examples: Browser, Linux, FreeBSD, Android, iOS, macOS, tvOS, watchOS, Windows.</param>
public static bool IsOSPlatform(string platform)
{
if (platform == null)
{
throw new ArgumentNullException(nameof(platform));
}
return platform.Equals("WINDOWS", StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -0,0 +1,138 @@
//// Licensed to the .NET Foundation under one or more agreements.
//// The .NET Foundation licenses this file to you under the MIT license.
//#nullable enable
//using System.Resources;
//namespace System
//{
// internal static partial class SR
// {
// private static readonly bool s_usingResourceKeys = false;
// // This method is used to decide if we need to append the exception message parameters to the message when calling SR.Format.
// // by default it returns the value of System.Resources.UseSystemResourceKeys AppContext switch or false if not specified.
// // Native code generators can replace the value this returns based on user input at the time of native code generation.
// // The Linker is also capable of replacing the value of this method when the application is being trimmed.
// private static bool UsingResourceKeys() => s_usingResourceKeys;
// internal static string GetResourceString(string resourceKey)
// {
// if (UsingResourceKeys())
// {
// return resourceKey;
// }
// string? resourceString = null;
// try
// {
// resourceString =
//#if SYSTEM_PRIVATE_CORELIB
// InternalGetResourceString(resourceKey);
//#else
// ResourceManager.GetString(resourceKey);
//#endif
// }
// catch (MissingManifestResourceException) { }
// return resourceString!; // only null if missing resources
// }
// internal static string GetResourceString(string resourceKey, string defaultString)
// {
// string resourceString = GetResourceString(resourceKey);
// return resourceKey == resourceString || resourceString == null ? defaultString : resourceString;
// }
// internal static string Format(string resourceFormat, object? p1)
// {
// if (UsingResourceKeys())
// {
// return string.Join(", ", resourceFormat, p1);
// }
// return string.Format(resourceFormat, p1);
// }
// internal static string Format(string resourceFormat, object? p1, object? p2)
// {
// if (UsingResourceKeys())
// {
// return string.Join(", ", resourceFormat, p1, p2);
// }
// return string.Format(resourceFormat, p1, p2);
// }
// internal static string Format(string resourceFormat, object? p1, object? p2, object? p3)
// {
// if (UsingResourceKeys())
// {
// return string.Join(", ", resourceFormat, p1, p2, p3);
// }
// return string.Format(resourceFormat, p1, p2, p3);
// }
// internal static string Format(string resourceFormat, params object?[]? args)
// {
// if (args != null)
// {
// if (UsingResourceKeys())
// {
// return resourceFormat + ", " + string.Join(", ", args);
// }
// return string.Format(resourceFormat, args);
// }
// return resourceFormat;
// }
// internal static string Format(IFormatProvider? provider, string resourceFormat, object? p1)
// {
// if (UsingResourceKeys())
// {
// return string.Join(", ", resourceFormat, p1);
// }
// return string.Format(provider, resourceFormat, p1);
// }
// internal static string Format(IFormatProvider? provider, string resourceFormat, object? p1, object? p2)
// {
// if (UsingResourceKeys())
// {
// return string.Join(", ", resourceFormat, p1, p2);
// }
// return string.Format(provider, resourceFormat, p1, p2);
// }
// internal static string Format(IFormatProvider? provider, string resourceFormat, object? p1, object? p2, object? p3)
// {
// if (UsingResourceKeys())
// {
// return string.Join(", ", resourceFormat, p1, p2, p3);
// }
// return string.Format(provider, resourceFormat, p1, p2, p3);
// }
// internal static string Format(IFormatProvider? provider, string resourceFormat, params object?[]? args)
// {
// if (args != null)
// {
// if (UsingResourceKeys())
// {
// return resourceFormat + ", " + string.Join(", ", args);
// }
// return string.Format(provider, resourceFormat, args);
// }
// return resourceFormat;
// }
// }
//}

View File

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace System
{
public static class ShimCompatExtensions
{
public static string Join<T>(char separator, IEnumerable<T> values)
{
return string.Join(separator.ToString(), values);
}
public static bool ContainsKey<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key)
{
return dict.TryGetValue(key, out _);
}
public static bool ContainsKey(this GroupCollection dict, string key)
{
try
{
return dict[key] != null;
}
catch
{
return false;
}
}
public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key)
{
return GetValueOrDefault(dict, key, default(TValue)!);
}
/// <summary>
/// Gets the value for a given key if a matching key exists in the dictionary.
/// </summary>
/// <param name="key">The key to search for.</param>
/// <param name="defaultValue">The default value to return if no matching key is found in the dictionary.</param>
/// <returns>
/// The value for the key, or <paramref name="defaultValue"/> if no matching key was found.
/// </returns>
public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue defaultValue)
{
TValue value;
if (dict.TryGetValue(key, out value!))
{
return value;
}
return defaultValue;
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CoreFx.Polyfill
{
public static class StringExtensions
{
public static string[] Split(this string str)
{
return str.Split(str.ToCharArray());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using TraceLog;
#if !NET5_0_OR_GREATER
using NetFX.Polyfill;
#endif
namespace HttpServer
{
internal static class HttpParser
{
public static string ParseRequestStatusLine(string statusLine)
{
try
{
var arr = statusLine.Split(' ');
if (arr.Length > 2)
{
return arr[0];
}
}
catch (Exception ex)
{
Log.Debug(ex, ex.Message);
}
throw new IOException($"Invalid HTTP status line: {statusLine}");
}
internal static void ParseHeader(string headerLine, out string key, out string value)
{
var index = headerLine.IndexOf(":");
if (index > 0)
{
key = headerLine.Substring(0, index).Trim();
value = headerLine.Substring(index + 1).Trim();
}
throw new IOException("Invalid header");
}
internal static long ParseContentLength(Dictionary<string, List<string>> headers)
{
return Int64.Parse(headers.GetValueOrDefault("Content-Length")?[0] ?? "-1");
}
private static bool ShouldKeepAlive(Dictionary<string, List<string>> headers)
{
var value = headers.GetValueOrDefault("Connection")?[0] ?? "close";
if (value.Equals("keep-alive", StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
return false;
}
internal static RequestContext ParseContext(TcpClient tcp)
{
string path = "/";
Dictionary<string, List<string>> headers = new();
byte[]? body = null;
var io = tcp.GetStream();
var first = true;
foreach (var line in LineReader.ReadLines(io))
{
if (first)
{
path = ParseRequestStatusLine(line);
first = false;
}
ParseHeader(line, out string headerName, out string headerValue);
var values = headers.GetValueOrDefault(headerName, new List<string>());
values.Add(headerName);
headers[headerName] = values;
}
var contentLength = ParseContentLength(headers);
if (contentLength > 0)
{
body = new byte[contentLength];
using var ms = new MemoryStream(body);
io.CopyTo(ms, contentLength);
ms.Close();
}
return new RequestContext(path, headers, body, tcp, ShouldKeepAlive(headers));
}
internal static void CopyTo(this Stream stream, Stream destination, long limit = Int64.MaxValue)
{
byte[] buffer = new byte[8192];
int read;
while ((read = stream.Read(buffer, 0, (int)Math.Min(buffer.Length, limit))) != 0)
{
destination.Write(buffer, 0, read);
limit -= read;
}
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using TraceLog;
namespace HttpServer
{
public class NanoServer
{
private readonly TcpListener listener;
public event EventHandler<RequestContextEventArgs>? RequestReceived;
public NanoServer(int port) : this(IPAddress.Any, port) { }
public NanoServer(IPAddress host, int port)
{
this.listener = new TcpListener(host, port);
}
public void Start()
{
listener.Start();
while (true)
{
var tcp = listener.AcceptTcpClient();
ProcessRequest(tcp);
}
}
public void Stop()
{
try
{
this.listener.Stop();
}
catch { }
}
private void ProcessRequest(TcpClient tcp)
{
new Thread(() =>
{
try
{
while (true)
{
var ctx = HttpParser.ParseContext(tcp);
this.RequestReceived?.Invoke(this, new RequestContextEventArgs(ctx));
if (!ctx.KeepAlive)
{
break;
}
}
}
catch (Exception ex)
{
Log.Debug(ex, ex.Message);
throw;
}
finally
{
try { tcp.Close(); } catch { }
}
}).Start();
}
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net3.5;net4.5;net4.7.2;net5.0</TargetFrameworks>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net5.0' ">
<ProjectReference Include="..\NetFX.Polyfill\NetFX.Polyfill.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TraceLog\TraceLog.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace HttpServer
{
internal static class LineReader
{
internal static IEnumerable<string> ReadLines(Stream stream)
{
var buffer = new StringBuilder();
while (true)
{
var x = stream.ReadByte();
if (x == -1) throw new IOException("Unexpected EOF");
if (x == '\n')
{
if (buffer.Length == 0) yield break;
yield return buffer.ToString();
}
if (x != '\r') buffer.Append((char)x);
}
}
}
}

View File

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
#if !NET5_0_OR_GREATER
using NetFX.Polyfill;
#endif
namespace HttpServer
{
public class RequestContext
{
private TcpClient tcp;
public string RequestPath { get; }
public byte[]? RequestBody { get; }
public Dictionary<string, List<string>> RequestHeaders { get; }
public byte[]? ResponseBody { set; get; }
public Dictionary<string, List<string>> ResponseHeaders { set; get; }
public ResponseStatus ResponseStatus { set; get; }
public bool KeepAlive { get; private set; }
internal RequestContext(string path, Dictionary<string, List<string>> headers, byte[]? body, TcpClient tcp, bool keepAlive)
{
this.RequestPath = path;
this.RequestHeaders = headers;
this.RequestBody = body;
this.tcp = tcp;
this.ResponseHeaders = new();
this.ResponseStatus = new ResponseStatus { StatusCode = 200, StatusMessage = "OK" };
this.KeepAlive = keepAlive;
}
public void SendResponse()
{
var io = this.tcp.GetStream();
var responseBuffer = new StringBuilder();
responseBuffer.Append($"HTTP/1.0 {this.ResponseStatus.StatusCode} {this.ResponseStatus.StatusMessage}\r\n");
foreach (var headerName in ResponseHeaders.Keys)
{
if (headerName.Equals("content-length", StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
foreach (var value in ResponseHeaders[headerName])
{
responseBuffer.Append($"{headerName}: {value}\r\n");
}
}
responseBuffer.Append($"Connection: keep-alive\r\n");
if (ResponseBody != null && ResponseBody.Length > 0)
{
responseBuffer.Append($"Content-Length: {ResponseBody.Length}\r\n");
}
responseBuffer.Append("\r\n");
var bytes = Encoding.UTF8.GetBytes(responseBuffer.ToString());
io.Write(bytes, 0, bytes.Length);
if (ResponseBody != null && ResponseBody.Length > 0)
{
io.Write(ResponseBody, 0, ResponseBody.Length);
}
io.Flush();
}
public void AddResponseHeader(string name, string value)
{
var values = this.ResponseHeaders.GetValueOrDefault(name, new List<string>(1));
values.Add(value);
this.ResponseHeaders[name] = values;
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace HttpServer
{
public class RequestContextEventArgs : EventArgs
{
public RequestContext RequestContext { get; }
public RequestContextEventArgs(RequestContext context)
{
this.RequestContext = context;
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace HttpServer
{
public struct ResponseStatus
{
public string StatusMessage;
public int StatusCode;
}
}

View File

@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
internal static partial class Interop
{
internal static partial class WinHttp
{
internal class SafeWinHttpHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private SafeWinHttpHandle? _parentHandle;
public SafeWinHttpHandle() : base(true)
{
}
public static void DisposeAndClearHandle(ref SafeWinHttpHandle? safeHandle)
{
if (safeHandle != null)
{
safeHandle.Dispose();
safeHandle = null;
}
}
public void SetParentHandle(SafeWinHttpHandle parentHandle)
{
Debug.Assert(_parentHandle == null);
Debug.Assert(parentHandle != null);
Debug.Assert(!parentHandle.IsInvalid);
bool ignore = false;
parentHandle.DangerousAddRef(ref ignore);
_parentHandle = parentHandle;
}
// Important: WinHttp API calls should not happen while another WinHttp call for the same handle did not
// return. During finalization that was not initiated by the Dispose pattern we don't expect any other WinHttp
// calls in progress.
protected override bool ReleaseHandle()
{
if (_parentHandle != null)
{
_parentHandle.DangerousRelease();
_parentHandle = null;
}
return Interop.WinHttp.WinHttpCloseHandle(handle);
}
}
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net3.5;net4.5;net4.7.2;net5.0</TargetFrameworks>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,208 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Runtime.InteropServices;
using System.Text;
Interop
{
class Libraries
{
public const string WinHttp = "winhttp.dll";
}
internal static partial class WinHttp
{
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
public static extern SafeWinHttpHandle WinHttpOpen(
IntPtr userAgent,
uint accessType,
string? proxyName,
string? proxyBypass, int flags);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpCloseHandle(
IntPtr handle);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
public static extern SafeWinHttpHandle WinHttpConnect(
SafeWinHttpHandle sessionHandle,
string serverName,
ushort serverPort,
uint reserved);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
public static extern SafeWinHttpHandle WinHttpOpenRequest(
SafeWinHttpHandle connectHandle,
string verb,
string objectName,
string? version,
string referrer,
string acceptTypes,
uint flags);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpAddRequestHeaders(
SafeWinHttpHandle requestHandle,
#pragma warning disable CA1838 // Uses pooled StringBuilder
[In] StringBuilder headers,
#pragma warning restore CA1838
uint headersLength,
uint modifiers);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpAddRequestHeaders(
SafeWinHttpHandle requestHandle,
string headers,
uint headersLength,
uint modifiers);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpSendRequest(
SafeWinHttpHandle requestHandle,
IntPtr headers,
uint headersLength,
IntPtr optional,
uint optionalLength,
uint totalLength,
IntPtr context);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpReceiveResponse(
SafeWinHttpHandle requestHandle,
IntPtr reserved);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpQueryDataAvailable(
SafeWinHttpHandle requestHandle,
IntPtr parameterIgnoredAndShouldBeNullForAsync);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpReadData(
SafeWinHttpHandle requestHandle,
IntPtr buffer,
uint bufferSize,
IntPtr parameterIgnoredAndShouldBeNullForAsync);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpQueryHeaders(
SafeWinHttpHandle requestHandle,
uint infoLevel,
string name,
IntPtr buffer,
ref uint bufferLength,
ref uint index);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpQueryHeaders(
SafeWinHttpHandle requestHandle,
uint infoLevel,
string name,
ref uint number,
ref uint bufferLength,
IntPtr index);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpQueryOption(
SafeWinHttpHandle handle,
uint option,
ref IntPtr buffer,
ref uint bufferSize);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpQueryOption(
SafeWinHttpHandle handle,
uint option,
IntPtr buffer,
ref uint bufferSize);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpQueryOption(
SafeWinHttpHandle handle,
uint option,
ref uint buffer,
ref uint bufferSize);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpWriteData(
SafeWinHttpHandle requestHandle,
IntPtr buffer,
uint bufferSize,
IntPtr parameterIgnoredAndShouldBeNullForAsync);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpSetOption(
SafeWinHttpHandle handle,
uint option,
ref uint optionData,
uint optionLength = sizeof(uint));
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpSetOption(
SafeWinHttpHandle handle,
uint option,
IntPtr optionData,
uint optionLength);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpSetCredentials(
SafeWinHttpHandle requestHandle,
uint authTargets,
uint authScheme,
string? userName,
string? password,
IntPtr reserved);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpQueryAuthSchemes(
SafeWinHttpHandle requestHandle,
out uint supportedSchemes,
out uint firstScheme,
out uint authTarget);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpSetTimeouts(
SafeWinHttpHandle handle,
int resolveTimeout,
int connectTimeout,
int sendTimeout,
int receiveTimeout);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpGetIEProxyConfigForCurrentUser(
out WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WinHttpGetProxyForUrl(
SafeWinHttpHandle? sessionHandle, string url,
ref WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions,
out WINHTTP_PROXY_INFO proxyInfo);
[DllImport(Interop.Libraries.WinHttp, CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr WinHttpSetStatusCallback(
SafeWinHttpHandle handle,
WINHTTP_STATUS_CALLBACK callback,
uint notificationFlags,
IntPtr reserved);
}
}

View File

@ -0,0 +1,307 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Runtime.InteropServices;
using System.Text;
internal static partial class Interop
{
internal static partial class WinHttp
{
public const uint ERROR_SUCCESS = 0;
public const uint ERROR_FILE_NOT_FOUND = 2;
public const uint ERROR_INVALID_HANDLE = 6;
public const uint ERROR_INVALID_PARAMETER = 87;
public const uint ERROR_INSUFFICIENT_BUFFER = 122;
public const uint ERROR_NOT_FOUND = 1168;
public const uint ERROR_WINHTTP_INVALID_OPTION = 12009;
public const uint ERROR_WINHTTP_LOGIN_FAILURE = 12015;
public const uint ERROR_WINHTTP_OPERATION_CANCELLED = 12017;
public const uint ERROR_WINHTTP_INCORRECT_HANDLE_STATE = 12019;
public const uint ERROR_WINHTTP_CONNECTION_ERROR = 12030;
public const uint ERROR_WINHTTP_RESEND_REQUEST = 12032;
public const uint ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED = 12044;
public const uint ERROR_WINHTTP_HEADER_NOT_FOUND = 12150;
public const uint ERROR_WINHTTP_SECURE_FAILURE = 12175;
public const uint ERROR_WINHTTP_AUTODETECTION_FAILED = 12180;
public const uint WINHTTP_OPTION_PROXY = 38;
public const uint WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0;
public const uint WINHTTP_ACCESS_TYPE_NO_PROXY = 1;
public const uint WINHTTP_ACCESS_TYPE_NAMED_PROXY = 3;
public const uint WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY = 4;
public const uint WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001;
public const uint WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002;
public const uint WINHTTP_AUTOPROXY_HOST_KEEPCASE = 0x00000004;
public const uint WINHTTP_AUTOPROXY_HOST_LOWERCASE = 0x00000008;
public const uint WINHTTP_AUTOPROXY_RUN_INPROCESS = 0x00010000;
public const uint WINHTTP_AUTOPROXY_RUN_OUTPROCESS_ONLY = 0x00020000;
public const uint WINHTTP_AUTOPROXY_NO_DIRECTACCESS = 0x00040000;
public const uint WINHTTP_AUTOPROXY_NO_CACHE_CLIENT = 0x00080000;
public const uint WINHTTP_AUTOPROXY_NO_CACHE_SVC = 0x00100000;
public const uint WINHTTP_AUTOPROXY_SORT_RESULTS = 0x00400000;
public const uint WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001;
public const uint WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002;
public const string WINHTTP_NO_PROXY_NAME = null;
public const string WINHTTP_NO_PROXY_BYPASS = null;
public const uint WINHTTP_ADDREQ_FLAG_ADD = 0x20000000;
public const uint WINHTTP_ADDREQ_FLAG_REPLACE = 0x80000000;
public const string WINHTTP_NO_REFERER = null;
public const string WINHTTP_DEFAULT_ACCEPT_TYPES = null;
public const ushort INTERNET_DEFAULT_PORT = 0;
public const ushort INTERNET_DEFAULT_HTTP_PORT = 80;
public const ushort INTERNET_DEFAULT_HTTPS_PORT = 443;
public const uint WINHTTP_FLAG_SECURE = 0x00800000;
public const uint WINHTTP_FLAG_ESCAPE_DISABLE = 0x00000040;
public const uint WINHTTP_FLAG_AUTOMATIC_CHUNKING = 0x00000200;
public const uint WINHTTP_QUERY_FLAG_NUMBER = 0x20000000;
public const uint WINHTTP_QUERY_VERSION = 18;
public const uint WINHTTP_QUERY_STATUS_CODE = 19;
public const uint WINHTTP_QUERY_STATUS_TEXT = 20;
public const uint WINHTTP_QUERY_RAW_HEADERS = 21;
public const uint WINHTTP_QUERY_RAW_HEADERS_CRLF = 22;
public const uint WINHTTP_QUERY_FLAG_TRAILERS = 0x02000000;
public const uint WINHTTP_QUERY_CONTENT_ENCODING = 29;
public const uint WINHTTP_QUERY_SET_COOKIE = 43;
public const uint WINHTTP_QUERY_CUSTOM = 65535;
public const string WINHTTP_HEADER_NAME_BY_INDEX = null;
public const byte[] WINHTTP_NO_OUTPUT_BUFFER = null;
public const uint WINHTTP_OPTION_DECOMPRESSION = 118;
public const uint WINHTTP_DECOMPRESSION_FLAG_GZIP = 0x00000001;
public const uint WINHTTP_DECOMPRESSION_FLAG_DEFLATE = 0x00000002;
public const uint WINHTTP_DECOMPRESSION_FLAG_ALL = WINHTTP_DECOMPRESSION_FLAG_GZIP | WINHTTP_DECOMPRESSION_FLAG_DEFLATE;
public const uint WINHTTP_OPTION_REDIRECT_POLICY = 88;
public const uint WINHTTP_OPTION_REDIRECT_POLICY_NEVER = 0;
public const uint WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP = 1;
public const uint WINHTTP_OPTION_REDIRECT_POLICY_ALWAYS = 2;
public const uint WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS = 89;
public const uint WINHTTP_OPTION_MAX_CONNS_PER_SERVER = 73;
public const uint WINHTTP_OPTION_MAX_CONNS_PER_1_0_SERVER = 74;
public const uint WINHTTP_OPTION_DISABLE_FEATURE = 63;
public const uint WINHTTP_DISABLE_COOKIES = 0x00000001;
public const uint WINHTTP_DISABLE_REDIRECTS = 0x00000002;
public const uint WINHTTP_DISABLE_AUTHENTICATION = 0x00000004;
public const uint WINHTTP_DISABLE_KEEP_ALIVE = 0x00000008;
public const uint WINHTTP_OPTION_ENABLE_FEATURE = 79;
public const uint WINHTTP_ENABLE_SSL_REVOCATION = 0x00000001;
public const uint WINHTTP_OPTION_CLIENT_CERT_CONTEXT = 47;
public const uint WINHTTP_OPTION_CLIENT_CERT_ISSUER_LIST = 94;
public const uint WINHTTP_OPTION_SERVER_CERT_CONTEXT = 78;
public const uint WINHTTP_OPTION_SECURITY_FLAGS = 31;
public const uint WINHTTP_OPTION_SECURE_PROTOCOLS = 84;
public const uint WINHTTP_FLAG_SECURE_PROTOCOL_SSL2 = 0x00000008;
public const uint WINHTTP_FLAG_SECURE_PROTOCOL_SSL3 = 0x00000020;
public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 = 0x00000080;
public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 = 0x00000200;
public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 = 0x00000800;
public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3 = 0x00002000;
public const uint SECURITY_FLAG_IGNORE_UNKNOWN_CA = 0x00000100;
public const uint SECURITY_FLAG_IGNORE_CERT_DATE_INVALID = 0x00002000;
public const uint SECURITY_FLAG_IGNORE_CERT_CN_INVALID = 0x00001000;
public const uint SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE = 0x00000200;
public const uint WINHTTP_OPTION_AUTOLOGON_POLICY = 77;
public const uint WINHTTP_AUTOLOGON_SECURITY_LEVEL_MEDIUM = 0; // default creds only sent to intranet servers (default)
public const uint WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW = 1; // default creds set to all servers
public const uint WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH = 2; // default creds never sent
public const uint WINHTTP_AUTH_SCHEME_BASIC = 0x00000001;
public const uint WINHTTP_AUTH_SCHEME_NTLM = 0x00000002;
public const uint WINHTTP_AUTH_SCHEME_PASSPORT = 0x00000004;
public const uint WINHTTP_AUTH_SCHEME_DIGEST = 0x00000008;
public const uint WINHTTP_AUTH_SCHEME_NEGOTIATE = 0x00000010;
public const uint WINHTTP_AUTH_TARGET_SERVER = 0x00000000;
public const uint WINHTTP_AUTH_TARGET_PROXY = 0x00000001;
public const uint WINHTTP_OPTION_USERNAME = 0x1000;
// [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Suppression approved. It is property descriptor, not secret value.")]
public const uint WINHTTP_OPTION_PASSWORD = 0x1001;
public const uint WINHTTP_OPTION_PROXY_USERNAME = 0x1002;
// [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Suppression approved. It is property descriptor, not secret value.")]
public const uint WINHTTP_OPTION_PROXY_PASSWORD = 0x1003;
public const uint WINHTTP_OPTION_SERVER_SPN_USED = 106;
public const uint WINHTTP_OPTION_SERVER_CBT = 108;
public const uint WINHTTP_OPTION_CONNECT_TIMEOUT = 3;
public const uint WINHTTP_OPTION_SEND_TIMEOUT = 5;
public const uint WINHTTP_OPTION_RECEIVE_TIMEOUT = 6;
public const uint WINHTTP_OPTION_URL = 34;
public const uint WINHTTP_OPTION_MAX_RESPONSE_HEADER_SIZE = 91;
public const uint WINHTTP_OPTION_MAX_RESPONSE_DRAIN_SIZE = 92;
public const uint WINHTTP_OPTION_CONNECTION_INFO = 93;
public const uint WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS = 111;
public const uint WINHTTP_OPTION_ENABLE_HTTP2_PLUS_CLIENT_CERT = 161;
public const uint WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL = 133;
public const uint WINHTTP_OPTION_HTTP_PROTOCOL_USED = 134;
public const uint WINHTTP_PROTOCOL_FLAG_HTTP2 = 0x1;
public const uint WINHTTP_HTTP2_PLUS_CLIENT_CERT_FLAG = 0x1;
public const uint WINHTTP_OPTION_DISABLE_STREAM_QUEUE = 139;
public const uint WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET = 114;
public const uint WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT = 115;
public const uint WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL = 116;
public const uint WINHTTP_OPTION_WEB_SOCKET_RECEIVE_BUFFER_SIZE = 122;
public const uint WINHTTP_OPTION_WEB_SOCKET_SEND_BUFFER_SIZE = 123;
public const uint WINHTTP_OPTION_TCP_KEEPALIVE = 152;
public const uint WINHTTP_OPTION_STREAM_ERROR_CODE = 159;
public enum WINHTTP_WEB_SOCKET_BUFFER_TYPE
{
WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE = 0,
WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE = 1,
WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE = 2,
WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE = 3,
WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE = 4
}
public const uint WINHTTP_OPTION_CONTEXT_VALUE = 45;
public const uint WINHTTP_FLAG_ASYNC = 0x10000000;
public const uint WINHTTP_CALLBACK_STATUS_RESOLVING_NAME = 0x00000001;
public const uint WINHTTP_CALLBACK_STATUS_NAME_RESOLVED = 0x00000002;
public const uint WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER = 0x00000004;
public const uint WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER = 0x00000008;
public const uint WINHTTP_CALLBACK_STATUS_SENDING_REQUEST = 0x00000010;
public const uint WINHTTP_CALLBACK_STATUS_REQUEST_SENT = 0x00000020;
public const uint WINHTTP_CALLBACK_STATUS_RECEIVING_RESPONSE = 0x00000040;
public const uint WINHTTP_CALLBACK_STATUS_RESPONSE_RECEIVED = 0x00000080;
public const uint WINHTTP_CALLBACK_STATUS_CLOSING_CONNECTION = 0x00000100;
public const uint WINHTTP_CALLBACK_STATUS_CONNECTION_CLOSED = 0x00000200;
public const uint WINHTTP_CALLBACK_STATUS_HANDLE_CREATED = 0x00000400;
public const uint WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING = 0x00000800;
public const uint WINHTTP_CALLBACK_STATUS_DETECTING_PROXY = 0x00001000;
public const uint WINHTTP_CALLBACK_STATUS_REDIRECT = 0x00004000;
public const uint WINHTTP_CALLBACK_STATUS_INTERMEDIATE_RESPONSE = 0x00008000;
public const uint WINHTTP_CALLBACK_STATUS_SECURE_FAILURE = 0x00010000;
public const uint WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE = 0x00020000;
public const uint WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE = 0x00040000;
public const uint WINHTTP_CALLBACK_STATUS_READ_COMPLETE = 0x00080000;
public const uint WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE = 0x00100000;
public const uint WINHTTP_CALLBACK_STATUS_REQUEST_ERROR = 0x00200000;
public const uint WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE = 0x00400000;
public const uint WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE = 0x01000000;
public const uint WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE = 0x02000000;
public const uint WINHTTP_CALLBACK_STATUS_SHUTDOWN_COMPLETE = 0x04000000;
public const uint WINHTTP_CALLBACK_FLAG_SEND_REQUEST =
WINHTTP_CALLBACK_STATUS_SENDING_REQUEST |
WINHTTP_CALLBACK_STATUS_REQUEST_SENT;
public const uint WINHTTP_CALLBACK_FLAG_HANDLES =
WINHTTP_CALLBACK_STATUS_HANDLE_CREATED |
WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING;
public const uint WINHTTP_CALLBACK_FLAG_REDIRECT = WINHTTP_CALLBACK_STATUS_REDIRECT;
public const uint WINHTTP_CALLBACK_FLAG_SECURE_FAILURE = WINHTTP_CALLBACK_STATUS_SECURE_FAILURE;
public const uint WINHTTP_CALLBACK_FLAG_SENDREQUEST_COMPLETE = WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE;
public const uint WINHTTP_CALLBACK_FLAG_HEADERS_AVAILABLE = WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE;
public const uint WINHTTP_CALLBACK_FLAG_DATA_AVAILABLE = WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE;
public const uint WINHTTP_CALLBACK_FLAG_READ_COMPLETE = WINHTTP_CALLBACK_STATUS_READ_COMPLETE;
public const uint WINHTTP_CALLBACK_FLAG_WRITE_COMPLETE = WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE;
public const uint WINHTTP_CALLBACK_FLAG_REQUEST_ERROR = WINHTTP_CALLBACK_STATUS_REQUEST_ERROR;
public const uint WINHTTP_CALLBACK_FLAG_GETPROXYFORURL_COMPLETE = WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE;
public const uint WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS =
WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE |
WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE |
WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE |
WINHTTP_CALLBACK_STATUS_READ_COMPLETE |
WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE |
WINHTTP_CALLBACK_STATUS_REQUEST_ERROR |
WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE;
public const uint WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS = 0xFFFFFFFF;
public const uint WININET_E_CONNECTION_RESET = 0x80072EFF;
public const int WINHTTP_INVALID_STATUS_CALLBACK = -1;
public delegate void WINHTTP_STATUS_CALLBACK(
IntPtr handle,
IntPtr context,
uint internetStatus,
IntPtr statusInformation,
uint statusInformationLength);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WINHTTP_AUTOPROXY_OPTIONS
{
public uint Flags;
public uint AutoDetectFlags;
[MarshalAs(UnmanagedType.LPWStr)]
public string? AutoConfigUrl;
public IntPtr Reserved1;
public uint Reserved2;
[MarshalAs(UnmanagedType.Bool)]
public bool AutoLoginIfChallenged;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WINHTTP_CURRENT_USER_IE_PROXY_CONFIG
{
[MarshalAs(UnmanagedType.Bool)]
public bool AutoDetect;
public IntPtr AutoConfigUrl;
public IntPtr Proxy;
public IntPtr ProxyBypass;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WINHTTP_PROXY_INFO
{
public uint AccessType;
public IntPtr Proxy;
public IntPtr ProxyBypass;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WINHTTP_ASYNC_RESULT
{
public IntPtr dwResult;
public uint dwError;
}
[StructLayout(LayoutKind.Sequential)]
public struct tcp_keepalive
{
public uint onoff;
public uint keepalivetime;
public uint keepaliveinterval;
}
public const uint API_RECEIVE_RESPONSE = 1;
public const uint API_QUERY_DATA_AVAILABLE = 2;
public const uint API_READ_DATA = 3;
public const uint API_WRITE_DATA = 4;
public const uint API_SEND_REQUEST = 5;
public enum WINHTTP_WEB_SOCKET_OPERATION
{
WINHTTP_WEB_SOCKET_SEND_OPERATION = 0,
WINHTTP_WEB_SOCKET_RECEIVE_OPERATION = 1,
WINHTTP_WEB_SOCKET_CLOSE_OPERATION = 2,
WINHTTP_WEB_SOCKET_SHUTDOWN_OPERATION = 3
}
}
}

32
app/XDM/Lang/English.txt Normal file
View File

@ -0,0 +1,32 @@
DESC_NEW=New
DESC_DEL=Delete
MENU_PAUSE=Pause
MENU_RESUME=Resume
CTX_OPEN_FILE=Open
CTX_OPEN_FOLDER=Open folder
LBL_SEARCH=Search
TITLE_SETTINGS=Settings
MENU_DELETE_COMPLETED=Remove finished downloads
MENU_IMPORT=Import
MENU_EXPORT=Export
MENU_EXIT=Exit
MENU_UPDATE=Check for update
MENU_ABOUT=About XDM
LBL_REPORT_PROBLEM=Report a problem
LBL_SUPPORT_PAGE=Help and support
MENU_DELETE_DWN=Delete downloads
LBL_NEW_DOWNLOAD=New download
LBL_MENU=Menu
LBL_IMPORT_EXPORT=Import/Export
SETTINGS_MONITORING=Browser monitoring
ALL_UNFINISHED=Incomplete
ALL_FINISHED=Complete
CAT_DOCUMENTS=Document
CAT_COMPRESSED=Compressed
CAT_MUSIC=Music
CAT_VIDEOS=Video
CAT_PROGRAMS=Application
LBL_VIDEO_DOWNLOAD=Video download
MENU_BATCH_DOWNLOAD=Batch download
LBL_QUEUE_MODE=Download {0} at a time
DESC_Q_TITLE=Queue and scheduler

View File

@ -0,0 +1,42 @@
using System;
using System.Text.RegularExpressions;
namespace MediaParser.Dash
{
public static class DashUtil
{
private static Regex XS_DURATION_PATTERN = new Regex(
"^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$" +
"(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static long ParseXsDuration(string value)
{
var match = XS_DURATION_PATTERN.Match(value);
if (match.Success)
{
var negated = !string.IsNullOrEmpty(match.Groups[1].ToString());
// Durations containing years and months aren't completely defined. We assume there are
// 30.4368 days in a month, and 365.242 days in a year.
var years = match.Groups[3].Value;//matcher.group(3);
var durationSeconds = (!string.IsNullOrEmpty(years)) ? Double.Parse(years) * 31556908 : 0;
var months = match.Groups[5].ToString();//matcher.group(5);
durationSeconds += (!string.IsNullOrEmpty(months)) ? Double.Parse(months) * 2629739 : 0;
var days = match.Groups[7].ToString();//matcher.group(7);
durationSeconds += (!string.IsNullOrEmpty(days)) ? Double.Parse(days) * 86400 : 0;
var hours = match.Groups[10].ToString();//matcher.group(10);
durationSeconds += (!string.IsNullOrEmpty(hours)) ? Double.Parse(hours) * 3600 : 0;
var minutes = match.Groups[12].ToString();//matcher.group(12);
durationSeconds += (!string.IsNullOrEmpty(minutes)) ? Double.Parse(minutes) * 60 : 0;
var seconds = match.Groups[14].ToString();//matcher.group(14);
durationSeconds += (!string.IsNullOrEmpty(seconds)) ? Double.Parse(seconds) : 0;
var durationMillis = (long)(durationSeconds * 1000);
return negated ? -durationMillis : durationMillis;
}
else
{
return (long)(Double.Parse(value) * 3600 * 1000);
}
}
}
}

View File

@ -0,0 +1,431 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.IO;
using MediaParser.Util;
#if !NET5_0_OR_GREATER
using NetFX.Polyfill;
#endif
namespace MediaParser.Dash
{
public static class MpdParser
{
private static string trickModeUri = "http://dashif.org/guidelines/trickmode";
private static readonly Regex TemplatePattern =
new Regex(@"\$(RepresentationID|Time|Time%0(?<time_digits>[\d]+)(?<time_dx>[dx])?|Number|Number%0(?<num_digits>[\d]+)(?<num_dx>[dx])?|Bandwidth)\$");
//@"\$(RepresentationID|Time|Time(%0([\d]+)([dx])?)?|Number|Number(%0([\d]+)([dx])?)?|Bandwidth)\$");
public static IList<IList<(Representation Video, Representation Audio)>> Parse(
string[] manifestLines,
string playlistUrl)
{
using var memstream = new MemoryStream(Encoding.UTF8.GetBytes(string.Join("\n", manifestLines)));
var xmldoc = new XmlDocument();
xmldoc.Load(new XmlTextReader(memstream)
{
Namespaces = false
});
if (xmldoc.DocumentElement?.Name != "MPD")
throw new Exception("Missing MPD start tag: " + xmldoc.DocumentElement.Name);
if (xmldoc.DocumentElement.Attributes["type"]?.Value == "dynamic")
throw new Exception("Manifest type dynamic is not supported");
if (xmldoc.DocumentElement.SelectSingleNode("descendant::ContentProtection") != null)
throw new Exception("Encrypted manifest");
var mediaList = new List<IList<(Representation Video, Representation Audio)>>();
var mediaPresentationDuration =
DashUtil.ParseXsDuration(xmldoc.DocumentElement.Attributes["mediaPresentationDuration"]?.Value ?? "0");
var baseUrl = new Uri(playlistUrl);
var baseUrlNodeRoot = xmldoc.DocumentElement.SelectSingleNode("child::BaseURL");
if (baseUrlNodeRoot != null)
{
baseUrl = UrlResolver.Resolve(baseUrl, baseUrlNodeRoot.InnerText);
}
var periods = xmldoc.DocumentElement.SelectNodes("child::Period");
if (periods == null || periods.Count < 1) throw new Exception("No period found!");
if (periods.Count > 1)
{
var periodDurations = CalculatePeriodDurationsIfMissing(periods, mediaPresentationDuration);
for (var i = 0; i < periods.Count; i++)
{
var period = periods[i];
mediaList.Add(ParsePeriod(period, baseUrl, periodDurations[i]));
}
}
else if (periods.Count == 1)
{
mediaList.Add(ParsePeriod(periods[0], baseUrl, mediaPresentationDuration));
}
return mediaList;
}
private static IList<(Representation Video, Representation Audio)> ParsePeriod(XmlNode period, Uri baseUrl, long mediaPresentationDuration)
{
var periodDuration = mediaPresentationDuration;
if (period.Attributes?["duration"] != null)
{
periodDuration = DashUtil.ParseXsDuration(period.Attributes["duration"]!.Value!);
}
var xmlBaseUrl = period.SelectSingleNode("child::BaseURL");
if (xmlBaseUrl != null)
{
baseUrl = UrlResolver.Resolve(baseUrl, xmlBaseUrl.InnerText);
}
//first check if Indexed addressing is used
//https://dashif-documents.azurewebsites.net/Guidelines-TimingModel/master/Guidelines-TimingModel.html#addressing-indexed
var hasParentSegmentBase = period.SelectSingleNode("child::SegmentBase") != null;
var xmlAdaptationSets = period.SelectNodes("child::AdaptationSet");
var audioList = new List<Representation>();
var videoList = new List<Representation>();
for (var i = 0; i < xmlAdaptationSets?.Count; i++)
{
var representations = ParseAdaptationSet(xmlAdaptationSets[i], baseUrl, periodDuration, hasParentSegmentBase);
if (representations.Count == 0) continue;
if (representations[0].MimeType.StartsWith("audio"))
{
audioList.AddRange(representations);
}
else
{
videoList.AddRange(representations);
}
}
var mediaList = new List<(Representation? Video, Representation? Audio)>();
if (videoList.Count > 0 && audioList.Count > 0)
{
foreach (var video in videoList)
{
foreach (var audio in audioList)
{
mediaList.Add((Video: video, Audio: audio));
}
}
}
else if (videoList.Count > 0)
{
foreach (var video in videoList)
{
mediaList.Add((Video: video, Audio: null));
}
}
else if (audioList.Count > 0)
{
foreach (var audio in audioList)
{
mediaList.Add((Video: null, Audio: audio));
}
}
return mediaList;
}
private static IList<Representation> ParseAdaptationSet(XmlNode xmlAdaptationSet, Uri baseUrl, long periodDuration, bool hasParentSegmentBase)
{
var representations = new List<Representation>();
var xmlBaseUrl = xmlAdaptationSet.SelectSingleNode("child::BaseURL");
if (xmlBaseUrl != null)
{
baseUrl = UrlResolver.Resolve(baseUrl, xmlBaseUrl.InnerText);
}
if (ContainsTrickMode(xmlAdaptationSet))
{
return representations;
}
var xmlRepresentations = xmlAdaptationSet.SelectNodes("child::Representation");
for (var i = 0; i < xmlRepresentations?.Count; i++)
{
var representation = ParseRepresentation(xmlRepresentations[i], baseUrl, periodDuration, hasParentSegmentBase);
if (representation != null) representations.Add(representation);
}
return representations;
}
private static Representation ParseRepresentation(XmlNode xmlRepresentation, Uri baseUrl, long periodDuration, bool hasParentSegmentBase)
{
var mimeType = (xmlRepresentation?.Attributes?["mimeType"]?.Value ?? xmlRepresentation?.ParentNode?.Attributes?["mimeType"]?.Value ?? "").ToLowerInvariant();
var width = xmlRepresentation.Attributes["width"]?.Value ?? xmlRepresentation.ParentNode.Attributes["width"]?.Value;
var height = xmlRepresentation.Attributes["height"]?.Value ?? xmlRepresentation.ParentNode.Attributes["height"]?.Value;
var bandwidth = xmlRepresentation.Attributes["bandwidth"]?.Value ?? xmlRepresentation.ParentNode.Attributes["bandwidth"]?.Value;
var codec = xmlRepresentation.Attributes["codecs"]?.Value ?? xmlRepresentation.ParentNode.Attributes["codecs"]?.Value;
var lang = xmlRepresentation.Attributes["lang"]?.Value ?? xmlRepresentation.ParentNode.Attributes["lang"]?.Value;
if (!mimeType.StartsWith("audio") && !mimeType.StartsWith("video"))
{
return null;
}
var xmlBaseUrl = xmlRepresentation.SelectSingleNode("child::BaseURL");
if (xmlBaseUrl != null)
{
baseUrl = UrlResolver.Resolve(baseUrl, xmlBaseUrl.InnerText);
}
if (xmlRepresentation.SelectSingleNode("child::SegmentBase") != null || hasParentSegmentBase)
{
//index addressing
var segmentUri = baseUrl;
return new Representation(new List<Uri> { segmentUri }, Int32.Parse(width ?? "-1"),
Int32.Parse(height ?? "-1"), codec, Int64.Parse(bandwidth ?? "-1"),
periodDuration, mimeType, lang);
}
else if (xmlRepresentation.SelectSingleNode("child::SegmentList") != null)
{
var xmlSegmentList = xmlRepresentation.SelectSingleNode("child::SegmentList");
var xmlSegmentURLs = xmlSegmentList?.SelectNodes("child::SegmentURL");
var segments = new List<Uri>();
var xmlInit = xmlSegmentList.SelectSingleNode("child::Initialization") ??
xmlSegmentList.SelectSingleNode("child::RepresentationIndex");
if (xmlInit != null)
{
var sourceURL = xmlInit?.Attributes?["sourceURL"]?.Value;
segments.Add(UrlResolver.Resolve(baseUrl, sourceURL));
}
foreach (XmlNode xmlSegmentURL in xmlSegmentURLs)
{
var media = xmlSegmentURL?.Attributes?["media"]?.Value;
segments.Add(UrlResolver.Resolve(baseUrl, media));
}
return segments.Count > 0 ? new Representation(segments, Int32.Parse(width ?? "-1"),
Int32.Parse(height ?? "-1"), codec, Int64.Parse(bandwidth ?? "-1"),
periodDuration, mimeType, lang) : null;
}
else
{
//simple or explicit addressing
var xmlSegmentTemplate = xmlRepresentation.SelectSingleNode("child::SegmentTemplate") ??
xmlRepresentation.ParentNode.SelectSingleNode("child::SegmentTemplate");
if (xmlSegmentTemplate != null)
{
var xmlSegmentTimeline = xmlSegmentTemplate.SelectSingleNode("child::SegmentTimeline");
//simple addressing
if (xmlSegmentTimeline == null)
{
var timescale = Int64.Parse(xmlSegmentTemplate?.Attributes?["timescale"]?.Value ?? "1");
var duration = Int64.Parse(xmlSegmentTemplate?.Attributes?["duration"]?.Value ?? "1");
var startNumber = Int64.Parse(xmlSegmentTemplate?.Attributes?["startNumber"]?.Value ?? "1");
var segmentCount = (int)Math.Ceiling(((double)periodDuration / 1000) / ((double)duration / timescale));
var representationId = xmlRepresentation.Attributes["id"].Value;
var number = startNumber;
var time = startNumber;
var initUrl = xmlSegmentTemplate?.Attributes?["initialization"]?.Value?.Replace("$$", "\0");
var mediaUrl = xmlSegmentTemplate?.Attributes?["media"]?.Value?.Replace("$$", "\0");
var mediaMatches = new List<Match>(TemplatePattern.Matches(mediaUrl).GetAsEnumerable());
var segments = new List<Uri>(segmentCount + (initUrl != null ? 1 : 0));
if (initUrl != null)
{
var initializationUrl = ParseTemplate(new List<Match>(TemplatePattern.Matches(initUrl).GetAsEnumerable()), initUrl,
number, time, bandwidth, representationId).Replace("\0", "$");
segments.Add(UrlResolver.Resolve(baseUrl, initializationUrl));
}
for (var i = 0; i < segmentCount; i++)
{
var segmentUrl = ParseTemplate(mediaMatches, mediaUrl, number,
time, bandwidth, representationId).Replace("\0", "$");
segments.Add(UrlResolver.Resolve(baseUrl, segmentUrl));
number++;
time += duration;
}
return segments.Count > 0 ? new Representation(segments, Int32.Parse(width ?? "-1"),
Int32.Parse(height ?? "-1"), codec, Int64.Parse(bandwidth ?? "-1"),
periodDuration, mimeType, lang) : null;
}
else if (xmlSegmentTimeline != null)
{
//explicit addressing
var xmlSs = xmlSegmentTimeline.SelectNodes("child::S");
if (xmlSs != null && xmlSs.Count > 0)
{
var representationId = xmlRepresentation.Attributes["id"].Value;
var number = Int64.Parse(xmlSegmentTemplate.Attributes["startNumber"]?.Value ?? "1");
var time = 0L;
var initUrl = xmlSegmentTemplate.Attributes["initialization"]?.Value?.Replace("$$", "\0");
var mediaUrl = xmlSegmentTemplate.Attributes["media"].Value.Replace("$$", "\0");
var mediaMatches = new List<Match>(TemplatePattern.Matches(mediaUrl).GetAsEnumerable());
var segments = new List<Uri>();
if (initUrl != null)
{
var initializationUrl = ParseTemplate(new List<Match>(TemplatePattern.Matches(initUrl).GetAsEnumerable()), initUrl,
number, time, bandwidth, representationId).Replace("\0", "$");
segments.Add(UrlResolver.Resolve(baseUrl, initializationUrl));
}
for (var i = 0; i < xmlSs.Count; i++)
{
var xmls = xmlSs[i];
var d = Int64.Parse(xmls.Attributes["d"].Value);
var t = Int64.Parse(xmls.Attributes["t"]?.Value ?? "-1");
var r = Int64.Parse(xmls.Attributes["r"]?.Value ?? "-1");
if (t > 0) time = t;
var duration = d;
var segmentUrl = ParseTemplate(mediaMatches, mediaUrl, number,
time, bandwidth, representationId).Replace("\0", "$");
segments.Add(UrlResolver.Resolve(baseUrl, segmentUrl));
number++;
time += duration;
if (r > 0)
{
for (var k = 0; k < r; k++)
{
segmentUrl = ParseTemplate(mediaMatches, mediaUrl, number,
time, bandwidth, representationId).Replace("\0", "$");
segments.Add(UrlResolver.Resolve(baseUrl, segmentUrl));
number++;
time += duration;
}
}
}
return segments.Count > 0 ? new Representation(segments, Int32.Parse(width ?? "-1"),
Int32.Parse(height ?? "-1"), codec, Int64.Parse(bandwidth ?? "-1"),
periodDuration, mimeType, lang) : null;
}
}
}
}
return null;
}
private static string ParseTemplate(IEnumerable<Match> matches, string templateUrl, long number, long time, string bandwidth,
string representationId)
{
foreach (Match match in matches)
{
var variable = match.Groups[1].Value;
if (variable.StartsWith("Number"))
{
if (variable == "Number")
{
templateUrl = templateUrl.Replace($"${variable}$", number.ToString());
}
else
{
var num = FormatDigit(number, match, true);
templateUrl = templateUrl.Replace($"${variable}$", num);
}
}
else if (variable.StartsWith("Time"))
{
if (variable == "Time")
{
templateUrl = templateUrl.Replace($"${variable}$", time.ToString());
}
else
{
var num = FormatDigit(time, match, false);
templateUrl = templateUrl.Replace($"${variable}$", num);
}
}
else if (variable == "RepresentationID")
{
templateUrl = templateUrl.Replace($"${variable}$", representationId);
}
else if (variable == "Bandwidth")
{
templateUrl = templateUrl.Replace($"${variable}$", bandwidth);
}
}
return templateUrl;
}
private static string FormatDigit(long digit, Match match, bool number)
{
var digitWidth = match.Groups[number ? "num_digits" : "time_digits"].Value;
var dx = "D";
if (match.Groups.ContainsKey("num_dx") || match.Groups.ContainsKey("time_dx"))
{
dx = number ? match.Groups["num_dx"].Value : match.Groups["time_dx"].Value;
}
return digit.ToString(dx + digitWidth);
}
public static IList<long> CalculatePeriodDurationsIfMissing(XmlNodeList periods, long mediaPresentationDuration)
{
var stack = new Stack<XmlNode>();
foreach (XmlNode node in periods)
{
stack.Push(node);
}
var list = new List<long>(periods.Count);
var last = mediaPresentationDuration;
var count = stack.Count;
for (int i = 0; i < count; i++)
{
var node = stack.Pop();
var sduration = node.Attributes["duration"]?.Value;
var sstart = node.Attributes["start"]?.Value;
if (sstart == null && sduration == null)
{
throw new Exception("Both period start and duration is missing");
}
if (sduration != null)
{
var duration = DashUtil.ParseXsDuration(sduration);
list.Add(duration);
last = mediaPresentationDuration - duration;
continue;
}
if (sstart == null && i == stack.Count - 1)
{
sstart = "PT0S";
}
var start = DashUtil.ParseXsDuration(sstart);
list.Add(last - start);
last = start;
}
list.Reverse();
return list;
}
private static bool ContainsTrickMode(XmlNode xmlAdaptationSet)
{
var xmlEssentialProperty = xmlAdaptationSet.SelectSingleNode("child::EssentialProperty");
if (xmlEssentialProperty != null)
{
var schemeIdUri = xmlEssentialProperty.Attributes?["schemeIdUri"]?.Value;
if (trickModeUri == schemeIdUri)
{
return true;
}
}
var xmlSupplementalProperty = xmlAdaptationSet.SelectSingleNode("child::SupplementalProperty");
if (xmlSupplementalProperty != null)
{
var schemeIdUri = xmlSupplementalProperty.Attributes?["schemeIdUri"]?.Value;
if (trickModeUri == schemeIdUri)
{
return true;
}
}
return false;
}
public static IEnumerable<Match> GetAsEnumerable(this MatchCollection matchCollection)
{
foreach (Match item in matchCollection)
{
yield return item;
}
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
namespace MediaParser.Dash
{
public class Representation
{
public int Width { get; }
public int Height { get; }
public string Codec { get; }
public long Bandwidth { get; }
public long Duration { get; }
public List<Uri> Segments { get; }
public string MimeType { get; }
public string Language { get; }
public Representation(List<Uri> segments, int width, int height,
string codec, long bandwidth, long duration, string mimeType, string language)
{
this.Segments = segments;
this.Width = width;
this.Height = height;
this.Codec = codec;
this.Bandwidth = bandwidth;
this.Duration = duration;
this.MimeType = mimeType;
this.Language = language;
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MediaParser.Hls
{
public static class HlsHelper
{
public static Dictionary<string, string> ParseAttributes(string attrText)
{
var dict = new Dictionary<string, string>();
var pairBuf = new StringBuilder();
var insideQuote = false;
foreach (var ch in attrText)
{
if (ch == ',' && !insideQuote)
{
var keyValuePair = ParseKeyValuePair(pairBuf.ToString());
if (keyValuePair.HasValue)
{
dict[keyValuePair.Value.key] = keyValuePair.Value.value;
}
pairBuf = new StringBuilder();
continue;
}
if (ch == '"') insideQuote = !insideQuote;
pairBuf.Append(ch);
}
if (pairBuf.Length > 0)
{
var keyValuePair = ParseKeyValuePair(pairBuf.ToString());
if (keyValuePair.HasValue)
{
dict[keyValuePair.Value.key] = keyValuePair.Value.value;
}
}
return dict;
}
public static (string key, string value)? ParseKeyValuePair(string pair)
{
var index = pair.IndexOf('=');
if (index == -1) return null;
return (key: pair.Substring(0, index), value: pair.Substring(index + 1).Trim('"', '\'', ' '));
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MediaParser.Hls
{
public class HlsMediaSegment
{
public HlsMediaSegment(Uri url)
{
this.Url = url;
}
public Uri Url { get; set; }
public (long start, long end) ByteRange { get; set; }
public double Duration { get; set; }
public Uri? KeyUrl { get; set; }
public string? IV { get; set; }
}
}

View File

@ -0,0 +1,267 @@
using MediaParser.Util;
using System;
using System.Collections.Generic;
#if !NET5_0_OR_GREATER
using NetFX.Polyfill;
#endif
namespace MediaParser.Hls
{
public static class HlsParser
{
public static readonly string AUDIO = "AUDIO";
public static readonly string VIDEO = "VIDEO";
public static readonly string EXT_X_STREAM_INF = "#EXT-X-STREAM-INF:";
public static readonly string EXT_X_MEDIA = "#EXT-X-MEDIA:";
public static readonly string EXT_X_BYTERANGE = "#EXT-X-BYTERANGE:";
public static readonly string EXTINF = "#EXTINF:";
public static readonly string EXT_X_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE:";
public static readonly string EXT_X_KEY = "#EXT-X-KEY:";
public static readonly string EXT_X_MAP = "#EXT-X-MAP:";
public static readonly string EXT_X_I_FRAMES_ONLY = "#EXT-X-I-FRAMES-ONLY:";
public static HlsPlaylist ParseMediaSegments(IEnumerable<string> manifestLines, string playlistUrl)
{
var mediaSegments = new List<HlsMediaSegment>();
var mediaSequence = 0L;
var startOffset = 0L;
var segmentLength = 0L;
var segmentEndOffset = 0L;
var duration = 0.0;
var totalDuration = 0.0;
var hasByteRange = false;
var isEncrypted = false;
var baseUrl = new Uri(playlistUrl);
var keyFrameOnly = false;
Uri? keyUrl = null;
string? iv = null;
var sigFound = false;
foreach (var lineText in manifestLines)
{
var line = lineText.Trim(' ', '\r');
if (line.Length < 1) continue;
if (!sigFound)
{
if (line.StartsWith("#EXTM3U"))
{
sigFound = true;
continue;
}
else
{
return null;
}
}
if (line[0] != '#')
{
var url = UrlResolver.Resolve(baseUrl, line);
mediaSegments.Add(new HlsMediaSegment(url)
{
ByteRange = (start: startOffset, end: segmentLength),
Duration = duration,
KeyUrl = keyUrl,
IV = iv
});
mediaSequence++;
totalDuration += duration;
}
else if (line.StartsWith(EXT_X_I_FRAMES_ONLY))
{
keyFrameOnly = true;
}
else if (line.StartsWith(EXT_X_BYTERANGE))
{
hasByteRange = true;
var attrList = line.Substring(EXT_X_BYTERANGE.Length).Trim();
(long offset, long length) = ParseByteRange(attrList);
if (offset > 0)
{
startOffset = offset;
}
else
{
startOffset = segmentEndOffset;
}
segmentLength = length;
segmentEndOffset += segmentLength;
}
else if (line.StartsWith(EXTINF))
{
var attrs = line.Substring(EXTINF.Length).Trim();
if (attrs.Length > 0)
{
duration = Double.Parse(attrs.Split(',')[0]);
}
}
else if (line.StartsWith(EXT_X_MEDIA_SEQUENCE))
{
mediaSequence = Int32.Parse(line.Substring(EXT_X_MEDIA_SEQUENCE.Length).Trim());
}
else if (line.StartsWith(EXT_X_KEY))
{
var encAttrs = HlsHelper.ParseAttributes(line.Substring(EXT_X_KEY.Length));
if (encAttrs.ContainsKey("METHOD"))
{
if (encAttrs["METHOD"] == "AES-128" && encAttrs.GetValueOrDefault("KEYFORMAT", "identity") == "identity")
{
isEncrypted = true;
keyUrl = UrlResolver.Resolve(baseUrl, encAttrs["URI"]);
iv = encAttrs.ContainsKey("IV") ? encAttrs["IV"] : mediaSequence.ToString("X");
}
else if (encAttrs["METHOD"] != "NONE")
{
return null;
}
}
}
else if (line.StartsWith(EXT_X_MAP))
{
var encAttrs = HlsHelper.ParseAttributes(line.Substring(EXT_X_MAP.Length));
if (encAttrs.ContainsKey("URI"))
{
mediaSegments.Add(new HlsMediaSegment(UrlResolver.Resolve(baseUrl, encAttrs["URI"]))
{
ByteRange = encAttrs.ContainsKey("BYTERANGE") ? ParseByteRange(encAttrs["BYTERANGE"]) : (0, 0),
Duration = 0,
KeyUrl = keyUrl,
IV = iv
});
}
}
}
if (mediaSegments.Count > 0)
{
return new HlsPlaylist
{
MediaSegments = mediaSegments,
HasByteRange = hasByteRange,
IsEncrypted = isEncrypted,
TotalDuration = totalDuration,
IsKeyIFrameOnly = keyFrameOnly
};
}
return null;
}
public static List<HlsPlaylistContainer> ParseMasterPlaylist(IEnumerable<string> manifestLines, string playlistUrl)
{
var containers = new List<HlsPlaylistContainer>();
var baseUrl = new Uri(playlistUrl);
var sigFound = false;
var mapExtStreamInf = new List<Dictionary<string, string>>();
var mapExtMedia = new List<Dictionary<string, string>>();
var urls = new List<Uri>();
foreach (var lineText in manifestLines)
{
var line = lineText.Trim();
if (line.Length < 1) continue;
if (!sigFound)
{
if (line.StartsWith("#EXTM3U"))
{
sigFound = true;
continue;
}
else
{
return null;
}
}
if (line[0] != '#')
{
urls.Add(UrlResolver.Resolve(baseUrl, line));
}
else if (line.StartsWith(EXT_X_STREAM_INF))
{
mapExtStreamInf.Add(HlsHelper.ParseAttributes(line.Substring(EXT_X_STREAM_INF.Length)));
}
else if (line.StartsWith(EXT_X_MEDIA))
{
mapExtMedia.Add(HlsHelper.ParseAttributes(line.Substring(EXT_X_MEDIA.Length)));
}
}
if (mapExtStreamInf.Count < 1) return null;
for (var i = 0; i < urls.Count; i++)
{
var extStreamInf = mapExtStreamInf[i];
if (extStreamInf.ContainsKey(AUDIO))
{
var groupId = extStreamInf[AUDIO];
foreach (var media in mapExtMedia)
{
if (media["GROUP-ID"] == groupId && media["TYPE"] == AUDIO)
{
containers.Add(new HlsPlaylistContainer
{
VideoPlaylist = urls[i],
AudioPlaylist = media.ContainsKey("URI") ? UrlResolver.Resolve(baseUrl, media["URI"]) : null,
Attributes = MergeDict(extStreamInf, media)
});
}
}
}
else if (extStreamInf.ContainsKey(VIDEO))
{
var groupId = extStreamInf[VIDEO];
foreach (var media in mapExtMedia)
{
if (media["GROUP-ID"] == groupId && media["TYPE"] == VIDEO)
{
containers.Add(new HlsPlaylistContainer
{
VideoPlaylist = media.ContainsKey("URI") ? UrlResolver.Resolve(baseUrl, media["URI"]) : null,
AudioPlaylist = urls[i],
Attributes = MergeDict(extStreamInf, media)
});
}
}
}
else
{
containers.Add(new HlsPlaylistContainer
{
VideoPlaylist = urls[i],
Attributes = extStreamInf
});
}
}
return containers;
}
private static Dictionary<string, string> MergeDict(Dictionary<string, string> d1, Dictionary<string, string> d2)
{
var dict = new Dictionary<string, string>();
foreach (var ent in d1)
{
dict[ent.Key] = ent.Value;
}
foreach (var ent in d2)
{
dict[ent.Key] = ent.Value;
}
return dict;
}
private static (long Offset, long Length) ParseByteRange(string str)
{
long offset = 0, length;
var attrs = str.Split('@');
length = Int32.Parse(attrs[0]);
if (attrs.Length == 2)
{
offset = Int32.Parse(attrs[1]);
}
return (offset, length);
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MediaParser.Hls
{
public class HlsPlaylist
{
public IList<HlsMediaSegment>? MediaSegments { get; set; }
public bool IsEncrypted { get; set; }
public double TotalDuration { get; set; }
public bool HasByteRange { get; set; }
public bool IsKeyIFrameOnly { get; set; }
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
#if !NET5_0_OR_GREATER
using NetFX.Polyfill;
#endif
namespace MediaParser.Hls
{
public class HlsPlaylistContainer
{
public Uri? VideoPlaylist { get; set; }
public Uri? AudioPlaylist { get; set; }
public Dictionary<string, string>? Attributes { get; set; }
public string? Quality => GetQuality();
private string? GetQuality()
{
if (Attributes == null) return null;
var resolution = Attributes.GetValueOrDefault("RESOLUTION");
var bandwidth = Attributes.GetValueOrDefault("BANDWIDTH");
var name = Attributes.GetValueOrDefault("NAME");
var lang = Attributes.GetValueOrDefault("LANGUAGE");
var text = new StringBuilder();
if (resolution != null)
{
text.Append(resolution);
}
if (bandwidth != null)
{
try
{
if (text.Length > 0) text.Append(' ');
text.Append((Int64.Parse(bandwidth) / 1024) + " Kbps");
}
catch { }
}
if (name != null)
{
if (text.Length > 0) text.Append(' ');
text.Append(name);
}
if (name == null && lang != null)
{
if (text.Length > 0) text.Append(' ');
text.Append(lang);
}
return text.Length > 0 ? text.ToString() : null;
}
}
public enum MediaType
{
/// <summary>
/// Contains both audio and video
/// </summary>
GENERIC,
/// <summary>
/// Contains only video
/// </summary>
VIDEO_ONLY,
/// <summary>
/// Contains only audio
/// </summary>
AUDIO_ONLY
}
}

View File

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net3.5;net4.5;net4.7.2;net5.0</TargetFrameworks>
<Platforms>AnyCPU;x86</Platforms>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<!--<ItemGroup Condition=" '$(TargetFramework)' == 'net4.5.2' ">
<ProjectReference Include="..\CoreFx.Polyfill\CoreFx.Polyfill.csproj" />
</ItemGroup>-->
<ItemGroup Condition=" '$(TargetFramework)' != 'net5.0' ">
<ProjectReference Include="..\NetFX.Polyfill\NetFX.Polyfill.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net4.5'">
<PackageReference Include="System.ValueTuple">
<Version>4.5.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net4.7.2'">
<PackageReference Include="System.ValueTuple">
<Version>4.5.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
<PackageReference Include="System.ValueTuple">
<Version>4.5.0</Version>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MediaParser.Util
{
internal static class UrlResolver
{
internal static Uri Resolve(Uri baseUrl, string url)
{
if (url.StartsWith("https://") || url.StartsWith("http://"))
{
return new Uri(url);
}
return new Uri(baseUrl, url);
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MediaParser.YouTube
{
public class VideoFormatData
{
public StreamingData? StreamingData { get; set; }
public VideoDetails? VideoDetails { get; set; }
}
public class VideoFormat
{
public string? SignatureCipher { get; set; }
public string? MimeType { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string? QualityLabel { get; set; }
public int Itag { get; set; }
public long Bitrate { get; set; }
public long ContentLength { get; set; }
public string? Url { get; set; }
}
public class VideoDetails
{
public string? Title { get; set; }
}
public class StreamingData
{
public List<VideoFormat>? Formats { get; set; }
public List<VideoFormat>? AdaptiveFormats { get; set; }
}
public class ParsedDualUrlVideoFormat
{
public ParsedDualUrlVideoFormat(string title, string videoUrl, string audioUrl,
string formatDescription, string mediaContainer, long size)
{
this.Title = title;
this.VideoUrl = videoUrl;
this.AudioUrl = audioUrl;
this.FormatDescription = formatDescription;
this.MediaContainer = mediaContainer;
this.Size = size;
}
public string Title { get; }
public string VideoUrl { get; }
public string AudioUrl { get; }
public string FormatDescription { get; }
public string MediaContainer { get; }
public long Size { get; }
}
public class ParsedUrlVideoFormat
{
public ParsedUrlVideoFormat(string title, string mediaUrl, string formatDescription, string mediaContainer, long size)
{
this.Title = title;
this.MediaUrl = mediaUrl;
this.FormatDescription = formatDescription;
this.MediaContainer = mediaContainer;
this.Size = size;
}
public string Title { get; }
public string MediaUrl { get; }
public string FormatDescription { get; }
public string MediaContainer { get; }
public long Size { get; }
}
}

View File

@ -0,0 +1,173 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
namespace MediaParser.YouTube
{
public class YoutubeDataFormatParser
{
public static (List<ParsedDualUrlVideoFormat> DualVideoItems, List<ParsedUrlVideoFormat> VideoItems)
GetFormats(string file)
{
var items = JsonConvert.DeserializeObject<VideoFormatData>(File.ReadAllText(file),
new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Ignore
});
var dualVideoItems = new List<ParsedDualUrlVideoFormat>();
var videoItems = new List<ParsedUrlVideoFormat>();
var maxOfEachQualityVideoGroupMp4 = items.StreamingData?.AdaptiveFormats
.Where(i => i.MimeType.StartsWith("video/mp4") && i.Url != null)
.GroupBy(x => x.QualityLabel)
.Select(g => g.OrderByDescending(a => a.ContentLength / a.Bitrate).First());
var maxOfEachQualityVideoGroupWebm = items.StreamingData?.AdaptiveFormats
.Where(i => i.MimeType.StartsWith("video/webm") && i.Url != null)
.GroupBy(x => x.QualityLabel)
.Select(g => g.OrderByDescending(a => a.ContentLength / a.Bitrate).First());
// .Select(g => g.OrderByDescending(a => a.Bitrate).First());
var maxOfEachQualityAudioMp4 = items.StreamingData?.AdaptiveFormats
.Where(i => i.MimeType.StartsWith("audio/mp4") && i.Url != null)
.GroupBy(x => x.QualityLabel + x.MimeType)
.Select(g => g.OrderByDescending(a => a.ContentLength / a.Bitrate).First());
var maxOfEachQualityAudioWebm = items.StreamingData?.AdaptiveFormats
.Where(i => i.MimeType.StartsWith("audio/webm") && i.Url != null)
.GroupBy(x => x.QualityLabel + x.MimeType)
.Select(g => g.OrderByDescending(a => a.ContentLength / a.Bitrate).First());
if (maxOfEachQualityVideoGroupMp4 != null && maxOfEachQualityAudioMp4 != null)
{
foreach (var video in maxOfEachQualityVideoGroupMp4)
{
foreach (var audio in maxOfEachQualityAudioMp4)
{
var ext = GetMediaExtension(video.MimeType, audio.MimeType);
dualVideoItems.Add(
new ParsedDualUrlVideoFormat(items.VideoDetails.Title,
video.Url,
audio.Url,
video.QualityLabel,
ext,
video.ContentLength + audio.ContentLength
)
);
}
}
}
if (maxOfEachQualityVideoGroupWebm != null && maxOfEachQualityAudioWebm != null)
{
foreach (var video in maxOfEachQualityVideoGroupWebm)
{
foreach (var audio in maxOfEachQualityAudioWebm)
{
var ext = GetMediaExtension(video.MimeType, audio.MimeType);
dualVideoItems.Add(
new ParsedDualUrlVideoFormat(items.VideoDetails.Title,
video.Url,
audio.Url,
video.QualityLabel,
ext,
video.ContentLength + audio.ContentLength
)
);
}
}
}
//var videoList = new List<VideoFormat>();
//var audioList = new List<VideoFormat>();
//videoList.AddRange(items.StreamingData.AdaptiveFormats.Where(item => item.MimeType.StartsWith("video/")));
//audioList.AddRange(items.StreamingData.AdaptiveFormats.Where(item => item.MimeType.StartsWith("audio/")));
//var bestMp4Audio = BestAudioFormat("audio/mp4", audioList);
//var bestWebmAudio = BestAudioFormat("audio/webm", audioList);
//foreach (var video in videoList)
//{
// foreach (var audio in new List<VideoFormat> { bestMp4Audio, bestWebmAudio })
// {
// dualVideoItems.Add(
// new ParsedDualUrlVideoFormat(
// ParseUrl(video.SignatureCipher),
// ParseUrl(audio.SignatureCipher),
// video.QualityLabel + " " + GetMediaExtension(video.MimeType, audio.MimeType)
// )
// );
// }
//}
if (items.StreamingData != null)
{
videoItems.AddRange(
items.StreamingData?.Formats.Where(
item => item.MimeType.StartsWith("video/") && item.Url != null).Select(
item => new ParsedUrlVideoFormat(items.VideoDetails.Title,
item.Url,
item.QualityLabel,
(item.MimeType.StartsWith("video/mp4") ? "MP4" : "MKV"),
item.ContentLength)));
}
return (dualVideoItems, videoItems);
}
private static VideoFormat BestAudioFormat(string mime, List<VideoFormat> audioList)
{
VideoFormat bestAudio = null;
var highestBitrate = -1L;
foreach (var audio in audioList)
{
if (audio.MimeType.StartsWith(mime))
{
if (highestBitrate < audio.Bitrate)
{
highestBitrate = audio.Bitrate;
bestAudio = audio;
}
}
}
return bestAudio;
}
private static string GetMediaExtension(string videoMime, string audioMime)
{
if (videoMime.StartsWith("video/mp4") && audioMime.StartsWith("audio/mp4"))
{
return "MP4";
}
return "MKV";
}
//private static string ParseUrl(string text)
//{
// var arr = text.Split('&');
// var finalUrl = new StringBuilder();
// String url = null;
// foreach (var item in arr)
// {
// if (item.StartsWith("url"))
// {
// url = WebUtility.UrlDecode(item);
// continue;
// }
// finalUrl.Append('&');
// finalUrl.Append(item);
// }
// finalUrl.Insert(0, url.Substring(url.IndexOf('=') + 1));
// return finalUrl.ToString();
//}
}
}

View File

@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MockServer
{
public class MockServer: IDisposable
{
private Dictionary<string, string> fileMap = new Dictionary<string, string>();
private Dictionary<string, Dictionary<string, string>> headerMap = new Dictionary<string, Dictionary<string, string>>();
private Dictionary<string, string> fileHashMap = new Dictionary<string, string>();
public readonly CancellationTokenSource CancellationToken = new CancellationTokenSource();
private HttpListener listener;
private long stopAfterBytes = -1;
private long bytesServed = 0;
public bool NonResumable { get; set; }
public bool HasContentLength { get; set; } = true;
private bool closed = false;
public string BaseUrl => "http://127.0.0.1:49000/";
public string GetHash(string id)
{
return fileHashMap["/" + id];
}
public void StartAsync()
{
listener = new HttpListener();
listener.Prefixes.Add(BaseUrl);
listener.Start();
Task.Factory.StartNew(Start);
}
public void Stop()
{
if (this.closed)
{
return;
}
this.closed = true;
this.CancellationToken.Cancel();
this.listener.Stop();
foreach (var key in fileMap.Keys)
{
var file = fileMap[key];
try
{
File.Delete(file);
}
catch { }
}
}
public void Start()
{
while (!this.CancellationToken.IsCancellationRequested)
{
HttpListenerContext context = listener.GetContext();
if (fileMap.ContainsKey(context.Request.RawUrl))
{
new RequestHandler
{
FilePath = fileMap[context.Request.RawUrl],
Hash = fileHashMap[context.Request.RawUrl],
Headers = headerMap.GetValueOrDefault(context.Request.RawUrl, null),
CancellationToken = CancellationToken.Token,
NonResumable = this.NonResumable,
HasContentLength = this.HasContentLength,
BytesAction =
x =>
{
Interlocked.Add(ref bytesServed, x);
if (bytesServed >= stopAfterBytes && stopAfterBytes > 0)
{
this.listener.Stop();
}
}
}.HandleAsync(context.Request, context.Response);
}
}
}
public (string File, string Hash, long Size) AddMockHandler(string id, long stopAfterPercent = -1, Dictionary<string, string> headers = null, int start = 50, int end = 100, byte[] contents = null, long fixedSize = -1)
{
long size;
long sz = 0;
string tempFile = Path.Combine(Path.GetTempPath(), id);
if (contents == null)
{
Random random = new Random();
size = fixedSize > 0 ? fixedSize : random.Next(start, end) * 1024 * 1024;
sz = size;
int bufSize = 128 * 1024;
byte[] b = new byte[bufSize];
if (stopAfterPercent >= 0)
{
stopAfterBytes = size * stopAfterPercent / 100;
}
using (FileStream fs = new FileStream(tempFile, FileMode.Create))
{
while (size > 0)
{
int rem = (int)Math.Min(bufSize, size);
random.NextBytes(b);
fs.Write(b, 0, rem);
size -= rem;
}
}
}
else
{
File.WriteAllBytes(tempFile, contents);
size = contents.Length;
}
var hash = "";
using (SHA256 sha256Hash = SHA256.Create())
{
using (FileStream fs = new FileStream(tempFile, FileMode.Open))
{
byte[] buf = new byte[8192];
while (true)
{
int x = fs.Read(buf, 0, buf.Length);
if (x == 0) break;
sha256Hash.TransformBlock(buf, 0, x, null, 0);
}
sha256Hash.TransformFinalBlock(new byte[0] { }, 0, 0);
}
byte[] bytes = sha256Hash.Hash;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
builder.Append(bytes[i].ToString("x2"));
}
this.fileHashMap.Add("/" + id, builder.ToString());
hash = builder.ToString();
}
this.fileMap.Add("/" + id, tempFile);
if (headers != null)
{
this.headerMap.Add("/" + id, headers);
}
return (File: tempFile, Hash: hash, Size: sz);
}
public void Dispose()
{
Stop();
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;net4.5.2</TargetFrameworks>
<Platforms>AnyCPU;x86</Platforms>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net4.5.2' ">
<ProjectReference Include="..\CoreFx.Polyfill\CoreFx.Polyfill.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace MockServer
{
public class RequestHandler
{
private Regex rx = new Regex(@"bytes=(\d+)-(\d*)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public string FilePath { get; set; }
public string Hash { get; set; }
private byte[] buffer = new byte[8192];
public CancellationToken CancellationToken { get; set; }
public Action<long> BytesAction;
public bool NonResumable { get; set; }
public Dictionary<string, string> Headers;
public bool HasContentLength { get; set; } = true;
public void HandleAsync(HttpListenerRequest request, HttpListenerResponse response)
{
Task.Factory.StartNew(() =>
{
HandleRequest(request, response);
});
}
private void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
{
var r = new Random();
var match = rx.Match(request.Headers.Get("Range") ?? "");
if (!match.Success || !HasContentLength || NonResumable)
{
NonResumable = true;
}
response.StatusCode = NonResumable ? 200 : 206;
if (Headers != null)
{
foreach (var header in Headers)
{
response.Headers.Add(header.Key, header.Value);
}
}
using Stream fs = new BufferedStream(new FileStream(FilePath, FileMode.Open, FileAccess.Read,
FileShare.ReadWrite));
long rem;
if (NonResumable)
{
if (HasContentLength)
{
response.ContentLength64 = fs.Length;
}
else
{
response.SendChunked = true;
}
rem = fs.Length;
}
else
{
//Thread.Sleep(r.Next(10, 50));
long startRange = long.Parse(match.Groups[1].ToString());
var endRangeStr = match.Groups[2].ToString();
long endRange = endRangeStr.Length > 0 ? long.Parse(endRangeStr) : fs.Length - 1;
response.Headers.Add("X-start", match.Groups[1].ToString());
if (match.Groups.Count > 2)
response.Headers.Add("X-End", match.Groups[2].ToString());
response.Headers.Add("Content-Range", "bytes " + startRange + "-" + endRange + "/" + fs.Length);
response.ContentLength64 = endRange - startRange + 1;
fs.Seek(startRange, SeekOrigin.Begin);
rem = endRange - startRange + 1;
}
while (rem > 0 && !CancellationToken.IsCancellationRequested)
{
try
{
Thread.Sleep(r.Next(1, NonResumable ? 2 : 30));
long k = rem > buffer.Length ? buffer.Length : rem;
int x = fs.Read(buffer, 0, (int)k);
response.OutputStream.Write(buffer, 0, (int)k);
BytesAction(x);
rem -= k;
}
catch
{
break;
}
}
response.OutputStream.Flush();
response.OutputStream.Close();
response.Close();
}
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
<supportedRuntime version="v2.0.50727" />
</startup>
</configuration>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

View File

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net3.5;net4.5;net4.7.2;net5.0</TargetFrameworks>
<InvariantGlobalization>true</InvariantGlobalization>
<LangVersion>9.0</LangVersion>
<Platforms>AnyCPU;x86</Platforms>
<!--<PublishTrimmed>true</PublishTrimmed>
<TrimMode>Link</TrimMode>
<Platforms>AnyCPU;x86</Platforms>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>-->
</PropertyGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net5.0' ">
<ProjectReference Include="..\NetFX.Polyfill\NetFX.Polyfill.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BrowserMonitor\BrowserMonitor.csproj" />
<ProjectReference Include="..\TraceLog\TraceLog.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net5.0' or '$(TargetFramework)' == 'net4.7.2' or '$(TargetFramework)' == 'net4.5' ">
<AppConfig>App.PostDotNet4.config</AppConfig>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net3.5' ">
<AppConfig>App.PreDotNet4.config</AppConfig>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,337 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Diagnostics;
using Newtonsoft.Json;
using BrowserMonitoring;
using Newtonsoft.Json.Serialization;
#if NET35
using NetFX.Polyfill;
#else
using System.Collections.Concurrent;
#endif
namespace NativeHost
{
public class NativeMessagingHostApp
{
static BlockingCollection<byte[]> receivedBrowserMessages = new();
static BlockingCollection<byte[]> queuedBrowserMessages = new();
static CamelCasePropertyNamesContractResolver cr = new();
static StreamWriter log = new StreamWriter(new FileStream(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "native-host.log"), FileMode.Create));
static void Main(string[] args)
{
//var json = BinaryToJson(new byte[0]);
#if !NET35
Debug(Environment.Is64BitProcess+"");
#endif
try
{
Debug("Trying to open mutex");
using var mutex = Mutex.OpenExisting(@"Global\XDM_Active_Instance");
Debug("Mutex opened");
}
catch
{
Debug("Mutex open failed, spawn xdm process...++");
CreateXDMInstance();
}
Debug("next");
try
{
var inputReader = Console.OpenStandardInput();
var outputWriter = Console.OpenStandardOutput();
var t1 = new Thread(() =>
{
try
{
while (true)
{
//read from process stdin and write to blocking queue,
//they will be sent to xdm once pipe handshake complets
var msg = ReadMessageBytes(inputReader);
Debug(Encoding.UTF8.GetString(msg));
receivedBrowserMessages.Add(JsonToBinary(msg));
}
}
catch (Exception exx)
{
Debug(exx.ToString());
Environment.Exit(1);
}
});
t1.Start();
var t2 = new Thread(() =>
{
try
{
while (true)
{
//read from blocking queue and write to stdout,
//these messages were queued by xdm
var msg = queuedBrowserMessages.Take();//doesn't make much sense to it async
var json = BinaryToJson(msg);
Debug(Encoding.UTF8.GetString(json));
WriteMessage(outputWriter, json);
}
}
catch (Exception exx)
{
Debug(exx.ToString());
Environment.Exit(1);
}
});
t2.Start();
Debug("Start message proccessing...");
ProcessMessages();
Debug("Finished message proccessing.");
}
catch (Exception ex)
{
Debug(ex.ToString());
}
}
private static void CreateXDMInstance()
{
try
{
Debug("XDM instance creating...1");
ProcessStartInfo psi = new()
{
FileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "XDM.WinForm.UI.exe"),
UseShellExecute = true,
Arguments = "-m"
};
Debug("XDM instance creating...");
Process.Start(psi);
Debug("XDM instance created");
}
catch (Exception ex)
{
Debug(ex.ToString());
}
}
private static void ProcessMessages()
{
Debug("Log start");
try
{
NamedPipeServerStream inPipe = null;
NamedPipeClientStream outPipe = null;
while (true)
{
try
{
var pipeName = Guid.NewGuid().ToString();
inPipe = new NamedPipeServerStream(pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.WriteThrough);
//start handshake with XDM
outPipe = new NamedPipeClientStream(".", "XDM_Ipc_Browser_Monitoring_Pipe", PipeDirection.Out);
Debug("start handshake with XDM");
outPipe.Connect();
WriteMessage(outPipe, pipeName);
Debug("pipename: " + pipeName);
inPipe.WaitForConnection();
var syncMsgBytes = ReadMessageBytes(inPipe);
Debug("No task message size: " + syncMsgBytes.Length);
queuedBrowserMessages.Add(syncMsgBytes);
//handshake with XDM is complete
Debug("handshake with XDM is complete");
using var waitHandle = new ManualResetEvent(false);
//queue messages from xdm pipe for browser
var task1 = new Thread(() =>
{
try
{
while (true)
{
var syncMsgBytes = ReadMessageBytes(inPipe);
//Debug("Task1 message size: " + syncMsgBytes.Length);
if (syncMsgBytes.Length == 0)
{
break;
}
queuedBrowserMessages.Add(syncMsgBytes);
}
}
catch (Exception ex)
{
Debug(ex.ToString());
queuedBrowserMessages.Add(Encoding.UTF8.GetBytes("{\"appExited\":\"true\"}"));
}
waitHandle.Set();
Debug("Task1 finished");
}
);
//queue messages to xdm pipe from browser
var task2 = new Thread(() =>
{
try
{
while (true)
{
byte[] syncMsgBytes = null;
Debug("Task2 reading from browser stdin...");
syncMsgBytes = receivedBrowserMessages.Take();
if (syncMsgBytes.Length == 2 && (char)syncMsgBytes[0] == '{' && (char)syncMsgBytes[1] == '}')
{
Debug("Task2 empty object received: " + syncMsgBytes.Length + " " + Encoding.UTF8.GetString(syncMsgBytes));
throw new OperationCanceledException("Empty object");
}
//Debug("Task2 message size fron browser stdin: " + syncMsgBytes.Length);
//Debug(Encoding.UTF8.GetString(syncMsgBytes));
WriteMessage(outPipe, syncMsgBytes);
}
}
catch (Exception ex)
{
Debug(ex.ToString());
}
waitHandle.Set();
Debug("Task2 finished");
}
);
task1.Start();
task2.Start();
waitHandle.WaitOne();
Debug("Any one task finished");
}
catch (Exception ex)
{
Debug(ex.ToString());
}
try
{
inPipe.Disconnect();
}
catch { }
try
{
outPipe.Dispose();
}
catch { }
try
{
inPipe.Dispose();
}
catch { }
}
}
catch (Exception exxxx)
{
Debug(exxxx.ToString());
}
}
private static void Debug(string msg)
{
try
{
log.WriteLine(msg);
log.Flush();
//File.AppendAllText(@"c:\log.txt", msg + "\r\n");
Trace.WriteLine($"[{DateTime.Now}][NativeHost] {msg}");
}
catch(Exception ex)
{
log.WriteLine(ex.ToString());
log.Flush();
}
}
private static void WriteMessage(Stream pipe, string message)
{
var msgBytes = Encoding.UTF8.GetBytes(message);
WriteMessage(pipe, msgBytes);
}
private static void WriteMessage(Stream pipe, byte[] msgBytes)
{
pipe.Write(BitConverter.GetBytes(msgBytes.Length), 0, 4);
pipe.Write(msgBytes, 0, msgBytes.Length);
pipe.Flush();
}
private static byte[] ReadMessageBytes(Stream pipe)
{
var b4 = new byte[4];
ReadFully(pipe, b4, 4);
var syncLength = BitConverter.ToInt32(b4, 0);
var bytes = new byte[syncLength];
ReadFully(pipe, bytes, syncLength);
return bytes;
}
private static string ReadMessageString(Stream pipe)
{
var b4 = new byte[4];
ReadFully(pipe, b4, 4);
var syncLength = BitConverter.ToInt32(b4, 0);
var bytes = new byte[syncLength];
ReadFully(pipe, bytes, syncLength);
return Encoding.UTF8.GetString(bytes);
}
private static void ReadFully(Stream stream, byte[] buf, int bytesToRead)
{
var rem = bytesToRead;
var index = 0;
while (rem > 0)
{
var c = stream.Read(buf, index, rem);
if (c == 0) throw new IOException("Unexpected EOF");
index += c;
rem -= c;
}
}
private static byte[] JsonToBinary(byte[] input)
{
var envelop = JsonConvert.DeserializeObject<RawBrowserMessageEnvelop>(Encoding.UTF8.GetString(input),
new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Ignore
}
);
using var ms = new MemoryStream();
using var w = new BinaryWriter(ms);
envelop.Serialize(w);
w.Close();
ms.Close();
return ms.ToArray();
}
private static byte[] BinaryToJson(byte[] input)
{
var msg = SyncMessage.Deserialize(input);
var json = JsonConvert.SerializeObject(msg, Formatting.Indented, new JsonSerializerSettings
{
ContractResolver = cr
});
return Encoding.UTF8.GetBytes(json);
}
}
}

View File

@ -0,0 +1,39 @@
#if NET35
using System.Collections.Generic;
using System.Threading;
namespace NetFX.Polyfill
{
public class BlockingCollection<T>
{
private object _queueLock = new();
private Queue<T> _queue = new();
private AutoResetEvent _objectAvailableEvent = new(false);
public T Take()
{
lock (_queueLock)
{
if (_queue.Count > 0)
return _queue.Dequeue();
}
_objectAvailableEvent.WaitOne();
return Take();
}
public void Add(T obj)
{
lock (_queueLock)
{
_queue.Enqueue(obj);
}
_objectAvailableEvent.Set();
}
}
}
#endif

View File

@ -0,0 +1,99 @@
//using System;
//using System.Collections;
//using System.Collections.Generic;
//using System.Linq;
//using System.Text;
//namespace NetFX.Polyfill
//{
// public class ConcurrentDictionary<K, V> : IDictionary<K, V>
// {
// private Dictionary<K, V> dictionary;
// public V this[K key] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
// public ICollection<K> Keys => throw new NotImplementedException();
// public ICollection<V> Values => throw new NotImplementedException();
// public int Count => throw new NotImplementedException();
// public bool IsReadOnly => throw new NotImplementedException();
// public void Add(K key, V value)
// {
// throw new NotImplementedException();
// }
// public void Add(KeyValuePair<K, V> item)
// {
// throw new NotImplementedException();
// }
// public void Clear()
// {
// throw new NotImplementedException();
// }
// public bool Contains(KeyValuePair<K, V> item)
// {
// throw new NotImplementedException();
// }
// public bool ContainsKey(K key)
// {
// throw new NotImplementedException();
// }
// public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex)
// {
// throw new NotImplementedException();
// }
// public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
// {
// lock (this)
// {
// foreach (var item in dictionary)
// {
// yield return item;
// }
// }
// }
// public bool Remove(K key)
// {
// lock (this)
// {
// return dictionary.Remove(key);
// }
// }
// public bool Remove(KeyValuePair<K, V> item)
// {
// lock (this)
// {
// return dictionary.Remove(item.Key);
// }
// }
// public bool TryGetValue(K key, out V value)
// {
// lock (this)
// {
// return dictionary.TryGetValue(key, out value);
// }
// }
// IEnumerator IEnumerable.GetEnumerator()
// {
// lock (this)
// {
// foreach (var item in dictionary)
// {
// yield return item;
// }
// }
// }
// }
//}

View File

@ -0,0 +1,27 @@
#if !NET5_0_OR_GREATER
using System.Collections.Generic;
namespace NetFX.Polyfill
{
public static class DictionaryExtensions
{
public static V GetValueOrDefault<K, V>(this Dictionary<K, V> dict, K key, V defaultValue)
{
if (dict.TryGetValue(key, out V value))
{
return value;
}
return defaultValue;
}
public static V GetValueOrDefault<K, V>(this Dictionary<K, V> dict, K key)
{
#pragma warning disable CS8604 // Possible null reference argument.
return dict.GetValueOrDefault<K, V>(key, default);
#pragma warning restore CS8604 // Possible null reference argument.
}
}
}
#endif

View File

@ -0,0 +1,16 @@
#if !NET5_0_OR_GREATER
using System.Text.RegularExpressions;
namespace NetFX.Polyfill
{
public static class GroupCollectionExtension
{
public static bool ContainsKey(this GroupCollection collection, string key)
{
return collection[key].Success;
}
}
}
#endif

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net3.5;net4.5;net4.7.2</TargetFrameworks>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x86</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetZip" Version="1.12.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net3.5' ">
<PackageReference Include="System.Buffers" Version="4.5.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NetFX.Polyfill
{
public static class ProcessStartInfoHelper
{
public static string ArgumentListToArgsString(IList<string> arguments)
{
var stringBuilder = new StringBuilder();
foreach (var argument in arguments)
{
AppendArgument(stringBuilder, argument);
}
return stringBuilder.ToString();
}
public static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length != 0)
{
stringBuilder.Append(' ');
}
// Parsing rules for non-argv[0] arguments:
// - Backslash is a normal character except followed by a quote.
// - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote
// - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote
// - Parsing stops at first whitespace outside of quoted region.
// - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode.
if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument))
{
// Simple case - no quoting or changes needed.
stringBuilder.Append(argument);
}
else
{
stringBuilder.Append(Quote);
int idx = 0;
while (idx < argument.Length)
{
char c = argument[idx++];
if (c == Backslash)
{
int numBackSlash = 1;
while (idx < argument.Length && argument[idx] == Backslash)
{
idx++;
numBackSlash++;
}
if (idx == argument.Length)
{
// We'll emit an end quote after this so must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2);
}
else if (argument[idx] == Quote)
{
// Backslashes will be followed by a quote. Must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2 + 1);
stringBuilder.Append(Quote);
idx++;
}
else
{
// Backslash will not be followed by a quote, so emit as normal characters.
stringBuilder.Append(Backslash, numBackSlash);
}
continue;
}
if (c == Quote)
{
// Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed
// by another quote (which parses differently pre-2008 vs. post-2008.)
stringBuilder.Append(Backslash);
stringBuilder.Append(Quote);
continue;
}
stringBuilder.Append(c);
}
stringBuilder.Append(Quote);
}
}
private static bool ContainsNoWhitespaceOrQuotes(string s)
{
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (char.IsWhiteSpace(c) || c == Quote)
{
return false;
}
}
return true;
}
private const char Quote = '\"';
private const char Backslash = '\\';
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace NetFX.Polyfill
{
public static class StreamExtension
{
public static void CopyTo(this Stream stream, Stream destination)
{
#if NET35
var buffer = new byte[8192];
#else
var buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(8192);
#endif
try
{
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) != 0)
destination.Write(buffer, 0, read);
}
finally
{
#if !NET35
System.Buffers.ArrayPool<byte>.Shared.Return(buffer);
#endif
}
}
}
}

View File

@ -0,0 +1,62 @@
//https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TupleElementNamesAttribute.cs
#if NET35
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Indicates that the use of <see cref="System.ValueTuple"/> on a member is meant to be treated as a tuple with element names.
/// </summary>
//[CLSCompliant(false)]
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Event)]
public sealed class TupleElementNamesAttribute : Attribute
{
private readonly string?[] _transformNames;
/// <summary>
/// Initializes a new instance of the <see
/// cref="TupleElementNamesAttribute"/> class.
/// </summary>
/// <param name="transformNames">
/// Specifies, in a pre-order depth-first traversal of a type's
/// construction, which <see cref="System.ValueType"/> occurrences are
/// meant to carry element names.
/// </param>
/// <remarks>
/// This constructor is meant to be used on types that contain an
/// instantiation of <see cref="System.ValueType"/> that contains
/// element names. For instance, if <c>C</c> is a generic type with
/// two type parameters, then a use of the constructed type <c>C{<see
/// cref="System.ValueTuple{T1, T2}"/>, <see
/// cref="System.ValueTuple{T1, T2, T3}"/></c> might be intended to
/// treat the first type argument as a tuple with element names and the
/// second as a tuple without element names. In which case, the
/// appropriate attribute specification should use a
/// <c>transformNames</c> value of <c>{ "name1", "name2", null, null,
/// null }</c>.
/// </remarks>
public TupleElementNamesAttribute(string?[] transformNames)
{
if (transformNames == null)
{
throw new ArgumentNullException(nameof(transformNames));
}
_transformNames = transformNames;
}
/// <summary>
/// Specifies, in a pre-order depth-first traversal of a type's
/// construction, which <see cref="System.ValueTuple"/> elements are
/// meant to carry element names.
/// </summary>
public IList<string?> TransformNames => _transformNames;
}
}
#endif

View File

@ -0,0 +1,66 @@
#if NET35
namespace System
{
public struct ValueTuple<T1, T2>
{
public T1 Item1;
public T2 Item2;
public ValueTuple(T1 t1, T2 t2)
{
this.Item1 = t1;
this.Item2 = t2;
}
}
public struct ValueTuple<T1, T2, T3>
{
public T1 Item1;
public T2 Item2;
public T3 Item3;
public ValueTuple(T1 t1, T2 t2, T3 t3)
{
this.Item1 = t1;
this.Item2 = t2;
this.Item3 = t3;
}
}
public struct ValueTuple<T1, T2, T3, T4>
{
public T1 Item1;
public T2 Item2;
public T3 Item3;
public T4 Item4;
public ValueTuple(T1 t1, T2 t2, T3 t3, T4 t4)
{
this.Item1 = t1;
this.Item2 = t2;
this.Item3 = t3;
this.Item4 = t4;
}
}
public struct ValueTuple<T1, T2, T3, T4, T5>
{
public T1 Item1;
public T2 Item2;
public T3 Item3;
public T4 Item4;
public T5 Item5;
public ValueTuple(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5)
{
this.Item1 = t1;
this.Item2 = t2;
this.Item3 = t3;
this.Item4 = t4;
this.Item5 = t5;
}
}
}
#endif

View File

@ -0,0 +1,91 @@
//using System;
//using System.Reflection;
//using System.Text;
//using XDM.Core.Lib.Common;
//namespace SerializeGenerator
//{
// class Program
// {
// static int c = 1;
// static void Main(string[] args)
// {
// var type = typeof(Config);
// }
// private static void GenerateCode(Type type, StringBuilder serializer, StringBuilder deserializer)
// {
// foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
// {
// if (property.PropertyType.IsArray || property.PropertyType.GetInterface("IEnumerable") != null)
// {
// if (property.PropertyType.IsArray)
// {
// serializer.Append($"var c{c++}=obj.{property.Name}?.Length??0;\r\n");
// serializer.Append($"writer.Write(c{c});\r\n");
// serializer.Append($"if(obj.{property.Name}!=null)" + "{foreach(var item in obj." + property.Name + "){");
// //deserializer.Append($"var c{c++}=reader.ReadInt32();\r\n");
// //deserializer.Append("if(c" + c + ">0){");
// GenerateCode(property.PropertyType, serializer, deserializer);
// serializer.Append("}}\r\n");
// }
// }
// if (property.PropertyType == typeof(string)
// || property.PropertyType == typeof(int)
// || property.PropertyType == typeof(long)
// || property.PropertyType == typeof(bool)
// || property.PropertyType == typeof(double)
// || property.PropertyType == typeof(float)
// || property.PropertyType.IsEnum)
// {
// if (property.PropertyType == typeof(string))
// {
// serializer.Append($"writer.Write(obj.{property.Name}??string.Empty);\r\n");
// deserializer.Append($"obj.{property.Name}=Helper.ReadString(reader);\r\n");
// }
// else if (property.PropertyType.IsEnum)
// {
// serializer.Append($"writer.Write((int)obj.{property.Name});\r\n");
// deserializer.Append($"obj.{property.Name}=({property.PropertyType})reader.ReadInt32();\r\n");
// }
// else if (property.PropertyType == typeof(int))
// {
// serializer.Append($"writer.Write(obj.{property.Name});\r\n");
// deserializer.Append($"obj.{property.Name}=reader.ReadInt32();\r\n");
// }
// else if (property.PropertyType == typeof(long))
// {
// serializer.Append($"writer.Write(obj.{property.Name});\r\n");
// deserializer.Append($"obj.{property.Name}=reader.ReadInt64();\r\n");
// }
// else if (property.PropertyType == typeof(bool))
// {
// serializer.Append($"writer.Write(obj.{property.Name});\r\n");
// deserializer.Append($"obj.{property.Name}=reader.ReadBoolean();\r\n");
// }
// else if (property.PropertyType == typeof(double))
// {
// serializer.Append($"writer.Write(obj.{property.Name});\r\n");
// deserializer.Append($"obj.{property.Name}=reader.ReadDouble();\r\n");
// }
// else if (property.PropertyType == typeof(float))
// {
// serializer.Append($"writer.Write(obj.{property.Name});\r\n");
// deserializer.Append($"obj.{property.Name}=reader.ReadFloat();\r\n");
// }
// }
// else
// {
// if (property.PropertyType.IsValueType || property.PropertyType.IsClass)
// {
// GenerateCode(property.PropertyType, serializer, deserializer);
// }
// }
// }
// }
// }
//}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\XDM_CoreFx\XDM.Core.csproj" />
</ItemGroup>
</Project>

18
app/XDM/TraceLog/Log.cs Normal file
View File

@ -0,0 +1,18 @@
using System;
using System.Diagnostics;
namespace TraceLog
{
public static class Log
{
public static void Debug(object obj, string message)
{
Trace.WriteLine(message + " : " + obj);
}
public static void Debug(string message)
{
Trace.WriteLine(message);
}
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net3.5;net4.5;net4.7.2;net5.0</TargetFrameworks>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x86</Platforms>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Translations
{
public static class TextResource
{
private static Dictionary<string, string> texts = new();
static TextResource()
{
Load("English");
}
public static void Load(string language)
{
var file = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.Combine("Lang", $"{language}.txt"));
if (File.Exists(file))
{
LoadTexts(file);
}
}
public static string GetText(string key)
{
if (texts.TryGetValue(key, out string? label) && label != null)
{
return label;
}
return string.Empty;
}
private static void LoadTexts(string path)
{
var lines = File.ReadAllLines(path);
foreach (var line in lines)
{
var index = line.IndexOf('=');
var key = line.Substring(0, index);
var val = line.Substring(index + 1);
texts[key] = val;
}
}
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net3.5;net4.5;net4.7.2;net5.0</TargetFrameworks>
<Platforms>AnyCPU;x86</Platforms>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,103 @@
using System.IO;
using TraceLog;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.UI;
namespace XDM.Common.UI
{
internal static class CommonUtils
{
private static string AutoSelectText = "Automatically select based on file type";
private static string BrowseText = "Browse...";
//internal static void ProcessManualSelection(string selectedFile, IFileSelectable window, ref string? selectedFolder)
//{
// var file = Path.GetFileName(selectedFile);
// var folder = Path.GetDirectoryName(selectedFile);
// window.SelectedFileName = file;
// if (!Config.Instance.RecentFolders.Contains(folder!))
// {
// Config.Instance.RecentFolders.Insert(0, folder!);
// selectedFolder = folder!;
// }
// Config.Instance.FolderSelectionMode = FolderSelectionMode.Manual;
// Config.SaveConfig();
// window.FolderSelectionMode = FolderSelectionMode.Manual;
//}
internal static string[] GetFolderValues()
{
if (!Config.Instance.RecentFolders.Contains(Config.Instance.DefaultDownloadFolder))
{
Config.Instance.RecentFolders.Insert(0, Config.Instance.DefaultDownloadFolder);
}
var arr = new string[Config.Instance.RecentFolders.Count + 2];
arr[0] = AutoSelectText;
arr[1] = BrowseText;
var k = 2;
for (var i = 0; i < Config.Instance.RecentFolders.Count; i++, k++)
{
arr[k] = Config.Instance.RecentFolders[i];
}
return arr;
}
internal static void OnFileBrowsed(object? sender, FileBrowsedEventArgs args)
{
var file = Path.GetFileName(args.SelectedFile);
var folder = Path.GetDirectoryName(args.SelectedFile)!;
if (!Config.Instance.RecentFolders.Contains(folder))
{
Config.Instance.RecentFolders.Insert(0, folder);
}
if (sender != null)
{
var fileSelectable = (IFileSelectable)sender;
fileSelectable.SelectedFileName = file;
fileSelectable.SetFolderValues(GetFolderValues());
fileSelectable.SeletedFolderIndex = 2;
}
Config.Instance.FolderSelectionMode = FolderSelectionMode.Manual;
Config.SaveConfig();
}
internal static void OnDropdownSelectionChanged(object? sender, FileBrowsedEventArgs args)
{
if (sender != null)
{
var fileSelectable = (IFileSelectable)sender;
var index = fileSelectable.SeletedFolderIndex;
if (index == 0)
{
Config.Instance.FolderSelectionMode = FolderSelectionMode.Auto;
}
else
{
Config.Instance.FolderSelectionMode = FolderSelectionMode.Manual;
if (index != 1)
{
Config.Instance.RecentFolders.Remove(args.SelectedFile);
Config.Instance.RecentFolders.Insert(0, args.SelectedFile);
}
}
Config.SaveConfig();
}
}
internal static string? SelectedFolderFromIndex(int index)
{
if (Config.Instance.FolderSelectionMode == FolderSelectionMode.Auto) return null;
if (index == 0 || index == 1)
{
Log.Debug($"Index value {index} is invalid for {Config.Instance.FolderSelectionMode}");
return null;
}
if (index - 2 < Config.Instance.RecentFolders.Count)
{
return Config.Instance.RecentFolders[index - 2];
}
return Config.Instance.DefaultDownloadFolder;
}
}
}

View File

@ -0,0 +1,163 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using TraceLog;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Common.Segmented;
using XDM.Core.Lib.Common.Segmented;
namespace XDM.Common.UI
{
public class ComponentUpdaterUI
{
private IApp app;
private UpdateMode updateMode;
private IUpdaterUI updaterUI;
private IList<UpdateInfo>? updates;
private int count = 0;
private SingleSourceHTTPDownloader? http;
private readonly IList<string> files = new List<string>();
private long size;
private long downloaded;
public ComponentUpdaterUI(IUpdaterUI updaterUI, IApp app, UpdateMode updateMode)
{
this.updaterUI = updaterUI;
this.updateMode = updateMode;
this.app = app;
try
{
this.updaterUI.Cancelled += (s, e) =>
{
if (this.http != null)
{
this.http.Stop();
}
this.http = null;
};
}
catch (Exception ex)
{
Log.Debug(ex, "ComponentUpdaterUI");
}
}
public void StartUpdate()
{
new Thread(() =>
{
try
{
updaterUI.Inderminate = true;
if (!UpdateChecker.GetAppUpdates(app.AppVerion, out IList<UpdateInfo> updates, out _, this.updateMode))
{
updaterUI.DownloadFailed(this, new DownloadFailedEventArgs(ErrorCode.Generic));
}
if (updates.Count == 0)
{
updaterUI.ShowNoUpdateMessage();
return;
}
foreach (var update in updates)
{
size += update.Size;
}
updaterUI.Inderminate = false;
StartUpdate(updates[0]);
}
catch (Exception ex)
{
Log.Debug(ex, ex.Message);
updaterUI.DownloadFailed(this, new DownloadFailedEventArgs(ErrorCode.Generic));
}
}).Start();
}
private void StartUpdate(UpdateInfo update)
{
try
{
Log.Debug("Downloading " + update.Name);
updaterUI.Label = "Downloading " + update.Name;
http = new SingleSourceHTTPDownloader(new SingleSourceHTTPDownloadInfo
{
Uri = update.Url,
Headers = new Dictionary<string, List<string>>
{
["User-Agent"] = new List<string>{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" }
}
});
http.SetTargetDirectory(Path.GetTempPath());
http.Started += updaterUI.DownloadStarted;
//http.Probed += HandleProbeResult;
http.Finished += Finished;
http.ProgressChanged += ProgressChanged;
http.Cancelled += updaterUI.DownloadCancelled;
http.Failed += updaterUI.DownloadFailed;
http.Start();
}
catch (Exception ex)
{
Log.Debug(ex, "StartUpdate");
updaterUI.DownloadFailed(this, new DownloadFailedEventArgs(ErrorCode.Generic));
}
}
private void ProgressChanged(object? sender, ProgressResultEventArgs e)
{
try
{
var totalProgress = (int)(((downloaded + e.Downloaded) * 100) / size);
this.updaterUI.DownloadProgressChanged(this, new ProgressResultEventArgs { Progress = totalProgress });
}
catch (Exception ex)
{
Log.Debug(ex, "ProgressChanged");
}
}
private void Finished(object? sender, EventArgs e)
{
try
{
Log.Debug("Finished " + updates[count].Name);
downloaded += updates[count].Size;
count++;
files.Add(http!.TargetFile);
if (count == updates.Count)
{
foreach (var file in files)
{
var name = Path.GetFileName(file);
var bakup = Path.Combine(Config.DataDir, name + ".bak");
var target = Path.Combine(Config.DataDir, name);
File.Move(file, bakup);
File.Delete(target);
File.Move(bakup, target);
}
File.WriteAllText(Path.Combine(Config.DataDir, "update-info.json"),
JsonConvert.SerializeObject(new UpdateHistory
{
FFmpegUpdateDate = DateTime.Now,
YoutubeDLUpdateDate = DateTime.Now
}));
updaterUI.DownloadFinished(sender, e);
return;
}
StartUpdate(updates[count]);
}
catch (Exception ex)
{
Log.Debug(ex, "Finished");
}
}
}
}

View File

@ -0,0 +1,23 @@
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Util;
namespace XDM.Common.UI
{
public static class DownloadCompleteDialogHelper
{
public static void ShowDialog(IApp app, IDownloadCompleteDialog dwnCmpldDlg, string file, string folder)
{
dwnCmpldDlg.FileNameText = file;
dwnCmpldDlg.FolderText = folder;
dwnCmpldDlg.FileOpenClicked += (sender, args) =>
{
Helpers.OpenFile(args.Path);
};
dwnCmpldDlg.FolderOpenClicked += (sender, args) =>
{
Helpers.OpenFolder(args.Path, args.FileName);
};
dwnCmpldDlg.ShowDownloadCompleteDialog();
}
}
}

View File

@ -0,0 +1,73 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TraceLog;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Common.Segmented;
using XDM.Core.Lib.Util;
namespace XDM.Common.UI
{
public static class LinkRefreshDialogHelper
{
public static void RefreshLink(BaseDownloadEntry item, IApp app, IRefreshLinkDialogSkeleton dialog)
{
string referer = null;
if (item.DownloadType == "Http")
{
var state = DownloadStateStore.SingleSourceHTTPDownloaderStateFromBytes(
File.ReadAllBytes(Path.Combine(Config.DataDir, item.Id + ".state")));
//JsonConvert.DeserializeObject<SingleSourceHTTPDownloaderState>(
// File.ReadAllText(Path.Combine(Config.DataDir, item.Id + ".state")));
referer = GetReferer(state.Headers);
}
else if (item.DownloadType == "Dash")
{
var state = DownloadStateStore.DualSourceHTTPDownloaderStateFromBytes(
File.ReadAllBytes(Path.Combine(Config.DataDir, item.Id + ".state")));
//JsonConvert.DeserializeObject<DualSourceHTTPDownloaderState>(
// File.ReadAllText(Path.Combine(Config.DataDir, item.Id + ".state")));
referer = GetReferer(state.Headers1);
}
Log.Debug("Referer: " + referer);
if (referer != null)
{
OpenBrowser(referer);
if (item.DownloadType == "Http")
{
var downloader = new SingleSourceHTTPDownloader(item.Id);
downloader.RestoreState();
app.RefreshedLinkReceived += (_, _) => dialog.LinkReceived();
app.WaitFromRefreshedLink(downloader);
}
else if (item.DownloadType == "Dash")
{
var downloader = new DualSourceHTTPDownloader(item.Id);
downloader.RestoreState();
app.RefreshedLinkReceived += (_, _) => dialog.LinkReceived();
app.WaitFromRefreshedLink(downloader);
}
dialog.ShowWindow();
}
dialog.WatchingStopped += (a, b) =>
{
app.ClearRefreshLinkCandidate();
};
}
private static string GetReferer(Dictionary<string, List<string>> headers)
{
return headers?.Where(
header => header.Key.ToLowerInvariant() == "referer")
.FirstOrDefault().Value?.FirstOrDefault();
}
private static void OpenBrowser(string url)
{
Helpers.OpenBrowser(url);
}
}
}

View File

@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Common.Segmented;
using XDM.Core.Lib.Util;
namespace XDM.Common.UI
{
public class NewDownloadDialogHelper
{
public static void CreateAndShowDialog(IApp app, IAppUI appUi,
INewDownloadDialogSkeleton window, Message message = null)
{
window.SetFolderValues(CommonUtils.GetFolderValues());
window.SeletedFolderIndex = Config.Instance.FolderSelectionMode == FolderSelectionMode.Auto ? 0 : 2;
var fileName = string.Empty;
if (message != null)
{
window.Url = message.Url;
fileName = Helpers.SanitizeFileName(message.File ?? Helpers.GetFileName(new Uri(message.Url)));
window.SelectedFileName = fileName;
var contentLength = 0L;
var header = message.GetResponseHeaderFirstValue("Content-Length");
if (!string.IsNullOrEmpty(header))
{
try
{
contentLength = Int64.Parse(header);
}
catch { }
}
window.SetFileSizeText(contentLength > 0 ? Helpers.FormatSize(contentLength) : "---");
}
else
{
var url = appUi.GetUrlFromClipboard();
if (url != null)
{
window.Url = url;
window.SelectedFileName = Helpers.SanitizeFileName(Helpers.GetFileName(new Uri(url)));
}
window.UrlChangedEvent += (sender, args) =>
{
if (Helpers.IsUriValid(window.Url))
{
window.SelectedFileName = Helpers.SanitizeFileName(Helpers.GetFileName(new Uri(window.Url)));
fileName = window.SelectedFileName;
}
};
}
window.FileBrowsedEvent += CommonUtils.OnFileBrowsed;
window.DropdownSelectionChangedEvent += CommonUtils.OnDropdownSelectionChanged;
window.UrlBlockedEvent += (sender, args) =>
{
if (Helpers.IsUriValid(window.Url))
{
var url = new Uri(window.Url);
var blockedHost = new List<string>();
blockedHost.AddRange(Config.Instance.BlockedHosts);
blockedHost.Add(url.Host);
Config.Instance.BlockedHosts = blockedHost.ToArray();
Config.SaveConfig();
app.ApplyConfig();
window.DisposeWindow();
}
};
window.DownloadClicked += (a, b) =>
{
OnDownloadClicked(app, appUi, window, fileName, CommonUtils.SelectedFolderFromIndex(window.SeletedFolderIndex), message, true);
};
window.DownloadLaterClicked += (a, b) =>
{
OnDownloadClicked(app, appUi, window, fileName, CommonUtils.SelectedFolderFromIndex(window.SeletedFolderIndex), message, false, b.QueueId);
};
window.QueueSchedulerClicked += (s, e) =>
{
appUi.ShowQueueWindow(s);
};
window.ShowWindow();
}
//public static void CreateAndShowDialog(IApp app, IAppUI appUi, INewDownloadDialogSkeleton window)
//{
// window.FolderSelectionMode = Config.FolderSelectionMode;
// window.ConflictResolution = Config.FileConflictResolution;
// var url = appUi.GetUrlFromClipboard();
// if (url != null)
// {
// window.Url = url;
// window.File = Helpers.GetFileName(new Uri(url));
// }
// var file = string.Empty;
// window.UrlChangedEvent += (sender, args) =>
// {
// if (Helpers.IsUriValid(window.Url))
// {
// window.File = Helpers.GetFileName(new Uri(window.Url));
// file = window.File;
// }
// };
// string selectedFolder = null;
// window.BrowseClicked += (a, b) =>
// {
// var selectedFile = window.SelectFile();
// if (selectedFile != null)
// {
// CommonUtils.GetFolder(selectedFile, window, ref selectedFolder);
// }
// };
// window.DownloadClicked += (a, b) =>
// {
// OnDownloadClicked(app, appUi, window, file, selectedFolder, null, true);
// };
// window.ShowWindow();
//}
private static void OnDownloadClicked(IApp app, IAppUI appUi, INewDownloadDialogSkeleton window,
string fileName, string? selectedFolder, Message message, bool startImmediately, string? queueId = null)
{
if (!Helpers.IsUriValid(window.Url))
{
appUi.ShowMessageBox(window, "URL is invalid");
return;
}
if (string.IsNullOrEmpty(window.SelectedFileName))
{
appUi.ShowMessageBox(window, "No filename");
return;
}
var contentLength = 0L;
var header = message?.GetResponseHeaderFirstValue("Content-Length") ?? message?.GetResponseHeaderFirstValue("content-length");// message?.ResponseHeaders?.Keys.Where(key => key.Equals("content-length", StringComparison.InvariantCultureIgnoreCase));
if (!string.IsNullOrEmpty(header))
{
try
{
contentLength = Int64.Parse(header);
}
catch { }
}
app.StartDownload(
new SingleSourceHTTPDownloadInfo
{
Uri = window.Url,
Headers = message?.RequestHeaders,
Cookies = message?.Cookies,
ContentLength = contentLength
},
Helpers.SanitizeFileName(window.SelectedFileName),
window.SelectedFileName != fileName ? FileNameFetchMode.None : FileNameFetchMode.FileNameAndExtension,
selectedFolder,
startImmediately,
window.Authentication, window.Proxy ?? Config.Instance.Proxy,
window.EnableSpeedLimit ? window.SpeedLimit : 0, queueId);
//var http = new SingleSourceHTTPDownloader(new SingleSourceHTTPDownloadInfo
//{
// Uri = window.Url,
// Headers = message?.RequestHeaders,
// Cookies = message?.Cookies,
// ContentLength = contentLength
//});
//if (window.File != fileName)
//{
// http.SetFileName(Helpers.SanitizeFileName(window.File), FileNameFetchMode.None);
//}
//else
//{
// http.SetFileName(Helpers.SanitizeFileName(window.File), FileNameFetchMode.FileNameAndExtension);
//}
//if (window.FolderSelectionMode == FolderSelectionMode.Manual)
//{
// http.SetTargetDirectory(selectedFolder);
//}
//app.StartDownload(http, startImmediately);
window.DisposeWindow();
}
}
}

View File

@ -0,0 +1,89 @@
using System.IO;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.Common.MediaProcessor;
using XDM.Core.Lib.Util;
namespace XDM.Common.UI
{
public class NewVideoDownloadDialogHelper
{
public static void ShowVideoDownloadDialog(IApp app, IAppUI appUi, INewVideoDownloadDialog window, string id, string name, long size)
{
window.SetFolderValues(CommonUtils.GetFolderValues());
window.SeletedFolderIndex = Config.Instance.FolderSelectionMode == FolderSelectionMode.Auto ? 0 : 2;
window.SelectedFileName = Helpers.SanitizeFileName(name);
window.FileSize = Helpers.FormatSize(size);
window.FileBrowsedEvent += CommonUtils.OnFileBrowsed;
window.DropdownSelectionChangedEvent += CommonUtils.OnDropdownSelectionChanged;
window.DownloadClicked += (a, b) =>
{
if (string.IsNullOrEmpty(window.SelectedFileName))
{
appUi.ShowMessageBox(window, "No filename");
return;
}
if (app.IsFFmpegRequiredForDownload(id) && !IsFFmpegInstalled())
{
if (appUi.Confirm(window, "Download FFmpeg?"))
{
appUi.InstallLatestFFmpeg();
}
return;
}
app.StartVideoDownload(id, Helpers.SanitizeFileName(window.SelectedFileName),
CommonUtils.SelectedFolderFromIndex(window.SeletedFolderIndex),
true,
window.Authentication,
window.Proxy ?? Config.Instance.Proxy,
window.EnableSpeedLimit ? window.SpeedLimit : 0,
null);
window.DisposeWindow();
};
window.DownloadLaterClicked += (a, b) =>
{
if (string.IsNullOrEmpty(window.SelectedFileName))
{
appUi.ShowMessageBox(window, "No filename");
return;
}
app.StartVideoDownload(id, Helpers.SanitizeFileName(window.SelectedFileName),
CommonUtils.SelectedFolderFromIndex(window.SeletedFolderIndex),
false,
window.Authentication,
window.Proxy ?? Config.Instance.Proxy,
window.EnableSpeedLimit ? window.SpeedLimit : 0,
b.QueueId);
window.DisposeWindow();
};
window.CancelClicked += (a, b) =>
{
window.DisposeWindow();
};
window.QueueSchedulerClicked += (s, e) =>
{
appUi.ShowQueueWindow(s);
};
window.ShowWindow();
}
private static bool IsFFmpegInstalled()
{
try
{
FFmpegMediaProcessor.FindFFmpegBinary();
}
catch (FileNotFoundException)
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net3.5;net4.5;net4.7.2;net5.0</TargetFrameworks>
<Platforms>AnyCPU;x86</Platforms>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TraceLog\TraceLog.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\XDM_CoreFx\XDM.Core.csproj" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net5.0' ">
<ProjectReference Include="..\NetFX.Polyfill\NetFX.Polyfill.csproj" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,984 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Gtk;
using System.IO;
using GLib;
using Application = Gtk.Application;
using IoPath = System.IO.Path;
using XDM.Core.Lib.Common;
using XDMApp;
using XDM.Core.Lib.Util;
using XDM.Core.Lib.Common;
using XDM.Common.UI;
using Newtonsoft.Json;
using Log = Serilog.Log;
using Translations;
using XDM.Core.Lib.UI;
using Menu = Gtk.Menu;
using MenuItem = Gtk.MenuItem;
namespace XDM.GtkUI
{
public class AppWinPeer : Window, IAppWinPeer
{
private TreeStore categoryTreeStore;
private TreeView categoryTree;
private ListStore inprogressDownloadsStore, finishedDownloadsStore;
private TreeView lvInprogress, lvFinished;
private ScrolledWindow swInProgress, swFinished;
private TreeModelFilter filter;
private string? searchKeyword;
private Category? category;
private ToolButton btnNew, btnDel, btnOpenFile, btnOpenFolder, btnResume, btnPause;
private IButton newButton, deleteButton, pauseButton, resumeButton, openFileButton, openFolderButton;
private IMenuItem[] menuItems;
public IEnumerable<FinishedDownloadEntry> FinishedDownloads
{
get => GetAllFinishedDownloads();
set => SetFinishedDownloads(value);
}
public IEnumerable<InProgressDownloadEntry> InProgressDownloads
{
get => GetAllInProgressDownloads();
set => SetInProgressDownloads(value);
}
public IList<IInProgressDownloadRow> SelectedInProgressRows => GetSelectedInProgressDownloads();
public IList<IFinishedDownloadRow> SelectedFinishedRows => GetSelectedFinishedDownloads();
public IButton NewButton => this.newButton;
public IButton DeleteButton => this.deleteButton;
public IButton PauseButton => this.pauseButton;
public IButton ResumeButton => this.resumeButton;
public IButton OpenFileButton => this.openFileButton;
public IButton OpenFolderButton => this.openFolderButton;
public bool IsInProgressViewSelected => GetSelectedCategory() == 0;
public IMenuItem[] MenuItems => this.menuItems;
public Dictionary<string, IMenuItem> MenuItemMap { get; private set; }
public event EventHandler ClipboardChanged;
public event EventHandler InProgressContextMenuOpening;
public event EventHandler FinishedContextMenuOpening;
public event EventHandler SelectionChanged;
public event EventHandler NewDownloadClicked;
public event EventHandler YoutubeDLDownloadClicked;
public event EventHandler BatchDownloadClicked;
public event EventHandler SettingsClicked;
public event EventHandler ClearAllFinishedClicked;
public event EventHandler ExportClicked;
public event EventHandler ImportClicked;
public event EventHandler BrowserMonitoringButtonClicked;
public event EventHandler BrowserMonitoringSettingsClicked;
public event EventHandler UpdateClicked;
public event EventHandler HelpClicked;
public event EventHandler SupportPageClicked;
public event EventHandler BugReportClicked;
public event EventHandler CheckForUpdateClicked;
public event EventHandler<CategoryChangedEventArgs> CategoryChanged;
public event EventHandler SchedulerClicked;
public event EventHandler MoveToQueueClicked;
private const int FINISHED_DATA_INDEX = 3;
private const int INPROGRESS_DATA_INDEX = 5;
private Menu menuInProgress, menuFinished;
public AppWinPeer() : base("Xtreme Download Manager")
{
SetDefaultSize(800, 500);
SetPosition(WindowPosition.Center);
DeleteEvent += AppWin1_DeleteEvent;
var hbMain = new HBox();
Add(hbMain);
hbMain.PackStart(CreateCategoryTree(), false, true, 2);
hbMain.PackStart(CreateMainPanel(), true, true, 1);
categoryTreeStore!.GetIterFirst(out TreeIter iter);
categoryTreeStore.IterNext(ref iter);
categoryTree!.Selection.SelectIter(iter);
CreateMenu();
}
private void CreateMenu()
{
menuItems = new IMenuItem[]
{
new MenuItemWrapper("pause",new MenuItem("Pause")),
new MenuItemWrapper("resume",new MenuItem("Resume") ),
new MenuItemWrapper("delete",new MenuItem("Delete")),
new MenuItemWrapper("saveAs",new MenuItem("Save As")),
new MenuItemWrapper("refresh",new MenuItem("Refresh link")),
new MenuItemWrapper("showProgress",new MenuItem("Show progress")),
new MenuItemWrapper("copyURL",new MenuItem("Copy URL")),
new MenuItemWrapper("properties",new MenuItem("Properties")),
new MenuItemWrapper("open",new MenuItem("Open")),
new MenuItemWrapper("openFolder",new MenuItem("Open folder")),
new MenuItemWrapper("deleteDownloads",new MenuItem("Delete downloads")),
new MenuItemWrapper("copyURL1",new MenuItem("Copy URL")),
new MenuItemWrapper("copyFile",new MenuItem("Copy file")),
new MenuItemWrapper("properties1",new MenuItem("Properties")),
new MenuItemWrapper("restart",new MenuItem("Restart")),
new MenuItemWrapper("schedule",new MenuItem("Schedule")),
new MenuItemWrapper("downloadAgain",new MenuItem("Download again"))
};
var dict = new Dictionary<string, IMenuItem>();
foreach (var mi in menuItems)
{
dict[mi.Name] = mi;
}
this.MenuItemMap = dict;
menuFinished = new Menu();
menuFinished.Append(((MenuItemWrapper)dict["open"]).MenuItem);
menuFinished.Append(((MenuItemWrapper)dict["openFolder"]).MenuItem);
menuFinished.Append(((MenuItemWrapper)dict["deleteDownloads"]).MenuItem);
menuFinished.Append(((MenuItemWrapper)dict["downloadAgain"]).MenuItem);
menuFinished.Append(((MenuItemWrapper)dict["copyURL1"]).MenuItem);
menuFinished.Append(((MenuItemWrapper)dict["copyFile"]).MenuItem);
menuFinished.Append(((MenuItemWrapper)dict["properties1"]).MenuItem);
menuFinished.ShowAll();
menuInProgress = new Menu();
menuInProgress.Append(((MenuItemWrapper)dict["pause"]).MenuItem);
menuInProgress.Append(((MenuItemWrapper)dict["resume"]).MenuItem);
menuInProgress.Append(((MenuItemWrapper)dict["delete"]).MenuItem);
menuInProgress.Append(((MenuItemWrapper)dict["saveAs"]).MenuItem);
menuInProgress.Append(((MenuItemWrapper)dict["refresh"]).MenuItem);
menuInProgress.Append(((MenuItemWrapper)dict["restart"]).MenuItem);
menuInProgress.Append(((MenuItemWrapper)dict["schedule"]).MenuItem);
menuInProgress.Append(((MenuItemWrapper)dict["showProgress"]).MenuItem);
menuInProgress.Append(((MenuItemWrapper)dict["copyURL"]).MenuItem);
menuInProgress.Append(((MenuItemWrapper)dict["properties"]).MenuItem);
menuInProgress.ShowAll();
}
private Widget CreateMainPanel()
{
var vbMain = new VBox();
vbMain.PackStart(CreateToolbar(), false, false, 1);
vbMain.PackStart(CreateInProgressListView(), true, true, 1);
vbMain.PackStart(CreateFinishedListView(), true, true, 1);
return vbMain;
}
private Widget CreateToolbar()
{
var toolbar = new Toolbar
{
Style = ToolbarStyle.BothHoriz
};
btnNew = new ToolButton(new Image(LoadSvg("links-line", 14)), TextResource.GetText("DESC_NEW")) { IsImportant = true, MarginStart = 0, MarginEnd = 0 };
toolbar.Add(btnNew);
btnDel = new ToolButton(new Image(LoadSvg("delete-bin-7-line", 14)), TextResource.GetText("DESC_DEL")) { IsImportant = true, MarginStart = 0, MarginEnd = 0 };
toolbar.Add(btnDel);
btnOpenFile = new ToolButton(new Image(LoadSvg("external-link-line", 14)), TextResource.GetText("CTX_OPEN_FILE")) { IsImportant = true, MarginStart = 0, MarginEnd = 0 };
toolbar.Add(btnOpenFile);
btnOpenFolder = new ToolButton(new Image(LoadSvg("folder-shared-line", 14)), TextResource.GetText("CTX_OPEN_FOLDER")) { IsImportant = true, MarginStart = 0, MarginEnd = 0 };
toolbar.Add(btnOpenFolder);
btnResume = new ToolButton(new Image(LoadSvg("play-line", 14)), TextResource.GetText("MENU_RESUME")) { IsImportant = true, MarginStart = 0, MarginEnd = 0 };
toolbar.Add(btnResume);
btnPause = new ToolButton(new Image(LoadSvg("pause-line", 14)), TextResource.GetText("MENU_PAUSE")) { IsImportant = true, MarginStart = 0, MarginEnd = 0 };
toolbar.Add(btnPause);
toolbar.Add(new ToolItem() { Expand = true });
//toolbar.Add(new SeparatorToolItem() { Expand = true, Draw = false });
var cont = new ToolItem() { MarginEnd = 3 };
var searchEntry = new Entry() { WidthChars = 10, PlaceholderText = TextResource.GetText("LBL_SEARCH") };
searchEntry.Activated += (a, b) =>
{
searchKeyword = searchEntry.Text;
filter.Refilter();
};
cont.Add(searchEntry);
toolbar.Add(cont);
var btnMenu = new ToolButton(new Image(LoadSvg("menu-line", 14)), string.Empty) { IsImportant = false };
toolbar.Add(btnMenu);
newButton = new ButtonWrapper(this.btnNew);
deleteButton = new ButtonWrapper(this.btnDel);
pauseButton = new ButtonWrapper(this.btnPause);
resumeButton = new ButtonWrapper(this.btnResume);
openFileButton = new ButtonWrapper(this.btnOpenFile);
openFolderButton = new ButtonWrapper(this.btnOpenFolder);
return toolbar;
}
private Widget CreateCategoryTree()
{
string GetFontIcon(string name)
{
switch (name)
{
case "CAT_DOCUMENTS":
return "file-text-line";
case "CAT_MUSIC":
return "file-music-line";
case "CAT_VIDEOS":
return "movie-line";
case "CAT_COMPRESSED":
return "file-zip-line";
case "CAT_PROGRAMS":
return "function-line";
default:
return "file-line";
}
}
categoryTree = new TreeView()
{
HeadersVisible = false
};
categoryTree.StyleContext.AddClass("dark");
var cols = new TreeViewColumn();
var cell1 = new CellRendererPixbuf();
cell1.SetPadding(5, 5);
cols.PackStart(cell1, false);
cols.AddAttribute(cell1, "pixbuf", 0);
var cell2 = new CellRendererText();
cell2.SetPadding(0, 5);
cols.PackStart(cell2, true);
cols.AddAttribute(cell2, "text", 1);
categoryTreeStore = new TreeStore(typeof(Gdk.Pixbuf), typeof(string), typeof(Category));
categoryTreeStore.AppendValues(LoadSvg("arrow-down-line"), TextResource.GetText("ALL_UNFINISHED"));
var iter = categoryTreeStore.AppendValues(LoadSvg("check-line"), TextResource.GetText("ALL_FINISHED"));
foreach (var category in Config.Instance.Categories)
{
categoryTreeStore.AppendValues(iter, LoadSvg(GetFontIcon(category.Name), 20),
category.DisplayName, category);
}
categoryTree.AppendColumn(cols);
categoryTree.Model = categoryTreeStore;
categoryTree.Selection.Mode = SelectionMode.Browse;
categoryTree.ExpandAll();
categoryTree.Selection.Changed += OnCategoryChanged;
var scrolledWindow = new ScrolledWindow
{
OverlayScrolling = true,
Margin = 5,
MarginEnd = 2
};
//scrolledWindow.Margin = 5;
scrolledWindow.ShadowType = ShadowType.In;
scrolledWindow.SetPolicy(PolicyType.Automatic, PolicyType.Automatic);
scrolledWindow.Add(categoryTree);
scrolledWindow.SetSizeRequest(200, 200);
return scrolledWindow;
}
private void OnCategoryChanged(object? sender, EventArgs e)
{
if (lvInprogress == null || lvFinished == null)
{
return;
}
var paths = categoryTree.Selection.GetSelectedRows();
if (paths == null || paths.Length == 0)
{
return;
}
if (paths[0].Depth == 1)
{
var index = paths[0].Indices[0];
if (index == 0)
{
swInProgress.ShowAll();
swFinished.Hide();
category = null;
btnOpenFile.Visible = btnOpenFolder.Visible = false;
btnPause.Visible = btnResume.Visible = true;
}
else
{
swFinished.ShowAll();
swInProgress.Hide();
category = null;
filter.Refilter();
btnOpenFile.Visible = btnOpenFolder.Visible = true;
btnPause.Visible = btnResume.Visible = false;
}
}
else
{
swFinished.ShowAll();
swInProgress.Hide();
if (categoryTree.Selection.GetSelected(out ITreeModel model, out TreeIter iter))
{
category = (Category)model.GetValue(iter, 2);
}
filter.Refilter();
}
}
private Widget CreateInProgressListView()
{
inprogressDownloadsStore = new ListStore(typeof(string), // file name
typeof(string), // date modified
typeof(string), // size
typeof(int), // progress
typeof(string), // status
typeof(InProgressDownloadEntry) // download type
);
lvInprogress = new TreeView(inprogressDownloadsStore);
lvInprogress.Selection.Mode = SelectionMode.Multiple;
//File name column
var fileNameColumn = new TreeViewColumn
{
Resizable = true,
Reorderable = false,
Title = "Name",
Sizing = TreeViewColumnSizing.Fixed,
FixedWidth = 200
};
var fileIconRenderer = new CellRendererPixbuf { };
fileIconRenderer.SetPadding(5, 5);
fileNameColumn.PackStart(fileIconRenderer, false);
fileNameColumn.SetCellDataFunc(fileIconRenderer, new CellLayoutDataFunc(GetFileIcon));
var fileNameRendererText = new CellRendererText();
fileNameColumn.PackStart(fileNameRendererText, false);
fileNameColumn.SetAttributes(fileNameRendererText, "text", 0);
lvInprogress.AppendColumn(fileNameColumn);
//Last modified column
var lastModifiedRendererText = new CellRendererText();
var lastModifiedColumn = new TreeViewColumn("Date added", lastModifiedRendererText, "text", 1)
{
Resizable = true,
Reorderable = false,
Sizing = TreeViewColumnSizing.Fixed,
FixedWidth = 100
};
lastModifiedColumn.SetAttributes(lastModifiedRendererText, "text", 1);
lvInprogress.AppendColumn(lastModifiedColumn);
//File size column
var fileSizeRendererText = new CellRendererText();
//fileSizeRendererText.Xalign = 1.0f;
var fileSizeColumn = new TreeViewColumn
{
Resizable = true,
Reorderable = false,
Sizing = TreeViewColumnSizing.Fixed,
FixedWidth = 80,
Title = "Size",
};
fileSizeColumn.PackStart(fileSizeRendererText, false);
fileSizeColumn.SetAttributes(fileSizeRendererText, "text", 2);
lvInprogress.AppendColumn(fileSizeColumn);
//File progress column
var fileRendererProgress = new CellRendererProgress()
{
//Text = "Downloading",
};
fileRendererProgress.SetPadding(5, 10);
var progressColumn = new TreeViewColumn("Progress", fileRendererProgress, "value", 3)
{
Resizable = true,
Reorderable = false,
Sizing = TreeViewColumnSizing.Fixed,
FixedWidth = 80
};
progressColumn.SetAttributes(fileRendererProgress, "value", 3);
lvInprogress.AppendColumn(progressColumn);
//Download status column
var statusRendererText = new CellRendererText();
statusRendererText.SetPadding(5, 8);
var statusColumn = new TreeViewColumn("Status", statusRendererText, "text", 4)
{
Resizable = true,
Reorderable = false,
Sizing = TreeViewColumnSizing.Fixed,
FixedWidth = 80
};
statusColumn.SetAttributes(statusRendererText, "text", 4);
lvInprogress.AppendColumn(statusColumn);
lvInprogress.Selection.Changed += (_, _) =>
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
};
lvInprogress.ButtonReleaseEvent += (a, b) =>
{
if (b.Event.Type == Gdk.EventType.ButtonRelease && b.Event.Button == 3)
{
InProgressContextMenuOpening?.Invoke(this, EventArgs.Empty);
menuInProgress.PopupAtPointer(b.Event);
}
};
swInProgress = new ScrolledWindow { OverlayScrolling = true, Margin = 5, MarginStart = 0, MarginTop = 0, ShadowType = ShadowType.In };
swInProgress.SetPolicy(PolicyType.Automatic, PolicyType.Automatic);
swInProgress.Add(lvInprogress);
//scrolledWindow.SetSizeRequest(200, 200);
return swInProgress;
}
private Widget CreateFinishedListView()
{
finishedDownloadsStore = new ListStore(typeof(string), // file name
typeof(string), // date modified
typeof(string), // size
typeof(FinishedDownloadEntry) // download type
);
filter = new TreeModelFilter(finishedDownloadsStore, null);
filter.VisibleFunc = (model, iter) =>
{
var name = (string)model.GetValue(iter, 0);
return Helpers.IsOfCategoryOrMatchesKeyword(name, searchKeyword, category);
};
var sortedStore = new TreeModelSort(filter);
sortedStore.SetSortFunc(0, (model, iter1, iter2) =>
{
Console.WriteLine("called");
var t1 = (string)model.GetValue(iter1, 0);
var t2 = (string)model.GetValue(iter2, 0);
return t1.CompareTo(t2);
});
sortedStore.SetSortFunc(1, (model, iter1, iter2) =>
{
var t1 = (FinishedDownloadEntry)model.GetValue(iter1, 3);
var t2 = (FinishedDownloadEntry)model.GetValue(iter2, 3);
return t1.DateAdded.CompareTo(t2.DateAdded);
});
sortedStore.SetSortFunc(2, (model, iter1, iter2) =>
{
var t1 = (FinishedDownloadEntry)model.GetValue(iter1, 3);
var t2 = (FinishedDownloadEntry)model.GetValue(iter2, 3);
return t1.Size.CompareTo(t2.Size);
});
lvFinished = new TreeView(sortedStore);
lvFinished.Selection.Mode = SelectionMode.Multiple;
//File name column
var fileNameColumn = new TreeViewColumn
{
Resizable = true,
Reorderable = false,
Title = "Name",
Sizing = TreeViewColumnSizing.Fixed,
FixedWidth = 200
};
var fileIconRenderer = new CellRendererPixbuf { };
fileIconRenderer.SetPadding(5, 5);
fileNameColumn.PackStart(fileIconRenderer, false);
fileNameColumn.SetCellDataFunc(fileIconRenderer, new CellLayoutDataFunc(GetFileIcon));
fileNameColumn.SortColumnId = 0;
var fileNameRendererText = new CellRendererText();
fileNameColumn.PackStart(fileNameRendererText, false);
fileNameColumn.SetAttributes(fileNameRendererText, "text", 0);
lvFinished.AppendColumn(fileNameColumn);
//Last modified column
var lastModifiedRendererText = new CellRendererText();
var lastModifiedColumn = new TreeViewColumn("Date added", lastModifiedRendererText, "text", 1)
{
Resizable = true,
Reorderable = false,
Sizing = TreeViewColumnSizing.Fixed,
FixedWidth = 100
};
lastModifiedColumn.SetAttributes(lastModifiedRendererText, "text", 1);
lastModifiedColumn.SortColumnId = 1;
lvFinished.AppendColumn(lastModifiedColumn);
//File size column
var fileSizeRendererText = new CellRendererText();
//fileSizeRendererText.Xalign = 1.0f;
var fileSizeColumn = new TreeViewColumn
{
Resizable = true,
Reorderable = false,
Sizing = TreeViewColumnSizing.Fixed,
FixedWidth = 80,
Title = "Size",
};
fileSizeColumn.PackStart(fileSizeRendererText, false);
fileSizeColumn.SetAttributes(fileSizeRendererText, "text", 2);
fileSizeColumn.SortColumnId = 2;
lvFinished.AppendColumn(fileSizeColumn);
lvFinished.Selection.Changed += (_, _) =>
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
};
lvFinished.ButtonReleaseEvent += (a, b) =>
{
if (b.Event.Type == Gdk.EventType.ButtonRelease && b.Event.Button == 3)
{
FinishedContextMenuOpening?.Invoke(this, EventArgs.Empty);
menuFinished.PopupAtPointer(b.Event);
}
};
swFinished = new ScrolledWindow { OverlayScrolling = true, Margin = 5, MarginStart = 0, MarginTop = 0, ShadowType = ShadowType.In };
swFinished.SetPolicy(PolicyType.Automatic, PolicyType.Automatic);
swFinished.Add(lvFinished);
return swFinished;
}
void GetFileIcon(ICellLayout cell_layout,
CellRenderer cell, ITreeModel tree_model, TreeIter iter)
{
var name = (string)tree_model.GetValue(iter, 0);
var pix = LoadSvg(IconResource.GetSVGNameForFileType(name), 20);
((CellRendererPixbuf)cell).Pixbuf = pix;
}
private void AppWin1_DeleteEvent(object o, DeleteEventArgs args)
{
Application.Quit();
}
private static Gdk.Pixbuf LoadSvg(string name, int dimension = 16)
{
return new Gdk.Pixbuf(
IoPath.Combine(
AppDomain.CurrentDomain.BaseDirectory, "svg-icons", $"{name}.svg"), dimension, dimension, true);
}
public IInProgressDownloadRow? FindInProgressItem(string id)
{
if (!inprogressDownloadsStore!.GetIterFirst(out TreeIter iter))
{
return null;
}
do
{
var ent = (InProgressDownloadEntry)inprogressDownloadsStore.GetValue(iter, INPROGRESS_DATA_INDEX);
if (ent.Id == id)
{
return new InProgressEntryWrapper(ent, iter, inprogressDownloadsStore);
}
}
while (inprogressDownloadsStore.IterNext(ref iter));
return null;
}
public IFinishedDownloadRow? FindFinishedItem(string id)
{
if (!this.finishedDownloadsStore!.GetIterFirst(out TreeIter iter))
{
return null;
}
do
{
var ent = (FinishedDownloadEntry)finishedDownloadsStore.GetValue(iter, FINISHED_DATA_INDEX);
if (ent.Id == id)
{
return new FinishedEntryWrapper(ent, iter, finishedDownloadsStore);
}
}
while (finishedDownloadsStore.IterNext(ref iter));
return null;
}
public void AddToTop(InProgressDownloadEntry entry)
{
var iter = inprogressDownloadsStore.Insert(0);
inprogressDownloadsStore.SetValue(iter, 0, entry.Name);
inprogressDownloadsStore.SetValue(iter, 1, entry.DateAdded.ToShortDateString());
inprogressDownloadsStore.SetValue(iter, 2, Helpers.FormatSize(entry.Size));
inprogressDownloadsStore.SetValue(iter, 3, entry.Progress);
inprogressDownloadsStore.SetValue(iter, 4, entry.Status);
inprogressDownloadsStore.SetValue(iter, 5, entry);
}
public void AddToTop(FinishedDownloadEntry entry)
{
finishedDownloadsStore.AppendValues(
entry.Name,
entry.DateAdded.ToShortDateString(),
Helpers.FormatSize(entry.Size));
}
public void SwitchToInProgressView()
{
if (this.categoryTreeStore.GetIterFirst(out TreeIter iter))
{
this.categoryTree.Selection.SelectIter(iter);
}
}
public void ClearInProgressViewSelection()
{
this.lvInprogress.Selection.UnselectAll();
}
public void SwitchToFinishedView()
{
if (this.categoryTreeStore.GetIterFirst(out TreeIter iter) &&
this.categoryTreeStore.IterNext(ref iter))
{
this.categoryTree.Selection.SelectIter(iter);
}
}
public void ClearFinishedViewSelection()
{
this.lvFinished.Selection.UnselectAll();
}
public bool Confirm(object? window, string text)
{
if (window is not Window owner)
{
owner = this;
}
using var msg = new MessageDialog(owner, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, text);
if (msg.Run() == (int)ResponseType.Yes)
{
return true;
}
return false;
}
public IDownloadCompleteDialog CreateDownloadCompleteDialog(IApp app)
{
throw new NotImplementedException();
}
public INewDownloadDialogSkeleton CreateNewDownloadDialog(bool empty)
{
throw new NotImplementedException();
}
public INewVideoDownloadDialog CreateNewVideoDialog()
{
throw new NotImplementedException();
}
public IProgressWindow CreateProgressWindow(string downloadId, IApp app, IAppUI appUI)
{
throw new NotImplementedException();
}
public void RunOnUIThread(System.Action action)
{
Application.Invoke((a, b) => action.Invoke());
}
public void RunOnUIThread(Action<string, int, double, long> action, string id, int progress, double speed, long eta)
{
Application.Invoke((a, b) => action.Invoke(id, progress, speed, eta));
}
public void Delete(IInProgressDownloadRow row)
{
throw new NotImplementedException();
}
public void Delete(IFinishedDownloadRow row)
{
throw new NotImplementedException();
}
public void DeleteAllFinishedDownloads()
{
throw new NotImplementedException();
}
public void Delete(IEnumerable<IInProgressDownloadRow> rows)
{
throw new NotImplementedException();
}
public void Delete(IEnumerable<IFinishedDownloadRow> rows)
{
throw new NotImplementedException();
}
public string GetUrlFromClipboard()
{
throw new NotImplementedException();
}
public AuthenticationInfo? PromtForCredentials(string message)
{
throw new NotImplementedException();
}
public void ShowUpdateAvailableNotification()
{
//throw new NotImplementedException();
}
public void ShowMessageBox(object? window, string message)
{
if (window is not Window owner)
{
owner = this;
}
throw new NotImplementedException();
}
public void OpenNewDownloadMenu()
{
throw new NotImplementedException();
}
public string? SaveFileDialog(string? initialPath)
{
throw new NotImplementedException();
}
public void ShowRefreshLinkDialog(InProgressDownloadEntry entry, IApp app)
{
throw new NotImplementedException();
}
public void SetClipboardText(string text)
{
throw new NotImplementedException();
}
public void SetClipboardFile(string file)
{
throw new NotImplementedException();
}
public void ShowPropertiesDialog(BaseDownloadEntry ent, ShortState? state)
{
throw new NotImplementedException();
}
public void ShowYoutubeDLDialog(IAppUI appUI, IApp app)
{
throw new NotImplementedException();
}
public DownloadSchedule? ShowSchedulerDialog(DownloadSchedule schedule)
{
throw new NotImplementedException();
}
public void ShowBatchDownloadWindow(IApp app)
{
throw new NotImplementedException();
}
public void ShowSettingsDialog(IApp app, int page = 0)
{
throw new NotImplementedException();
}
public void ImportDownloads(IApp app)
{
throw new NotImplementedException();
}
public void ExportDownloads(IApp app)
{
throw new NotImplementedException();
}
public void UpdateBrowserMonitorButton()
{
throw new NotImplementedException();
}
public void ShowBrowserMonitoringDialog(IApp app)
{
throw new NotImplementedException();
}
public void UpdateParallalismLabel()
{
throw new NotImplementedException();
}
public IUpdaterUI CreateUpdateUIDialog(IAppUI ui)
{
throw new NotImplementedException();
}
public void ClearUpdateInformation()
{
throw new NotImplementedException();
}
private IEnumerable<FinishedDownloadEntry> GetAllFinishedDownloads()
{
if (!finishedDownloadsStore!.GetIterFirst(out TreeIter iter))
{
yield break;
}
yield return (FinishedDownloadEntry)finishedDownloadsStore.GetValue(iter, FINISHED_DATA_INDEX);
while (finishedDownloadsStore.IterNext(ref iter))
{
yield return (FinishedDownloadEntry)finishedDownloadsStore.GetValue(iter, FINISHED_DATA_INDEX);
}
}
private IEnumerable<InProgressDownloadEntry> GetAllInProgressDownloads()
{
if (!inprogressDownloadsStore!.GetIterFirst(out TreeIter iter))
{
yield break;
}
yield return (InProgressDownloadEntry)inprogressDownloadsStore.GetValue(iter, FINISHED_DATA_INDEX);
while (inprogressDownloadsStore.IterNext(ref iter))
{
yield return (InProgressDownloadEntry)inprogressDownloadsStore.GetValue(iter, FINISHED_DATA_INDEX);
}
}
private void SetFinishedDownloads(IEnumerable<FinishedDownloadEntry> finishedDownloads)
{
finishedDownloadsStore.Clear();
foreach (var item in finishedDownloads)
{
finishedDownloadsStore.AppendValues(item.Name,
item.DateAdded.ToShortDateString(),
Helpers.FormatSize(item.Size),
item);
}
}
private void SetInProgressDownloads(IEnumerable<InProgressDownloadEntry> incompleteDownloads)
{
inprogressDownloadsStore.Clear();
foreach (var item in incompleteDownloads)
{
inprogressDownloadsStore.AppendValues(item.Name,
item.DateAdded.ToShortDateString(),
Helpers.FormatSize(item.Size),
item.Progress,
Helpers.GenerateStatusText(item),
item);
}
}
private IList<IInProgressDownloadRow> GetSelectedInProgressDownloads()
{
var list = new List<IInProgressDownloadRow>(0);
var rows = lvInprogress.Selection.GetSelectedRows(out ITreeModel model);
if (rows != null && rows.Length > 0)
{
list.Capacity = rows.Length;
foreach (var row in rows)
{
if (model.GetIter(out TreeIter iter, row))
{
var ent = (InProgressDownloadEntry)model.GetValue(iter, INPROGRESS_DATA_INDEX);
list.Add(new InProgressEntryWrapper(ent, iter, model));
}
}
}
return list;
}
private IList<IFinishedDownloadRow> GetSelectedFinishedDownloads()
{
var list = new List<IFinishedDownloadRow>(0);
var rows = lvFinished.Selection.GetSelectedRows(out ITreeModel model);
if (rows != null && rows.Length > 0)
{
list.Capacity = rows.Length;
foreach (var row in rows)
{
if (model.GetIter(out TreeIter iter, row))
{
var ent = (FinishedDownloadEntry)model.GetValue(iter, FINISHED_DATA_INDEX);
list.Add(new FinishedEntryWrapper(ent, iter, model));
}
}
}
return list;
}
private int GetSelectedCategory()
{
var paths = categoryTree.Selection.GetSelectedRows();
if (paths != null && paths.Length > 0 && paths[0].Depth == 1)
{
return paths[0].Indices[0];
}
return -1;
}
public void ShowQueuesAndSchedulerWindow()
{
throw new NotImplementedException();
}
public IQueuesWindow CreateQueuesAndSchedulerWindow(IAppUI appUi, IEnumerable<DownloadQueue> queues)
{
throw new NotImplementedException();
}
public IQueueSelectionDialog CreateQueueSelectionDialog()
{
throw new NotImplementedException();
}
//private ref InProgressEntryWrapper? FindInProgressDownloadById()
//{
// if (!inprogressDownloadsStore!.GetIterFirst(out TreeIter iter))
// {
// return null;
// }
// var ent=(InProgressDownloadEntry)inprogressDownloadsStore.GetValue(iter, FINISHED_DATA_INDEX);
// while (inprogressDownloadsStore.IterNext(ref iter))
// {
// return (InProgressDownloadEntry)inprogressDownloadsStore.GetValue(iter, FINISHED_DATA_INDEX);
// }
//}
}
}

View File

@ -0,0 +1,37 @@
using Gtk;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XDM.Core.Lib.UI;
namespace XDM.GtkUI
{
internal class ButtonWrapper : IButton
{
private readonly ToolButton button;
public ButtonWrapper(ToolButton button)
{
this.button = button;
button.Clicked += (s, e) =>
{
this.Clicked?.Invoke(s, e);
};
}
public bool Visible { get => button.Visible; set => button.Visible = value; }
public bool Enable
{
get => button.Sensitive;
set
{
button.Sensitive = value;
}
}
public event EventHandler? Clicked;
}
}

View File

@ -0,0 +1,40 @@
using Gtk;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.UI;
using XDMApp;
namespace XDM.GtkUI
{
internal class FinishedEntryWrapper : IFinishedDownloadRow
{
private FinishedDownloadEntry entry;
private TreeIter treeIter;
private ITreeModel store;
public FinishedEntryWrapper(FinishedDownloadEntry entry, TreeIter treeIter, ITreeModel store)
{
this.entry = entry;
this.treeIter = treeIter;
this.store = store;
}
public string FileIconText => IconResource.GetSVGNameForFileType(DownloadEntry.Name);
public string Name => entry.Name;
public long Size => entry.Size;
public DateTime DateAdded => entry.DateAdded;
public FinishedDownloadEntry DownloadEntry => entry;
internal TreeIter TreeIter => treeIter;
internal ITreeModel Store => store;
}
}

View File

@ -0,0 +1,102 @@
using Gtk;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XDM.Core.Lib.Common;
using XDM.Core.Lib.UI;
using XDM.Core.Lib.Util;
using XDMApp;
namespace XDM.GtkUI
{
internal class InProgressEntryWrapper : IInProgressDownloadRow
{
private TreeIter treeIter;
private ITreeModel store;
public InProgressEntryWrapper(InProgressDownloadEntry downloadEntry,
TreeIter treeIter,
ITreeModel store)
{
this.DownloadEntry = downloadEntry;
this.treeIter = treeIter;
this.store = store;
}
public InProgressDownloadEntry DownloadEntry { get; }
public string FileIconText => IconResource.GetSVGNameForFileType(DownloadEntry.Name);
public string Name
{
get => DownloadEntry.Name;
set
{
this.DownloadEntry.Name = value;
store.SetValue(treeIter, 0, value);
}
}
public long Size
{
get => DownloadEntry.Size;
set
{
this.DownloadEntry.Size = value;
store.SetValue(treeIter, 2, Helpers.FormatSize( value));
}
}
public DateTime DateAdded
{
get => DownloadEntry.DateAdded;
set
{
this.DownloadEntry.DateAdded = value;
store.SetValue(treeIter, 1, value.ToShortDateString());
}
}
public int Progress
{
get => DownloadEntry.Progress;
set
{
this.DownloadEntry.Progress = value;
store.SetValue(treeIter, 3, value);
}
}
public DownloadStatus Status
{
get => this.DownloadEntry.Status;
set
{
this.DownloadEntry.Status = value;
store.SetValue(treeIter, 4, Helpers.GenerateStatusText(this.DownloadEntry));
}
}
public string DownloadSpeed
{
get => DownloadEntry.DownloadSpeed ?? string.Empty;
set
{
this.DownloadEntry.DownloadSpeed = value;
store.SetValue(treeIter, 4, Helpers.GenerateStatusText(this.DownloadEntry));
}
}
public string ETA
{
get => DownloadEntry.ETA ?? string.Empty;
set
{
this.DownloadEntry.ETA = value;
store.SetValue(treeIter, 4, Helpers.GenerateStatusText(this.DownloadEntry));
}
}
}
}

View File

@ -0,0 +1,32 @@
using Gtk;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XDM.Core.Lib.UI;
namespace XDM.GtkUI
{
internal class MenuItemWrapper : IMenuItem
{
private MenuItem menuItem;
private string name;
public string Name => name;
public MenuItem MenuItem => menuItem;
public bool Enabled
{
get => menuItem.IsSensitive;
set => menuItem.Sensitive = value;
}
public event EventHandler? Clicked;
public MenuItemWrapper(string name, MenuItem menuItem)
{
this.name = name;
this.menuItem = menuItem;
this.menuItem.ShowAll();
menuItem.Activated += (s, e) => Clicked?.Invoke(s, e);
}
}
}

View File

@ -0,0 +1,80 @@
using System;
using Gtk;
using Translations;
using XDM.Core.Lib.Common;
using XDMApp;
namespace XDM.GtkUI
{
class Program
{
static void Main(string[] args)
{
Application.Init();
//var screen = Gdk.Screen.Default;
//var provider = new CssProvider();
//provider.LoadFromData(@".dark
// {
// color: gray;
// background: rgb(36,41,46);
// }
// treeview.view :selected
// {
// background-color: rgb(10,106,182);
// color: white;
// }
//.listt
//{
//font-family: Segoe UI;
//}
// .dark2
// {
// color: gray;
// background: rgb(35,35,35);
// /*background: rgb(36,41,46);*/
// }
// .toolbar-border-dark
// {
// border-bottom: 1px solid rgb(20,20,20);
// }
// .toolbar-border-light
// {
// border-bottom: 2px solid rgb(240,240,240);
// }
// ");
//Gtk.StyleContext.AddProviderForScreen(screen, provider, 800);
var app = new XDMApp.XDMApp();
TextResource.Load(Config.Instance.Language);
var appWin = new AppWinPeer();
app.AppUI = new XDMApp.AppWin(appWin, app);
appWin.ShowAll();
Application.Run();
//var app = new XDMApp.XDMApp();
//var appWin = new AppWinPeer(app);
//appWin.ShowAll();
//Application.Run();
// Environment.SetEnvironmentVariable("PANGOCAIRO_BACKEND", "fc", EnvironmentVariableTarget.User);
// //Console.WriteLine(Environment.GetEnvironmentVariable("PANGOCAIRO_BACKEND"));
// //var arr = new string[] { "PANGOCAIRO_BACKEND=fc" };
// Application.Init();// "app", ref arr);
// Gtk.Settings.Default.ThemeName = "Adwaita";
// Gtk.Settings.Default.ApplicationPreferDarkTheme = true;
// App app = new App();
// var appWin = new AppWin();
// Console.WriteLine("Starting show all");
// appWin.Show();
// Console.WriteLine("Finished show all");
// Application.Run();
}
}
}

View File

@ -0,0 +1,109 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<InvariantGlobalization>true</InvariantGlobalization>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>Link</TrimMode>
<RootNamespace>XDM.GtkUI</RootNamespace>
<!--RuntimeIdentifier>linux-x64</RuntimeIdentifier-->
<!--RuntimeIdentifier>win-x64</RuntimeIdentifier-->
<!--RuntimeIdentifier>win-x86</RuntimeIdentifier-->
<!--<Platforms>AnyCPU;x86</Platforms>-->
<Platforms>AnyCPU;x86</Platforms>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GtkSharp" Version="3.24.24.34" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
</ItemGroup>
<ItemGroup>
<Content Include="images\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="glade\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="svg-icons\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<LangFiles Include="$(SolutionDir)\Lang\**" />
</ItemGroup>
<Target Name="Copy" AfterTargets="Build">
<Copy SourceFiles="@(LangFiles)" DestinationFolder="$(OutDir)\Lang" />
</Target>
<ItemGroup>
<None Remove="images\add-line.svg" />
<None Remove="images\apps-line.svg" />
<None Remove="images\check-line.svg" />
<None Remove="images\delete-bin-line.svg" />
<None Remove="images\download-line.svg" />
<None Remove="images\file-4-line.svg" />
<None Remove="images\file-chart-2-line.svg" />
<None Remove="images\file-download-line.svg" />
<None Remove="images\file-line.svg" />
<None Remove="images\file-text-line.svg" />
<None Remove="images\file-zip-line.svg" />
<None Remove="images\film-line.svg" />
<None Remove="images\folder-download-line.svg" />
<None Remove="images\image-line.svg" />
<None Remove="images\menu-line.svg" />
<None Remove="images\music-2-line.svg" />
<None Remove="images\pause-line.svg" />
<None Remove="images\play-line.svg" />
<None Remove="images\search-line.svg" />
<None Remove="images\task-line.svg" />
<None Remove="images\time-line.svg" />
<None Remove="images\video-download-line.svg" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Translations\Translations.csproj" />
<ProjectReference Include="..\XDMApp\XDMApp.csproj" />
<ProjectReference Include="..\XDM_CoreFx\XDM.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="AtkSharp">
<HintPath>D:\gtksharp\GtkSharp-master\BuildOutput\Release\AtkSharp.dll</HintPath>
</Reference>
<Reference Include="CairoSharp">
<HintPath>D:\gtksharp\GtkSharp-master\BuildOutput\Release\CairoSharp.dll</HintPath>
</Reference>
<Reference Include="GdkSharp">
<HintPath>D:\gtksharp\GtkSharp-master\BuildOutput\Release\GdkSharp.dll</HintPath>
</Reference>
<Reference Include="GioSharp">
<HintPath>D:\gtksharp\GtkSharp-master\BuildOutput\Release\GioSharp.dll</HintPath>
</Reference>
<Reference Include="GLibSharp">
<HintPath>D:\gtksharp\GtkSharp-master\BuildOutput\Release\GLibSharp.dll</HintPath>
</Reference>
<Reference Include="GtkSharp">
<HintPath>D:\gtksharp\GtkSharp-master\BuildOutput\Release\GtkSharp.dll</HintPath>
</Reference>
<Reference Include="GtkSourceSharp">
<HintPath>D:\gtksharp\GtkSharp-master\BuildOutput\Release\GtkSourceSharp.dll</HintPath>
</Reference>
<Reference Include="PangoSharp">
<HintPath>D:\gtksharp\GtkSharp-master\BuildOutput\Release\PangoSharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Folder Include="svg-icons\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,318 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<object class="GtkMenu">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkMenuItem" id="download-later">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Download Later</property>
<property name="use-underline">True</property>
</object>
</child>
</object>
<object class="GtkPopover" id="popover1">
<property name="can-focus">False</property>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Do not capture download from this address </property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="relief">none</property>
</object>
</child>
</object>
<object class="GtkPopover" id="popover2">
<property name="can-focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Add to queue</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkWindow" id="new-download-dialog">
<property name="can-focus">False</property>
<property name="window-position">center</property>
<property name="skip-taskbar-hint">True</property>
<child>
<!-- n-columns=6 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">20</property>
<property name="margin-bottom">30</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="row-spacing">10</property>
<property name="column-spacing">10</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="margin-start">20</property>
<property name="margin-end">10</property>
<property name="label" translatable="yes">Address</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="margin-start">20</property>
<property name="margin-end">10</property>
<property name="label" translatable="yes">File</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="txt-url">
<property name="width-request">300</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="editable">False</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
<property name="width">4</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkEntry" id="txt-file">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="editable">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btn-browse">
<property name="label" translatable="yes">...</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
<property name="width">4</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="img-file-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stock">gtk-file</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="lbl-file-size">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">label</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">5</property>
<property name="top-attach">0</property>
<property name="height">2</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">New Download</property>
<property name="has-subtitle">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButton" id="btn-download">
<property name="label" translatable="yes">Download</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="has-default">True</property>
<property name="receives-default">True</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="popover">popover2</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButton" id="btn-cancel">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="popover">popover1</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkListStore" id="queue_list">
<columns>
<!-- column-name gchararray1 -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Default Queue</col>
</row>
</data>
</object>
</interface>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkWindow" id="url-capture">
<property name="can_focus">False</property>
<property name="window_position">center</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">10</property>
<property name="margin_end">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="hexpand">False</property>
<property name="label" translatable="yes">Address</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="url-text">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">New Download</property>
<child>
<object class="GtkButton" id="cancel-btn">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="download-btn">
<property name="label" translatable="yes">Download</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<object class="GtkListStore" id="liststore1">
<columns>
<!-- column-name gchararray1 -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Audio+Video</col>
</row>
<row>
<col id="0" translatable="yes">Video only</col>
</row>
<row>
<col id="0" translatable="yes">Audio only</col>
</row>
</data>
</object>
<object class="GtkWindow" id="vid-win">
<property name="can_focus">False</property>
<property name="window_position">center-always</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">10</property>
<property name="margin_end">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="row_spacing">10</property>
<property name="column_spacing">10</property>
<child>
<object class="GtkComboBox" id="cmb-output-format">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">liststore1</property>
<child>
<object class="GtkCellRendererText"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkEntry" id="txt-file">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="editable">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btn-browse">
<property name="label" translatable="yes">...</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="margin_left">20</property>
<property name="margin_right">10</property>
<property name="margin_start">20</property>
<property name="margin_end">10</property>
<property name="label" translatable="yes">File</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="margin_left">20</property>
<property name="margin_right">10</property>
<property name="margin_start">20</property>
<property name="margin_end">10</property>
<property name="label" translatable="yes">Output</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkImage" id="imgbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">10</property>
<property name="margin_end">10</property>
<property name="stock">gtk-missing-image</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
<property name="height">2</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Download Video</property>
<property name="has_subtitle">False</property>
<child>
<object class="GtkButton" id="btn-cancel">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="btn-download">
<property name="label" translatable="yes">Download</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 198 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M6.75 2.5A4.25 4.25 0 0 1 11 6.75V11H6.75a4.25 4.25 0 1 1 0-8.5zM9 9V6.75A2.25 2.25 0 1 0 6.75 9H9zm-2.25 4H11v4.25A4.25 4.25 0 1 1 6.75 13zm0 2A2.25 2.25 0 1 0 9 17.25V15H6.75zm10.5-12.5a4.25 4.25 0 1 1 0 8.5H13V6.75a4.25 4.25 0 0 1 4.25-4.25zm0 6.5A2.25 2.25 0 1 0 15 6.75V9h2.25zM13 13h4.25A4.25 4.25 0 1 1 13 17.25V13zm2 2v2.25A2.25 2.25 0 1 0 17.25 15H15z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v3zm1 2H6v12h12V8zm-9 3h2v6H9v-6zm4 0h2v6h-2v-6zM9 4v2h6V4H9z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 19h18v2H3v-2zm10-5.828L19.071 7.1l1.414 1.414L12 17 3.515 8.515 4.929 7.1 11 13.17V2h2v11.172z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M21 16l-6.003 6H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v13zm-2-1V4H5v16h9v-5h5z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M15 4H5v16h14V8h-4V4zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992zM11 11V8h2v3h3v2h-3v3h-2v-3H8v-2h3z" fill="rgba(149,164,166,1)"/></svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M15 4H5v16h14V8h-4V4zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992zM12 8v4h4a4 4 0 1 1-4-4z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M13 12h3l-4 4-4-4h3V8h2v4zm2-8H5v16h14V8h-4V4zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M9 2.003V2h10.998C20.55 2 21 2.455 21 2.992v18.016a.993.993 0 0 1-.993.992H3.993A1 1 0 0 1 3 20.993V8l6-5.997zM5.83 8H9V4.83L5.83 8zM11 4v5a1 1 0 0 1-1 1H5v10h14V4h-8z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M21 8v12.993A1 1 0 0 1 20.007 22H3.993A.993.993 0 0 1 3 21.008V2.992C3 2.455 3.449 2 4.002 2h10.995L21 8zm-2 1h-5V4H5v16h14V9zM8 7h3v2H8V7zm0 4h8v2H8v-2zm0 4h8v2H8v-2z" fill="rgba(150,150,150,1)"/></svg>

After

Width:  |  Height:  |  Size: 332 B

Some files were not shown because too many files have changed in this diff Show More