commit
be9f27f9bd
|
@ -0,0 +1,129 @@
|
|||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# This workflow will build, test, sign and package a WPF or Windows Forms desktop application
|
||||
# built on .NET Core.
|
||||
# To learn how to migrate your existing application to .NET Core,
|
||||
# refer to https://docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework
|
||||
#
|
||||
# To configure this workflow:
|
||||
#
|
||||
# 1. Configure environment variables
|
||||
# GitHub sets default environment variables for every workflow run.
|
||||
# Replace the variables relative to your project in the "env" section below.
|
||||
#
|
||||
# 2. Signing
|
||||
# Generate a signing certificate in the Windows Application
|
||||
# Packaging Project or add an existing signing certificate to the project.
|
||||
# Next, use PowerShell to encode the .pfx file using Base64 encoding
|
||||
# by running the following Powershell script to generate the output string:
|
||||
#
|
||||
# $pfx_cert = Get-Content '.\SigningCertificate.pfx' -Encoding Byte
|
||||
# [System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt'
|
||||
#
|
||||
# Open the output file, SigningCertificate_Encoded.txt, and copy the
|
||||
# string inside. Then, add the string to the repo as a GitHub secret
|
||||
# and name it "Base64_Encoded_Pfx."
|
||||
# For more information on how to configure your signing certificate for
|
||||
# this workflow, refer to https://github.com/microsoft/github-actions-for-desktop-apps#signing
|
||||
#
|
||||
# Finally, add the signing certificate password to the repo as a secret and name it "Pfx_Key".
|
||||
# See "Build the Windows Application Packaging project" below to see how the secret is used.
|
||||
#
|
||||
# For more information on GitHub Actions, refer to https://github.com/features/actions
|
||||
# For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications,
|
||||
# refer to https://github.com/microsoft/github-actions-for-desktop-apps
|
||||
|
||||
name: .NET Core Desktop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "migrate-to-c#" ]
|
||||
pull_request:
|
||||
branches: [ "migrate-to-c#" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
#strategy:
|
||||
# matrix:
|
||||
# configuration: [Debug, Release]
|
||||
|
||||
runs-on: windows-latest # For a list of available runner types, refer to
|
||||
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
|
||||
|
||||
env:
|
||||
Solution_Name: app\XDM\XDM_CoreFx.sln # Replace with your solution name, i.e. MyWpfApp.sln.
|
||||
Test_Project_Path: app\XDM\XDM_Tests\XDM.SystemTests.csproj # Replace with the path to your test project, i.e. MyWpfApp.Tests\MyWpfApp.Tests.csproj.
|
||||
Wap_Project_Directory: app\XDM\XDM.WinForm.UI # Replace with the Wap project directory relative to the solution, i.e. MyWpfApp.Package.
|
||||
Wap_Project_Path: app\XDM\XDM.WinForm.UI\XDM.WinForm.UI.csproj # Replace with the path to your Wap project, i.e. MyWpf.App.Package\MyWpfApp.Package.wapproj.
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v1.0.2
|
||||
|
||||
#- name: Build Winforms project with Debug configuration
|
||||
# run: msbuild /t:build /p:Configuration=Debug /p:TargetFramework=net4.7.2 app\XDM\XDM.WinForm.UI\XDM.WinForm.UI.csproj
|
||||
|
||||
# Install the .NET Core workload
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 5.0.x
|
||||
|
||||
# Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild
|
||||
#- name: Setup MSBuild.exe
|
||||
# uses: microsoft/setup-msbuild@v1.0.2
|
||||
|
||||
# Execute all unit tests in the solution
|
||||
#- name: Execute unit tests
|
||||
# run: dotnet test $env:Test_Project_Path
|
||||
|
||||
# Execute all unit tests in the solution
|
||||
- name: Build Winforms project with Debug configuration
|
||||
run: dotnet build -f net4.7.2 -c Debug $env:Wap_Project_Path
|
||||
|
||||
- name: Build Winforms project with Release configuration
|
||||
run: dotnet build -f net4.7.2 -c Release $env:Wap_Project_Path
|
||||
|
||||
# Restore the application to populate the obj folder with RuntimeIdentifiers
|
||||
#- name: Restore the application
|
||||
# run: msbuild $env:Solution_Name /t:Restore /p:Configuration=$env:Configuration
|
||||
# env:
|
||||
# Configuration: ${{ matrix.configuration }}
|
||||
|
||||
# Decode the base 64 encoded pfx and save the Signing_Certificate
|
||||
#- name: Decode the pfx
|
||||
# run: |
|
||||
# $pfx_cert_byte = [System.Convert]::FromBase64String("${{ secrets.Base64_Encoded_Pfx }}")
|
||||
# $certificatePath = Join-Path -Path $env:Wap_Project_Directory -ChildPath GitHubActionsWorkflow.pfx
|
||||
# [IO.File]::WriteAllBytes("$certificatePath", $pfx_cert_byte)
|
||||
|
||||
# Create the app package by building and packaging the Windows Application Packaging project
|
||||
#- name: Create the app package
|
||||
# run: msbuild $env:Wap_Project_Path /p:Configuration=$env:Configuration /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle /p:PackageCertificateKeyFile=GitHubActionsWorkflow.pfx /p:PackageCertificatePassword=${{ secrets.Pfx_Key }}
|
||||
# env:
|
||||
# Appx_Bundle: Always
|
||||
# Appx_Bundle_Platforms: x86|x64
|
||||
# Appx_Package_Build_Mode: StoreUpload
|
||||
# Configuration: ${{ matrix.configuration }}
|
||||
|
||||
# Remove the pfx
|
||||
#- name: Remove the pfx
|
||||
# run: Remove-Item -path $env:Wap_Project_Directory\$env:Signing_Certificate
|
||||
|
||||
# Upload the MSIX package: https://github.com/marketplace/actions/upload-a-build-artifact
|
||||
#- name: Upload build artifacts
|
||||
# uses: actions/upload-artifact@v2
|
||||
# with:
|
||||
# name: MSIX Package
|
||||
# path: ${{ env.Wap_Project_Directory }}\AppPackages
|
|
@ -1,5 +1,358 @@
|
|||
|
||||
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/
|
||||
app/XDM/XDM.Win.Installer/ffmpeg-x86.exe
|
||||
*.wixobj
|
||||
app/XDM/XDM.Win.Installer/net4.7.2.wxs
|
||||
app/XDM/XDM.Win.Installer/net4.7.2/
|
||||
app/XDM/XDM.Win.Installer/*.msi
|
||||
*.wixpdb
|
||||
app/XDM/XDM.Win.Installer/*.exe
|
||||
app/xdm-browser-monitor/extension-code.js
|
||||
|
|
|
@ -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,4 @@
|
|||
[*.cs]
|
||||
|
||||
# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
dotnet_diagnostic.CS8618.severity = silent
|
|
@ -0,0 +1,2 @@
|
|||
Clone https://github.com/m-ab-s/media-autobuild_suite
|
||||
Put ffmpeg_options.txt inside build folder
|
|
@ -0,0 +1,16 @@
|
|||
# Lines starting with this character are ignored
|
||||
# To override some options specifically for the shared build, create a ffmpeg_options_shared.txt file.
|
||||
|
||||
# Basic built-in options, can be removed if you delete "--disable-autodetect"
|
||||
--disable-everything
|
||||
--disable-network
|
||||
--disable-autodetect
|
||||
--enable-small
|
||||
--enable-demuxer=mov,mp4,m4a,3gp,3g2,mj2,matroska,webm,mpegts
|
||||
--enable-libmp3lame
|
||||
--enable-encoder=libmp3lame
|
||||
--enable-muxer=mp4,matroska,mpegts,mp3
|
||||
--enable-protocol=file,srt,concat
|
||||
--enable-bsfs
|
||||
--enable-filter=acopy,concat,copy
|
||||
--enable-decoder=vorbis,opus,aac,ac3
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=غير مكتمل
|
||||
ALL_FINISHED=اكتمال
|
||||
CAT_DOCUMENTS=مستندات
|
||||
CAT_COMPRESSED=مضغوط
|
||||
CAT_MUSIC=موسيقى
|
||||
CAT_VIDEOS=فيديو
|
||||
CAT_PROGRAMS=برامج
|
||||
MENU_BATCH_DOWNLOAD=باتش التحميل
|
||||
MENU_DELETE_DWN=حذف التنزيلات
|
||||
MENU_DELETE_COMPLETED=تم الانتهاء من الحذف
|
||||
MENU_IMPORT=استيراد
|
||||
MENU_EXPORT=تصدير
|
||||
MENU_EXIT=خروج
|
||||
MENU_PAUSE=توقف
|
||||
MENU_RESUME=استئناف
|
||||
MENU_RESTART=أعاد التشغيل
|
||||
MENU_REFRESH_LINK=تحديث الرابط
|
||||
MENU_PROPERTIES=خصائص
|
||||
MENU_UPDATE=البحث عن تحديث
|
||||
MENU_ABOUT=XDM...حول
|
||||
SORT_DATE=تاريخ
|
||||
SORT_SIZE=حجم
|
||||
SORT_NAME=اسم
|
||||
ND_CANCEL=إلغاء
|
||||
MSG_OK=اوك
|
||||
LBL_MENU=القائمة
|
||||
TITLE_SETTINGS=اعدادات
|
||||
SETTINGS_MONITORING=رصد المتصفح
|
||||
DESC_NEW=جديد
|
||||
DESC_DEL=حذف
|
||||
DESC_Q_TITLE=قائمة الانتظار وجدولة
|
||||
Q_SCHEDULE_TXT=جدولة
|
||||
Q_MOVE_TO=حرك الى
|
||||
CTX_OPEN_FILE=فتح
|
||||
CTX_OPEN_FOLDER=فتح مجلد
|
||||
CTX_SAVE_AS=حفظ باسم
|
||||
CTX_COPY_URL=إنسخ الرابط
|
||||
CTX_COPY_FILE=نسخ الملف
|
||||
MENU_IMPORT=استيراد
|
||||
MENU_EXPORT=تصدير
|
||||
MENU_LANG=لغة
|
||||
MSG_LANG1=اختار اللغة
|
||||
MSG_LANG2=XDM يرجى ملاحظة أن التغييرات ستدخل حيز التنفيذ في المرة القادمة التي تبدأ فيها
|
||||
LBL_REPORT_PROBLEM=الإبلاغ عن خطأ
|
||||
LBL_SUPPORT_PAGE=صفحة الدعم
|
||||
LBL_SHOW_PROGRESS=عرض التقدم
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=正在進行
|
||||
ALL_FINISHED=已完成
|
||||
CAT_DOCUMENTS=檔案
|
||||
CAT_COMPRESSED=壓縮檔案
|
||||
CAT_MUSIC=音樂
|
||||
CAT_VIDEOS=影片
|
||||
CAT_PROGRAMS=程式
|
||||
MENU_BATCH_DOWNLOAD=批量下載
|
||||
MENU_DELETE_DWN=刪除此下載
|
||||
MENU_DELETE_COMPLETED=清除已完成
|
||||
MENU_IMPORT=匯入
|
||||
MENU_EXPORT=匯出
|
||||
MENU_EXIT=結束
|
||||
MENU_PAUSE=暫停
|
||||
MENU_RESUME=繼續
|
||||
MENU_RESTART=重新開始
|
||||
MENU_REFRESH_LINK=重新整理連結
|
||||
MENU_PROPERTIES=屬性
|
||||
MENU_UPDATE=檢查更新
|
||||
MENU_ABOUT=關於 XDM...
|
||||
SORT_DATE=按時間
|
||||
SORT_SIZE=按大小
|
||||
SORT_NAME=按名稱
|
||||
ND_CANCEL=取消
|
||||
MSG_OK=確定
|
||||
LBL_MENU=選單
|
||||
TITLE_SETTINGS=設定
|
||||
SETTINGS_MONITORING=瀏覽器監視延伸模組
|
||||
DESC_NEW=新增
|
||||
DESC_DEL=刪除
|
||||
DESC_Q_TITLE=佇列和下載安排
|
||||
Q_SCHEDULE_TXT=調度安排
|
||||
Q_MOVE_TO=移動到
|
||||
CTX_OPEN_FILE=打開
|
||||
CTX_OPEN_FOLDER=打開目錄
|
||||
CTX_SAVE_AS=另存為
|
||||
CTX_COPY_URL=複製URL
|
||||
CTX_COPY_FILE=複製檔案
|
||||
MENU_IMPORT=匯入
|
||||
MENU_EXPORT=匯出
|
||||
MENU_LANG=語言
|
||||
MSG_LANG1=選擇語言
|
||||
MSG_LANG2=請註意,變更將在下次啟動XDM時生效
|
||||
LBL_REPORT_PROBLEM=報告一個bug
|
||||
LBL_SUPPORT_PAGE=支援頁面
|
||||
LBL_SHOW_PROGRESS=顯示進度
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=未完成
|
||||
ALL_FINISHED=已完成
|
||||
CAT_DOCUMENTS=文档
|
||||
CAT_COMPRESSED=压缩
|
||||
CAT_MUSIC=音乐
|
||||
CAT_VIDEOS=视频
|
||||
CAT_PROGRAMS=程序
|
||||
MENU_BATCH_DOWNLOAD=批量下载
|
||||
MENU_DELETE_DWN=删除下载
|
||||
MENU_DELETE_COMPLETED=清除已完成
|
||||
MENU_IMPORT=导入
|
||||
MENU_EXPORT=导出
|
||||
MENU_EXIT=退出
|
||||
MENU_PAUSE=暂停
|
||||
MENU_RESUME=恢复
|
||||
MENU_RESTART=重新下载
|
||||
MENU_REFRESH_LINK=刷新链接
|
||||
MENU_PROPERTIES=属性
|
||||
MENU_UPDATE=检查更新
|
||||
MENU_ABOUT=关于XDM...
|
||||
SORT_DATE=时间
|
||||
SORT_SIZE=大小
|
||||
SORT_NAME=名称
|
||||
ND_CANCEL=取消
|
||||
MSG_OK=确定
|
||||
LBL_MENU=菜单
|
||||
TITLE_SETTINGS=设置
|
||||
SETTINGS_MONITORING=浏览器监视
|
||||
DESC_NEW=新建
|
||||
DESC_DEL=删除
|
||||
DESC_Q_TITLE=队列和计划
|
||||
Q_SCHEDULE_TXT=计划
|
||||
Q_MOVE_TO=移至
|
||||
CTX_OPEN_FILE=打开
|
||||
CTX_OPEN_FOLDER=打开目录
|
||||
CTX_SAVE_AS=保存为
|
||||
CTX_COPY_URL=复制URL
|
||||
CTX_COPY_FILE=复制文件
|
||||
MENU_IMPORT=导入
|
||||
MENU_EXPORT=导出
|
||||
MENU_LANG=语言
|
||||
MSG_LANG1=选择语言
|
||||
MSG_LANG2=在下次启动XDM时才会生效
|
||||
LBL_REPORT_PROBLEM=报告错误
|
||||
LBL_SUPPORT_PAGE=支持页
|
||||
LBL_SHOW_PROGRESS=显示进度
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Nedokonèeno
|
||||
ALL_FINISHED=Dokonèeno
|
||||
CAT_DOCUMENTS=Dokumenty
|
||||
CAT_COMPRESSED=Archivy
|
||||
CAT_MUSIC=Hudba
|
||||
CAT_VIDEOS=Video
|
||||
CAT_PROGRAMS=Programy
|
||||
MENU_BATCH_DOWNLOAD=Dávkové stahování
|
||||
MENU_DELETE_DWN=Smazat stahování
|
||||
MENU_DELETE_COMPLETED=Vymazat dokonèené
|
||||
MENU_IMPORT=Importovat
|
||||
MENU_EXPORT=Exportovat
|
||||
MENU_EXIT=Konec
|
||||
MENU_PAUSE=Pauza
|
||||
MENU_RESUME=Pokraèovat
|
||||
MENU_RESTART=Restartovat
|
||||
MENU_REFRESH_LINK=Obnovit odkaz
|
||||
MENU_PROPERTIES=Vlastnosti
|
||||
MENU_UPDATE=Zkontrolovat aktualizace
|
||||
MENU_ABOUT=O programu XDM...
|
||||
SORT_DATE=Datum
|
||||
SORT_SIZE=Velikost
|
||||
SORT_NAME=Název
|
||||
ND_CANCEL=ZRUŠIT
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Menu
|
||||
TITLE_SETTINGS=Nastavení
|
||||
SETTINGS_MONITORING=Integrace v prohlížeèi
|
||||
DESC_NEW=Nový
|
||||
DESC_DEL=Smazat
|
||||
DESC_Q_TITLE=Fronta a plánovaè
|
||||
Q_SCHEDULE_TXT=Plánovaè
|
||||
Q_MOVE_TO=Pøesunout do
|
||||
CTX_OPEN_FILE=Otevøít
|
||||
CTX_OPEN_FOLDER=Otevøít složku
|
||||
CTX_SAVE_AS=Uložit jako
|
||||
CTX_COPY_URL=Kopírovat URL
|
||||
CTX_COPY_FILE=Kopírovat soubor
|
||||
MENU_IMPORT=Importovat
|
||||
MENU_EXPORT=Exportovat
|
||||
MENU_LANG=Jazyk
|
||||
MSG_LANG1=Zvolte jazyk
|
||||
MSG_LANG2=Zmìny vejdou v platnost po restartování programu XDM
|
||||
LBL_REPORT_PROBLEM=Nahlásit chybu
|
||||
LBL_SUPPORT_PAGE=Stránka podpory
|
||||
LBL_SHOW_PROGRESS=Zobrazit prùbìh
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,192 @@
|
|||
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
|
||||
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
|
||||
DESC_Q_TITLE=*Queue and scheduler
|
||||
CTX_SAVE_AS=*Save As
|
||||
CTX_COPY_URL=*Copy URL
|
||||
CTX_COPY_FILE=*Copy File
|
||||
MENU_REFRESH_LINK=*Refresh link
|
||||
LBL_SHOW_PROGRESS=*Show progress
|
||||
MENU_PROPERTIES=*Properties
|
||||
MENU_RESTART=*Restart
|
||||
Q_SCHEDULE_TXT=*Scheduler
|
||||
Q_MOVE_TO=*Move to Queue
|
||||
SORT_NAME=*Name
|
||||
SORT_DATE=*Date
|
||||
SORT_SIZE=*Size
|
||||
SORT_STATUS=*Status
|
||||
MENU_LANG=*Language
|
||||
MSG_LANG1=*Select language
|
||||
MSG_LANG2=*Please note changes will take effect next time you start XDM
|
||||
MSG_OK=*OK
|
||||
ND_CANCEL=*Cancel
|
||||
STAT_DOWNLOADING=*Downloading
|
||||
STAT_STOPPED=*Stopped
|
||||
STAT_FINISHED=*Finished
|
||||
ND_ADDRESS=*Address
|
||||
ND_FILE=*File
|
||||
LBL_SAVE_IN=*Save in
|
||||
ND_IGNORE_URL=*Do not capture download from this address
|
||||
ND_MORE=*More...
|
||||
ND_DOWNLOAD_LATER=*Download Later
|
||||
ND_DOWNLOAD_NOW=*Download Now
|
||||
ND_AUTO_CAT=*Automatically select based on file type
|
||||
BTN_BROWSE=*Browse...
|
||||
ND_TITLE=*New Download
|
||||
DESC_ADV_TITLE=*Advanced settings
|
||||
ND_AUTH=*Authentication
|
||||
DESC_NET4=*Proxy settings
|
||||
SPEED_LIMIT_TITLE=*Speed limit
|
||||
DESC_USER=*User Name
|
||||
DESC_PASS=*Password
|
||||
ND_AUTH_REMEMBER=*Remember authentication for this website
|
||||
LBL_NET_OPT_DEF=*System default
|
||||
ND_NO_PROXY=*No Proxy
|
||||
ND_MANUAL_PROXY=*Manual Proxy
|
||||
PROXY_HOST=*Proxy Host
|
||||
PROXY_PORT=*Proxy Port
|
||||
DESC_NET7=*Proxy username
|
||||
DESC_NET8=*Proxy password
|
||||
ND_SYSTEM_PROXY=*Open system proxy settings
|
||||
MENU_SPEED_LIMITER=*Speed limiter
|
||||
MSG_SPEED_LIMIT=*Download speed limit [KB/Sec ](0 unlimited)
|
||||
MSG_INVALID_PORT=*Invalid port
|
||||
MSG_INVALID_URL=*Download address is invalid or unsupported
|
||||
MSG_NO_FILE=*Please enter file name
|
||||
LBL_NEW_QUEUE=*Name
|
||||
MSG_DOWNLOAD_FFMPEG=*Download FFmpeg?
|
||||
LBL_QUEUE_OPT3=*Do not use queue
|
||||
VID_PASTE_URL=*Please paste video URL link here
|
||||
SETTINGS_ADV=*Advanced settings
|
||||
VID_CHK=*Check/Uncheck All
|
||||
O_VID_FMT=*Formats
|
||||
BAT_PATTERN=*Pattern
|
||||
BAT_LINKS=*Links
|
||||
BAT_SELECT_ITEMS=*Select items to download
|
||||
BAT_PASTE_LINK=*Please paste download links below
|
||||
LBL_BATCH_DESC=*Download a group of sequential files using asterisk wild card (example http://xdman.sourceforge.net/images/edge*.png)
|
||||
LBL_BATCH_ASTERISK=*Replace asterisk with
|
||||
LBL_BATCH_LETTER=*Letters
|
||||
LBL_BATCH_NUM=*Numbers
|
||||
LBL_BATCH_FROM=*From
|
||||
LBL_BATCH_TO=*To
|
||||
LBL_BATCH_WILDCARD_SIZE=*Wildcard size
|
||||
LBL_BATCH_FILE1=*First file
|
||||
LBL_BATCH_FILE2=*Second file
|
||||
LBL_BATCH_FILEN=*Last file
|
||||
BAT_LEADING_ZERO=*Use leading zero
|
||||
BAT_NO_LINK=*No link
|
||||
MENU_START_Q=*Start Queue
|
||||
DESC_SAVE_Q=*Save
|
||||
Q_ADD=*Add
|
||||
Q_REMOVE=*Remove
|
||||
Q_MOVE_UP=*Move up
|
||||
Q_MOVE_DN=*Move down
|
||||
Q_MOVE_TO=*Move to...
|
||||
Q_LIST_FILES=*Files in queue
|
||||
Q_SCHEDULE_TXT=*Scheduler
|
||||
Q_ENABLE=*Enable scheduler
|
||||
MSG_Q_START=*Start queue at
|
||||
MSG_Q_STOP=*Stop queue at
|
||||
MSG_Q_DAILY=*Daily
|
||||
MSG_Q_D1=*Sunday
|
||||
MSG_Q_D2=*Monday
|
||||
MSG_Q_D3=*Tuesday
|
||||
MSG_Q_D4=*Wednesday
|
||||
MSG_Q_D5=*Thursday
|
||||
MSG_Q_D6=*Friday
|
||||
MSG_Q_D7=*Saturday
|
||||
SETTINGS_GENERAL=*General settings
|
||||
SETTINGS_NETWORK=*Network settings
|
||||
SETTINGS_CRED=*Password manager
|
||||
SETTINGS_ADV=*Advanced settings
|
||||
DESC_MONITORING_1=*Please select browsers to monitor and make sure, browser addon is installed and enabled in respective browsers
|
||||
DESC_OTHER_BROWSERS=*XDM can also be integrated into other Chromium based browser (SRWare Iron etc) or Mozilla based (Icewasel, Waterfox etc) browsers using below links
|
||||
DESC_CHROME=*Chromium based browsers
|
||||
DESC_MOZ=*Firefox based browsers
|
||||
DESC_FILETYPES=*XDM will automatically take over downloads from browser for below file types
|
||||
DESC_DEF=*Defaults
|
||||
DESC_VIDEOTYPES=*XDM will show download option when below video formats are played in browser
|
||||
LBL_MIN_VIDEO_SIZE=*Download video larger than
|
||||
DESC_SITEEXCEPTIONS=*Do not automatically capture downloads from below web sites
|
||||
MENU_CLIP_ADD=*Monitor clipboard
|
||||
LBL_GET_TIMESTAMP=*Get timestamp from server
|
||||
SHOW_DWN_PRG=*Show download progress window
|
||||
SHOW_DWN_COMPLETE=*Show download complete dialog
|
||||
LBL_START_AUTO=*Start download automatically
|
||||
LBL_OVERWRITE_EXISTING=*Overwrite existing file
|
||||
LBL_TEMP_FOLDER=*Temporary folder
|
||||
MSG_MAX_DOWNLOAD=*Maximum simultaneous downloads
|
||||
SETTINGS_CAT=*Download categories
|
||||
SETTINGS_CAT_NAME=*Category name
|
||||
SETTINGS_CAT_TYPES=*File types
|
||||
SETTINGS_CAT_FOLDER=*Download folder
|
||||
SETTINGS_ATUO_CAT=*Automatically select download folder based on file type
|
||||
SETTINGS_FOLDER=*Default download folder
|
||||
SETTINGS_DARK_THEME=*Use dark theme if possible (Require app restart)
|
||||
SETTINGS_CAT_ADD=*Add
|
||||
SETTINGS_CAT_EDIT=*Edit
|
||||
DESC_NET1=*Connection timeout
|
||||
DESC_NET2=*Segments per download
|
||||
NET_MAX_RETRY=*Maximum retry limit
|
||||
NET_SYSTEM_PROXY=*Use system proxy settings
|
||||
DESC_HOST=*Host/Server
|
||||
MSG_HALT=*Shutdown computer after all download completes
|
||||
MSG_AWAKE=*Prevent hibernate or sleep during downloads
|
||||
EXEC_CMD=*Run a program after all download finishes
|
||||
EXE_ANTI_VIR=*Scan file with antivirus after download
|
||||
ANTIVIR_CMD=*Antivirus executable
|
||||
ANTIVIR_ARGS=*Command line parameters
|
||||
AUTO_START=*Launch XDM when system starts
|
||||
LBL_DELETE_FILE=*Delete files from disk
|
||||
DEL_SEL_TEXT=*Are you sure you want to delete selected downloads?
|
||||
ERR_MSG_FILE_NOT_FOUND_MSG=*The file has been moved,renamed or deleted
|
||||
NO_ITEM_SELECTED=*Please select item to open
|
||||
MSG_QUEUE_NAME_MISSING=*Please specify queue name
|
||||
LBL_QUEUE_OPT1=*Create new queue
|
||||
MSG_QUEUE_NAME=*Queue name
|
||||
MSG_QUEUE_SELECT_ITEMS=*Select downloads to add to queue
|
||||
MSG_DWN_STOP=*Download stopped
|
||||
MSG_NO_SPEED_LIMIT=*No speed limit
|
||||
BTN_STOP_PROCESSING=*Stop
|
||||
DWN_HIDE=*Hide
|
||||
MSG_TIME_LEFT=*Time left
|
||||
CD_TITLE=*Download Complete
|
||||
REF_WAITING_FOR_LINK=*Waiting for download link...
|
||||
SORT_TYPE=*Type
|
||||
PROP_REFERER=*Referer
|
||||
PROP_COOKIE=*Cookies
|
||||
MSG_HEADERS=*Headers
|
||||
OPT_UPDATE_FFMPEG=*Update components
|
||||
MSG_FAILED=*Download failed
|
||||
MSG_NO_UPDATE=*No updates available/already upto date
|
||||
MSG_UPDATED=*Update successfull
|
||||
MSG_ALREADY_RUNNING=*An old version of XDM is already running
|
||||
MSG_BROWSER_LAUNCH_FAILED=*Unable to launch
|
||||
MSG_NATIVE_HOST_FAILED=*Error installing native host
|
|
@ -0,0 +1,219 @@
|
|||
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
|
||||
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
|
||||
DESC_Q_TITLE=Queue and scheduler
|
||||
CTX_SAVE_AS=Save As
|
||||
CTX_COPY_URL=Copy URL
|
||||
CTX_COPY_FILE=Copy File
|
||||
MENU_REFRESH_LINK=Refresh link
|
||||
LBL_SHOW_PROGRESS=Show progress
|
||||
MENU_PROPERTIES=Properties
|
||||
MENU_RESTART=Restart
|
||||
Q_SCHEDULE_TXT=Scheduler
|
||||
Q_MOVE_TO=Move to Queue
|
||||
SORT_NAME=Name
|
||||
SORT_DATE=Date
|
||||
SORT_SIZE=Size
|
||||
SORT_STATUS=Status
|
||||
MENU_LANG=Language
|
||||
MSG_LANG1=Select language
|
||||
MSG_LANG2=Please note changes will take effect next time you start XDM
|
||||
MSG_OK=OK
|
||||
ND_CANCEL=Cancel
|
||||
STAT_DOWNLOADING=Downloading
|
||||
STAT_STOPPED=Stopped
|
||||
STAT_FINISHED=Finished
|
||||
ND_ADDRESS=Address
|
||||
ND_FILE=File
|
||||
LBL_SAVE_IN=Save in
|
||||
ND_IGNORE_URL=Do not capture download from this address
|
||||
ND_MORE=More...
|
||||
ND_DOWNLOAD_LATER=Download Later
|
||||
ND_DOWNLOAD_NOW=Download Now
|
||||
ND_AUTO_CAT=Automatically select based on file type
|
||||
BTN_BROWSE=Browse...
|
||||
ND_TITLE=New Download
|
||||
DESC_ADV_TITLE=Advanced settings
|
||||
ND_AUTH=Authentication
|
||||
DESC_NET4=Proxy settings
|
||||
SPEED_LIMIT_TITLE=Speed limit
|
||||
DESC_USER=User Name
|
||||
DESC_PASS=Password
|
||||
ND_AUTH_REMEMBER=Remember authentication for this website
|
||||
LBL_NET_OPT_DEF=System default
|
||||
ND_NO_PROXY=No Proxy
|
||||
ND_MANUAL_PROXY=Manual Proxy
|
||||
PROXY_HOST=Proxy Host
|
||||
PROXY_PORT=Proxy Port
|
||||
DESC_NET7=Proxy username
|
||||
DESC_NET8=Proxy password
|
||||
ND_SYSTEM_PROXY=Open system proxy settings
|
||||
MENU_SPEED_LIMITER=Speed limiter
|
||||
MSG_SPEED_LIMIT=Download speed limit [KB/Sec ](0 unlimited)
|
||||
MSG_INVALID_PORT=Invalid port
|
||||
MSG_INVALID_URL=Download address is invalid or unsupported
|
||||
MSG_NO_FILE=Please enter file name
|
||||
LBL_NEW_QUEUE=Name
|
||||
MSG_DOWNLOAD_FFMPEG=Download FFmpeg?
|
||||
LBL_QUEUE_OPT3=Do not use queue
|
||||
VID_PASTE_URL=Please paste video URL link here
|
||||
SETTINGS_ADV=Advanced settings
|
||||
VID_CHK=Select all
|
||||
O_VID_FMT=Format
|
||||
BAT_PATTERN=Pattern
|
||||
BAT_LINKS=Links
|
||||
BAT_SELECT_ITEMS=Select items to download
|
||||
BAT_PASTE_LINK=Please paste download links below
|
||||
LBL_BATCH_DESC=Download a group of sequential files using asterisk wild card (example http://xdman.sourceforge.net/images/edge*.png)
|
||||
LBL_BATCH_ASTERISK=Replace asterisk with
|
||||
LBL_BATCH_LETTER=Letters
|
||||
LBL_BATCH_NUM=Numbers
|
||||
LBL_BATCH_FROM=From
|
||||
LBL_BATCH_TO=To
|
||||
LBL_BATCH_WILDCARD_SIZE=Wildcard size
|
||||
LBL_BATCH_FILE1=First file
|
||||
LBL_BATCH_FILE2=Second file
|
||||
LBL_BATCH_FILEN=Last file
|
||||
BAT_LEADING_ZERO=Use leading zero
|
||||
BAT_NO_LINK=No link
|
||||
MENU_START_Q=Start Queue
|
||||
DESC_SAVE_Q=Save
|
||||
Q_ADD=Add
|
||||
Q_REMOVE=Remove
|
||||
Q_MOVE_UP=Move up
|
||||
Q_MOVE_DN=Move down
|
||||
Q_MOVE_TO=Move to...
|
||||
Q_LIST_FILES=Files in queue
|
||||
Q_SCHEDULE_TXT=Scheduler
|
||||
Q_ENABLE=Enable scheduler
|
||||
MSG_Q_START=Start queue at
|
||||
MSG_Q_STOP=Stop queue at
|
||||
MSG_Q_DAILY=Daily
|
||||
MSG_Q_D1=Sunday
|
||||
MSG_Q_D2=Monday
|
||||
MSG_Q_D3=Tuesday
|
||||
MSG_Q_D4=Wednesday
|
||||
MSG_Q_D5=Thursday
|
||||
MSG_Q_D6=Friday
|
||||
MSG_Q_D7=Saturday
|
||||
SETTINGS_GENERAL=General settings
|
||||
SETTINGS_NETWORK=Network settings
|
||||
SETTINGS_CRED=Password manager
|
||||
SETTINGS_ADV=Advanced settings
|
||||
DESC_MONITORING_1=Please make sure XDM extension is installed in your browser. To install the browser extension, please click on the buttons below or copy paste the link directly in the browser
|
||||
DESC_OTHER_BROWSERS=XDM can also be integrated into other Chromium based browser (Vivaldi, Brave browser etc) or Mozilla based (Icewasel, Waterfox etc) browsers using below links
|
||||
DESC_CHROME=Chromium based browsers
|
||||
DESC_MOZ=Firefox based browsers
|
||||
DESC_FILETYPES=XDM will automatically take over downloads from browser for below file types
|
||||
DESC_DEF=Defaults
|
||||
DESC_VIDEOTYPES=XDM will show download option when below video formats are played in browser
|
||||
LBL_MIN_VIDEO_SIZE=Download video larger than
|
||||
DESC_SITEEXCEPTIONS=Do not automatically capture downloads from below web sites
|
||||
MENU_CLIP_ADD=Monitor clipboard
|
||||
LBL_GET_TIMESTAMP=Get timestamp from server
|
||||
SHOW_DWN_PRG=Show download progress window
|
||||
SHOW_DWN_COMPLETE=Show download complete dialog
|
||||
LBL_START_AUTO=Start download automatically
|
||||
LBL_OVERWRITE_EXISTING=Overwrite existing file
|
||||
LBL_TEMP_FOLDER=Temporary folder
|
||||
MSG_MAX_DOWNLOAD=Maximum simultaneous downloads
|
||||
SETTINGS_CAT=Download categories
|
||||
SETTINGS_CAT_NAME=Category name
|
||||
SETTINGS_CAT_TYPES=File types
|
||||
SETTINGS_CAT_FOLDER=Download folder
|
||||
SETTINGS_ATUO_CAT=Automatically select download folder based on file type
|
||||
SETTINGS_FOLDER=Default download folder
|
||||
SETTINGS_DARK_THEME=Use dark theme if possible (Require app restart)
|
||||
SETTINGS_CAT_ADD=Add
|
||||
SETTINGS_CAT_EDIT=Edit
|
||||
DESC_NET1=Connection timeout in seconds
|
||||
DESC_NET2=Segments per download
|
||||
NET_MAX_RETRY=Maximum retry limit
|
||||
NET_SYSTEM_PROXY=Use system proxy settings
|
||||
DESC_HOST=Host/Server
|
||||
MSG_HALT=Shutdown computer after all download completes
|
||||
MSG_AWAKE=Prevent hibernate or sleep during downloads
|
||||
EXEC_CMD=Run a program after all download finishes
|
||||
EXE_ANTI_VIR=Scan file with antivirus after download
|
||||
ANTIVIR_CMD=Antivirus executable
|
||||
ANTIVIR_ARGS=Command line parameters
|
||||
AUTO_START=Launch XDM when system starts
|
||||
LBL_DELETE_FILE=Delete files from disk
|
||||
DEL_SEL_TEXT=Are you sure you want to delete selected downloads?
|
||||
ERR_MSG_FILE_NOT_FOUND_MSG=The file has been moved,renamed or deleted
|
||||
NO_ITEM_SELECTED=Please select item to open
|
||||
MSG_QUEUE_NAME_MISSING=Please specify queue name
|
||||
LBL_QUEUE_OPT1=Create new queue
|
||||
MSG_QUEUE_NAME=Queue name
|
||||
MSG_QUEUE_SELECT_ITEMS=Select downloads to add to queue
|
||||
MSG_DWN_STOP=Download stopped
|
||||
MSG_NO_SPEED_LIMIT=No speed limit
|
||||
BTN_STOP_PROCESSING=Stop
|
||||
DWN_HIDE=Hide
|
||||
MSG_TIME_LEFT=Time left
|
||||
CD_TITLE=Download Complete
|
||||
REF_WAITING_FOR_LINK=Waiting for download link...
|
||||
SORT_TYPE=Type
|
||||
PROP_REFERER=Referer
|
||||
PROP_COOKIE=Cookies
|
||||
MSG_HEADERS=Headers
|
||||
OPT_UPDATE_FFMPEG=Update components
|
||||
MSG_FAILED=Download failed
|
||||
MSG_NO_UPDATE=No updates available/already upto date
|
||||
MSG_UPDATED=Update successfull
|
||||
MSG_ALREADY_RUNNING=An old version of XDM is already running
|
||||
MSG_BROWSER_LAUNCH_FAILED=Unable to launch
|
||||
MSG_NATIVE_HOST_FAILED=Error installing native host
|
||||
MSG_DONT_SHOW_AGAIN=Don't show this again
|
||||
MSG_NO_USERNAME=User name required
|
||||
MSG_REF_LINK_MSG=New download link is accepted
|
||||
MSG_CATEGORY=Category
|
||||
MSG_CAT_NAME_MISSING=Category name required
|
||||
MSG_CAT_FILE_TYPES_MISSING=File types required
|
||||
MSG_CAT_FOLDER_MISSING=Download folder required
|
||||
MSG_HOST_NAME_MISSING=Host name required
|
||||
MSG_QUALITY=Quality
|
||||
MSG_MP3=Mp3 Audio
|
||||
MSG_TIME=Time
|
||||
STAT_ASSEMBLING=Assembling
|
||||
STAT_WAITING=Waiting
|
||||
MSG_UPDATE_AVAILABLE=Update(s) available
|
||||
MSG_RESTORE=Restore Window
|
||||
MSG_DOUBLE_CLICK_ACTION=Double click on download item
|
||||
MSG_OPEN_FILE=Open file
|
||||
MSG_FALLBACK_UA=User agent to be used if download is added manually
|
||||
MSG_SAVE_AS_MP3=Save as MP3
|
||||
MSG_VID_WIKI_TEXT=If you are interested in downloading streaming video from the browser using XDM
|
||||
MSG_VID_WIKI_LINK=Please click here
|
||||
NO_REFRESH_LINK=Link refresh is not available for this download
|
||||
MSG_NO_VIDEO=No video found. However there might be other ways to download the video. Please click "Learn More" button for details.
|
||||
MSG_VIDEO_DOWNLOAD_HELP=Learn More
|
||||
MSG_READ_BROWSER_COOKIE=Read cookies from browser
|
||||
MSG_SELECT_FOLDER=Select
|
||||
MSG_IMPORT_DONE=Import complete
|
|
@ -0,0 +1,4 @@
|
|||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Incomplets
|
||||
ALL_FINISHED=Terminés
|
||||
CAT_DOCUMENTS=Documents
|
||||
CAT_COMPRESSED=Compressés
|
||||
CAT_MUSIC=Musiques
|
||||
CAT_VIDEOS=Vidéos
|
||||
CAT_PROGRAMS=Programmes
|
||||
MENU_BATCH_DOWNLOAD=Téléchargement par lot
|
||||
MENU_DELETE_DWN=Supprimer le(s) téléchargement(s)
|
||||
MENU_DELETE_COMPLETED=Supprimer téléchargement(s) terminé(s)
|
||||
MENU_IMPORT=Importer
|
||||
MENU_EXPORT=Exporter
|
||||
MENU_EXIT=Quitter
|
||||
MENU_PAUSE=Mettre en pause
|
||||
MENU_RESUME=Reprendre
|
||||
MENU_RESTART=Recommencer
|
||||
MENU_REFRESH_LINK=Rafraîchir le lien
|
||||
MENU_PROPERTIES=Propriétés
|
||||
MENU_UPDATE=Vérifier les mises à jour
|
||||
MENU_ABOUT=A propos de XDM...
|
||||
SORT_DATE=Date
|
||||
SORT_SIZE=Taille
|
||||
SORT_NAME=Nom
|
||||
ND_CANCEL=ANNULER
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Menu
|
||||
TITLE_SETTINGS=Paramètres
|
||||
SETTINGS_MONITORING=Intégration navigateur
|
||||
DESC_NEW=Nouveau
|
||||
DESC_DEL=Supprimer
|
||||
DESC_Q_TITLE=File d'attente/Planification
|
||||
Q_SCHEDULE_TXT=Planification :
|
||||
Q_MOVE_TO=Déplacer
|
||||
CTX_OPEN_FILE=Ouvrir
|
||||
CTX_OPEN_FOLDER=Ouvrir l'emplacement
|
||||
CTX_SAVE_AS=Enregistrer sous
|
||||
CTX_COPY_URL=Copier l'URL
|
||||
CTX_COPY_FILE=Copier le fichier
|
||||
MENU_IMPORT=Importer
|
||||
MENU_EXPORT=Exporter
|
||||
MENU_LANG=Langue
|
||||
MSG_LANG1=Choisir langue
|
||||
MSG_LANG2=Veuillez noter que les changements seront pris en compte au prochain démarrage de XDM
|
||||
LBL_REPORT_PROBLEM=Rapporter un bug
|
||||
LBL_SUPPORT_PAGE=Page de support
|
||||
LBL_SHOW_PROGRESS=Afficher la progression
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Unvollständig
|
||||
ALL_FINISHED=Vollständig
|
||||
CAT_DOCUMENTS=Dokumente
|
||||
CAT_COMPRESSED=Komprimiert
|
||||
CAT_MUSIC=Musik
|
||||
CAT_VIDEOS=Video
|
||||
CAT_PROGRAMS=Programme
|
||||
MENU_BATCH_DOWNLOAD=Batch herunterladen
|
||||
MENU_DELETE_DWN=Downloads löschen
|
||||
MENU_DELETE_COMPLETED=Abgeschlossene Downloads löschen
|
||||
MENU_IMPORT=Importieren
|
||||
MENU_EXPORT=Exportieren
|
||||
MENU_EXIT=Beenden
|
||||
MENU_PAUSE=Pausieren
|
||||
MENU_RESUME=Fortsetzen
|
||||
MENU_RESTART=Neu starten
|
||||
MENU_REFRESH_LINK=Link aktualisieren
|
||||
MENU_PROPERTIES=Eigenschaften
|
||||
MENU_UPDATE=Auf Updates überprüfen
|
||||
MENU_ABOUT=Über XDM...
|
||||
SORT_DATE=Datum
|
||||
SORT_SIZE=Größe
|
||||
SORT_NAME=Name
|
||||
ND_CANCEL=ABBRECHEN
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Menü
|
||||
TITLE_SETTINGS=Einstellungen
|
||||
SETTINGS_MONITORING=Browser Überwachung
|
||||
DESC_NEW=Neu
|
||||
DESC_DEL=Löschen
|
||||
DESC_Q_TITLE=Warteschlange und Planer
|
||||
Q_SCHEDULE_TXT=Planer
|
||||
Q_MOVE_TO=Verschieben nach
|
||||
CTX_OPEN_FILE=Öffnen
|
||||
CTX_OPEN_FOLDER=Ordner öffnen
|
||||
CTX_SAVE_AS=Speichern unter
|
||||
CTX_COPY_URL=URL kopieren
|
||||
CTX_COPY_FILE=Datei kopieren
|
||||
MENU_IMPORT=Importieren
|
||||
MENU_EXPORT=Exportieren
|
||||
MENU_LANG=Sprache
|
||||
MSG_LANG1=Sprache auswählen
|
||||
MSG_LANG2=Bitte berücksichtigen Sie, dass die Änderungen erst beim nächsten Start von XDM wirksam werden
|
||||
LBL_REPORT_PROBLEM=Fehler melden
|
||||
LBL_SUPPORT_PAGE=Unterstützungsseite
|
||||
LBL_SHOW_PROGRESS=Fortschritt anzeigen
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Befejezetlen
|
||||
ALL_FINISHED=Befejezett
|
||||
CAT_DOCUMENTS=Dokumentumok
|
||||
CAT_COMPRESSED=Tömörített
|
||||
CAT_MUSIC=Zene
|
||||
CAT_VIDEOS=Videó
|
||||
CAT_PROGRAMS=Programok
|
||||
MENU_BATCH_DOWNLOAD=Kötegelt letöltés
|
||||
MENU_DELETE_DWN=Letöltések törlése
|
||||
MENU_DELETE_COMPLETED=Törlés befejeződött
|
||||
MENU_IMPORT=Import
|
||||
MENU_EXPORT=Export
|
||||
MENU_EXIT=Kilép
|
||||
MENU_PAUSE=Szünet
|
||||
MENU_RESUME=Folytatás
|
||||
MENU_RESTART=Újraindítás
|
||||
MENU_REFRESH_LINK=Link frissítés
|
||||
MENU_PROPERTIES=Tulajdonságok
|
||||
MENU_UPDATE=Frissítés ellenőrzése
|
||||
MENU_ABOUT=Az XDM-ről...
|
||||
SORT_DATE=Dátum
|
||||
SORT_SIZE=Méret
|
||||
SORT_NAME=Név
|
||||
ND_CANCEL=Mégse
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Menü
|
||||
TITLE_SETTINGS=Beállítások
|
||||
SETTINGS_MONITORING=Böngészőfelügyelet
|
||||
DESC_NEW=Új
|
||||
DESC_DEL=Töröl
|
||||
DESC_Q_TITLE=Sor és időzítő
|
||||
Q_SCHEDULE_TXT=Időzítő
|
||||
Q_MOVE_TO=Áthelyezés
|
||||
CTX_OPEN_FILE=Megnyit
|
||||
CTX_OPEN_FOLDER=Mappa megnyitása
|
||||
CTX_SAVE_AS=Mentés másként
|
||||
CTX_COPY_URL=Webcím másolása
|
||||
CTX_COPY_FILE=Fájl másolása
|
||||
MENU_IMPORT=Import
|
||||
MENU_EXPORT=Export
|
||||
MENU_LANG=Nyelv
|
||||
MSG_LANG1=Válasszon nyelvet
|
||||
MSG_LANG2=Kérjük, vegye figyelembe, hogy a változtatások az XDM újraindítása után lépnek érvénybe
|
||||
LBL_REPORT_PROBLEM=Hibajelentés
|
||||
LBL_SUPPORT_PAGE=Támogatási oldal
|
||||
LBL_SHOW_PROGRESS=Haladás megjelenítése
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Belum Selesai
|
||||
ALL_FINISHED=Selesai
|
||||
CAT_DOCUMENTS=Dokumen
|
||||
CAT_COMPRESSED=Kompresi
|
||||
CAT_MUSIC=Musik
|
||||
CAT_VIDEOS=Video
|
||||
CAT_PROGRAMS=Program
|
||||
MENU_BATCH_DOWNLOAD=Unduh rangkap
|
||||
MENU_DELETE_DWN=Hapus unduhan
|
||||
MENU_DELETE_COMPLETED=Hapus unduhan selesai
|
||||
MENU_IMPORT=Impor
|
||||
MENU_EXPORT=Ekspor
|
||||
MENU_EXIT=Keluar
|
||||
MENU_PAUSE=Jeda
|
||||
MENU_RESUME=Lanjutkan
|
||||
MENU_RESTART=Mulai Ulang
|
||||
MENU_REFRESH_LINK=Segarkan link
|
||||
MENU_PROPERTIES=Properti
|
||||
MENU_UPDATE=Periksa pembaruan
|
||||
MENU_ABOUT=Tentang XDM...
|
||||
SORT_DATE=Tanggal
|
||||
SORT_SIZE=Ukuran
|
||||
SORT_NAME=Nama
|
||||
ND_CANCEL=BATAL
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Menu
|
||||
TITLE_SETTINGS=Pengaturan
|
||||
SETTINGS_MONITORING=Monitor browser
|
||||
DESC_NEW=Baru
|
||||
DESC_DEL=Hapus
|
||||
DESC_Q_TITLE=Antrian dan Jadwal
|
||||
Q_SCHEDULE_TXT=Jadwal
|
||||
Q_MOVE_TO=Pindah ke
|
||||
CTX_OPEN_FILE=Buka
|
||||
CTX_OPEN_FOLDER=Buka folder
|
||||
CTX_SAVE_AS=Simpan Sebagai
|
||||
CTX_COPY_URL=Salin URL
|
||||
CTX_COPY_FILE=Salin Berkas
|
||||
MENU_IMPORT=Impor
|
||||
MENU_EXPORT=Ekspor
|
||||
MENU_LANG=Bahasa
|
||||
MSG_LANG1=Pilih bahasa
|
||||
MSG_LANG2=Perhatian perubahan akan berlaku saat anda memulai ulang XDM
|
||||
LBL_REPORT_PROBLEM=Laporkan kutu
|
||||
LBL_SUPPORT_PAGE=Halaman dukungan
|
||||
LBL_SHOW_PROGRESS=Lihat proses
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Incompleti
|
||||
ALL_FINISHED=Completati
|
||||
CAT_DOCUMENTS=Documenti
|
||||
CAT_COMPRESSED=Compressi
|
||||
CAT_MUSIC=Musica
|
||||
CAT_VIDEOS=Video
|
||||
CAT_PROGRAMS=Programmi
|
||||
MENU_BATCH_DOWNLOAD=Scarica in batch
|
||||
MENU_DELETE_DWN=Cancella downloads
|
||||
MENU_DELETE_COMPLETED=Cancella completati
|
||||
MENU_IMPORT=Importa
|
||||
MENU_EXPORT=Esporta
|
||||
MENU_EXIT=Esci
|
||||
MENU_PAUSE=Pausa
|
||||
MENU_RESUME=Riprendi
|
||||
MENU_RESTART=Riavvia
|
||||
MENU_REFRESH_LINK=Aggiorna collegamento
|
||||
MENU_PROPERTIES=Proprietà
|
||||
MENU_UPDATE=Controlla aggiornamenti
|
||||
MENU_ABOUT=Informazioni su XDM...
|
||||
SORT_DATE=Data
|
||||
SORT_SIZE=Dimensione
|
||||
SORT_NAME=Nome
|
||||
ND_CANCEL=ANNULLA
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Menu
|
||||
TITLE_SETTINGS=Impostazioni
|
||||
SETTINGS_MONITORING=Monitoraggio browser
|
||||
DESC_NEW=Nuovo
|
||||
DESC_DEL=Elimina
|
||||
DESC_Q_TITLE=Accoda e pianifica
|
||||
Q_SCHEDULE_TXT=Pianificatore
|
||||
Q_MOVE_TO=Sposta a
|
||||
CTX_OPEN_FILE=Apri
|
||||
CTX_OPEN_FOLDER=Apri cartella
|
||||
CTX_SAVE_AS=Salva come
|
||||
CTX_COPY_URL=Copia URL
|
||||
CTX_COPY_FILE=Copia File
|
||||
MENU_IMPORT=Importa
|
||||
MENU_EXPORT=Esporta
|
||||
MENU_LANG=Lingua
|
||||
MSG_LANG1=Seleziona lingua
|
||||
MSG_LANG2=Si prega di notare che i cambiamenti entreranno in vigore al prossimo avvio di XDM
|
||||
LBL_REPORT_PROBLEM=Riporta un bug
|
||||
LBL_SUPPORT_PAGE=Pagina del supporto
|
||||
LBL_SHOW_PROGRESS=Mostra progresso
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=미완료
|
||||
ALL_FINISHED=완료
|
||||
CAT_DOCUMENTS=문서
|
||||
CAT_COMPRESSED=압축됨
|
||||
CAT_MUSIC=음악
|
||||
CAT_VIDEOS=비디오
|
||||
CAT_PROGRAMS=프로그램
|
||||
MENU_BATCH_DOWNLOAD=배치 다운로드
|
||||
MENU_DELETE_DWN=다운로드 삭제
|
||||
MENU_DELETE_COMPLETED=클리어 완료
|
||||
MENU_IMPORT=Import
|
||||
MENU_EXPORT=Export
|
||||
MENU_EXIT=종료
|
||||
MENU_PAUSE=일시 중지
|
||||
MENU_RESUME=계속
|
||||
MENU_RESTART=다시 시작
|
||||
MENU_REFRESH_LINK=리프레시 링크
|
||||
MENU_PROPERTIES=속성
|
||||
MENU_UPDATE=업데이트 확인
|
||||
MENU_ABOUT=XDM 정보
|
||||
SORT_DATE=날짜
|
||||
SORT_SIZE=사이즈
|
||||
SORT_NAME=이름
|
||||
ND_CANCEL=취소
|
||||
MSG_OK=OK
|
||||
LBL_MENU=메뉴
|
||||
TITLE_SETTINGS=설정
|
||||
SETTINGS_MONITORING=브라우저 모니터링
|
||||
DESC_NEW=새로 만들기
|
||||
DESC_DEL=삭제
|
||||
DESC_Q_TITLE=큐 및 스케줄러
|
||||
Q_SCHEDULE_TXT=스케줄러
|
||||
Q_MOVE_TO=이동하기
|
||||
CTX_OPEN_FILE=열림
|
||||
CTX_OPEN_FOLDER=폴더 열기
|
||||
CTX_SAVE_AS=다른 이름으로 저장
|
||||
CTX_COPY_URL=URL 복사
|
||||
CTX_COPY_FILE=파일 복사
|
||||
MENU_IMPORT=Import
|
||||
MENU_EXPORT=Export
|
||||
MENU_LANG=언어
|
||||
MSG_LANG1=언어 선택
|
||||
MSG_LANG2=다음 번에 XDM을 시작할 때 변경 사항이 적용된다는 점에 유의하십시오.
|
||||
LBL_REPORT_PROBLEM=버그리포트
|
||||
LBL_SUPPORT_PAGE=지원페이지
|
||||
LBL_SHOW_PROGRESS=진행 표시
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,4 @@
|
|||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,11 @@
|
|||
ALL_UNFINISHED=सबै नसकिएको
|
||||
ALL_FINISHED=नसकिएको
|
||||
CAT_DOCUMENTS=कागजात
|
||||
CAT_COMPRESSED=थिचिएको
|
||||
CAT_MUSIC=गाना
|
||||
CAT_VIDEOS=मुभि
|
||||
CAT_PROGRAMS=प्रोग्राम
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Nieukończone
|
||||
ALL_FINISHED=Ukończone
|
||||
CAT_DOCUMENTS=Dokumenty
|
||||
CAT_COMPRESSED=Archiwa
|
||||
CAT_MUSIC=Muzyka
|
||||
CAT_VIDEOS=Wideo
|
||||
CAT_PROGRAMS=Programy
|
||||
MENU_BATCH_DOWNLOAD=Pobieranie wsadowe
|
||||
MENU_DELETE_DWN=Usuń pobieranie
|
||||
MENU_DELETE_COMPLETED=Wyczyść zakończone
|
||||
MENU_IMPORT=Zaimportuj
|
||||
MENU_EXPORT=Wyeksportuj
|
||||
MENU_EXIT=Zamknij
|
||||
MENU_PAUSE=Wstrzymaj
|
||||
MENU_RESUME=Wznów
|
||||
MENU_RESTART=Zrestartuj
|
||||
MENU_REFRESH_LINK=Odśwież link
|
||||
MENU_PROPERTIES=Właściwości
|
||||
MENU_UPDATE=Aktualizuj
|
||||
MENU_ABOUT=O programie
|
||||
SORT_DATE=Data
|
||||
SORT_SIZE=Rozmiar
|
||||
SORT_NAME=Nazwa
|
||||
ND_CANCEL=Anuluj
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Menu
|
||||
TITLE_SETTINGS=Ustawienia
|
||||
SETTINGS_MONITORING=Monitor przeglądarki
|
||||
DESC_NEW=Nowy wpis
|
||||
DESC_DEL=Usuń
|
||||
DESC_Q_TITLE=Kolejka i harmonogram
|
||||
Q_SCHEDULE_TXT=Harmonogram
|
||||
Q_MOVE_TO=Przenieś do
|
||||
CTX_OPEN_FILE=Otwórz
|
||||
CTX_OPEN_FOLDER=Otwórz folder
|
||||
CTX_SAVE_AS=Zapisz jako
|
||||
CTX_COPY_URL=Skopiuj URL
|
||||
CTX_COPY_FILE=Skopiuj plik
|
||||
MENU_IMPORT=Zaimportuj
|
||||
MENU_EXPORT=Wyeksportuj
|
||||
MENU_LANG=Język
|
||||
MSG_LANG1=Wybierz język
|
||||
MSG_LANG2=Zmiany zaczną obowiązywać po restarcie programu
|
||||
LBL_REPORT_PROBLEM=Zgłoś błąd
|
||||
LBL_SUPPORT_PAGE=Strona wsparcia
|
||||
LBL_SHOW_PROGRESS=Pokaż postęp
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Incompleto
|
||||
ALL_FINISHED=Completo
|
||||
CAT_DOCUMENTS=Documentos
|
||||
CAT_COMPRESSED=Compactos
|
||||
CAT_MUSIC=Áudio
|
||||
CAT_VIDEOS=Vídeo
|
||||
CAT_PROGRAMS=Programas
|
||||
MENU_BATCH_DOWNLOAD=Pacote de download
|
||||
MENU_DELETE_DWN=Excluir download
|
||||
MENU_DELETE_COMPLETED=Limpeza concluída
|
||||
MENU_IMPORT=Importar
|
||||
MENU_EXPORT=Exportar
|
||||
MENU_EXIT=Fechar XDM
|
||||
MENU_PAUSE=Pausar
|
||||
MENU_RESUME=Continuar download
|
||||
MENU_RESTART=Reiniciar download
|
||||
MENU_REFRESH_LINK=Atualizar link
|
||||
MENU_PROPERTIES=Propriedades
|
||||
MENU_UPDATE=Procurar atualizações
|
||||
MENU_ABOUT=Sobre o XDM...
|
||||
SORT_DATE=Data
|
||||
SORT_SIZE=Tamanho
|
||||
SORT_NAME=Nome
|
||||
ND_CANCEL=Fechar
|
||||
MSG_OK=Certo
|
||||
LBL_MENU=Menu
|
||||
TITLE_SETTINGS=Configura\u00e7\u00f5es
|
||||
SETTINGS_MONITORING=Monitorar navegador(es)
|
||||
DESC_NEW=Novo
|
||||
DESC_DEL=Exclu\u00edr
|
||||
DESC_Q_TITLE=Fila e programa\u00e7\u00e3o
|
||||
Q_SCHEDULE_TXT=Programar
|
||||
Q_MOVE_TO=Mover para
|
||||
CTX_OPEN_FILE=Abrir
|
||||
CTX_OPEN_FOLDER=Abrir pasta
|
||||
CTX_SAVE_AS=Salvar como
|
||||
CTX_COPY_URL=Copiar URL
|
||||
CTX_COPY_FILE=Copiar arquivo
|
||||
MENU_IMPORT=Importar
|
||||
MENU_EXPORT=Exportar
|
||||
MENU_LANG=Idioma
|
||||
MSG_LANG1=Selecionar idioma
|
||||
MSG_LANG2=Vale constar que as mudan\u00e7as aparecer\u00e3o ao fechar e abrir o XDM.
|
||||
LBL_REPORT_PROBLEM=Reportar um bug
|
||||
LBL_SUPPORT_PAGE=P\u00e1gina de ajuda
|
||||
LBL_SHOW_PROGRESS=Mostrar Progresso
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Incomplete
|
||||
ALL_FINISHED=Complete
|
||||
CAT_DOCUMENTS=Documente
|
||||
CAT_COMPRESSED=Comprimate
|
||||
CAT_MUSIC=Musică
|
||||
CAT_VIDEOS=Video
|
||||
CAT_PROGRAMS=Programe
|
||||
MENU_BATCH_DOWNLOAD=Descărcare în serie
|
||||
MENU_DELETE_DWN=Șterge descărcări
|
||||
MENU_DELETE_COMPLETED=Șterge complete
|
||||
MENU_IMPORT=Importă
|
||||
MENU_EXPORT=Exportă
|
||||
MENU_EXIT=Ieși
|
||||
MENU_PAUSE=Pauză
|
||||
MENU_RESUME=Continuă
|
||||
MENU_RESTART=Repornește
|
||||
MENU_REFRESH_LINK=Actualizează link
|
||||
MENU_PROPERTIES=Proprietăți
|
||||
MENU_UPDATE=Caută actualizări
|
||||
MENU_ABOUT=Despre XDM...
|
||||
SORT_DATE=Data
|
||||
SORT_SIZE=Dimensiune
|
||||
SORT_NAME=Nume
|
||||
ND_CANCEL=RENUNȚĂ
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Meniu
|
||||
TITLE_SETTINGS=Reglări
|
||||
SETTINGS_MONITORING=Monitorizare Browser
|
||||
DESC_NEW=Nou
|
||||
DESC_DEL=Șterge
|
||||
DESC_Q_TITLE=Coadă și planificator
|
||||
Q_SCHEDULE_TXT=Planificator
|
||||
Q_MOVE_TO=Mută în
|
||||
CTX_OPEN_FILE=Deschide
|
||||
CTX_OPEN_FOLDER=Deschide Dosar
|
||||
CTX_SAVE_AS=Salvează ca
|
||||
CTX_COPY_URL=Copiază URL
|
||||
CTX_COPY_FILE=Copiază Fișier
|
||||
MENU_IMPORT=Importă
|
||||
MENU_EXPORT=Exportă
|
||||
MENU_LANG=Limbă
|
||||
MSG_LANG1=Selecționează limba
|
||||
MSG_LANG2=Modificările vor avea efect data viitoare când porniți XDM
|
||||
LBL_REPORT_PROBLEM=Raportează o eroare
|
||||
LBL_SUPPORT_PAGE=Pagină de Suport
|
||||
LBL_SHOW_PROGRESS=Afișează progres
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Незавершенные
|
||||
ALL_FINISHED=Завершенные
|
||||
CAT_DOCUMENTS=Документы
|
||||
CAT_COMPRESSED=Сжатые
|
||||
CAT_MUSIC=Музыка
|
||||
CAT_VIDEOS=Видео
|
||||
CAT_PROGRAMS=Программы
|
||||
MENU_BATCH_DOWNLOAD=Пакетная загрузка
|
||||
MENU_DELETE_DWN=Удалить загрузки
|
||||
MENU_DELETE_COMPLETED=Очистить загруженные
|
||||
MENU_IMPORT=Импорт
|
||||
MENU_EXPORT=Экспорт
|
||||
MENU_EXIT=Выход
|
||||
MENU_PAUSE=Пауза
|
||||
MENU_RESUME=Возобновить
|
||||
MENU_RESTART=Перезапустить
|
||||
MENU_REFRESH_LINK=Обновить ссылку
|
||||
MENU_PROPERTIES=Свойства
|
||||
MENU_UPDATE=Проверить обновления
|
||||
MENU_ABOUT=Об XDM...
|
||||
SORT_DATE=Дата
|
||||
SORT_SIZE=Размер
|
||||
SORT_NAME=Имя
|
||||
ND_CANCEL=Отмена
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Меню
|
||||
TITLE_SETTINGS=Настройки
|
||||
SETTINGS_MONITORING=Отслеживание в браузере
|
||||
DESC_NEW=Новый
|
||||
DESC_DEL=Удалить
|
||||
DESC_Q_TITLE=Очередь и планировщик
|
||||
Q_SCHEDULE_TXT=Планировщик
|
||||
Q_MOVE_TO=Перейти к
|
||||
CTX_OPEN_FILE=Открыть
|
||||
CTX_OPEN_FOLDER=Открыть каталог
|
||||
CTX_SAVE_AS=Сохранить как
|
||||
CTX_COPY_URL=Копировать ссылку
|
||||
CTX_COPY_FILE=Копировать файл
|
||||
MENU_IMPORT=Импорт
|
||||
MENU_EXPORT=Экспорт
|
||||
MENU_LANG=Язык
|
||||
MSG_LANG1=Выберите язык
|
||||
MSG_LANG2=Пожалуйста, внесите изменения, которые будут применены при следующем запуске XDM
|
||||
LBL_REPORT_PROBLEM=Сообщить об ошибке
|
||||
LBL_SUPPORT_PAGE=Страница поддержки
|
||||
LBL_SHOW_PROGRESS=Показать прогресс
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Nezavršeno
|
||||
ALL_FINISHED=Završeno
|
||||
CAT_DOCUMENTS=Dokumenta
|
||||
CAT_COMPRESSED=Zapakovano
|
||||
CAT_MUSIC=Muzika
|
||||
CAT_VIDEOS=Video
|
||||
CAT_PROGRAMS=Programi
|
||||
MENU_BATCH_DOWNLOAD=Grupno preuzimanje
|
||||
MENU_DELETE_DWN=Obriši preuzimanja
|
||||
MENU_DELETE_COMPLETED=Obriši završena
|
||||
MENU_IMPORT=Uvezi
|
||||
MENU_EXPORT=Izvezi
|
||||
MENU_EXIT=Izlaz
|
||||
MENU_PAUSE=Pauziraj
|
||||
MENU_RESUME=Nastavi
|
||||
MENU_RESTART=Ponovo počni
|
||||
MENU_REFRESH_LINK=Osveži adresu
|
||||
MENU_PROPERTIES=Svojstva
|
||||
MENU_UPDATE=Proveri nadogradnje
|
||||
MENU_ABOUT=O programu XDM...
|
||||
SORT_DATE=Datum
|
||||
SORT_SIZE=Veličina
|
||||
SORT_NAME=Naziv
|
||||
ND_CANCEL=PONIŠTI
|
||||
MSG_OK=U REDU
|
||||
LBL_MENU=Meni
|
||||
TITLE_SETTINGS=Postavke
|
||||
SETTINGS_MONITORING=Nadgledanje pregledača
|
||||
DESC_NEW=Novo
|
||||
DESC_DEL=Obriši
|
||||
DESC_Q_TITLE=Lista i zakazivanje
|
||||
Q_SCHEDULE_TXT=Zakazivač
|
||||
Q_MOVE_TO=Pomeri na
|
||||
CTX_OPEN_FILE=Otvori
|
||||
CTX_OPEN_FOLDER=Otvori lokaciju
|
||||
CTX_SAVE_AS=Sačuvaj kao
|
||||
CTX_COPY_URL=Kopiraj adresu
|
||||
CTX_COPY_FILE=Kopiraj datoteku
|
||||
MENU_IMPORT=Uvezi
|
||||
MENU_EXPORT=Izvezi
|
||||
MENU_LANG=Jezik
|
||||
MSG_LANG1=Izaberi jezik
|
||||
MSG_LANG2=Promene će biti vidljive nakon ponovnog pokretanja programa
|
||||
LBL_REPORT_PROBLEM=Prijavi grešku
|
||||
LBL_SUPPORT_PAGE=Stranica podrške
|
||||
LBL_SHOW_PROGRESS=Prikaži napredak
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Незавршено
|
||||
ALL_FINISHED=Завршено
|
||||
CAT_DOCUMENTS=Документа
|
||||
CAT_COMPRESSED=Запаковано
|
||||
CAT_MUSIC=Музика
|
||||
CAT_VIDEOS=Видео
|
||||
CAT_PROGRAMS=Програми
|
||||
MENU_BATCH_DOWNLOAD=Групно преузимање
|
||||
MENU_DELETE_DWN=Обриши преузимања
|
||||
MENU_DELETE_COMPLETED=Обриши завршена
|
||||
MENU_IMPORT=Увези
|
||||
MENU_EXPORT=Извези
|
||||
MENU_EXIT=Излаз
|
||||
MENU_PAUSE=Паузирај
|
||||
MENU_RESUME=Настави
|
||||
MENU_RESTART=Поново почни
|
||||
MENU_REFRESH_LINK=Освежи адресу
|
||||
MENU_PROPERTIES=Својства
|
||||
MENU_UPDATE=Провери надоградње
|
||||
MENU_ABOUT=О програму XDM...
|
||||
SORT_DATE=Датум
|
||||
SORT_SIZE=Величина
|
||||
SORT_NAME=Назив
|
||||
ND_CANCEL=ПОНИШТИ
|
||||
MSG_OK=У РЕДУ
|
||||
LBL_MENU=Мени
|
||||
TITLE_SETTINGS=Поставке
|
||||
SETTINGS_MONITORING=Надгледање прегледача
|
||||
DESC_NEW=Ново
|
||||
DESC_DEL=Обриши
|
||||
DESC_Q_TITLE=Листа и заказивање
|
||||
Q_SCHEDULE_TXT=Заказивач
|
||||
Q_MOVE_TO=Помери на
|
||||
CTX_OPEN_FILE=Отвори
|
||||
CTX_OPEN_FOLDER=Отвори локацију
|
||||
CTX_SAVE_AS=Сачувај као
|
||||
CTX_COPY_URL=Копирај адресу
|
||||
CTX_COPY_FILE=Копирај датотеку
|
||||
MENU_IMPORT=Увези
|
||||
MENU_EXPORT=Извези
|
||||
MENU_LANG=Језик
|
||||
MSG_LANG1=Изабери језик
|
||||
MSG_LANG2=Промене ће бити видљиве након поновног покретања програма
|
||||
LBL_REPORT_PROBLEM=Пријави грешку
|
||||
LBL_SUPPORT_PAGE=Страница подршке
|
||||
LBL_SHOW_PROGRESS=Прикажи напредак
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Incompleto
|
||||
ALL_FINISHED=Completado
|
||||
CAT_DOCUMENTS=Documentos
|
||||
CAT_COMPRESSED=Comprimido
|
||||
CAT_MUSIC=Música
|
||||
CAT_VIDEOS=Vídeo
|
||||
CAT_PROGRAMS=Programas
|
||||
MENU_BATCH_DOWNLOAD=Descarga por lotes
|
||||
MENU_DELETE_DWN=Eliminar descargas
|
||||
MENU_DELETE_COMPLETED=Limpiar finalizados
|
||||
MENU_IMPORT=Importar
|
||||
MENU_EXPORT=Exportar
|
||||
MENU_EXIT=Salir
|
||||
MENU_PAUSE=Pausa
|
||||
MENU_RESUME=Reanudar
|
||||
MENU_RESTART=Reiniciar
|
||||
MENU_REFRESH_LINK=Actualizar enlace
|
||||
MENU_PROPERTIES=Propiedades
|
||||
MENU_UPDATE=Buscar actualizaciones
|
||||
MENU_ABOUT=Sobre XDM ...
|
||||
SORT_DATE=Fecha
|
||||
SORT_SIZE=Tamaño
|
||||
SORT_NAME=Nombre
|
||||
ND_CANCEL=CANCELAR
|
||||
MSG_OK=Ok
|
||||
LBL_MENU=Menú
|
||||
TITLE_SETTINGS=Opciones
|
||||
SETTINGS_MONITORING=Monitorización del navegador
|
||||
DESC_NEW=Nuevo
|
||||
DESC_DEL=Borrar
|
||||
DESC_Q_TITLE=Colas y planificador
|
||||
Q_SCHEDULE_TXT=Planificador
|
||||
Q_MOVE_TO=Mover a
|
||||
CTX_OPEN_FILE=Abrir
|
||||
CTX_OPEN_FOLDER=Abrir carpeta
|
||||
CTX_SAVE_AS=Guardar como
|
||||
CTX_COPY_URL=Copiar URL
|
||||
CTX_COPY_FILE=Copiar archivo
|
||||
MENU_IMPORT=Importar
|
||||
MENU_EXPORT=Exportar
|
||||
MENU_LANG=Idioma
|
||||
MSG_LANG1=Seleccione el idioma
|
||||
MSG_LANG2=Por favor, tenga en cuenta que los cambios tendrán efecto la próxima vez que inicie XDM
|
||||
LBL_REPORT_PROBLEM=Reportar un error
|
||||
LBL_SUPPORT_PAGE=Pagina de soporte
|
||||
LBL_SHOW_PROGRESS=Mostrar progreso
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,27 @@
|
|||
ALL_UNFINISHED=正在下載
|
||||
ALL_FINISHED=已完成
|
||||
CAT_DOCUMENTS=檔案
|
||||
CAT_COMPRESSED=壓縮檔
|
||||
CAT_MUSIC=音樂
|
||||
CAT_VIDEOS=影片
|
||||
CAT_PROGRAMS=程式
|
||||
MENU_BATCH_DOWNLOAD=批次下載
|
||||
MENU_DELETE_DWN=删除此下載
|
||||
MENU_DELETE_COMPLETED=清除已完成
|
||||
MENU_IMPORT=匯入
|
||||
MENU_EXPORT=匯出
|
||||
MENU_EXIT=離開
|
||||
MENU_PAUSE=暫停
|
||||
MENU_RESUME=繼續
|
||||
MENU_RESTART=重新開始
|
||||
MENU_REFRESH_LINK=更新網址
|
||||
MENU_PROPERTIES=內容
|
||||
MENU_UPDATE=檢查更新
|
||||
MENU_ABOUT=關於 XDM...
|
||||
SORT_DATE=依照時間
|
||||
SORT_SIZE=依照大小
|
||||
SORT_NAME=依照名稱
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Bitmeyenler
|
||||
ALL_FINISHED=Bitenler
|
||||
CAT_DOCUMENTS=Belgeler
|
||||
CAT_COMPRESSED=Sıkıştırılmış
|
||||
CAT_MUSIC=Müzikler
|
||||
CAT_VIDEOS=Videolar
|
||||
CAT_PROGRAMS=Programlar
|
||||
MENU_BATCH_DOWNLOAD=Toplu indirme
|
||||
MENU_DELETE_DWN=İndirmeleri sil
|
||||
MENU_DELETE_COMPLETED=Bitenleri temizle
|
||||
MENU_IMPORT=İçe aktar
|
||||
MENU_EXPORT=Dışa aktar
|
||||
MENU_EXIT=Çık
|
||||
MENU_PAUSE=Duraklat
|
||||
MENU_RESUME=Sürdür
|
||||
MENU_RESTART=Yeniden başlat
|
||||
MENU_REFRESH_LINK=Bağlantıyı yenile
|
||||
MENU_PROPERTIES=Özellikler
|
||||
MENU_UPDATE=Güncellemeleri kontrol et
|
||||
MENU_ABOUT=XDM hakkında...
|
||||
SORT_DATE=Tarih
|
||||
SORT_SIZE=Boyut
|
||||
SORT_NAME=Ad
|
||||
ND_CANCEL=İPTAL ET
|
||||
MSG_OK=Tamam
|
||||
LBL_MENU=Menü
|
||||
TITLE_SETTINGS=Ayarlar
|
||||
SETTINGS_MONITORING=Tarayıcı izleme
|
||||
DESC_NEW=Yeni
|
||||
DESC_DEL=Sil
|
||||
DESC_Q_TITLE=Kuyruk ve zamanlayıcı
|
||||
Q_SCHEDULE_TXT=Zamanlayıcı
|
||||
Q_MOVE_TO=Şuraya taşı:
|
||||
CTX_OPEN_FILE=Aç
|
||||
CTX_OPEN_FOLDER=Klasörü aç
|
||||
CTX_SAVE_AS=Farklı Kaydet
|
||||
CTX_COPY_URL=URL Kopyala
|
||||
CTX_COPY_FILE=Dosya Kopyala
|
||||
MENU_IMPORT=İçe aktar
|
||||
MENU_EXPORT=Dışa aktar
|
||||
MENU_LANG=Diller
|
||||
MSG_LANG1=Dil seçin
|
||||
MSG_LANG2=Lütfen, değişikliklerin XDM'yi bir sonraki başlatışınızda geçerli olacağını unutmayın.
|
||||
LBL_REPORT_PROBLEM=Hata bildir
|
||||
LBL_SUPPORT_PAGE=Destek sayfası
|
||||
LBL_SHOW_PROGRESS=İlerlemeyi göster
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,50 @@
|
|||
ALL_UNFINISHED=Незавершені
|
||||
ALL_FINISHED=Завершені
|
||||
CAT_DOCUMENTS=Документи
|
||||
CAT_COMPRESSED=Стиснуті
|
||||
CAT_MUSIC=Музика
|
||||
CAT_VIDEOS=Відео
|
||||
CAT_PROGRAMS=Програми
|
||||
MENU_BATCH_DOWNLOAD=Пакетне завантаження
|
||||
MENU_DELETE_DWN=Видалити завантаження
|
||||
MENU_DELETE_COMPLETED=Очистити завершені
|
||||
MENU_IMPORT=Імпорт
|
||||
MENU_EXPORT=Експорт
|
||||
MENU_EXIT=Вихід
|
||||
MENU_PAUSE=Пауза
|
||||
MENU_RESUME=Відновити
|
||||
MENU_RESTART=Перезавантажити
|
||||
MENU_REFRESH_LINK=Оновити посилання
|
||||
MENU_PROPERTIES=Властивості
|
||||
MENU_UPDATE=Перевірити оновлення
|
||||
MENU_ABOUT=Про XDM...
|
||||
SORT_DATE=Дата
|
||||
SORT_SIZE=Розмір
|
||||
SORT_NAME=Ім'я
|
||||
ND_CANCEL=СКАСУВАТИ
|
||||
MSG_OK=Добре
|
||||
LBL_MENU=Меню
|
||||
TITLE_SETTINGS=Налаштування
|
||||
SETTINGS_MONITORING=Відстеження браузера
|
||||
DESC_NEW=Новий
|
||||
DESC_DEL=Видалити
|
||||
DESC_Q_TITLE=Черга і планувальник
|
||||
Q_SCHEDULE_TXT=Планувальник
|
||||
Q_MOVE_TO=Перемістити до
|
||||
CTX_OPEN_FILE=Відкрити
|
||||
CTX_OPEN_FOLDER=Відкрити теку
|
||||
CTX_SAVE_AS=Зберегти як
|
||||
CTX_COPY_URL=Копіювати адресу URL
|
||||
CTX_COPY_FILE=Копіювати файл
|
||||
MENU_IMPORT=Імпорт
|
||||
MENU_EXPORT=Експорт
|
||||
MENU_LANG=Мова
|
||||
MSG_LANG1=Обрати мову
|
||||
MSG_LANG2=Зверніть увагу, що зміни наберуть чинності наступного разу, коли ви запустите XDM
|
||||
LBL_REPORT_PROBLEM=Повідомити про помилку
|
||||
LBL_SUPPORT_PAGE=Сторінка підтримки
|
||||
LBL_SHOW_PROGRESS=Показати хід виконання
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,49 @@
|
|||
ALL_UNFINISHED=Đang Tải Về
|
||||
ALL_FINISHED=Đã Xong
|
||||
CAT_DOCUMENTS=Tài Liệu
|
||||
CAT_COMPRESSED=Tập Tin Nén
|
||||
CAT_MUSIC=Nhạc
|
||||
CAT_VIDEOS=Video
|
||||
CAT_PROGRAMS=Chương Trình
|
||||
MENU_BATCH_DOWNLOAD=Batch download
|
||||
MENU_DELETE_DWN=Xóa
|
||||
MENU_DELETE_COMPLETED=Xóa Các Tệp Tải Xong
|
||||
MENU_IMPORT=Nhập
|
||||
MENU_EXPORT=Xuất
|
||||
MENU_EXIT=Thoát
|
||||
MENU_PAUSE=Tạm Dừng
|
||||
MENU_RESUME=Tiếp Tục Tải Về
|
||||
MENU_RESTART=Khởi Động Lại
|
||||
MENU_REFRESH_LINK=Làm Mới Liên Kết
|
||||
MENU_PROPERTIES=Thuộc Tính
|
||||
MENU_UPDATE=Kiểm Tra Cập Nhật
|
||||
MENU_ABOUT=Về XDM...
|
||||
SORT_DATE=Ngày
|
||||
SORT_SIZE=Kích Cỡ
|
||||
ND_CANCEL=HỦY BỎ
|
||||
MSG_OK=OK
|
||||
LBL_MENU=Menu
|
||||
TITLE_SETTINGS=Cài Đặt
|
||||
SETTINGS_MONITORING=Bắt Link Tự Động
|
||||
DESC_NEW=Mới
|
||||
DESC_DEL=Xóa
|
||||
DESC_Q_TITLE=Queue and scheduler
|
||||
Q_SCHEDULE_TXT=Kế hoạch
|
||||
Q_MOVE_TO=Di chuyển tới
|
||||
CTX_OPEN_FILE=Mở
|
||||
CTX_OPEN_FOLDER=Mở Thư Mục
|
||||
CTX_SAVE_AS=Lưu Như
|
||||
CTX_COPY_URL=Sao Chép Liên Kết
|
||||
CTX_COPY_FILE=Sao Chép Tập Tin
|
||||
MENU_IMPORT=Nhập
|
||||
MENU_EXPORT=Xuất
|
||||
MENU_LANG=Ngôn Ngữ
|
||||
MSG_LANG1=Lựa Chọn Ngôn Ngữ
|
||||
MSG_LANG2=Vui lòng khởi động lại XDM để thay đổi có hiệu lực
|
||||
LBL_REPORT_PROBLEM=Báo Lỗi
|
||||
LBL_SUPPORT_PAGE=Trang Hỗ Trợ
|
||||
LBL_SHOW_PROGRESS=Hiển thị quá trình tải về
|
||||
SORT_STATUS=Status
|
||||
LBL_SEARCH=Search
|
||||
LBL_VIDEO_DOWNLOAD=Video download
|
||||
LBL_NEW_DOWNLOAD=New download
|
|
@ -0,0 +1,25 @@
|
|||
English=English.txt
|
||||
Arabic (العربية)=Arabic.txt
|
||||
Chinese simplified (简体中文)=Chinese simplified.txt
|
||||
Chinese Traditional (繁體中文)=Chinese Traditional.txt
|
||||
Czech=Czech.txt
|
||||
Farsi - Persian (فارسی)=Farsi-Persian.txt
|
||||
French(Français)=French.txt
|
||||
German(Deutsch)=German.txt
|
||||
Hungarian=Hungarian.txt
|
||||
Indonesian (Bahasa Indonesia)=Indonesian.txt
|
||||
Italian=Italian.txt
|
||||
Korea (한국어)=Korea.txt
|
||||
Malayalam=Malayalam.txt
|
||||
Nepali=Nepali.txt
|
||||
Polish=Polish.txt
|
||||
Portuguese Brazil (Português (Brasil))=Portuguese Brazil.txt
|
||||
Romanian (ROMÂNĂ)=Romanian.txt
|
||||
Russian(Rусский)=Russian.txt
|
||||
Serbian - Latin (Srpski (latinica))=Serbian - Latin.txt
|
||||
Serbian Cyrillic (Српски (ћирилица))=Serbian Cyrillic.txt
|
||||
Spanish (Español)=Spanish.txt
|
||||
Traditional Chinese - Taiwan (繁體中文(台灣))=Traditional Chinese - Taiwan.txt
|
||||
Turkish(Türkçe)=Turkish.txt
|
||||
Ukrainian(Українська)=Ukrainian.txt
|
||||
Vietnamese=Vietnamese.txt
|
|
@ -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,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net5.0;</TargetFrameworks>
|
||||
<Platforms>AnyCPU;x86;x64</Platforms>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net5.0|x86'">
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</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>
|
||||
|
||||
</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,39 @@
|
|||
#if NET35
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace NetFX.Polyfill2
|
||||
{
|
||||
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,40 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net3.5;net4.7.2;net6.0</TargetFrameworks>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x86;x64</Platforms>
|
||||
<AssemblyName>xdm-messaging-host</AssemblyName>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>Link</TrimMode>
|
||||
<!--<Platforms>AnyCPU;x86</Platforms>-->
|
||||
<!--<RuntimeIdentifiers>win-x86;linux-x64</RuntimeIdentifiers>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<!--<ItemGroup Condition=" '$(TargetFramework)' != 'net5.0' or '$(TargetFramework)' != 'net6.0' ">
|
||||
<ProjectReference Include="..\XDM.Compatibility\XDM.Compatibility.csproj" />
|
||||
</ItemGroup>-->
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net6.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>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net4.7.2|x86'">
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\XDM.Messaging\XDM.Messaging.projitems" Label="Shared" />
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,304 @@
|
|||
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 Newtonsoft.Json.Serialization;
|
||||
using XDM.Core.BrowserMonitoring;
|
||||
|
||||
#if NET35
|
||||
using NetFX.Polyfill2;
|
||||
#else
|
||||
using System.Collections.Concurrent;
|
||||
#endif
|
||||
|
||||
namespace NativeHost
|
||||
{
|
||||
public class NativeMessagingHostApp
|
||||
{
|
||||
static bool isFirefox = true;
|
||||
static BlockingCollection<byte[]> receivedBrowserMessages = new();
|
||||
static BlockingCollection<byte[]> queuedBrowserMessages = new();
|
||||
static CamelCasePropertyNamesContractResolver cr = new();
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var debugMode = Environment.GetEnvironmentVariable("XDM_DEBUG_MODE");
|
||||
if (!string.IsNullOrEmpty(debugMode) && debugMode == "1")
|
||||
{
|
||||
var logFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "messaging-log.txt");
|
||||
Trace.Listeners.Add(new TextWriterTraceListener(logFile, "myListener"));
|
||||
Trace.AutoFlush = true;
|
||||
}
|
||||
Debug("Application_Startup");
|
||||
if (args.Length > 0 && args[0].StartsWith("chrome-extension:"))
|
||||
{
|
||||
isFirefox = false;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
Debug("Process running from: " + AppDomain.CurrentDomain.BaseDirectory);
|
||||
#if !NET35
|
||||
Debug("Is64BitProcess: " + Environment.Is64BitProcess);
|
||||
#endif
|
||||
try
|
||||
{
|
||||
var inputReader = Console.OpenStandardInput();
|
||||
var outputWriter = Console.OpenStandardOutput();
|
||||
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();
|
||||
}
|
||||
|
||||
var t1 = new Thread(() =>
|
||||
{
|
||||
Debug("t1 reading messages from stdin sent by browser: ");
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
//read from process stdin and write to blocking queue,
|
||||
//they will be sent to xdm once pipe handshake complets
|
||||
Debug("Waiting for message - stdin...");
|
||||
var msg = NativeMessageSerializer.ReadMessageBytes(inputReader, false);
|
||||
Debug("Reading message from stdin - size: " + msg.Length);
|
||||
Debug("Stdin - " + 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();
|
||||
Debug("Sending to browser: " + Encoding.UTF8.GetString(msg));
|
||||
var json = BinaryToJson(msg);
|
||||
Debug("Sending to browser: ");
|
||||
Debug(Encoding.UTF8.GetString(json));
|
||||
NativeMessageSerializer.WriteMessage(outputWriter, json, false);
|
||||
}
|
||||
}
|
||||
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(bool minimized = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = Path.Combine(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".."),
|
||||
Environment.OSVersion.Platform == PlatformID.Win32NT ? "xdm-app.exe" : "xdm-app");
|
||||
Debug("XDM instance creating...1 " + file);
|
||||
if (isFirefox && Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||
{
|
||||
var args = minimized ? " -m" : "";
|
||||
if (!NativeProcess.Win32CreateProcess(file, $"\"{file}\"{args}"))
|
||||
{
|
||||
Debug("Win32 create process failed!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessStartInfo psi = new()
|
||||
{
|
||||
FileName = file,
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
if (minimized)
|
||||
{
|
||||
psi.Arguments = "-m";
|
||||
}
|
||||
|
||||
Debug("XDM instance creating...");
|
||||
Process.Start(psi);
|
||||
Debug("XDM instance created");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessMessages()
|
||||
{
|
||||
Debug("start");
|
||||
|
||||
try
|
||||
{
|
||||
NamedPipeClientStream? pipe = null;
|
||||
{
|
||||
try
|
||||
{
|
||||
//start handshake with XDM
|
||||
pipe = new NamedPipeClientStream(".", "XDM_Ipc_Browser_Monitoring_Pipe", PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
Debug("start handshake with XDM");
|
||||
pipe.Connect();
|
||||
|
||||
//handshake with XDM is complete
|
||||
Debug("handshake with XDM is complete");
|
||||
|
||||
using var waitHandle = new ManualResetEvent(false);
|
||||
|
||||
//Direction: XDM ---> NativeHost
|
||||
//Read messages from XDM's named pipe and add them to queuedBrowserMessages
|
||||
var task1 = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var syncMsgBytes = NativeMessageSerializer.ReadMessageBytes(pipe);
|
||||
Debug("Message received from XDM of size: " + syncMsgBytes.Length);
|
||||
if (syncMsgBytes.Length == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
Debug("Message from XDM: " + Encoding.UTF8.GetString(syncMsgBytes));
|
||||
queuedBrowserMessages.Add(syncMsgBytes);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug(ex.ToString(), ex);
|
||||
queuedBrowserMessages.Add(Encoding.UTF8.GetBytes("{\"appExited\":\"true\"}"));
|
||||
}
|
||||
waitHandle.Set();
|
||||
Debug("Task1 finished");
|
||||
}
|
||||
);
|
||||
|
||||
//Direction: NativeHost ---> XDM
|
||||
//Take messages from receivedBrowserMessages and write them to XDM's named pipe
|
||||
var task2 = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Debug("Task2 reading messages queued by browser...");
|
||||
byte[] 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("Sending message to XDM...");
|
||||
NativeMessageSerializer.WriteMessage(pipe, syncMsgBytes);
|
||||
Debug("Sent message to XDM");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug(ex.ToString(), ex);
|
||||
}
|
||||
waitHandle.Set();
|
||||
Debug("Task2 finished");
|
||||
}
|
||||
);
|
||||
|
||||
task1.Start();
|
||||
task2.Start();
|
||||
|
||||
waitHandle.WaitOne();
|
||||
Debug("Any one task finished");
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug(ex.ToString(), ex);
|
||||
}
|
||||
try
|
||||
{
|
||||
pipe?.Close();
|
||||
}
|
||||
catch { }
|
||||
try
|
||||
{
|
||||
pipe?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
catch (Exception exxxx)
|
||||
{
|
||||
Debug(exxxx.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void Debug(string msg, Exception? ex2 = null)
|
||||
{
|
||||
Trace.WriteLine($"[xdm-native-messaging-host {DateTime.Now}] {msg}");
|
||||
if (ex2 != null)
|
||||
{
|
||||
Trace.WriteLine($"[xdm-native-messaging-host {DateTime.Now}] {ex2}");
|
||||
}
|
||||
}
|
||||
|
||||
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,82 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace NativeHost
|
||||
{
|
||||
internal static class NativeProcess
|
||||
{
|
||||
internal static bool Win32CreateProcess(string? file, string? args)
|
||||
{
|
||||
const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
|
||||
const uint CREATE_BREAKAWAY_FROM_JOB = 0x01000000;
|
||||
const uint CREATE_NEW_PROCESS_GROUP = 0x00000200;
|
||||
PROCESS_INFORMATION pInfo = new PROCESS_INFORMATION();
|
||||
STARTUPINFO sInfo = new STARTUPINFO();
|
||||
SECURITY_ATTRIBUTES pSec = new SECURITY_ATTRIBUTES();
|
||||
SECURITY_ATTRIBUTES tSec = new SECURITY_ATTRIBUTES();
|
||||
pSec.nLength = Marshal.SizeOf(pSec);
|
||||
tSec.nLength = Marshal.SizeOf(tSec);
|
||||
|
||||
return CreateProcess(file, args,
|
||||
ref pSec, ref tSec, false, CREATE_UNICODE_ENVIRONMENT | CREATE_BREAKAWAY_FROM_JOB | CREATE_NEW_PROCESS_GROUP,
|
||||
IntPtr.Zero, null, ref sInfo, out pInfo);
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
internal static extern bool CreateProcess(
|
||||
string? lpApplicationName,
|
||||
string? lpCommandLine,
|
||||
ref SECURITY_ATTRIBUTES lpProcessAttributes,
|
||||
ref SECURITY_ATTRIBUTES lpThreadAttributes,
|
||||
bool bInheritHandles,
|
||||
uint dwCreationFlags,
|
||||
IntPtr lpEnvironment,
|
||||
string? lpCurrentDirectory,
|
||||
[In] ref STARTUPINFO lpStartupInfo,
|
||||
out PROCESS_INFORMATION lpProcessInformation);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct SECURITY_ATTRIBUTES
|
||||
{
|
||||
public int nLength;
|
||||
public IntPtr lpSecurityDescriptor;
|
||||
public int bInheritHandle;
|
||||
}
|
||||
|
||||
// This also works with CharSet.Ansi as long as the calling function uses the same character set.
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
internal struct STARTUPINFO
|
||||
{
|
||||
public Int32 cb;
|
||||
public string lpReserved;
|
||||
public string lpDesktop;
|
||||
public string lpTitle;
|
||||
public Int32 dwX;
|
||||
public Int32 dwY;
|
||||
public Int32 dwXSize;
|
||||
public Int32 dwYSize;
|
||||
public Int32 dwXCountChars;
|
||||
public Int32 dwYCountChars;
|
||||
public Int32 dwFillAttribute;
|
||||
public Int32 dwFlags;
|
||||
public Int16 wShowWindow;
|
||||
public Int16 cbReserved2;
|
||||
public IntPtr lpReserved2;
|
||||
public IntPtr hStdInput;
|
||||
public IntPtr hStdOutput;
|
||||
public IntPtr hStdError;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct PROCESS_INFORMATION
|
||||
{
|
||||
public IntPtr hProcess;
|
||||
public IntPtr hThread;
|
||||
public int dwProcessId;
|
||||
public int dwThreadId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Language files are stored in $(SOLUTION)\Lang directory
|
|
@ -0,0 +1,55 @@
|
|||
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.txt");
|
||||
}
|
||||
|
||||
public static void Load(string language)
|
||||
{
|
||||
var file = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.Combine("Lang", $"{language}"));
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var index = line.IndexOf('=');
|
||||
var key = line.Substring(0, index);
|
||||
var val = line.Substring(index + 1);
|
||||
texts[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetKeys()
|
||||
{
|
||||
return texts.Keys;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net3.5;net4.5;net4.7.2;net5.0</TargetFrameworks>
|
||||
<Platforms>AnyCPU;x86;x64</Platforms>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net4.7.2|x86'">
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,39 @@
|
|||
#if NET35
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace XDM.Compatibility
|
||||
{
|
||||
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,27 @@
|
|||
#if !NET5_0_OR_GREATER
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XDM.Compatibility
|
||||
{
|
||||
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 XDM.Compatibility
|
||||
{
|
||||
public static class GroupCollectionExtension
|
||||
{
|
||||
public static bool ContainsKey(this GroupCollection collection, string key)
|
||||
{
|
||||
return collection[key].Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace XDM.Compatibility
|
||||
{
|
||||
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 XDM.Compatibility
|
||||
{
|
||||
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,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' < '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||
<HasSharedItems>true</HasSharedItems>
|
||||
<SharedGUID>6bffa6bc-bf01-450e-96b7-9a1d3dcc2333</SharedGUID>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<Import_RootNamespace>XDM.Compatibility</Import_RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)BlockingCollection.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)DictionaryExtensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)GroupCollectionExtension.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ProcessStartInfoHelper.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)StreamExtension.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)TupleElementNamesAttribute.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ValueTuplePolyfill.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>6bffa6bc-bf01-450e-96b7-9a1d3dcc2333</ProjectGuid>
|
||||
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
|
||||
<PropertyGroup />
|
||||
<Import Project="XDM.Compatibility.projitems" Label="Shared" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
|
||||
</Project>
|
|
@ -0,0 +1,817 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TraceLog;
|
||||
using Translations;
|
||||
using XDM.Core.UI;
|
||||
using XDM.Core;
|
||||
using XDM.Core.DataAccess;
|
||||
using XDM.Core.Downloader;
|
||||
using XDM.Core.Util;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public class UIService : IAppController
|
||||
{
|
||||
private IMainView peer;
|
||||
private IAppService app;
|
||||
private delegate void UpdateItemCallBack(string id, string targetFileName, long size);
|
||||
private Action<string, int, double, long> updateProgressAction;
|
||||
private long lastProgressUpdate = 0;
|
||||
public event EventHandler WindowLoaded;
|
||||
|
||||
public UIService(IMainView peer, IAppService app)
|
||||
{
|
||||
this.peer = peer;
|
||||
this.app = app;
|
||||
this.updateProgressAction = new Action<string, int, double, long>(this.UpdateProgressOnUI);
|
||||
|
||||
AttachedEventHandler();
|
||||
|
||||
this.LoadDownloadList();
|
||||
|
||||
UpdateToolbarButtonState();
|
||||
}
|
||||
|
||||
public IAppService App { get => app; set => app = value; }
|
||||
|
||||
public void AddItemToTop(
|
||||
string id,
|
||||
string targetFileName,
|
||||
DateTime date,
|
||||
long fileSize,
|
||||
string type,
|
||||
FileNameFetchMode fileNameFetchMode,
|
||||
string primaryUrl,
|
||||
DownloadStartType startType,
|
||||
AuthenticationInfo? authentication,
|
||||
ProxyInfo? proxyInfo,
|
||||
int maxSpeedLimit)
|
||||
{
|
||||
var downloadEntry = new InProgressDownloadEntry
|
||||
{
|
||||
Name = targetFileName,
|
||||
DateAdded = date,
|
||||
DownloadType = type,
|
||||
Id = id,
|
||||
Progress = 0,
|
||||
Size = fileSize,
|
||||
Status = startType == DownloadStartType.Waiting ? DownloadStatus.Waiting : DownloadStatus.Stopped,
|
||||
TargetDir = "",
|
||||
PrimaryUrl = primaryUrl,
|
||||
Authentication = authentication,
|
||||
Proxy = proxyInfo,
|
||||
MaxSpeedLimitInKiB = maxSpeedLimit,
|
||||
};
|
||||
AppDB.Instance.Downloads.AddNewDownload(downloadEntry);
|
||||
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
//var downloadEntry = new InProgressDownloadEntry
|
||||
//{
|
||||
// Name = targetFileName,
|
||||
// DateAdded = date,
|
||||
// DownloadType = type,
|
||||
// Id = id,
|
||||
// Progress = 0,
|
||||
// Size = fileSize,
|
||||
// Status = startType == DownloadStartType.Waiting ? DownloadStatus.Waiting : DownloadStatus.Stopped,
|
||||
// TargetDir = "",
|
||||
// PrimaryUrl = primaryUrl,
|
||||
// Authentication = authentication,
|
||||
// Proxy = proxyInfo,
|
||||
// MaxSpeedLimitInKiB = maxSpeedLimit,
|
||||
//};
|
||||
|
||||
this.peer.AddToTop(downloadEntry);
|
||||
this.peer.SwitchToInProgressView();
|
||||
this.peer.ClearInProgressViewSelection();
|
||||
|
||||
//this.SaveInProgressList();
|
||||
UpdateToolbarButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
public bool Confirm(object? window, string text)
|
||||
{
|
||||
return peer.Confirm(window, text);
|
||||
}
|
||||
|
||||
public IDownloadCompleteDialog CreateDownloadCompleteDialog()
|
||||
{
|
||||
return peer.CreateDownloadCompleteDialog(this.app);
|
||||
}
|
||||
|
||||
public INewDownloadDialogSkeleton CreateNewDownloadDialog(bool empty)
|
||||
{
|
||||
return peer.CreateNewDownloadDialog(empty);
|
||||
}
|
||||
|
||||
public INewVideoDownloadDialog CreateNewVideoDialog()
|
||||
{
|
||||
return peer.CreateNewVideoDialog();
|
||||
}
|
||||
|
||||
public IProgressWindow CreateProgressWindow(string downloadId)
|
||||
{
|
||||
return peer.CreateProgressWindow(downloadId, this.app, this);
|
||||
}
|
||||
|
||||
public void DownloadCanelled(string id)
|
||||
{
|
||||
DownloadFailed(id);
|
||||
}
|
||||
|
||||
public void DownloadFailed(string id)
|
||||
{
|
||||
AppDB.Instance.Downloads.UpdateDownloadStatus(id, DownloadStatus.Stopped);
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
CallbackActions.DownloadFailed(id, peer);
|
||||
//SaveInProgressList();
|
||||
UpdateToolbarButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
public void DownloadFinished(string id, long finalFileSize, string filePath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
var name = Path.GetFileName(filePath);
|
||||
var folder = Path.GetDirectoryName(filePath);
|
||||
AppDB.Instance.Downloads.MarkAsFinished(id, finalFileSize, name, folder);
|
||||
}
|
||||
|
||||
Log.Debug("Final file name: " + filePath);
|
||||
var downloadEntry = AppDB.Instance.Downloads.GetDownloadById(id);
|
||||
if (downloadEntry != null)
|
||||
{
|
||||
var finishedEntry = new FinishedDownloadEntry
|
||||
{
|
||||
Name = Path.GetFileName(filePath),
|
||||
Id = downloadEntry.Id,
|
||||
DateAdded = downloadEntry.DateAdded,
|
||||
Size = downloadEntry.Size > 0 ? downloadEntry.Size : finalFileSize,
|
||||
DownloadType = downloadEntry.DownloadType,
|
||||
TargetDir = Path.GetDirectoryName(filePath)!,
|
||||
PrimaryUrl = downloadEntry.PrimaryUrl,
|
||||
Authentication = downloadEntry.Authentication,
|
||||
Proxy = downloadEntry.Proxy
|
||||
};
|
||||
AppDB.Instance.Downloads.UpdateDownloadEntry(finishedEntry);
|
||||
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
var download = peer.FindInProgressItem(id);
|
||||
if (download == null) return;
|
||||
|
||||
peer.AddToTop(finishedEntry);
|
||||
peer.Delete(download);
|
||||
|
||||
QueueManager.RemoveFinishedDownload(download.DownloadEntry.Id);
|
||||
|
||||
if (app.ActiveDownloadCount == 0 && peer.IsInProgressViewSelected)
|
||||
{
|
||||
Log.Debug("switching to finished listview");
|
||||
peer.SwitchToFinishedView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//var download = peer.FindInProgressItem(id);
|
||||
//if (download == null) return;
|
||||
|
||||
//peer.AddToTop(finishedEntry);
|
||||
//peer.Delete(download);
|
||||
|
||||
//QueueManager.RemoveFinishedDownload(download.DownloadEntry.Id);
|
||||
|
||||
//if (app.ActiveDownloadCount == 0 && peer.IsInProgressViewSelected)
|
||||
//{
|
||||
// Log.Debug("switching to finished listview");
|
||||
// peer.SwitchToFinishedView();
|
||||
//}
|
||||
|
||||
//RunOnUiThread(() =>
|
||||
//{
|
||||
// CallbackActions.DownloadFinished(id, finalFileSize, filePath, peer, app, () =>
|
||||
// {
|
||||
// UpdateToolbarButtonState();
|
||||
// QueueWindowManager.RefreshView();
|
||||
// });
|
||||
|
||||
// //this.SaveFinishedList();
|
||||
// //this.SaveInProgressList();
|
||||
// //UpdateToolbarButtonState();
|
||||
// //QueueWindowManager.RefreshView();
|
||||
//});
|
||||
}
|
||||
|
||||
public void DownloadStarted(string id)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
CallbackActions.DownloadStarted(id, peer);
|
||||
UpdateToolbarButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
public IEnumerable<InProgressDownloadEntry> GetAllInProgressDownloads()
|
||||
{
|
||||
//var downloads = new List<InProgressDownloadEntry>();
|
||||
//if (!AppDB.Instance.DownloadsDB.LoadDownloads(out downloads, out _, QueryMode.InProgress))
|
||||
//{
|
||||
// Log.Debug("GetAllInProgressDownloads::failed");
|
||||
//}
|
||||
//return downloads;
|
||||
return peer.InProgressDownloads;
|
||||
}
|
||||
|
||||
public InProgressDownloadEntry? GetInProgressDownloadEntry(string downloadId)
|
||||
{
|
||||
return peer.FindInProgressItem(downloadId)?.DownloadEntry;
|
||||
}
|
||||
|
||||
public string? GetUrlFromClipboard()
|
||||
{
|
||||
var text = peer.GetUrlFromClipboard();
|
||||
if (Helpers.IsUriValid(text))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public AuthenticationInfo? PromtForCredentials(string message)
|
||||
{
|
||||
return peer.PromtForCredentials(message);
|
||||
}
|
||||
|
||||
public void RenameFileOnUI(string id, string folder, string file)
|
||||
{
|
||||
if (!AppDB.Instance.Downloads.UpdateNameAndFolder(id, file, folder))
|
||||
{
|
||||
Log.Debug("RenameFileOnUI::failed");
|
||||
}
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
var downloadEntry = this.peer.FindInProgressItem(id);
|
||||
if (downloadEntry == null) return;
|
||||
if (file != null)
|
||||
{
|
||||
downloadEntry.Name = file;
|
||||
}
|
||||
if (folder != null)
|
||||
{
|
||||
downloadEntry.DownloadEntry.TargetDir = folder;
|
||||
}
|
||||
//this.SaveInProgressList();
|
||||
});
|
||||
}
|
||||
|
||||
public void ResumeDownload(string downloadId)
|
||||
{
|
||||
var idDict = new Dictionary<string, BaseDownloadEntry>();
|
||||
var download = peer.FindInProgressItem(downloadId);
|
||||
if (download == null) return;
|
||||
idDict[download.DownloadEntry.Id] = download.DownloadEntry;
|
||||
App.ResumeDownload(idDict);
|
||||
}
|
||||
|
||||
public void RunOnUiThread(Action action)
|
||||
{
|
||||
peer.RunOnUIThread(action);
|
||||
}
|
||||
|
||||
public void SetDownloadStatusWaiting(string id)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
var download = this.peer.FindInProgressItem(id);
|
||||
if (download == null) return;
|
||||
download.Status = DownloadStatus.Waiting;
|
||||
UpdateToolbarButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowUpdateAvailableNotification()
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
peer.ShowUpdateAvailableNotification();
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowDownloadCompleteDialog(string file, string folder)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
DownloadCompleteDialogHelper.ShowDialog(this.App, CreateDownloadCompleteDialog(), file, folder);
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowMessageBox(object? window, string message)
|
||||
{
|
||||
peer.ShowMessageBox(window, message);
|
||||
}
|
||||
|
||||
public void ShowNewDownloadDialog(Message message)
|
||||
{
|
||||
var url = message.Url;
|
||||
if (NewDownloadPromptTracker.IsPromptAlreadyOpen(url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
peer.RunOnUIThread(() =>
|
||||
{
|
||||
NewDownloadPromptTracker.PromptOpen(url);
|
||||
NewDownloadDialogHelper.CreateAndShowDialog(this.App, this, this.CreateNewDownloadDialog(false), message,
|
||||
() => NewDownloadPromptTracker.PromptClosed(url));
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowVideoDownloadDialog(string videoId, string name, long size, string? contentType)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
NewVideoDownloadDialogHelper.ShowVideoDownloadDialog(this.App, this, this.CreateNewVideoDialog(),
|
||||
videoId, name, size, contentType);
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateItem(string id, string targetFileName, long size)
|
||||
{
|
||||
if (!AppDB.Instance.Downloads.UpdateNameAndSize(id, size, targetFileName))
|
||||
{
|
||||
Log.Debug("UpdateItem::failed");
|
||||
}
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
var download = peer.FindInProgressItem(id);
|
||||
if (download == null) return;
|
||||
download.Name = targetFileName;
|
||||
download.Size = size;
|
||||
//this.SaveInProgressList();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateProgressOnUI(string id, int progress, double speed, long eta)
|
||||
{
|
||||
var downloadEntry = peer.FindInProgressItem(id);
|
||||
if (downloadEntry != null)
|
||||
{
|
||||
downloadEntry.Progress = progress;
|
||||
downloadEntry.DownloadSpeed = Helpers.FormatSize(speed) + "/s";
|
||||
downloadEntry.ETA = Helpers.ToHMS(eta);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateProgress(string id, int progress, double speed, long eta)
|
||||
{
|
||||
if (!AppDB.Instance.Downloads.UpdateDownloadProgress(id, progress))
|
||||
{
|
||||
Log.Debug("UpdateProgress::failed");
|
||||
}
|
||||
peer.RunOnUIThread(this.updateProgressAction, id, progress, speed, eta);
|
||||
}
|
||||
|
||||
private void LoadDownloadList()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (AppDB.Instance.Downloads.LoadDownloads(out var inProgressDownloads, out var finishedDownloads))
|
||||
{
|
||||
peer.InProgressDownloads = inProgressDownloads;
|
||||
peer.FinishedDownloads = finishedDownloads;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Could not load download list");
|
||||
}
|
||||
//peer.InProgressDownloads = TransactedIO.ReadInProgressList("inprogress-downloads.dat", Config.DataDir);
|
||||
//peer.FinishedDownloads = TransactedIO.ReadFinishedList("finished-downloads.dat", Config.DataDir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "LoadDownloadList");
|
||||
}
|
||||
}
|
||||
|
||||
//private void SaveInProgressList()
|
||||
//{
|
||||
// lock (this)
|
||||
// {
|
||||
// TransactedIO.WriteInProgressList(peer.InProgressDownloads, "inprogress-downloads.dat", Config.DataDir);
|
||||
// }
|
||||
//}
|
||||
|
||||
//private void SaveFinishedList()
|
||||
//{
|
||||
// lock (this)
|
||||
// {
|
||||
// TransactedIO.WriteFinishedList(peer.FinishedDownloads, "finished-downloads.dat", Config.DataDir);
|
||||
// }
|
||||
//}
|
||||
|
||||
private void DisableButton(IButton button)
|
||||
{
|
||||
button.Enable = false;
|
||||
}
|
||||
|
||||
private void EnableButton(IButton button)
|
||||
{
|
||||
button.Enable = true;
|
||||
}
|
||||
|
||||
private void UpdateToolbarButtonState()
|
||||
{
|
||||
DisableButton(peer.OpenFileButton);
|
||||
DisableButton(peer.OpenFolderButton);
|
||||
DisableButton(peer.PauseButton);
|
||||
DisableButton(peer.ResumeButton);
|
||||
DisableButton(peer.DeleteButton);
|
||||
|
||||
if (peer.IsInProgressViewSelected)
|
||||
{
|
||||
peer.OpenFileButton.Visible = peer.OpenFolderButton.Visible = false;
|
||||
peer.PauseButton.Visible = peer.ResumeButton.Visible = true;
|
||||
var selectedRows = peer.SelectedInProgressRows;
|
||||
if (selectedRows.Count > 0)
|
||||
{
|
||||
EnableButton(peer.DeleteButton);
|
||||
}
|
||||
if (selectedRows.Count > 1)
|
||||
{
|
||||
EnableButton(peer.ResumeButton);
|
||||
EnableButton(peer.PauseButton);
|
||||
}
|
||||
else if (selectedRows.Count == 1)
|
||||
{
|
||||
var ent = selectedRows[0];
|
||||
var isActive = App.IsDownloadActive(ent.DownloadEntry.Id);
|
||||
if (isActive)
|
||||
{
|
||||
EnableButton(peer.PauseButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnableButton(peer.ResumeButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
peer.OpenFileButton.Visible = peer.OpenFolderButton.Visible = true;
|
||||
peer.PauseButton.Visible = peer.ResumeButton.Visible = false;
|
||||
if (peer.SelectedFinishedRows.Count > 0)
|
||||
{
|
||||
EnableButton(peer.DeleteButton);
|
||||
}
|
||||
|
||||
if (peer.SelectedFinishedRows.Count == 1)
|
||||
{
|
||||
EnableButton(peer.OpenFileButton);
|
||||
EnableButton(peer.OpenFolderButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteDownloads()
|
||||
{
|
||||
UIActions.DeleteDownloads(peer.IsInProgressViewSelected,
|
||||
peer, App, null);
|
||||
//inProgress =>
|
||||
//{
|
||||
// if (inProgress)
|
||||
// {
|
||||
// SaveInProgressList();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// SaveFinishedList();
|
||||
// }
|
||||
//});
|
||||
}
|
||||
|
||||
private void AttachedEventHandler()
|
||||
{
|
||||
peer.NewDownloadClicked += (s, e) =>
|
||||
{
|
||||
peer.RunOnUIThread(() =>
|
||||
{
|
||||
NewDownloadDialogHelper.CreateAndShowDialog(this.App, this, CreateNewDownloadDialog(true));
|
||||
});
|
||||
};
|
||||
|
||||
peer.YoutubeDLDownloadClicked += (s, e) =>
|
||||
{
|
||||
peer.ShowYoutubeDLDialog(this, app);
|
||||
};
|
||||
|
||||
peer.BatchDownloadClicked += (s, e) =>
|
||||
{
|
||||
peer.ShowBatchDownloadWindow(app, this);
|
||||
};
|
||||
|
||||
peer.SelectionChanged += (s, e) =>
|
||||
{
|
||||
UpdateToolbarButtonState();
|
||||
};
|
||||
|
||||
peer.CategoryChanged += (s, e) =>
|
||||
{
|
||||
UpdateToolbarButtonState();
|
||||
};
|
||||
|
||||
peer.NewButton.Clicked += (s, e) =>
|
||||
{
|
||||
peer.OpenNewDownloadMenu();
|
||||
};
|
||||
|
||||
peer.DeleteButton.Clicked += (a, b) =>
|
||||
{
|
||||
DeleteDownloads();
|
||||
};
|
||||
|
||||
peer.DownloadListDoubleClicked += (a, b) => UIActions.OnDblClick(peer, App);
|
||||
|
||||
peer.OpenFolderButton.Clicked += (a, b) => UIActions.OpenSelectedFolder(peer);
|
||||
|
||||
peer.OpenFileButton.Clicked += (a, b) =>
|
||||
{
|
||||
UIActions.OpenSelectedFile(peer);
|
||||
};
|
||||
|
||||
peer.PauseButton.Clicked += (a, b) =>
|
||||
{
|
||||
if (peer.IsInProgressViewSelected)
|
||||
{
|
||||
UIActions.StopSelectedDownloads(peer, App);
|
||||
}
|
||||
};
|
||||
|
||||
peer.ResumeButton.Clicked += (a, b) =>
|
||||
{
|
||||
if (peer.IsInProgressViewSelected)
|
||||
{
|
||||
UIActions.ResumeDownloads(peer, App);
|
||||
}
|
||||
};
|
||||
|
||||
peer.SettingsClicked += (s, e) =>
|
||||
{
|
||||
peer.ShowSettingsDialog(app, 1);
|
||||
peer.UpdateParallalismLabel();
|
||||
};
|
||||
|
||||
peer.BrowserMonitoringSettingsClicked += (s, e) =>
|
||||
{
|
||||
peer.ShowBrowserMonitoringDialog(app);
|
||||
peer.UpdateParallalismLabel();
|
||||
};
|
||||
|
||||
peer.ClearAllFinishedClicked += (s, e) =>
|
||||
{
|
||||
peer.DeleteAllFinishedDownloads();
|
||||
AppDB.Instance.Downloads.RemoveAllFinished();
|
||||
//SaveFinishedList();
|
||||
};
|
||||
|
||||
peer.ImportClicked += (s, e) =>
|
||||
{
|
||||
var file = peer.OpenFileDialog(null, "zip", null);
|
||||
if (!string.IsNullOrEmpty(file) && File.Exists(file))
|
||||
{
|
||||
Log.Debug("Exporting to: " + file);
|
||||
app.Import(file!);
|
||||
}
|
||||
LoadDownloadList();
|
||||
};
|
||||
|
||||
peer.ExportClicked += (s, e) =>
|
||||
{
|
||||
var file = peer.SaveFileDialog("xdm-download-list.zip", "zip", "All files (*.*)|*.*");
|
||||
if (!string.IsNullOrEmpty(file))
|
||||
{
|
||||
Log.Debug("Exporting to: " + file);
|
||||
app.Export(file!);
|
||||
}
|
||||
};
|
||||
|
||||
peer.HelpClicked += (s, e) =>
|
||||
{
|
||||
Helpers.OpenBrowser(app.HelpPage);
|
||||
};
|
||||
|
||||
peer.UpdateClicked += (s, e) =>
|
||||
{
|
||||
if (App.IsAppUpdateAvailable)
|
||||
{
|
||||
Helpers.OpenBrowser(App.UpdatePage);
|
||||
return;
|
||||
}
|
||||
if (App.IsComponentUpdateAvailable)
|
||||
{
|
||||
if (peer.Confirm(peer, App.ComponentUpdateText))
|
||||
{
|
||||
LaunchUpdater(UpdateMode.FFmpegUpdateOnly | UpdateMode.YoutubeDLUpdateOnly);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
peer.ShowMessageBox(peer, TextResource.GetText("MSG_NO_UPDATE"));
|
||||
};
|
||||
|
||||
peer.BrowserMonitoringButtonClicked += (s, e) =>
|
||||
{
|
||||
if (Config.Instance.IsBrowserMonitoringEnabled)
|
||||
{
|
||||
Config.Instance.IsBrowserMonitoringEnabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Config.Instance.IsBrowserMonitoringEnabled = true;
|
||||
}
|
||||
Config.SaveConfig();
|
||||
app.ApplyConfig();
|
||||
peer.UpdateBrowserMonitorButton();
|
||||
};
|
||||
|
||||
peer.SupportPageClicked += (s, e) =>
|
||||
{
|
||||
Helpers.OpenBrowser("https://subhra74.github.io/xdm/redirect-support.html");
|
||||
};
|
||||
|
||||
peer.BugReportClicked += (s, e) =>
|
||||
{
|
||||
Helpers.OpenBrowser("https://subhra74.github.io/xdm/redirect-issue.html");
|
||||
};
|
||||
|
||||
peer.CheckForUpdateClicked += (s, e) =>
|
||||
{
|
||||
Helpers.OpenBrowser(app.UpdatePage);
|
||||
};
|
||||
|
||||
peer.SchedulerClicked += (s, e) =>
|
||||
{
|
||||
ShowQueueWindow(peer);
|
||||
};
|
||||
|
||||
peer.WindowCreated += (s, e) =>
|
||||
{
|
||||
this.WindowLoaded?.Invoke(this, EventArgs.Empty);
|
||||
};
|
||||
|
||||
AttachContextMenuEvents();
|
||||
|
||||
peer.InProgressContextMenuOpening += (_, _) => InProgressContextMenuOpening();
|
||||
peer.FinishedContextMenuOpening += (_, _) => FinishedContextMenuOpening();
|
||||
}
|
||||
|
||||
public void ShowQueueWindow(object window)
|
||||
{
|
||||
QueueWindowManager.ShowWindow(window, peer.CreateQueuesAndSchedulerWindow(this), this.app);
|
||||
}
|
||||
|
||||
private void LaunchUpdater(UpdateMode updateMode)
|
||||
{
|
||||
var updateDlg = peer.CreateUpdateUIDialog(this);
|
||||
var updates = App.Updates?.Where(u => u.IsExternal)?.ToList() ?? new List<UpdateInfo>(0);
|
||||
if (updates.Count == 0) return;
|
||||
var commonUpdateUi = new ComponentUpdaterUI(updateDlg, app, updateMode);
|
||||
updateDlg.Load += (_, _) => commonUpdateUi.StartUpdate();
|
||||
updateDlg.Finished += (_, _) =>
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
peer.ClearUpdateInformation();
|
||||
});
|
||||
};
|
||||
updateDlg.Show();
|
||||
}
|
||||
|
||||
private void AttachContextMenuEvents()
|
||||
{
|
||||
try
|
||||
{
|
||||
peer.MenuItemMap["pause"].Clicked += (_, _) => UIActions.StopSelectedDownloads(peer, App);
|
||||
peer.MenuItemMap["resume"].Clicked += (_, _) => UIActions.ResumeDownloads(peer, App);
|
||||
peer.MenuItemMap["delete"].Clicked += (_, _) => DeleteDownloads();
|
||||
peer.MenuItemMap["saveAs"].Clicked += (_, _) => UIActions.SaveAs(peer, App);
|
||||
peer.MenuItemMap["refresh"].Clicked += (_, _) => UIActions.RefreshLink(peer, App);
|
||||
peer.MenuItemMap["moveToQueue"].Clicked += (_, _) => UIActions.MoveToQueue(peer, this);
|
||||
peer.MenuItemMap["showProgress"].Clicked += (_, _) => UIActions.ShowProgressWindow(peer, App);
|
||||
peer.MenuItemMap["copyURL"].Clicked += (_, _) => UIActions.CopyURL1(peer, App);
|
||||
peer.MenuItemMap["copyURL1"].Clicked += (_, _) => UIActions.CopyURL2(peer, App);
|
||||
peer.MenuItemMap["properties"].Clicked += (_, _) => UIActions.ShowSeletectedItemProperties(peer, App);
|
||||
peer.MenuItemMap["open"].Clicked += (_, _) => UIActions.OpenSelectedFile(peer);
|
||||
peer.MenuItemMap["openFolder"].Clicked += (_, _) => UIActions.OpenSelectedFolder(peer);
|
||||
peer.MenuItemMap["deleteDownloads"].Clicked += (_, _) => DeleteDownloads();
|
||||
peer.MenuItemMap["copyFile"].Clicked += (_, _) => UIActions.CopyFile(peer);
|
||||
peer.MenuItemMap["properties1"].Clicked += (_, _) => UIActions.ShowSeletectedItemProperties(peer, App);
|
||||
peer.MenuItemMap["downloadAgain"].Clicked += (_, _) => UIActions.RestartDownload(peer, App);
|
||||
peer.MenuItemMap["restart"].Clicked += (_, _) => UIActions.RestartDownload(peer, App);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void InProgressContextMenuOpening()
|
||||
{
|
||||
foreach (var menu in peer.MenuItems)
|
||||
{
|
||||
menu.Enabled = false;
|
||||
}
|
||||
peer.MenuItemMap["delete"].Enabled = true;
|
||||
peer.MenuItemMap["schedule"].Enabled = true;
|
||||
peer.MenuItemMap["moveToQueue"].Enabled = true;
|
||||
var selectedRows = peer.SelectedInProgressRows;
|
||||
if (selectedRows.Count > 1)
|
||||
{
|
||||
peer.MenuItemMap["pause"].Enabled = true;
|
||||
peer.MenuItemMap["resume"].Enabled = true;
|
||||
peer.MenuItemMap["showProgress"].Enabled = true;
|
||||
}
|
||||
else if (selectedRows.Count == 1)
|
||||
{
|
||||
peer.MenuItemMap["showProgress"].Enabled = true;
|
||||
peer.MenuItemMap["copyURL"].Enabled = true;
|
||||
peer.MenuItemMap["saveAs"].Enabled = true;
|
||||
peer.MenuItemMap["refresh"].Enabled = true;
|
||||
peer.MenuItemMap["properties"].Enabled = true;
|
||||
peer.MenuItemMap["saveAs"].Enabled = true;
|
||||
peer.MenuItemMap["saveAs"].Enabled = true;
|
||||
peer.MenuItemMap["copyURL"].Enabled = true;
|
||||
|
||||
var ent = selectedRows[0].DownloadEntry;//selectedRows[0].Cells[1].Value as InProgressDownloadEntry;
|
||||
if (ent == null) return;
|
||||
var isActive = App.IsDownloadActive(ent.Id);
|
||||
Log.Debug("Selected item active: " + isActive);
|
||||
if (isActive)
|
||||
{
|
||||
peer.MenuItemMap["pause"].Enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
peer.MenuItemMap["resume"].Enabled = true;
|
||||
peer.MenuItemMap["restart"].Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FinishedContextMenuOpening()
|
||||
{
|
||||
foreach (var menu in peer.MenuItems)
|
||||
{
|
||||
menu.Enabled = false;
|
||||
}
|
||||
|
||||
peer.MenuItemMap["deleteDownloads"].Enabled = true;
|
||||
|
||||
var selectedRows = peer.SelectedFinishedRows;
|
||||
if (selectedRows.Count == 1)
|
||||
{
|
||||
foreach (var menu in peer.MenuItems)
|
||||
{
|
||||
menu.Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void InstallLatestFFmpeg()
|
||||
{
|
||||
LaunchUpdater(UpdateMode.FFmpegUpdateOnly);
|
||||
}
|
||||
|
||||
public void InstallLatestYoutubeDL()
|
||||
{
|
||||
LaunchUpdater(UpdateMode.YoutubeDLUpdateOnly);
|
||||
}
|
||||
|
||||
public void ShowDownloadSelectionWindow(FileNameFetchMode mode, IEnumerable<object> downloads)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
peer.ShowDownloadSelectionWindow(this.App, this, mode, downloads);
|
||||
});
|
||||
}
|
||||
|
||||
public IClipboardMonitor GetClipboardMonitor()
|
||||
{
|
||||
return peer.GetClipboardMonitor();
|
||||
}
|
||||
|
||||
public void ShowFloatingVideoWidget()
|
||||
{
|
||||
peer.ShowFloatingWidget();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using TraceLog;
|
||||
using Translations;
|
||||
using XDM.Core.Util;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public static class AppUpdater
|
||||
{
|
||||
private static Timer UpdateCheckTimer;
|
||||
public static IList<UpdateInfo>? Updates { get; private set; }
|
||||
public static bool ComponentsInstalled { get; private set; }
|
||||
public static bool IsAppUpdateAvailable => Updates?.Any(u => !u.IsExternal) ?? false;
|
||||
public static bool IsComponentUpdateAvailable => Updates?.Any(u => u.IsExternal) ?? false;
|
||||
public static string ComponentUpdateText => GetUpdateText();
|
||||
public static string UpdatePage => $"https://subhra74.github.io/xdm/update-checker.html?v={ApplicationContext.CoreService.AppVerion}";
|
||||
|
||||
public static void QueryNewVersion()
|
||||
{
|
||||
UpdateCheckTimer = new(
|
||||
callback: a => CheckForUpdate(),
|
||||
state: null,
|
||||
dueTime: TimeSpan.FromSeconds(5),
|
||||
period: TimeSpan.FromHours(3));
|
||||
}
|
||||
|
||||
private static void CheckForUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Debug("Checking for updates...");
|
||||
if (UpdateChecker.GetAppUpdates(ApplicationContext.CoreService.AppVerion, out IList<UpdateInfo> upd, out bool firstUpdate))
|
||||
{
|
||||
Updates = upd;
|
||||
ComponentsInstalled = !firstUpdate;
|
||||
ApplicationContext.Application.ShowUpdateAvailableNotification();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "CheckForUpdate");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetUpdateText()
|
||||
{
|
||||
if (Updates == null || Updates.Count < 1) return TextResource.GetText("MSG_NO_UPDATE");
|
||||
var text = new StringBuilder();
|
||||
var size = 0L;
|
||||
text.Append((ComponentsInstalled ? "Update available: " : "XDM require FFmpeg and YoutubeDL to download streaming videos") + Environment.NewLine);
|
||||
foreach (var update in Updates)
|
||||
{
|
||||
text.Append(update.Name + " " + update.TagName + Environment.NewLine);
|
||||
size += update.Size;
|
||||
}
|
||||
text.Append(Environment.NewLine + "Total download: " + FormattingHelper.FormatSize(size) + Environment.NewLine);
|
||||
text.Append("Would you like to continue?");
|
||||
return text.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,723 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TraceLog;
|
||||
using Translations;
|
||||
using XDM.Core.UI;
|
||||
using XDM.Core;
|
||||
using XDM.Core.DataAccess;
|
||||
using XDM.Core.Downloader;
|
||||
using XDM.Core.Util;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public class Application : IApplication
|
||||
{
|
||||
private delegate void UpdateItemCallBack(string id, string targetFileName, long size);
|
||||
private Action<string, int, double, long> updateProgressAction;
|
||||
public event EventHandler WindowLoaded;
|
||||
|
||||
public Application()
|
||||
{
|
||||
ApplicationContext.Initialized += AppInstance_Initialized;
|
||||
this.updateProgressAction = new Action<string, int, double, long>(this.UpdateProgressOnUI);
|
||||
}
|
||||
|
||||
private void AppInstance_Initialized(object? sender, EventArgs e)
|
||||
{
|
||||
AppDB.Instance.Init(Path.Combine(Config.DataDir, "downloads.db"));
|
||||
AttachedEventHandler();
|
||||
LoadDownloadList();
|
||||
UpdateToolbarButtonState();
|
||||
AppUpdater.QueryNewVersion();
|
||||
WindowLoaded += (_, _) => ApplicationContext.ClipboardMonitor.Start();
|
||||
}
|
||||
|
||||
public void AddItemToTop(
|
||||
string id,
|
||||
string targetFileName,
|
||||
DateTime date,
|
||||
long fileSize,
|
||||
string type,
|
||||
FileNameFetchMode fileNameFetchMode,
|
||||
string primaryUrl,
|
||||
DownloadStartType startType,
|
||||
AuthenticationInfo? authentication,
|
||||
ProxyInfo? proxyInfo)
|
||||
{
|
||||
var downloadEntry = new InProgressDownloadItem
|
||||
{
|
||||
Name = targetFileName,
|
||||
DateAdded = date,
|
||||
DownloadType = type,
|
||||
Id = id,
|
||||
Progress = 0,
|
||||
Size = fileSize,
|
||||
Status = startType == DownloadStartType.Waiting ? DownloadStatus.Waiting : DownloadStatus.Stopped,
|
||||
TargetDir = "",
|
||||
PrimaryUrl = primaryUrl,
|
||||
Authentication = authentication,
|
||||
Proxy = proxyInfo
|
||||
};
|
||||
AppDB.Instance.Downloads.AddNewDownload(downloadEntry);
|
||||
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
ApplicationContext.MainWindow.AddToTop(downloadEntry);
|
||||
ApplicationContext.MainWindow.SwitchToInProgressView();
|
||||
ApplicationContext.MainWindow.ClearInProgressViewSelection();
|
||||
UpdateToolbarButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
public bool Confirm(object? window, string text)
|
||||
{
|
||||
return ApplicationContext.MainWindow.Confirm(window, text);
|
||||
}
|
||||
|
||||
public IDownloadCompleteDialog CreateDownloadCompleteDialog()
|
||||
{
|
||||
return ApplicationContext.PlatformUIService.CreateDownloadCompleteDialog();
|
||||
}
|
||||
|
||||
public INewDownloadDialog CreateNewDownloadDialog(bool empty)
|
||||
{
|
||||
return ApplicationContext.PlatformUIService.CreateNewDownloadDialog(empty);
|
||||
}
|
||||
|
||||
public INewVideoDownloadDialog CreateNewVideoDialog()
|
||||
{
|
||||
return ApplicationContext.PlatformUIService.CreateNewVideoDialog();
|
||||
}
|
||||
|
||||
public IProgressWindow CreateProgressWindow(string downloadId)
|
||||
{
|
||||
return ApplicationContext.PlatformUIService.CreateProgressWindow(downloadId);
|
||||
}
|
||||
|
||||
public void DownloadCanelled(string id)
|
||||
{
|
||||
DownloadFailed(id);
|
||||
}
|
||||
|
||||
public void DownloadFailed(string id)
|
||||
{
|
||||
AppDB.Instance.Downloads.UpdateDownloadStatus(id, DownloadStatus.Stopped);
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
CallbackActions.DownloadFailed(id);
|
||||
UpdateToolbarButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
public void DownloadFinished(string id, long finalFileSize, string filePath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
var name = Path.GetFileName(filePath);
|
||||
var folder = Path.GetDirectoryName(filePath);
|
||||
AppDB.Instance.Downloads.MarkAsFinished(id, finalFileSize, name, folder);
|
||||
}
|
||||
|
||||
Log.Debug("Final file name: " + filePath);
|
||||
var downloadEntry = AppDB.Instance.Downloads.GetDownloadById(id);
|
||||
if (downloadEntry != null)
|
||||
{
|
||||
var finishedEntry = new FinishedDownloadItem
|
||||
{
|
||||
Name = Path.GetFileName(filePath),
|
||||
Id = downloadEntry.Id,
|
||||
DateAdded = downloadEntry.DateAdded,
|
||||
Size = downloadEntry.Size > 0 ? downloadEntry.Size : finalFileSize,
|
||||
DownloadType = downloadEntry.DownloadType,
|
||||
TargetDir = Path.GetDirectoryName(filePath)!,
|
||||
PrimaryUrl = downloadEntry.PrimaryUrl,
|
||||
Authentication = downloadEntry.Authentication,
|
||||
Proxy = downloadEntry.Proxy
|
||||
};
|
||||
AppDB.Instance.Downloads.UpdateDownloadEntry(finishedEntry);
|
||||
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
var download = ApplicationContext.MainWindow.FindInProgressItem(id);
|
||||
if (download == null) return;
|
||||
|
||||
ApplicationContext.MainWindow.AddToTop(finishedEntry);
|
||||
ApplicationContext.MainWindow.Delete(download);
|
||||
|
||||
QueueManager.RemoveFinishedDownload(download.DownloadEntry.Id);
|
||||
|
||||
if (ApplicationContext.CoreService.ActiveDownloadCount == 0 && ApplicationContext.MainWindow.IsInProgressViewSelected)
|
||||
{
|
||||
Log.Debug("switching to finished listview");
|
||||
ApplicationContext.MainWindow.SwitchToFinishedView();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void DownloadStarted(string id)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
CallbackActions.DownloadStarted(id);
|
||||
UpdateToolbarButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
public IEnumerable<InProgressDownloadItem> GetAllInProgressDownloads()
|
||||
{
|
||||
return ApplicationContext.MainWindow.InProgressDownloads;
|
||||
}
|
||||
|
||||
public InProgressDownloadItem? GetInProgressDownloadEntry(string downloadId)
|
||||
{
|
||||
return ApplicationContext.MainWindow.FindInProgressItem(downloadId)?.DownloadEntry;
|
||||
}
|
||||
|
||||
public string? GetUrlFromClipboard()
|
||||
{
|
||||
var text = ApplicationContext.MainWindow.GetUrlFromClipboard();
|
||||
if (Helpers.IsUriValid(text))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public AuthenticationInfo? PromtForCredentials(string message)
|
||||
{
|
||||
return ApplicationContext.PlatformUIService.PromtForCredentials(ApplicationContext.MainWindow, message);
|
||||
}
|
||||
|
||||
public void RenameFileOnUI(string id, string folder, string file)
|
||||
{
|
||||
if (!AppDB.Instance.Downloads.UpdateNameAndFolder(id, file, folder))
|
||||
{
|
||||
Log.Debug("RenameFileOnUI::failed");
|
||||
}
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
var downloadEntry = ApplicationContext.MainWindow.FindInProgressItem(id);
|
||||
if (downloadEntry == null) return;
|
||||
if (file != null)
|
||||
{
|
||||
downloadEntry.Name = file;
|
||||
}
|
||||
if (folder != null)
|
||||
{
|
||||
downloadEntry.DownloadEntry.TargetDir = folder;
|
||||
}
|
||||
//this.SaveInProgressList();
|
||||
});
|
||||
}
|
||||
|
||||
public void ResumeDownload(string downloadId)
|
||||
{
|
||||
var idDict = new Dictionary<string, DownloadItemBase>();
|
||||
var download = ApplicationContext.MainWindow.FindInProgressItem(downloadId);
|
||||
if (download == null) return;
|
||||
idDict[download.DownloadEntry.Id] = download.DownloadEntry;
|
||||
ApplicationContext.CoreService.ResumeDownload(idDict);
|
||||
}
|
||||
|
||||
public void RunOnUiThread(Action action)
|
||||
{
|
||||
ApplicationContext.MainWindow.RunOnUIThread(action);
|
||||
}
|
||||
|
||||
public void SetDownloadStatusWaiting(string id)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
var download = ApplicationContext.MainWindow.FindInProgressItem(id);
|
||||
if (download == null) return;
|
||||
download.Status = DownloadStatus.Waiting;
|
||||
UpdateToolbarButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowUpdateAvailableNotification()
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
ApplicationContext.MainWindow.ShowUpdateAvailableNotification();
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowDownloadCompleteDialog(string file, string folder)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
DownloadCompleteUIController.ShowDialog(CreateDownloadCompleteDialog(), file, folder);
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowMessageBox(object? window, string message)
|
||||
{
|
||||
ApplicationContext.PlatformUIService.ShowMessageBox(window, message);
|
||||
}
|
||||
|
||||
public void ShowNewDownloadDialog(Message message)
|
||||
{
|
||||
var url = message.Url;
|
||||
if (NewDownloadPromptTracker.IsPromptAlreadyOpen(url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
ApplicationContext.MainWindow.RunOnUIThread(() =>
|
||||
{
|
||||
NewDownloadPromptTracker.PromptOpen(url);
|
||||
NewDownloadDialogUIController.CreateAndShowDialog(this.CreateNewDownloadDialog(false), message,
|
||||
() => NewDownloadPromptTracker.PromptClosed(url));
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowVideoDownloadDialog(string videoId, string name, long size, string? contentType)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
NewVideoDownloadDialogUIController.ShowVideoDownloadDialog(this.CreateNewVideoDialog(),
|
||||
videoId, name, size, contentType);
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateItem(string id, string targetFileName, long size)
|
||||
{
|
||||
if (!AppDB.Instance.Downloads.UpdateNameAndSize(id, size, targetFileName))
|
||||
{
|
||||
Log.Debug("UpdateItem::failed");
|
||||
}
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
var download = ApplicationContext.MainWindow.FindInProgressItem(id);
|
||||
if (download == null) return;
|
||||
download.Name = targetFileName;
|
||||
download.Size = size;
|
||||
//this.SaveInProgressList();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateProgressOnUI(string id, int progress, double speed, long eta)
|
||||
{
|
||||
var downloadEntry = ApplicationContext.MainWindow.FindInProgressItem(id);
|
||||
if (downloadEntry != null)
|
||||
{
|
||||
downloadEntry.Progress = progress;
|
||||
downloadEntry.DownloadSpeed = FormattingHelper.FormatSize(speed) + "/s";
|
||||
downloadEntry.ETA = FormattingHelper.ToHMS(eta);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateProgress(string id, int progress, double speed, long eta)
|
||||
{
|
||||
if (!AppDB.Instance.Downloads.UpdateDownloadProgress(id, progress))
|
||||
{
|
||||
Log.Debug("UpdateProgress::failed");
|
||||
}
|
||||
ApplicationContext.MainWindow.RunOnUIThread(this.updateProgressAction, id, progress, speed, eta);
|
||||
}
|
||||
|
||||
private void LoadDownloadList()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (AppDB.Instance.Downloads.LoadDownloads(out var inProgressDownloads, out var finishedDownloads))
|
||||
{
|
||||
ApplicationContext.MainWindow.InProgressDownloads = inProgressDownloads;
|
||||
ApplicationContext.MainWindow.FinishedDownloads = finishedDownloads;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Could not load download list");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "LoadDownloadList");
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableButton(IButton button)
|
||||
{
|
||||
button.Enable = false;
|
||||
}
|
||||
|
||||
private void EnableButton(IButton button)
|
||||
{
|
||||
button.Enable = true;
|
||||
}
|
||||
|
||||
private void UpdateToolbarButtonState()
|
||||
{
|
||||
DisableButton(ApplicationContext.MainWindow.OpenFileButton);
|
||||
DisableButton(ApplicationContext.MainWindow.OpenFolderButton);
|
||||
DisableButton(ApplicationContext.MainWindow.PauseButton);
|
||||
DisableButton(ApplicationContext.MainWindow.ResumeButton);
|
||||
DisableButton(ApplicationContext.MainWindow.DeleteButton);
|
||||
|
||||
if (ApplicationContext.MainWindow.IsInProgressViewSelected)
|
||||
{
|
||||
ApplicationContext.MainWindow.OpenFileButton.Visible = ApplicationContext.MainWindow.OpenFolderButton.Visible = false;
|
||||
ApplicationContext.MainWindow.PauseButton.Visible = ApplicationContext.MainWindow.ResumeButton.Visible = true;
|
||||
var selectedRows = ApplicationContext.MainWindow.SelectedInProgressRows;
|
||||
if (selectedRows.Count > 0)
|
||||
{
|
||||
EnableButton(ApplicationContext.MainWindow.DeleteButton);
|
||||
}
|
||||
if (selectedRows.Count > 1)
|
||||
{
|
||||
EnableButton(ApplicationContext.MainWindow.ResumeButton);
|
||||
EnableButton(ApplicationContext.MainWindow.PauseButton);
|
||||
}
|
||||
else if (selectedRows.Count == 1)
|
||||
{
|
||||
var ent = selectedRows[0];
|
||||
var isActive = ApplicationContext.CoreService.IsDownloadActive(ent.DownloadEntry.Id);
|
||||
if (isActive)
|
||||
{
|
||||
EnableButton(ApplicationContext.MainWindow.PauseButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnableButton(ApplicationContext.MainWindow.ResumeButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplicationContext.MainWindow.OpenFileButton.Visible = ApplicationContext.MainWindow.OpenFolderButton.Visible = true;
|
||||
ApplicationContext.MainWindow.PauseButton.Visible = ApplicationContext.MainWindow.ResumeButton.Visible = false;
|
||||
if (ApplicationContext.MainWindow.SelectedFinishedRows.Count > 0)
|
||||
{
|
||||
EnableButton(ApplicationContext.MainWindow.DeleteButton);
|
||||
}
|
||||
|
||||
if (ApplicationContext.MainWindow.SelectedFinishedRows.Count == 1)
|
||||
{
|
||||
EnableButton(ApplicationContext.MainWindow.OpenFileButton);
|
||||
EnableButton(ApplicationContext.MainWindow.OpenFolderButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteDownloads()
|
||||
{
|
||||
UIActions.DeleteDownloads(ApplicationContext.MainWindow.IsInProgressViewSelected, null);
|
||||
}
|
||||
|
||||
private void AttachedEventHandler()
|
||||
{
|
||||
ApplicationContext.MainWindow.NewDownloadClicked += (s, e) =>
|
||||
{
|
||||
ApplicationContext.MainWindow.RunOnUIThread(() =>
|
||||
{
|
||||
NewDownloadDialogUIController.CreateAndShowDialog(CreateNewDownloadDialog(true));
|
||||
});
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.YoutubeDLDownloadClicked += (s, e) =>
|
||||
{
|
||||
ApplicationContext.PlatformUIService.ShowYoutubeDLDialog();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.BatchDownloadClicked += (s, e) =>
|
||||
{
|
||||
ApplicationContext.PlatformUIService.ShowBatchDownloadWindow();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.SelectionChanged += (s, e) =>
|
||||
{
|
||||
UpdateToolbarButtonState();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.CategoryChanged += (s, e) =>
|
||||
{
|
||||
UpdateToolbarButtonState();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.NewButton.Clicked += (s, e) =>
|
||||
{
|
||||
ApplicationContext.MainWindow.OpenNewDownloadMenu();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.DeleteButton.Clicked += (a, b) =>
|
||||
{
|
||||
DeleteDownloads();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.DownloadListDoubleClicked += (a, b) => UIActions.OnDblClick();
|
||||
|
||||
ApplicationContext.MainWindow.OpenFolderButton.Clicked += (a, b) => UIActions.OpenSelectedFolder();
|
||||
|
||||
ApplicationContext.MainWindow.OpenFileButton.Clicked += (a, b) =>
|
||||
{
|
||||
UIActions.OpenSelectedFile();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.PauseButton.Clicked += (a, b) =>
|
||||
{
|
||||
if (ApplicationContext.MainWindow.IsInProgressViewSelected)
|
||||
{
|
||||
UIActions.StopSelectedDownloads();
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.ResumeButton.Clicked += (a, b) =>
|
||||
{
|
||||
if (ApplicationContext.MainWindow.IsInProgressViewSelected)
|
||||
{
|
||||
UIActions.ResumeDownloads();
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.SettingsClicked += (s, e) =>
|
||||
{
|
||||
ApplicationContext.PlatformUIService.ShowSettingsDialog(1);
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.BrowserMonitoringSettingsClicked += (s, e) =>
|
||||
{
|
||||
ApplicationContext.PlatformUIService.ShowBrowserMonitoringDialog();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.ClearAllFinishedClicked += (s, e) =>
|
||||
{
|
||||
ApplicationContext.MainWindow.DeleteAllFinishedDownloads();
|
||||
AppDB.Instance.Downloads.RemoveAllFinished();
|
||||
//SaveFinishedList();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.ImportClicked += (s, e) =>
|
||||
{
|
||||
var file = ApplicationContext.PlatformUIService.OpenFileDialog(null, "zip", null);
|
||||
if (!string.IsNullOrEmpty(file) && File.Exists(file))
|
||||
{
|
||||
Log.Debug("Exporting to: " + file);
|
||||
ApplicationContext.CoreService.Import(file!);
|
||||
}
|
||||
LoadDownloadList();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.ExportClicked += (s, e) =>
|
||||
{
|
||||
var file = ApplicationContext.PlatformUIService.SaveFileDialog("xdm-download-list.zip", "zip", "All files (*.*)|*.*");
|
||||
if (!string.IsNullOrEmpty(file))
|
||||
{
|
||||
Log.Debug("Exporting to: " + file);
|
||||
ApplicationContext.CoreService.Export(file!);
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.HelpClicked += (s, e) =>
|
||||
{
|
||||
PlatformHelper.OpenBrowser(Links.SupportUrl);
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.UpdateClicked += (s, e) =>
|
||||
{
|
||||
if (AppUpdater.IsAppUpdateAvailable)
|
||||
{
|
||||
PlatformHelper.OpenBrowser(AppUpdater.UpdatePage);
|
||||
return;
|
||||
}
|
||||
if (AppUpdater.IsComponentUpdateAvailable)
|
||||
{
|
||||
if (ApplicationContext.MainWindow.Confirm(ApplicationContext.MainWindow, AppUpdater.ComponentUpdateText))
|
||||
{
|
||||
LaunchUpdater(UpdateMode.FFmpegUpdateOnly | UpdateMode.YoutubeDLUpdateOnly);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
ApplicationContext.PlatformUIService.ShowMessageBox(ApplicationContext.MainWindow, TextResource.GetText("MSG_NO_UPDATE"));
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.BrowserMonitoringButtonClicked += (s, e) =>
|
||||
{
|
||||
if (Config.Instance.IsBrowserMonitoringEnabled)
|
||||
{
|
||||
Config.Instance.IsBrowserMonitoringEnabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Config.Instance.IsBrowserMonitoringEnabled = true;
|
||||
}
|
||||
Config.SaveConfig();
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
ApplicationContext.MainWindow.UpdateBrowserMonitorButton();
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.SupportPageClicked += (s, e) =>
|
||||
{
|
||||
PlatformHelper.OpenBrowser(Links.SupportUrl);
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.BugReportClicked += (s, e) =>
|
||||
{
|
||||
PlatformHelper.OpenBrowser(Links.IssueUrl);
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.CheckForUpdateClicked += (s, e) =>
|
||||
{
|
||||
PlatformHelper.OpenBrowser(AppUpdater.UpdatePage);
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.SchedulerClicked += (s, e) =>
|
||||
{
|
||||
ShowQueueWindow(ApplicationContext.MainWindow);
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.WindowCreated += (s, e) =>
|
||||
{
|
||||
this.WindowLoaded?.Invoke(this, EventArgs.Empty);
|
||||
};
|
||||
|
||||
AttachContextMenuEvents();
|
||||
|
||||
ApplicationContext.MainWindow.InProgressContextMenuOpening += (_, _) => InProgressContextMenuOpening();
|
||||
ApplicationContext.MainWindow.FinishedContextMenuOpening += (_, _) => FinishedContextMenuOpening();
|
||||
}
|
||||
|
||||
public void ShowQueueWindow(object window)
|
||||
{
|
||||
QueueWindowManager.ShowWindow(window, ApplicationContext.PlatformUIService.CreateQueuesAndSchedulerWindow());
|
||||
}
|
||||
|
||||
private void LaunchUpdater(UpdateMode updateMode)
|
||||
{
|
||||
var updateDlg = ApplicationContext.PlatformUIService.CreateUpdateUIDialog();
|
||||
var updates = AppUpdater.Updates?.Where(u => u.IsExternal)?.ToList() ?? new List<UpdateInfo>(0);
|
||||
if (updates.Count == 0) return;
|
||||
var commonUpdateUi = new ComponentUpdaterUIController(updateDlg, updateMode);
|
||||
updateDlg.Load += (_, _) => commonUpdateUi.StartUpdate();
|
||||
updateDlg.Finished += (_, _) =>
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
ApplicationContext.MainWindow.ClearUpdateInformation();
|
||||
});
|
||||
};
|
||||
updateDlg.Show();
|
||||
}
|
||||
|
||||
private void AttachContextMenuEvents()
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplicationContext.MainWindow.MenuItemMap["pause"].Clicked += (_, _) => UIActions.StopSelectedDownloads();
|
||||
ApplicationContext.MainWindow.MenuItemMap["resume"].Clicked += (_, _) => UIActions.ResumeDownloads();
|
||||
ApplicationContext.MainWindow.MenuItemMap["delete"].Clicked += (_, _) => DeleteDownloads();
|
||||
ApplicationContext.MainWindow.MenuItemMap["saveAs"].Clicked += (_, _) => UIActions.SaveAs();
|
||||
ApplicationContext.MainWindow.MenuItemMap["refresh"].Clicked += (_, _) => UIActions.RefreshLink();
|
||||
ApplicationContext.MainWindow.MenuItemMap["moveToQueue"].Clicked += (_, _) => UIActions.MoveToQueue();
|
||||
ApplicationContext.MainWindow.MenuItemMap["showProgress"].Clicked += (_, _) => UIActions.ShowProgressWindow();
|
||||
ApplicationContext.MainWindow.MenuItemMap["copyURL"].Clicked += (_, _) => UIActions.CopyURL1();
|
||||
ApplicationContext.MainWindow.MenuItemMap["copyURL1"].Clicked += (_, _) => UIActions.CopyURL2();
|
||||
ApplicationContext.MainWindow.MenuItemMap["properties"].Clicked += (_, _) => UIActions.ShowSeletectedItemProperties();
|
||||
ApplicationContext.MainWindow.MenuItemMap["open"].Clicked += (_, _) => UIActions.OpenSelectedFile();
|
||||
ApplicationContext.MainWindow.MenuItemMap["openFolder"].Clicked += (_, _) => UIActions.OpenSelectedFolder();
|
||||
ApplicationContext.MainWindow.MenuItemMap["deleteDownloads"].Clicked += (_, _) => DeleteDownloads();
|
||||
ApplicationContext.MainWindow.MenuItemMap["copyFile"].Clicked += (_, _) => UIActions.CopyFile();
|
||||
ApplicationContext.MainWindow.MenuItemMap["properties1"].Clicked += (_, _) => UIActions.ShowSeletectedItemProperties();
|
||||
ApplicationContext.MainWindow.MenuItemMap["downloadAgain"].Clicked += (_, _) => UIActions.RestartDownload();
|
||||
ApplicationContext.MainWindow.MenuItemMap["restart"].Clicked += (_, _) => UIActions.RestartDownload();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void InProgressContextMenuOpening()
|
||||
{
|
||||
foreach (var menu in ApplicationContext.MainWindow.MenuItems)
|
||||
{
|
||||
menu.Enabled = false;
|
||||
}
|
||||
ApplicationContext.MainWindow.MenuItemMap["delete"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["schedule"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["moveToQueue"].Enabled = true;
|
||||
var selectedRows = ApplicationContext.MainWindow.SelectedInProgressRows;
|
||||
if (selectedRows.Count > 1)
|
||||
{
|
||||
ApplicationContext.MainWindow.MenuItemMap["pause"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["resume"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["showProgress"].Enabled = true;
|
||||
}
|
||||
else if (selectedRows.Count == 1)
|
||||
{
|
||||
ApplicationContext.MainWindow.MenuItemMap["showProgress"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["copyURL"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["saveAs"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["refresh"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["properties"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["saveAs"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["saveAs"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["copyURL"].Enabled = true;
|
||||
|
||||
var ent = selectedRows[0].DownloadEntry;//selectedRows[0].Cells[1].Value as InProgressDownloadEntry;
|
||||
if (ent == null) return;
|
||||
var isActive = ApplicationContext.CoreService.IsDownloadActive(ent.Id);
|
||||
Log.Debug("Selected item active: " + isActive);
|
||||
if (isActive)
|
||||
{
|
||||
ApplicationContext.MainWindow.MenuItemMap["pause"].Enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplicationContext.MainWindow.MenuItemMap["resume"].Enabled = true;
|
||||
ApplicationContext.MainWindow.MenuItemMap["restart"].Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FinishedContextMenuOpening()
|
||||
{
|
||||
foreach (var menu in ApplicationContext.MainWindow.MenuItems)
|
||||
{
|
||||
menu.Enabled = false;
|
||||
}
|
||||
|
||||
ApplicationContext.MainWindow.MenuItemMap["deleteDownloads"].Enabled = true;
|
||||
|
||||
var selectedRows = ApplicationContext.MainWindow.SelectedFinishedRows;
|
||||
if (selectedRows.Count == 1)
|
||||
{
|
||||
foreach (var menu in ApplicationContext.MainWindow.MenuItems)
|
||||
{
|
||||
menu.Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void InstallLatestFFmpeg()
|
||||
{
|
||||
LaunchUpdater(UpdateMode.FFmpegUpdateOnly);
|
||||
}
|
||||
|
||||
public void InstallLatestYoutubeDL()
|
||||
{
|
||||
LaunchUpdater(UpdateMode.YoutubeDLUpdateOnly);
|
||||
}
|
||||
|
||||
public void ShowDownloadSelectionWindow(FileNameFetchMode mode, IEnumerable<IRequestData> downloads)
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
ApplicationContext.PlatformUIService.ShowDownloadSelectionWindow(mode, downloads);
|
||||
});
|
||||
}
|
||||
|
||||
public IPlatformClipboardMonitor GetPlatformClipboardMonitor()
|
||||
{
|
||||
return ApplicationContext.MainWindow.GetClipboardMonitor();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using XDM.Core.BrowserMonitoring;
|
||||
using XDM.Core.UI;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public static class ApplicationContext
|
||||
{
|
||||
private static IApplicationCore? s_ApplicationCore;
|
||||
private static IApplication? s_IApplication;
|
||||
private static IApplicationWindow? s_ApplicationWindow;
|
||||
private static ILinkRefresher? s_LinkRefresher;
|
||||
private static IVideoTracker? s_VideoTracker;
|
||||
private static IClipboardMonitor? s_ClipboardMonitor;
|
||||
private static IPlatformUIService? s_PlatformUIService;
|
||||
private static bool s_Init = false;
|
||||
|
||||
public static event EventHandler? Initialized;
|
||||
public static event EventHandler<ApplicationEvent>? ApplicationEvent;
|
||||
|
||||
public static IApplicationCore CoreService
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_Init)
|
||||
{
|
||||
throw new Exception("ApplicationContext is not initialized...");
|
||||
}
|
||||
return s_ApplicationCore!;
|
||||
}
|
||||
}
|
||||
|
||||
public static IApplication Application
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_Init)
|
||||
{
|
||||
throw new Exception("ApplicationContext is not initialized...");
|
||||
}
|
||||
return s_IApplication!;
|
||||
}
|
||||
}
|
||||
|
||||
public static IApplicationWindow MainWindow
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_Init)
|
||||
{
|
||||
throw new Exception("ApplicationContext is not initialized...");
|
||||
}
|
||||
return s_ApplicationWindow!;
|
||||
}
|
||||
}
|
||||
|
||||
public static ILinkRefresher LinkRefresher
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_Init)
|
||||
{
|
||||
throw new Exception("ApplicationContext is not initialized...");
|
||||
}
|
||||
return s_LinkRefresher!;
|
||||
}
|
||||
}
|
||||
|
||||
public static IVideoTracker VideoTracker
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_Init)
|
||||
{
|
||||
throw new Exception("ApplicationContext is not initialized...");
|
||||
}
|
||||
return s_VideoTracker!;
|
||||
}
|
||||
}
|
||||
|
||||
public static IClipboardMonitor ClipboardMonitor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_Init)
|
||||
{
|
||||
throw new Exception("ApplicationContext is not initialized...");
|
||||
}
|
||||
return s_ClipboardMonitor!;
|
||||
}
|
||||
}
|
||||
|
||||
public static IPlatformUIService PlatformUIService
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_Init)
|
||||
{
|
||||
throw new Exception("ApplicationContext is not initialized...");
|
||||
}
|
||||
return s_PlatformUIService!;
|
||||
}
|
||||
}
|
||||
|
||||
public static void BroadcastConfigChange()
|
||||
{
|
||||
ApplicationEvent?.Invoke(null, new ApplicationEvent("ConfigChanged"));
|
||||
}
|
||||
|
||||
public static AppInstanceConfigurer Configurer()
|
||||
{
|
||||
return new AppInstanceConfigurer();
|
||||
}
|
||||
|
||||
public class AppInstanceConfigurer
|
||||
{
|
||||
public AppInstanceConfigurer RegisterApplicationCore(IApplicationCore service)
|
||||
{
|
||||
s_ApplicationCore = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppInstanceConfigurer RegisterApplication(IApplication service)
|
||||
{
|
||||
s_IApplication = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppInstanceConfigurer RegisterApplicationWindow(IApplicationWindow service)
|
||||
{
|
||||
s_ApplicationWindow = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppInstanceConfigurer RegisterLinkRefresher(ILinkRefresher service)
|
||||
{
|
||||
s_LinkRefresher = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppInstanceConfigurer RegisterCapturedVideoTracker(IVideoTracker service)
|
||||
{
|
||||
s_VideoTracker = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppInstanceConfigurer RegisterClipboardMonitor(IClipboardMonitor service)
|
||||
{
|
||||
s_ClipboardMonitor = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppInstanceConfigurer RegisterPlatformUIService(IPlatformUIService service)
|
||||
{
|
||||
s_PlatformUIService = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Configure()
|
||||
{
|
||||
if (s_ApplicationCore == null || s_IApplication == null
|
||||
|| s_ApplicationWindow == null || s_LinkRefresher == null
|
||||
|| s_VideoTracker == null || s_ClipboardMonitor == null
|
||||
|| s_PlatformUIService == null)
|
||||
{
|
||||
throw new Exception("Please configure all dependecies");
|
||||
}
|
||||
s_Init = true;
|
||||
Initialized?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ApplicationEvent : EventArgs
|
||||
{
|
||||
public ApplicationEvent(string eventType, object? data = null)
|
||||
{
|
||||
EventType = eventType;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public string EventType { get; }
|
||||
public object? Data { get; set; }
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,818 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using XDM.Core.UI;
|
||||
using XDM.Core;
|
||||
using XDM.Core.MediaProcessor;
|
||||
using XDM.Core.Util;
|
||||
using XDM.Core.BrowserMonitoring;
|
||||
using XDM.Core.Collections;
|
||||
using System.Timers;
|
||||
using TraceLog;
|
||||
using Translations;
|
||||
using XDM.Core.Downloader;
|
||||
using XDM.Core.Downloader.Progressive.DualHttp;
|
||||
using XDM.Core.Downloader.Progressive.SingleHttp;
|
||||
using XDM.Core.Downloader.Adaptive.Hls;
|
||||
using XDM.Core.Downloader.Adaptive.Dash;
|
||||
using XDM.Core.Downloader.Progressive;
|
||||
using XDM.Core.DataAccess;
|
||||
using XDM.Core.IO;
|
||||
|
||||
#if !NET5_0_OR_GREATER
|
||||
using XDM.Compatibility;
|
||||
#endif
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public class ApplicationCore : IApplicationCore
|
||||
{
|
||||
public Version AppVerion => new(8, 0, 0);
|
||||
private Dictionary<string, (IBaseDownloader Downloader, bool NonInteractive)> liveDownloads = new();
|
||||
private GenericOrderedDictionary<string, bool> queuedDownloads = new();
|
||||
private GenericOrderedDictionary<string, IProgressWindow> activeProgressWindows = new();
|
||||
private Scheduler scheduler;
|
||||
private Timer awakePingTimer;
|
||||
public int ActiveDownloadCount { get => liveDownloads.Count + queuedDownloads.Count; }
|
||||
|
||||
public ApplicationCore()
|
||||
{
|
||||
ApplicationContext.Initialized += AppInstance_Initialized;
|
||||
}
|
||||
|
||||
private void AppInstance_Initialized(object sender, EventArgs e)
|
||||
{
|
||||
awakePingTimer = new Timer(60000)
|
||||
{
|
||||
AutoReset = true
|
||||
};
|
||||
awakePingTimer.Elapsed += (a, b) => PlatformHelper.SendKeepAlivePing();
|
||||
|
||||
try
|
||||
{
|
||||
QueueManager.Load();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.ToString());
|
||||
}
|
||||
|
||||
StartScheduler();
|
||||
StartBrowserMonitoring();
|
||||
}
|
||||
|
||||
public void StartBrowserMonitoring()
|
||||
{
|
||||
BrowserMonitor.RunNativeHostHandler();
|
||||
BrowserMonitor.RunHttpIpcHandler();
|
||||
}
|
||||
|
||||
public string? StartDownload(
|
||||
IRequestData downloadInfo,
|
||||
string fileName,
|
||||
FileNameFetchMode fileNameFetchMode,
|
||||
string? targetFolder,
|
||||
bool startImmediately,
|
||||
AuthenticationInfo? authentication,
|
||||
ProxyInfo? proxyInfo,
|
||||
string? queueId,
|
||||
bool convertToMp3)
|
||||
{
|
||||
Log.Debug($"Starting download: {fileName} {fileNameFetchMode} {convertToMp3}");
|
||||
|
||||
IBaseDownloader? http;
|
||||
|
||||
switch (downloadInfo)
|
||||
{
|
||||
case SingleSourceHTTPDownloadInfo info:
|
||||
http = new SingleSourceHTTPDownloader(info, authentication: authentication,
|
||||
proxy: proxyInfo, mediaProcessor: new FFmpegMediaProcessor(),
|
||||
convertToMp3: convertToMp3);
|
||||
RequestDataIO.SaveDownloadInfo(http.Id!, info);
|
||||
break;
|
||||
case DualSourceHTTPDownloadInfo info:
|
||||
http = new DualSourceHTTPDownloader(info, authentication: authentication,
|
||||
proxy: proxyInfo, mediaProcessor: new FFmpegMediaProcessor());
|
||||
RequestDataIO.SaveDownloadInfo(http.Id!, info);
|
||||
break;
|
||||
case MultiSourceHLSDownloadInfo info:
|
||||
http = new MultiSourceHLSDownloader(info, authentication: authentication,
|
||||
proxy: proxyInfo, mediaProcessor: new FFmpegMediaProcessor());
|
||||
RequestDataIO.SaveDownloadInfo(http.Id!, info);
|
||||
break;
|
||||
case MultiSourceDASHDownloadInfo info:
|
||||
http = new MultiSourceDASHDownloader(info, authentication: authentication,
|
||||
proxy: proxyInfo, mediaProcessor: new FFmpegMediaProcessor());
|
||||
RequestDataIO.SaveDownloadInfo(http.Id!, info);
|
||||
break;
|
||||
default:
|
||||
Log.Debug("Unknow request info :: skipping download");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(queueId))
|
||||
{
|
||||
QueueManager.AddDownloadsToQueue(queueId!, new string[] { http.Id! });
|
||||
}
|
||||
http.SetFileName(FileHelper.SanitizeFileName(fileName), fileNameFetchMode);
|
||||
http.SetTargetDirectory(targetFolder);
|
||||
StartDownload(http, startImmediately, authentication, proxyInfo);
|
||||
return http.Id;
|
||||
}
|
||||
|
||||
private void StartDownload(IBaseDownloader download,
|
||||
bool startImmediately,
|
||||
AuthenticationInfo? authentication,
|
||||
ProxyInfo? proxyInfo)
|
||||
{
|
||||
if (!awakePingTimer.Enabled)
|
||||
{
|
||||
Log.Debug("Starting keep awaik timer");
|
||||
awakePingTimer.Start();
|
||||
}
|
||||
var id = download.Id;
|
||||
var startType = DownloadStartType.Waiting;
|
||||
|
||||
if (!startImmediately)
|
||||
{
|
||||
startType = DownloadStartType.Stopped;
|
||||
}
|
||||
else if (liveDownloads.Count >= Config.Instance.MaxParallelDownloads)
|
||||
{
|
||||
startImmediately = false;
|
||||
queuedDownloads.Add(id, false);
|
||||
}
|
||||
|
||||
ApplicationContext.Application.AddItemToTop(id, download.TargetFileName, DateTime.Now,
|
||||
download.FileSize, download.Type, download.FileNameFetchMode,
|
||||
download.PrimaryUrl?.ToString(), startType, authentication,
|
||||
proxyInfo);
|
||||
|
||||
if (startImmediately)
|
||||
{
|
||||
this.liveDownloads.Add(download.Id, (Downloader: download, NonInteractive: false));
|
||||
download.Started += HandleDownloadStart;
|
||||
download.Probed += HandleProbeResult;
|
||||
download.Finished += DownloadFinished;
|
||||
download.ProgressChanged += DownloadProgressChanged;
|
||||
download.AssembingProgressChanged += AssembleProgressChanged;
|
||||
download.Cancelled += DownloadCancelled;
|
||||
download.Failed += DownloadFailed;
|
||||
|
||||
var showProgress = Config.Instance.ShowProgressWindow;
|
||||
if (showProgress)
|
||||
{
|
||||
ApplicationContext.Application.RunOnUiThread(() =>
|
||||
{
|
||||
var prgWin = CreateProgressWindow(download);
|
||||
activeProgressWindows[download.Id] = prgWin;
|
||||
prgWin.FileNameText = download.TargetFileName;
|
||||
prgWin.FileSizeText = $"{TextResource.GetText("STAT_DOWNLOADING")} ...";
|
||||
prgWin.UrlText = download.PrimaryUrl?.ToString() ?? string.Empty;
|
||||
prgWin.ShowProgressWindow();
|
||||
});
|
||||
}
|
||||
|
||||
download.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
download.SaveForLater();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddBatchLinks(List<Message> messages)
|
||||
{
|
||||
var list = new List<IRequestData>(messages.Count);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
var url = message.Url;
|
||||
if (string.IsNullOrEmpty(url)) continue;
|
||||
var file = FileHelper.SanitizeFileName(message.File ?? FileHelper.GetFileName(new Uri(message.Url)));
|
||||
var si = new SingleSourceHTTPDownloadInfo
|
||||
{
|
||||
Uri = url,
|
||||
File = file,
|
||||
Headers = message?.RequestHeaders,
|
||||
Cookies = message?.Cookies
|
||||
};
|
||||
list.Add(si);
|
||||
}
|
||||
ApplicationContext.Application.ShowDownloadSelectionWindow(FileNameFetchMode.FileNameAndExtension, list);
|
||||
}
|
||||
|
||||
public void AddDownload(Message message)
|
||||
{
|
||||
if (ApplicationContext.LinkRefresher.LinkAccepted(message)) return;
|
||||
|
||||
if (Config.Instance.StartDownloadAutomatically)
|
||||
{
|
||||
var url = message.Url;
|
||||
var file = FileHelper.SanitizeFileName(message.File ?? FileHelper.GetFileName(new Uri(message.Url)));
|
||||
StartDownload(
|
||||
new SingleSourceHTTPDownloadInfo
|
||||
{
|
||||
Uri = url,
|
||||
File = file,
|
||||
Headers = message?.RequestHeaders,
|
||||
Cookies = message?.Cookies
|
||||
},
|
||||
file,
|
||||
FileNameFetchMode.FileNameAndExtension,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
Config.Instance.Proxy, null, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Adding download");
|
||||
ApplicationContext.Application.ShowNewDownloadDialog(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void ResumeNonInteractiveDownloads(IEnumerable<string> idList)
|
||||
{
|
||||
foreach (var id in idList)
|
||||
{
|
||||
var entry = AppDB.Instance.Downloads.GetDownloadById(id);// ApplicationContext.Current.GetInProgressDownloadEntry(id);
|
||||
if (entry != null)
|
||||
{
|
||||
ResumeDownload(new Dictionary<string, DownloadItemBase> { [id] = entry }, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ResumeDownload(Dictionary<string, DownloadItemBase> list,
|
||||
bool nonInteractive = false)
|
||||
{
|
||||
if (!awakePingTimer.Enabled)
|
||||
{
|
||||
Log.Debug("Starting keep awake timer");
|
||||
awakePingTimer.Start();
|
||||
}
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (liveDownloads.ContainsKey(item.Key) || queuedDownloads.ContainsKey(item.Key)) return;
|
||||
if (liveDownloads.Count >= Config.Instance.MaxParallelDownloads)
|
||||
{
|
||||
queuedDownloads.Add(item.Key, nonInteractive);
|
||||
ApplicationContext.Application.RunOnUiThread(() =>
|
||||
{
|
||||
ApplicationContext.Application.SetDownloadStatusWaiting(item.Key);
|
||||
Log.Debug("Setting status waiting...");
|
||||
});
|
||||
continue;
|
||||
}
|
||||
IBaseDownloader? download = null;
|
||||
switch (item.Value.DownloadType)
|
||||
{
|
||||
case "Http":
|
||||
download = new SingleSourceHTTPDownloader((string)item.Key,
|
||||
mediaProcessor: new FFmpegMediaProcessor());
|
||||
break;
|
||||
case "Dash":
|
||||
download = new DualSourceHTTPDownloader((string)item.Key,
|
||||
mediaProcessor: new FFmpegMediaProcessor());
|
||||
break;
|
||||
case "Hls":
|
||||
download = new MultiSourceHLSDownloader(item.Key,
|
||||
mediaProcessor: new FFmpegMediaProcessor());
|
||||
break;
|
||||
case "Mpd-Dash":
|
||||
download = new MultiSourceDASHDownloader(item.Key,
|
||||
mediaProcessor: new FFmpegMediaProcessor());
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
download.Started += HandleDownloadStart;
|
||||
download.Probed += HandleProbeResult;
|
||||
download.Finished += DownloadFinished;
|
||||
download.ProgressChanged += DownloadProgressChanged;
|
||||
download.AssembingProgressChanged += AssembleProgressChanged;
|
||||
download.Cancelled += DownloadCancelled;
|
||||
download.Failed += DownloadFailed;
|
||||
download.SetTargetDirectory(item.Value.TargetDir);
|
||||
download.SetFileName(item.Value.Name, item.Value.FileNameFetchMode);
|
||||
liveDownloads[item.Key] = (Downloader: download, NonInteractive: nonInteractive);
|
||||
|
||||
var showProgressWindow = Config.Instance.ShowProgressWindow;
|
||||
if (showProgressWindow && !nonInteractive)
|
||||
{
|
||||
var prgWin = GetProgressWindow(download);// CreateOrGetProgressWindow(download);
|
||||
ApplicationContext.Application.RunOnUiThread(() =>
|
||||
{
|
||||
if (prgWin == null)
|
||||
{
|
||||
prgWin = CreateProgressWindow(download);
|
||||
activeProgressWindows[download.Id] = prgWin;
|
||||
}
|
||||
prgWin.FileNameText = download.TargetFileName;
|
||||
prgWin.FileSizeText = $"{TextResource.GetText("STAT_DOWNLOADING")} ...";
|
||||
prgWin.DownloadStarted();
|
||||
prgWin.ShowProgressWindow();
|
||||
});
|
||||
}
|
||||
liveDownloads[item.Key].Downloader.Resume();
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowProgressWindow(string downloadId)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
if (!liveDownloads.ContainsKey(downloadId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var downloader = liveDownloads[downloadId].Downloader;
|
||||
var prgWin = CreateOrGetProgressWindow(downloader);
|
||||
prgWin.FileNameText = downloader.TargetFileName;
|
||||
prgWin.FileSizeText = $"{TextResource.GetText("STAT_DOWNLOADING")} ...";
|
||||
prgWin.ShowProgressWindow();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error showing progress window");
|
||||
}
|
||||
}
|
||||
|
||||
public void StopDownloads(IEnumerable<string> list, bool closeProgressWindow = false)
|
||||
{
|
||||
var ids = new List<string>(list);
|
||||
foreach (var id in ids)
|
||||
{
|
||||
(var http, _) = liveDownloads.GetValueOrDefault(id);
|
||||
if (http != null)
|
||||
{
|
||||
http.Stop();
|
||||
//liveDownloads.Remove(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (queuedDownloads.ContainsKey(id))
|
||||
{
|
||||
queuedDownloads.Remove(id);
|
||||
ApplicationContext.Application.DownloadCanelled(id);
|
||||
if (activeProgressWindows.ContainsKey(id))
|
||||
{
|
||||
var prgWin = activeProgressWindows[id];
|
||||
prgWin.DownloadCancelled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeProgressWindows.ContainsKey(id) && closeProgressWindow)
|
||||
{
|
||||
var prgWin = activeProgressWindows[id];
|
||||
activeProgressWindows.Remove(id);
|
||||
prgWin.DestroyWindow();
|
||||
Log.Debug("Progress window removed");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void DownloadProgressChanged(object source, ProgressResultEventArgs args)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var http = source as IBaseDownloader;
|
||||
ApplicationContext.Application.UpdateProgress(http.Id, args.Progress, args.DownloadSpeed, args.Eta);
|
||||
if (activeProgressWindows.ContainsKey(http.Id))
|
||||
{
|
||||
var prgWin = activeProgressWindows[http.Id];
|
||||
prgWin.DownloadProgress = args.Progress;
|
||||
prgWin.FileSizeText = $"{TextResource.GetText("STAT_DOWNLOADING")} {FormattingHelper.FormatSize(args.Downloaded)} / {FormattingHelper.FormatSize(http.FileSize)}";
|
||||
prgWin.DownloadSpeedText = FormattingHelper.FormatSize((long)args.DownloadSpeed) + "/s";
|
||||
prgWin.DownloadETAText = $"{TextResource.GetText("MSG_TIME_LEFT")}: {FormattingHelper.ToHMS(args.Eta)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AssembleProgressChanged(object source, ProgressResultEventArgs args)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var http = source as IBaseDownloader;
|
||||
if (activeProgressWindows.ContainsKey(http.Id))
|
||||
{
|
||||
var prgWin = activeProgressWindows[http.Id];
|
||||
prgWin.DownloadProgress = args.Progress;
|
||||
prgWin.FileSizeText = $"{TextResource.GetText("STAT_ASSEMBLING")} {FormattingHelper.FormatSize(args.Downloaded)} / {FormattingHelper.FormatSize(http.FileSize)}";
|
||||
prgWin.DownloadSpeedText = "---";
|
||||
prgWin.DownloadETAText = "---";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadFinished(object source, EventArgs args)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var http = source as IBaseDownloader;
|
||||
DetachEventHandlers(http);
|
||||
ApplicationContext.Application.DownloadFinished(http.Id, http.FileSize < 0 ? new FileInfo(http.TargetFile).Length : http.FileSize, http.TargetFile);
|
||||
|
||||
var showCompleteDialog = false;
|
||||
if (liveDownloads.ContainsKey(http.Id))
|
||||
{
|
||||
(_, bool nonInteractive) = liveDownloads[http.Id];
|
||||
liveDownloads.Remove(http.Id);
|
||||
|
||||
if (!nonInteractive && Config.Instance.ShowDownloadCompleteWindow)
|
||||
{
|
||||
showCompleteDialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (activeProgressWindows.ContainsKey(http.Id))
|
||||
{
|
||||
var prgWin = activeProgressWindows[http.Id];
|
||||
activeProgressWindows.Remove(http.Id);
|
||||
prgWin.DownloadId = null;
|
||||
prgWin.DestroyWindow();
|
||||
}
|
||||
|
||||
if (showCompleteDialog)
|
||||
{
|
||||
ApplicationContext.Application.ShowDownloadCompleteDialog(http.TargetFileName, Path.GetDirectoryName(http.TargetFile));
|
||||
}
|
||||
|
||||
if (Config.Instance.ScanWithAntiVirus)
|
||||
{
|
||||
PlatformHelper.RunAntivirus(Config.Instance.AntiVirusExecutable, Config.Instance.AntiVirusArgs, http.TargetFile);
|
||||
}
|
||||
|
||||
Helpers.RunGC();
|
||||
ProcessNextQueuedItem();
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadFailed(object source, DownloadFailedEventArgs args)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
Log.Debug("Download failed: " + args.ErrorCode);
|
||||
var http = source as IBaseDownloader;
|
||||
DetachEventHandlers(http);
|
||||
liveDownloads.Remove(http.Id);
|
||||
ApplicationContext.Application.DownloadFailed(http.Id);
|
||||
if (activeProgressWindows.ContainsKey(http.Id))
|
||||
{
|
||||
var prgWin = activeProgressWindows[http.Id];
|
||||
prgWin.DownloadFailed(new ErrorDetails { Message = ErrorMessages.GetLocalizedErrorMessage(args.ErrorCode) });
|
||||
}
|
||||
|
||||
Helpers.RunGC();
|
||||
ProcessNextQueuedItem();
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadCancelled(object source, EventArgs args)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
Log.Debug("Download cancelled");
|
||||
var http = source as IBaseDownloader;
|
||||
DetachEventHandlers(http);
|
||||
liveDownloads.Remove(http.Id);
|
||||
ApplicationContext.Application.DownloadCanelled(http.Id);
|
||||
|
||||
if (activeProgressWindows.ContainsKey(http.Id))
|
||||
{
|
||||
var prgWin = activeProgressWindows[http.Id];
|
||||
prgWin.DownloadCancelled();
|
||||
}
|
||||
|
||||
Helpers.RunGC();
|
||||
ProcessNextQueuedItem();
|
||||
}
|
||||
}
|
||||
|
||||
void HandleProbeResult(object source, EventArgs args)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var http = source as IBaseDownloader;
|
||||
ApplicationContext.Application.UpdateItem(http.Id, http.TargetFileName, http.FileSize > 0 ? http.FileSize : 0);
|
||||
if (activeProgressWindows.ContainsKey(http.Id))
|
||||
{
|
||||
var prgWin = activeProgressWindows[http.Id];
|
||||
prgWin.FileNameText = http.TargetFileName;
|
||||
prgWin.FileSizeText =
|
||||
$"{TextResource.GetText("STAT_DOWNLOADING")} {FormattingHelper.FormatSize(0)} / {FormattingHelper.FormatSize(http.FileSize)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HandleDownloadStart(object source, EventArgs args)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var http = source as IBaseDownloader;
|
||||
ApplicationContext.Application.DownloadStarted(http.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private IProgressWindow? GetProgressWindow(IBaseDownloader downloader)
|
||||
{
|
||||
IProgressWindow? prgWin = null;
|
||||
#pragma warning disable CS8604 // Possible null reference argument.
|
||||
if (activeProgressWindows.ContainsKey(downloader.Id))
|
||||
#pragma warning restore CS8604 // Possible null reference argument.
|
||||
{
|
||||
prgWin = activeProgressWindows[downloader.Id];
|
||||
}
|
||||
return prgWin;
|
||||
}
|
||||
|
||||
private IProgressWindow CreateOrGetProgressWindow(IBaseDownloader downloader)
|
||||
{
|
||||
IProgressWindow prgWin = null;
|
||||
if (activeProgressWindows.ContainsKey(downloader.Id))
|
||||
{
|
||||
prgWin = activeProgressWindows[downloader.Id];
|
||||
}
|
||||
else
|
||||
{
|
||||
prgWin = CreateProgressWindow(downloader);
|
||||
activeProgressWindows[downloader.Id] = prgWin;
|
||||
}
|
||||
return prgWin;
|
||||
}
|
||||
|
||||
private IProgressWindow CreateProgressWindow(IBaseDownloader downloader)
|
||||
{
|
||||
var prgWin = ApplicationContext.Application.CreateProgressWindow(downloader.Id);
|
||||
prgWin.UrlText = ApplicationContext.Application.GetInProgressDownloadEntry(downloader.Id)?.PrimaryUrl;
|
||||
prgWin.DownloadSpeedText = "---";
|
||||
prgWin.DownloadETAText = "---";
|
||||
prgWin.FileSizeText = "---";
|
||||
return prgWin;
|
||||
}
|
||||
|
||||
public bool IsDownloadActive(string id)
|
||||
{
|
||||
return liveDownloads.ContainsKey(id) || queuedDownloads.ContainsKey(id);
|
||||
}
|
||||
|
||||
private void ProcessNextQueuedItem()
|
||||
{
|
||||
if (queuedDownloads.Count > 0)
|
||||
{
|
||||
var kv = queuedDownloads.First();
|
||||
queuedDownloads.Remove(kv.Key);
|
||||
var entry = AppDB.Instance.Downloads.GetDownloadById(kv.Key);// ApplicationContext.Current.GetInProgressDownloadEntry(kv.Key);
|
||||
if (entry != null)
|
||||
{
|
||||
ResumeDownload(new Dictionary<string, DownloadItemBase> { [kv.Key] = entry }, kv.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Config.Instance.ShutdownAfterAllFinished)
|
||||
{
|
||||
PlatformHelper.ShutDownPC();
|
||||
}
|
||||
if (awakePingTimer.Enabled)
|
||||
{
|
||||
Log.Debug("Stopping keep awake timer");
|
||||
awakePingTimer.Stop();
|
||||
}
|
||||
if (Config.Instance.RunCommandAfterCompletion)
|
||||
{
|
||||
PlatformHelper.RunCommand(Config.Instance.AfterCompletionCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void StartScheduler()
|
||||
{
|
||||
this.scheduler = new Scheduler();
|
||||
this.scheduler.Start();
|
||||
}
|
||||
|
||||
public void RenameDownload(string id, string folder, string file)
|
||||
{
|
||||
if (liveDownloads.ContainsKey(id))
|
||||
{
|
||||
var downloader = liveDownloads[id].Downloader;
|
||||
downloader.SetTargetDirectory(folder);
|
||||
downloader.SetFileName(file, downloader.FileNameFetchMode);
|
||||
}
|
||||
ApplicationContext.Application.RenameFileOnUI(id, folder, file);
|
||||
}
|
||||
|
||||
private void DetachEventHandlers(IBaseDownloader download)
|
||||
{
|
||||
try
|
||||
{
|
||||
download.Started -= HandleDownloadStart;
|
||||
download.Probed -= HandleProbeResult;
|
||||
download.Finished -= DownloadFinished;
|
||||
download.ProgressChanged -= DownloadProgressChanged;
|
||||
download.AssembingProgressChanged += AssembleProgressChanged;
|
||||
download.Cancelled -= DownloadCancelled;
|
||||
download.Failed -= DownloadFailed;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public AuthenticationInfo? PromptForCredential(string id, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (liveDownloads[id].NonInteractive)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return ApplicationContext.Application.PromtForCredentials(message);
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
public void HideProgressWindow(string id)
|
||||
{
|
||||
if (activeProgressWindows.ContainsKey(id))
|
||||
{
|
||||
var prgWin = activeProgressWindows[id];
|
||||
activeProgressWindows.Remove(id);
|
||||
prgWin.DestroyWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public string? GetPrimaryUrl(DownloadItemBase entry)
|
||||
{
|
||||
if (entry == null) return null;
|
||||
switch (entry.DownloadType)
|
||||
{
|
||||
case "Http":
|
||||
var h1 = RequestDataIO.LoadSingleSourceHTTPDownloadInfo(entry.Id);// LoadInfo<SingleSourceHTTPDownloadInfo>(entry.Id);
|
||||
if (h1 != null)
|
||||
{
|
||||
return h1.Uri;
|
||||
}
|
||||
break;
|
||||
case "Dash":
|
||||
var h2 = RequestDataIO.LoadDualSourceHTTPDownloadInfo(entry.Id);// LoadInfo<DualSourceHTTPDownloadInfo>(entry.Id);
|
||||
if (h2 != null)
|
||||
{
|
||||
return h2.Uri1;
|
||||
}
|
||||
break;
|
||||
case "Hls":
|
||||
var hls = RequestDataIO.LoadMultiSourceHLSDownloadInfo(entry.Id);// LoadInfo<MultiSourceHLSDownloadInfo>(entry.Id);
|
||||
if (hls != null)
|
||||
{
|
||||
return hls.VideoUri;
|
||||
}
|
||||
break;
|
||||
case "Mpd-Dash":
|
||||
var dash = RequestDataIO.LoadMultiSourceDASHDownloadInfo(entry.Id); //LoadInfo<MultiSourceDASHDownloadInfo>(entry.Id);
|
||||
if (dash != null)
|
||||
{
|
||||
return dash.Url;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveDownload(DownloadItemBase entry, bool deleteDownloadedFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (entry == null) return;
|
||||
string? tempDir = null;
|
||||
var validEntry = false;
|
||||
switch (entry.DownloadType)
|
||||
{
|
||||
case "Http":
|
||||
var h1 = DownloadStateIO.LoadSingleSourceHTTPDownloaderState(entry.Id);
|
||||
if (h1 != null)
|
||||
{
|
||||
tempDir = h1.TempDir;
|
||||
validEntry = true;
|
||||
}
|
||||
break;
|
||||
case "Dash":
|
||||
var h2 = DownloadStateIO.LoadDualSourceHTTPDownloaderState(entry.Id);
|
||||
if (h2 != null)
|
||||
{
|
||||
tempDir = h2.TempDir;
|
||||
validEntry = true;
|
||||
}
|
||||
break;
|
||||
case "Hls":
|
||||
var hls = DownloadStateIO.LoadMultiSourceHLSDownloadState(entry.Id);
|
||||
if (hls != null)
|
||||
{
|
||||
tempDir = hls.TempDirectory;
|
||||
validEntry = true;
|
||||
}
|
||||
break;
|
||||
case "Mpd-Dash":
|
||||
var dash = DownloadStateIO.LoadMultiSourceDASHDownloadState(entry.Id);
|
||||
if (dash != null)
|
||||
{
|
||||
tempDir = dash.TempDirectory;
|
||||
validEntry = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (validEntry)
|
||||
{
|
||||
var infoFile = Path.Combine(Config.DataDir, entry.Id + ".info");
|
||||
var stateFile = Path.Combine(Config.DataDir, entry.Id + ".state");
|
||||
if (Directory.Exists(tempDir) && !string.IsNullOrEmpty(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
if (File.Exists(stateFile))
|
||||
{
|
||||
File.Delete(stateFile);
|
||||
}
|
||||
if (File.Exists(infoFile))
|
||||
{
|
||||
File.Delete(infoFile);
|
||||
}
|
||||
|
||||
if (entry is FinishedDownloadItem && deleteDownloadedFile)
|
||||
{
|
||||
var outFile = Path.Combine(entry.TargetDir, entry.Name);
|
||||
if (File.Exists(outFile))
|
||||
{
|
||||
File.Delete(outFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error deleting temp folder");
|
||||
}
|
||||
}
|
||||
|
||||
public void RestartDownload(DownloadItemBase entry)
|
||||
{
|
||||
if (entry == null) return;
|
||||
var convertToMp3 = false;
|
||||
IRequestData? request;
|
||||
try
|
||||
{
|
||||
switch (entry.DownloadType)
|
||||
{
|
||||
case "Http":
|
||||
var info = RequestDataIO.LoadSingleSourceHTTPDownloadInfo(entry.Id);
|
||||
request = info;
|
||||
convertToMp3 = info?.ConvertToMp3 ?? false;
|
||||
break;
|
||||
case "Dash":
|
||||
request = RequestDataIO.LoadDualSourceHTTPDownloadInfo(entry.Id);
|
||||
break;
|
||||
case "Hls":
|
||||
request = RequestDataIO.LoadMultiSourceHLSDownloadInfo(entry.Id);
|
||||
break;
|
||||
case "Mpd-Dash":
|
||||
request = RequestDataIO.LoadMultiSourceDASHDownloadInfo(entry.Id);
|
||||
break;
|
||||
default:
|
||||
request = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (request != null)
|
||||
{
|
||||
this.StartDownload(request, entry.Name,
|
||||
FileNameFetchMode.FileNameAndExtension,
|
||||
entry.TargetDir, true, entry.Authentication, entry.Proxy, null,
|
||||
convertToMp3);
|
||||
RemoveDownload(entry, false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error restarting download");
|
||||
}
|
||||
}
|
||||
|
||||
public void Export(string path)
|
||||
{
|
||||
ImportExport.Export(path);
|
||||
}
|
||||
|
||||
public void Import(string path)
|
||||
{
|
||||
ImportExport.Import(path);
|
||||
ApplicationContext.Application.ShowMessageBox(null, TextResource.GetText("MSG_IMPORT_DONE"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public static class ArgsProcessor
|
||||
{
|
||||
public static void Process(Dictionary<string, string?> args)
|
||||
{
|
||||
if (args.ContainsKey("-u"))
|
||||
{
|
||||
var url = args["-u"];
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
ApplicationContext.CoreService.AddDownload(new Message { Url = url! });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<string, string?> ParseArgs(string[] args, int start = 0)
|
||||
{
|
||||
var options = new Dictionary<string, string?>();
|
||||
var key = string.Empty;
|
||||
for (int i = start; i < args.Length; i++)
|
||||
{
|
||||
var arg = args[i];
|
||||
if (key != string.Empty)
|
||||
{
|
||||
options[key] = arg;
|
||||
key = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
key = arg;
|
||||
options[key] = null;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public struct AuthenticationInfo
|
||||
{
|
||||
public string UserName { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
using XDM.Core.Util;
|
||||
|
||||
namespace XDM.Core.BrowserMonitoring
|
||||
{
|
||||
internal static class BrowserMessageHandler
|
||||
{
|
||||
internal static void Handle(RawBrowserMessageEnvelop envelop)
|
||||
{
|
||||
//Log.Debug("Type: " + envelop.MessageType);
|
||||
if (envelop.MessageType == "videoIds")
|
||||
{
|
||||
foreach (var item in envelop.VideoIds)
|
||||
{
|
||||
ApplicationContext.VideoTracker.AddVideoDownload(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (envelop.MessageType == "clear")
|
||||
{
|
||||
ApplicationContext.VideoTracker.ClearVideoList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (envelop.MessageType == "sync")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (envelop.MessageType == "custom")
|
||||
{
|
||||
var args = ArgsProcessor.ParseArgs(envelop.CustomData.Split('\r'));
|
||||
ArgsProcessor.Process(args);
|
||||
return;
|
||||
}
|
||||
|
||||
var rawMessage = envelop.Message;
|
||||
if (rawMessage == null && envelop.Messages == null)
|
||||
{
|
||||
Log.Debug("Raw message/messages is null");
|
||||
return;
|
||||
};
|
||||
|
||||
switch (envelop.MessageType)
|
||||
{
|
||||
case "download":
|
||||
{
|
||||
var message = Parse(rawMessage);
|
||||
if (!(Helpers.IsBlockedHost(message.Url) || Helpers.IsCompressedJSorCSS(message.Url)))
|
||||
{
|
||||
ApplicationContext.CoreService.AddDownload(message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "links":
|
||||
{
|
||||
var messages = new List<Message>(envelop.Messages.Length);
|
||||
foreach (var msg in envelop.Messages)
|
||||
{
|
||||
var message = Parse(msg);
|
||||
messages.Add(message);
|
||||
}
|
||||
ApplicationContext.CoreService.AddBatchLinks(messages);
|
||||
break;
|
||||
}
|
||||
case "video":
|
||||
{
|
||||
var message = Parse(rawMessage);
|
||||
var contentType = message.GetResponseHeaderFirstValue("Content-Type");
|
||||
|
||||
if (VideoUrlHelper.IsYtFormat(contentType))
|
||||
{
|
||||
VideoUrlHelper.ProcessPostYtFormats(message);
|
||||
}
|
||||
|
||||
//if (VideoUrlHelper.IsFBFormat(contentType, message.Url))
|
||||
//{
|
||||
// VideoUrlHelper.ProcessPostFBFormats(message, ApplicationContext.Core);
|
||||
//}
|
||||
|
||||
if (VideoUrlHelper.IsHLS(contentType))
|
||||
{
|
||||
VideoUrlHelper.ProcessHLSVideo(message);
|
||||
}
|
||||
|
||||
if (VideoUrlHelper.IsDASH(contentType))
|
||||
{
|
||||
VideoUrlHelper.ProcessDashVideo(message);
|
||||
}
|
||||
|
||||
if (!VideoUrlHelper.ProcessYtDashSegment(message))
|
||||
{
|
||||
if (VideoUrlHelper.IsNormalVideo(contentType, message.Url, message.GetContentLength()))
|
||||
{
|
||||
VideoUrlHelper.ProcessNormalVideo(message);
|
||||
}
|
||||
}
|
||||
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,19 @@
|
|||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.BrowserMonitoring
|
||||
{
|
||||
public static class BrowserMonitor
|
||||
{
|
||||
public static void RunHttpIpcHandler()
|
||||
{
|
||||
var handler = new IpcHttpHandler();
|
||||
handler.StartHttpIpcChannel();
|
||||
}
|
||||
|
||||
public static void RunNativeHostHandler()
|
||||
{
|
||||
var handler = new NativeMessagingHostHandler();
|
||||
handler.StartPipedChannel();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using TraceLog;
|
||||
using XDM.Core.Collections;
|
||||
using XDM.Core.Downloader;
|
||||
using XDM.Core.Downloader.Adaptive.Dash;
|
||||
using XDM.Core.Downloader.Adaptive.Hls;
|
||||
using XDM.Core.Downloader.Progressive.DualHttp;
|
||||
using XDM.Core.Downloader.Progressive.SingleHttp;
|
||||
using XDM.Core.Util;
|
||||
|
||||
namespace XDM.Core.BrowserMonitoring
|
||||
{
|
||||
public class VideoTracker : IVideoTracker
|
||||
{
|
||||
private GenericOrderedDictionary<string, (DualSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> ytVideoList = new();
|
||||
private GenericOrderedDictionary<string, (SingleSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> videoList = new();
|
||||
private GenericOrderedDictionary<string, (MultiSourceHLSDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> hlsVideoList = new();
|
||||
private GenericOrderedDictionary<string, (MultiSourceDASHDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> dashVideoList = new();
|
||||
|
||||
public void ClearVideoList()
|
||||
{
|
||||
ytVideoList.Clear();
|
||||
hlsVideoList.Clear();
|
||||
dashVideoList.Clear();
|
||||
videoList.Clear();
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
|
||||
public bool IsFFmpegRequiredForDownload(string id)
|
||||
{
|
||||
return ytVideoList.ContainsKey(id) || dashVideoList.ContainsKey(id) || hlsVideoList.ContainsKey(id);
|
||||
}
|
||||
|
||||
public void StartVideoDownload(string videoId,
|
||||
string name,
|
||||
string? folder,
|
||||
bool startImmediately,
|
||||
AuthenticationInfo? authentication,
|
||||
ProxyInfo? proxyInfo,
|
||||
int maxSpeedLimit,
|
||||
string? queueId,
|
||||
bool convertToMp3 = false //only applicable for dual source http downloads
|
||||
)
|
||||
{
|
||||
//IBaseDownloader downloader = null;
|
||||
if (ytVideoList.ContainsKey(videoId))
|
||||
{
|
||||
ApplicationContext.CoreService.StartDownload(ytVideoList[videoId].Info, name, FileNameFetchMode.ExtensionOnly,
|
||||
folder, startImmediately, authentication, proxyInfo, Helpers.GetSpeedLimit(), queueId);
|
||||
}
|
||||
else if (videoList.ContainsKey(videoId))
|
||||
{
|
||||
ApplicationContext.CoreService.StartDownload(videoList[videoId].Info, name, convertToMp3 ? FileNameFetchMode.None : FileNameFetchMode.ExtensionOnly,
|
||||
folder, startImmediately, authentication, proxyInfo, Helpers.GetSpeedLimit(), queueId, convertToMp3);
|
||||
}
|
||||
else if (hlsVideoList.ContainsKey(videoId))
|
||||
{
|
||||
ApplicationContext.CoreService.StartDownload(hlsVideoList[videoId].Info, name, FileNameFetchMode.ExtensionOnly,
|
||||
folder, startImmediately, authentication, proxyInfo, Helpers.GetSpeedLimit(), queueId);
|
||||
}
|
||||
else if (dashVideoList.ContainsKey(videoId))
|
||||
{
|
||||
ApplicationContext.CoreService.StartDownload(dashVideoList[videoId].Info, name, FileNameFetchMode.ExtensionOnly,
|
||||
folder, startImmediately, authentication, proxyInfo, Helpers.GetSpeedLimit(), queueId);
|
||||
}
|
||||
}
|
||||
|
||||
public List<(string ID, string File, string DisplayName, DateTime Time)> GetVideoList(bool encode = true)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var list = new List<(string ID, string File, string DisplayName, DateTime Time)>();
|
||||
foreach (var e in ytVideoList)
|
||||
{
|
||||
list.Add((e.Key, encode ? Helpers.EncodeToCharCode(e.Value.Info.File) : e.Value.Info.File, e.Value.DisplayInfo.Quality, e.Value.DisplayInfo.CreationTime));
|
||||
}
|
||||
foreach (var e in videoList)
|
||||
{
|
||||
list.Add((e.Key, encode ? Helpers.EncodeToCharCode(e.Value.Info.File) : e.Value.Info.File, e.Value.DisplayInfo.Quality, e.Value.DisplayInfo.CreationTime));
|
||||
}
|
||||
foreach (var e in hlsVideoList)
|
||||
{
|
||||
list.Add((e.Key, encode ? Helpers.EncodeToCharCode(e.Value.Info.File) : e.Value.Info.File, e.Value.DisplayInfo.Quality, e.Value.DisplayInfo.CreationTime));
|
||||
}
|
||||
foreach (var e in dashVideoList)
|
||||
{
|
||||
list.Add((e.Key, encode ? Helpers.EncodeToCharCode(e.Value.Info.File) : e.Value.Info.File, e.Value.DisplayInfo.Quality, e.Value.DisplayInfo.CreationTime));
|
||||
}
|
||||
list.Sort((a, b) => a.Time.CompareTo(b.Time));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotifications(IEnumerable<(DualSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> notifications)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
foreach (var info in notifications)
|
||||
{
|
||||
ytVideoList.Add(Guid.NewGuid().ToString(), info);
|
||||
}
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotifications(IEnumerable<(SingleSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> notifications)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
foreach (var info in notifications)
|
||||
{
|
||||
videoList.Add(Guid.NewGuid().ToString(), info);
|
||||
}
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotifications(IEnumerable<(MultiSourceHLSDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> notifications)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
foreach (var info in notifications)
|
||||
{
|
||||
hlsVideoList.Add(Guid.NewGuid().ToString(), info);
|
||||
}
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotifications(IEnumerable<(MultiSourceDASHDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> notifications)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
foreach (var info in notifications)
|
||||
{
|
||||
dashVideoList.Add(Guid.NewGuid().ToString(), info);
|
||||
}
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotification(StreamingVideoDisplayInfo displayInfo, DualSourceHTTPDownloadInfo info)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
ytVideoList.Add(Guid.NewGuid().ToString(), (info, displayInfo));
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotification(StreamingVideoDisplayInfo displayInfo, SingleSourceHTTPDownloadInfo info)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
videoList.Add(Guid.NewGuid().ToString(), (info, displayInfo));
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotification(StreamingVideoDisplayInfo displayInfo, MultiSourceHLSDownloadInfo info)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
hlsVideoList.Add(id, (info, displayInfo));
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotification(StreamingVideoDisplayInfo displayInfo, MultiSourceDASHDownloadInfo info)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
Log.Debug("DASH video added with id: " + id);
|
||||
dashVideoList.Add(id, (info, displayInfo));
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoDownload(string videoId)
|
||||
{
|
||||
var name = string.Empty;
|
||||
var size = 0L;
|
||||
var contentType = string.Empty;
|
||||
var valid = false;
|
||||
if (ytVideoList.ContainsKey(videoId))
|
||||
{
|
||||
if (ApplicationContext.LinkRefresher.LinkAccepted(ytVideoList[videoId].Info)) return;
|
||||
name = ytVideoList[videoId].Info.File;
|
||||
size = ytVideoList[videoId].DisplayInfo.Size;
|
||||
contentType = ytVideoList[videoId].Info.ContentType1;
|
||||
valid = true;
|
||||
}
|
||||
else if (videoList.ContainsKey(videoId))
|
||||
{
|
||||
if (ApplicationContext.LinkRefresher.LinkAccepted(videoList[videoId].Info)) return;
|
||||
name = videoList[videoId].Info.File;
|
||||
size = videoList[videoId].DisplayInfo.Size;
|
||||
contentType = videoList[videoId].Info.ContentType;
|
||||
valid = true;
|
||||
}
|
||||
else if (hlsVideoList.ContainsKey(videoId))
|
||||
{
|
||||
Log.Debug("Download HLS video added with id: " + videoId);
|
||||
name = hlsVideoList[videoId].Info.File;
|
||||
valid = true;
|
||||
try
|
||||
{
|
||||
contentType = hlsVideoList[videoId].Info.ContentType;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
else if (dashVideoList.ContainsKey(videoId))
|
||||
{
|
||||
Log.Debug("Download DASH video added with id: " + videoId);
|
||||
name = dashVideoList[videoId].Info.File;
|
||||
contentType = dashVideoList[videoId].Info.ContentType;
|
||||
valid = true;
|
||||
}
|
||||
if (valid)
|
||||
{
|
||||
if (Config.Instance.StartDownloadAutomatically)
|
||||
{
|
||||
StartVideoDownload(
|
||||
videoId, Helpers.SanitizeFileName(name),
|
||||
null, true, null, Config.Instance.Proxy,
|
||||
Helpers.GetSpeedLimit(), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplicationContext.Application.ShowVideoDownloadDialog(videoId, name, size, contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using System.Net;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using XDM.Core;
|
||||
using XDM.Core.Util;
|
||||
using XDM.Core.HttpServer;
|
||||
using System.Threading;
|
||||
using TraceLog;
|
||||
using Translations;
|
||||
|
||||
namespace XDM.Core.BrowserMonitoring
|
||||
{
|
||||
public class IpcHttpHandler
|
||||
{
|
||||
private NanoServer server;
|
||||
|
||||
public IpcHttpHandler()
|
||||
{
|
||||
server = new NanoServer(IPAddress.Loopback, 9614);
|
||||
server.RequestReceived += (sender, args) =>
|
||||
{
|
||||
HandleRequest(args.RequestContext);
|
||||
};
|
||||
}
|
||||
|
||||
public void StartHttpIpcChannel()
|
||||
{
|
||||
new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
server.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex.ToString());
|
||||
ApplicationContext.Application.ShowMessageBox(null, TextResource.GetText("MSG_ALREADY_RUNNING"));
|
||||
}
|
||||
}).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":
|
||||
{
|
||||
var text = Encoding.UTF8.GetString(context.RequestBody!);
|
||||
Log.Debug(text);
|
||||
var message = Message.ParseMessage(text);
|
||||
if (!(Helpers.IsBlockedHost(message.Url) || Helpers.IsCompressedJSorCSS(message.Url)))
|
||||
{
|
||||
ApplicationContext.CoreService.AddDownload(message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "/video":
|
||||
{
|
||||
var text = Encoding.UTF8.GetString(context.RequestBody!);
|
||||
Log.Debug(text);
|
||||
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);
|
||||
}
|
||||
if (VideoUrlHelper.IsDASH(contentType))
|
||||
{
|
||||
VideoUrlHelper.ProcessDashVideo(message2);
|
||||
}
|
||||
if (!VideoUrlHelper.ProcessYtDashSegment(message2))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "/links":
|
||||
{
|
||||
var text = Encoding.UTF8.GetString(context.RequestBody!);
|
||||
Log.Debug(text);
|
||||
var arr = text.Split(new string[] { "\r\n\r\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
ApplicationContext.CoreService.AddBatchLinks(arr.Select(str => Message.ParseMessage(str.Trim())).ToList());
|
||||
break;
|
||||
}
|
||||
case "/item":
|
||||
{
|
||||
foreach (var item in Encoding.UTF8.GetString(context.RequestBody!).Split(new char[] { '\r', '\n' }))
|
||||
{
|
||||
ApplicationContext.VideoTracker.AddVideoDownload(item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "/clear":
|
||||
ApplicationContext.VideoTracker.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 = ApplicationContext.VideoTracker.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,217 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace XDM.Core.BrowserMonitoring
|
||||
{
|
||||
public class JsonMessageParser
|
||||
{
|
||||
private T? ReadProperty<T>(JsonTextReader reader, string name)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.PropertyName && reader.Value?.ToString() == name &&
|
||||
reader.Read() && reader.Value != null)
|
||||
{
|
||||
return (T)reader.Value;
|
||||
}
|
||||
return default(T);
|
||||
}
|
||||
|
||||
private bool IsObjectStart(JsonTextReader reader, string name)
|
||||
{
|
||||
return reader.TokenType == JsonToken.PropertyName && reader.Value?.ToString() == name &&
|
||||
reader.Read() && reader.TokenType == JsonToken.StartObject;
|
||||
}
|
||||
|
||||
private bool IsListStart(JsonTextReader reader, string name)
|
||||
{
|
||||
return reader.TokenType == JsonToken.PropertyName && reader.Value?.ToString() == name &&
|
||||
reader.Read() && reader.TokenType == JsonToken.StartArray;
|
||||
}
|
||||
|
||||
private void SkipUnknownParts(JsonTextReader reader)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.PropertyName && reader.Value != null)
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
var n = 1;
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndObject) n--;
|
||||
if (reader.TokenType == JsonToken.StartObject) n++;
|
||||
if (n == 0) return;
|
||||
}
|
||||
}
|
||||
else if (reader.TokenType == JsonToken.StartArray)
|
||||
{
|
||||
var n = 1;
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndArray) n--;
|
||||
if (reader.TokenType == JsonToken.StartArray) n++;
|
||||
if (n == 0) return;
|
||||
}
|
||||
}
|
||||
else if (reader.Value != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RawBrowserMessage ReadMessageObject(JsonTextReader reader)
|
||||
{
|
||||
var msg = new RawBrowserMessage { Cookies = new(), ResponseHeaders = new(), RequestHeaders = new() };
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndObject) break;
|
||||
var url = ReadProperty<string>(reader, "url");
|
||||
if (url != null)
|
||||
{
|
||||
msg.Url = url;
|
||||
}
|
||||
if (IsObjectStart(reader, "cookies"))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndObject) break;
|
||||
if (reader.TokenType == JsonToken.PropertyName && reader.Value != null)
|
||||
{
|
||||
var cookieName = (string)reader.Value;
|
||||
if (reader.Read() && reader.TokenType == JsonToken.String)
|
||||
{
|
||||
var cookieValue = (string)reader.Value;
|
||||
msg.Cookies[cookieName] = cookieValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (IsObjectStart(reader, "responseHeaders"))// && IsListStart(reader, "realUA"))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndObject) break;
|
||||
if (reader.TokenType == JsonToken.PropertyName && reader.Value != null)
|
||||
{
|
||||
var headerName = (string)reader.Value;
|
||||
if (IsListStart(reader, headerName))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndArray) break;
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
if (msg.ResponseHeaders.TryGetValue(headerName, out var list))
|
||||
{
|
||||
list.Add((string)reader.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.ResponseHeaders[headerName] = new() { (string)reader.Value };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (IsObjectStart(reader, "requestHeaders"))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndObject) break;
|
||||
if (reader.TokenType == JsonToken.PropertyName && reader.Value != null)
|
||||
{
|
||||
var headerName = (string)reader.Value;
|
||||
if (IsListStart(reader, headerName))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndArray) break;
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
if (msg.RequestHeaders.TryGetValue(headerName, out var list))
|
||||
{
|
||||
list.Add((string)reader.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.RequestHeaders[headerName] = new() { (string)reader.Value };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SkipUnknownParts(reader);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
public RawBrowserMessageEnvelop Parse(Stream stream)
|
||||
{
|
||||
var envelop = new RawBrowserMessageEnvelop();
|
||||
var reader = new JsonTextReader(new StreamReader(stream));
|
||||
if (reader.Read() && reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndObject) break;
|
||||
var messageType = ReadProperty<string>(reader, "messageType");
|
||||
if (messageType != null)
|
||||
{
|
||||
envelop.MessageType = messageType;
|
||||
}
|
||||
var customData = ReadProperty<string>(reader, "customData");
|
||||
if (customData != null)
|
||||
{
|
||||
envelop.CustomData = customData;
|
||||
}
|
||||
if (IsObjectStart(reader, "message"))
|
||||
{
|
||||
var msg = ReadMessageObject(reader);
|
||||
envelop.Message = msg;
|
||||
}
|
||||
if (IsListStart(reader, "messages"))
|
||||
{
|
||||
var list = new List<RawBrowserMessage>();
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndArray) break;
|
||||
if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
var msg = ReadMessageObject(reader);
|
||||
list.Add(msg);
|
||||
envelop.Messages = list.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (IsListStart(reader, "videoIds"))
|
||||
{
|
||||
var list = new List<RawBrowserMessage>();
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonToken.EndArray) break;
|
||||
if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
var msg = ReadMessageObject(reader);
|
||||
list.Add(msg);
|
||||
envelop.Messages = list.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
SkipUnknownParts(reader);
|
||||
}
|
||||
}
|
||||
return envelop;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.BrowserMonitoring
|
||||
{
|
||||
internal static class NativeMessagingConfigurer
|
||||
{
|
||||
private static void CreateMessagingHostManifest(Browser browser, string appName, string manifestPath)
|
||||
{
|
||||
var allowedExtensions = browser == Browser.Firefox ? new[] {
|
||||
"browser-mon@xdman.sourceforge.net"
|
||||
} : new[] {
|
||||
"chrome-extension://danmljfachfhpbfikjgedlfifabhofcj/",
|
||||
"chrome-extension://dkckaoghoiffdbomfbbodbbgmhjblecj/",
|
||||
"chrome-extension://ejpbcmllmliidhlpkcgbphhmaodjihnc/",
|
||||
"chrome-extension://fogpiboapmefmkbodpmfnohfflonbgig/"
|
||||
};
|
||||
var folder = Path.GetDirectoryName(manifestPath)!;
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
using var stream = new FileStream(manifestPath, FileMode.Create);
|
||||
using var textWriter = new StreamWriter(stream);
|
||||
using var writer = new JsonTextWriter(textWriter);
|
||||
writer.Formatting = Formatting.Indented;
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(appName);
|
||||
writer.WritePropertyName("description");
|
||||
writer.WriteValue("Native messaging host for Xtreme Download Manager");
|
||||
writer.WritePropertyName("path");
|
||||
writer.WriteValue(Path.Combine(
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "MessagingHost"),
|
||||
Environment.OSVersion.Platform == PlatformID.Win32NT ?
|
||||
"xdm-messaging-host.exe" : "xdm-messaging-host"));
|
||||
writer.WritePropertyName("type");
|
||||
writer.WriteValue("stdio");
|
||||
writer.WritePropertyName(browser == Browser.Firefox ? "allowed_extensions" : "allowed_origins");
|
||||
writer.WriteStartArray();
|
||||
foreach (var extension in allowedExtensions)
|
||||
{
|
||||
writer.WriteValue(extension);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
public static void InstallNativeMessagingHost(Browser browser)
|
||||
{
|
||||
var appName = browser == Browser.Firefox ? "xdmff.native_host" :
|
||||
"xdm_chrome.native_host";
|
||||
var os = Environment.OSVersion.Platform;
|
||||
if (os == PlatformID.Win32NT)
|
||||
{
|
||||
var manifestPath = Path.Combine(Config.DataDir, $"{appName}.json");
|
||||
CreateMessagingHostManifest(browser, appName, manifestPath);
|
||||
var regPath = (browser == Browser.Firefox ?
|
||||
@"Software\Mozilla\NativeMessagingHosts\" :
|
||||
@"SOFTWARE\Google\Chrome\NativeMessagingHosts");
|
||||
using var regKey = Registry.CurrentUser.CreateSubKey(regPath);
|
||||
using var key = regKey.CreateSubKey(appName, RegistryKeyPermissionCheck.ReadWriteSubTree);
|
||||
key.SetValue(null, manifestPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
string manifestPath;
|
||||
var home = Environment.GetEnvironmentVariable("HOME")!;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
manifestPath = Path.Combine(home, browser == Browser.Firefox ?
|
||||
$"Library/Application Support/Mozilla/NativeMessagingHosts/{appName}.json" :
|
||||
$"Library/Application Support/Google/Chrome/NativeMessagingHosts/{appName}.json");
|
||||
}
|
||||
else
|
||||
{
|
||||
manifestPath = Path.Combine(home, browser == Browser.Firefox ?
|
||||
$".mozilla/native-messaging-hosts/{appName}.json" :
|
||||
$".config/google-chrome/NativeMessagingHosts/{appName}.json");
|
||||
}
|
||||
Log.Debug($"Manifest file: {manifestPath}");
|
||||
CreateMessagingHostManifest(browser, appName, manifestPath);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Browser
|
||||
{
|
||||
Chrome, Firefox, MSEdge
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
using System;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading;
|
||||
using TraceLog;
|
||||
|
||||
namespace XDM.Core.BrowserMonitoring
|
||||
{
|
||||
internal sealed class NativeMessagingHostChannel
|
||||
{
|
||||
private NamedPipeServerStream pipe;
|
||||
internal event EventHandler<NativeMessageEventArgs>? MessageReceived;
|
||||
internal event EventHandler? Disconnected;
|
||||
private Thread readerThread;
|
||||
|
||||
internal NativeMessagingHostChannel(NamedPipeServerStream pipe)
|
||||
{
|
||||
this.pipe = pipe;
|
||||
readerThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ReadDataFromPipe();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.ToString());
|
||||
Disconnect();
|
||||
}
|
||||
this.Disconnected?.Invoke(this, EventArgs.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
internal void Disconnect()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (pipe.IsConnected)
|
||||
{
|
||||
pipe.Disconnect();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
internal void Publish(byte[] data)
|
||||
{
|
||||
try
|
||||
{
|
||||
NativeMessageSerializer.WriteMessage(pipe, data);
|
||||
pipe.Flush();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
internal void ReadDataFromPipe()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Log.Debug("Waiting for message from native host...");
|
||||
var bytes = NativeMessageSerializer.ReadMessageBytes(pipe);
|
||||
Log.Debug($"Message from native host {bytes.Length} bytes");
|
||||
this.MessageReceived?.Invoke(this, new NativeMessageEventArgs(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
internal void Start(byte[] initialConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
readerThread.Start();
|
||||
NativeMessageSerializer.WriteMessage(pipe, initialConfig);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class NativeMessageEventArgs : EventArgs
|
||||
{
|
||||
private byte[] data;
|
||||
|
||||
internal NativeMessageEventArgs(byte[] data)
|
||||
{
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
internal byte[] Data => data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,369 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.IO.Pipes;
|
||||
using System.IO;
|
||||
using XDM.Core;
|
||||
using System.Threading;
|
||||
#if NET35
|
||||
using XDM.Compatibility;
|
||||
#else
|
||||
using System.Collections.Concurrent;
|
||||
#endif
|
||||
using TraceLog;
|
||||
|
||||
namespace XDM.Core.BrowserMonitoring
|
||||
{
|
||||
public class NativeMessagingHostHandler : IDisposable
|
||||
{
|
||||
private int MaxPipeInstance = 254;
|
||||
private static readonly string PipeName = "XDM_Ipc_Browser_Monitoring_Pipe";
|
||||
private List<NativeMessagingHostChannel> connectedChannels = new();
|
||||
private static Mutex globalMutex;
|
||||
private Thread listenerThread;
|
||||
|
||||
public static void EnsureSingleInstance()
|
||||
{
|
||||
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)
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs().Skip(1);
|
||||
if (args.Count() > 0)
|
||||
{
|
||||
Log.Debug(ex, "Sending args to running instance");
|
||||
SendArgsToRunningInstance(args);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
globalMutex = new Mutex(true, @"Global\XDM_Active_Instance");
|
||||
}
|
||||
|
||||
public NativeMessagingHostHandler()
|
||||
{
|
||||
EnsureSingleInstance();
|
||||
}
|
||||
|
||||
public void BroadcastConfig()
|
||||
{
|
||||
var bytes = GetSyncBytes();
|
||||
lock (this)
|
||||
{
|
||||
foreach (var channel in connectedChannels)
|
||||
{
|
||||
try
|
||||
{
|
||||
channel.Publish(bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void StartPipedChannel()
|
||||
{
|
||||
listenerThread = new Thread(() =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var pipe =
|
||||
new NamedPipeServerStream(PipeName,
|
||||
PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances,
|
||||
PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
|
||||
Log.Debug("Waiting for native host pipe...");
|
||||
pipe.WaitForConnection();
|
||||
Log.Debug("Pipe request received");
|
||||
lock (connectedChannels)
|
||||
{
|
||||
var channel = CreateChannel(pipe);
|
||||
connectedChannels.Add(channel);
|
||||
channel.Start(GetSyncBytes());
|
||||
}
|
||||
}
|
||||
});
|
||||
listenerThread.Start();
|
||||
ApplicationContext.ApplicationEvent += ApplicationContext_ApplicationEvent;
|
||||
}
|
||||
|
||||
private void ApplicationContext_ApplicationEvent(object? sender, ApplicationEvent e)
|
||||
{
|
||||
if (e.EventType == "ConfigChanged")
|
||||
{
|
||||
BroadcastConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private NativeMessagingHostChannel CreateChannel(NamedPipeServerStream pipe)
|
||||
{
|
||||
var channel = new NativeMessagingHostChannel(pipe);
|
||||
channel.MessageReceived += (sender, args) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var br = new BinaryReader(new MemoryStream(args.Data));
|
||||
var envelop = RawBrowserMessageEnvelop.Deserialize(br);
|
||||
BrowserMessageHandler.Handle(envelop);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.ToString());
|
||||
}
|
||||
};
|
||||
channel.Disconnected += (sender, bytes) =>
|
||||
{
|
||||
lock (connectedChannels)
|
||||
{
|
||||
connectedChannels.Remove((NativeMessagingHostChannel)sender);
|
||||
}
|
||||
};
|
||||
return channel;
|
||||
}
|
||||
|
||||
//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];
|
||||
// NativeMessageSerializer.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(NativeMessageSerializer.ReadMessageBytes(inPipe));
|
||||
// Log.Debug("Client pipe: " + clientPipeName);
|
||||
// if (clientPipeName.StartsWith("XDM-ApplicationContext.Core-"))
|
||||
// {
|
||||
// var command = NativeMessageSerializer.ReadMessageBytes(inPipe);
|
||||
// var args = ArgsProcessor.ParseArgs(Encoding.UTF8.GetString(command).Split('\r'));
|
||||
// ArgsProcessor.Process(ApplicationContext.Core, args);
|
||||
// return;
|
||||
// }
|
||||
// var outPipe = new NamedPipeClientStream(".", clientPipeName, PipeDirection.Out);
|
||||
// outPipe.Connect();
|
||||
// SendConfig(outPipe);
|
||||
// inOutMap[inPipe] = outPipe;
|
||||
// Log.Debug("Message handshake completed");
|
||||
// while (true)
|
||||
// {
|
||||
// var text = NativeMessageSerializer.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(ApplicationContext.Core, 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(ApplicationContext.Core);
|
||||
// NativeMessageSerializer.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);
|
||||
// if (syncLength > 4 * 8196)
|
||||
// {
|
||||
// throw new ArgumentException($"Message length too long: {syncLength}");
|
||||
// }
|
||||
// var bytes = new byte[syncLength];
|
||||
// ReadFully(pipe, bytes, syncLength);
|
||||
// return bytes;
|
||||
//}
|
||||
|
||||
//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)
|
||||
//{
|
||||
|
||||
// var bytes = BitConverter.GetBytes(msgBytes.Length);
|
||||
// pipe.Write(bytes, 0, bytes.Length);
|
||||
// pipe.Write(msgBytes, 0, msgBytes.Length);
|
||||
// pipe.Flush();
|
||||
//}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (connectedChannels)
|
||||
{
|
||||
foreach (var channel in connectedChannels)
|
||||
{
|
||||
channel.Disconnect();
|
||||
}
|
||||
}
|
||||
//foreach (var pipe in inPipes)
|
||||
//{
|
||||
// try { pipe.Disconnect(); } catch { }
|
||||
// try { pipe.Dispose(); } catch { }
|
||||
//}
|
||||
}
|
||||
|
||||
private static 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 = ApplicationContext.VideoTracker.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();
|
||||
}
|
||||
|
||||
private static void SendArgsToRunningInstance(IEnumerable<string> args)
|
||||
{
|
||||
if (args == null || args.Count() < 1) return;
|
||||
try
|
||||
{
|
||||
using var npc = new NamedPipeClientStream(".", PipeName, PipeDirection.Out);
|
||||
npc.Connect();
|
||||
var b = new MemoryStream();
|
||||
var wb = new BinaryWriter(b);
|
||||
wb.Write(Int32.MaxValue);
|
||||
wb.Write(string.Join("\r", args.ToArray()));
|
||||
wb.Close();
|
||||
NativeMessageSerializer.WriteMessage(npc, b.ToArray());
|
||||
npc.Flush();
|
||||
npc.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class InstanceAlreadyRunningException : Exception
|
||||
{
|
||||
public InstanceAlreadyRunningException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using TraceLog;
|
||||
using XDM.Core.Collections;
|
||||
using XDM.Core.Downloader;
|
||||
using XDM.Core.Downloader.Adaptive.Dash;
|
||||
using XDM.Core.Downloader.Adaptive.Hls;
|
||||
using XDM.Core.Downloader.Progressive.DualHttp;
|
||||
using XDM.Core.Downloader.Progressive.SingleHttp;
|
||||
using XDM.Core.Util;
|
||||
|
||||
namespace XDM.Core.BrowserMonitoring
|
||||
{
|
||||
public class VideoTracker : IVideoTracker
|
||||
{
|
||||
private GenericOrderedDictionary<string, (DualSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> ytVideoList = new();
|
||||
private GenericOrderedDictionary<string, (SingleSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> videoList = new();
|
||||
private GenericOrderedDictionary<string, (MultiSourceHLSDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> hlsVideoList = new();
|
||||
private GenericOrderedDictionary<string, (MultiSourceDASHDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> dashVideoList = new();
|
||||
|
||||
public void ClearVideoList()
|
||||
{
|
||||
ytVideoList.Clear();
|
||||
hlsVideoList.Clear();
|
||||
dashVideoList.Clear();
|
||||
videoList.Clear();
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
|
||||
public bool IsFFmpegRequiredForDownload(string id)
|
||||
{
|
||||
return ytVideoList.ContainsKey(id) || dashVideoList.ContainsKey(id) || hlsVideoList.ContainsKey(id);
|
||||
}
|
||||
|
||||
public void StartVideoDownload(string videoId,
|
||||
string name,
|
||||
string? folder,
|
||||
bool startImmediately,
|
||||
AuthenticationInfo? authentication,
|
||||
ProxyInfo? proxyInfo,
|
||||
int maxSpeedLimit,
|
||||
string? queueId,
|
||||
bool convertToMp3 = false //only applicable for dual source http downloads
|
||||
)
|
||||
{
|
||||
//IBaseDownloader downloader = null;
|
||||
if (ytVideoList.ContainsKey(videoId))
|
||||
{
|
||||
ApplicationContext.CoreService.StartDownload(ytVideoList[videoId].Info, name, FileNameFetchMode.ExtensionOnly,
|
||||
folder, startImmediately, authentication, proxyInfo, queueId, false);
|
||||
}
|
||||
else if (videoList.ContainsKey(videoId))
|
||||
{
|
||||
ApplicationContext.CoreService.StartDownload(videoList[videoId].Info, name, convertToMp3 ? FileNameFetchMode.None : FileNameFetchMode.ExtensionOnly,
|
||||
folder, startImmediately, authentication, proxyInfo, queueId, convertToMp3);
|
||||
}
|
||||
else if (hlsVideoList.ContainsKey(videoId))
|
||||
{
|
||||
ApplicationContext.CoreService.StartDownload(hlsVideoList[videoId].Info, name, FileNameFetchMode.ExtensionOnly,
|
||||
folder, startImmediately, authentication, proxyInfo, queueId, false);
|
||||
}
|
||||
else if (dashVideoList.ContainsKey(videoId))
|
||||
{
|
||||
ApplicationContext.CoreService.StartDownload(dashVideoList[videoId].Info, name, FileNameFetchMode.ExtensionOnly,
|
||||
folder, startImmediately, authentication, proxyInfo, queueId, false);
|
||||
}
|
||||
}
|
||||
|
||||
public List<(string ID, string File, string DisplayName, DateTime Time)> GetVideoList(bool encode = true)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var list = new List<(string ID, string File, string DisplayName, DateTime Time)>();
|
||||
foreach (var e in ytVideoList)
|
||||
{
|
||||
list.Add((e.Key, encode ? Helpers.EncodeToCharCode(e.Value.Info.File) : e.Value.Info.File, e.Value.DisplayInfo.Quality, e.Value.DisplayInfo.CreationTime));
|
||||
}
|
||||
foreach (var e in videoList)
|
||||
{
|
||||
list.Add((e.Key, encode ? Helpers.EncodeToCharCode(e.Value.Info.File) : e.Value.Info.File, e.Value.DisplayInfo.Quality, e.Value.DisplayInfo.CreationTime));
|
||||
}
|
||||
foreach (var e in hlsVideoList)
|
||||
{
|
||||
list.Add((e.Key, encode ? Helpers.EncodeToCharCode(e.Value.Info.File) : e.Value.Info.File, e.Value.DisplayInfo.Quality, e.Value.DisplayInfo.CreationTime));
|
||||
}
|
||||
foreach (var e in dashVideoList)
|
||||
{
|
||||
list.Add((e.Key, encode ? Helpers.EncodeToCharCode(e.Value.Info.File) : e.Value.Info.File, e.Value.DisplayInfo.Quality, e.Value.DisplayInfo.CreationTime));
|
||||
}
|
||||
list.Sort((a, b) => a.Time.CompareTo(b.Time));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotifications(IEnumerable<(DualSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> notifications)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
foreach (var info in notifications)
|
||||
{
|
||||
ytVideoList.Add(Guid.NewGuid().ToString(), info);
|
||||
}
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotifications(IEnumerable<(SingleSourceHTTPDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> notifications)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
foreach (var info in notifications)
|
||||
{
|
||||
videoList.Add(Guid.NewGuid().ToString(), info);
|
||||
}
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotifications(IEnumerable<(MultiSourceHLSDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> notifications)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
foreach (var info in notifications)
|
||||
{
|
||||
hlsVideoList.Add(Guid.NewGuid().ToString(), info);
|
||||
}
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotifications(IEnumerable<(MultiSourceDASHDownloadInfo Info, StreamingVideoDisplayInfo DisplayInfo)> notifications)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
foreach (var info in notifications)
|
||||
{
|
||||
dashVideoList.Add(Guid.NewGuid().ToString(), info);
|
||||
}
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotification(StreamingVideoDisplayInfo displayInfo, DualSourceHTTPDownloadInfo info)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
ytVideoList.Add(Guid.NewGuid().ToString(), (info, displayInfo));
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotification(StreamingVideoDisplayInfo displayInfo, SingleSourceHTTPDownloadInfo info)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
videoList.Add(Guid.NewGuid().ToString(), (info, displayInfo));
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotification(StreamingVideoDisplayInfo displayInfo, MultiSourceHLSDownloadInfo info)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
hlsVideoList.Add(id, (info, displayInfo));
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoNotification(StreamingVideoDisplayInfo displayInfo, MultiSourceDASHDownloadInfo info)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
Log.Debug("DASH video added with id: " + id);
|
||||
dashVideoList.Add(id, (info, displayInfo));
|
||||
ApplicationContext.BroadcastConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddVideoDownload(string videoId)
|
||||
{
|
||||
var name = string.Empty;
|
||||
var size = 0L;
|
||||
var contentType = string.Empty;
|
||||
var valid = false;
|
||||
if (ytVideoList.ContainsKey(videoId))
|
||||
{
|
||||
if (ApplicationContext.LinkRefresher.LinkAccepted(ytVideoList[videoId].Info)) return;
|
||||
name = ytVideoList[videoId].Info.File;
|
||||
size = ytVideoList[videoId].DisplayInfo.Size;
|
||||
contentType = ytVideoList[videoId].Info.ContentType1;
|
||||
valid = true;
|
||||
}
|
||||
else if (videoList.ContainsKey(videoId))
|
||||
{
|
||||
if (ApplicationContext.LinkRefresher.LinkAccepted(videoList[videoId].Info)) return;
|
||||
name = videoList[videoId].Info.File;
|
||||
size = videoList[videoId].DisplayInfo.Size;
|
||||
contentType = videoList[videoId].Info.ContentType;
|
||||
valid = true;
|
||||
}
|
||||
else if (hlsVideoList.ContainsKey(videoId))
|
||||
{
|
||||
Log.Debug("Download HLS video added with id: " + videoId);
|
||||
name = hlsVideoList[videoId].Info.File;
|
||||
valid = true;
|
||||
try
|
||||
{
|
||||
contentType = hlsVideoList[videoId].Info.ContentType;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
else if (dashVideoList.ContainsKey(videoId))
|
||||
{
|
||||
Log.Debug("Download DASH video added with id: " + videoId);
|
||||
name = dashVideoList[videoId].Info.File;
|
||||
contentType = dashVideoList[videoId].Info.ContentType;
|
||||
valid = true;
|
||||
}
|
||||
if (valid)
|
||||
{
|
||||
if (Config.Instance.StartDownloadAutomatically)
|
||||
{
|
||||
StartVideoDownload(
|
||||
videoId, FileHelper.SanitizeFileName(name),
|
||||
null, true, null, Config.Instance.Proxy,
|
||||
Helpers.GetSpeedLimit(), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplicationContext.Application.ShowVideoDownloadDialog(videoId, name, size, contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,885 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using XDM.Core;
|
||||
using XDM.Core.Util;
|
||||
//using XDM.Core.Downloader.YT.Dash;
|
||||
using System.IO;
|
||||
using XDM.Core.MediaParser.Hls;
|
||||
using XDM.Core.MediaParser.Dash;
|
||||
using XDM.Core.MediaParser.YouTube;
|
||||
using System.Security.Cryptography;
|
||||
using TraceLog;
|
||||
using XDM.Core.Clients.Http;
|
||||
using XDM.Core.Downloader.Progressive.DualHttp;
|
||||
using XDM.Core.Downloader.Progressive.SingleHttp;
|
||||
using XDM.Core.Downloader.Adaptive.Dash;
|
||||
using XDM.Core.Downloader.Adaptive.Hls;
|
||||
|
||||
#if !NET5_0_OR_GREATER
|
||||
using XDM.Compatibility;
|
||||
#endif
|
||||
|
||||
namespace XDM.Core.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)
|
||||
{
|
||||
//var file = message.File ?? FileHelper.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 (ApplicationContext.CoreService)
|
||||
{
|
||||
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 = FileHelper.SanitizeFileName(item.Title) + "." + fileExt,
|
||||
Cookies1 = message.Cookies,
|
||||
Cookies2 = message.Cookies
|
||||
};
|
||||
|
||||
var size = item.Size > 0 ? FormattingHelper.FormatSize(item.Size) : string.Empty;
|
||||
var displayInfo = new StreamingVideoDisplayInfo
|
||||
{
|
||||
Quality = $"[{fileExt.ToUpperInvariant()}] {size} {item.FormatDescription}",
|
||||
Size = item.Size,
|
||||
CreationTime = DateTime.Now
|
||||
};
|
||||
|
||||
//var displayText = $"[{fileExt.ToUpperInvariant()}] {size} {item.FormatDescription}";
|
||||
list.Add((Info: mediaItem, DisplayInfo: displayInfo));
|
||||
//ApplicationContext.Core.AddVideoNotification(displayText, mediaItem);
|
||||
}
|
||||
ApplicationContext.VideoTracker.AddVideoNotifications(list);
|
||||
}
|
||||
}
|
||||
if (VideoItems != null && VideoItems.Count > 0)
|
||||
{
|
||||
lock (ApplicationContext.CoreService)
|
||||
{
|
||||
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 = FileHelper.SanitizeFileName(item.Title) + "." + fileExt,
|
||||
Cookies = message.Cookies
|
||||
};
|
||||
var size = item.Size > 0 ? FormattingHelper.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,
|
||||
CreationTime = DateTime.Now
|
||||
};
|
||||
|
||||
list.Add((Info: mediaItem, DisplayInfo: displayInfo));
|
||||
}
|
||||
ApplicationContext.VideoTracker.AddVideoNotifications(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Failed to parse youtube manifest");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ProcessDashVideo(Message message)
|
||||
{
|
||||
var file = message.File ?? FileHelper.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 + FileHelper.SanitizeFileName(file) + "." + fileExt,
|
||||
Cookies1 = message.Cookies,
|
||||
Cookies2 = message.Cookies
|
||||
};
|
||||
var displayText = $"[{fileExt.ToUpperInvariant()}] {GetQualityString(video, audio)}";
|
||||
Log.Debug("Display text dash: " + displayText);
|
||||
ApplicationContext.VideoTracker.AddVideoNotification(new StreamingVideoDisplayInfo
|
||||
{
|
||||
Quality = displayText,
|
||||
CreationTime = DateTime.Now
|
||||
}, 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 + FileHelper.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);
|
||||
ApplicationContext.VideoTracker.AddVideoNotification(new StreamingVideoDisplayInfo
|
||||
{
|
||||
Quality = displayText,
|
||||
CreationTime = DateTime.Now
|
||||
}, mediaItem);
|
||||
}
|
||||
}
|
||||
else if (video != null)
|
||||
{
|
||||
Log.Debug("DASH manifest contains no audio and 1 video, making it SingleSourceHTTPDownload");
|
||||
AddSingleItem(video, message, prefix, false, file);
|
||||
}
|
||||
else if (audio != null)
|
||||
{
|
||||
Log.Debug("DASH manifest contains 1 audio and no video, making it SingleSourceHTTPDownload");
|
||||
AddSingleItem(audio, message, prefix, true, file);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("No audio or video in dash mpd");
|
||||
}
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddSingleItem(Representation item, Message message, 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 + FileHelper.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);
|
||||
ApplicationContext.VideoTracker.AddVideoNotification(new StreamingVideoDisplayInfo
|
||||
{
|
||||
Quality = displayText,
|
||||
CreationTime = DateTime.Now
|
||||
}, 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)
|
||||
{
|
||||
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 = FileHelper.SanitizeFileName(message.File ?? (FileHelper.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);
|
||||
ApplicationContext.VideoTracker.AddVideoNotification(new StreamingVideoDisplayInfo
|
||||
{
|
||||
Quality = displayText,
|
||||
CreationTime = DateTime.Now
|
||||
}, 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 = FileHelper.GetFileName(mediaPlaylist.MediaSegments.Last().Url);
|
||||
var container = FileExtensionHelper.GuessContainerFormatFromSegmentExtension(Path.GetExtension(file));
|
||||
var video = new MultiSourceHLSDownloadInfo
|
||||
{
|
||||
VideoUri = message.Url,
|
||||
Headers = message.RequestHeaders,
|
||||
File = FileHelper.SanitizeFileName(message.File ?? FileHelper.GetFileName(new Uri(message.Url))) + ".ts",
|
||||
Cookies = message.Cookies,
|
||||
};
|
||||
var displayText = $"[{container}]";
|
||||
ApplicationContext.VideoTracker.AddVideoNotification(new StreamingVideoDisplayInfo
|
||||
{
|
||||
Quality = displayText,
|
||||
CreationTime = DateTime.Now
|
||||
}, video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ProcessYtDashSegment(Message message)
|
||||
{
|
||||
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, _) = ParsingHelper.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) = ParsingHelper.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))
|
||||
{
|
||||
if (!info.IsVideo && mime.StartsWith("audio/"))
|
||||
{
|
||||
HandleDashAudio(info, message);
|
||||
}
|
||||
var di = GetDASHPair(info);
|
||||
if (di == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var video = CreateDualSourceHTTPDownloadInfo(di, info, message);
|
||||
// new DualSourceHTTPDownloadInfo
|
||||
//{
|
||||
// Uri1 = di.Url,
|
||||
// Uri2 = info.Url,
|
||||
// Headers1 = di.Headers,
|
||||
// Headers2 = info.Headers,
|
||||
// File = FileHelper.SanitizeFileName(message.File ?? FileHelper.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 ? FormattingHelper.FormatSize(size) : string.Empty)}";
|
||||
ApplicationContext.VideoTracker.AddVideoNotification(new StreamingVideoDisplayInfo
|
||||
{
|
||||
Quality = displayText,
|
||||
Size = size,
|
||||
CreationTime = DateTime.Now
|
||||
}, video);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void HandleDashAudio(DashInfo info, Message message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var size = info.Length;
|
||||
Log.Debug("Itag: " + info.ITag + " " + info.ITag);
|
||||
var name = FileHelper.GetFileName(new Uri(info.Url));
|
||||
var ext = Path.GetExtension(name);
|
||||
|
||||
if (string.IsNullOrEmpty(ext))
|
||||
{
|
||||
ext = info.Mime.Contains("webm") ? ".webm" : info.Mime.Contains("mp4") ? ".mp4" : "mkv";
|
||||
}
|
||||
|
||||
var quality = ext.Substring(1)?.ToUpperInvariant();
|
||||
var displayText = $"[{quality} AUDIO] {(size > 0 ? FormattingHelper.FormatSize(size) : string.Empty)}";
|
||||
|
||||
var video = new SingleSourceHTTPDownloadInfo
|
||||
{
|
||||
Uri = info.Url,
|
||||
Headers = info.Headers,
|
||||
File = FileHelper.SanitizeFileName(message.File ?? FileHelper.GetFileName(new Uri(message.Url))) + ext,
|
||||
Cookies = info.Cookies,
|
||||
ContentLength = size,
|
||||
ContentType = info.Mime
|
||||
};
|
||||
|
||||
ApplicationContext.VideoTracker.AddVideoNotification(new StreamingVideoDisplayInfo
|
||||
{
|
||||
Quality = displayText,
|
||||
Size = size,
|
||||
CreationTime = DateTime.Now
|
||||
}, video);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static DualSourceHTTPDownloadInfo CreateDualSourceHTTPDownloadInfo(DashInfo info1, DashInfo info2, Message message)
|
||||
{
|
||||
var (video, audio) = info1.IsVideo ? (info1, info2) : (info2, info1);
|
||||
return new DualSourceHTTPDownloadInfo
|
||||
{
|
||||
Uri1 = video.Url,
|
||||
Uri2 = audio.Url,
|
||||
Headers1 = video.Headers,
|
||||
Headers2 = audio.Headers,
|
||||
File = FileHelper.SanitizeFileName(message.File ?? FileHelper.GetFileName(new Uri(message.Url))) + ".mkv",
|
||||
Cookies1 = video.Cookies,
|
||||
Cookies2 = audio.Cookies,
|
||||
ContentLength = video.Length + audio.Length
|
||||
};
|
||||
}
|
||||
|
||||
internal static void ProcessNormalVideo(Message message2)
|
||||
{
|
||||
if (IsMediaFragment(message2.GetRequestHeaderFirstValue("Referer")))
|
||||
{
|
||||
Log.Debug($"Skipping url:{message2.Url} as it seems a media fragment");
|
||||
return;
|
||||
}
|
||||
|
||||
var file = (message2.File ?? FileHelper.GetFileName(new Uri(message2.Url)));
|
||||
var type = message2.GetResponseHeaderFirstValue("Content-Type")?.ToLowerInvariant() ?? string.Empty;
|
||||
var len = message2.GetContentLength();
|
||||
if (string.IsNullOrEmpty(file))
|
||||
{
|
||||
file = FileHelper.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 = FileHelper.SanitizeFileName(file) + "." + ext,
|
||||
Cookies = message2.Cookies,
|
||||
ContentLength = len
|
||||
};
|
||||
|
||||
var size = long.Parse(message2.GetResponseHeaderFirstValue("Content-Length"));
|
||||
var displayText = $"[{ext.ToUpperInvariant()}] {(size > 0 ? FormattingHelper.FormatSize(size) : string.Empty)}";
|
||||
ApplicationContext.VideoTracker.AddVideoNotification(new StreamingVideoDisplayInfo
|
||||
{
|
||||
Quality = displayText,
|
||||
Size = size,
|
||||
CreationTime = DateTime.Now
|
||||
}, 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,60 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
using XDM.Core.UI;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
internal static class CallbackActions
|
||||
{
|
||||
public static void DownloadStarted(string id)
|
||||
{
|
||||
var download = ApplicationContext.MainWindow.FindInProgressItem(id);
|
||||
if (download == null) return;
|
||||
download.Status = DownloadStatus.Downloading;
|
||||
}
|
||||
|
||||
public static void DownloadFailed(string id)
|
||||
{
|
||||
var download = ApplicationContext.MainWindow.FindInProgressItem(id);
|
||||
if (download == null) return;
|
||||
download.Status = DownloadStatus.Stopped;
|
||||
}
|
||||
|
||||
public static void DownloadFinished(string id, long finalFileSize, string filePath, Action callback)
|
||||
{
|
||||
Log.Debug("Final file name: " + filePath);
|
||||
var download = ApplicationContext.MainWindow.FindInProgressItem(id);
|
||||
if (download == null) return;
|
||||
var downloadEntry = download.DownloadEntry;
|
||||
downloadEntry.Progress = 100;
|
||||
|
||||
var finishedEntry = new FinishedDownloadItem
|
||||
{
|
||||
Name = Path.GetFileName(filePath),
|
||||
Id = downloadEntry.Id,
|
||||
DateAdded = downloadEntry.DateAdded,
|
||||
Size = downloadEntry.Size > 0 ? downloadEntry.Size : finalFileSize,
|
||||
DownloadType = downloadEntry.DownloadType,
|
||||
TargetDir = Path.GetDirectoryName(filePath)!,
|
||||
PrimaryUrl = downloadEntry.PrimaryUrl,
|
||||
Authentication = downloadEntry.Authentication,
|
||||
Proxy = downloadEntry.Proxy
|
||||
};
|
||||
|
||||
ApplicationContext.MainWindow.AddToTop(finishedEntry);
|
||||
ApplicationContext.MainWindow.Delete(download);
|
||||
|
||||
QueueManager.RemoveFinishedDownload(download.DownloadEntry.Id);
|
||||
|
||||
if (ApplicationContext.CoreService.ActiveDownloadCount == 0 && ApplicationContext.MainWindow.IsInProgressViewSelected)
|
||||
{
|
||||
Log.Debug("switching to finished listview");
|
||||
ApplicationContext.MainWindow.SwitchToFinishedView();
|
||||
}
|
||||
|
||||
callback.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public class CancelFlag
|
||||
{
|
||||
public static CancelFlag None = new CancelFlag();
|
||||
|
||||
private bool _cancelled = false;
|
||||
private ReaderWriterLockSlim locker = new(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
try
|
||||
{
|
||||
locker.EnterWriteLock();
|
||||
this._cancelled = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCancellationRequested
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
locker.EnterReadLock();
|
||||
return this._cancelled;
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ThrowIfCancellationRequested()
|
||||
{
|
||||
try
|
||||
{
|
||||
locker.EnterReadLock();
|
||||
if (this._cancelled)
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public struct Category
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public HashSet<string> FileExtensions { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public bool IsPredefined { get; set; }
|
||||
public string DefaultFolder { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return DisplayName;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
#if NET5_0_OR_GREATER
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
internal class DotNetHttpClient : IHttpClient
|
||||
{
|
||||
private bool disposed;
|
||||
private HttpClient? hc;
|
||||
private CancellationTokenSource cts = new();
|
||||
private ProxyInfo? proxy;
|
||||
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100);
|
||||
|
||||
internal DotNetHttpClient(ProxyInfo? proxy)
|
||||
{
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRequest(Uri uri, HttpMethod method,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("DotNetHttpClient");
|
||||
}
|
||||
|
||||
var http = new HttpRequestMessage
|
||||
{
|
||||
Method = method,
|
||||
RequestUri = uri
|
||||
};
|
||||
|
||||
lock (this)
|
||||
{
|
||||
if (this.hc == null)
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
PreAuthenticate = true,
|
||||
UseDefaultCredentials = true,
|
||||
MaxConnectionsPerServer = 100,
|
||||
ServerCertificateCustomValidationCallback = (a, b, c, d) => true
|
||||
};
|
||||
|
||||
var p = ProxyHelper.GetProxy(this.proxy);
|
||||
if (p != null)
|
||||
{
|
||||
handler.Proxy = p;
|
||||
}
|
||||
|
||||
if (authentication != null && !string.IsNullOrEmpty(authentication.Value.UserName))
|
||||
{
|
||||
handler.Credentials = new NetworkCredential(authentication.Value.UserName, authentication.Value.Password);
|
||||
}
|
||||
|
||||
this.hc = new HttpClient(handler)
|
||||
{
|
||||
Timeout = this.Timeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var e in headers)
|
||||
{
|
||||
SetHeader(http, e.Key, e.Value);
|
||||
}
|
||||
}
|
||||
if (cookies != null)
|
||||
{
|
||||
foreach (var e in cookies)
|
||||
{
|
||||
SetHeader(http, e.Key, e.Value);
|
||||
}
|
||||
}
|
||||
return http;
|
||||
}
|
||||
|
||||
public HttpRequest CreateGetRequest(Uri uri,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null)
|
||||
{
|
||||
var req = this.CreateRequest(uri, HttpMethod.Get, headers, cookies, authentication);
|
||||
return new HttpRequest { Session = new DotNetHttpSession { Request = req } };
|
||||
}
|
||||
|
||||
public HttpRequest CreatePostRequest(Uri uri,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null,
|
||||
byte[]? body = null)
|
||||
{
|
||||
var req = this.CreateRequest(uri, HttpMethod.Post, headers, cookies, authentication);
|
||||
if (body != null)
|
||||
{
|
||||
req.Content = new ByteArrayContent(body);
|
||||
if (headers != null && headers.TryGetValue("Content-Type", out List<string>? values))
|
||||
{
|
||||
if (values != null && values.Count > 0)
|
||||
{
|
||||
req.Content.Headers.ContentType = new MediaTypeHeaderValue(values[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new HttpRequest { Session = new DotNetHttpSession { Request = req } };
|
||||
}
|
||||
|
||||
public HttpResponse Send(HttpRequest request)
|
||||
{
|
||||
HttpRequestMessage r;
|
||||
HttpResponseMessage? response = null;
|
||||
if (request.Session == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request.Session));
|
||||
}
|
||||
if (request.Session is not DotNetHttpSession session)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request.Session));
|
||||
}
|
||||
if (session.Request == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(session.Request));
|
||||
}
|
||||
r = session.Request;
|
||||
try
|
||||
{
|
||||
response = this.hc!.Send(r, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (HttpRequestException we)
|
||||
{
|
||||
Log.Debug(we, we.Message);
|
||||
response?.Dispose();
|
||||
}
|
||||
session.Response = response;
|
||||
return new HttpResponse { Session = session };
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposed = true;
|
||||
cts.Cancel();
|
||||
this.hc?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
this.Dispose();
|
||||
}
|
||||
|
||||
private void SetHeader(HttpRequestMessage request, string key, IEnumerable<string> values)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (!string.Equals(key, "Content-Type", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error setting header value");
|
||||
}
|
||||
}
|
||||
|
||||
private void SetHeader(HttpRequestMessage request, string key, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.Equals(key, "Content-Type", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation(key, value);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error setting header value");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,98 @@
|
|||
#if NET5_0_OR_GREATER
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
internal class DotNetHttpSession : IHttpSession
|
||||
{
|
||||
private CancellationTokenSource cancellationTokenSource = new();
|
||||
|
||||
internal CancellationToken CancellationToken => cancellationTokenSource.Token;
|
||||
|
||||
internal HttpRequestMessage? Request { get; set; }
|
||||
|
||||
internal HttpResponseMessage? Response { get; set; }
|
||||
|
||||
public string? ContentType => Response?.Content?.Headers?.ContentType?.MediaType;
|
||||
|
||||
public string? ContentDispositionFileName => GetContentDisposition();
|
||||
|
||||
public long ContentLength => Response?.Content?.Headers?.ContentLength ?? Response?.Content?.Headers?.ContentRange?.Length ?? -1;
|
||||
|
||||
public DateTime LastModified => Response!.Content?.Headers?.LastModified?.Date ?? DateTime.Now;
|
||||
|
||||
public HttpStatusCode StatusCode => Response!.StatusCode;
|
||||
|
||||
public Uri ResponseUri => Response!.RequestMessage!.RequestUri;
|
||||
|
||||
public void Abort()
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
Response?.Dispose();
|
||||
}
|
||||
|
||||
public void AddRange(long range)
|
||||
{
|
||||
Request!.Headers.Range = new RangeHeaderValue(range, null);
|
||||
}
|
||||
|
||||
public void AddRange(long start, long end)
|
||||
{
|
||||
Request!.Headers.Range = new RangeHeaderValue(start, end);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Response?.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Response?.Dispose();
|
||||
}
|
||||
|
||||
public Stream GetResponseStream()
|
||||
{
|
||||
return Response!.Content.ReadAsStreamAsync(this.CancellationToken).Result;
|
||||
}
|
||||
|
||||
public string? ReadAsString(CancelFlag cancellationToken)
|
||||
{
|
||||
return Response?.Content.ReadAsStringAsync(this.CancellationToken).Result;
|
||||
}
|
||||
|
||||
public void EnsureSuccessStatusCode()
|
||||
{
|
||||
WebRequestExtensions.EnsureSuccessStatusCode(Response!.StatusCode, Response!.ReasonPhrase);
|
||||
}
|
||||
|
||||
private string? GetContentDisposition()
|
||||
{
|
||||
if (Response!.Content.Headers.TryGetValues("Content-Disposition", out IEnumerable<string>? values) && values != null)
|
||||
{
|
||||
var cd = values.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(cd))
|
||||
{
|
||||
return WebRequestExtensions.GetContentDispositionFileName(cd!);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public long GetTotalLengthFromContentRange()
|
||||
{
|
||||
var len = Response?.Content?.Headers?.ContentRange?.Length ?? -1;
|
||||
return len;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
public static class HttpClientFactory
|
||||
{
|
||||
public static IHttpClient NewHttpClient(ProxyInfo? proxyInfo)
|
||||
{
|
||||
ProxyInfo? proxy = null;
|
||||
if (proxyInfo.HasValue)
|
||||
{
|
||||
if (proxyInfo.Value.ProxyType != ProxyType.Custom)
|
||||
{
|
||||
proxy = proxyInfo;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(proxyInfo.Value.Host) && proxyInfo.Value.Port > 0)
|
||||
{
|
||||
proxy = proxyInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if (Environment.Version.Major == 2)
|
||||
{
|
||||
return new WinHttpClient(proxy);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
return new DotNetHttpClient(proxy);
|
||||
#else
|
||||
return new NetFxHttpClient(proxy);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
public class HttpRequest
|
||||
{
|
||||
//private Uri uri;
|
||||
//private Dictionary<string, List<string>>? headers = null;
|
||||
//private Dictionary<string, string>? cookies = null;
|
||||
//private AuthenticationInfo? authentication = null;
|
||||
//private long rangeStart = 0, rangeEnd = -1;
|
||||
|
||||
//public HttpRequest(Uri uri,
|
||||
// Dictionary<string, List<string>>? headers,
|
||||
// Dictionary<string, string>? cookies,
|
||||
// AuthenticationInfo? authentication)
|
||||
//{
|
||||
// this.uri = uri;
|
||||
// this.headers = headers;
|
||||
// this.cookies = cookies;
|
||||
// this.authentication = authentication;
|
||||
//}
|
||||
|
||||
//public Uri Uri => uri;
|
||||
//public Dictionary<string, List<string>>? Headers => headers;
|
||||
//public Dictionary<string, string>? Cookies => cookies;
|
||||
//public AuthenticationInfo? Authentication => authentication;
|
||||
//public long RangeStart => rangeStart;
|
||||
//public long RangeEnd => rangeEnd;
|
||||
|
||||
internal IHttpSession? Session { get; set; }
|
||||
|
||||
public void AddRange(long range)
|
||||
{
|
||||
this.Session!.AddRange(range);
|
||||
}
|
||||
|
||||
public void AddRange(long start, long end)
|
||||
{
|
||||
this.Session!.AddRange(start, end);
|
||||
}
|
||||
|
||||
public void Abort()
|
||||
{
|
||||
this.Session?.Abort();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
public class HttpResponse : IDisposable
|
||||
{
|
||||
internal IHttpSession? Session { get; set; }
|
||||
public void EnsureSuccessStatusCode() => Session!.EnsureSuccessStatusCode();
|
||||
|
||||
public string ContentType => Session!.ContentType;
|
||||
|
||||
public HttpStatusCode StatusCode => Session!.StatusCode;
|
||||
|
||||
public long ContentLength => Session!.ContentLength;
|
||||
public long ContentRangeLength => Session!.GetTotalLengthFromContentRange();
|
||||
|
||||
public string ReadAsString(CancelFlag cancellationToken) => Session!.ReadAsString(cancellationToken);
|
||||
|
||||
public Uri ResponseUri => Session!.ResponseUri;
|
||||
|
||||
public string? ContentDispositionFileName => Session!.ContentDispositionFileName;
|
||||
|
||||
public DateTime LastModified => Session!.LastModified;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.Close();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Session?.Close();
|
||||
}
|
||||
|
||||
public Stream GetResponseStream() => Session!.GetResponseStream();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
class HttpSession
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
public interface IHttpClient : IDisposable
|
||||
{
|
||||
public TimeSpan Timeout { get; set; }
|
||||
|
||||
public HttpRequest CreateGetRequest(Uri uri,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null);
|
||||
|
||||
public HttpRequest CreatePostRequest(Uri uri,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null,
|
||||
byte[]? body = null);
|
||||
|
||||
public HttpResponse Send(HttpRequest request);
|
||||
public void Close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
interface IHttpClientFactory
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
internal interface IHttpSession : IDisposable
|
||||
{
|
||||
public void AddRange(long range);
|
||||
|
||||
public void AddRange(long start, long end);
|
||||
|
||||
public string? ContentType { get; }
|
||||
|
||||
public string? ContentDispositionFileName { get; }
|
||||
|
||||
public long ContentLength { get; }
|
||||
|
||||
public DateTime LastModified { get; }
|
||||
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
|
||||
public string ReadAsString(CancelFlag cancellationToken);
|
||||
|
||||
public void EnsureSuccessStatusCode();
|
||||
|
||||
public Uri ResponseUri { get; }
|
||||
|
||||
public void Close();
|
||||
|
||||
public Stream GetResponseStream();
|
||||
|
||||
public void Abort();
|
||||
|
||||
public long GetTotalLengthFromContentRange();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,246 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
internal class NetFxHttpClient : IHttpClient
|
||||
{
|
||||
private readonly string connectionGroupName = Guid.NewGuid().ToString();
|
||||
private HashSet<ServicePoint> servicePoints = new();
|
||||
private bool disposed;
|
||||
private ProxyInfo? proxy;
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100);
|
||||
|
||||
internal NetFxHttpClient(ProxyInfo? proxy)
|
||||
{
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
private HttpWebRequest CreateRequest(Uri uri)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("HttpWebRequestClient");
|
||||
}
|
||||
|
||||
var http = (HttpWebRequest)WebRequest.Create(uri);
|
||||
var p = ProxyHelper.GetProxy(this.proxy);
|
||||
if (p != null)
|
||||
{
|
||||
http.Proxy = p;
|
||||
}
|
||||
http.Timeout = http.ReadWriteTimeout = (int)Timeout.TotalMilliseconds;
|
||||
http.UseDefaultCredentials = true;
|
||||
http.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
http.AllowAutoRedirect = true;
|
||||
|
||||
http.ConnectionGroupName = this.connectionGroupName;
|
||||
var sp = http.ServicePoint;
|
||||
lock (servicePoints)
|
||||
{
|
||||
if (sp != null)
|
||||
{
|
||||
servicePoints.Add(sp);
|
||||
}
|
||||
}
|
||||
return http;
|
||||
}
|
||||
|
||||
public HttpRequest CreateGetRequest(Uri uri,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null)
|
||||
{
|
||||
var req = this.CreateRequest(uri);
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var e in headers)
|
||||
{
|
||||
SetHeader(req, e.Key, e.Value);
|
||||
}
|
||||
}
|
||||
if (cookies != null)
|
||||
{
|
||||
foreach (var e in cookies)
|
||||
{
|
||||
SetHeader(req, e.Key, e.Value);
|
||||
}
|
||||
}
|
||||
if (authentication != null && !string.IsNullOrEmpty(authentication.Value.UserName))
|
||||
{
|
||||
req.Credentials = new NetworkCredential(authentication.Value.UserName, authentication.Value.Password);
|
||||
}
|
||||
|
||||
return new HttpRequest { Session = new NetFxHttpSession { Request = req } };
|
||||
}
|
||||
|
||||
public HttpRequest CreatePostRequest(Uri uri,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null,
|
||||
byte[]? body = null)
|
||||
{
|
||||
var req = this.CreateRequest(uri);
|
||||
req.Method = "POST";
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var e in headers)
|
||||
{
|
||||
SetHeader(req, e.Key, e.Value);
|
||||
}
|
||||
}
|
||||
if (cookies != null)
|
||||
{
|
||||
foreach (var e in cookies)
|
||||
{
|
||||
SetHeader(req, e.Key, e.Value);
|
||||
}
|
||||
}
|
||||
if (authentication != null && !string.IsNullOrEmpty(authentication.Value.UserName))
|
||||
{
|
||||
req.Credentials = new NetworkCredential(authentication.Value.UserName, authentication.Value.Password);
|
||||
}
|
||||
if (body != null)
|
||||
{
|
||||
req.ContentLength = body.Length;
|
||||
using var rs = req.GetRequestStream();
|
||||
rs.Write(body, 0, body.Length);
|
||||
rs.Close();
|
||||
}
|
||||
return new HttpRequest { Session = new NetFxHttpSession { Request = req } };
|
||||
}
|
||||
|
||||
public HttpResponse Send(HttpRequest request)
|
||||
{
|
||||
HttpWebRequest r;
|
||||
HttpWebResponse response;
|
||||
if (request.Session == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request.Session));
|
||||
}
|
||||
if (request.Session is not NetFxHttpSession session)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request.Session));
|
||||
}
|
||||
if (session.Request == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(session.Request));
|
||||
}
|
||||
r = session.Request;
|
||||
try
|
||||
{
|
||||
response = (HttpWebResponse)r.GetResponse();
|
||||
}
|
||||
catch (WebException we)
|
||||
{
|
||||
Log.Debug(we, we.Message);
|
||||
if (we.Response == null)
|
||||
{
|
||||
throw new Exception("Connectivity error");
|
||||
}
|
||||
response = (HttpWebResponse?)we.Response!;
|
||||
response.Discard();
|
||||
response.Close();
|
||||
}
|
||||
|
||||
var servicePoint = r.ServicePoint;
|
||||
if (servicePoint != null)
|
||||
{
|
||||
servicePoints.Add(servicePoint);
|
||||
}
|
||||
session.Response = response;
|
||||
return new HttpResponse { Session = session };
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposed = true;
|
||||
foreach (var servicePoint in servicePoints)
|
||||
{
|
||||
Log.Debug("Disposing service point");
|
||||
Log.Debug("ConnectionName: " + servicePoint.ConnectionName +
|
||||
"\nCurrentConnections: " + servicePoint.CurrentConnections +
|
||||
"\nAddress: " + servicePoint.Address +
|
||||
"\nGetHashCode(): " + servicePoint.GetHashCode());
|
||||
servicePoint.CloseConnectionGroup(this.connectionGroupName);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
this.Dispose();
|
||||
}
|
||||
|
||||
private static void SetHeader(HttpWebRequest request, string key, IEnumerable<string> values)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
SetHeader(request, key, value);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error setting header value");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetHeader(HttpWebRequest request, string key, string value)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "accept":
|
||||
request.Accept = value;
|
||||
break;
|
||||
case "connection":
|
||||
request.Connection = value;
|
||||
break;
|
||||
case "content-type":
|
||||
request.ContentType = value;
|
||||
break;
|
||||
case "expect":
|
||||
request.Expect = value;
|
||||
break;
|
||||
#if !NET35
|
||||
case "date":
|
||||
request.Date = DateTime.Parse(value);
|
||||
break;
|
||||
case "host":
|
||||
request.Host = value;
|
||||
break;
|
||||
#endif
|
||||
case "if-modified-since":
|
||||
request.IfModifiedSince = DateTime.Parse(value);
|
||||
break;
|
||||
case "referer":
|
||||
request.Referer = value;
|
||||
break;
|
||||
case "user-agent":
|
||||
request.UserAgent = value;
|
||||
break;
|
||||
case "transfer-encoding":
|
||||
request.TransferEncoding = value;
|
||||
break;
|
||||
case "range":
|
||||
case "content-length":
|
||||
break;
|
||||
default:
|
||||
request.Headers.Add(key, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
internal class NetFxHttpSession : IHttpSession
|
||||
{
|
||||
internal HttpWebRequest? Request { get; set; }
|
||||
|
||||
internal HttpWebResponse? Response { get; set; }
|
||||
|
||||
public string ContentType => Response!.ContentType;
|
||||
|
||||
public string? ContentDispositionFileName => Response!.GetContentDispositionFileName();
|
||||
|
||||
public long ContentLength => Response!.GetContentLength();
|
||||
|
||||
public DateTime LastModified => Response!.LastModified;
|
||||
|
||||
public HttpStatusCode StatusCode => Response!.StatusCode;
|
||||
|
||||
public Uri ResponseUri => Response!.ResponseUri;
|
||||
|
||||
public void Abort()
|
||||
{
|
||||
try { Request!.Abort(); } catch { }
|
||||
}
|
||||
|
||||
public void AddRange(long range)
|
||||
{
|
||||
Request!.AddRange(range);
|
||||
}
|
||||
|
||||
public void AddRange(long start, long end)
|
||||
{
|
||||
Request!.AddRange(start, end);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Response!.Close();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
#if NET35
|
||||
Response!.Close();
|
||||
#else
|
||||
Response!.Dispose();
|
||||
#endif
|
||||
}
|
||||
|
||||
public Stream GetResponseStream()
|
||||
{
|
||||
return Response!.GetResponseStream();
|
||||
}
|
||||
|
||||
public string ReadAsString(CancelFlag cancellationToken)
|
||||
{
|
||||
return Response!.ReadAsString(cancellationToken);
|
||||
}
|
||||
|
||||
public void EnsureSuccessStatusCode()
|
||||
{
|
||||
Response!.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public long GetTotalLengthFromContentRange()
|
||||
{
|
||||
var contentRange = Response!.Headers.GetValues("Content-Range")?.First();
|
||||
return WebRequestExtensions.ContentLengthFromContentRange(contentRange);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
internal static class ProxyHelper
|
||||
{
|
||||
internal static IWebProxy? GetProxy(ProxyInfo? proxy)
|
||||
{
|
||||
if (proxy.HasValue)
|
||||
{
|
||||
Log.Debug("Proxy type: " + proxy.Value.ProxyType);
|
||||
if (proxy.Value.ProxyType == ProxyType.Direct)
|
||||
{
|
||||
return new WebProxy();
|
||||
}
|
||||
else if (proxy.Value.ProxyType == ProxyType.Custom)
|
||||
{
|
||||
var p = new WebProxy(proxy.Value.Host, proxy.Value.Port);
|
||||
if (!string.IsNullOrEmpty(proxy.Value.UserName))
|
||||
{
|
||||
p.Credentials = new NetworkCredential(proxy.Value.UserName, proxy.Value.Password);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
using XDM.Core.Util;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
public static class WebRequestExtensions
|
||||
{
|
||||
public static void EnsureSuccessStatusCode(this HttpWebResponse response)
|
||||
{
|
||||
if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.PartialContent)
|
||||
{
|
||||
throw new HttpException(response.StatusDescription, null, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void EnsureSuccessStatusCode(HttpStatusCode statusCode, string? statusDescription)
|
||||
{
|
||||
if (statusCode != HttpStatusCode.OK && statusCode != HttpStatusCode.PartialContent)
|
||||
{
|
||||
throw new HttpException(statusDescription ?? "Invalid response", null, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Discard(this HttpWebResponse response)
|
||||
{
|
||||
#if NET35
|
||||
var bytes = new byte[8192];
|
||||
#else
|
||||
var bytes = System.Buffers.ArrayPool<byte>.Shared.Rent(8192);
|
||||
#endif
|
||||
try
|
||||
{
|
||||
using var stream = response.GetResponseStream();
|
||||
while (true)
|
||||
{
|
||||
int x = stream.Read(bytes, 0, bytes.Length);
|
||||
if (x == 0) break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
#if !NET35
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(bytes);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public static long GetContentLength(this HttpWebResponse response)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (response.ContentLength > 0) return response.ContentLength;
|
||||
var contentRange = response.Headers.GetValues("Content-Range")?.First();
|
||||
return ContentLengthFromContentRange(contentRange);
|
||||
}
|
||||
catch { }
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static long ContentLengthFromContentRange(string? contentRange)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (contentRange == null) return -1;
|
||||
var index = contentRange.IndexOf('/');
|
||||
if (index == -1) return -1;
|
||||
if (Int64.TryParse(contentRange.Substring(index + 1).Trim(), out long result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static string? GetContentDispositionFileName(this HttpWebResponse response)
|
||||
{
|
||||
return GetContentDispositionFileName(response.Headers.Get("Content-Disposition"));
|
||||
}
|
||||
|
||||
public static string? GetContentDispositionFileName(string contentDisposition)
|
||||
{
|
||||
try
|
||||
{
|
||||
var r1 = new Regex(@"\s*filename\*\s*=\s*[^']*\'\s*\'(.*)");
|
||||
var r2 = new Regex("\\s*filename\\s*=\\s*\"([^\"]*)\"");
|
||||
var r3 = new Regex("filename\\s*=\\s*([^\"]+)");
|
||||
|
||||
//var contentDisposition = response.Headers.Get("Content-Disposition");
|
||||
if (contentDisposition != null)
|
||||
{
|
||||
Log.Debug("Trying to get filename from: " + contentDisposition);
|
||||
foreach (var r in new Regex[] { r1, r2, r3 })
|
||||
{
|
||||
var m = r.Match(contentDisposition);
|
||||
if (m.Success && m.Groups.Count >= 2)
|
||||
{
|
||||
return FileHelper.SanitizeFileName(Uri.UnescapeDataString(m.Groups[1].Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string ReadAsString(this HttpWebResponse response, CancelFlag cancellationToken)
|
||||
{
|
||||
#if NET35
|
||||
var buf = new byte[8192];
|
||||
#else
|
||||
var buf = System.Buffers.ArrayPool<byte>.Shared.Rent(8192);
|
||||
#endif
|
||||
try
|
||||
{
|
||||
var sourceStream = response.GetResponseStream();
|
||||
var ms = new MemoryStream();
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var x = sourceStream.Read(buf, 0, buf.Length);
|
||||
if (x == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
ms.Write(buf, 0, x);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
return Encoding.UTF8.GetString(ms.ToArray());
|
||||
}
|
||||
finally
|
||||
{
|
||||
#if !NET35
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(buf);
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if NET35
|
||||
|
||||
public static void AddRange(this HttpWebRequest request, long start, long end)
|
||||
{
|
||||
var method = typeof(WebHeaderCollection).GetMethod
|
||||
("AddWithoutValidate", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
|
||||
string key = "Range";
|
||||
string val = string.Format("bytes={0}-{1}", start, end);
|
||||
|
||||
method.Invoke(request.Headers, new object[] { key, val });
|
||||
}
|
||||
|
||||
public static void AddRange(this HttpWebRequest request, long start)
|
||||
{
|
||||
var method = typeof(WebHeaderCollection).GetMethod
|
||||
("AddWithoutValidate", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
|
||||
string key = "Range";
|
||||
string val = string.Format("bytes={0}-", start);
|
||||
|
||||
method.Invoke(request.Headers, new object[] { key, val });
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,347 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using XDM.Core;
|
||||
using static Interop.WinHttp;
|
||||
using System.Runtime.InteropServices;
|
||||
using TraceLog;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
internal class WinHttpClient : IHttpClient
|
||||
{
|
||||
private SafeWinHttpHandle? sessionHandle;
|
||||
private bool disposed;
|
||||
private ProxyInfo? proxy;
|
||||
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100);
|
||||
|
||||
internal WinHttpClient(ProxyInfo? proxy)
|
||||
{
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
private HttpRequest CreateRequest(
|
||||
Uri uri,
|
||||
string method,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null,
|
||||
byte[]? body = null)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("HttpWebRequestClient");
|
||||
}
|
||||
|
||||
lock (this)
|
||||
{
|
||||
if (this.sessionHandle == null)
|
||||
{
|
||||
if (this.proxy.HasValue && this.proxy.Value.ProxyType == ProxyType.System &&
|
||||
GetProxyForUrl(uri, out string? proxy, out string? bypass) && proxy != null)
|
||||
{
|
||||
sessionHandle = WinHttpOpen(
|
||||
IntPtr.Zero,
|
||||
WINHTTP_ACCESS_TYPE_NAMED_PROXY,
|
||||
proxy,
|
||||
bypass,
|
||||
0);
|
||||
}
|
||||
else if (this.proxy.HasValue && this.proxy.Value.ProxyType == ProxyType.Custom)
|
||||
{
|
||||
sessionHandle = WinHttpOpen(
|
||||
IntPtr.Zero,
|
||||
WINHTTP_ACCESS_TYPE_NAMED_PROXY,
|
||||
"http://" + this.proxy.Value.Host + ":" + this.proxy.Value.Port,
|
||||
WINHTTP_NO_PROXY_BYPASS,
|
||||
0);
|
||||
}
|
||||
else
|
||||
{
|
||||
sessionHandle = WinHttpOpen(
|
||||
IntPtr.Zero,
|
||||
WINHTTP_ACCESS_TYPE_NO_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME,
|
||||
WINHTTP_NO_PROXY_BYPASS,
|
||||
0);
|
||||
}
|
||||
|
||||
if (sessionHandle.IsInvalid)
|
||||
{
|
||||
throw new IOException(nameof(sessionHandle) + " " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
uint optionData = 100;
|
||||
if (!WinHttpSetOption(sessionHandle, WINHTTP_OPTION_MAX_CONNS_PER_SERVER, ref optionData))
|
||||
{
|
||||
Log.Debug("Unable to set connection limit");
|
||||
}
|
||||
|
||||
//const uint WINHTTP_FLAG_SECURE_PROTOCOL_SSL2 = 0x00000008;
|
||||
//const uint WINHTTP_FLAG_SECURE_PROTOCOL_SSL3 = 0x00000020;
|
||||
//const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 = 0x00000080;
|
||||
//const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 = 0x00000200;
|
||||
//const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 = 0x00000800;
|
||||
//const uint WINHTTP_FLAG_SECURE_PROTOCOL_ALL = (WINHTTP_FLAG_SECURE_PROTOCOL_SSL2 | WINHTTP_FLAG_SECURE_PROTOCOL_SSL3 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1);
|
||||
|
||||
var proto = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2;
|
||||
if (!WinHttpSetOption(sessionHandle, WINHTTP_OPTION_SECURE_PROTOCOLS, ref proto))
|
||||
{
|
||||
Log.Debug("WINHTTP_OPTION_SECURE_PROTOCOLS not set: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
var timeout = (int)Timeout.TotalMilliseconds;
|
||||
if (!WinHttpSetTimeouts(sessionHandle, timeout, timeout, timeout, timeout))
|
||||
{
|
||||
Log.Debug("Set WinHttpSetTimeouts failed: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hConnect = WinHttpConnect(sessionHandle, uri.Host, (ushort)uri.Port, 0);
|
||||
if (hConnect.IsInvalid)
|
||||
{
|
||||
throw new IOException(nameof(hConnect));
|
||||
}
|
||||
var hRequest = WinHttpOpenRequest(hConnect, method, uri.PathAndQuery,
|
||||
null, null, null,
|
||||
uri.Scheme == "https" ? WINHTTP_FLAG_ESCAPE_DISABLE | WINHTTP_FLAG_SECURE : WINHTTP_FLAG_ESCAPE_DISABLE);
|
||||
if (hRequest.IsInvalid)
|
||||
{
|
||||
throw new IOException(nameof(hRequest));
|
||||
}
|
||||
|
||||
var dwFlags = SECURITY_FLAG_IGNORE_UNKNOWN_CA |
|
||||
SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE |
|
||||
SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
|
||||
SECURITY_FLAG_IGNORE_CERT_DATE_INVALID;
|
||||
|
||||
if (!WinHttpSetOption(
|
||||
hRequest,
|
||||
WINHTTP_OPTION_SECURITY_FLAGS,
|
||||
ref dwFlags))
|
||||
{
|
||||
Log.Debug("Ignore cert error: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Ignore cert error config set");
|
||||
}
|
||||
|
||||
if (authentication.HasValue && !string.IsNullOrEmpty(authentication.Value.UserName))
|
||||
{
|
||||
if (!WinHttpSetCredentials(hRequest, WINHTTP_AUTH_TARGET_SERVER,
|
||||
WINHTTP_AUTH_SCHEME_BASIC, authentication.Value.UserName, authentication.Value.Password, IntPtr.Zero))
|
||||
{
|
||||
Log.Debug("Error WinHttpSetCredentials - server: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.proxy.HasValue && !string.IsNullOrEmpty(this.proxy.Value.UserName))
|
||||
{
|
||||
if (!WinHttpSetCredentials(hRequest, WINHTTP_AUTH_TARGET_PROXY,
|
||||
WINHTTP_AUTH_SCHEME_BASIC, authentication.Value.UserName, authentication.Value.Password, IntPtr.Zero))
|
||||
{
|
||||
Log.Debug("Error WinHttpSetCredentials - proxy: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
//dwFlags = WINHTTP_FLAG_SECURE_PROTOCOL_SSL3 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1;
|
||||
|
||||
//if (!WinHttpSetOption(
|
||||
// hConnect,
|
||||
// WINHTTP_OPTION_SECURE_PROTOCOLS,
|
||||
// ref dwFlags))
|
||||
//{
|
||||
// Log.Debug("WINHTTP_OPTION_SECURE_PROTOCOLS: " + Marshal.GetLastWin32Error());
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// Log.Debug("WINHTTP_OPTION_SECURE_PROTOCOLS set");
|
||||
//}
|
||||
|
||||
return new HttpRequest { Session = new WinHttpSession(uri, hConnect, hRequest, headers, cookies) };
|
||||
}
|
||||
|
||||
public HttpRequest CreateGetRequest(
|
||||
Uri uri,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null)
|
||||
{
|
||||
return CreateRequest(uri, "GET", headers, cookies, authentication);
|
||||
}
|
||||
|
||||
public HttpRequest CreatePostRequest(
|
||||
Uri uri,
|
||||
Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
AuthenticationInfo? authentication = null,
|
||||
byte[]? body = null)
|
||||
{
|
||||
return CreateRequest(uri, "POST", headers, cookies, authentication, body);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
disposed = true;
|
||||
this.sessionHandle?.Close();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public HttpResponse Send(HttpRequest request)
|
||||
{
|
||||
var session = (WinHttpSession)request.Session!;
|
||||
var headerBuf = PrepareHeaders(session.Headers, session.Cookies, session.RangeStart, session.RangeEnd);
|
||||
if (headerBuf.Length > 0)
|
||||
{
|
||||
if (!WinHttpAddRequestHeaders(session.RequestHandle, headerBuf, (uint)headerBuf.Length, WINHTTP_ADDREQ_FLAG_ADD))
|
||||
{
|
||||
Log.Debug("Unable to set headers");
|
||||
}
|
||||
}
|
||||
var lpOptional = IntPtr.Zero;
|
||||
var dwOptionalLength = 0;
|
||||
if (session.PostData.HasValue)
|
||||
{
|
||||
lpOptional = session.PostData.Value;
|
||||
dwOptionalLength = session.PostDataSize;
|
||||
}
|
||||
if (WinHttpSendRequest(session.RequestHandle, IntPtr.Zero, 0, lpOptional,
|
||||
(uint)dwOptionalLength, (uint)dwOptionalLength, IntPtr.Zero))
|
||||
{
|
||||
if (WinHttpReceiveResponse(session.RequestHandle, IntPtr.Zero))
|
||||
{
|
||||
var headerSize = CalculateHeaderBufferSize(session.RequestHandle);
|
||||
if (headerSize > 0)
|
||||
{
|
||||
var headers = GetHeaders(session.RequestHandle, headerSize);
|
||||
session.SetHeaders(headers);
|
||||
return new HttpResponse { Session = session };
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Failed CalculateHeaderBufferSize: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Failed WinHttpReceiveResponse: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Failed WinHttpSendRequest: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
private uint CalculateHeaderBufferSize(SafeWinHttpHandle hRequest)
|
||||
{
|
||||
uint bufferLengthInBytes = 0;
|
||||
uint index = 0;
|
||||
if (!WinHttpQueryHeaders(
|
||||
hRequest,
|
||||
WINHTTP_QUERY_RAW_HEADERS_CRLF,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
IntPtr.Zero,
|
||||
ref bufferLengthInBytes,
|
||||
ref index) && Marshal.GetLastWin32Error() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
return bufferLengthInBytes;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private string GetHeaders(SafeWinHttpHandle hRequest, uint bufferLengthInBytes)
|
||||
{
|
||||
uint index = 0;
|
||||
var pBuf = Marshal.AllocHGlobal((int)bufferLengthInBytes);
|
||||
if (!WinHttpQueryHeaders(
|
||||
hRequest,
|
||||
WINHTTP_QUERY_RAW_HEADERS_CRLF,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
pBuf,
|
||||
ref bufferLengthInBytes,
|
||||
ref index))
|
||||
{
|
||||
throw new IOException("Unable to read headers: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
Log.Debug("Header len after read: " + bufferLengthInBytes);
|
||||
var header = Marshal.PtrToStringAuto(pBuf);
|
||||
Marshal.FreeHGlobal(pBuf);
|
||||
return header;
|
||||
}
|
||||
|
||||
private StringBuilder PrepareHeaders(Dictionary<string, List<string>>? headers = null,
|
||||
Dictionary<string, string>? cookies = null,
|
||||
long rangeStart = -1, long rangeEnd = -1)
|
||||
{
|
||||
var buf = new StringBuilder();
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var key in headers.Keys)
|
||||
{
|
||||
buf.Append(key).Append(": ").Append(string.Join(", ", headers[key].ToArray())).Append("\r\n");
|
||||
}
|
||||
}
|
||||
if (cookies != null && cookies.Count > 0)
|
||||
{
|
||||
buf.Append("Cookie: ");
|
||||
var first = true;
|
||||
foreach (var key in cookies.Keys)
|
||||
{
|
||||
if (!first) buf.Append(", ");
|
||||
buf.Append(cookies[key]);
|
||||
first = false;
|
||||
}
|
||||
buf.Append("\r\n");
|
||||
}
|
||||
if (rangeStart > 0 && rangeEnd > 0)
|
||||
{
|
||||
buf.Append(string.Format("Range: bytes={0}-{1}\r\n", rangeStart, rangeEnd));
|
||||
}
|
||||
else if (rangeStart > 0)
|
||||
{
|
||||
buf.Append(string.Format("Range: bytes={0}-\r\n", rangeStart));
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private bool GetProxyForUrl(Uri url, out string? proxyHost, out string? bypass)
|
||||
{
|
||||
proxyHost = null;
|
||||
bypass = null;
|
||||
var handle = WinHttpOpen(
|
||||
IntPtr.Zero,
|
||||
WINHTTP_ACCESS_TYPE_NO_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME,
|
||||
WINHTTP_NO_PROXY_BYPASS,
|
||||
0);
|
||||
try
|
||||
{
|
||||
var _proxyHelper = new WinInetProxyHelper();
|
||||
if (_proxyHelper.GetProxyForUrl(handle, url, out WINHTTP_PROXY_INFO info))
|
||||
{
|
||||
proxyHost = Marshal.PtrToStringAuto(info.Proxy);
|
||||
bypass = Marshal.PtrToStringAuto(info.ProxyBypass);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using static Interop.WinHttp;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
internal class WinHttpResponseStream : Stream
|
||||
{
|
||||
private SafeWinHttpHandle hRequest;
|
||||
|
||||
public WinHttpResponseStream(SafeWinHttpHandle hRequest)
|
||||
{
|
||||
this.hRequest = hRequest;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => -1;
|
||||
|
||||
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
lock (hRequest)
|
||||
{
|
||||
var pData = Marshal.AllocHGlobal(count);
|
||||
try
|
||||
{
|
||||
var ret = WinHttpReadData(hRequest, pData, (uint)count, out long lpdwNumberOfBytesAvailable);
|
||||
if (ret && lpdwNumberOfBytesAvailable == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if (lpdwNumberOfBytesAvailable == 0)
|
||||
{
|
||||
throw new IOException("Unable to read from reponse");
|
||||
}
|
||||
Marshal.Copy(pData, buffer, offset, (int)lpdwNumberOfBytesAvailable);
|
||||
return (int)lpdwNumberOfBytesAvailable;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(pData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[DllImport("winhttp.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool WinHttpReadData(
|
||||
SafeWinHttpHandle requestHandle,
|
||||
IntPtr buffer,
|
||||
uint bufferSize,
|
||||
out long lpdwNumberOfBytesAvailable);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
using static Interop.WinHttp;
|
||||
|
||||
namespace XDM.Core.Clients.Http
|
||||
{
|
||||
internal class WinHttpSession : IHttpSession
|
||||
{
|
||||
private SafeWinHttpHandle hConnect, hRequest;
|
||||
private WINHTTP_STATUS_CALLBACK callback;
|
||||
private Uri responseUri;
|
||||
private HttpStatusCode statusCode;
|
||||
private string? statusDescription;
|
||||
private long contentLength;
|
||||
private string contentRange;
|
||||
private string? contentType;
|
||||
private string? contentDispositionFileName;
|
||||
private DateTime lastModified = DateTime.Now;
|
||||
private long rangeStart = -1, rangeEnd = -1;
|
||||
private Dictionary<string, List<string>>? headers = null;
|
||||
private Dictionary<string, string>? cookies = null;
|
||||
private Stream responseStream;
|
||||
private IntPtr? postData;
|
||||
private int postDataSize;
|
||||
|
||||
public Dictionary<string, List<string>>? Headers => this.headers;
|
||||
public Dictionary<string, string>? Cookies => this.cookies;
|
||||
|
||||
public SafeWinHttpHandle ConnectHandle => hConnect;
|
||||
public SafeWinHttpHandle RequestHandle => hRequest;
|
||||
public IntPtr? PostData => postData;
|
||||
public int PostDataSize => postDataSize;
|
||||
|
||||
public WinHttpSession(
|
||||
Uri responseUri,
|
||||
SafeWinHttpHandle hConnect,
|
||||
SafeWinHttpHandle hRequest,
|
||||
Dictionary<string, List<string>>? headers,
|
||||
Dictionary<string, string>? cookies,
|
||||
byte[]? postData = null)
|
||||
{
|
||||
this.responseUri = responseUri;
|
||||
this.hConnect = hConnect;
|
||||
this.hRequest = hRequest;
|
||||
this.callback = new(WinHttpCallback);
|
||||
IntPtr oldCallback = WinHttpSetStatusCallback(
|
||||
hRequest,
|
||||
this.callback,
|
||||
WINHTTP_CALLBACK_FLAG_REDIRECT,
|
||||
IntPtr.Zero);
|
||||
if (oldCallback == new IntPtr(WINHTTP_INVALID_STATUS_CALLBACK))
|
||||
{
|
||||
int lastError = Marshal.GetLastWin32Error();
|
||||
if (lastError != ERROR_INVALID_HANDLE) // Ignore error if handle was already closed.
|
||||
{
|
||||
throw new IOException(nameof(WinHttpSetStatusCallback));
|
||||
}
|
||||
}
|
||||
this.headers = headers;
|
||||
this.cookies = cookies;
|
||||
if (postData != null && postData.Length > 0)
|
||||
{
|
||||
this.postData = Marshal.AllocHGlobal(postData.Length);
|
||||
Marshal.Copy(postData, 0, this.postData.Value, postData.Length);
|
||||
this.postDataSize = postData.Length;
|
||||
}
|
||||
this.responseStream = new WinHttpResponseStream(this.hRequest);
|
||||
}
|
||||
|
||||
private void WinHttpCallback(
|
||||
IntPtr handle,
|
||||
IntPtr context,
|
||||
uint internetStatus,
|
||||
IntPtr statusInformation,
|
||||
uint statusInformationLength)
|
||||
{
|
||||
if (internetStatus == WINHTTP_CALLBACK_STATUS_REDIRECT)
|
||||
{
|
||||
responseUri = new Uri(Marshal.PtrToStringUni(statusInformation)!);
|
||||
Log.Debug("Redirected to: " + responseUri);
|
||||
}
|
||||
}
|
||||
|
||||
public string? ContentType => this.contentType;
|
||||
|
||||
public string? ContentDispositionFileName => this.contentDispositionFileName;
|
||||
|
||||
public long ContentLength => this.contentLength;
|
||||
|
||||
public DateTime LastModified => this.lastModified;
|
||||
|
||||
public HttpStatusCode StatusCode => this.statusCode;
|
||||
|
||||
public Uri ResponseUri => responseUri;
|
||||
|
||||
public long RangeEnd => rangeEnd;
|
||||
|
||||
public long RangeStart => rangeStart;
|
||||
|
||||
public void Abort()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
public void AddRange(long range)
|
||||
{
|
||||
this.rangeStart = range;
|
||||
}
|
||||
|
||||
public void AddRange(long start, long end)
|
||||
{
|
||||
this.rangeStart = start;
|
||||
this.rangeEnd = end;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
this.RequestHandle.Close();
|
||||
this.ConnectHandle.Close();
|
||||
if (this.postData.HasValue)
|
||||
{
|
||||
Marshal.FreeHGlobal(this.postData.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
public void EnsureSuccessStatusCode()
|
||||
{
|
||||
if (statusCode != HttpStatusCode.OK && statusCode != HttpStatusCode.PartialContent)
|
||||
{
|
||||
throw new HttpException(statusDescription, null, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
public Stream GetResponseStream()
|
||||
{
|
||||
return this.responseStream;
|
||||
}
|
||||
|
||||
public string ReadAsString(CancelFlag cancellationToken)
|
||||
{
|
||||
#if NET35
|
||||
var buf = new byte[8192];
|
||||
#else
|
||||
var buf = System.Buffers.ArrayPool<byte>.Shared.Rent(8192);
|
||||
#endif
|
||||
try
|
||||
{
|
||||
var sourceStream = responseStream;
|
||||
var ms = new MemoryStream();
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var x = sourceStream.Read(buf, 0, buf.Length);
|
||||
if (x == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
ms.Write(buf, 0, x);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
return Encoding.UTF8.GetString(ms.ToArray());
|
||||
}
|
||||
finally
|
||||
{
|
||||
#if !NET35
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(buf);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetHeaders(string headers)
|
||||
{
|
||||
var lines = headers.Split('\r', '\n');
|
||||
var status = lines[0];
|
||||
this.statusCode = (HttpStatusCode)Int32.Parse(status.Split(' ')[1].Trim());
|
||||
this.statusDescription = status.Split(' ')[2];
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var index = line.IndexOf(':');
|
||||
if (index > 0)
|
||||
{
|
||||
var key = line.Substring(0, index).ToLowerInvariant();
|
||||
var value = line.Substring(index + 1).Trim();
|
||||
switch (key)
|
||||
{
|
||||
case "content-length":
|
||||
this.contentLength = Int64.Parse(value);
|
||||
break;
|
||||
case "content-range":
|
||||
this.contentRange = value;
|
||||
break;
|
||||
case "content-type":
|
||||
this.contentType = value;
|
||||
break;
|
||||
case "content-disposition":
|
||||
this.contentDispositionFileName = WebRequestExtensions.GetContentDispositionFileName(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long GetTotalLengthFromContentRange()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(contentRange))
|
||||
{
|
||||
return WebRequestExtensions.ContentLengthFromContentRange(contentRange);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,184 @@
|
|||
// 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 SafeWinHttpHandle = Interop.WinHttp.SafeWinHttpHandle;
|
||||
|
||||
namespace System.Net.Http
|
||||
{
|
||||
// This class is only used on OS versions where WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY
|
||||
// is not supported (i.e. before Win8.1/Win2K12R2) in the WinHttpOpen() function.
|
||||
internal sealed class WinInetProxyHelper
|
||||
{
|
||||
private const int RecentAutoDetectionInterval = 120_000; // 2 minutes in milliseconds.
|
||||
private readonly string? _autoConfigUrl, _proxy, _proxyBypass;
|
||||
private readonly bool _autoDetect;
|
||||
private readonly bool _useProxy;
|
||||
private bool _autoDetectionFailed;
|
||||
private int _lastTimeAutoDetectionFailed; // Environment.TickCount units (milliseconds).
|
||||
|
||||
public WinInetProxyHelper()
|
||||
{
|
||||
Interop.WinHttp.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig = default;
|
||||
|
||||
try
|
||||
{
|
||||
if (Interop.WinHttp.WinHttpGetIEProxyConfigForCurrentUser(out proxyConfig))
|
||||
{
|
||||
_autoConfigUrl = Marshal.PtrToStringUni(proxyConfig.AutoConfigUrl)!;
|
||||
_autoDetect = proxyConfig.AutoDetect;
|
||||
_proxy = Marshal.PtrToStringUni(proxyConfig.Proxy)!;
|
||||
_proxyBypass = Marshal.PtrToStringUni(proxyConfig.ProxyBypass)!;
|
||||
|
||||
_useProxy = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We match behavior of WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY and ignore errors.
|
||||
int lastError = Marshal.GetLastWin32Error();
|
||||
}
|
||||
}
|
||||
|
||||
finally
|
||||
{
|
||||
// FreeHGlobal already checks for null pointer before freeing the memory.
|
||||
Marshal.FreeHGlobal(proxyConfig.AutoConfigUrl);
|
||||
Marshal.FreeHGlobal(proxyConfig.Proxy);
|
||||
Marshal.FreeHGlobal(proxyConfig.ProxyBypass);
|
||||
}
|
||||
}
|
||||
|
||||
public string? AutoConfigUrl => _autoConfigUrl;
|
||||
|
||||
public bool AutoDetect => _autoDetect;
|
||||
|
||||
public bool AutoSettingsUsed => AutoDetect || !string.IsNullOrEmpty(AutoConfigUrl);
|
||||
|
||||
public bool ManualSettingsUsed => !string.IsNullOrEmpty(Proxy);
|
||||
|
||||
public bool ManualSettingsOnly => !AutoSettingsUsed && ManualSettingsUsed;
|
||||
|
||||
public string? Proxy => _proxy;
|
||||
|
||||
public string? ProxyBypass => _proxyBypass;
|
||||
|
||||
public bool RecentAutoDetectionFailure =>
|
||||
_autoDetectionFailed &&
|
||||
Environment.TickCount - _lastTimeAutoDetectionFailed <= RecentAutoDetectionInterval;
|
||||
|
||||
public bool GetProxyForUrl(
|
||||
SafeWinHttpHandle? sessionHandle,
|
||||
Uri uri,
|
||||
out Interop.WinHttp.WINHTTP_PROXY_INFO proxyInfo)
|
||||
{
|
||||
proxyInfo.AccessType = Interop.WinHttp.WINHTTP_ACCESS_TYPE_NO_PROXY;
|
||||
proxyInfo.Proxy = IntPtr.Zero;
|
||||
proxyInfo.ProxyBypass = IntPtr.Zero;
|
||||
|
||||
if (!_useProxy)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool useProxy = false;
|
||||
|
||||
Interop.WinHttp.WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions;
|
||||
autoProxyOptions.AutoConfigUrl = AutoConfigUrl;
|
||||
autoProxyOptions.AutoDetectFlags = AutoDetect ?
|
||||
(Interop.WinHttp.WINHTTP_AUTO_DETECT_TYPE_DHCP | Interop.WinHttp.WINHTTP_AUTO_DETECT_TYPE_DNS_A) : 0;
|
||||
autoProxyOptions.AutoLoginIfChallenged = false;
|
||||
autoProxyOptions.Flags =
|
||||
(AutoDetect ? Interop.WinHttp.WINHTTP_AUTOPROXY_AUTO_DETECT : 0) |
|
||||
(!string.IsNullOrEmpty(AutoConfigUrl) ? Interop.WinHttp.WINHTTP_AUTOPROXY_CONFIG_URL : 0);
|
||||
autoProxyOptions.Reserved1 = IntPtr.Zero;
|
||||
autoProxyOptions.Reserved2 = 0;
|
||||
|
||||
// AutoProxy Cache.
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/WinHttp/autoproxy-cache
|
||||
// If the out-of-process service is active when WinHttpGetProxyForUrl is called, the cached autoproxy
|
||||
// URL and script are available to the whole computer. However, if the out-of-process service is used,
|
||||
// and the fAutoLogonIfChallenged flag in the pAutoProxyOptions structure is true, then the autoproxy
|
||||
// URL and script are not cached. Therefore, calling WinHttpGetProxyForUrl with the fAutoLogonIfChallenged
|
||||
// member set to TRUE results in additional overhead operations that may affect performance.
|
||||
// The following steps can be used to improve performance:
|
||||
// 1. Call WinHttpGetProxyForUrl with the fAutoLogonIfChallenged parameter set to false. The autoproxy
|
||||
// URL and script are cached for future calls to WinHttpGetProxyForUrl.
|
||||
// 2. If Step 1 fails, with ERROR_WINHTTP_LOGIN_FAILURE, then call WinHttpGetProxyForUrl with the
|
||||
// fAutoLogonIfChallenged member set to TRUE.
|
||||
//
|
||||
// We match behavior of WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY and ignore errors.
|
||||
|
||||
#pragma warning disable CA1845 // file is shared with a build that lacks string.Concat for spans
|
||||
// Underlying code does not understand WebSockets so we need to convert it to http or https.
|
||||
string destination = uri.AbsoluteUri;
|
||||
if (uri.Scheme == "wss")
|
||||
{
|
||||
destination = "https" + destination.Substring("wss".Length);
|
||||
}
|
||||
else if (uri.Scheme == "ws")
|
||||
{
|
||||
destination = "http" + destination.Substring("ws".Length);
|
||||
}
|
||||
#pragma warning restore CA1845
|
||||
|
||||
var repeat = false;
|
||||
do
|
||||
{
|
||||
_autoDetectionFailed = false;
|
||||
if (Interop.WinHttp.WinHttpGetProxyForUrl(
|
||||
sessionHandle!,
|
||||
destination,
|
||||
ref autoProxyOptions,
|
||||
out proxyInfo))
|
||||
{
|
||||
useProxy = true;
|
||||
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
var lastError = Marshal.GetLastWin32Error();
|
||||
|
||||
if (lastError == Interop.WinHttp.ERROR_WINHTTP_LOGIN_FAILURE)
|
||||
{
|
||||
if (repeat)
|
||||
{
|
||||
// We don't retry more than once.
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
repeat = true;
|
||||
autoProxyOptions.AutoLoginIfChallenged = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lastError == Interop.WinHttp.ERROR_WINHTTP_AUTODETECTION_FAILED)
|
||||
{
|
||||
_autoDetectionFailed = true;
|
||||
_lastTimeAutoDetectionFailed = Environment.TickCount;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (repeat);
|
||||
|
||||
// Fall back to manual settings if available.
|
||||
if (!useProxy && !string.IsNullOrEmpty(Proxy))
|
||||
{
|
||||
proxyInfo.AccessType = Interop.WinHttp.WINHTTP_ACCESS_TYPE_NAMED_PROXY;
|
||||
proxyInfo.Proxy = Marshal.StringToHGlobalUni(Proxy);
|
||||
proxyInfo.ProxyBypass = string.IsNullOrEmpty(ProxyBypass) ?
|
||||
IntPtr.Zero : Marshal.StringToHGlobalUni(ProxyBypass);
|
||||
|
||||
useProxy = true;
|
||||
}
|
||||
|
||||
return useProxy;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using TraceLog;
|
||||
using XDM.Core.Util;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public interface IClipboardMonitor
|
||||
{
|
||||
void Start();
|
||||
void Stop();
|
||||
}
|
||||
|
||||
public class ClipboardMonitor : IClipboardMonitor
|
||||
{
|
||||
private bool isClipboardMonitorActive = false;
|
||||
private string lastClipboardText;
|
||||
|
||||
public ClipboardMonitor()
|
||||
{
|
||||
ApplicationContext.ApplicationEvent += ApplicationContext_ApplicationEvent;
|
||||
}
|
||||
|
||||
private void ApplicationContext_ApplicationEvent(object sender, ApplicationEvent e)
|
||||
{
|
||||
if (e.EventType == "ConfigChanged")
|
||||
{
|
||||
if (Config.Instance.MonitorClipboard)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Log.Debug("StartClipboardMonitor");
|
||||
if (isClipboardMonitorActive) return;
|
||||
var cm = ApplicationContext.Application.GetPlatformClipboardMonitor();
|
||||
if (Config.Instance.MonitorClipboard)
|
||||
{
|
||||
cm.StartClipboardMonitoring();
|
||||
isClipboardMonitorActive = true;
|
||||
cm.ClipboardChanged += Cm_ClipboardChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!isClipboardMonitorActive) return;
|
||||
var cm = ApplicationContext.Application.GetPlatformClipboardMonitor();
|
||||
cm.StopClipboardMonitoring();
|
||||
isClipboardMonitorActive = false;
|
||||
cm.ClipboardChanged -= Cm_ClipboardChanged;
|
||||
}
|
||||
|
||||
private void Cm_ClipboardChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var text = ApplicationContext.Application.GetPlatformClipboardMonitor().GetClipboardText();
|
||||
if (!string.IsNullOrEmpty(text) && Helpers.IsUriValid(text) && text != lastClipboardText)
|
||||
{
|
||||
lastClipboardText = text;
|
||||
ApplicationContext.CoreService.AddDownload(new Message { Url = text });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace XDM.Core.Collections
|
||||
{
|
||||
public class GenericOrderedDictionary<K, V> : IDictionary<K, V>
|
||||
{
|
||||
private readonly List<K> keys = new();
|
||||
private readonly Dictionary<K, V> dict = new();
|
||||
|
||||
public V this[K key]
|
||||
{
|
||||
get => dict[key];
|
||||
set
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (!dict.ContainsKey(key))
|
||||
{
|
||||
keys.Add(key);
|
||||
}
|
||||
dict[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<K> Keys => keys;
|
||||
|
||||
public ICollection<V> Values
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return keys.Select(key => dict[key]).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => keys.Count;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Add(K key, V value)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (!dict.ContainsKey(key))
|
||||
{
|
||||
keys.Add(key);
|
||||
}
|
||||
dict[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<K, V> item)
|
||||
{
|
||||
Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
keys.Clear();
|
||||
dict.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(KeyValuePair<K, V> item)
|
||||
{
|
||||
return ContainsKey(item.Key);
|
||||
}
|
||||
|
||||
public bool ContainsKey(K key)
|
||||
{
|
||||
return dict.ContainsKey(key);
|
||||
}
|
||||
|
||||
public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
foreach (var key in this.keys)
|
||||
{
|
||||
var kv = new KeyValuePair<K, V>(key, this.dict[key]);
|
||||
array[arrayIndex++] = kv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
|
||||
{
|
||||
foreach (var key in this.keys)
|
||||
{
|
||||
yield return new KeyValuePair<K, V>(key, this.dict[key]);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(K key)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
keys.Remove(key);
|
||||
return dict.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<K, V> item)
|
||||
{
|
||||
return Remove(item.Key);
|
||||
}
|
||||
|
||||
public bool TryGetValue(K key, out V value)
|
||||
{
|
||||
return this.dict.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
foreach (var key in this.keys)
|
||||
{
|
||||
yield return new KeyValuePair<K, V>(key, this.dict[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,489 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TraceLog;
|
||||
using XDM.Core.IO;
|
||||
using XDM.Core.Util;
|
||||
|
||||
namespace XDM.Core
|
||||
{
|
||||
public class Config
|
||||
{
|
||||
private static Config instance;
|
||||
private static object lockObj = new();
|
||||
public static Config Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
lock (lockObj)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
LoadConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance!;
|
||||
}
|
||||
|
||||
private set
|
||||
{
|
||||
instance = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static string DataDir { get; set; }
|
||||
|
||||
public bool IsBrowserMonitoringEnabled { get; set; } = true;
|
||||
|
||||
public static string DefaultFallbackUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36";
|
||||
|
||||
public string FallbackUserAgent { get; set; } = DefaultFallbackUserAgent;
|
||||
|
||||
public static string[] DefaultVideoExtensions => new string[]
|
||||
{
|
||||
"MP4", "M3U8", "F4M", "WEBM", "OGG", "MP3", "AAC", "FLV", "MKV", "DIVX",
|
||||
"MOV", "MPG", "MPEG","OPUS"
|
||||
};
|
||||
|
||||
public string[] VideoExtensions { get; set; }
|
||||
|
||||
public static string[] DefaultFileExtensions => new string[]
|
||||
{
|
||||
"3GP", "7Z", "AVI", "BZ2", "DEB", "DOC", "DOCX", "EXE", "ISO",
|
||||
"MSI", "PDF", "PPT", "PPTX", "RAR", "RPM", "XLS", "XLSX", "SIT", "SITX", "TAR", "JAR", "ZIP", "XZ"
|
||||
};
|
||||
|
||||
public string[] FileExtensions { get; set; }
|
||||
|
||||
public static string[] DefaultBlockedHosts => new string[]
|
||||
{
|
||||
"update.microsoft.com","windowsupdate.com","thwawte.com"
|
||||
};
|
||||
|
||||
public string[] BlockedHosts { get; set; }
|
||||
|
||||
public string Language { get; set; } = "English";
|
||||
|
||||
public bool AllowSystemDarkTheme { get; set; } = false;
|
||||
|
||||
private Config()
|
||||
{
|
||||
VideoExtensions = DefaultVideoExtensions;
|
||||
FileExtensions = DefaultFileExtensions;
|
||||
BlockedHosts = DefaultBlockedHosts;
|
||||
}
|
||||
|
||||
public List<string> RecentFolders { get; set; } = new List<string>();
|
||||
|
||||
public FolderSelectionMode FolderSelectionMode { get; set; }
|
||||
|
||||
public FileConflictResolution FileConflictResolution { get; set; }
|
||||
|
||||
public int MaxRetry { get; set; } = 10;
|
||||
|
||||
public int RetryDelay { get; set; } = 10;
|
||||
|
||||
public int MaxParallelDownloads { get; set; } = 1;
|
||||
|
||||
public bool ShowProgressWindow { get; set; } = true;
|
||||
|
||||
public bool ShowDownloadCompleteWindow { get; set; } = true;
|
||||
|
||||
public bool StartDownloadAutomatically { get; set; } = false;
|
||||
|
||||
public bool FetchServerTimeStamp { get; set; } = false;
|
||||
|
||||
public bool MonitorClipboard { get; set; } = false;
|
||||
|
||||
public int MinVideoSize { get; set; } = 1 * 1024;
|
||||
|
||||
public string TempDir { get; set; }
|
||||
|
||||
public int NetworkTimeout { get; set; } = 30;
|
||||
|
||||
public int MaxSegments { get; set; } = 8;
|
||||
|
||||
public int DefaltDownloadSpeed { get; set; } = 0;
|
||||
|
||||
public bool EnableSpeedLimit { get; set; } = false;
|
||||
|
||||
public bool ShutdownAfterAllFinished { get; set; } = false;
|
||||
|
||||
public bool KeepPCAwake { get; set; } = true;
|
||||
|
||||
public bool RunCommandAfterCompletion { get; set; } = false;
|
||||
|
||||
public string AfterCompletionCommand { get; set; }
|
||||
|
||||
public bool ScanWithAntiVirus { get; set; } = false;
|
||||
|
||||
public string AntiVirusExecutable { get; set; }
|
||||
|
||||
public string AntiVirusArgs { get; set; }
|
||||
|
||||
public ProxyInfo? Proxy { get; set; }
|
||||
|
||||
public bool DoubleClickOpenFile { get; set; } = false;
|
||||
|
||||
public bool RunOnLogon
|
||||
{
|
||||
get => PlatformHelper.IsAutoStartEnabled();
|
||||
set => PlatformHelper.EnableAutoStart(value);
|
||||
}
|
||||
|
||||
public string UserSelectedDownloadFolder { get; set; }
|
||||
|
||||
public string DefaultDownloadFolder { get; set; } =
|
||||
PlatformHelper.GetOsDefaultDownloadFolder();
|
||||
|
||||
public static IEnumerable<Category> DefaultCategories = new[]
|
||||
{
|
||||
new Category
|
||||
{
|
||||
Name="CAT_DOCUMENTS",
|
||||
DisplayName="Document",
|
||||
FileExtensions=new HashSet<string>
|
||||
{
|
||||
".DOC", ".DOCX", ".PDF", ".MD", ".XLSX",".XLS", ".CBZ"
|
||||
},
|
||||
DefaultFolder=Path.Combine(PlatformHelper.GetOsDefaultDownloadFolder(),
|
||||
"Documents"),
|
||||
IsPredefined=true
|
||||
},
|
||||
new Category
|
||||
{
|
||||
Name="CAT_MUSIC",
|
||||
DisplayName="Music",
|
||||
FileExtensions=new HashSet<string>
|
||||
{
|
||||
".MP3", ".AAC",".MPA",".WMA",".MIDI"
|
||||
},
|
||||
DefaultFolder=Path.Combine(PlatformHelper.GetOsDefaultDownloadFolder(),"Music"),
|
||||
IsPredefined=true
|
||||
},
|
||||
new Category
|
||||
{
|
||||
Name="CAT_VIDEOS",
|
||||
DisplayName="Video",
|
||||
FileExtensions=new HashSet<string>
|
||||
{
|
||||
".MP4", ".WEBM", ".OGG", ".FLV", ".MKV", ".DIVX",
|
||||
".MOV", ".MPG", ".MPEG",".OPUS",".AVI",".WMV",".TS"
|
||||
},
|
||||
DefaultFolder=Path.Combine(PlatformHelper.GetOsDefaultDownloadFolder(),"Video"),
|
||||
IsPredefined=true
|
||||
},
|
||||
new Category
|
||||
{
|
||||
Name="CAT_COMPRESSED",
|
||||
DisplayName="Compressed",
|
||||
FileExtensions=new HashSet<string>
|
||||
{
|
||||
".7Z", ".ZIP", ".RAR", ".BZ2", ".GZ",".XZ", ".TAR"
|
||||
},
|
||||
DefaultFolder=Path.Combine(PlatformHelper.GetOsDefaultDownloadFolder(),"Compressed"),
|
||||
IsPredefined=true
|
||||
},
|
||||
new Category
|
||||
{
|
||||
Name="CAT_PROGRAMS",
|
||||
DisplayName="Application",
|
||||
FileExtensions=new HashSet<string>
|
||||
{
|
||||
".EXE", ".DEB", ".RPM", ".MSI"
|
||||
},
|
||||
DefaultFolder=Path.Combine(PlatformHelper.GetOsDefaultDownloadFolder(),"Programs"),
|
||||
IsPredefined=true
|
||||
},
|
||||
//new Category
|
||||
//{
|
||||
// Name="Other",
|
||||
// DisplayName="Other",
|
||||
// FileExtensions=new HashSet<string>
|
||||
// {
|
||||
// }
|
||||
//}
|
||||
};
|
||||
|
||||
public IEnumerable<Category> Categories = DefaultCategories;
|
||||
|
||||
public IEnumerable<PasswordEntry> UserCredentials { get; set; } = new List<PasswordEntry>();
|
||||
|
||||
public static void LoadConfig(string? path = null)
|
||||
{
|
||||
DataDir = path ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".xdman");
|
||||
instance = new Config
|
||||
{
|
||||
TempDir = Path.Combine(DataDir, "temp")
|
||||
};
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(DataDir))
|
||||
{
|
||||
Directory.CreateDirectory(DataDir);
|
||||
}
|
||||
|
||||
var bytes = TransactedIO.ReadBytes("settings.dat", DataDir);
|
||||
if (bytes != null)
|
||||
{
|
||||
using var ms = new MemoryStream(bytes);
|
||||
using var reader = new BinaryReader(ms);
|
||||
ConfigIO.DeserializeConfig(instance, reader);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
|
||||
|
||||
//var json = TransactedIO.Read("settings.json", Config.DataDir);
|
||||
//Config? instance = null;
|
||||
//if (json != null)
|
||||
//{
|
||||
// instance = JsonConvert.DeserializeObject<Config>(
|
||||
// json, new JsonSerializerSettings
|
||||
// {
|
||||
// MissingMemberHandling = MissingMemberHandling.Ignore,
|
||||
// ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
|
||||
// });
|
||||
//}
|
||||
//if (instance == null)
|
||||
//{
|
||||
// Instance = new Config
|
||||
// {
|
||||
// TempDir = Path.Combine(Config.DataDir, "temp")
|
||||
// };
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// Instance = instance;
|
||||
//}
|
||||
|
||||
//var path = Path.Combine(Config.DataDir, "settings.json");
|
||||
//if (File.Exists(path))
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// var instance = JsonConvert.DeserializeObject<Config>(
|
||||
// File.ReadAllText(path), new JsonSerializerSettings
|
||||
// {
|
||||
// MissingMemberHandling = MissingMemberHandling.Ignore,
|
||||
// ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
|
||||
// });
|
||||
// if (instance != null)
|
||||
// {
|
||||
// Instance = instance;
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// catch (Exception exx)
|
||||
// {
|
||||
// Log.Debug(exx, "Error loading config");
|
||||
// }
|
||||
//}
|
||||
//Instance = new Config
|
||||
//{
|
||||
// TempDir = Path.Combine(Config.DataDir, "temp")
|
||||
//};
|
||||
}
|
||||
|
||||
//private static void PopulateConfig32(Config instance, BinaryReader r)
|
||||
//{
|
||||
// instance.AfterCompletionCommand = XDM.Messaging.StreamHelper.ReadString(r);
|
||||
// instance.AntiVirusArgs = XDM.Messaging.StreamHelper.ReadString(r);
|
||||
// instance.AntiVirusExecutable = XDM.Messaging.StreamHelper.ReadString(r);
|
||||
// var count = r.ReadInt32();
|
||||
// instance.BlockedHosts = new string[count];
|
||||
// for (int i = 0; i < count; i++)
|
||||
// {
|
||||
// instance.BlockedHosts[i] = r.ReadString();
|
||||
// }
|
||||
// count = r.ReadInt32();
|
||||
// var list = new List<Category>(count);
|
||||
// for (int i = 0; i < count; i++)
|
||||
// {
|
||||
// var category = new Category
|
||||
// {
|
||||
// DefaultFolder = XDM.Messaging.StreamHelper.ReadString(r),
|
||||
// DisplayName = XDM.Messaging.StreamHelper.ReadString(r),
|
||||
// FileExtensions = new HashSet<string>(),
|
||||
// };
|
||||
// var c2 = r.ReadInt32();
|
||||
// for (int j = 0; j < c2; j++)
|
||||
// {
|
||||
// category.FileExtensions.Add(r.ReadString());
|
||||
// }
|
||||
// category.IsPredefined = r.ReadBoolean();
|
||||
// category.Name = r.ReadString();
|
||||
// list.Add(category);
|
||||
// }
|
||||
// instance.Categories = list;
|
||||
// instance.DefaultDownloadFolder = XDM.Messaging.StreamHelper.ReadString(r);
|
||||
// instance.EnableSpeedLimit = r.ReadBoolean();
|
||||
// instance.FetchServerTimeStamp = r.ReadBoolean();
|
||||
// instance.FileConflictResolution = (FileConflictResolution)r.ReadInt32();
|
||||
// count = r.ReadInt32();
|
||||
// instance.FileExtensions = new string[count];
|
||||
// for (int i = 0; i < count; i++)
|
||||
// {
|
||||
// instance.FileExtensions[i] = r.ReadString();
|
||||
// }
|
||||
// instance.FolderSelectionMode = (FolderSelectionMode)r.ReadInt32();
|
||||
// instance.DefaltDownloadSpeed = r.ReadInt32();
|
||||
// instance.IsBrowserMonitoringEnabled = r.ReadBoolean();
|
||||
// instance.KeepPCAwake = r.ReadBoolean();
|
||||
// instance.Language = r.ReadString();
|
||||
// instance.MaxParallelDownloads = r.ReadInt32();
|
||||
// instance.MaxRetry = r.ReadInt32();
|
||||
// instance.MaxSegments = r.ReadInt32();
|
||||
// instance.MinVideoSize = r.ReadInt32();
|
||||
// instance.MonitorClipboard = r.ReadBoolean();
|
||||
// instance.NetworkTimeout = r.ReadInt32();
|
||||
// count = r.ReadInt32();
|
||||
// instance.RecentFolders = new List<string>(count);
|
||||
// for (int i = 0; i < count; i++)
|
||||
// {
|
||||
// instance.RecentFolders.Add(r.ReadString());
|
||||
// }
|
||||
// instance.RetryDelay = r.ReadInt32();
|
||||
// instance.RunCommandAfterCompletion = r.ReadBoolean();
|
||||
// instance.RunOnLogon = r.ReadBoolean();
|
||||
// instance.ScanWithAntiVirus = r.ReadBoolean();
|
||||
// instance.ShowDownloadCompleteWindow = r.ReadBoolean();
|
||||
// instance.ShowProgressWindow = r.ReadBoolean();
|
||||
// instance.ShutdownAfterAllFinished = r.ReadBoolean();
|
||||
// instance.StartDownloadAutomatically = r.ReadBoolean();
|
||||
// instance.TempDir = XDM.Messaging.StreamHelper.ReadString(r);
|
||||
// count = r.ReadInt32();
|
||||
// var list2 = new List<PasswordEntry>(count);
|
||||
// for (int i = 0; i < count; i++)
|
||||
// {
|
||||
// var passwordEntry = new PasswordEntry
|
||||
// {
|
||||
// Host = XDM.Messaging.StreamHelper.ReadString(r),
|
||||
// User = XDM.Messaging.StreamHelper.ReadString(r),
|
||||
// Password = XDM.Messaging.StreamHelper.ReadString(r)
|
||||
// };
|
||||
// list2.Add(passwordEntry);
|
||||
// }
|
||||
// instance.UserCredentials = list2;
|
||||
// count = r.ReadInt32();
|
||||
// instance.VideoExtensions = new string[count];
|
||||
// for (int i = 0; i < count; i++)
|
||||
// {
|
||||
// instance.VideoExtensions[i] = r.ReadString();
|
||||
// }
|
||||
// instance.Proxy = ProxyInfoSerializer.Deserialize(r);
|
||||
// instance.AllowSystemDarkTheme = r.ReadBoolean();
|
||||
//}
|
||||
|
||||
public static void SaveConfig()
|
||||
{
|
||||
ConfigIO.SerializeConfig();
|
||||
}
|
||||
|
||||
//public static void SaveConfig3()
|
||||
//{
|
||||
// using var ms = new MemoryStream();
|
||||
// using var writer = new BinaryWriter(ms);
|
||||
// writer.Write(Instance.AfterCompletionCommand ?? string.Empty);
|
||||
// writer.Write(Instance.AntiVirusArgs ?? string.Empty);
|
||||
// writer.Write(Instance.AntiVirusExecutable ?? string.Empty);
|
||||
// var count = Instance.BlockedHosts?.Length ?? 0;
|
||||
// writer.Write(count);
|
||||
// for (int i = 0; i < count; i++)
|
||||
// {
|
||||
// writer.Write(Instance.BlockedHosts![i]);
|
||||
// }
|
||||
// count = Instance.Categories.Count();
|
||||
// writer.Write(count);
|
||||
// foreach (var category in Instance.Categories)
|
||||
// {
|
||||
// writer.Write(category.DefaultFolder);
|
||||
// writer.Write(category.DisplayName ?? string.Empty);
|
||||
// count = category.FileExtensions.Count();
|
||||
// writer.Write(count);
|
||||
// foreach (var ext in category.FileExtensions)
|
||||
// {
|
||||
// writer.Write(ext);
|
||||
// }
|
||||
// writer.Write(category.IsPredefined);
|
||||
// writer.Write(category.Name);
|
||||
// }
|
||||
// writer.Write(Instance.DefaultDownloadFolder ?? string.Empty);
|
||||
// writer.Write(Instance.EnableSpeedLimit);
|
||||
// writer.Write(Instance.FetchServerTimeStamp);
|
||||
// writer.Write((int)Instance.FileConflictResolution);
|
||||
// count = Instance.FileExtensions.Length;
|
||||
// writer.Write(count);
|
||||
// foreach (var ext in Instance.FileExtensions)
|
||||
// {
|
||||
// writer.Write(ext);
|
||||
// }
|
||||
// writer.Write((int)Instance.FolderSelectionMode);
|
||||
// writer.Write(Instance.DefaltDownloadSpeed);
|
||||
// writer.Write(Instance.IsBrowserMonitoringEnabled);
|
||||
// writer.Write(Instance.KeepPCAwake);
|
||||
// writer.Write(Instance.Language);
|
||||
// writer.Write(Instance.MaxParallelDownloads);
|
||||
// writer.Write(Instance.MaxRetry);
|
||||
// writer.Write(Instance.MaxSegments);
|
||||
// writer.Write(Instance.MinVideoSize);
|
||||
// writer.Write(Instance.MonitorClipboard);
|
||||
// writer.Write(Instance.NetworkTimeout);
|
||||
// count = Instance.RecentFolders.Count;
|
||||
// writer.Write(count);
|
||||
// foreach (var recentFolder in Instance.RecentFolders)
|
||||
// {
|
||||
// writer.Write(recentFolder);
|
||||
// }
|
||||
// writer.Write(Instance.RetryDelay);
|
||||
// writer.Write(Instance.RunCommandAfterCompletion);
|
||||
// writer.Write(Instance.RunOnLogon);
|
||||
// writer.Write(Instance.ScanWithAntiVirus);
|
||||
// writer.Write(Instance.ShowDownloadCompleteWindow);
|
||||
// writer.Write(Instance.ShowProgressWindow);
|
||||
// writer.Write(Instance.ShutdownAfterAllFinished);
|
||||
// writer.Write(Instance.StartDownloadAutomatically);
|
||||
// writer.Write(Instance.TempDir);
|
||||
// count = Instance.UserCredentials.Count();
|
||||
// writer.Write(count);
|
||||
// foreach (var pe in Instance.UserCredentials)
|
||||
// {
|
||||
// writer.Write(pe.Host ?? string.Empty);
|
||||
// writer.Write(pe.User ?? string.Empty);
|
||||
// writer.Write(pe.Password ?? string.Empty);
|
||||
// }
|
||||
// count = Instance.VideoExtensions.Length;
|
||||
// writer.Write(count);
|
||||
// foreach (var ext in Instance.VideoExtensions)
|
||||
// {
|
||||
// writer.Write(ext);
|
||||
// }
|
||||
// //ProxyInfoSerializer.Serialize(Instance.Proxy, writer);
|
||||
// //writer.Write(Instance.AllowSystemDarkTheme);
|
||||
// writer.Close();
|
||||
// ms.Close();
|
||||
// TransactedIO.WriteBytes(ms.ToArray(), "settings.db", Config.DataDir);
|
||||
// //TransactedIO.Write(JsonConvert.SerializeObject(Config.Instance), "settings.json", Config.DataDir);
|
||||
// //File.WriteAllText(Path.Combine(Config.DataDir, "settings.json"), JsonConvert.SerializeObject(Config.Instance));
|
||||
//}
|
||||
}
|
||||
|
||||
public enum FolderSelectionMode
|
||||
{
|
||||
Auto, Manual
|
||||
}
|
||||
|
||||
public enum FileConflictResolution
|
||||
{
|
||||
AutoRename,
|
||||
Overwrite
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
using XDM.Core.Downloader;
|
||||
|
||||
namespace XDM.Core.DataAccess
|
||||
{
|
||||
public class AppDB
|
||||
{
|
||||
private static object lockObj = new();
|
||||
private bool init = false;
|
||||
private SQLiteConnection db;
|
||||
private AppDB() { }
|
||||
private DownloadList downloadsDB;
|
||||
public DownloadList Downloads => downloadsDB;
|
||||
private static AppDB instance;
|
||||
public static AppDB Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (lockObj)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = new AppDB();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Init(string file)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
try
|
||||
{
|
||||
string cs = $"URI=file:{file}";
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
SQLiteConnection.CreateFile(file);
|
||||
}
|
||||
db = new SQLiteConnection(cs);
|
||||
db.Open();
|
||||
SchemaInitializer.Init(db);
|
||||
this.downloadsDB = new DownloadList(db);
|
||||
init = true;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Export(string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
DataImportExport.CopyToFile(db, file);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Debug(e, e.Message);
|
||||
return false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Import(string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
DataImportExport.CopyFromFile(db, file);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Debug(e, e.Message);
|
||||
return false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using TraceLog;
|
||||
|
||||
namespace XDM.Core.DataAccess
|
||||
{
|
||||
public static class DataImportExport
|
||||
{
|
||||
public static bool CopyToFile(SQLiteConnection sql, string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cs = $"URI=file:{file}";
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
SQLiteConnection.CreateFile(file);
|
||||
}
|
||||
using var dest = new SQLiteConnection(cs);
|
||||
dest.Open();
|
||||
sql.BackupDatabase(dest, "main", "main", -1, null, 0);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool CopyFromFile(SQLiteConnection sql, string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var attachCmd = new SQLiteCommand($"ATTACH '{file}' as db", sql);
|
||||
attachCmd.ExecuteNonQuery();
|
||||
var tx = sql.BeginTransaction();
|
||||
try
|
||||
{
|
||||
using var mergeCmd = new SQLiteCommand($"INSERT OR IGNORE INTO downloads SELECT * FROM db.downloads", sql);
|
||||
mergeCmd.ExecuteNonQuery();
|
||||
tx.Commit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tx.Rollback();
|
||||
Log.Debug("Error during merge insert, performing rollback!!");
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
using var detachCmd = new SQLiteCommand($"DETACH db", sql);
|
||||
detachCmd.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//public bool CopyDB(SQLiteConnection sourceDB, SQLiteConnection targetDB, )
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// using var attachCmd = new SQLiteCommand($"ATTACH '{file}' as db", db);
|
||||
// attachCmd.ExecuteNonQuery();
|
||||
// var tx = db.BeginTransaction();
|
||||
// try
|
||||
// {
|
||||
// using var mergeCmd = new SQLiteCommand($"INSERT OR IGNORE INTO downloads SELECT * FROM db.downloads", db);
|
||||
// mergeCmd.ExecuteNonQuery();
|
||||
// tx.Commit();
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// tx.Rollback();
|
||||
// Log.Debug("Error during merge insert, performing rollback!!");
|
||||
// Log.Debug(ex, ex.Message);
|
||||
// }
|
||||
// using var detachCmd = new SQLiteCommand($"DETACH db", db);
|
||||
// detachCmd.ExecuteNonQuery();
|
||||
// return true;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Debug(ex, ex.Message);
|
||||
// return false;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,477 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using TraceLog;
|
||||
using XDM.Core;
|
||||
using XDM.Core.DataAccess.Extensions;
|
||||
using XDM.Core.Downloader;
|
||||
|
||||
namespace XDM.Core.DataAccess
|
||||
{
|
||||
public class DownloadList
|
||||
{
|
||||
private SQLiteConnection db;
|
||||
|
||||
public DownloadList(SQLiteConnection db)
|
||||
{
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
private SQLiteCommand cmdFetchAll, cmdFetchConditional, cmdFetchOne, cmdUpdateProgress, cmdUpdateTargetDir,
|
||||
cmdInsertOne, cmdMarkFinished, cmdUpdateStatus, cmdUpdateNameAndSize, cmdUpdateNameAndFolder, cmdUpdateOne, cmdDelete;
|
||||
|
||||
public bool LoadDownloads(
|
||||
out List<InProgressDownloadItem> inProgressDownloads,
|
||||
out List<FinishedDownloadItem> finishedDownloads, QueryMode queryMode = QueryMode.All)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
inProgressDownloads = new List<InProgressDownloadItem>();
|
||||
finishedDownloads = new List<FinishedDownloadItem>();
|
||||
try
|
||||
{
|
||||
SQLiteCommand sqlCommand;
|
||||
if (queryMode == QueryMode.All)
|
||||
{
|
||||
if (cmdFetchAll == null)
|
||||
{
|
||||
cmdFetchAll = new SQLiteCommand("SELECT * FROM downloads", db);
|
||||
}
|
||||
sqlCommand = cmdFetchAll;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cmdFetchConditional == null)
|
||||
{
|
||||
cmdFetchConditional = new SQLiteCommand("SELECT * FROM downloads WHERE completed=@completed", db);
|
||||
}
|
||||
SetParam("@completed", queryMode == QueryMode.InProgress ? 0 : 1, cmdFetchConditional.Parameters);
|
||||
sqlCommand = cmdFetchConditional;
|
||||
}
|
||||
using SQLiteDataReader r = sqlCommand.ExecuteReader();
|
||||
while (r.Read())
|
||||
{
|
||||
var id = r.GetSafeString(0);
|
||||
var inProgress = r.GetInt32(1) == 0;
|
||||
DownloadItemBase entry = r.GetInt32(1) == 0 ? new InProgressDownloadItem() : new FinishedDownloadItem();
|
||||
entry.Id = id;
|
||||
entry.Name = r.GetSafeString(2);
|
||||
entry.DateAdded = DateTime.FromBinary(r.GetInt64(3));
|
||||
entry.Size = r.GetInt64(4);
|
||||
entry.DownloadType = r.GetSafeString(7);
|
||||
entry.FileNameFetchMode = (FileNameFetchMode)r.GetInt32(8);
|
||||
entry.MaxSpeedLimitInKiB = r.GetInt32(9);
|
||||
entry.TargetDir = r.GetSafeString(10);
|
||||
entry.PrimaryUrl = r.GetSafeString(11);
|
||||
entry.RefererUrl = r.GetSafeString(12);
|
||||
if (r.GetInt32(13) == 1)
|
||||
{
|
||||
var user = r.GetSafeString(14);
|
||||
var pass = r.GetSafeString(15);
|
||||
if (user != null)
|
||||
{
|
||||
entry.Authentication = new AuthenticationInfo
|
||||
{
|
||||
UserName = user,
|
||||
Password = pass
|
||||
};
|
||||
}
|
||||
}
|
||||
var proxy = new ProxyInfo { };
|
||||
proxy.ProxyType = (ProxyType)r.GetInt32(16);
|
||||
proxy.Host = r.GetSafeString(17);
|
||||
proxy.Port = r.GetInt32(18);
|
||||
proxy.UserName = r.GetSafeString(19);
|
||||
proxy.Password = r.GetSafeString(20);
|
||||
entry.Proxy = proxy;
|
||||
|
||||
if (inProgress)
|
||||
{
|
||||
var inp = (InProgressDownloadItem)entry;
|
||||
inp.Status = DownloadStatus.Stopped;
|
||||
inp.Progress = r.GetInt32(6);
|
||||
inProgressDownloads.Add(inp);
|
||||
}
|
||||
else
|
||||
{
|
||||
finishedDownloads.Add((FinishedDownloadItem)entry);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadItemBase? GetDownloadById(string id)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdFetchOne == null)
|
||||
{
|
||||
cmdFetchOne = new SQLiteCommand("SELECT * FROM downloads WHERE id=@id", db);
|
||||
}
|
||||
SetParam("@id", id, cmdFetchOne.Parameters);
|
||||
//cmdFetchOne.Parameters["@id"].Value = id;
|
||||
using SQLiteDataReader r = cmdFetchOne.ExecuteReader();
|
||||
if (r.Read())
|
||||
{
|
||||
var inProgress = r.GetInt32(1) == 0;
|
||||
DownloadItemBase entry = r.GetInt32(1) == 0 ? new InProgressDownloadItem() : new FinishedDownloadItem();
|
||||
entry.Id = id;
|
||||
entry.Name = r.GetSafeString(2);
|
||||
entry.DateAdded = DateTime.FromBinary(r.GetInt64(3));
|
||||
entry.Size = r.GetInt64(4);
|
||||
entry.DownloadType = r.GetSafeString(7);
|
||||
entry.FileNameFetchMode = (FileNameFetchMode)r.GetInt32(8);
|
||||
entry.MaxSpeedLimitInKiB = r.GetInt32(9);
|
||||
entry.TargetDir = r.GetSafeString(10);
|
||||
entry.PrimaryUrl = r.GetSafeString(11);
|
||||
entry.RefererUrl = r.GetSafeString(12);
|
||||
if (r.GetInt32(13) == 1)
|
||||
{
|
||||
var user = r.GetSafeString(14);
|
||||
var pass = r.GetSafeString(15);
|
||||
if (user != null)
|
||||
{
|
||||
entry.Authentication = new AuthenticationInfo
|
||||
{
|
||||
UserName = user,
|
||||
Password = pass
|
||||
};
|
||||
}
|
||||
}
|
||||
var proxy = new ProxyInfo { };
|
||||
proxy.ProxyType = (ProxyType)r.GetInt32(16);
|
||||
proxy.Host = r.GetSafeString(17);
|
||||
proxy.Port = r.GetInt32(18);
|
||||
proxy.UserName = r.GetSafeString(19);
|
||||
proxy.Password = r.GetSafeString(20);
|
||||
entry.Proxy = proxy;
|
||||
|
||||
if (inProgress)
|
||||
{
|
||||
var inp = (InProgressDownloadItem)entry;
|
||||
inp.Status = DownloadStatus.Stopped;
|
||||
inp.Progress = r.GetInt32(6);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddNewDownload(InProgressDownloadItem entry)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdInsertOne == null)
|
||||
{
|
||||
cmdInsertOne = new SQLiteCommand(@"INSERT INTO downloads(
|
||||
id, completed, name, date_added, size, status,
|
||||
progress, download_type, filenamefetchmode, maxspeedlimitinkib, targetdir, primary_url,
|
||||
referer_url, auth, user, pass, proxy, proxy_host,
|
||||
proxy_port, proxy_user, proxy_pass, proxy_type)
|
||||
VALUES(
|
||||
@id, @completed, @name, @date_added, @size, @status,
|
||||
@progress, @download_type, @filenamefetchmode, @maxspeedlimitinkib, @targetdir, @primary_url,
|
||||
@referer_url, @auth, @user, @pass, @proxy, @proxy_host,
|
||||
@proxy_port, @proxy_user, @proxy_pass, @proxy_type)", db);
|
||||
}
|
||||
SetParam("@id", entry.Id, cmdInsertOne.Parameters);
|
||||
SetParam("@completed", 0, cmdInsertOne.Parameters);
|
||||
SetParam("@name", entry.Name, cmdInsertOne.Parameters);
|
||||
SetParam("@date_added", entry.DateAdded.ToBinary(), cmdInsertOne.Parameters);
|
||||
SetParam("@size", entry.Size, cmdInsertOne.Parameters);
|
||||
SetParam("@status", (int)entry.Status, cmdInsertOne.Parameters);
|
||||
SetParam("@progress", entry.Progress, cmdInsertOne.Parameters);
|
||||
SetParam("@download_type", entry.DownloadType, cmdInsertOne.Parameters);
|
||||
SetParam("@filenamefetchmode", (int)entry.FileNameFetchMode, cmdInsertOne.Parameters);
|
||||
SetParam("@maxspeedlimitinkib", entry.MaxSpeedLimitInKiB, cmdInsertOne.Parameters);
|
||||
SetParam("@targetdir", entry.TargetDir, cmdInsertOne.Parameters);
|
||||
SetParam("@primary_url", entry.PrimaryUrl, cmdInsertOne.Parameters);
|
||||
SetParam("@referer_url", entry.RefererUrl, cmdInsertOne.Parameters);
|
||||
SetParam("@auth", entry.Authentication.HasValue ? 1 : 0, cmdInsertOne.Parameters);
|
||||
SetParam("@user", entry.Authentication?.UserName ?? null, cmdInsertOne.Parameters);
|
||||
SetParam("@pass", entry.Authentication?.Password ?? null, cmdInsertOne.Parameters);
|
||||
SetParam("@proxy", (int)(entry.Proxy?.ProxyType ?? 0), cmdInsertOne.Parameters);
|
||||
SetParam("@proxy_host", entry.Proxy?.Host ?? null, cmdInsertOne.Parameters);
|
||||
SetParam("@proxy_port", (int)(entry.Proxy?.Port ?? 0), cmdInsertOne.Parameters);
|
||||
SetParam("@proxy_user", entry.Proxy?.UserName ?? null, cmdInsertOne.Parameters);
|
||||
SetParam("@proxy_pass", entry.Proxy?.Password ?? null, cmdInsertOne.Parameters);
|
||||
SetParam("@proxy_type", 1, cmdInsertOne.Parameters);
|
||||
cmdInsertOne.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateDownloadEntry(FinishedDownloadItem entry)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdUpdateOne == null)
|
||||
{
|
||||
cmdUpdateOne = new SQLiteCommand(@"UPDATE downloads SET name=@name, date_added=@date_added, size=@size,
|
||||
download_type=@download_type, targetdir=@targetdir, primary_url=@primary_url,
|
||||
auth=@auth, user=@user, pass=@pass, proxy=@proxy, proxy_host=@proxy_host,
|
||||
proxy_port=@proxy_port, proxy_user=@proxy_user, proxy_pass=@proxy_pass,
|
||||
proxy_type=@proxy_type WHERE id=@id", db);
|
||||
}
|
||||
SetParam("@id", entry.Id, cmdUpdateOne.Parameters);
|
||||
SetParam("@name", entry.Name, cmdUpdateOne.Parameters);
|
||||
SetParam("@date_added", entry.DateAdded.ToBinary(), cmdUpdateOne.Parameters);
|
||||
SetParam("@size", entry.Size, cmdUpdateOne.Parameters);
|
||||
SetParam("@download_type", entry.DownloadType, cmdUpdateOne.Parameters);
|
||||
SetParam("@primary_url", entry.PrimaryUrl, cmdUpdateOne.Parameters);
|
||||
SetParam("@auth", entry.Authentication.HasValue ? 1 : 0, cmdUpdateOne.Parameters);
|
||||
SetParam("@user", entry.Authentication?.UserName ?? null, cmdUpdateOne.Parameters);
|
||||
SetParam("@pass", entry.Authentication?.Password ?? null, cmdUpdateOne.Parameters);
|
||||
SetParam("@proxy", (int)(entry.Proxy?.ProxyType ?? 0), cmdUpdateOne.Parameters);
|
||||
SetParam("@proxy_host", entry.Proxy?.Host ?? null, cmdUpdateOne.Parameters);
|
||||
SetParam("@proxy_port", (int)(entry.Proxy?.Port ?? 0), cmdUpdateOne.Parameters);
|
||||
SetParam("@proxy_user", entry.Proxy?.UserName ?? null, cmdUpdateOne.Parameters);
|
||||
SetParam("@proxy_pass", entry.Proxy?.Password ?? null, cmdUpdateOne.Parameters);
|
||||
SetParam("@proxy_type", 1, cmdUpdateOne.Parameters);
|
||||
SetParam("@targetdir", entry.TargetDir, cmdUpdateOne.Parameters);
|
||||
cmdUpdateOne.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateDownloadProgress(string id, int progress)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdUpdateProgress == null)
|
||||
{
|
||||
cmdUpdateProgress = new SQLiteCommand("UPDATE downloads SET progress=@progress WHERE id=@id", db);
|
||||
}
|
||||
SetParam("@progress", progress, cmdUpdateProgress.Parameters);
|
||||
SetParam("@id", id, cmdUpdateProgress.Parameters);
|
||||
//cmdUpdateProgress.Parameters["@progress"].Value = progress;
|
||||
//cmdUpdateProgress.Parameters["@id"].Value = id;
|
||||
cmdUpdateProgress.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateDownloadFolder(string id, string folder)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdUpdateTargetDir == null)
|
||||
{
|
||||
cmdUpdateTargetDir = new SQLiteCommand("UPDATE downloads SET targetdir=@targetdir WHERE id=@id", db);
|
||||
}
|
||||
SetParam("@targetdir", folder, cmdUpdateTargetDir.Parameters);
|
||||
SetParam("@id", id, cmdUpdateTargetDir.Parameters);
|
||||
//cmdUpdateProgress.Parameters["@targetdir"].Value = folder;
|
||||
//cmdUpdateProgress.Parameters["@id"].Value = id;
|
||||
cmdUpdateProgress.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetParam<T>(string name, T value, SQLiteParameterCollection param)
|
||||
{
|
||||
if (!param.Contains(name))
|
||||
{
|
||||
param.AddWithValue(name, value);
|
||||
return;
|
||||
}
|
||||
param[name].Value = value;
|
||||
}
|
||||
|
||||
public bool MarkAsFinished(string id, long finalFileSize, string file, string folder)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdMarkFinished == null)
|
||||
{
|
||||
cmdMarkFinished = new SQLiteCommand("UPDATE downloads SET targetdir=@targetdir, name=@name, " +
|
||||
"size=@finalFileSize, completed=@completed WHERE id=@id", db);
|
||||
}
|
||||
SetParam("@targetdir", folder, cmdMarkFinished.Parameters);
|
||||
SetParam("@name", file, cmdMarkFinished.Parameters);
|
||||
SetParam("@finalFileSize", finalFileSize, cmdMarkFinished.Parameters);
|
||||
SetParam("@id", id, cmdMarkFinished.Parameters);
|
||||
SetParam("@completed", 1, cmdMarkFinished.Parameters);
|
||||
cmdMarkFinished.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateDownloadStatus(string id, DownloadStatus status)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdUpdateStatus == null)
|
||||
{
|
||||
cmdUpdateStatus = new SQLiteCommand("UPDATE downloads SET status=@status WHERE id=@id", db);
|
||||
}
|
||||
SetParam("@status", (int)status, cmdUpdateStatus.Parameters);
|
||||
SetParam("@id", id, cmdUpdateStatus.Parameters);
|
||||
cmdUpdateStatus.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateNameAndSize(string id, long size, string name)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdUpdateNameAndSize == null)
|
||||
{
|
||||
cmdUpdateNameAndSize = new SQLiteCommand("UPDATE downloads SET name=@name, size=@size WHERE id=@id", db);
|
||||
}
|
||||
SetParam("@id", id, cmdUpdateNameAndSize.Parameters);
|
||||
SetParam("@name", name, cmdUpdateNameAndSize.Parameters);
|
||||
SetParam("@size", size, cmdUpdateNameAndSize.Parameters);
|
||||
cmdUpdateNameAndSize.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateNameAndFolder(string id, string name, string folder)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdUpdateNameAndFolder == null)
|
||||
{
|
||||
cmdUpdateNameAndFolder = new SQLiteCommand("UPDATE downloads SET name=@name, targetdir=@targetdir WHERE id=@id", db);
|
||||
}
|
||||
SetParam("@name", name, cmdUpdateNameAndFolder.Parameters);
|
||||
SetParam("@targetdir", folder, cmdUpdateNameAndFolder.Parameters);
|
||||
SetParam("@id", id, cmdUpdateNameAndFolder.Parameters);
|
||||
cmdUpdateNameAndFolder.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveAllFinished()
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cmdClearAllFinished = new SQLiteCommand("DELETE FROM downloads WHERE completed=1", db);
|
||||
cmdClearAllFinished.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveDownloadById(string id)
|
||||
{
|
||||
lock (db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cmdDelete == null)
|
||||
{
|
||||
cmdDelete = new SQLiteCommand("DELETE FROM downloads WHERE id=@id", db);
|
||||
}
|
||||
SetParam("@id", id, cmdDelete.Parameters);
|
||||
cmdDelete.ExecuteNonQuery();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum QueryMode
|
||||
{
|
||||
Finished,
|
||||
InProgress,
|
||||
All
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace XDM.Core.DataAccess.Extensions
|
||||
{
|
||||
public static class DataReaderExtensions
|
||||
{
|
||||
public static string GetSafeString(this SQLiteDataReader r, int index)
|
||||
{
|
||||
if (!r.IsDBNull(index))
|
||||
{
|
||||
return r.GetString(index);
|
||||
}
|
||||
#pragma warning disable CS8603 // Possible null reference return.
|
||||
return null;
|
||||
#pragma warning restore CS8603 // Possible null reference return.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace XDM.Core.DataAccess
|
||||
{
|
||||
public static class SchemaInitializer
|
||||
{
|
||||
private static void CreateTablesIfNotExists(SQLiteConnection c)
|
||||
{
|
||||
var query = @"CREATE TABLE IF NOT EXISTS downloads(
|
||||
id TEXT PRIMARY KEY,
|
||||
completed INT,
|
||||
name TEXT,
|
||||
date_added INT,
|
||||
size INT,
|
||||
status INT,
|
||||
progress INT,
|
||||
download_type TEXT,
|
||||
filenamefetchmode INT,
|
||||
maxspeedlimitinkib INT,
|
||||
targetdir TEXT,
|
||||
primary_url TEXT,
|
||||
referer_url TEXT,
|
||||
auth INT,
|
||||
user TEXT,
|
||||
pass TEXT,
|
||||
proxy INT,
|
||||
proxy_host TEXT,
|
||||
proxy_port INT,
|
||||
proxy_user TEXT,
|
||||
proxy_pass TEXT,
|
||||
proxy_type INT
|
||||
) WITHOUT ROWID";
|
||||
using var cmd = new SQLiteCommand(c);
|
||||
cmd.CommandText = query;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public static void Init(SQLiteConnection c)
|
||||
{
|
||||
CreateTablesIfNotExists(c);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue