Merged c# repo to xdm repo
|
@ -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/
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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"};
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal class IsExternalInit { }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
// }
|
||||
//}
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = '\\';
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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('"', '\'', ' '));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
//}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
</configuration>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -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 = '\\';
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |