Add project files.
This commit is contained in:
commit
d9b49bd6bc
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Set default behavior to automatically normalize line endings.
|
||||||
|
###############################################################################
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Set default behavior for command prompt diff.
|
||||||
|
#
|
||||||
|
# This is need for earlier builds of msysgit that does not have it on by
|
||||||
|
# default for csharp files.
|
||||||
|
# Note: This is only used by command line
|
||||||
|
###############################################################################
|
||||||
|
#*.cs diff=csharp
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Set the merge driver for project and solution files
|
||||||
|
#
|
||||||
|
# Merging from the command prompt will add diff markers to the files if there
|
||||||
|
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||||
|
# the diff markers are never inserted). Diff markers may cause the following
|
||||||
|
# file extensions to fail to load in VS. An alternative would be to treat
|
||||||
|
# these files as binary and thus will always conflict and require user
|
||||||
|
# intervention with every merge. To do so, just uncomment the entries below
|
||||||
|
###############################################################################
|
||||||
|
#*.sln merge=binary
|
||||||
|
#*.csproj merge=binary
|
||||||
|
#*.vbproj merge=binary
|
||||||
|
#*.vcxproj merge=binary
|
||||||
|
#*.vcproj merge=binary
|
||||||
|
#*.dbproj merge=binary
|
||||||
|
#*.fsproj merge=binary
|
||||||
|
#*.lsproj merge=binary
|
||||||
|
#*.wixproj merge=binary
|
||||||
|
#*.modelproj merge=binary
|
||||||
|
#*.sqlproj merge=binary
|
||||||
|
#*.wwaproj merge=binary
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# behavior for image files
|
||||||
|
#
|
||||||
|
# image files are treated as binary by default.
|
||||||
|
###############################################################################
|
||||||
|
#*.jpg binary
|
||||||
|
#*.png binary
|
||||||
|
#*.gif binary
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# diff behavior for common document formats
|
||||||
|
#
|
||||||
|
# Convert binary document formats to text before diffing them. This feature
|
||||||
|
# is only available from the command line. Turn it on by uncommenting the
|
||||||
|
# entries below.
|
||||||
|
###############################################################################
|
||||||
|
#*.doc diff=astextplain
|
||||||
|
#*.DOC diff=astextplain
|
||||||
|
#*.docx diff=astextplain
|
||||||
|
#*.DOCX diff=astextplain
|
||||||
|
#*.dot diff=astextplain
|
||||||
|
#*.DOT diff=astextplain
|
||||||
|
#*.pdf diff=astextplain
|
||||||
|
#*.PDF diff=astextplain
|
||||||
|
#*.rtf diff=astextplain
|
||||||
|
#*.RTF diff=astextplain
|
43
.github/workflows/publish-docker.yml
vendored
Normal file
43
.github/workflows/publish-docker.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
name: Publish Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'OFDLV*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push_to_registry:
|
||||||
|
name: Push docker image to registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Extract version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION="${GITHUB_REF_NAME#OFDLV}"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to ghcr
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.actor }}/of-dl:latest
|
||||||
|
ghcr.io/${{ github.actor }}/of-dl:${{ steps.version.outputs.version }}
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
66
.github/workflows/publish-docs.yml
vendored
Normal file
66
.github/workflows/publish-docs.yml
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
name: Publish docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'OFDLV*'
|
||||||
|
paths:
|
||||||
|
- 'docs/**'
|
||||||
|
- '.github/workflows/publish-docs.yml'
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||||
|
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build job
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./docs
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: './docs/.nvmrc'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build with docusaurus
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: './docs/build'
|
||||||
|
|
||||||
|
# Deployment job
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
64
.github/workflows/publish-release.yml
vendored
Normal file
64
.github/workflows/publish-release.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: Publish release zip
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'OFDLV*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup dotnet 8.x
|
||||||
|
uses: actions/setup-dotnet@v3
|
||||||
|
with:
|
||||||
|
dotnet-version: 8.x
|
||||||
|
|
||||||
|
- name: Display dotnet version
|
||||||
|
run: dotnet --version
|
||||||
|
|
||||||
|
- name: Extract version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION="${GITHUB_REF_NAME#OFDLV}"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Run build
|
||||||
|
run: |
|
||||||
|
dotnet publish -p:Version=${{ steps.version.outputs.version }} -p:WarningLevel=0 -c Release -r win-x86 --self-contained true -p:PublishSingleFile=true -o outwin
|
||||||
|
dotnet publish -p:Version=${{ steps.version.outputs.version }} -p:WarningLevel=0 -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o outlin
|
||||||
|
cp ./OF\ DL/rules.json outwin/
|
||||||
|
chmod +x ./outlin/OF\ DL
|
||||||
|
cd outwin
|
||||||
|
../outlin/OF\ DL --non-interactive || true
|
||||||
|
mkdir -p cdm/devices/chrome_1610
|
||||||
|
wget https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip
|
||||||
|
unzip ffmpeg-release-essentials.zip ffmpeg\*/bin/ffmpeg.exe ffmpeg\*/LICENSE
|
||||||
|
mv ffmpeg*/bin/ffmpeg.exe .
|
||||||
|
mv ffmpeg*/LICENSE LICENSE.ffmpeg
|
||||||
|
zip ../OFDLV${{ steps.version.outputs.version }}.zip OF\ DL.exe e_sqlite3.dll rules.json config.conf cdm ffmpeg.exe LICENSE.ffmpeg
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
id: create_release
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
release_name: ${{ steps.version.outputs.version }}
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload Windows zip
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./OFDLV${{ steps.version.outputs.version }}.zip
|
||||||
|
asset_name: OFDLV${{ steps.version.outputs.version }}.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
367
.gitignore
vendored
Normal file
367
.gitignore
vendored
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
## 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/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Oo]ut/
|
||||||
|
[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/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Coverlet is a free, cross platform Code Coverage Tool
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
|
||||||
|
# Fody - auto-generated XML schema
|
||||||
|
FodyWeavers.xsd
|
||||||
|
/OF DL/auth.json
|
||||||
|
/OF DL/config.json
|
||||||
|
/OF DL/device_client_id_blob
|
||||||
|
/OF DL/device_private_key
|
66
Dockerfile
Normal file
66
Dockerfile
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
FROM alpine:3.20 AS build
|
||||||
|
|
||||||
|
ARG VERSION
|
||||||
|
|
||||||
|
RUN apk --no-cache --repository community add \
|
||||||
|
dotnet8-sdk
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY ["OF DL.sln", "/src/OF DL.sln"]
|
||||||
|
COPY ["OF DL", "/src/OF DL"]
|
||||||
|
|
||||||
|
WORKDIR "/src"
|
||||||
|
|
||||||
|
# Build release
|
||||||
|
RUN dotnet publish -p:WarningLevel=0 -p:Version=$VERSION -c Release --self-contained true -p:PublishSingleFile=true -o out
|
||||||
|
|
||||||
|
# Generate default config.conf files
|
||||||
|
RUN /src/out/OF\ DL --non-interactive || true && \
|
||||||
|
# Set download path in default config.conf to /data
|
||||||
|
sed -e 's/DownloadPath = ""/DownloadPath = "\/data"/' /src/config.conf > /src/updated_config.conf && \
|
||||||
|
mv /src/updated_config.conf /src/config.conf
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:3.20 AS final
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apk --no-cache --repository community add \
|
||||||
|
bash \
|
||||||
|
tini \
|
||||||
|
dotnet8-runtime \
|
||||||
|
ffmpeg \
|
||||||
|
udev \
|
||||||
|
ttf-freefont \
|
||||||
|
chromium \
|
||||||
|
supervisor \
|
||||||
|
xvfb \
|
||||||
|
x11vnc \
|
||||||
|
novnc
|
||||||
|
|
||||||
|
# Redirect webroot to vnc.html instead of displaying directory listing
|
||||||
|
RUN echo "<!DOCTYPE html><html><head><meta http-equiv=\"Refresh\" content=\"0; url='vnc.html'\" /></head><body></body></html>" > /usr/share/novnc/index.html
|
||||||
|
|
||||||
|
# Create directories for configuration and downloaded files
|
||||||
|
RUN mkdir /data /config /config/logs /default-config
|
||||||
|
|
||||||
|
# Copy release
|
||||||
|
COPY --from=build /src/out /app
|
||||||
|
|
||||||
|
# Copy default configuration files
|
||||||
|
COPY --from=build /src/config.conf /default-config
|
||||||
|
COPY --from=build ["/src/OF DL/rules.json", "/default-config"]
|
||||||
|
|
||||||
|
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY docker/entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
ENV DISPLAY=:0.0 \
|
||||||
|
DISPLAY_WIDTH=1024 \
|
||||||
|
DISPLAY_HEIGHT=768 \
|
||||||
|
OFDL_PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
|
||||||
|
OFDL_DOCKER=true
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
WORKDIR /config
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
CMD ["/app/entrypoint.sh"]
|
25
OF DL.sln
Normal file
25
OF DL.sln
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.5.33516.290
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL", "OF DL\OF DL.csproj", "{318B2CE3-D046-4276-A2CF-89E6DF32F1B3}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{318B2CE3-D046-4276-A2CF-89E6DF32F1B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{318B2CE3-D046-4276-A2CF-89E6DF32F1B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{318B2CE3-D046-4276-A2CF-89E6DF32F1B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{318B2CE3-D046-4276-A2CF-89E6DF32F1B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {54D0035A-5593-4A55-AD35-A319DB9B7232}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
28
OF DL/CDMApi.cs
Normal file
28
OF DL/CDMApi.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace WidevineClient.Widevine
|
||||||
|
{
|
||||||
|
public class CDMApi
|
||||||
|
{
|
||||||
|
string SessionId { get; set; }
|
||||||
|
|
||||||
|
public byte[] GetChallenge(string initDataB64, string certDataB64, bool offline = false, bool raw = false)
|
||||||
|
{
|
||||||
|
SessionId = CDM.OpenSession(initDataB64, Constants.DEVICE_NAME, offline, raw);
|
||||||
|
CDM.SetServiceCertificate(SessionId, Convert.FromBase64String(certDataB64));
|
||||||
|
return CDM.GetLicenseRequest(SessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ProvideLicense(string licenseB64)
|
||||||
|
{
|
||||||
|
CDM.ProvideLicense(SessionId, Convert.FromBase64String(licenseB64));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ContentKey> GetKeys()
|
||||||
|
{
|
||||||
|
return CDM.GetKeys(SessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
OF DL/Crypto/CryptoUtils.cs
Normal file
33
OF DL/Crypto/CryptoUtils.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Engines;
|
||||||
|
using Org.BouncyCastle.Crypto.Macs;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace WidevineClient.Crypto
|
||||||
|
{
|
||||||
|
public class CryptoUtils
|
||||||
|
{
|
||||||
|
public static byte[] GetHMACSHA256Digest(byte[] data, byte[] key)
|
||||||
|
{
|
||||||
|
return new HMACSHA256(key).ComputeHash(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] GetCMACDigest(byte[] data, byte[] key)
|
||||||
|
{
|
||||||
|
IBlockCipher cipher = new AesEngine();
|
||||||
|
IMac mac = new CMac(cipher, 128);
|
||||||
|
|
||||||
|
KeyParameter keyParam = new KeyParameter(key);
|
||||||
|
|
||||||
|
mac.Init(keyParam);
|
||||||
|
|
||||||
|
mac.BlockUpdate(data, 0, data.Length);
|
||||||
|
|
||||||
|
byte[] outBytes = new byte[16];
|
||||||
|
|
||||||
|
mac.DoFinal(outBytes, 0);
|
||||||
|
return outBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
OF DL/Crypto/Padding.cs
Normal file
121
OF DL/Crypto/Padding.cs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace WidevineClient.Crypto
|
||||||
|
{
|
||||||
|
public class Padding
|
||||||
|
{
|
||||||
|
public static byte[] AddPKCS7Padding(byte[] data, int k)
|
||||||
|
{
|
||||||
|
int m = k - (data.Length % k);
|
||||||
|
|
||||||
|
byte[] padding = new byte[m];
|
||||||
|
Array.Fill(padding, (byte)m);
|
||||||
|
|
||||||
|
byte[] paddedBytes = new byte[data.Length + padding.Length];
|
||||||
|
Buffer.BlockCopy(data, 0, paddedBytes, 0, data.Length);
|
||||||
|
Buffer.BlockCopy(padding, 0, paddedBytes, data.Length, padding.Length);
|
||||||
|
|
||||||
|
return paddedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] RemovePKCS7Padding(byte[] paddedByteArray)
|
||||||
|
{
|
||||||
|
var last = paddedByteArray[^1];
|
||||||
|
if (paddedByteArray.Length <= last)
|
||||||
|
{
|
||||||
|
return paddedByteArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SubArray(paddedByteArray, 0, (paddedByteArray.Length - last));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T[] SubArray<T>(T[] arr, int start, int length)
|
||||||
|
{
|
||||||
|
var result = new T[length];
|
||||||
|
Buffer.BlockCopy(arr, start, result, 0, length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] AddPSSPadding(byte[] hash)
|
||||||
|
{
|
||||||
|
int modBits = 2048;
|
||||||
|
int hLen = 20;
|
||||||
|
int emLen = 256;
|
||||||
|
|
||||||
|
int lmask = 0;
|
||||||
|
for (int i = 0; i < 8 * emLen - (modBits - 1); i++)
|
||||||
|
lmask = lmask >> 1 | 0x80;
|
||||||
|
|
||||||
|
if (emLen < hLen + hLen + 2)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] salt = new byte[hLen];
|
||||||
|
new Random().NextBytes(salt);
|
||||||
|
|
||||||
|
byte[] m_prime = Enumerable.Repeat((byte)0, 8).ToArray().Concat(hash).Concat(salt).ToArray();
|
||||||
|
byte[] h = SHA1.Create().ComputeHash(m_prime);
|
||||||
|
|
||||||
|
byte[] ps = Enumerable.Repeat((byte)0, emLen - hLen - hLen - 2).ToArray();
|
||||||
|
byte[] db = ps.Concat(new byte[] { 0x01 }).Concat(salt).ToArray();
|
||||||
|
|
||||||
|
byte[] dbMask = MGF1(h, emLen - hLen - 1);
|
||||||
|
|
||||||
|
byte[] maskedDb = new byte[dbMask.Length];
|
||||||
|
for (int i = 0; i < dbMask.Length; i++)
|
||||||
|
maskedDb[i] = (byte)(db[i] ^ dbMask[i]);
|
||||||
|
|
||||||
|
maskedDb[0] = (byte)(maskedDb[0] & ~lmask);
|
||||||
|
|
||||||
|
byte[] padded = maskedDb.Concat(h).Concat(new byte[] { 0xBC }).ToArray();
|
||||||
|
|
||||||
|
return padded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] RemoveOAEPPadding(byte[] data)
|
||||||
|
{
|
||||||
|
int k = 256;
|
||||||
|
int hLen = 20;
|
||||||
|
|
||||||
|
byte[] maskedSeed = data[1..(hLen + 1)];
|
||||||
|
byte[] maskedDB = data[(hLen + 1)..];
|
||||||
|
|
||||||
|
byte[] seedMask = MGF1(maskedDB, hLen);
|
||||||
|
|
||||||
|
byte[] seed = new byte[maskedSeed.Length];
|
||||||
|
for (int i = 0; i < maskedSeed.Length; i++)
|
||||||
|
seed[i] = (byte)(maskedSeed[i] ^ seedMask[i]);
|
||||||
|
|
||||||
|
byte[] dbMask = MGF1(seed, k - hLen - 1);
|
||||||
|
|
||||||
|
byte[] db = new byte[maskedDB.Length];
|
||||||
|
for (int i = 0; i < maskedDB.Length; i++)
|
||||||
|
db[i] = (byte)(maskedDB[i] ^ dbMask[i]);
|
||||||
|
|
||||||
|
int onePos = BitConverter.ToString(db[hLen..]).Replace("-", "").IndexOf("01") / 2;
|
||||||
|
byte[] unpadded = db[(hLen + onePos + 1)..];
|
||||||
|
|
||||||
|
return unpadded;
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] MGF1(byte[] seed, int maskLen)
|
||||||
|
{
|
||||||
|
SHA1 hobj = SHA1.Create();
|
||||||
|
int hLen = hobj.HashSize / 8;
|
||||||
|
List<byte> T = new List<byte>();
|
||||||
|
for (int i = 0; i < (int)Math.Ceiling(((double)maskLen / (double)hLen)); i++)
|
||||||
|
{
|
||||||
|
byte[] c = BitConverter.GetBytes(i);
|
||||||
|
Array.Reverse(c);
|
||||||
|
byte[] digest = hobj.ComputeHash(seed.Concat(c).ToArray());
|
||||||
|
T.AddRange(digest);
|
||||||
|
}
|
||||||
|
return T.GetRange(0, maskLen).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
275
OF DL/Entities/Archived/Archived.cs
Normal file
275
OF DL/Entities/Archived/Archived.cs
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using OF_DL.Utils;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Archived
|
||||||
|
{
|
||||||
|
public class Archived
|
||||||
|
{
|
||||||
|
public List<List> list { get; set; }
|
||||||
|
public bool hasMore { get; set; }
|
||||||
|
public string headMarker { get; set; }
|
||||||
|
public string tailMarker { get; set; }
|
||||||
|
public Counters counters { get; set; }
|
||||||
|
public class Author
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public string _view { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Counters
|
||||||
|
{
|
||||||
|
public int? audiosCount { get; set; }
|
||||||
|
public int? photosCount { get; set; }
|
||||||
|
public int? videosCount { get; set; }
|
||||||
|
public int? mediasCount { get; set; }
|
||||||
|
public int? postsCount { get; set; }
|
||||||
|
public int? streamsCount { get; set; }
|
||||||
|
public int? archivedPostsCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Dash
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Drm
|
||||||
|
{
|
||||||
|
public Manifest manifest { get; set; }
|
||||||
|
public Signature signature { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Files
|
||||||
|
{
|
||||||
|
public Full full { get; set; }
|
||||||
|
public Thumb thumb { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
public SquarePreview squarePreview { get; set; }
|
||||||
|
public Drm drm { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Full
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public List<object> sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SquarePreview
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thumb
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Hls
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Info
|
||||||
|
{
|
||||||
|
public Source source { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LinkedPost
|
||||||
|
{
|
||||||
|
public string responseType { get; set; }
|
||||||
|
public int? id { get; set; }
|
||||||
|
public DateTime? postedAt { get; set; }
|
||||||
|
public string postedAtPrecise { get; set; }
|
||||||
|
public object expiredAt { get; set; }
|
||||||
|
public Author author { get; set; }
|
||||||
|
public string text { get; set; }
|
||||||
|
private string _rawText;
|
||||||
|
public string rawText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_rawText))
|
||||||
|
{
|
||||||
|
_rawText = XmlUtils.EvaluateInnerText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _rawText;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_rawText = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool? lockedText { get; set; }
|
||||||
|
public bool? isFavorite { get; set; }
|
||||||
|
public bool? canReport { get; set; }
|
||||||
|
public bool? canDelete { get; set; }
|
||||||
|
public bool? canComment { get; set; }
|
||||||
|
public bool? canEdit { get; set; }
|
||||||
|
public bool? isPinned { get; set; }
|
||||||
|
public int? favoritesCount { get; set; }
|
||||||
|
public int? mediaCount { get; set; }
|
||||||
|
public bool? isMediaReady { get; set; }
|
||||||
|
public object voting { get; set; }
|
||||||
|
public bool? isOpened { get; set; }
|
||||||
|
public bool? canToggleFavorite { get; set; }
|
||||||
|
public object streamId { get; set; }
|
||||||
|
public string? price { get; set; }
|
||||||
|
public bool? hasVoting { get; set; }
|
||||||
|
public bool? isAddedToBookmarks { get; set; }
|
||||||
|
public bool? isArchived { get; set; }
|
||||||
|
public bool? isPrivateArchived { get; set; }
|
||||||
|
public bool? isDeleted { get; set; }
|
||||||
|
public bool? hasUrl { get; set; }
|
||||||
|
public bool? isCouplePeopleMedia { get; set; }
|
||||||
|
public string cantCommentReason { get; set; }
|
||||||
|
public int? commentsCount { get; set; }
|
||||||
|
public List<object> mentionedUsers { get; set; }
|
||||||
|
public List<object> linkedUsers { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public bool? canViewMedia { get; set; }
|
||||||
|
public List<object> preview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class List
|
||||||
|
{
|
||||||
|
public string responseType { get; set; }
|
||||||
|
public long id { get; set; }
|
||||||
|
public DateTime postedAt { get; set; }
|
||||||
|
public string postedAtPrecise { get; set; }
|
||||||
|
public object expiredAt { get; set; }
|
||||||
|
public Author author { get; set; }
|
||||||
|
public string text { get; set; }
|
||||||
|
private string _rawText;
|
||||||
|
public string rawText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_rawText))
|
||||||
|
{
|
||||||
|
_rawText = XmlUtils.EvaluateInnerText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _rawText;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_rawText = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool? lockedText { get; set; }
|
||||||
|
public bool? isFavorite { get; set; }
|
||||||
|
public bool? canReport { get; set; }
|
||||||
|
public bool? canDelete { get; set; }
|
||||||
|
public bool? canComment { get; set; }
|
||||||
|
public bool? canEdit { get; set; }
|
||||||
|
public bool? isPinned { get; set; }
|
||||||
|
public int? favoritesCount { get; set; }
|
||||||
|
public int? mediaCount { get; set; }
|
||||||
|
public bool? isMediaReady { get; set; }
|
||||||
|
public object voting { get; set; }
|
||||||
|
public bool isOpened { get; set; }
|
||||||
|
public bool? canToggleFavorite { get; set; }
|
||||||
|
public object streamId { get; set; }
|
||||||
|
public string price { get; set; }
|
||||||
|
public bool? hasVoting { get; set; }
|
||||||
|
public bool? isAddedToBookmarks { get; set; }
|
||||||
|
public bool isArchived { get; set; }
|
||||||
|
public bool? isPrivateArchived { get; set; }
|
||||||
|
public bool? isDeleted { get; set; }
|
||||||
|
public bool? hasUrl { get; set; }
|
||||||
|
public bool? isCouplePeopleMedia { get; set; }
|
||||||
|
public int? commentsCount { get; set; }
|
||||||
|
public List<object> mentionedUsers { get; set; }
|
||||||
|
public List<object> linkedUsers { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public bool? canViewMedia { get; set; }
|
||||||
|
public List<object> preview { get; set; }
|
||||||
|
public string cantCommentReason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Manifest
|
||||||
|
{
|
||||||
|
public string hls { get; set; }
|
||||||
|
public string dash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Medium
|
||||||
|
{
|
||||||
|
public long id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public bool? convertedToVideo { get; set; }
|
||||||
|
public bool canView { get; set; }
|
||||||
|
public bool? hasError { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public Info info { get; set; }
|
||||||
|
public Source source { get; set; }
|
||||||
|
public string squarePreview { get; set; }
|
||||||
|
public string full { get; set; }
|
||||||
|
public string preview { get; set; }
|
||||||
|
public string thumb { get; set; }
|
||||||
|
public Files files { get; set; }
|
||||||
|
public VideoSources videoSources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Preview
|
||||||
|
{
|
||||||
|
public int? width { get; set; }
|
||||||
|
public int? height { get; set; }
|
||||||
|
public int? size { get; set; }
|
||||||
|
public string url { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Signature
|
||||||
|
{
|
||||||
|
public Hls hls { get; set; }
|
||||||
|
public Dash dash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Source
|
||||||
|
{
|
||||||
|
public string source { get; set; }
|
||||||
|
public int? width { get; set; }
|
||||||
|
public int? height { get; set; }
|
||||||
|
public int? size { get; set; }
|
||||||
|
public int? duration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoSources
|
||||||
|
{
|
||||||
|
[JsonProperty("720")]
|
||||||
|
public string _720 { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("240")]
|
||||||
|
public string _240 { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
OF DL/Entities/Archived/ArchivedCollection.cs
Normal file
15
OF DL/Entities/Archived/ArchivedCollection.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Archived
|
||||||
|
{
|
||||||
|
public class ArchivedCollection
|
||||||
|
{
|
||||||
|
public Dictionary<long, string> ArchivedPosts = new Dictionary<long, string>();
|
||||||
|
public List<Archived.List> ArchivedPostObjects = new List<Archived.List>();
|
||||||
|
public List<Archived.Medium> ArchivedPostMedia = new List<Archived.Medium>();
|
||||||
|
}
|
||||||
|
}
|
19
OF DL/Entities/Auth.cs
Normal file
19
OF DL/Entities/Auth.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public class Auth
|
||||||
|
{
|
||||||
|
public string? USER_ID { get; set; } = string.Empty;
|
||||||
|
public string? USER_AGENT { get; set; } = string.Empty;
|
||||||
|
public string? X_BC { get; set; } = string.Empty;
|
||||||
|
public string? COOKIE { get; set; } = string.Empty;
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? FFMPEG_PATH { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
27
OF DL/Entities/CDRMProjectRequest.cs
Normal file
27
OF DL/Entities/CDRMProjectRequest.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public class CDRMProjectRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("pssh")]
|
||||||
|
public string PSSH { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonProperty("licurl")]
|
||||||
|
public string LicenseURL { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonProperty("headers")]
|
||||||
|
public string Headers { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonProperty("cookies")]
|
||||||
|
public string Cookies { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonProperty("data")]
|
||||||
|
public string Data { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
110
OF DL/Entities/Config.cs
Normal file
110
OF DL/Entities/Config.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using OF_DL.Enumerations;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
|
||||||
|
public class Config : IDownloadConfig, IFileNameFormatConfig
|
||||||
|
{
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadAvatarHeaderPhoto { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadPaidPosts { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadPosts { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadArchived { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadStreams { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadStories { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadHighlights { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadMessages { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadPaidMessages { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadImages { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadVideos { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadAudios { get; set; } = true;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool IncludeExpiredSubscriptions { get; set; } = false;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool IncludeRestrictedSubscriptions { get; set; } = false;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool SkipAds { get; set; } = false;
|
||||||
|
|
||||||
|
public string? DownloadPath { get; set; } = string.Empty;
|
||||||
|
public string? PaidPostFileNameFormat { get; set; } = string.Empty;
|
||||||
|
public string? PostFileNameFormat { get; set; } = string.Empty;
|
||||||
|
public string? PaidMessageFileNameFormat { get; set; } = string.Empty;
|
||||||
|
public string? MessageFileNameFormat { get; set; } = string.Empty;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } = false;
|
||||||
|
public int? Timeout { get; set; } = -1;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool FolderPerPaidPost { get; set; } = false;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool FolderPerPost { get; set; } = false;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool FolderPerPaidMessage { get; set; } = false;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool FolderPerMessage { get; set; } = false;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool LimitDownloadRate { get; set; } = false;
|
||||||
|
public int DownloadLimitInMbPerSec { get; set; } = 4;
|
||||||
|
|
||||||
|
// Indicates if you want to download only on specific dates.
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadOnlySpecificDates { get; set; } = false;
|
||||||
|
|
||||||
|
// This enum will define if we want data from before or after the CustomDate.
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public DownloadDateSelection DownloadDateSelection { get; set; } = DownloadDateSelection.before;
|
||||||
|
// This is the specific date used in combination with the above enum.
|
||||||
|
|
||||||
|
[JsonConverter(typeof(ShortDateConverter))]
|
||||||
|
public DateTime? CustomDate { get; set; } = null;
|
||||||
|
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool ShowScrapeSize { get; set; } = false;
|
||||||
|
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadPostsIncrementally { get; set; } = false;
|
||||||
|
|
||||||
|
public bool NonInteractiveMode { get; set; } = false;
|
||||||
|
public string NonInteractiveModeListName { get; set; } = string.Empty;
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool NonInteractiveModePurchasedTab { get; set; } = false;
|
||||||
|
public string? FFmpegPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } = false;
|
||||||
|
|
||||||
|
public Dictionary<string, CreatorConfig> CreatorConfigs { get; set; } = new Dictionary<string, CreatorConfig>();
|
||||||
|
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool DownloadDuplicatedMedia { get; set; } = false;
|
||||||
|
|
||||||
|
public string IgnoredUsersListName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error;
|
||||||
|
|
||||||
|
[ToggleableConfig]
|
||||||
|
public bool IgnoreOwnMessages { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreatorConfig : IFileNameFormatConfig
|
||||||
|
{
|
||||||
|
public string? PaidPostFileNameFormat { get; set; }
|
||||||
|
public string? PostFileNameFormat { get; set; }
|
||||||
|
public string? PaidMessageFileNameFormat { get; set; }
|
||||||
|
public string? MessageFileNameFormat { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
28
OF DL/Entities/DynamicRules.cs
Normal file
28
OF DL/Entities/DynamicRules.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public class DynamicRules
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName="app-token")]
|
||||||
|
public string? AppToken { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName="app_token")]
|
||||||
|
private string AppToken2 { set { AppToken = value; } }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName="static_param")]
|
||||||
|
public string? StaticParam { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName="prefix")]
|
||||||
|
public string? Prefix { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName="suffix")]
|
||||||
|
public string? Suffix { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName="checksum_constant")]
|
||||||
|
public int? ChecksumConstant { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "checksum_indexes")]
|
||||||
|
public List<int> ChecksumIndexes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
11
OF DL/Entities/FileNameFormatConfig.cs
Normal file
11
OF DL/Entities/FileNameFormatConfig.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public class FileNameFormatConfig : IFileNameFormatConfig
|
||||||
|
{
|
||||||
|
public string? PaidPostFileNameFormat { get; set; }
|
||||||
|
public string? PostFileNameFormat { get; set; }
|
||||||
|
public string? PaidMessageFileNameFormat { get; set; }
|
||||||
|
public string? MessageFileNameFormat { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
108
OF DL/Entities/Highlights/HighlightMedia.cs
Normal file
108
OF DL/Entities/Highlights/HighlightMedia.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Highlights
|
||||||
|
{
|
||||||
|
public class HighlightMedia
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public int userId { get; set; }
|
||||||
|
public string title { get; set; }
|
||||||
|
public int coverStoryId { get; set; }
|
||||||
|
public string cover { get; set; }
|
||||||
|
public int storiesCount { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public List<Story> stories { get; set; }
|
||||||
|
public class Files
|
||||||
|
{
|
||||||
|
public Full full { get; set; }
|
||||||
|
public Thumb thumb { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
public SquarePreview squarePreview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Full
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public List<object> sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Medium
|
||||||
|
{
|
||||||
|
public long id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public bool convertedToVideo { get; set; }
|
||||||
|
public bool canView { get; set; }
|
||||||
|
public bool hasError { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public Files files { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Preview
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public Sources sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Source
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int duration { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public Sources sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Sources
|
||||||
|
{
|
||||||
|
[JsonProperty("720")]
|
||||||
|
public string _720 { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("240")]
|
||||||
|
public string _240 { get; set; }
|
||||||
|
public string w150 { get; set; }
|
||||||
|
public string w480 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SquarePreview
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public Sources sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Story
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public int userId { get; set; }
|
||||||
|
public bool isWatched { get; set; }
|
||||||
|
public bool isReady { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public object question { get; set; }
|
||||||
|
public bool canLike { get; set; }
|
||||||
|
public bool isLiked { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thumb
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
OF DL/Entities/Highlights/Highlights.cs
Normal file
24
OF DL/Entities/Highlights/Highlights.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Highlights
|
||||||
|
{
|
||||||
|
public class Highlights
|
||||||
|
{
|
||||||
|
public List<List> list { get; set; }
|
||||||
|
public bool hasMore { get; set; }
|
||||||
|
public class List
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public int userId { get; set; }
|
||||||
|
public string title { get; set; }
|
||||||
|
public int coverStoryId { get; set; }
|
||||||
|
public string cover { get; set; }
|
||||||
|
public int storiesCount { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
OF DL/Entities/IDownloadConfig.cs
Normal file
54
OF DL/Entities/IDownloadConfig.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using OF_DL.Enumerations;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public interface IDownloadConfig
|
||||||
|
{
|
||||||
|
bool DownloadAvatarHeaderPhoto { get; set; }
|
||||||
|
bool DownloadPaidPosts { get; set; }
|
||||||
|
bool DownloadPosts { get; set; }
|
||||||
|
bool DownloadArchived { get; set; }
|
||||||
|
bool DownloadStreams { get; set; }
|
||||||
|
bool DownloadStories { get; set; }
|
||||||
|
bool DownloadHighlights { get; set; }
|
||||||
|
bool DownloadMessages { get; set; }
|
||||||
|
bool DownloadPaidMessages { get; set; }
|
||||||
|
bool DownloadImages { get; set; }
|
||||||
|
bool DownloadVideos { get; set; }
|
||||||
|
bool DownloadAudios { get; set; }
|
||||||
|
|
||||||
|
int? Timeout { get; set; }
|
||||||
|
bool FolderPerPaidPost { get; set; }
|
||||||
|
bool FolderPerPost { get; set; }
|
||||||
|
bool FolderPerPaidMessage { get; set; }
|
||||||
|
bool FolderPerMessage { get; set; }
|
||||||
|
|
||||||
|
bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; }
|
||||||
|
bool ShowScrapeSize { get; set; }
|
||||||
|
bool LimitDownloadRate { get; set; }
|
||||||
|
int DownloadLimitInMbPerSec { get; set; }
|
||||||
|
string? FFmpegPath { get; set; }
|
||||||
|
|
||||||
|
bool SkipAds { get; set; }
|
||||||
|
bool BypassContentForCreatorsWhoNoLongerExist { get; set; }
|
||||||
|
|
||||||
|
#region Download Date Configurations
|
||||||
|
|
||||||
|
bool DownloadOnlySpecificDates { get; set; }
|
||||||
|
|
||||||
|
// This enum will define if we want data from before or after the CustomDate.
|
||||||
|
DownloadDateSelection DownloadDateSelection { get; set; }
|
||||||
|
|
||||||
|
// This is the specific date used in combination with the above enum.
|
||||||
|
DateTime? CustomDate { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
bool DownloadPostsIncrementally { get; set; }
|
||||||
|
|
||||||
|
bool DownloadDuplicatedMedia { get; set; }
|
||||||
|
public LoggingLevel LoggingLevel { get; set; }
|
||||||
|
|
||||||
|
bool IgnoreOwnMessages { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
11
OF DL/Entities/IFileNameFormatConfig.cs
Normal file
11
OF DL/Entities/IFileNameFormatConfig.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public interface IFileNameFormatConfig
|
||||||
|
{
|
||||||
|
string? PaidPostFileNameFormat { get; set; }
|
||||||
|
string? PostFileNameFormat { get; set; }
|
||||||
|
string? PaidMessageFileNameFormat { get; set; }
|
||||||
|
string? MessageFileNameFormat { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
41
OF DL/Entities/Lists/UserList.cs
Normal file
41
OF DL/Entities/Lists/UserList.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Lists
|
||||||
|
{
|
||||||
|
public class UserList
|
||||||
|
{
|
||||||
|
public List<List> list { get; set; }
|
||||||
|
public bool? hasMore { get; set; }
|
||||||
|
public class List
|
||||||
|
{
|
||||||
|
public string id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public string name { get; set; }
|
||||||
|
public int? usersCount { get; set; }
|
||||||
|
public int? postsCount { get; set; }
|
||||||
|
public bool? canUpdate { get; set; }
|
||||||
|
public bool? canDelete { get; set; }
|
||||||
|
public bool? canManageUsers { get; set; }
|
||||||
|
public bool? canAddUsers { get; set; }
|
||||||
|
public bool? canPinnedToFeed { get; set; }
|
||||||
|
public bool? isPinnedToFeed { get; set; }
|
||||||
|
public bool? canPinnedToChat { get; set; }
|
||||||
|
public bool? isPinnedToChat { get; set; }
|
||||||
|
public string order { get; set; }
|
||||||
|
public string direction { get; set; }
|
||||||
|
public List<User> users { get; set; }
|
||||||
|
public List<object> customOrderUsersIds { get; set; }
|
||||||
|
public List<object> posts { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public int? id { get; set; }
|
||||||
|
public string _view { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
171
OF DL/Entities/Lists/UsersList.cs
Normal file
171
OF DL/Entities/Lists/UsersList.cs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Lists
|
||||||
|
{
|
||||||
|
public class UsersList
|
||||||
|
{
|
||||||
|
public string view { get; set; }
|
||||||
|
public string avatar { get; set; }
|
||||||
|
public AvatarThumbs avatarThumbs { get; set; }
|
||||||
|
public string header { get; set; }
|
||||||
|
public HeaderSize headerSize { get; set; }
|
||||||
|
public HeaderThumbs headerThumbs { get; set; }
|
||||||
|
public int? id { get; set; }
|
||||||
|
public string name { get; set; }
|
||||||
|
public string username { get; set; }
|
||||||
|
public bool? canLookStory { get; set; }
|
||||||
|
public bool? canCommentStory { get; set; }
|
||||||
|
public bool? hasNotViewedStory { get; set; }
|
||||||
|
public bool? isVerified { get; set; }
|
||||||
|
public bool? canPayInternal { get; set; }
|
||||||
|
public bool? hasScheduledStream { get; set; }
|
||||||
|
public bool? hasStream { get; set; }
|
||||||
|
public bool? hasStories { get; set; }
|
||||||
|
public bool? tipsEnabled { get; set; }
|
||||||
|
public bool? tipsTextEnabled { get; set; }
|
||||||
|
public int? tipsMin { get; set; }
|
||||||
|
public int? tipsMinInternal { get; set; }
|
||||||
|
public int? tipsMax { get; set; }
|
||||||
|
public bool? canEarn { get; set; }
|
||||||
|
public bool? canAddSubscriber { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public List<SubscriptionBundle> subscriptionBundles { get; set; }
|
||||||
|
public string displayName { get; set; }
|
||||||
|
public string notice { get; set; }
|
||||||
|
public bool? isPaywallRequired { get; set; }
|
||||||
|
public bool? unprofitable { get; set; }
|
||||||
|
public List<ListsState> listsStates { get; set; }
|
||||||
|
public bool? isMuted { get; set; }
|
||||||
|
public bool? isRestricted { get; set; }
|
||||||
|
public bool? canRestrict { get; set; }
|
||||||
|
public bool? subscribedBy { get; set; }
|
||||||
|
public bool? subscribedByExpire { get; set; }
|
||||||
|
public DateTime? subscribedByExpireDate { get; set; }
|
||||||
|
public bool? subscribedByAutoprolong { get; set; }
|
||||||
|
public bool? subscribedIsExpiredNow { get; set; }
|
||||||
|
public string? currentSubscribePrice { get; set; }
|
||||||
|
public bool? subscribedOn { get; set; }
|
||||||
|
public bool? subscribedOnExpiredNow { get; set; }
|
||||||
|
public string subscribedOnDuration { get; set; }
|
||||||
|
public bool? canReport { get; set; }
|
||||||
|
public bool? canReceiveChatMessage { get; set; }
|
||||||
|
public bool? hideChat { get; set; }
|
||||||
|
public DateTime? lastSeen { get; set; }
|
||||||
|
public bool? isPerformer { get; set; }
|
||||||
|
public bool? isRealPerformer { get; set; }
|
||||||
|
public SubscribedByData subscribedByData { get; set; }
|
||||||
|
public SubscribedOnData subscribedOnData { get; set; }
|
||||||
|
public bool? canTrialSend { get; set; }
|
||||||
|
public bool? isBlocked { get; set; }
|
||||||
|
public List<object> promoOffers { get; set; }
|
||||||
|
public class AvatarThumbs
|
||||||
|
{
|
||||||
|
public string c50 { get; set; }
|
||||||
|
public string c144 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeaderSize
|
||||||
|
{
|
||||||
|
public int? width { get; set; }
|
||||||
|
public int? height { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeaderThumbs
|
||||||
|
{
|
||||||
|
public string w480 { get; set; }
|
||||||
|
public string w760 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ListsState
|
||||||
|
{
|
||||||
|
public string id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public string name { get; set; }
|
||||||
|
public bool hasUser { get; set; }
|
||||||
|
public bool canAddUser { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Subscribe
|
||||||
|
{
|
||||||
|
public object id { get; set; }
|
||||||
|
public int? userId { get; set; }
|
||||||
|
public int? subscriberId { get; set; }
|
||||||
|
public DateTime? date { get; set; }
|
||||||
|
public int? duration { get; set; }
|
||||||
|
public DateTime? startDate { get; set; }
|
||||||
|
public DateTime? expireDate { get; set; }
|
||||||
|
public object cancelDate { get; set; }
|
||||||
|
public string? price { get; set; }
|
||||||
|
public string? regularPrice { get; set; }
|
||||||
|
public string? discount { get; set; }
|
||||||
|
public string action { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public object offerStart { get; set; }
|
||||||
|
public object offerEnd { get; set; }
|
||||||
|
public bool? isCurrent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscribedByData
|
||||||
|
{
|
||||||
|
public string? price { get; set; }
|
||||||
|
public string? newPrice { get; set; }
|
||||||
|
public string? regularPrice { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public string? discountPercent { get; set; }
|
||||||
|
public string? discountPeriod { get; set; }
|
||||||
|
public DateTime? subscribeAt { get; set; }
|
||||||
|
public DateTime? expiredAt { get; set; }
|
||||||
|
public object renewedAt { get; set; }
|
||||||
|
public object discountFinishedAt { get; set; }
|
||||||
|
public object discountStartedAt { get; set; }
|
||||||
|
public string status { get; set; }
|
||||||
|
public bool? isMuted { get; set; }
|
||||||
|
public string unsubscribeReason { get; set; }
|
||||||
|
public string duration { get; set; }
|
||||||
|
public bool? showPostsInFeed { get; set; }
|
||||||
|
public List<Subscribe> subscribes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscribedOnData
|
||||||
|
{
|
||||||
|
public string? price { get; set; }
|
||||||
|
public string? newPrice { get; set; }
|
||||||
|
public string? regularPrice { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public string? discountPercent { get; set; }
|
||||||
|
public string? discountPeriod { get; set; }
|
||||||
|
public DateTime? subscribeAt { get; set; }
|
||||||
|
public DateTime? expiredAt { get; set; }
|
||||||
|
public object renewedAt { get; set; }
|
||||||
|
public object discountFinishedAt { get; set; }
|
||||||
|
public object discountStartedAt { get; set; }
|
||||||
|
public object status { get; set; }
|
||||||
|
public bool? isMuted { get; set; }
|
||||||
|
public string unsubscribeReason { get; set; }
|
||||||
|
public string duration { get; set; }
|
||||||
|
public string? tipsSumm { get; set; }
|
||||||
|
public string? subscribesSumm { get; set; }
|
||||||
|
public string? messagesSumm { get; set; }
|
||||||
|
public string? postsSumm { get; set; }
|
||||||
|
public string? streamsSumm { get; set; }
|
||||||
|
public string? totalSumm { get; set; }
|
||||||
|
public DateTime? lastActivity { get; set; }
|
||||||
|
public int? recommendations { get; set; }
|
||||||
|
public List<object> subscribes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscriptionBundle
|
||||||
|
{
|
||||||
|
public int? id { get; set; }
|
||||||
|
public string? discount { get; set; }
|
||||||
|
public string? duration { get; set; }
|
||||||
|
public string? price { get; set; }
|
||||||
|
public bool? canBuy { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
15
OF DL/Entities/Messages/MessageCollection.cs
Normal file
15
OF DL/Entities/Messages/MessageCollection.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Messages
|
||||||
|
{
|
||||||
|
public class MessageCollection
|
||||||
|
{
|
||||||
|
public Dictionary<long, string> Messages = new Dictionary<long, string>();
|
||||||
|
public List<Messages.List> MessageObjects = new List<Messages.List>();
|
||||||
|
public List<Messages.Medium> MessageMedia = new List<Messages.Medium>();
|
||||||
|
}
|
||||||
|
}
|
184
OF DL/Entities/Messages/Messages.cs
Normal file
184
OF DL/Entities/Messages/Messages.cs
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Messages
|
||||||
|
{
|
||||||
|
public class Messages
|
||||||
|
{
|
||||||
|
public List<List> list { get; set; }
|
||||||
|
public bool hasMore { get; set; }
|
||||||
|
public class Dash
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Drm
|
||||||
|
{
|
||||||
|
public Manifest manifest { get; set; }
|
||||||
|
public Signature signature { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Files
|
||||||
|
{
|
||||||
|
public Full full { get; set; }
|
||||||
|
public Thumb thumb { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
public SquarePreview squarePreview { get; set; }
|
||||||
|
public Drm drm { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Full
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public List<object> sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SquarePreview
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thumb
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FromUser
|
||||||
|
{
|
||||||
|
public int? id { get; set; }
|
||||||
|
public string _view { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Hls
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Info
|
||||||
|
{
|
||||||
|
public Source source { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class List
|
||||||
|
{
|
||||||
|
public string responseType { get; set; }
|
||||||
|
public string text { get; set; }
|
||||||
|
public object giphyId { get; set; }
|
||||||
|
public bool? lockedText { get; set; }
|
||||||
|
public bool? isFree { get; set; }
|
||||||
|
public string? price { get; set; }
|
||||||
|
public bool? isMediaReady { get; set; }
|
||||||
|
public int? mediaCount { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public List<object> previews { get; set; }
|
||||||
|
public bool? isTip { get; set; }
|
||||||
|
public bool? isReportedByMe { get; set; }
|
||||||
|
public bool? isCouplePeopleMedia { get; set; }
|
||||||
|
public object queueId { get; set; }
|
||||||
|
public FromUser fromUser { get; set; }
|
||||||
|
public bool? isFromQueue { get; set; }
|
||||||
|
public bool? canUnsendQueue { get; set; }
|
||||||
|
public int? unsendSecondsQueue { get; set; }
|
||||||
|
public long id { get; set; }
|
||||||
|
public bool? isOpened { get; set; }
|
||||||
|
public bool? isNew { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public DateTime? changedAt { get; set; }
|
||||||
|
public int? cancelSeconds { get; set; }
|
||||||
|
public bool? isLiked { get; set; }
|
||||||
|
public bool? canPurchase { get; set; }
|
||||||
|
public string canPurchaseReason { get; set; }
|
||||||
|
public bool? canReport { get; set; }
|
||||||
|
public bool? canBePinned { get; set; }
|
||||||
|
public bool? isPinned { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Manifest
|
||||||
|
{
|
||||||
|
public string hls { get; set; }
|
||||||
|
public string dash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Medium
|
||||||
|
{
|
||||||
|
public long id { get; set; }
|
||||||
|
public bool canView { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public string src { get; set; }
|
||||||
|
public string preview { get; set; }
|
||||||
|
public string thumb { get; set; }
|
||||||
|
public object locked { get; set; }
|
||||||
|
public int? duration { get; set; }
|
||||||
|
public bool? hasError { get; set; }
|
||||||
|
public string squarePreview { get; set; }
|
||||||
|
public Video video { get; set; }
|
||||||
|
public VideoSources videoSources { get; set; }
|
||||||
|
public Source source { get; set; }
|
||||||
|
public Info info { get; set; }
|
||||||
|
public Files files { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Preview
|
||||||
|
{
|
||||||
|
public int? width { get; set; }
|
||||||
|
public int? height { get; set; }
|
||||||
|
public int? size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Signature
|
||||||
|
{
|
||||||
|
public Hls hls { get; set; }
|
||||||
|
public Dash dash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Source
|
||||||
|
{
|
||||||
|
public string source { get; set; }
|
||||||
|
public int? width { get; set; }
|
||||||
|
public int? height { get; set; }
|
||||||
|
public int? size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Video
|
||||||
|
{
|
||||||
|
public string mp4 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoSources
|
||||||
|
{
|
||||||
|
[JsonProperty("720")]
|
||||||
|
public string _720 { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("240")]
|
||||||
|
public string _240 { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
119
OF DL/Entities/Messages/SingleMessage.cs
Normal file
119
OF DL/Entities/Messages/SingleMessage.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using static OF_DL.Entities.Messages.Messages;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Messages
|
||||||
|
{
|
||||||
|
public class AvatarThumbs
|
||||||
|
{
|
||||||
|
public string c50 { get; set; }
|
||||||
|
public string c144 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FromUser
|
||||||
|
{
|
||||||
|
public string view { get; set; }
|
||||||
|
public string avatar { get; set; }
|
||||||
|
public AvatarThumbs avatarThumbs { get; set; }
|
||||||
|
public string header { get; set; }
|
||||||
|
public HeaderSize headerSize { get; set; }
|
||||||
|
public HeaderThumbs headerThumbs { get; set; }
|
||||||
|
public int? id { get; set; }
|
||||||
|
public string name { get; set; }
|
||||||
|
public string username { get; set; }
|
||||||
|
public bool canLookStory { get; set; }
|
||||||
|
public bool canCommentStory { get; set; }
|
||||||
|
public bool hasNotViewedStory { get; set; }
|
||||||
|
public bool isVerified { get; set; }
|
||||||
|
public bool canPayInternal { get; set; }
|
||||||
|
public bool hasScheduledStream { get; set; }
|
||||||
|
public bool hasStream { get; set; }
|
||||||
|
public bool hasStories { get; set; }
|
||||||
|
public bool tipsEnabled { get; set; }
|
||||||
|
public bool tipsTextEnabled { get; set; }
|
||||||
|
public int tipsMin { get; set; }
|
||||||
|
public int tipsMinInternal { get; set; }
|
||||||
|
public int tipsMax { get; set; }
|
||||||
|
public bool canEarn { get; set; }
|
||||||
|
public bool canAddSubscriber { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public List<object> subscriptionBundles { get; set; }
|
||||||
|
public bool isPaywallRequired { get; set; }
|
||||||
|
public List<ListsState> listsStates { get; set; }
|
||||||
|
public bool isRestricted { get; set; }
|
||||||
|
public bool canRestrict { get; set; }
|
||||||
|
public object subscribedBy { get; set; }
|
||||||
|
public object subscribedByExpire { get; set; }
|
||||||
|
public DateTime subscribedByExpireDate { get; set; }
|
||||||
|
public object subscribedByAutoprolong { get; set; }
|
||||||
|
public bool subscribedIsExpiredNow { get; set; }
|
||||||
|
public object currentSubscribePrice { get; set; }
|
||||||
|
public object subscribedOn { get; set; }
|
||||||
|
public object subscribedOnExpiredNow { get; set; }
|
||||||
|
public object subscribedOnDuration { get; set; }
|
||||||
|
public int callPrice { get; set; }
|
||||||
|
public DateTime? lastSeen { get; set; }
|
||||||
|
public bool canReport { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeaderSize
|
||||||
|
{
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeaderThumbs
|
||||||
|
{
|
||||||
|
public string w480 { get; set; }
|
||||||
|
public string w760 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ListsState
|
||||||
|
{
|
||||||
|
public string id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public string name { get; set; }
|
||||||
|
public bool hasUser { get; set; }
|
||||||
|
public bool canAddUser { get; set; }
|
||||||
|
public string cannotAddUserReason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Preview
|
||||||
|
{
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SingleMessage
|
||||||
|
{
|
||||||
|
public string responseType { get; set; }
|
||||||
|
public string text { get; set; }
|
||||||
|
public object giphyId { get; set; }
|
||||||
|
public bool lockedText { get; set; }
|
||||||
|
public bool isFree { get; set; }
|
||||||
|
public double price { get; set; }
|
||||||
|
public bool isMediaReady { get; set; }
|
||||||
|
public int mediaCount { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public List<object> previews { get; set; }
|
||||||
|
public bool isTip { get; set; }
|
||||||
|
public bool isReportedByMe { get; set; }
|
||||||
|
public bool isCouplePeopleMedia { get; set; }
|
||||||
|
public long queueId { get; set; }
|
||||||
|
public FromUser fromUser { get; set; }
|
||||||
|
public bool isFromQueue { get; set; }
|
||||||
|
public bool canUnsendQueue { get; set; }
|
||||||
|
public int unsendSecondsQueue { get; set; }
|
||||||
|
public long id { get; set; }
|
||||||
|
public bool isOpened { get; set; }
|
||||||
|
public bool isNew { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public DateTime? changedAt { get; set; }
|
||||||
|
public int cancelSeconds { get; set; }
|
||||||
|
public bool isLiked { get; set; }
|
||||||
|
public bool canPurchase { get; set; }
|
||||||
|
public bool canReport { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
21
OF DL/Entities/OFDLRequest.cs
Normal file
21
OF DL/Entities/OFDLRequest.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public class OFDLRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("pssh")]
|
||||||
|
public string PSSH { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonProperty("licenceURL")]
|
||||||
|
public string LicenseURL { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonProperty("headers")]
|
||||||
|
public string Headers { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
213
OF DL/Entities/Post/Post.cs
Normal file
213
OF DL/Entities/Post/Post.cs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using OF_DL.Utils;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static OF_DL.Entities.Messages.Messages;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Post;
|
||||||
|
|
||||||
|
#pragma warning disable IDE1006 // Naming Styles
|
||||||
|
public class Post
|
||||||
|
{
|
||||||
|
public List<List> list { get; set; }
|
||||||
|
public bool hasMore { get; set; }
|
||||||
|
|
||||||
|
public string headMarker { get; set; }
|
||||||
|
|
||||||
|
public string tailMarker { get; set; }
|
||||||
|
public class Author
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public string _view { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Dash
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Drm
|
||||||
|
{
|
||||||
|
public Manifest manifest { get; set; }
|
||||||
|
public Signature signature { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Files
|
||||||
|
{
|
||||||
|
public Full full { get; set; }
|
||||||
|
public Thumb thumb { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
public SquarePreview squarePreview { get; set; }
|
||||||
|
public Drm drm { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Full
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public List<object> sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SquarePreview
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thumb
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Hls
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Info
|
||||||
|
{
|
||||||
|
public Source source { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class List
|
||||||
|
{
|
||||||
|
public string responseType { get; set; }
|
||||||
|
public long id { get; set; }
|
||||||
|
public DateTime postedAt { get; set; }
|
||||||
|
public string postedAtPrecise { get; set; }
|
||||||
|
public object expiredAt { get; set; }
|
||||||
|
public Author author { get; set; }
|
||||||
|
public string text { get; set; }
|
||||||
|
|
||||||
|
private string _rawText;
|
||||||
|
public string rawText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if(string.IsNullOrEmpty(_rawText))
|
||||||
|
{
|
||||||
|
_rawText = XmlUtils.EvaluateInnerText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _rawText;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_rawText = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool? lockedText { get; set; }
|
||||||
|
public bool? isFavorite { get; set; }
|
||||||
|
public bool? canReport { get; set; }
|
||||||
|
public bool? canDelete { get; set; }
|
||||||
|
public bool? canComment { get; set; }
|
||||||
|
public bool? canEdit { get; set; }
|
||||||
|
public bool? isPinned { get; set; }
|
||||||
|
public int? favoritesCount { get; set; }
|
||||||
|
public int? mediaCount { get; set; }
|
||||||
|
public bool? isMediaReady { get; set; }
|
||||||
|
public object voting { get; set; }
|
||||||
|
public bool isOpened { get; set; }
|
||||||
|
public bool? canToggleFavorite { get; set; }
|
||||||
|
public object streamId { get; set; }
|
||||||
|
public string? price { get; set; }
|
||||||
|
public bool? hasVoting { get; set; }
|
||||||
|
public bool? isAddedToBookmarks { get; set; }
|
||||||
|
public bool isArchived { get; set; }
|
||||||
|
public bool? isPrivateArchived { get; set; }
|
||||||
|
public bool? isDeleted { get; set; }
|
||||||
|
public bool? hasUrl { get; set; }
|
||||||
|
public bool? isCouplePeopleMedia { get; set; }
|
||||||
|
public string cantCommentReason { get; set; }
|
||||||
|
public int? votingType { get; set; }
|
||||||
|
public int? commentsCount { get; set; }
|
||||||
|
public List<object> mentionedUsers { get; set; }
|
||||||
|
public List<object> linkedUsers { get; set; }
|
||||||
|
public bool? canVote { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public bool? canViewMedia { get; set; }
|
||||||
|
public List<object> preview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Manifest
|
||||||
|
{
|
||||||
|
public string? hls { get; set; }
|
||||||
|
public string? dash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Medium
|
||||||
|
{
|
||||||
|
public long id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public bool? convertedToVideo { get; set; }
|
||||||
|
public bool canView { get; set; }
|
||||||
|
public bool? hasError { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public Info info { get; set; }
|
||||||
|
public Source source { get; set; }
|
||||||
|
public string squarePreview { get; set; }
|
||||||
|
public string full { get; set; }
|
||||||
|
public string preview { get; set; }
|
||||||
|
public string thumb { get; set; }
|
||||||
|
public Files files { get; set; }
|
||||||
|
public VideoSources videoSources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Preview
|
||||||
|
{
|
||||||
|
public int? width { get; set; }
|
||||||
|
public int? height { get; set; }
|
||||||
|
public int? size { get; set; }
|
||||||
|
public string url { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Signature
|
||||||
|
{
|
||||||
|
public Hls hls { get; set; }
|
||||||
|
public Dash dash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Source
|
||||||
|
{
|
||||||
|
public string? source { get; set; }
|
||||||
|
public int? width { get; set; }
|
||||||
|
public int? height { get; set; }
|
||||||
|
public int? size { get; set; }
|
||||||
|
public int? duration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoSources
|
||||||
|
{
|
||||||
|
[JsonProperty("720")]
|
||||||
|
public object _720 { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("240")]
|
||||||
|
public object _240 { get; set; }
|
||||||
|
}
|
||||||
|
#pragma warning restore IDE1006 // Naming Styles
|
||||||
|
}
|
15
OF DL/Entities/Post/PostCollection.cs
Normal file
15
OF DL/Entities/Post/PostCollection.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Post
|
||||||
|
{
|
||||||
|
public class PostCollection
|
||||||
|
{
|
||||||
|
public Dictionary<long, string> Posts = new Dictionary<long, string>();
|
||||||
|
public List<Post.List> PostObjects = new List<Post.List>();
|
||||||
|
public List<Post.Medium> PostMedia = new List<Post.Medium>();
|
||||||
|
}
|
||||||
|
}
|
197
OF DL/Entities/Post/SinglePost.cs
Normal file
197
OF DL/Entities/Post/SinglePost.cs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using OF_DL.Utils;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static OF_DL.Entities.Post.Post;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Post
|
||||||
|
{
|
||||||
|
public class SinglePost
|
||||||
|
{
|
||||||
|
public string responseType { get; set; }
|
||||||
|
public int id { get; set; }
|
||||||
|
public DateTime postedAt { get; set; }
|
||||||
|
public string postedAtPrecise { get; set; }
|
||||||
|
public object expiredAt { get; set; }
|
||||||
|
public Author author { get; set; }
|
||||||
|
public string text { get; set; }
|
||||||
|
private string _rawText;
|
||||||
|
public string rawText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_rawText))
|
||||||
|
{
|
||||||
|
_rawText = XmlUtils.EvaluateInnerText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _rawText;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_rawText = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool lockedText { get; set; }
|
||||||
|
public bool isFavorite { get; set; }
|
||||||
|
public bool canReport { get; set; }
|
||||||
|
public bool canDelete { get; set; }
|
||||||
|
public bool canComment { get; set; }
|
||||||
|
public bool canEdit { get; set; }
|
||||||
|
public bool isPinned { get; set; }
|
||||||
|
public int favoritesCount { get; set; }
|
||||||
|
public int mediaCount { get; set; }
|
||||||
|
public bool isMediaReady { get; set; }
|
||||||
|
public object voting { get; set; }
|
||||||
|
public bool isOpened { get; set; }
|
||||||
|
public bool canToggleFavorite { get; set; }
|
||||||
|
public string streamId { get; set; }
|
||||||
|
public string price { get; set; }
|
||||||
|
public bool hasVoting { get; set; }
|
||||||
|
public bool isAddedToBookmarks { get; set; }
|
||||||
|
public bool isArchived { get; set; }
|
||||||
|
public bool isPrivateArchived { get; set; }
|
||||||
|
public bool isDeleted { get; set; }
|
||||||
|
public bool hasUrl { get; set; }
|
||||||
|
public bool isCouplePeopleMedia { get; set; }
|
||||||
|
public int commentsCount { get; set; }
|
||||||
|
public List<object> mentionedUsers { get; set; }
|
||||||
|
public List<object> linkedUsers { get; set; }
|
||||||
|
public string tipsAmount { get; set; }
|
||||||
|
public string tipsAmountRaw { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public bool canViewMedia { get; set; }
|
||||||
|
public List<object> preview { get; set; }
|
||||||
|
public class Author
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public string _view { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Files
|
||||||
|
{
|
||||||
|
public Full full { get; set; }
|
||||||
|
public Thumb thumb { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
public SquarePreview squarePreview { get; set; }
|
||||||
|
public Drm drm { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Full
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public List<object> sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SquarePreview
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thumb
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Info
|
||||||
|
{
|
||||||
|
public Source source { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Medium
|
||||||
|
{
|
||||||
|
public long id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public bool convertedToVideo { get; set; }
|
||||||
|
public bool canView { get; set; }
|
||||||
|
public bool hasError { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public Info info { get; set; }
|
||||||
|
public Source source { get; set; }
|
||||||
|
public string squarePreview { get; set; }
|
||||||
|
public string full { get; set; }
|
||||||
|
public string preview { get; set; }
|
||||||
|
public string thumb { get; set; }
|
||||||
|
public bool hasCustomPreview { get; set; }
|
||||||
|
public Files files { get; set; }
|
||||||
|
public VideoSources videoSources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Preview
|
||||||
|
{
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public string url { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Source
|
||||||
|
{
|
||||||
|
public string source { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public int duration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoSources
|
||||||
|
{
|
||||||
|
[JsonProperty("720")]
|
||||||
|
public object _720 { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("240")]
|
||||||
|
public object _240 { get; set; }
|
||||||
|
}
|
||||||
|
public class Dash
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Drm
|
||||||
|
{
|
||||||
|
public Manifest manifest { get; set; }
|
||||||
|
public Signature signature { get; set; }
|
||||||
|
}
|
||||||
|
public class Hls
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
public class Manifest
|
||||||
|
{
|
||||||
|
public string? hls { get; set; }
|
||||||
|
public string? dash { get; set; }
|
||||||
|
}
|
||||||
|
public class Signature
|
||||||
|
{
|
||||||
|
public Hls hls { get; set; }
|
||||||
|
public Dash dash { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
OF DL/Entities/Post/SinglePostCollection.cs
Normal file
15
OF DL/Entities/Post/SinglePostCollection.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Post
|
||||||
|
{
|
||||||
|
public class SinglePostCollection
|
||||||
|
{
|
||||||
|
public Dictionary<long, string> SinglePosts = new Dictionary<long, string>();
|
||||||
|
public List<SinglePost> SinglePostObjects = new List<SinglePost>();
|
||||||
|
public List<SinglePost.Medium> SinglePostMedia = new List<SinglePost.Medium>();
|
||||||
|
}
|
||||||
|
}
|
16
OF DL/Entities/Purchased/PaidMessageCollection.cs
Normal file
16
OF DL/Entities/Purchased/PaidMessageCollection.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static OF_DL.Entities.Messages.Messages;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Purchased
|
||||||
|
{
|
||||||
|
public class PaidMessageCollection
|
||||||
|
{
|
||||||
|
public Dictionary<long, string> PaidMessages = new Dictionary<long, string>();
|
||||||
|
public List<Purchased.List> PaidMessageObjects = new List<Purchased.List>();
|
||||||
|
public List<Medium> PaidMessageMedia = new List<Medium>();
|
||||||
|
}
|
||||||
|
}
|
11
OF DL/Entities/Purchased/PaidPostCollection.cs
Normal file
11
OF DL/Entities/Purchased/PaidPostCollection.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using static OF_DL.Entities.Messages.Messages;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Purchased
|
||||||
|
{
|
||||||
|
public class PaidPostCollection
|
||||||
|
{
|
||||||
|
public Dictionary<long, string> PaidPosts = new Dictionary<long, string>();
|
||||||
|
public List<Purchased.List> PaidPostObjects = new List<Purchased.List>();
|
||||||
|
public List<Medium> PaidPostMedia = new List<Medium>();
|
||||||
|
}
|
||||||
|
}
|
90
OF DL/Entities/Purchased/Purchased.cs
Normal file
90
OF DL/Entities/Purchased/Purchased.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static OF_DL.Entities.Messages.Messages;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Purchased
|
||||||
|
{
|
||||||
|
public class Purchased
|
||||||
|
{
|
||||||
|
public List<List> list { get; set; }
|
||||||
|
public bool hasMore { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class FromUser
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public string _view { get; set; }
|
||||||
|
}
|
||||||
|
public class Author
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public string _view { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Hls
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class List
|
||||||
|
{
|
||||||
|
public string responseType { get; set; }
|
||||||
|
public string text { get; set; }
|
||||||
|
public object giphyId { get; set; }
|
||||||
|
public bool? lockedText { get; set; }
|
||||||
|
public bool? isFree { get; set; }
|
||||||
|
public string? price { get; set; }
|
||||||
|
public bool? isMediaReady { get; set; }
|
||||||
|
public int? mediaCount { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public List<object> previews { get; set; }
|
||||||
|
public List<object> preview { get; set; }
|
||||||
|
public bool? isTip { get; set; }
|
||||||
|
public bool? isReportedByMe { get; set; }
|
||||||
|
public bool? isCouplePeopleMedia { get; set; }
|
||||||
|
public object queueId { get; set; }
|
||||||
|
public FromUser fromUser { get; set; }
|
||||||
|
public Author author { get; set; }
|
||||||
|
public bool? isFromQueue { get; set; }
|
||||||
|
public bool? canUnsendQueue { get; set; }
|
||||||
|
public int? unsendSecondsQueue { get; set; }
|
||||||
|
public long id { get; set; }
|
||||||
|
public bool isOpened { get; set; }
|
||||||
|
public bool? isNew { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public DateTime? postedAt { get; set; }
|
||||||
|
public DateTime? changedAt { get; set; }
|
||||||
|
public int? cancelSeconds { get; set; }
|
||||||
|
public bool? isLiked { get; set; }
|
||||||
|
public bool? canPurchase { get; set; }
|
||||||
|
public bool? canReport { get; set; }
|
||||||
|
public bool? isCanceled { get; set; }
|
||||||
|
public bool? isArchived { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Manifest
|
||||||
|
{
|
||||||
|
public string hls { get; set; }
|
||||||
|
public string dash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
16
OF DL/Entities/Purchased/PurchasedTabCollection.cs
Normal file
16
OF DL/Entities/Purchased/PurchasedTabCollection.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Purchased
|
||||||
|
{
|
||||||
|
public class PurchasedTabCollection
|
||||||
|
{
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public PaidPostCollection PaidPosts { get; set; } = new PaidPostCollection();
|
||||||
|
public PaidMessageCollection PaidMessages { get; set; } = new PaidMessageCollection();
|
||||||
|
}
|
||||||
|
}
|
18
OF DL/Entities/Purchased/SinglePaidMessageCollection.cs
Normal file
18
OF DL/Entities/Purchased/SinglePaidMessageCollection.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using OF_DL.Entities.Messages;
|
||||||
|
using OF_DL.Entities.Post;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static OF_DL.Entities.Messages.Messages;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Purchased
|
||||||
|
{
|
||||||
|
public class SinglePaidMessageCollection
|
||||||
|
{
|
||||||
|
public Dictionary<long, string> SingleMessages = new Dictionary<long, string>();
|
||||||
|
public List<SingleMessage> SingleMessageObjects = new List<SingleMessage>();
|
||||||
|
public List<Medium> SingleMessageMedia = new List<Medium>();
|
||||||
|
}
|
||||||
|
}
|
13
OF DL/Entities/ShortDateConverter.cs
Normal file
13
OF DL/Entities/ShortDateConverter.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public class ShortDateConverter : IsoDateTimeConverter
|
||||||
|
{
|
||||||
|
public ShortDateConverter()
|
||||||
|
{
|
||||||
|
DateTimeFormat = "yyyy-MM-dd";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
OF DL/Entities/Stories/Stories.cs
Normal file
96
OF DL/Entities/Stories/Stories.cs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Stories
|
||||||
|
{
|
||||||
|
public class Stories
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public int userId { get; set; }
|
||||||
|
public bool isWatched { get; set; }
|
||||||
|
public bool isReady { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public object question { get; set; }
|
||||||
|
public bool canLike { get; set; }
|
||||||
|
public bool isLiked { get; set; }
|
||||||
|
public class Files
|
||||||
|
{
|
||||||
|
public Full full { get; set; }
|
||||||
|
public Thumb thumb { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
public SquarePreview squarePreview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Full
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public List<object> sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Medium
|
||||||
|
{
|
||||||
|
public long id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public bool convertedToVideo { get; set; }
|
||||||
|
public bool canView { get; set; }
|
||||||
|
public bool hasError { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public Files files { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Preview
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public Sources sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Source
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int duration { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public Sources sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Sources
|
||||||
|
{
|
||||||
|
[JsonProperty("720")]
|
||||||
|
public object _720 { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("240")]
|
||||||
|
public object _240 { get; set; }
|
||||||
|
public string w150 { get; set; }
|
||||||
|
public string w480 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SquarePreview
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public Sources sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thumb
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
216
OF DL/Entities/Streams/Streams.cs
Normal file
216
OF DL/Entities/Streams/Streams.cs
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using OF_DL.Utils;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Streams
|
||||||
|
{
|
||||||
|
public class Streams
|
||||||
|
{
|
||||||
|
public List<List> list { get; set; }
|
||||||
|
public bool hasMore { get; set; }
|
||||||
|
public string headMarker { get; set; }
|
||||||
|
public string tailMarker { get; set; }
|
||||||
|
public Counters counters { get; set; }
|
||||||
|
public class Author
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public string _view { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Counters
|
||||||
|
{
|
||||||
|
public int audiosCount { get; set; }
|
||||||
|
public int photosCount { get; set; }
|
||||||
|
public int videosCount { get; set; }
|
||||||
|
public int mediasCount { get; set; }
|
||||||
|
public int postsCount { get; set; }
|
||||||
|
public int streamsCount { get; set; }
|
||||||
|
public int archivedPostsCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Files
|
||||||
|
{
|
||||||
|
public Full full { get; set; }
|
||||||
|
public Thumb thumb { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
public SquarePreview squarePreview { get; set; }
|
||||||
|
public Drm drm { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Full
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public List<object> sources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SquarePreview
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thumb
|
||||||
|
{
|
||||||
|
public string url { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Info
|
||||||
|
{
|
||||||
|
public Source source { get; set; }
|
||||||
|
public Preview preview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class List
|
||||||
|
{
|
||||||
|
public string responseType { get; set; }
|
||||||
|
public long id { get; set; }
|
||||||
|
public DateTime postedAt { get; set; }
|
||||||
|
public string postedAtPrecise { get; set; }
|
||||||
|
public object expiredAt { get; set; }
|
||||||
|
public Author author { get; set; }
|
||||||
|
public string text { get; set; }
|
||||||
|
private string _rawText;
|
||||||
|
public string rawText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_rawText))
|
||||||
|
{
|
||||||
|
_rawText = XmlUtils.EvaluateInnerText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _rawText;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_rawText = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool lockedText { get; set; }
|
||||||
|
public bool isFavorite { get; set; }
|
||||||
|
public bool canReport { get; set; }
|
||||||
|
public bool canDelete { get; set; }
|
||||||
|
public bool canComment { get; set; }
|
||||||
|
public bool canEdit { get; set; }
|
||||||
|
public bool isPinned { get; set; }
|
||||||
|
public int favoritesCount { get; set; }
|
||||||
|
public int mediaCount { get; set; }
|
||||||
|
public bool isMediaReady { get; set; }
|
||||||
|
public object voting { get; set; }
|
||||||
|
public bool isOpened { get; set; }
|
||||||
|
public bool canToggleFavorite { get; set; }
|
||||||
|
public int streamId { get; set; }
|
||||||
|
public string price { get; set; }
|
||||||
|
public bool hasVoting { get; set; }
|
||||||
|
public bool isAddedToBookmarks { get; set; }
|
||||||
|
public bool isArchived { get; set; }
|
||||||
|
public bool isPrivateArchived { get; set; }
|
||||||
|
public bool isDeleted { get; set; }
|
||||||
|
public bool hasUrl { get; set; }
|
||||||
|
public bool isCouplePeopleMedia { get; set; }
|
||||||
|
public string cantCommentReason { get; set; }
|
||||||
|
public int commentsCount { get; set; }
|
||||||
|
public List<object> mentionedUsers { get; set; }
|
||||||
|
public List<object> linkedUsers { get; set; }
|
||||||
|
public string tipsAmount { get; set; }
|
||||||
|
public string tipsAmountRaw { get; set; }
|
||||||
|
public List<Medium> media { get; set; }
|
||||||
|
public bool canViewMedia { get; set; }
|
||||||
|
public List<object> preview { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Medium
|
||||||
|
{
|
||||||
|
public long id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public bool convertedToVideo { get; set; }
|
||||||
|
public bool canView { get; set; }
|
||||||
|
public bool hasError { get; set; }
|
||||||
|
public DateTime? createdAt { get; set; }
|
||||||
|
public Info info { get; set; }
|
||||||
|
public Source source { get; set; }
|
||||||
|
public string squarePreview { get; set; }
|
||||||
|
public string full { get; set; }
|
||||||
|
public string preview { get; set; }
|
||||||
|
public string thumb { get; set; }
|
||||||
|
public bool hasCustomPreview { get; set; }
|
||||||
|
public Files files { get; set; }
|
||||||
|
public VideoSources videoSources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Preview
|
||||||
|
{
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public string url { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Source
|
||||||
|
{
|
||||||
|
public string source { get; set; }
|
||||||
|
public int width { get; set; }
|
||||||
|
public int height { get; set; }
|
||||||
|
public int size { get; set; }
|
||||||
|
public int duration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoSources
|
||||||
|
{
|
||||||
|
[JsonProperty("720")]
|
||||||
|
public object _720 { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("240")]
|
||||||
|
public object _240 { get; set; }
|
||||||
|
}
|
||||||
|
public class Drm
|
||||||
|
{
|
||||||
|
public Manifest manifest { get; set; }
|
||||||
|
public Signature signature { get; set; }
|
||||||
|
}
|
||||||
|
public class Manifest
|
||||||
|
{
|
||||||
|
public string? hls { get; set; }
|
||||||
|
public string? dash { get; set; }
|
||||||
|
}
|
||||||
|
public class Signature
|
||||||
|
{
|
||||||
|
public Hls hls { get; set; }
|
||||||
|
public Dash dash { get; set; }
|
||||||
|
}
|
||||||
|
public class Hls
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
public class Dash
|
||||||
|
{
|
||||||
|
[JsonProperty("CloudFront-Policy")]
|
||||||
|
public string CloudFrontPolicy { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Signature")]
|
||||||
|
public string CloudFrontSignature { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("CloudFront-Key-Pair-Id")]
|
||||||
|
public string CloudFrontKeyPairId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
OF DL/Entities/Streams/StreamsCollection.cs
Normal file
15
OF DL/Entities/Streams/StreamsCollection.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities.Streams
|
||||||
|
{
|
||||||
|
public class StreamsCollection
|
||||||
|
{
|
||||||
|
public Dictionary<long, string> Streams = new Dictionary<long, string>();
|
||||||
|
public List<Streams.List> StreamObjects = new List<Streams.List>();
|
||||||
|
public List<Streams.Medium> StreamMedia = new List<Streams.Medium>();
|
||||||
|
}
|
||||||
|
}
|
165
OF DL/Entities/Subscriptions.cs
Normal file
165
OF DL/Entities/Subscriptions.cs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public class Subscriptions
|
||||||
|
{
|
||||||
|
public List<List> list { get; set; }
|
||||||
|
public bool hasMore { get; set; }
|
||||||
|
public class AvatarThumbs
|
||||||
|
{
|
||||||
|
public string c50 { get; set; }
|
||||||
|
public string c144 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeaderSize
|
||||||
|
{
|
||||||
|
public int? width { get; set; }
|
||||||
|
public int? height { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeaderThumbs
|
||||||
|
{
|
||||||
|
public string w480 { get; set; }
|
||||||
|
public string w760 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class List
|
||||||
|
{
|
||||||
|
public string view { get; set; }
|
||||||
|
public string avatar { get; set; }
|
||||||
|
public AvatarThumbs avatarThumbs { get; set; }
|
||||||
|
public string header { get; set; }
|
||||||
|
public HeaderSize headerSize { get; set; }
|
||||||
|
public HeaderThumbs headerThumbs { get; set; }
|
||||||
|
public int id { get; set; }
|
||||||
|
public string name { get; set; }
|
||||||
|
public string username { get; set; }
|
||||||
|
public bool? canLookStory { get; set; }
|
||||||
|
public bool? canCommentStory { get; set; }
|
||||||
|
public bool? hasNotViewedStory { get; set; }
|
||||||
|
public bool? isVerified { get; set; }
|
||||||
|
public bool? canPayInternal { get; set; }
|
||||||
|
public bool? hasScheduledStream { get; set; }
|
||||||
|
public bool? hasStream { get; set; }
|
||||||
|
public bool? hasStories { get; set; }
|
||||||
|
public bool? tipsEnabled { get; set; }
|
||||||
|
public bool? tipsTextEnabled { get; set; }
|
||||||
|
public int? tipsMin { get; set; }
|
||||||
|
public int? tipsMinInternal { get; set; }
|
||||||
|
public int? tipsMax { get; set; }
|
||||||
|
public bool? canEarn { get; set; }
|
||||||
|
public bool? canAddSubscriber { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public bool? isPaywallRequired { get; set; }
|
||||||
|
public bool? unprofitable { get; set; }
|
||||||
|
public List<ListsState> listsStates { get; set; }
|
||||||
|
public bool? isMuted { get; set; }
|
||||||
|
public bool? isRestricted { get; set; }
|
||||||
|
public bool? canRestrict { get; set; }
|
||||||
|
public bool? subscribedBy { get; set; }
|
||||||
|
public bool? subscribedByExpire { get; set; }
|
||||||
|
public DateTime? subscribedByExpireDate { get; set; }
|
||||||
|
public bool? subscribedByAutoprolong { get; set; }
|
||||||
|
public bool? subscribedIsExpiredNow { get; set; }
|
||||||
|
public string? currentSubscribePrice { get; set; }
|
||||||
|
public bool? subscribedOn { get; set; }
|
||||||
|
public bool? subscribedOnExpiredNow { get; set; }
|
||||||
|
public string subscribedOnDuration { get; set; }
|
||||||
|
public bool? canReport { get; set; }
|
||||||
|
public bool? canReceiveChatMessage { get; set; }
|
||||||
|
public bool? hideChat { get; set; }
|
||||||
|
public DateTime? lastSeen { get; set; }
|
||||||
|
public bool? isPerformer { get; set; }
|
||||||
|
public bool? isRealPerformer { get; set; }
|
||||||
|
public SubscribedByData subscribedByData { get; set; }
|
||||||
|
public SubscribedOnData subscribedOnData { get; set; }
|
||||||
|
public bool? canTrialSend { get; set; }
|
||||||
|
public bool? isBlocked { get; set; }
|
||||||
|
public string displayName { get; set; }
|
||||||
|
public string notice { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ListsState
|
||||||
|
{
|
||||||
|
public object id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public string name { get; set; }
|
||||||
|
public bool? hasUser { get; set; }
|
||||||
|
public bool? canAddUser { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Subscribe
|
||||||
|
{
|
||||||
|
public object id { get; set; }
|
||||||
|
public int? userId { get; set; }
|
||||||
|
public int? subscriberId { get; set; }
|
||||||
|
public DateTime? date { get; set; }
|
||||||
|
public int? duration { get; set; }
|
||||||
|
public DateTime? startDate { get; set; }
|
||||||
|
public DateTime? expireDate { get; set; }
|
||||||
|
public object cancelDate { get; set; }
|
||||||
|
public string? price { get; set; }
|
||||||
|
public string? regularPrice { get; set; }
|
||||||
|
public string? discount { get; set; }
|
||||||
|
public string action { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public object offerStart { get; set; }
|
||||||
|
public object offerEnd { get; set; }
|
||||||
|
public bool? isCurrent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscribedByData
|
||||||
|
{
|
||||||
|
public string? price { get; set; }
|
||||||
|
public string? newPrice { get; set; }
|
||||||
|
public string? regularPrice { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public int? discountPercent { get; set; }
|
||||||
|
public int? discountPeriod { get; set; }
|
||||||
|
public DateTime? subscribeAt { get; set; }
|
||||||
|
public DateTime? expiredAt { get; set; }
|
||||||
|
public DateTime? renewedAt { get; set; }
|
||||||
|
public object discountFinishedAt { get; set; }
|
||||||
|
public object discountStartedAt { get; set; }
|
||||||
|
public string status { get; set; }
|
||||||
|
public bool? isMuted { get; set; }
|
||||||
|
public string unsubscribeReason { get; set; }
|
||||||
|
public string duration { get; set; }
|
||||||
|
public bool? showPostsInFeed { get; set; }
|
||||||
|
public List<Subscribe> subscribes { get; set; }
|
||||||
|
public bool? hasActivePaidSubscriptions { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscribedOnData
|
||||||
|
{
|
||||||
|
public string? price { get; set; }
|
||||||
|
public string? newPrice { get; set; }
|
||||||
|
public string? regularPrice { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public int? discountPercent { get; set; }
|
||||||
|
public int? discountPeriod { get; set; }
|
||||||
|
public DateTime? subscribeAt { get; set; }
|
||||||
|
public DateTime? expiredAt { get; set; }
|
||||||
|
public DateTime? renewedAt { get; set; }
|
||||||
|
public object discountFinishedAt { get; set; }
|
||||||
|
public object discountStartedAt { get; set; }
|
||||||
|
public object status { get; set; }
|
||||||
|
public bool? isMuted { get; set; }
|
||||||
|
public string unsubscribeReason { get; set; }
|
||||||
|
public string duration { get; set; }
|
||||||
|
public string? tipsSumm { get; set; }
|
||||||
|
public string? subscribesSumm { get; set; }
|
||||||
|
public string? messagesSumm { get; set; }
|
||||||
|
public string? postsSumm { get; set; }
|
||||||
|
public string? streamsSumm { get; set; }
|
||||||
|
public string? totalSumm { get; set; }
|
||||||
|
public List<Subscribe> subscribes { get; set; }
|
||||||
|
public bool? hasActivePaidSubscriptions { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
OF DL/Entities/ToggleableConfigAttribute.cs
Normal file
8
OF DL/Entities/ToggleableConfigAttribute.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
internal class ToggleableConfigAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
191
OF DL/Entities/User.cs
Normal file
191
OF DL/Entities/User.cs
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Entities
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public string view { get; set; }
|
||||||
|
public string? avatar { get; set; }
|
||||||
|
public AvatarThumbs avatarThumbs { get; set; }
|
||||||
|
public string? header { get; set; }
|
||||||
|
public HeaderSize headerSize { get; set; }
|
||||||
|
public HeaderThumbs headerThumbs { get; set; }
|
||||||
|
public int? id { get; set; }
|
||||||
|
public string name { get; set; }
|
||||||
|
public string username { get; set; }
|
||||||
|
public bool? canLookStory { get; set; }
|
||||||
|
public bool? canCommentStory { get; set; }
|
||||||
|
public bool? hasNotViewedStory { get; set; }
|
||||||
|
public bool? isVerified { get; set; }
|
||||||
|
public bool? canPayInternal { get; set; }
|
||||||
|
public bool? hasScheduledStream { get; set; }
|
||||||
|
public bool? hasStream { get; set; }
|
||||||
|
public bool? hasStories { get; set; }
|
||||||
|
public bool? tipsEnabled { get; set; }
|
||||||
|
public bool? tipsTextEnabled { get; set; }
|
||||||
|
public int? tipsMin { get; set; }
|
||||||
|
public int? tipsMinInternal { get; set; }
|
||||||
|
public int? tipsMax { get; set; }
|
||||||
|
public bool? canEarn { get; set; }
|
||||||
|
public bool? canAddSubscriber { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public string displayName { get; set; }
|
||||||
|
public string notice { get; set; }
|
||||||
|
public bool? isPaywallRequired { get; set; }
|
||||||
|
public bool? unprofitable { get; set; }
|
||||||
|
public List<ListsState> listsStates { get; set; }
|
||||||
|
public bool? isMuted { get; set; }
|
||||||
|
public bool? isRestricted { get; set; }
|
||||||
|
public bool? canRestrict { get; set; }
|
||||||
|
public bool? subscribedBy { get; set; }
|
||||||
|
public bool? subscribedByExpire { get; set; }
|
||||||
|
public DateTime? subscribedByExpireDate { get; set; }
|
||||||
|
public bool? subscribedByAutoprolong { get; set; }
|
||||||
|
public bool? subscribedIsExpiredNow { get; set; }
|
||||||
|
public string? currentSubscribePrice { get; set; }
|
||||||
|
public bool? subscribedOn { get; set; }
|
||||||
|
public bool? subscribedOnExpiredNow { get; set; }
|
||||||
|
public string subscribedOnDuration { get; set; }
|
||||||
|
public DateTime? joinDate { get; set; }
|
||||||
|
public bool? isReferrerAllowed { get; set; }
|
||||||
|
public string about { get; set; }
|
||||||
|
public string rawAbout { get; set; }
|
||||||
|
public object website { get; set; }
|
||||||
|
public object wishlist { get; set; }
|
||||||
|
public object location { get; set; }
|
||||||
|
public int? postsCount { get; set; }
|
||||||
|
public int? archivedPostsCount { get; set; }
|
||||||
|
public int? privateArchivedPostsCount { get; set; }
|
||||||
|
public int? photosCount { get; set; }
|
||||||
|
public int? videosCount { get; set; }
|
||||||
|
public int? audiosCount { get; set; }
|
||||||
|
public int? mediasCount { get; set; }
|
||||||
|
public DateTime? lastSeen { get; set; }
|
||||||
|
public int? favoritesCount { get; set; }
|
||||||
|
public int? favoritedCount { get; set; }
|
||||||
|
public bool? showPostsInFeed { get; set; }
|
||||||
|
public bool? canReceiveChatMessage { get; set; }
|
||||||
|
public bool? isPerformer { get; set; }
|
||||||
|
public bool? isRealPerformer { get; set; }
|
||||||
|
public bool? isSpotifyConnected { get; set; }
|
||||||
|
public int? subscribersCount { get; set; }
|
||||||
|
public bool? hasPinnedPosts { get; set; }
|
||||||
|
public bool? hasLabels { get; set; }
|
||||||
|
public bool? canChat { get; set; }
|
||||||
|
public string? callPrice { get; set; }
|
||||||
|
public bool? isPrivateRestriction { get; set; }
|
||||||
|
public bool? showSubscribersCount { get; set; }
|
||||||
|
public bool? showMediaCount { get; set; }
|
||||||
|
public SubscribedByData subscribedByData { get; set; }
|
||||||
|
public SubscribedOnData subscribedOnData { get; set; }
|
||||||
|
public bool? canPromotion { get; set; }
|
||||||
|
public bool? canCreatePromotion { get; set; }
|
||||||
|
public bool? canCreateTrial { get; set; }
|
||||||
|
public bool? isAdultContent { get; set; }
|
||||||
|
public bool? canTrialSend { get; set; }
|
||||||
|
public bool? hadEnoughLastPhotos { get; set; }
|
||||||
|
public bool? hasLinks { get; set; }
|
||||||
|
public DateTime? firstPublishedPostDate { get; set; }
|
||||||
|
public bool? isSpringConnected { get; set; }
|
||||||
|
public bool? isFriend { get; set; }
|
||||||
|
public bool? isBlocked { get; set; }
|
||||||
|
public bool? canReport { get; set; }
|
||||||
|
public class AvatarThumbs
|
||||||
|
{
|
||||||
|
public string c50 { get; set; }
|
||||||
|
public string c144 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeaderSize
|
||||||
|
{
|
||||||
|
public int? width { get; set; }
|
||||||
|
public int? height { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeaderThumbs
|
||||||
|
{
|
||||||
|
public string w480 { get; set; }
|
||||||
|
public string w760 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ListsState
|
||||||
|
{
|
||||||
|
public string id { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public string name { get; set; }
|
||||||
|
public bool hasUser { get; set; }
|
||||||
|
public bool canAddUser { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Subscribe
|
||||||
|
{
|
||||||
|
public long? id { get; set; }
|
||||||
|
public int? userId { get; set; }
|
||||||
|
public int? subscriberId { get; set; }
|
||||||
|
public DateTime? date { get; set; }
|
||||||
|
public int? duration { get; set; }
|
||||||
|
public DateTime? startDate { get; set; }
|
||||||
|
public DateTime? expireDate { get; set; }
|
||||||
|
public object cancelDate { get; set; }
|
||||||
|
public string? price { get; set; }
|
||||||
|
public string? regularPrice { get; set; }
|
||||||
|
public int? discount { get; set; }
|
||||||
|
public string action { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public object offerStart { get; set; }
|
||||||
|
public object offerEnd { get; set; }
|
||||||
|
public bool? isCurrent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscribedByData
|
||||||
|
{
|
||||||
|
public string? price { get; set; }
|
||||||
|
public string? newPrice { get; set; }
|
||||||
|
public string? regularPrice { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public int? discountPercent { get; set; }
|
||||||
|
public int? discountPeriod { get; set; }
|
||||||
|
public DateTime? subscribeAt { get; set; }
|
||||||
|
public DateTime? expiredAt { get; set; }
|
||||||
|
public object? renewedAt { get; set; }
|
||||||
|
public object? discountFinishedAt { get; set; }
|
||||||
|
public object? discountStartedAt { get; set; }
|
||||||
|
public string? status { get; set; }
|
||||||
|
public bool? isMuted { get; set; }
|
||||||
|
public string? unsubscribeReason { get; set; }
|
||||||
|
public string? duration { get; set; }
|
||||||
|
public bool? showPostsInFeed { get; set; }
|
||||||
|
public List<Subscribe>? subscribes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscribedOnData
|
||||||
|
{
|
||||||
|
public string? price { get; set; }
|
||||||
|
public string? newPrice { get; set; }
|
||||||
|
public string? regularPrice { get; set; }
|
||||||
|
public string? subscribePrice { get; set; }
|
||||||
|
public int? discountPercent { get; set; }
|
||||||
|
public int? discountPeriod { get; set; }
|
||||||
|
public DateTime? subscribeAt { get; set; }
|
||||||
|
public DateTime? expiredAt { get; set; }
|
||||||
|
public DateTime? renewedAt { get; set; }
|
||||||
|
public object? discountFinishedAt { get; set; }
|
||||||
|
public object? discountStartedAt { get; set; }
|
||||||
|
public object? status { get; set; }
|
||||||
|
public bool? isMuted { get; set; }
|
||||||
|
public string? unsubscribeReason { get; set; }
|
||||||
|
public string? duration { get; set; }
|
||||||
|
public string? tipsSumm { get; set; }
|
||||||
|
public string? subscribesSumm { get; set; }
|
||||||
|
public string? messagesSumm { get; set; }
|
||||||
|
public string? postsSumm { get; set; }
|
||||||
|
public string? streamsSumm { get; set; }
|
||||||
|
public string? totalSumm { get; set; }
|
||||||
|
public List<Subscribe>? subscribes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
OF DL/Enumerations/CustomFileNameOption.cs
Normal file
13
OF DL/Enumerations/CustomFileNameOption.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Enumerations;
|
||||||
|
|
||||||
|
public enum CustomFileNameOption
|
||||||
|
{
|
||||||
|
ReturnOriginal,
|
||||||
|
ReturnEmpty,
|
||||||
|
}
|
14
OF DL/Enumerations/DownloadDateSelection.cs
Normal file
14
OF DL/Enumerations/DownloadDateSelection.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Enumerations
|
||||||
|
{
|
||||||
|
public enum DownloadDateSelection
|
||||||
|
{
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
}
|
||||||
|
}
|
36
OF DL/Enumerations/LoggingLevel.cs
Normal file
36
OF DL/Enumerations/LoggingLevel.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Enumerations
|
||||||
|
{
|
||||||
|
public enum LoggingLevel
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// Anything and everything you might want to know about a running block of code.
|
||||||
|
Verbose,
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// Internal system events that aren't necessarily observable from the outside.
|
||||||
|
Debug,
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// The lifeblood of operational intelligence - things happen.
|
||||||
|
Information,
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// Service is degraded or endangered.
|
||||||
|
Warning,
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// Functionality is unavailable, invariants are broken or data is lost.
|
||||||
|
Error,
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// If you have a pager, it goes off when one of these occurs.
|
||||||
|
Fatal
|
||||||
|
}
|
||||||
|
}
|
19
OF DL/Enumerations/MediaType.cs
Normal file
19
OF DL/Enumerations/MediaType.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Enumurations
|
||||||
|
{
|
||||||
|
public enum MediaType
|
||||||
|
{
|
||||||
|
PaidPosts = 10,
|
||||||
|
Posts = 20,
|
||||||
|
Archived = 30,
|
||||||
|
Stories = 40,
|
||||||
|
Highlights = 50,
|
||||||
|
Messages = 60,
|
||||||
|
PaidMessages = 70
|
||||||
|
}
|
||||||
|
}
|
2839
OF DL/Helpers/APIHelper.cs
Normal file
2839
OF DL/Helpers/APIHelper.cs
Normal file
File diff suppressed because it is too large
Load Diff
166
OF DL/Helpers/AuthHelper.cs
Normal file
166
OF DL/Helpers/AuthHelper.cs
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
using OF_DL.Entities;
|
||||||
|
using PuppeteerSharp;
|
||||||
|
using PuppeteerSharp.BrowserData;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace OF_DL.Helpers;
|
||||||
|
|
||||||
|
public class AuthHelper
|
||||||
|
{
|
||||||
|
private readonly LaunchOptions _options = new()
|
||||||
|
{
|
||||||
|
Headless = false,
|
||||||
|
Channel = ChromeReleaseChannel.Stable,
|
||||||
|
DefaultViewport = null,
|
||||||
|
Args = ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||||
|
UserDataDir = Path.GetFullPath("chrome-data")
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly string[] _desiredCookies =
|
||||||
|
[
|
||||||
|
"auth_id",
|
||||||
|
"sess"
|
||||||
|
];
|
||||||
|
|
||||||
|
private const int LoginTimeout = 600000; // 10 minutes
|
||||||
|
private const int FeedLoadTimeout = 60000; // 1 minute
|
||||||
|
|
||||||
|
public async Task SetupBrowser(bool runningInDocker)
|
||||||
|
{
|
||||||
|
string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH");
|
||||||
|
if (executablePath != null)
|
||||||
|
{
|
||||||
|
Log.Information("OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", executablePath);
|
||||||
|
_options.ExecutablePath = executablePath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var browserFetcher = new BrowserFetcher();
|
||||||
|
var installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList();
|
||||||
|
if (installedBrowsers.Count == 0)
|
||||||
|
{
|
||||||
|
Log.Information("Downloading browser.");
|
||||||
|
var downloadedBrowser = await browserFetcher.DownloadAsync();
|
||||||
|
Log.Information("Browser downloaded. Path: {executablePath}",
|
||||||
|
downloadedBrowser.GetExecutablePath());
|
||||||
|
_options.ExecutablePath = downloadedBrowser.GetExecutablePath();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_options.ExecutablePath = installedBrowsers.First().GetExecutablePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runningInDocker)
|
||||||
|
{
|
||||||
|
Log.Information("Running in Docker. Disabling sandbox and GPU.");
|
||||||
|
_options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetBcToken(IPage page)
|
||||||
|
{
|
||||||
|
return await page.EvaluateExpressionAsync<string>("window.localStorage.getItem('bcTokenSha') || ''");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Auth?> GetAuthFromBrowser(bool isDocker = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IBrowser? browser;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
browser = await Puppeteer.LaunchAsync(_options);
|
||||||
|
}
|
||||||
|
catch (ProcessException e)
|
||||||
|
{
|
||||||
|
if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir))
|
||||||
|
{
|
||||||
|
Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again.");
|
||||||
|
Directory.Delete(_options.UserDataDir, true);
|
||||||
|
browser = await Puppeteer.LaunchAsync(_options);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not get browser");
|
||||||
|
}
|
||||||
|
|
||||||
|
IPage[]? pages = await browser.PagesAsync();
|
||||||
|
IPage? page = pages.First();
|
||||||
|
|
||||||
|
if (page == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not get page");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Debug("Navigating to OnlyFans.");
|
||||||
|
await page.GoToAsync("https://onlyfans.com");
|
||||||
|
|
||||||
|
Log.Debug("Waiting for user to login");
|
||||||
|
await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout });
|
||||||
|
Log.Debug("Feed element detected (user logged in)");
|
||||||
|
|
||||||
|
await page.ReloadAsync();
|
||||||
|
|
||||||
|
await page.WaitForNavigationAsync(new NavigationOptions {
|
||||||
|
WaitUntil = [WaitUntilNavigation.Networkidle2],
|
||||||
|
Timeout = FeedLoadTimeout
|
||||||
|
});
|
||||||
|
Log.Debug("DOM loaded. Getting BC token and cookies ...");
|
||||||
|
|
||||||
|
string xBc;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
xBc = await GetBcToken(page);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new Exception("Error getting bcToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, string> mappedCookies = (await page.GetCookiesAsync())
|
||||||
|
.Where(cookie => cookie.Domain.Contains("onlyfans.com"))
|
||||||
|
.ToDictionary(cookie => cookie.Name, cookie => cookie.Value);
|
||||||
|
|
||||||
|
mappedCookies.TryGetValue("auth_id", out string? userId);
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not find 'auth_id' cookie");
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedCookies.TryGetValue("sess", out string? sess);
|
||||||
|
if (sess == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not find 'sess' cookie");
|
||||||
|
}
|
||||||
|
|
||||||
|
string? userAgent = await browser.GetUserAgentAsync();
|
||||||
|
if (userAgent == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not get user agent");
|
||||||
|
}
|
||||||
|
|
||||||
|
string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key))
|
||||||
|
.Select(key => $"${key}={mappedCookies[key]};"));
|
||||||
|
|
||||||
|
return new Auth()
|
||||||
|
{
|
||||||
|
COOKIE = cookies,
|
||||||
|
USER_AGENT = userAgent,
|
||||||
|
USER_ID = userId,
|
||||||
|
X_BC = xBc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e, "Error getting auth from browser");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
OF DL/Helpers/Constants.cs
Normal file
6
OF DL/Helpers/Constants.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace OF_DL.Helpers;
|
||||||
|
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const string API_URL = "https://onlyfans.com/api2/v2";
|
||||||
|
}
|
531
OF DL/Helpers/DBHelper.cs
Normal file
531
OF DL/Helpers/DBHelper.cs
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OF_DL.Enumurations;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Serilog;
|
||||||
|
using OF_DL.Entities;
|
||||||
|
|
||||||
|
namespace OF_DL.Helpers
|
||||||
|
{
|
||||||
|
public class DBHelper : IDBHelper
|
||||||
|
{
|
||||||
|
private readonly IDownloadConfig downloadConfig;
|
||||||
|
|
||||||
|
public DBHelper(IDownloadConfig downloadConfig)
|
||||||
|
{
|
||||||
|
this.downloadConfig = downloadConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateDB(string folder)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(folder + "/Metadata"))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(folder + "/Metadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
string dbFilePath = $"{folder}/Metadata/user_data.db";
|
||||||
|
|
||||||
|
// connect to the new database file
|
||||||
|
using SqliteConnection connection = new($"Data Source={dbFilePath}");
|
||||||
|
// open the connection
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
// create the 'medias' table
|
||||||
|
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS medias (id INTEGER NOT NULL, media_id INTEGER, post_id INTEGER NOT NULL, link VARCHAR, directory VARCHAR, filename VARCHAR, size INTEGER, api_type VARCHAR, media_type VARCHAR, preview INTEGER, linked VARCHAR, downloaded INTEGER, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(media_id));", connection))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnsureCreatedAtColumnExists(connection, "medias");
|
||||||
|
|
||||||
|
//
|
||||||
|
// Alter existing databases to create unique constraint on `medias`
|
||||||
|
//
|
||||||
|
using (SqliteCommand cmd = new(@"
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE medias RENAME TO old_medias;
|
||||||
|
|
||||||
|
CREATE TABLE medias (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
media_id INTEGER,
|
||||||
|
post_id INTEGER NOT NULL,
|
||||||
|
link VARCHAR,
|
||||||
|
directory VARCHAR,
|
||||||
|
filename VARCHAR,
|
||||||
|
size INTEGER,
|
||||||
|
api_type VARCHAR,
|
||||||
|
media_type VARCHAR,
|
||||||
|
preview INTEGER,
|
||||||
|
linked VARCHAR,
|
||||||
|
downloaded INTEGER,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
record_created_at TIMESTAMP,
|
||||||
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE(media_id, api_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO medias SELECT * FROM old_medias;
|
||||||
|
|
||||||
|
DROP TABLE old_medias;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys=on;", connection))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the 'messages' table
|
||||||
|
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS messages (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, user_id INTEGER, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the 'posts' table
|
||||||
|
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS posts (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the 'stories' table
|
||||||
|
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS stories (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the 'others' table
|
||||||
|
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS others (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the 'products' table
|
||||||
|
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS products (id INTEGER NOT NULL, post_id INTEGER NOT NULL, text VARCHAR, price INTEGER, paid INTEGER, archived BOOLEAN, created_at TIMESTAMP, title VARCHAR, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(post_id));", connection))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the 'profiles' table
|
||||||
|
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS profiles (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(username));", connection))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.Close();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nInner Exception:");
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateUsersDB(Dictionary<string, int> users)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db");
|
||||||
|
Log.Debug("Database data source: " + connection.DataSource);
|
||||||
|
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS users (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, PRIMARY KEY(id), UNIQUE(username));", connection))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Debug("Adding missing creators");
|
||||||
|
foreach (KeyValuePair<string, int> user in users)
|
||||||
|
{
|
||||||
|
using (SqliteCommand checkCmd = new($"SELECT user_id, username FROM users WHERE user_id = @userId;", connection))
|
||||||
|
{
|
||||||
|
checkCmd.Parameters.AddWithValue("@userId", user.Value);
|
||||||
|
using (var reader = await checkCmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
if (!reader.Read())
|
||||||
|
{
|
||||||
|
using (SqliteCommand insertCmd = new($"INSERT INTO users (user_id, username) VALUES (@userId, @username);", connection))
|
||||||
|
{
|
||||||
|
insertCmd.Parameters.AddWithValue("@userId", user.Value);
|
||||||
|
insertCmd.Parameters.AddWithValue("@username", user.Key);
|
||||||
|
await insertCmd.ExecuteNonQueryAsync();
|
||||||
|
Log.Debug("Inserted new creator: " + user.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Debug("Creator " + user.Key + " already exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.Close();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nInner Exception:");
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CheckUsername(KeyValuePair<string, int> user, string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db");
|
||||||
|
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using (SqliteCommand checkCmd = new($"SELECT user_id, username FROM users WHERE user_id = @userId;", connection))
|
||||||
|
{
|
||||||
|
checkCmd.Parameters.AddWithValue("@userId", user.Value);
|
||||||
|
using (var reader = await checkCmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
long storedUserId = reader.GetInt64(0);
|
||||||
|
string storedUsername = reader.GetString(1);
|
||||||
|
|
||||||
|
if (storedUsername != user.Key)
|
||||||
|
{
|
||||||
|
using (SqliteCommand updateCmd = new($"UPDATE users SET username = @newUsername WHERE user_id = @userId;", connection))
|
||||||
|
{
|
||||||
|
updateCmd.Parameters.AddWithValue("@newUsername", user.Key);
|
||||||
|
updateCmd.Parameters.AddWithValue("@userId", user.Value);
|
||||||
|
await updateCmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
string oldPath = path.Replace(path.Split("/")[^1], storedUsername);
|
||||||
|
|
||||||
|
if (Directory.Exists(oldPath))
|
||||||
|
{
|
||||||
|
Directory.Move(path.Replace(path.Split("/")[^1], storedUsername), path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.Close();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nInner Exception:");
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at, int user_id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
|
||||||
|
connection.Open();
|
||||||
|
await EnsureCreatedAtColumnExists(connection, "messages");
|
||||||
|
using SqliteCommand cmd = new($"SELECT COUNT(*) FROM messages WHERE post_id=@post_id", connection);
|
||||||
|
cmd.Parameters.AddWithValue("@post_id", post_id);
|
||||||
|
int count = Convert.ToInt32(await cmd.ExecuteScalarAsync());
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
// If the record doesn't exist, insert a new one
|
||||||
|
using SqliteCommand insertCmd = new("INSERT INTO messages(post_id, text, price, paid, archived, created_at, user_id, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @user_id, @record_created_at)", connection);
|
||||||
|
insertCmd.Parameters.AddWithValue("@post_id", post_id);
|
||||||
|
insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value);
|
||||||
|
insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value);
|
||||||
|
insertCmd.Parameters.AddWithValue("@is_paid", is_paid);
|
||||||
|
insertCmd.Parameters.AddWithValue("@is_archived", is_archived);
|
||||||
|
insertCmd.Parameters.AddWithValue("@created_at", created_at);
|
||||||
|
insertCmd.Parameters.AddWithValue("@user_id", user_id);
|
||||||
|
insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
await insertCmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nInner Exception:");
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
|
||||||
|
connection.Open();
|
||||||
|
await EnsureCreatedAtColumnExists(connection, "posts");
|
||||||
|
using SqliteCommand cmd = new($"SELECT COUNT(*) FROM posts WHERE post_id=@post_id", connection);
|
||||||
|
cmd.Parameters.AddWithValue("@post_id", post_id);
|
||||||
|
int count = Convert.ToInt32(await cmd.ExecuteScalarAsync());
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
// If the record doesn't exist, insert a new one
|
||||||
|
using SqliteCommand insertCmd = new("INSERT INTO posts(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection);
|
||||||
|
insertCmd.Parameters.AddWithValue("@post_id", post_id);
|
||||||
|
insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value);
|
||||||
|
insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value);
|
||||||
|
insertCmd.Parameters.AddWithValue("@is_paid", is_paid);
|
||||||
|
insertCmd.Parameters.AddWithValue("@is_archived", is_archived);
|
||||||
|
insertCmd.Parameters.AddWithValue("@created_at", created_at);
|
||||||
|
insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
await insertCmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nInner Exception:");
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
|
||||||
|
connection.Open();
|
||||||
|
await EnsureCreatedAtColumnExists(connection, "stories");
|
||||||
|
using SqliteCommand cmd = new($"SELECT COUNT(*) FROM stories WHERE post_id=@post_id", connection);
|
||||||
|
cmd.Parameters.AddWithValue("@post_id", post_id);
|
||||||
|
int count = Convert.ToInt32(await cmd.ExecuteScalarAsync());
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
// If the record doesn't exist, insert a new one
|
||||||
|
using SqliteCommand insertCmd = new("INSERT INTO stories(post_id, text, price, paid, archived, created_at, record_created_at) VALUES(@post_id, @message_text, @price, @is_paid, @is_archived, @created_at, @record_created_at)", connection);
|
||||||
|
insertCmd.Parameters.AddWithValue("@post_id", post_id);
|
||||||
|
insertCmd.Parameters.AddWithValue("@message_text", message_text ?? (object)DBNull.Value);
|
||||||
|
insertCmd.Parameters.AddWithValue("@price", price ?? (object)DBNull.Value);
|
||||||
|
insertCmd.Parameters.AddWithValue("@is_paid", is_paid);
|
||||||
|
insertCmd.Parameters.AddWithValue("@is_archived", is_archived);
|
||||||
|
insertCmd.Parameters.AddWithValue("@created_at", created_at);
|
||||||
|
insertCmd.Parameters.AddWithValue("@record_created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
await insertCmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nInner Exception:");
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, string? filename, long? size, string api_type, string media_type, bool preview, bool downloaded, DateTime? created_at)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
|
||||||
|
connection.Open();
|
||||||
|
await EnsureCreatedAtColumnExists(connection, "medias");
|
||||||
|
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM medias WHERE media_id=@media_id");
|
||||||
|
if (downloadConfig.DownloadDuplicatedMedia)
|
||||||
|
{
|
||||||
|
sql.Append(" and api_type=@api_type");
|
||||||
|
}
|
||||||
|
|
||||||
|
using SqliteCommand cmd = new(sql.ToString(), connection);
|
||||||
|
cmd.Parameters.AddWithValue("@media_id", media_id);
|
||||||
|
cmd.Parameters.AddWithValue("@api_type", api_type);
|
||||||
|
int count = Convert.ToInt32(cmd.ExecuteScalar());
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
// If the record doesn't exist, insert a new one
|
||||||
|
using SqliteCommand insertCmd = new($"INSERT INTO medias(media_id, post_id, link, directory, filename, size, api_type, media_type, preview, downloaded, created_at, record_created_at) VALUES({media_id}, {post_id}, '{link}', '{directory?.ToString() ?? "NULL"}', '{filename?.ToString() ?? "NULL"}', {size?.ToString() ?? "NULL"}, '{api_type}', '{media_type}', {Convert.ToInt32(preview)}, {Convert.ToInt32(downloaded)}, '{created_at?.ToString("yyyy-MM-dd HH:mm:ss")}', '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}')", connection);
|
||||||
|
await insertCmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nInner Exception:");
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<bool> CheckDownloaded(string folder, long media_id, string api_type)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool downloaded = false;
|
||||||
|
|
||||||
|
using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"))
|
||||||
|
{
|
||||||
|
StringBuilder sql = new StringBuilder("SELECT downloaded FROM medias WHERE media_id=@media_id");
|
||||||
|
if(downloadConfig.DownloadDuplicatedMedia)
|
||||||
|
{
|
||||||
|
sql.Append(" and api_type=@api_type");
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.Open();
|
||||||
|
using SqliteCommand cmd = new (sql.ToString(), connection);
|
||||||
|
cmd.Parameters.AddWithValue("@media_id", media_id);
|
||||||
|
cmd.Parameters.AddWithValue("@api_type", api_type);
|
||||||
|
downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync());
|
||||||
|
}
|
||||||
|
return downloaded;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nInner Exception:");
|
||||||
|
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, bool downloaded, DateTime created_at)
|
||||||
|
{
|
||||||
|
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
// Construct the update command
|
||||||
|
StringBuilder sql = new StringBuilder("UPDATE medias SET directory=@directory, filename=@filename, size=@size, downloaded=@downloaded, created_at=@created_at WHERE media_id=@media_id");
|
||||||
|
if (downloadConfig.DownloadDuplicatedMedia)
|
||||||
|
{
|
||||||
|
sql.Append(" and api_type=@api_type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new command object
|
||||||
|
using SqliteCommand command = new(sql.ToString(), connection);
|
||||||
|
// Add parameters to the command object
|
||||||
|
command.Parameters.AddWithValue("@directory", directory);
|
||||||
|
command.Parameters.AddWithValue("@filename", filename);
|
||||||
|
command.Parameters.AddWithValue("@size", size);
|
||||||
|
command.Parameters.AddWithValue("@downloaded", downloaded ? 1 : 0);
|
||||||
|
command.Parameters.AddWithValue("@created_at", created_at);
|
||||||
|
command.Parameters.AddWithValue("@media_id", media_id);
|
||||||
|
command.Parameters.AddWithValue("@api_type", api_type);
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
await command.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<long> GetStoredFileSize(string folder, long media_id, string api_type)
|
||||||
|
{
|
||||||
|
long size;
|
||||||
|
using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
using SqliteCommand cmd = new($"SELECT size FROM medias WHERE media_id=@media_id and api_type=@api_type", connection);
|
||||||
|
cmd.Parameters.AddWithValue("@media_id", media_id);
|
||||||
|
cmd.Parameters.AddWithValue("@api_type", api_type);
|
||||||
|
size = Convert.ToInt64(await cmd.ExecuteScalarAsync());
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DateTime?> GetMostRecentPostDate(string folder)
|
||||||
|
{
|
||||||
|
DateTime? mostRecentDate = null;
|
||||||
|
using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
using SqliteCommand cmd = new(@"
|
||||||
|
SELECT
|
||||||
|
MIN(created_at) AS created_at
|
||||||
|
FROM (
|
||||||
|
SELECT MAX(P.created_at) AS created_at
|
||||||
|
FROM posts AS P
|
||||||
|
LEFT OUTER JOIN medias AS m
|
||||||
|
ON P.post_id = m.post_id
|
||||||
|
AND m.downloaded = 1
|
||||||
|
UNION
|
||||||
|
SELECT MIN(P.created_at) AS created_at
|
||||||
|
FROM posts AS P
|
||||||
|
INNER JOIN medias AS m
|
||||||
|
ON P.post_id = m.post_id
|
||||||
|
WHERE m.downloaded = 0
|
||||||
|
)", connection);
|
||||||
|
var scalarValue = await cmd.ExecuteScalarAsync();
|
||||||
|
if(scalarValue != null && scalarValue != DBNull.Value)
|
||||||
|
{
|
||||||
|
mostRecentDate = Convert.ToDateTime(scalarValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mostRecentDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureCreatedAtColumnExists(SqliteConnection connection, string tableName)
|
||||||
|
{
|
||||||
|
using SqliteCommand cmd = new($"PRAGMA table_info({tableName});", connection);
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
bool columnExists = false;
|
||||||
|
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
if (reader["name"].ToString() == "record_created_at")
|
||||||
|
{
|
||||||
|
columnExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!columnExists)
|
||||||
|
{
|
||||||
|
using SqliteCommand alterCmd = new($"ALTER TABLE {tableName} ADD COLUMN record_created_at TIMESTAMP;", connection);
|
||||||
|
await alterCmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
OF DL/Helpers/DownloadContext.cs
Normal file
36
OF DL/Helpers/DownloadContext.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using OF_DL.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Helpers
|
||||||
|
{
|
||||||
|
internal interface IDownloadContext
|
||||||
|
{
|
||||||
|
public IDownloadConfig DownloadConfig { get; }
|
||||||
|
public IFileNameFormatConfig FileNameFormatConfig { get; }
|
||||||
|
public APIHelper ApiHelper { get; }
|
||||||
|
public DBHelper DBHelper { get; }
|
||||||
|
public DownloadHelper DownloadHelper { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DownloadContext : IDownloadContext
|
||||||
|
{
|
||||||
|
public APIHelper ApiHelper { get; }
|
||||||
|
public DBHelper DBHelper { get; }
|
||||||
|
public DownloadHelper DownloadHelper { get; }
|
||||||
|
public IDownloadConfig DownloadConfig { get; }
|
||||||
|
public IFileNameFormatConfig FileNameFormatConfig { get; }
|
||||||
|
|
||||||
|
public DownloadContext(Auth auth, IDownloadConfig downloadConfig, IFileNameFormatConfig fileNameFormatConfig, APIHelper apiHelper, DBHelper dBHelper)
|
||||||
|
{
|
||||||
|
ApiHelper = apiHelper;
|
||||||
|
DBHelper = dBHelper;
|
||||||
|
DownloadConfig = downloadConfig;
|
||||||
|
FileNameFormatConfig = fileNameFormatConfig;
|
||||||
|
DownloadHelper = new DownloadHelper(auth, downloadConfig, fileNameFormatConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1790
OF DL/Helpers/DownloadHelper.cs
Normal file
1790
OF DL/Helpers/DownloadHelper.cs
Normal file
File diff suppressed because it is too large
Load Diff
188
OF DL/Helpers/FileNameHelper.cs
Normal file
188
OF DL/Helpers/FileNameHelper.cs
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
using HtmlAgilityPack;
|
||||||
|
using OF_DL.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Helpers
|
||||||
|
{
|
||||||
|
public class FileNameHelper : IFileNameHelper
|
||||||
|
{
|
||||||
|
private readonly Auth auth;
|
||||||
|
|
||||||
|
public FileNameHelper(Auth auth)
|
||||||
|
{
|
||||||
|
this.auth = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, string>> GetFilename(object obj1, object obj2, object obj3, List<string> selectedProperties, string username, Dictionary<string, int> users = null)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> values = new();
|
||||||
|
Type type1 = obj1.GetType();
|
||||||
|
Type type2 = obj2.GetType();
|
||||||
|
PropertyInfo[] properties1 = type1.GetProperties();
|
||||||
|
PropertyInfo[] properties2 = type2.GetProperties();
|
||||||
|
|
||||||
|
foreach (string propertyName in selectedProperties)
|
||||||
|
{
|
||||||
|
if (propertyName.Contains("media"))
|
||||||
|
{
|
||||||
|
object drmProperty = null;
|
||||||
|
object fileProperty = GetNestedPropertyValue(obj2, "files");
|
||||||
|
if(fileProperty != null)
|
||||||
|
{
|
||||||
|
drmProperty = GetNestedPropertyValue(obj2, "files.drm");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(fileProperty != null && drmProperty != null && propertyName == "mediaCreatedAt")
|
||||||
|
{
|
||||||
|
object mpdurl = GetNestedPropertyValue(obj2, "files.drm.manifest.dash");
|
||||||
|
object policy = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontPolicy");
|
||||||
|
object signature = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontSignature");
|
||||||
|
object kvp = GetNestedPropertyValue(obj2, "files.drm.signature.dash.CloudFrontKeyPairId");
|
||||||
|
DateTime lastModified = await DownloadHelper.GetDRMVideoLastModified(string.Join(",", mpdurl, policy, signature, kvp), auth);
|
||||||
|
values.Add(propertyName, lastModified.ToString("yyyy-MM-dd"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if((fileProperty == null || drmProperty == null) && propertyName == "mediaCreatedAt")
|
||||||
|
{
|
||||||
|
object source = GetNestedPropertyValue(obj2, "files.full.url");
|
||||||
|
if(source != null)
|
||||||
|
{
|
||||||
|
DateTime lastModified = await DownloadHelper.GetMediaLastModified(source.ToString());
|
||||||
|
values.Add(propertyName, lastModified.ToString("yyyy-MM-dd"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
object preview = GetNestedPropertyValue(obj2, "preview");
|
||||||
|
if(preview != null)
|
||||||
|
{
|
||||||
|
DateTime lastModified = await DownloadHelper.GetMediaLastModified(preview.ToString());
|
||||||
|
values.Add(propertyName, lastModified.ToString("yyyy-MM-dd"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
PropertyInfo? property = Array.Find(properties2, p => p.Name.Equals(propertyName.Replace("media", ""), StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (property != null)
|
||||||
|
{
|
||||||
|
object? propertyValue = property.GetValue(obj2);
|
||||||
|
if (propertyValue != null)
|
||||||
|
{
|
||||||
|
if (propertyValue is DateTime dateTimeValue)
|
||||||
|
{
|
||||||
|
values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
values.Add(propertyName, propertyValue.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propertyName.Contains("filename"))
|
||||||
|
{
|
||||||
|
string sourcePropertyPath = "files.full.url";
|
||||||
|
object sourcePropertyValue = GetNestedPropertyValue(obj2, sourcePropertyPath);
|
||||||
|
if (sourcePropertyValue != null)
|
||||||
|
{
|
||||||
|
Uri uri = new(sourcePropertyValue.ToString());
|
||||||
|
string filename = System.IO.Path.GetFileName(uri.LocalPath);
|
||||||
|
values.Add(propertyName, filename.Split(".")[0]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string propertyPath = "files.drm.manifest.dash";
|
||||||
|
object nestedPropertyValue = GetNestedPropertyValue(obj2, propertyPath);
|
||||||
|
if (nestedPropertyValue != null)
|
||||||
|
{
|
||||||
|
Uri uri = new(nestedPropertyValue.ToString());
|
||||||
|
string filename = System.IO.Path.GetFileName(uri.LocalPath);
|
||||||
|
values.Add(propertyName, filename.Split(".")[0] + "_source");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propertyName.Contains("username"))
|
||||||
|
{
|
||||||
|
if(!string.IsNullOrEmpty(username))
|
||||||
|
{
|
||||||
|
values.Add(propertyName, username);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string propertyPath = "id";
|
||||||
|
object nestedPropertyValue = GetNestedPropertyValue(obj3, propertyPath);
|
||||||
|
if (nestedPropertyValue != null)
|
||||||
|
{
|
||||||
|
values.Add(propertyName, users.FirstOrDefault(u => u.Value == Convert.ToInt32(nestedPropertyValue.ToString())).Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propertyName.Contains("text", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
PropertyInfo property = Array.Find(properties1, p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (property != null)
|
||||||
|
{
|
||||||
|
object propertyValue = property.GetValue(obj1);
|
||||||
|
if (propertyValue != null)
|
||||||
|
{
|
||||||
|
var pageDoc = new HtmlDocument();
|
||||||
|
pageDoc.LoadHtml(propertyValue.ToString());
|
||||||
|
var str = pageDoc.DocumentNode.InnerText;
|
||||||
|
if (str.Length > 100) // todo: add length limit to config
|
||||||
|
str = str.Substring(0, 100);
|
||||||
|
values.Add(propertyName, str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PropertyInfo property = Array.Find(properties1, p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (property != null)
|
||||||
|
{
|
||||||
|
object propertyValue = property.GetValue(obj1);
|
||||||
|
if (propertyValue != null)
|
||||||
|
{
|
||||||
|
if (propertyValue is DateTime dateTimeValue)
|
||||||
|
{
|
||||||
|
values.Add(propertyName, dateTimeValue.ToString("yyyy-MM-dd"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
values.Add(propertyName, propertyValue.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
static object GetNestedPropertyValue(object source, string propertyPath)
|
||||||
|
{
|
||||||
|
object value = source;
|
||||||
|
foreach (var propertyName in propertyPath.Split('.'))
|
||||||
|
{
|
||||||
|
PropertyInfo property = value.GetType().GetProperty(propertyName) ?? throw new ArgumentException($"Property '{propertyName}' not found.");
|
||||||
|
value = property.GetValue(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> BuildFilename(string fileFormat, Dictionary<string, string> values)
|
||||||
|
{
|
||||||
|
foreach (var kvp in values)
|
||||||
|
{
|
||||||
|
string placeholder = "{" + kvp.Key + "}";
|
||||||
|
fileFormat = fileFormat.Replace(placeholder, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WidevineClient.Utils.RemoveInvalidFileNameChars($"{fileFormat}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
OF DL/Helpers/Interfaces/IAPIHelper.cs
Normal file
38
OF DL/Helpers/Interfaces/IAPIHelper.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OF_DL.Entities;
|
||||||
|
using OF_DL.Entities.Archived;
|
||||||
|
using OF_DL.Entities.Messages;
|
||||||
|
using OF_DL.Entities.Post;
|
||||||
|
using OF_DL.Entities.Purchased;
|
||||||
|
using OF_DL.Entities.Streams;
|
||||||
|
using OF_DL.Enumurations;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace OF_DL.Helpers
|
||||||
|
{
|
||||||
|
public interface IAPIHelper
|
||||||
|
{
|
||||||
|
Task<string> GetDecryptionKeyCDRMProject(Dictionary<string, string> drmHeaders, string licenceURL, string pssh);
|
||||||
|
Task<string> GetDecryptionKeyCDM(Dictionary<string, string> drmHeaders, string licenceURL, string pssh);
|
||||||
|
Task<DateTime> GetDRMMPDLastModified(string mpdUrl, string policy, string signature, string kvp);
|
||||||
|
Task<string> GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp);
|
||||||
|
Task<Dictionary<string, int>> GetLists(string endpoint, IDownloadConfig config);
|
||||||
|
Task<List<string>> GetListUsers(string endpoint, IDownloadConfig config);
|
||||||
|
Task<Dictionary<long, string>> GetMedia(MediaType mediatype, string endpoint, string? username, string folder, IDownloadConfig config, List<long> paid_post_ids);
|
||||||
|
Task<PaidPostCollection> GetPaidPosts(string endpoint, string folder, string username, IDownloadConfig config, List<long> paid_post_ids, StatusContext ctx);
|
||||||
|
Task<PostCollection> GetPosts(string endpoint, string folder, IDownloadConfig config, List<long> paid_post_ids, StatusContext ctx);
|
||||||
|
Task<SinglePostCollection> GetPost(string endpoint, string folder, IDownloadConfig config);
|
||||||
|
Task<StreamsCollection> GetStreams(string endpoint, string folder, IDownloadConfig config, List<long> paid_post_ids, StatusContext ctx);
|
||||||
|
Task<ArchivedCollection> GetArchived(string endpoint, string folder, IDownloadConfig config, StatusContext ctx);
|
||||||
|
Task<MessageCollection> GetMessages(string endpoint, string folder, IDownloadConfig config, StatusContext ctx);
|
||||||
|
Task<PaidMessageCollection> GetPaidMessages(string endpoint, string folder, string username, IDownloadConfig config, StatusContext ctx);
|
||||||
|
Task<Dictionary<string, int>> GetPurchasedTabUsers(string endpoint, IDownloadConfig config, Dictionary<string, int> users);
|
||||||
|
Task<List<PurchasedTabCollection>> GetPurchasedTab(string endpoint, string folder, IDownloadConfig config, Dictionary<string, int> users);
|
||||||
|
Task<User> GetUserInfo(string endpoint);
|
||||||
|
Task<JObject> GetUserInfoById(string endpoint);
|
||||||
|
Dictionary<string, string> GetDynamicHeaders(string path, string queryParam);
|
||||||
|
Task<Dictionary<string, int>> GetActiveSubscriptions(string endpoint, bool includeRestrictedSubscriptions, IDownloadConfig config);
|
||||||
|
Task<Dictionary<string, int>> GetExpiredSubscriptions(string endpoint, bool includeRestrictedSubscriptions, IDownloadConfig config);
|
||||||
|
Task<string> GetDecryptionKeyOFDL(Dictionary<string, string> drmHeaders, string licenceURL, string pssh);
|
||||||
|
}
|
||||||
|
}
|
17
OF DL/Helpers/Interfaces/IDBHelper.cs
Normal file
17
OF DL/Helpers/Interfaces/IDBHelper.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace OF_DL.Helpers
|
||||||
|
{
|
||||||
|
public interface IDBHelper
|
||||||
|
{
|
||||||
|
Task AddMessage(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at, int user_id);
|
||||||
|
Task AddPost(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at);
|
||||||
|
Task AddStory(string folder, long post_id, string message_text, string price, bool is_paid, bool is_archived, DateTime created_at);
|
||||||
|
Task CreateDB(string folder);
|
||||||
|
Task CreateUsersDB(Dictionary<string, int> users);
|
||||||
|
Task CheckUsername(KeyValuePair<string, int> user, string path);
|
||||||
|
Task AddMedia(string folder, long media_id, long post_id, string link, string? directory, string? filename, long? size, string api_type, string media_type, bool preview, bool downloaded, DateTime? created_at);
|
||||||
|
Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, bool downloaded, DateTime created_at);
|
||||||
|
Task<long> GetStoredFileSize(string folder, long media_id, string api_type);
|
||||||
|
Task<bool> CheckDownloaded(string folder, long media_id, string api_type);
|
||||||
|
Task<DateTime?> GetMostRecentPostDate(string folder);
|
||||||
|
}
|
||||||
|
}
|
36
OF DL/Helpers/Interfaces/IDownloadHelper.cs
Normal file
36
OF DL/Helpers/Interfaces/IDownloadHelper.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using OF_DL.Entities;
|
||||||
|
using OF_DL.Entities.Archived;
|
||||||
|
using OF_DL.Entities.Messages;
|
||||||
|
using OF_DL.Entities.Post;
|
||||||
|
using OF_DL.Entities.Purchased;
|
||||||
|
using OF_DL.Entities.Streams;
|
||||||
|
using Spectre.Console;
|
||||||
|
using static OF_DL.Entities.Messages.Messages;
|
||||||
|
|
||||||
|
namespace OF_DL.Helpers
|
||||||
|
{
|
||||||
|
public interface IDownloadHelper
|
||||||
|
{
|
||||||
|
Task<long> CalculateTotalFileSize(List<string> urls);
|
||||||
|
Task<bool> DownloadArchivedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Archived.List messageInfo, Archived.Medium messageMedia, Archived.Author author, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadArchivedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Archived.List postInfo, Archived.Medium postMedia, Archived.Author author, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, SinglePost postInfo, SinglePost.Medium postMedia, SinglePost.Author author, Dictionary<string, int> users);
|
||||||
|
Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username);
|
||||||
|
Task<bool> DownloadMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Messages.List messageInfo, Messages.Medium messageMedia, Messages.FromUser fromUser, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadMessageMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Messages.List messageInfo, Messages.Medium messageMedia, Messages.FromUser fromUser, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Post.List postInfo, Post.Medium postMedia, Post.Author author, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Post.List? postInfo, Post.Medium? postMedia, Post.Author? author, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, SinglePost? postInfo, SinglePost.Medium? postMedia, SinglePost.Author? author, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadPurchasedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List messageInfo, Medium messageMedia, Purchased.FromUser fromUser, Dictionary<string, int> users);
|
||||||
|
|
||||||
|
Task<bool> DownloadSinglePurchasedMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadPurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List messageInfo, Medium messageMedia, Purchased.FromUser fromUser, Dictionary<string, int> users);
|
||||||
|
|
||||||
|
Task<bool> DownloadSinglePurchasedMessageDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string? filenameFormat, SingleMessage? messageInfo, Medium? messageMedia, Entities.Messages.FromUser? fromUser, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadPurchasedPostDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List postInfo, Medium postMedia, Purchased.FromUser fromUser, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadPurchasedPostMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string filenameFormat, Purchased.List messageInfo, Medium messageMedia, Purchased.FromUser fromUser, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadStoryMedia(string url, string folder, long media_id, string api_type, ProgressTask task);
|
||||||
|
Task<bool> DownloadStreamMedia(string url, string folder, long media_id, string api_type, ProgressTask task, string? filenameFormat, Streams.List? streamInfo, Streams.Medium? streamMedia, Streams.Author? author, Dictionary<string, int> users);
|
||||||
|
Task<bool> DownloadStreamsDRMVideo(string policy, string signature, string kvp, string url, string decryptionKey, string folder, DateTime lastModified, long media_id, string api_type, ProgressTask task, string filenameFormat, Streams.List streamInfo, Streams.Medium streamMedia, Streams.Author author, Dictionary<string, int> users);
|
||||||
|
}
|
||||||
|
}
|
8
OF DL/Helpers/Interfaces/IFileNameHelper.cs
Normal file
8
OF DL/Helpers/Interfaces/IFileNameHelper.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace OF_DL.Helpers
|
||||||
|
{
|
||||||
|
public interface IFileNameHelper
|
||||||
|
{
|
||||||
|
Task<string> BuildFilename(string fileFormat, Dictionary<string, string> values);
|
||||||
|
Task<Dictionary<string, string>> GetFilename(object obj1, object obj2, object obj3, List<string> selectedProperties, string username, Dictionary<string, int> users = null);
|
||||||
|
}
|
||||||
|
}
|
99
OF DL/HttpUtil.cs
Normal file
99
OF DL/HttpUtil.cs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace WidevineClient
|
||||||
|
{
|
||||||
|
class HttpUtil
|
||||||
|
{
|
||||||
|
public static HttpClient Client { get; set; } = new HttpClient(new HttpClientHandler
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = true,
|
||||||
|
//Proxy = null
|
||||||
|
});
|
||||||
|
|
||||||
|
public static byte[] PostData(string URL, Dictionary<string, string> headers, string postData)
|
||||||
|
{
|
||||||
|
var mediaType = postData.StartsWith("{") ? "application/json" : "application/x-www-form-urlencoded";
|
||||||
|
StringContent content = new StringContent(postData, Encoding.UTF8, mediaType);
|
||||||
|
//ByteArrayContent content = new ByteArrayContent(postData);
|
||||||
|
|
||||||
|
HttpResponseMessage response = Post(URL, headers, content);
|
||||||
|
byte[] bytes = response.Content.ReadAsByteArrayAsync().Result;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] PostData(string URL, Dictionary<string, string> headers, byte[] postData)
|
||||||
|
{
|
||||||
|
ByteArrayContent content = new ByteArrayContent(postData);
|
||||||
|
|
||||||
|
HttpResponseMessage response = Post(URL, headers, content);
|
||||||
|
byte[] bytes = response.Content.ReadAsByteArrayAsync().Result;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] PostData(string URL, Dictionary<string, string> headers, Dictionary<string, string> postData)
|
||||||
|
{
|
||||||
|
FormUrlEncodedContent content = new FormUrlEncodedContent(postData);
|
||||||
|
|
||||||
|
HttpResponseMessage response = Post(URL, headers, content);
|
||||||
|
byte[] bytes = response.Content.ReadAsByteArrayAsync().Result;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetWebSource(string URL, Dictionary<string, string> headers = null)
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = Get(URL, headers);
|
||||||
|
byte[] bytes = response.Content.ReadAsByteArrayAsync().Result;
|
||||||
|
return Encoding.UTF8.GetString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] GetBinary(string URL, Dictionary<string, string> headers = null)
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = Get(URL, headers);
|
||||||
|
byte[] bytes = response.Content.ReadAsByteArrayAsync().Result;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
public static string GetString(byte[] bytes)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HttpResponseMessage Get(string URL, Dictionary<string, string> headers = null)
|
||||||
|
{
|
||||||
|
HttpRequestMessage request = new HttpRequestMessage()
|
||||||
|
{
|
||||||
|
RequestUri = new Uri(URL),
|
||||||
|
Method = HttpMethod.Get
|
||||||
|
};
|
||||||
|
|
||||||
|
if (headers != null)
|
||||||
|
foreach (KeyValuePair<string, string> header in headers)
|
||||||
|
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
|
||||||
|
return Send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HttpResponseMessage Post(string URL, Dictionary<string, string> headers, HttpContent content)
|
||||||
|
{
|
||||||
|
HttpRequestMessage request = new HttpRequestMessage()
|
||||||
|
{
|
||||||
|
RequestUri = new Uri(URL),
|
||||||
|
Method = HttpMethod.Post,
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
|
||||||
|
if (headers != null)
|
||||||
|
foreach (KeyValuePair<string, string> header in headers)
|
||||||
|
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
|
||||||
|
return Send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HttpResponseMessage Send(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
return Client.SendAsync(request).Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
OF DL/Icon/download.ico
Normal file
BIN
OF DL/Icon/download.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
50
OF DL/OF DL.csproj
Normal file
50
OF DL/OF DL.csproj
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<RootNamespace>OF_DL</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ApplicationIcon>Icon\download.ico</ApplicationIcon>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Icon\download.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Akka" Version="1.5.39" />
|
||||||
|
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" />
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||||
|
<PackageReference Include="protobuf-net" Version="3.2.46" />
|
||||||
|
<PackageReference Include="PuppeteerSharp" Version="20.1.3" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
<PackageReference Include="System.Reactive" Version="6.0.1" />
|
||||||
|
<PackageReference Include="xFFmpeg.NET" Version="7.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Spectre.Console">
|
||||||
|
<HintPath>References\Spectre.Console.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="auth.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="config.conf">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="rules.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
3233
OF DL/Program.cs
Normal file
3233
OF DL/Program.cs
Normal file
File diff suppressed because it is too large
Load Diff
112
OF DL/References/Spectre.Console.deps.json
Normal file
112
OF DL/References/Spectre.Console.deps.json
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"runtimeTarget": {
|
||||||
|
"name": ".NETCoreApp,Version=v7.0",
|
||||||
|
"signature": ""
|
||||||
|
},
|
||||||
|
"compilationOptions": {},
|
||||||
|
"targets": {
|
||||||
|
".NETCoreApp,Version=v7.0": {
|
||||||
|
"Spectre.Console/0.0.0-preview.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.SourceLink.GitHub": "1.1.1",
|
||||||
|
"MinVer": "4.2.0",
|
||||||
|
"Roslynator.Analyzers": "4.1.2",
|
||||||
|
"StyleCop.Analyzers": "1.2.0-beta.435",
|
||||||
|
"System.Memory": "4.5.5",
|
||||||
|
"Wcwidth.Sources": "1.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"Spectre.Console.dll": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Build.Tasks.Git/1.1.1": {},
|
||||||
|
"Microsoft.SourceLink.Common/1.1.1": {},
|
||||||
|
"Microsoft.SourceLink.GitHub/1.1.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Build.Tasks.Git": "1.1.1",
|
||||||
|
"Microsoft.SourceLink.Common": "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MinVer/4.2.0": {},
|
||||||
|
"Roslynator.Analyzers/4.1.2": {},
|
||||||
|
"StyleCop.Analyzers/1.2.0-beta.435": {
|
||||||
|
"dependencies": {
|
||||||
|
"StyleCop.Analyzers.Unstable": "1.2.0.435"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"StyleCop.Analyzers.Unstable/1.2.0.435": {},
|
||||||
|
"System.Memory/4.5.5": {},
|
||||||
|
"Wcwidth.Sources/1.0.0": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"Spectre.Console/0.0.0-preview.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"Microsoft.Build.Tasks.Git/1.1.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==",
|
||||||
|
"path": "microsoft.build.tasks.git/1.1.1",
|
||||||
|
"hashPath": "microsoft.build.tasks.git.1.1.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.SourceLink.Common/1.1.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==",
|
||||||
|
"path": "microsoft.sourcelink.common/1.1.1",
|
||||||
|
"hashPath": "microsoft.sourcelink.common.1.1.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.SourceLink.GitHub/1.1.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
|
||||||
|
"path": "microsoft.sourcelink.github/1.1.1",
|
||||||
|
"hashPath": "microsoft.sourcelink.github.1.1.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"MinVer/4.2.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-Po4tv+sri1jsaebQ8F6+yD5ru9Gas5mR111F6HR2ULqwflvjjZXMstpeOc1GHMJeQa3g4E3b8MX8K2cShkuUAg==",
|
||||||
|
"path": "minver/4.2.0",
|
||||||
|
"hashPath": "minver.4.2.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Roslynator.Analyzers/4.1.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-bNl3GRSBFjJymYnwq/IRDD9MOSZz9VKdGk9RsN0MWIXoSRnVKQv84f6s9nLE13y20lZgMZKlDqGw2uInBH4JgA==",
|
||||||
|
"path": "roslynator.analyzers/4.1.2",
|
||||||
|
"hashPath": "roslynator.analyzers.4.1.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"StyleCop.Analyzers/1.2.0-beta.435": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==",
|
||||||
|
"path": "stylecop.analyzers/1.2.0-beta.435",
|
||||||
|
"hashPath": "stylecop.analyzers.1.2.0-beta.435.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"StyleCop.Analyzers.Unstable/1.2.0.435": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg==",
|
||||||
|
"path": "stylecop.analyzers.unstable/1.2.0.435",
|
||||||
|
"hashPath": "stylecop.analyzers.unstable.1.2.0.435.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Memory/4.5.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
|
||||||
|
"path": "system.memory/4.5.5",
|
||||||
|
"hashPath": "system.memory.4.5.5.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Wcwidth.Sources/1.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-86tmwfGXRz7GJQXBnoTFoMvqSqd6irfkEkRzQFR54W/nweaR8cUvzY8x++z+B/+eUPSuqD2Ah1iPJHgthy4pzg==",
|
||||||
|
"path": "wcwidth.sources/1.0.0",
|
||||||
|
"hashPath": "wcwidth.sources.1.0.0.nupkg.sha512"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
OF DL/References/Spectre.Console.dll
Normal file
BIN
OF DL/References/Spectre.Console.dll
Normal file
Binary file not shown.
189
OF DL/Utils.cs
Normal file
189
OF DL/Utils.cs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace WidevineClient
|
||||||
|
{
|
||||||
|
class Utils
|
||||||
|
{
|
||||||
|
public static double EvaluateEquation(string equation, int decimals = 3)
|
||||||
|
{
|
||||||
|
var dataTable = new DataTable();
|
||||||
|
return Math.Round((double)dataTable.Compute(equation, ""), decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RunCommand(string command, string args)
|
||||||
|
{
|
||||||
|
Process p = new Process();
|
||||||
|
p.StartInfo.UseShellExecute = false;
|
||||||
|
p.StartInfo.RedirectStandardOutput = true;
|
||||||
|
p.StartInfo.FileName = command;
|
||||||
|
p.StartInfo.Arguments = args;
|
||||||
|
p.StartInfo.CreateNoWindow = true;
|
||||||
|
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
|
||||||
|
p.Start();
|
||||||
|
string output = p.StandardOutput.ReadToEnd();
|
||||||
|
p.WaitForExit();
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunCommandCode(string command, string args)
|
||||||
|
{
|
||||||
|
Process p = new Process();
|
||||||
|
p.StartInfo.UseShellExecute = false;
|
||||||
|
p.StartInfo.RedirectStandardOutput = false;
|
||||||
|
p.StartInfo.FileName = command;
|
||||||
|
p.StartInfo.Arguments = args;
|
||||||
|
p.Start();
|
||||||
|
p.WaitForExit();
|
||||||
|
return p.ExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Xor(byte[] a, byte[] b)
|
||||||
|
{
|
||||||
|
byte[] x = new byte[Math.Min(a.Length, b.Length)];
|
||||||
|
|
||||||
|
for (int i = 0; i < x.Length; i++)
|
||||||
|
{
|
||||||
|
x[i] = (byte)(a[i] ^ b[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GenerateRandomId()
|
||||||
|
{
|
||||||
|
return BytesToHex(RandomBytes(3)).ToLower();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] RandomBytes(int length)
|
||||||
|
{
|
||||||
|
var bytes = new byte[length];
|
||||||
|
new Random().NextBytes(bytes);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] GetElementsInnerTextByAttribute(string html, string element, string attribute)
|
||||||
|
{
|
||||||
|
List<string> content = new List<string>();
|
||||||
|
|
||||||
|
foreach (string line in html.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None))
|
||||||
|
{
|
||||||
|
if (line.Contains("<" + element) && line.Contains(attribute))
|
||||||
|
{
|
||||||
|
string contentPart = line.Substring(0, line.LastIndexOf("<"));
|
||||||
|
if (contentPart.EndsWith(">"))
|
||||||
|
contentPart = contentPart[..^1];
|
||||||
|
|
||||||
|
contentPart = contentPart[(contentPart.LastIndexOf(">") + 1)..];
|
||||||
|
|
||||||
|
if (contentPart.Contains("<"))
|
||||||
|
contentPart = contentPart[..contentPart.IndexOf("<")];
|
||||||
|
|
||||||
|
content.Add(contentPart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BytesToHex(byte[] data)
|
||||||
|
{
|
||||||
|
return BitConverter.ToString(data).Replace("-", "");
|
||||||
|
}
|
||||||
|
public static byte[] HexToBytes(string hex)
|
||||||
|
{
|
||||||
|
hex = hex.Trim();
|
||||||
|
byte[] bytes = new byte[hex.Length / 2];
|
||||||
|
|
||||||
|
for (int i = 0; i < hex.Length; i += 2)
|
||||||
|
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsBase64Encoded(string str)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] data = Convert.FromBase64String(str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Base64Pad(string base64)
|
||||||
|
{
|
||||||
|
if (base64.Length % 4 != 0)
|
||||||
|
{
|
||||||
|
base64 = base64.PadRight(base64.Length + (4 - (base64.Length % 4)), '=');
|
||||||
|
}
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
public static string Base64ToString(string base64)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetString(Convert.FromBase64String(base64));
|
||||||
|
}
|
||||||
|
public static string StringToBase64(string str)
|
||||||
|
{
|
||||||
|
return Convert.ToBase64String(Encoding.UTF8.GetBytes(str));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void TitleProgress(long read, long length)
|
||||||
|
{
|
||||||
|
long readMB = read / 1024 / 1024;
|
||||||
|
long lengthMB = length / 1024 / 1024;
|
||||||
|
Console.Title = $"{readMB}/{lengthMB}MB";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void TitleProgressNoConversion(long read, long length)
|
||||||
|
{
|
||||||
|
Console.Title = $"{read}/{length}MB";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Version()
|
||||||
|
{
|
||||||
|
return System.Reflection.Assembly.GetCallingAssembly().GetName().Version.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? RemoveInvalidFileNameChars(string? fileName)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(fileName) ? fileName : string.Concat(fileName.Split(Path.GetInvalidFileNameChars()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<string> CalculateFolderMD5(string folder)
|
||||||
|
{
|
||||||
|
List<string> md5Hashes = new List<string>();
|
||||||
|
if (Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
string[] files = Directory.GetFiles(folder);
|
||||||
|
|
||||||
|
foreach (string file in files)
|
||||||
|
{
|
||||||
|
md5Hashes.Add(CalculateMD5(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return md5Hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string CalculateMD5(string filePath)
|
||||||
|
{
|
||||||
|
using (var md5 = MD5.Create())
|
||||||
|
{
|
||||||
|
using (var stream = File.OpenRead(filePath))
|
||||||
|
{
|
||||||
|
byte[] hash = md5.ComputeHash(stream);
|
||||||
|
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
159
OF DL/Utils/ThrottledStream.cs
Normal file
159
OF DL/Utils/ThrottledStream.cs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Concurrency;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OF_DL.Utils;
|
||||||
|
|
||||||
|
|
||||||
|
public class ThrottledStream : Stream
|
||||||
|
|
||||||
|
{
|
||||||
|
private readonly Stream parent;
|
||||||
|
private readonly int maxBytesPerSecond;
|
||||||
|
private readonly IScheduler scheduler;
|
||||||
|
private readonly IStopwatch stopwatch;
|
||||||
|
private readonly bool shouldThrottle;
|
||||||
|
|
||||||
|
private long processed;
|
||||||
|
|
||||||
|
public ThrottledStream(Stream parent, int maxBytesPerSecond, IScheduler scheduler, bool shouldThrottle)
|
||||||
|
{
|
||||||
|
this.shouldThrottle = shouldThrottle;
|
||||||
|
this.maxBytesPerSecond = maxBytesPerSecond;
|
||||||
|
this.parent = parent;
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
stopwatch = scheduler.StartStopwatch();
|
||||||
|
processed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThrottledStream(Stream parent, int maxBytesPerSecond, bool shouldThrottle)
|
||||||
|
: this(parent, maxBytesPerSecond, Scheduler.Immediate, shouldThrottle)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void Throttle(int bytes)
|
||||||
|
{
|
||||||
|
if (!shouldThrottle) return;
|
||||||
|
processed += bytes;
|
||||||
|
var targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond);
|
||||||
|
var actualTime = stopwatch.Elapsed;
|
||||||
|
var sleep = targetTime - actualTime;
|
||||||
|
if (sleep > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
using var waitHandle = new AutoResetEvent(initialState: false);
|
||||||
|
scheduler.Sleep(sleep).GetAwaiter().OnCompleted(() => waitHandle.Set());
|
||||||
|
waitHandle.WaitOne();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task ThrottleAsync(int bytes)
|
||||||
|
{
|
||||||
|
if (!shouldThrottle) return;
|
||||||
|
processed += bytes;
|
||||||
|
var targetTime = TimeSpan.FromSeconds((double)processed / maxBytesPerSecond);
|
||||||
|
var actualTime = stopwatch.Elapsed;
|
||||||
|
var sleep = targetTime - actualTime;
|
||||||
|
|
||||||
|
if (sleep > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
await Task.Delay(sleep, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var read = await parent.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
|
||||||
|
await ThrottleAsync(read).ConfigureAwait(false);
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
int bytesRead = await parent.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||||
|
await ThrottleAsync(bytesRead).ConfigureAwait(false);
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await ThrottleAsync(count).ConfigureAwait(false);
|
||||||
|
await parent.WriteAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await ThrottleAsync(buffer.Length).ConfigureAwait(false);
|
||||||
|
await parent.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override bool CanRead
|
||||||
|
{
|
||||||
|
get { return parent.CanRead; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override bool CanSeek
|
||||||
|
{
|
||||||
|
get { return parent.CanSeek; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override bool CanWrite
|
||||||
|
{
|
||||||
|
get { return parent.CanWrite; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override void Flush()
|
||||||
|
{
|
||||||
|
parent.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override long Length
|
||||||
|
{
|
||||||
|
get { return parent.Length; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return parent.Position;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
parent.Position = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
var read = parent.Read(buffer, offset, count);
|
||||||
|
Throttle(read);
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin)
|
||||||
|
{
|
||||||
|
return parent.Seek(offset, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetLength(long value)
|
||||||
|
{
|
||||||
|
parent.SetLength(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
Throttle(count);
|
||||||
|
parent.Write(buffer, offset, count);
|
||||||
|
}
|
||||||
|
}
|
25
OF DL/Utils/XmlUtils.cs
Normal file
25
OF DL/Utils/XmlUtils.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace OF_DL.Utils
|
||||||
|
{
|
||||||
|
internal static class XmlUtils
|
||||||
|
{
|
||||||
|
public static string EvaluateInnerText(string xmlValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsedText = XElement.Parse($"<root>{xmlValue}</root>");
|
||||||
|
return parsedText.Value;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ }
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
581
OF DL/Widevine/CDM.cs
Normal file
581
OF DL/Widevine/CDM.cs
Normal file
@ -0,0 +1,581 @@
|
|||||||
|
using ProtoBuf;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using WidevineClient.Crypto;
|
||||||
|
|
||||||
|
namespace WidevineClient.Widevine
|
||||||
|
{
|
||||||
|
public class CDM
|
||||||
|
{
|
||||||
|
static Dictionary<string, CDMDevice> Devices { get; } = new Dictionary<string, CDMDevice>()
|
||||||
|
{
|
||||||
|
[Constants.DEVICE_NAME] = new CDMDevice(Constants.DEVICE_NAME, null, null, null)
|
||||||
|
};
|
||||||
|
static Dictionary<string, Session> Sessions { get; set; } = new Dictionary<string, Session>();
|
||||||
|
|
||||||
|
static byte[] CheckPSSH(string psshB64)
|
||||||
|
{
|
||||||
|
byte[] systemID = new byte[] { 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237 };
|
||||||
|
|
||||||
|
if (psshB64.Length % 4 != 0)
|
||||||
|
{
|
||||||
|
psshB64 = psshB64.PadRight(psshB64.Length + (4 - (psshB64.Length % 4)), '=');
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] pssh = Convert.FromBase64String(psshB64);
|
||||||
|
|
||||||
|
if (pssh.Length < 30)
|
||||||
|
return pssh;
|
||||||
|
|
||||||
|
if (!pssh[12..28].SequenceEqual(systemID))
|
||||||
|
{
|
||||||
|
List<byte> newPssh = new List<byte>() { 0, 0, 0 };
|
||||||
|
newPssh.Add((byte)(32 + pssh.Length));
|
||||||
|
newPssh.AddRange(Encoding.UTF8.GetBytes("pssh"));
|
||||||
|
newPssh.AddRange(new byte[] { 0, 0, 0, 0 });
|
||||||
|
newPssh.AddRange(systemID);
|
||||||
|
newPssh.AddRange(new byte[] { 0, 0, 0, 0 });
|
||||||
|
newPssh[31] = (byte)(pssh.Length);
|
||||||
|
newPssh.AddRange(pssh);
|
||||||
|
|
||||||
|
return newPssh.ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return pssh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string OpenSession(string initDataB64, string deviceName, bool offline = false, bool raw = false)
|
||||||
|
{
|
||||||
|
byte[] initData = CheckPSSH(initDataB64);
|
||||||
|
|
||||||
|
var device = Devices[deviceName];
|
||||||
|
|
||||||
|
byte[] sessionId = new byte[16];
|
||||||
|
|
||||||
|
if (device.IsAndroid)
|
||||||
|
{
|
||||||
|
string randHex = "";
|
||||||
|
|
||||||
|
Random rand = new Random();
|
||||||
|
string choice = "ABCDEF0123456789";
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
randHex += choice[rand.Next(16)];
|
||||||
|
|
||||||
|
string counter = "01";
|
||||||
|
string rest = "00000000000000";
|
||||||
|
sessionId = Encoding.ASCII.GetBytes(randHex + counter + rest);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Random rand = new Random();
|
||||||
|
rand.NextBytes(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session;
|
||||||
|
dynamic parsedInitData = ParseInitData(initData);
|
||||||
|
|
||||||
|
if (parsedInitData != null)
|
||||||
|
{
|
||||||
|
session = new Session(sessionId, parsedInitData, device, offline);
|
||||||
|
}
|
||||||
|
else if (raw)
|
||||||
|
{
|
||||||
|
session = new Session(sessionId, initData, device, offline);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sessions.Add(Utils.BytesToHex(sessionId), session);
|
||||||
|
|
||||||
|
return Utils.BytesToHex(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static WidevineCencHeader ParseInitData(byte[] initData)
|
||||||
|
{
|
||||||
|
WidevineCencHeader cencHeader;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cencHeader = Serializer.Deserialize<WidevineCencHeader>(new MemoryStream(initData[32..]));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//needed for HBO Max
|
||||||
|
|
||||||
|
PSSHBox psshBox = PSSHBox.FromByteArray(initData);
|
||||||
|
cencHeader = Serializer.Deserialize<WidevineCencHeader>(new MemoryStream(psshBox.Data));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
//Logger.Verbose("Unable to parse, unsupported init data format");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cencHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CloseSession(string sessionId)
|
||||||
|
{
|
||||||
|
//Logger.Debug($"CloseSession(session_id={Utils.BytesToHex(sessionId)})");
|
||||||
|
//Logger.Verbose("Closing CDM session");
|
||||||
|
|
||||||
|
if (Sessions.ContainsKey(sessionId))
|
||||||
|
{
|
||||||
|
Sessions.Remove(sessionId);
|
||||||
|
//Logger.Verbose("CDM session closed");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Logger.Info($"Session {sessionId} not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SetServiceCertificate(string sessionId, byte[] certData)
|
||||||
|
{
|
||||||
|
//Logger.Debug($"SetServiceCertificate(sessionId={Utils.BytesToHex(sessionId)}, cert={certB64})");
|
||||||
|
//Logger.Verbose($"Setting service certificate");
|
||||||
|
|
||||||
|
if (!Sessions.ContainsKey(sessionId))
|
||||||
|
{
|
||||||
|
//Logger.Error("Session ID doesn't exist");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignedMessage signedMessage = new SignedMessage();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
signedMessage = Serializer.Deserialize<SignedMessage>(new MemoryStream(certData));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
//Logger.Warn("Failed to parse cert as SignedMessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
SignedDeviceCertificate serviceCertificate;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//Logger.Debug("Service cert provided as signedmessage");
|
||||||
|
serviceCertificate = Serializer.Deserialize<SignedDeviceCertificate>(new MemoryStream(signedMessage.Msg));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
//Logger.Debug("Service cert provided as signeddevicecertificate");
|
||||||
|
serviceCertificate = Serializer.Deserialize<SignedDeviceCertificate>(new MemoryStream(certData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
//Logger.Error("Failed to parse service certificate");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sessions[sessionId].ServiceCertificate = serviceCertificate;
|
||||||
|
Sessions[sessionId].PrivacyMode = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] GetLicenseRequest(string sessionId)
|
||||||
|
{
|
||||||
|
//Logger.Debug($"GetLicenseRequest(sessionId={Utils.BytesToHex(sessionId)})");
|
||||||
|
//Logger.Verbose($"Getting license request");
|
||||||
|
|
||||||
|
if (!Sessions.ContainsKey(sessionId))
|
||||||
|
{
|
||||||
|
//Logger.Error("Session ID doesn't exist");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = Sessions[sessionId];
|
||||||
|
|
||||||
|
//Logger.Debug("Building license request");
|
||||||
|
|
||||||
|
dynamic licenseRequest;
|
||||||
|
|
||||||
|
if (session.InitData is WidevineCencHeader)
|
||||||
|
{
|
||||||
|
licenseRequest = new SignedLicenseRequest
|
||||||
|
{
|
||||||
|
Type = SignedLicenseRequest.MessageType.LicenseRequest,
|
||||||
|
Msg = new LicenseRequest
|
||||||
|
{
|
||||||
|
Type = LicenseRequest.RequestType.New,
|
||||||
|
KeyControlNonce = 1093602366,
|
||||||
|
ProtocolVersion = ProtocolVersion.Current,
|
||||||
|
ContentId = new LicenseRequest.ContentIdentification
|
||||||
|
{
|
||||||
|
CencId = new LicenseRequest.ContentIdentification.Cenc
|
||||||
|
{
|
||||||
|
LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default,
|
||||||
|
RequestId = session.SessionId,
|
||||||
|
Pssh = session.InitData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
licenseRequest = new SignedLicenseRequestRaw
|
||||||
|
{
|
||||||
|
Type = SignedLicenseRequestRaw.MessageType.LicenseRequest,
|
||||||
|
Msg = new LicenseRequestRaw
|
||||||
|
{
|
||||||
|
Type = LicenseRequestRaw.RequestType.New,
|
||||||
|
KeyControlNonce = 1093602366,
|
||||||
|
ProtocolVersion = ProtocolVersion.Current,
|
||||||
|
ContentId = new LicenseRequestRaw.ContentIdentification
|
||||||
|
{
|
||||||
|
CencId = new LicenseRequestRaw.ContentIdentification.Cenc
|
||||||
|
{
|
||||||
|
LicenseType = session.Offline ? LicenseType.Offline : LicenseType.Default,
|
||||||
|
RequestId = session.SessionId,
|
||||||
|
Pssh = session.InitData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.PrivacyMode)
|
||||||
|
{
|
||||||
|
//Logger.Debug("Privacy mode & serivce certificate loaded, encrypting client id");
|
||||||
|
|
||||||
|
EncryptedClientIdentification encryptedClientIdProto = new EncryptedClientIdentification();
|
||||||
|
|
||||||
|
//Logger.Debug("Unencrypted client id " + Utils.SerializeToString(clientId));
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
Serializer.Serialize(memoryStream, session.Device.ClientID);
|
||||||
|
byte[] data = Padding.AddPKCS7Padding(memoryStream.ToArray(), 16);
|
||||||
|
|
||||||
|
using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider
|
||||||
|
{
|
||||||
|
BlockSize = 128,
|
||||||
|
Padding = PaddingMode.PKCS7,
|
||||||
|
Mode = CipherMode.CBC
|
||||||
|
};
|
||||||
|
aesProvider.GenerateKey();
|
||||||
|
aesProvider.GenerateIV();
|
||||||
|
|
||||||
|
using MemoryStream mstream = new MemoryStream();
|
||||||
|
using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateEncryptor(aesProvider.Key, aesProvider.IV), CryptoStreamMode.Write);
|
||||||
|
cryptoStream.Write(data, 0, data.Length);
|
||||||
|
encryptedClientIdProto.EncryptedClientId = mstream.ToArray();
|
||||||
|
|
||||||
|
using RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
|
||||||
|
RSA.ImportRSAPublicKey(session.ServiceCertificate.DeviceCertificate.PublicKey, out int bytesRead);
|
||||||
|
encryptedClientIdProto.EncryptedPrivacyKey = RSA.Encrypt(aesProvider.Key, RSAEncryptionPadding.OaepSHA1);
|
||||||
|
encryptedClientIdProto.EncryptedClientIdIv = aesProvider.IV;
|
||||||
|
encryptedClientIdProto.ServiceId = Encoding.UTF8.GetString(session.ServiceCertificate.DeviceCertificate.ServiceId);
|
||||||
|
encryptedClientIdProto.ServiceCertificateSerialNumber = session.ServiceCertificate.DeviceCertificate.SerialNumber;
|
||||||
|
|
||||||
|
licenseRequest.Msg.EncryptedClientId = encryptedClientIdProto;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
licenseRequest.Msg.ClientId = session.Device.ClientID;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Logger.Debug("Signing license request");
|
||||||
|
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
Serializer.Serialize(memoryStream, licenseRequest.Msg);
|
||||||
|
byte[] data = memoryStream.ToArray();
|
||||||
|
session.LicenseRequest = data;
|
||||||
|
|
||||||
|
licenseRequest.Signature = session.Device.Sign(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Logger.Verbose("License request created");
|
||||||
|
|
||||||
|
byte[] requestBytes;
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
Serializer.Serialize(memoryStream, licenseRequest);
|
||||||
|
requestBytes = memoryStream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
Sessions[sessionId] = session;
|
||||||
|
|
||||||
|
//Logger.Debug($"license request b64: {Convert.ToBase64String(requestBytes)}");
|
||||||
|
return requestBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ProvideLicense(string sessionId, byte[] license)
|
||||||
|
{
|
||||||
|
//Logger.Debug($"ProvideLicense(sessionId={Utils.BytesToHex(sessionId)}, licenseB64={licenseB64})");
|
||||||
|
//Logger.Verbose("Decrypting provided license");
|
||||||
|
|
||||||
|
if (!Sessions.ContainsKey(sessionId))
|
||||||
|
{
|
||||||
|
throw new Exception("Session ID doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = Sessions[sessionId];
|
||||||
|
|
||||||
|
if (session.LicenseRequest == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Generate a license request first");
|
||||||
|
}
|
||||||
|
|
||||||
|
SignedLicense signedLicense;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
signedLicense = Serializer.Deserialize<SignedLicense>(new MemoryStream(license));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new Exception("Unable to parse license");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Logger.Debug("License: " + Utils.SerializeToString(signedLicense));
|
||||||
|
|
||||||
|
session.License = signedLicense;
|
||||||
|
|
||||||
|
//Logger.Debug($"Deriving keys from session key");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sessionKey = session.Device.Decrypt(session.License.SessionKey);
|
||||||
|
|
||||||
|
if (sessionKey.Length != 16)
|
||||||
|
{
|
||||||
|
throw new Exception("Unable to decrypt session key");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.SessionKey = sessionKey;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new Exception("Unable to decrypt session key");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Logger.Debug("Session key: " + Utils.BytesToHex(session.SessionKey));
|
||||||
|
|
||||||
|
session.DerivedKeys = DeriveKeys(session.LicenseRequest, session.SessionKey);
|
||||||
|
|
||||||
|
//Logger.Debug("Verifying license signature");
|
||||||
|
|
||||||
|
byte[] licenseBytes;
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
Serializer.Serialize(memoryStream, signedLicense.Msg);
|
||||||
|
licenseBytes = memoryStream.ToArray();
|
||||||
|
}
|
||||||
|
byte[] hmacHash = CryptoUtils.GetHMACSHA256Digest(licenseBytes, session.DerivedKeys.Auth1);
|
||||||
|
|
||||||
|
if (!hmacHash.SequenceEqual(signedLicense.Signature))
|
||||||
|
{
|
||||||
|
throw new Exception("License signature mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (License.KeyContainer key in signedLicense.Msg.Keys)
|
||||||
|
{
|
||||||
|
string type = key.Type.ToString();
|
||||||
|
|
||||||
|
if (type == "Signing")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] keyId;
|
||||||
|
byte[] encryptedKey = key.Key;
|
||||||
|
byte[] iv = key.Iv;
|
||||||
|
keyId = key.Id;
|
||||||
|
if (keyId == null)
|
||||||
|
{
|
||||||
|
keyId = Encoding.ASCII.GetBytes(key.Type.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] decryptedKey;
|
||||||
|
|
||||||
|
using MemoryStream mstream = new MemoryStream();
|
||||||
|
using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider
|
||||||
|
{
|
||||||
|
Mode = CipherMode.CBC,
|
||||||
|
Padding = PaddingMode.PKCS7
|
||||||
|
};
|
||||||
|
using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateDecryptor(session.DerivedKeys.Enc, iv), CryptoStreamMode.Write);
|
||||||
|
cryptoStream.Write(encryptedKey, 0, encryptedKey.Length);
|
||||||
|
decryptedKey = mstream.ToArray();
|
||||||
|
|
||||||
|
List<string> permissions = new List<string>();
|
||||||
|
if (type == "OperatorSession")
|
||||||
|
{
|
||||||
|
foreach (PropertyInfo perm in key._OperatorSessionKeyPermissions.GetType().GetProperties())
|
||||||
|
{
|
||||||
|
if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1)
|
||||||
|
{
|
||||||
|
permissions.Add(perm.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.ContentKeys.Add(new ContentKey
|
||||||
|
{
|
||||||
|
KeyID = keyId,
|
||||||
|
Type = type,
|
||||||
|
Bytes = decryptedKey,
|
||||||
|
Permissions = permissions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Logger.Debug($"Key count: {session.Keys.Count}");
|
||||||
|
|
||||||
|
Sessions[sessionId] = session;
|
||||||
|
|
||||||
|
//Logger.Verbose("Decrypted all keys");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DerivedKeys DeriveKeys(byte[] message, byte[] key)
|
||||||
|
{
|
||||||
|
byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0, }).Concat(message).Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray();
|
||||||
|
byte[] authKeyBase = Encoding.UTF8.GetBytes("AUTHENTICATION").Concat(new byte[] { 0x0, }).Concat(message).Concat(new byte[] { 0x0, 0x0, 0x2, 0x0 }).ToArray();
|
||||||
|
|
||||||
|
byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray();
|
||||||
|
byte[] authKey1 = new byte[] { 0x01 }.Concat(authKeyBase).ToArray();
|
||||||
|
byte[] authKey2 = new byte[] { 0x02 }.Concat(authKeyBase).ToArray();
|
||||||
|
byte[] authKey3 = new byte[] { 0x03 }.Concat(authKeyBase).ToArray();
|
||||||
|
byte[] authKey4 = new byte[] { 0x04 }.Concat(authKeyBase).ToArray();
|
||||||
|
|
||||||
|
byte[] encCmacKey = CryptoUtils.GetCMACDigest(encKey, key);
|
||||||
|
byte[] authCmacKey1 = CryptoUtils.GetCMACDigest(authKey1, key);
|
||||||
|
byte[] authCmacKey2 = CryptoUtils.GetCMACDigest(authKey2, key);
|
||||||
|
byte[] authCmacKey3 = CryptoUtils.GetCMACDigest(authKey3, key);
|
||||||
|
byte[] authCmacKey4 = CryptoUtils.GetCMACDigest(authKey4, key);
|
||||||
|
|
||||||
|
byte[] authCmacCombined1 = authCmacKey1.Concat(authCmacKey2).ToArray();
|
||||||
|
byte[] authCmacCombined2 = authCmacKey3.Concat(authCmacKey4).ToArray();
|
||||||
|
|
||||||
|
return new DerivedKeys
|
||||||
|
{
|
||||||
|
Auth1 = authCmacCombined1,
|
||||||
|
Auth2 = authCmacCombined2,
|
||||||
|
Enc = encCmacKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ContentKey> GetKeys(string sessionId)
|
||||||
|
{
|
||||||
|
if (Sessions.ContainsKey(sessionId))
|
||||||
|
return Sessions[sessionId].ContentKeys;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Session not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
public static List<string> ProvideLicense(string requestB64, string licenseB64)
|
||||||
|
{
|
||||||
|
byte[] licenseRequest;
|
||||||
|
|
||||||
|
var request = Serializer.Deserialize<SignedLicenseRequest>(new MemoryStream(Convert.FromBase64String(requestB64)));
|
||||||
|
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
Serializer.Serialize(ms, request.Msg);
|
||||||
|
licenseRequest = ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
SignedLicense signedLicense;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
signedLicense = Serializer.Deserialize<SignedLicense>(new MemoryStream(Convert.FromBase64String(licenseB64)));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] sessionKey;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
sessionKey = Controllers.Adapter.OaepDecrypt(Convert.ToBase64String(signedLicense.SessionKey));
|
||||||
|
|
||||||
|
if (sessionKey.Length != 16)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[] { 0x0, }).Concat(licenseRequest).Concat(new byte[] { 0x0, 0x0, 0x0, 0x80 }).ToArray();
|
||||||
|
|
||||||
|
byte[] encKey = new byte[] { 0x01 }.Concat(encKeyBase).ToArray();
|
||||||
|
|
||||||
|
byte[] encCmacKey = GetCmacDigest(encKey, sessionKey);
|
||||||
|
|
||||||
|
byte[] encryptionKey = encCmacKey;
|
||||||
|
|
||||||
|
List<string> keys = new List<string>();
|
||||||
|
|
||||||
|
foreach (License.KeyContainer key in signedLicense.Msg.Keys)
|
||||||
|
{
|
||||||
|
string type = key.Type.ToString();
|
||||||
|
if (type == "Signing")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] keyId;
|
||||||
|
byte[] encryptedKey = key.Key;
|
||||||
|
byte[] iv = key.Iv;
|
||||||
|
keyId = key.Id;
|
||||||
|
if (keyId == null)
|
||||||
|
{
|
||||||
|
keyId = Encoding.ASCII.GetBytes(key.Type.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] decryptedKey;
|
||||||
|
|
||||||
|
using MemoryStream mstream = new MemoryStream();
|
||||||
|
using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider
|
||||||
|
{
|
||||||
|
Mode = CipherMode.CBC,
|
||||||
|
Padding = PaddingMode.PKCS7
|
||||||
|
};
|
||||||
|
using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateDecryptor(encryptionKey, iv), CryptoStreamMode.Write);
|
||||||
|
cryptoStream.Write(encryptedKey, 0, encryptedKey.Length);
|
||||||
|
decryptedKey = mstream.ToArray();
|
||||||
|
|
||||||
|
List<string> permissions = new List<string>();
|
||||||
|
if (type == "OPERATOR_SESSION")
|
||||||
|
{
|
||||||
|
foreach (FieldInfo perm in key._OperatorSessionKeyPermissions.GetType().GetFields())
|
||||||
|
{
|
||||||
|
if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1)
|
||||||
|
{
|
||||||
|
permissions.Add(perm.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys.Add(BitConverter.ToString(keyId).Replace("-","").ToLower() + ":" + BitConverter.ToString(decryptedKey).Replace("-", "").ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}*/
|
93
OF DL/Widevine/CDMDevice.cs
Normal file
93
OF DL/Widevine/CDMDevice.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Digests;
|
||||||
|
using Org.BouncyCastle.Crypto.Encodings;
|
||||||
|
using Org.BouncyCastle.Crypto.Engines;
|
||||||
|
using Org.BouncyCastle.Crypto.Signers;
|
||||||
|
using Org.BouncyCastle.OpenSsl;
|
||||||
|
using ProtoBuf;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace WidevineClient.Widevine
|
||||||
|
{
|
||||||
|
public class CDMDevice
|
||||||
|
{
|
||||||
|
public string DeviceName { get; set; }
|
||||||
|
public ClientIdentification ClientID { get; set; }
|
||||||
|
AsymmetricCipherKeyPair DeviceKeys { get; set; }
|
||||||
|
|
||||||
|
public virtual bool IsAndroid { get; set; } = true;
|
||||||
|
|
||||||
|
public CDMDevice(string deviceName, byte[] clientIdBlobBytes = null, byte[] privateKeyBytes = null, byte[] vmpBytes = null)
|
||||||
|
{
|
||||||
|
DeviceName = deviceName;
|
||||||
|
|
||||||
|
string privateKeyPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_private_key");
|
||||||
|
string vmpPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_vmp_blob");
|
||||||
|
|
||||||
|
if (clientIdBlobBytes == null)
|
||||||
|
{
|
||||||
|
string clientIDPath = Path.Join(Constants.DEVICES_FOLDER, deviceName, "device_client_id_blob");
|
||||||
|
|
||||||
|
if (!File.Exists(clientIDPath))
|
||||||
|
throw new Exception("No client id blob found");
|
||||||
|
|
||||||
|
clientIdBlobBytes = File.ReadAllBytes(clientIDPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientID = Serializer.Deserialize<ClientIdentification>(new MemoryStream(clientIdBlobBytes));
|
||||||
|
|
||||||
|
if (privateKeyBytes != null)
|
||||||
|
{
|
||||||
|
using var reader = new StringReader(Encoding.UTF8.GetString(privateKeyBytes));
|
||||||
|
DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject();
|
||||||
|
}
|
||||||
|
else if (File.Exists(privateKeyPath))
|
||||||
|
{
|
||||||
|
using var reader = File.OpenText(privateKeyPath);
|
||||||
|
DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vmpBytes != null)
|
||||||
|
{
|
||||||
|
var vmp = Serializer.Deserialize<FileHashes>(new MemoryStream(vmpBytes));
|
||||||
|
ClientID.FileHashes = vmp;
|
||||||
|
}
|
||||||
|
else if (File.Exists(vmpPath))
|
||||||
|
{
|
||||||
|
var vmp = Serializer.Deserialize<FileHashes>(new MemoryStream(File.ReadAllBytes(vmpPath)));
|
||||||
|
ClientID.FileHashes = vmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual byte[] Decrypt(byte[] data)
|
||||||
|
{
|
||||||
|
OaepEncoding eng = new OaepEncoding(new RsaEngine());
|
||||||
|
eng.Init(false, DeviceKeys.Private);
|
||||||
|
|
||||||
|
int length = data.Length;
|
||||||
|
int blockSize = eng.GetInputBlockSize();
|
||||||
|
|
||||||
|
List<byte> plainText = new List<byte>();
|
||||||
|
|
||||||
|
for (int chunkPosition = 0; chunkPosition < length; chunkPosition += blockSize)
|
||||||
|
{
|
||||||
|
int chunkSize = Math.Min(blockSize, length - chunkPosition);
|
||||||
|
plainText.AddRange(eng.ProcessBlock(data, chunkPosition, chunkSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
return plainText.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual byte[] Sign(byte[] data)
|
||||||
|
{
|
||||||
|
PssSigner eng = new PssSigner(new RsaEngine(), new Sha1Digest());
|
||||||
|
|
||||||
|
eng.Init(true, DeviceKeys.Private);
|
||||||
|
eng.BlockUpdate(data, 0, data.Length);
|
||||||
|
return eng.GenerateSignature();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
OF DL/Widevine/Constants.cs
Normal file
9
OF DL/Widevine/Constants.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace WidevineClient.Widevine
|
||||||
|
{
|
||||||
|
public class Constants
|
||||||
|
{
|
||||||
|
public static string WORKING_FOLDER { get; set; } = System.IO.Path.GetFullPath(System.IO.Path.Join(System.IO.Directory.GetCurrentDirectory(), "cdm"));
|
||||||
|
public static string DEVICES_FOLDER { get; set; } = System.IO.Path.GetFullPath(System.IO.Path.Join(WORKING_FOLDER, "devices"));
|
||||||
|
public static string DEVICE_NAME { get; set; } = "chrome_1610";
|
||||||
|
}
|
||||||
|
}
|
42
OF DL/Widevine/ContentKey.cs
Normal file
42
OF DL/Widevine/ContentKey.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace WidevineClient.Widevine
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class ContentKey
|
||||||
|
{
|
||||||
|
[JsonPropertyName("key_id")]
|
||||||
|
public byte[] KeyID { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("bytes")]
|
||||||
|
public byte[] Bytes { get; set; }
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonPropertyName("permissions")]
|
||||||
|
public List<string> Permissions {
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return PermissionsString.Split(",").ToList();
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
PermissionsString = string.Join(",", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string PermissionsString { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{BitConverter.ToString(KeyID).Replace("-", "").ToLower()}:{BitConverter.ToString(Bytes).Replace("-", "").ToLower()}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
OF DL/Widevine/DerivedKeys.cs
Normal file
9
OF DL/Widevine/DerivedKeys.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace WidevineClient.Widevine
|
||||||
|
{
|
||||||
|
public class DerivedKeys
|
||||||
|
{
|
||||||
|
public byte[] Auth1 { get; set; }
|
||||||
|
public byte[] Auth2 { get; set; }
|
||||||
|
public byte[] Enc { get; set; }
|
||||||
|
}
|
||||||
|
}
|
63
OF DL/Widevine/PSSHBox.cs
Normal file
63
OF DL/Widevine/PSSHBox.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace WidevineClient.Widevine
|
||||||
|
{
|
||||||
|
class PSSHBox
|
||||||
|
{
|
||||||
|
static readonly byte[] PSSH_HEADER = new byte[] { 0x70, 0x73, 0x73, 0x68 };
|
||||||
|
|
||||||
|
public List<byte[]> KIDs { get; set; } = new List<byte[]>();
|
||||||
|
public byte[] Data { get; set; }
|
||||||
|
|
||||||
|
PSSHBox(List<byte[]> kids, byte[] data)
|
||||||
|
{
|
||||||
|
KIDs = kids;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PSSHBox FromByteArray(byte[] psshbox)
|
||||||
|
{
|
||||||
|
using var stream = new System.IO.MemoryStream(psshbox);
|
||||||
|
|
||||||
|
stream.Seek(4, System.IO.SeekOrigin.Current);
|
||||||
|
byte[] header = new byte[4];
|
||||||
|
stream.Read(header, 0, 4);
|
||||||
|
|
||||||
|
if (!header.SequenceEqual(PSSH_HEADER))
|
||||||
|
throw new Exception("Not a pssh box");
|
||||||
|
|
||||||
|
stream.Seek(20, System.IO.SeekOrigin.Current);
|
||||||
|
byte[] kidCountBytes = new byte[4];
|
||||||
|
stream.Read(kidCountBytes, 0, 4);
|
||||||
|
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(kidCountBytes);
|
||||||
|
uint kidCount = BitConverter.ToUInt32(kidCountBytes);
|
||||||
|
|
||||||
|
List<byte[]> kids = new List<byte[]>();
|
||||||
|
for (int i = 0; i < kidCount; i++)
|
||||||
|
{
|
||||||
|
byte[] kid = new byte[16];
|
||||||
|
stream.Read(kid);
|
||||||
|
kids.Add(kid);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] dataLengthBytes = new byte[4];
|
||||||
|
stream.Read(dataLengthBytes);
|
||||||
|
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(dataLengthBytes);
|
||||||
|
uint dataLength = BitConverter.ToUInt32(dataLengthBytes);
|
||||||
|
|
||||||
|
if (dataLength == 0)
|
||||||
|
return new PSSHBox(kids, null);
|
||||||
|
|
||||||
|
byte[] data = new byte[dataLength];
|
||||||
|
stream.Read(data);
|
||||||
|
|
||||||
|
return new PSSHBox(kids, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
OF DL/Widevine/Session.cs
Normal file
27
OF DL/Widevine/Session.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace WidevineClient.Widevine
|
||||||
|
{
|
||||||
|
class Session
|
||||||
|
{
|
||||||
|
public byte[] SessionId { get; set; }
|
||||||
|
public dynamic InitData { get; set; }
|
||||||
|
public bool Offline { get; set; }
|
||||||
|
public CDMDevice Device { get; set; }
|
||||||
|
public byte[] SessionKey { get; set; }
|
||||||
|
public DerivedKeys DerivedKeys { get; set; }
|
||||||
|
public byte[] LicenseRequest { get; set; }
|
||||||
|
public SignedLicense License { get; set; }
|
||||||
|
public SignedDeviceCertificate ServiceCertificate { get; set; }
|
||||||
|
public bool PrivacyMode { get; set; }
|
||||||
|
public List<ContentKey> ContentKeys { get; set; } = new List<ContentKey>();
|
||||||
|
|
||||||
|
public Session(byte[] sessionId, dynamic initData, CDMDevice device, bool offline)
|
||||||
|
{
|
||||||
|
SessionId = sessionId;
|
||||||
|
InitData = initData;
|
||||||
|
Offline = offline;
|
||||||
|
Device = device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2257
OF DL/Widevine/WvProto2.cs
Normal file
2257
OF DL/Widevine/WvProto2.cs
Normal file
File diff suppressed because it is too large
Load Diff
8
OF DL/rules.json
Normal file
8
OF DL/rules.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"app-token": "33d57ade8c02dbc5a333db99ff9ae26a",
|
||||||
|
"static_param": "RyY8GpixStP90t68HWIJ8Qzo745n0hy0",
|
||||||
|
"prefix": "30586",
|
||||||
|
"suffix": "67000213",
|
||||||
|
"checksum_constant": 521,
|
||||||
|
"checksum_indexes": [ 0, 2, 3, 7, 7, 8, 8, 10, 11, 13, 14, 16, 17, 17, 17, 19, 19, 20, 21, 21, 23, 23, 24, 24, 27, 27, 29, 30, 31, 34, 35, 39 ]
|
||||||
|
}
|
39
README.md
Normal file
39
README.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# PLEASE READ BEFORE DOWNLOADING
|
||||||
|
THIS TOOL CANNOT BYPASS PAYWALLS, IT CAN ONLY DOWNLOAD CONTENT YOU HAVE ACCESS TO, PLEASE DO NOT DOWNLOAD THIS TOOL THINKING YOU CAN BYPASS PAYING FOR THINGS!!!!!
|
||||||
|
|
||||||
|
# OF-DL
|
||||||
|
Scrape all the media from an OnlyFans account
|
||||||
|
|
||||||
|
Join the discord [here](https://discord.com/invite/6bUW8EJ53j)
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
Please refer to https://sim0n00ps.github.io/OF-DL/ for instructions on:
|
||||||
|
- Requirements
|
||||||
|
- Installing the Program
|
||||||
|
- Running the Program
|
||||||
|
- Config options
|
||||||
|
|
||||||
|
# Video Tutorial
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/1474e85a-30df-4cf0-abf1-2ed9433f61c3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Issues
|
||||||
|
If you have any questions or issues please raise them on here, all I ask is that you please look back through previous issues to see if your issue or question has been fixed/answered already, it gets annoying when people create issues without checking previous issues which usually leads to me or others repeating themselves, thank you.
|
||||||
|
|
||||||
|
# Disclaimers
|
||||||
|
This tool is not associated or involved with Onlyfans in any way.
|
||||||
|
I am not responsible for anything that happens to you or your account when using the tool.
|
||||||
|
|
||||||
|
# Donations
|
||||||
|
If you would like to donate then here is a link to my ko-fi page https://ko-fi.com/sim0n00ps. Donations are not required but are very much appreciated :)
|
25
docker/entrypoint.sh
Normal file
25
docker/entrypoint.sh
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
mkdir -p /config/cdm/devices/chrome_1610
|
||||||
|
mkdir -p /config/logs/
|
||||||
|
|
||||||
|
if [ ! -f /config/config.conf ] && [ ! -f /config/config.json ]; then
|
||||||
|
cp /default-config/config.conf /config/config.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f /config/rules.json ]; then
|
||||||
|
cp /default-config/rules.json /config/rules.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
supervisord -c /etc/supervisor/conf.d/supervisord.conf &
|
||||||
|
} &> /dev/null
|
||||||
|
|
||||||
|
# Wait for the 3 supervisor programs to start: X11 (Xvfb), X11vnc, and noVNC
|
||||||
|
NUM_RUNNING_SERVICES=$(supervisorctl -c /etc/supervisor/conf.d/supervisord.conf status | grep RUNNING | wc -l)
|
||||||
|
while [ $NUM_RUNNING_SERVICES != "3" ]; do
|
||||||
|
sleep 1
|
||||||
|
NUM_RUNNING_SERVICES=$(supervisorctl -c /etc/supervisor/conf.d/supervisord.conf status | grep RUNNING | wc -l)
|
||||||
|
done
|
||||||
|
|
||||||
|
/app/OF\ DL
|
25
docker/supervisord.conf
Normal file
25
docker/supervisord.conf
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[unix_http_server]
|
||||||
|
file=/tmp/supervisor.sock
|
||||||
|
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
logfile=/config/logs/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[rpcinterface:supervisor]
|
||||||
|
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||||
|
|
||||||
|
[supervisorctl]
|
||||||
|
serverurl=unix:///tmp/supervisor.sock
|
||||||
|
|
||||||
|
[program:X11]
|
||||||
|
command=Xvfb :0 -screen 0 "%(ENV_DISPLAY_WIDTH)s"x"%(ENV_DISPLAY_HEIGHT)s"x24
|
||||||
|
autorestart=true
|
||||||
|
|
||||||
|
[program:x11vnc]
|
||||||
|
command=/usr/bin/x11vnc
|
||||||
|
autorestart=true
|
||||||
|
|
||||||
|
[program:websockify]
|
||||||
|
command=websockify --web /usr/share/novnc 8080 localhost:5900
|
||||||
|
autorestart=true
|
20
docs/.gitignore
vendored
Normal file
20
docs/.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.docusaurus
|
||||||
|
.cache-loader
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
1
docs/.nvmrc
Normal file
1
docs/.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
20.16.0
|
41
docs/README.md
Normal file
41
docs/README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Website
|
||||||
|
|
||||||
|
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
Using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ USE_SSH=true yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Not using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
3
docs/babel.config.js
Normal file
3
docs/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||||
|
};
|
8
docs/docs/config/_category_.json
Normal file
8
docs/docs/config/_category_.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"label": "Configuration",
|
||||||
|
"position": 2,
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index",
|
||||||
|
"description": "Configuration options and information for OF-DL"
|
||||||
|
}
|
||||||
|
}
|
78
docs/docs/config/auth.md
Normal file
78
docs/docs/config/auth.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
|
||||||
|
## Current Method (versions >= 1.9.0)
|
||||||
|
|
||||||
|
OF DL allows you to log in to your OnlyFans account directly. This simplifies the authentication process significantly.
|
||||||
|
When prompted by the application, log into your OnlyFans account. Do not close the opened window, tab, or navigate away to another webpage.
|
||||||
|
The new window will close automatically when the authentication process has finished.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
Some users have reported that "Sign in with Google" has not been working with this authentication method.
|
||||||
|
If you use the Google sign-in option to log into your OnlyFans account, use one of the [legacy authentication methods](#legacy-methods) described below.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::info
|
||||||
|
|
||||||
|
If you are using docker, follow the special [authentication instructions documented](/docs/installation/docker) to authenticate OF-DL
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Legacy Methods
|
||||||
|
|
||||||
|
Legacy authentication methods involve creating/editing `auth.json` file yourself.
|
||||||
|
|
||||||
|
### Browser Extension
|
||||||
|
|
||||||
|
You can use a browser extension to help get the required info for the `auth.json` file. The extension supports Google Chrome and Firefox and can be found [here](https://github.com/whimsical-c4lic0/OF-DL-Auth-Helper/) (https://github.com/whimsical-c4lic0/OF-DL-Auth-Helper/).
|
||||||
|
|
||||||
|
### Manual Method
|
||||||
|
|
||||||
|
Open `auth.json` in a text editor of your choice. The default windows notepad is sufficient. When you open `auth.json` for the first time you should see something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"USER_ID": "",
|
||||||
|
"USER_AGENT": "",
|
||||||
|
"X_BC": "",
|
||||||
|
"COOKIE": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, log into OnlyFans, and press F12 to open the dev tools. In the filter box, type `api`, and open any page on OnlyFans (e.g. Messages). You should see some requests appear in the list within the network tab:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click on one of the requests you see in the list, and scroll down until you find the 'Request Headers' section.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You should be able to find the 3 fields we need, `Cookie`, `User-Agent` and `x-bc`.
|
||||||
|
|
||||||
|
Within Cookie you will find 2 bits of information that we need to copy, these being `sess` and `auth_id`.
|
||||||
|
|
||||||
|
So heading back to your text editor:
|
||||||
|
|
||||||
|
The value of `USER_ID` will be set to what `auth_id` is set to.
|
||||||
|
|
||||||
|
The value of `USER_AGENT` will be set to what the `User-Agent` is set to in the Request Headers.
|
||||||
|
|
||||||
|
The value of `X_BC` will be set to what the `X-Bc` is set to in the Request Headers.
|
||||||
|
|
||||||
|
The value of `COOKIE` will be set to `auth_id=YOUR AUTH_ID HERE; sess=YOUR SESS HERE;`, please make sure you copy the values from within the Cookie field found in the Request Headers section.
|
||||||
|
|
||||||
|
If you have done everything correct you should end up with something like this (this is all dummy info):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"USER_ID": "123456",
|
||||||
|
"USER_AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
||||||
|
"X_BC": "2a9b28a68e7c03a9f0d3b98c28d70e8105e1f1df",
|
||||||
|
"COOKIE": "auth_id=123456; sess=k3s9tnzdc8vt2h47ljxpmwqy5r;"
|
||||||
|
}
|
||||||
|
```
|
32
docs/docs/config/cdm.md
Normal file
32
docs/docs/config/cdm.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# CDM (optional, but recommended)
|
||||||
|
|
||||||
|
Without Widevine/CDM keys, OF DL uses the 3rd party website cdrm-project.org for decrypting DRM videos. With keys, OF DL directly communicates with OnlyFans. It is highly recommended to use keys, both in case the cdrm-project site is having issues (which occur frequently, in our experience) and it will result in faster download speeds, too. However, this is optional, as things will work as long as cdrm-project is functional.
|
||||||
|
|
||||||
|
Two files need to be generated, called `device_client_id_blob` and `device_private_key`. In your main OF DL folder (where you have `config.json` and `auth.json`), create a folder called `cdm` (if it does not already exist). Inside it, create a folder called `devices` and inside that, create a folder called `chrome_1610`. Finally, inside this last folder (`chrome_1610`), place the two key files. (Note that this folder name is a legacy name and OFDL does not actually use Chrome itself.)
|
||||||
|
|
||||||
|
## Manual Generation Method
|
||||||
|
|
||||||
|
You can find a tutorial on how to do this [here](https://forum.videohelp.com/threads/408031-Dumping-Your-own-L3-CDM-with-Android-Studio).
|
||||||
|
|
||||||
|
I have also made some [batch scripts](https://github.com/sim0n00ps/L3-Dumping) to run the commands included in the guide linked above that can save you some time and makes the process a little simpler.
|
||||||
|
|
||||||
|
## Discord Method
|
||||||
|
|
||||||
|
Generating these keys can be complicated, so the team (shout out to Masaki here) have set up a bot on the Discord server to help securely deliver these keys to users who need them. You can join the discord sever [here](https://discord.com/invite/6bUW8EJ53j)
|
||||||
|
|
||||||
|
After joining, visit the bot [here](https://discord.com/channels/1198332760947966094/1333835216313122887) (the pinned post in the `#ofdl` support forum)
|
||||||
|
|
||||||
|
## After install
|
||||||
|
|
||||||
|
Restart OF DL, and you should no longer see the yellow warning message about cdrm-project and instead see two green messages like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
device_client_id_blob located successfully!
|
||||||
|
device_private_key located successfully!
|
||||||
|
```
|
||||||
|
|
||||||
|
You are now independent of cdrm-project!
|
516
docs/docs/config/configuration.md
Normal file
516
docs/docs/config/configuration.md
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
The `config.conf` file contains all the options you can change, these options are listed below:
|
||||||
|
|
||||||
|
# Configuration - External Tools
|
||||||
|
|
||||||
|
## FFmpegPath
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `""`
|
||||||
|
|
||||||
|
Allowed values: Any valid path or `""`
|
||||||
|
|
||||||
|
Description: This is the path to the FFmpeg executable (`ffmpeg.exe` on Windows and `ffmpeg` on Linux/macOS).
|
||||||
|
If the path is not set then the program will try to find it in both the same directory as the OF-DL executable as well
|
||||||
|
as the PATH environment variable.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
If you are using a Windows path, you will need to escape the backslashes, e.g. `"C:\\ffmpeg\\bin\\ffmpeg.exe"`
|
||||||
|
For example, this is not valid: `"C:\some\path\ffmpeg.exe"`, but `"C:/some/path/ffmpeg.exe"` and `"C:\\some\\path\\ffmpeg.exe"` are both valid.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
# Configuration - Download Settings
|
||||||
|
|
||||||
|
## DownloadAvatarHeaderPhoto
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Avatar and header images will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadPaidPosts
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Paid posts will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadPosts
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Free posts will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadArchived
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Posts in the "Archived" tab will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadStreams
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Posts in the "Streams" tab will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadStories
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Stories on a user's profile will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadHighlights
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Highlights on a user's will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadMessages
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Free media within messages (including paid message previews) will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadPaidMessages
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Paid media within messages (excluding paid message previews) will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadImages
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Images will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadVideos
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Videos will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## DownloadAudios
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Audios will be downloaded if set to `true`
|
||||||
|
|
||||||
|
## IgnoreOwnMessages
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: By default (or when set to `false`), messages that were sent by yourself will be added to the metadata DB and any media which has been sent by yourself will be downloaded. If set to `true`, the program will not add messages sent by yourself to the metadata DB and will not download any media which has been sent by yourself.
|
||||||
|
|
||||||
|
## DownloadPostsIncrementally
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: If set to `true`, only new posts will be downloaded from the date of the last post that was downloaded based off what's in the `user_data.db` file.
|
||||||
|
If set to `false`, the default behaviour will apply, and all posts will be gathered and compared against the database to see if they need to be downloaded or not.
|
||||||
|
|
||||||
|
## BypassContentForCreatorsWhoNoLongerExist
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: When a creator no longer exists (their account has been deleted), most of their content will be inaccessible.
|
||||||
|
Purchased content, however, will still be accessible by downloading media usisng the "Download Purchased Tab" menu option
|
||||||
|
or with the [NonInteractiveModePurchasedTab](#noninteractivemodepurchasedtab) config option when downloading media in non-interactive mode.
|
||||||
|
|
||||||
|
## DownloadDuplicatedMedia
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: By default (or when set to `false`), the program will not download duplicated media. If set to `true`, duplicated media will be downloaded.
|
||||||
|
|
||||||
|
## SkipAds
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: Posts and messages that contain #ad or free trial links will be ignored if set to `true`
|
||||||
|
|
||||||
|
## DownloadPath
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `""`
|
||||||
|
|
||||||
|
Allowed values: Any valid path
|
||||||
|
|
||||||
|
Description: If left blank then content will be downloaded to `__user_data__/sites/OnlyFans/{username}`.
|
||||||
|
If you set the download path to `"S:/"`, then content will be downloaded to `S:/{username}`
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
If you are using a Windows path, you will need to escape the backslashes, e.g. `"C:\\Users\\user\\Downloads\\OnlyFans\\"`
|
||||||
|
Please make sure your path ends with a `/`
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## DownloadOnlySpecificDates
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: If set to `true`, posts will be downloaded based on the [DownloadDateSelection](#downloaddateselection) and [CustomDate](#customdate) config options.
|
||||||
|
If set to `false`, all posts will be downloaded.
|
||||||
|
|
||||||
|
## DownloadDateSelection
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `"before"`
|
||||||
|
|
||||||
|
Allowed values: `"before"`, `"after"`
|
||||||
|
|
||||||
|
Description: [DownloadOnlySpecificDates](#downloadonlyspecificdates) needs to be set to `true` for this to work. This will get all posts from before
|
||||||
|
the date if set to `"before"`, and all posts from the date you specify up until the current date if set to `"after"`.
|
||||||
|
The date you specify will be in the [CustomDate](#customdate) config option.
|
||||||
|
|
||||||
|
## CustomDate
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `null`
|
||||||
|
|
||||||
|
Allowed values: Any date in `yyyy-mm-dd` format or `null`
|
||||||
|
|
||||||
|
Description: [DownloadOnlySpecificDates](#downloadonlyspecificdates) needs to be set to `true` for this to work.
|
||||||
|
This date will be used when you are trying to download between/after a certain date. See [DownloadOnlySpecificDates](#downloadonlyspecificdates) and
|
||||||
|
[DownloadDateSelection](#downloaddateselection) for more information.
|
||||||
|
|
||||||
|
# Configuration - File Settings
|
||||||
|
|
||||||
|
## PaidPostFileNameFormat
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `""`
|
||||||
|
|
||||||
|
Allowed values: Any valid string
|
||||||
|
|
||||||
|
Description: Please refer to [custom filename formats](/docs/config/custom-filename-formats#paidpostfilenameformat) page to see what fields you can use.
|
||||||
|
|
||||||
|
## PostFileNameFormat
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `""`
|
||||||
|
|
||||||
|
Allowed values: Any valid string
|
||||||
|
|
||||||
|
Description: Please refer to the [custom filename formats](/docs/config/custom-filename-formats#postfilenameformat) page to see what fields you can use.
|
||||||
|
|
||||||
|
## PaidMessageFileNameFormat
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `""`
|
||||||
|
|
||||||
|
Allowed values: Any valid string
|
||||||
|
|
||||||
|
Description: Please refer to [custom filename formats](/docs/config/custom-filename-formats#paidmessagefilenameformat) page to see what fields you can use.
|
||||||
|
|
||||||
|
## MessageFileNameFormat
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `""`
|
||||||
|
|
||||||
|
Allowed values: Any valid string
|
||||||
|
|
||||||
|
Description: Please refer to [custom filename formats](/docs/config/custom-filename-formats#messagefilenameformat) page to see what fields you can use.
|
||||||
|
|
||||||
|
## RenameExistingFilesWhenCustomFormatIsSelected
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: When `true`, any current files downloaded will have the current format applied to them.
|
||||||
|
When `false`, only new files will have the current format applied to them.
|
||||||
|
|
||||||
|
# Configuration - Creator-Specific Configurations
|
||||||
|
|
||||||
|
## CreatorConfigs
|
||||||
|
|
||||||
|
Type: `object`
|
||||||
|
|
||||||
|
Default: `{}`
|
||||||
|
|
||||||
|
Allowed values: An array of Creator Config objects
|
||||||
|
|
||||||
|
Description: This configuration options allows you to set file name formats for specific creators.
|
||||||
|
This is useful if you want to have different file name formats for different creators. The values set here will override the global values set in the config file
|
||||||
|
(see [PaidPostFileNameFormat](#paidpostfilenameformat), [PostFileNameFormat](#postfilenameformat),
|
||||||
|
[PaidMessageFileNAmeFormat](#paidmessagefilenameformat), and [MessageFileNameFormat](#messagefilenameformat)).
|
||||||
|
For more information on the file name formats, see the [custom filename formats](/docs/config/custom-filename-formats) page.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
"CreatorConfigs": {
|
||||||
|
"creator_one": {
|
||||||
|
"PaidPostFileNameFormat": "{id}_{mediaid}_{filename}",
|
||||||
|
"PostFileNameFormat": "{username}_{id}_{mediaid}_{mediaCreatedAt}",
|
||||||
|
"PaidMessageFileNameFormat": "{id}_{mediaid}_{createdAt}",
|
||||||
|
"MessageFileNameFormat": "{id}_{mediaid}_{filename}"
|
||||||
|
},
|
||||||
|
"creator_two": {
|
||||||
|
"PaidPostFileNameFormat": "{id}_{mediaid}",
|
||||||
|
"PostFileNameFormat": "{username}_{id}_{mediaid}",
|
||||||
|
"PaidMessageFileNameFormat": "{id}_{mediaid}",
|
||||||
|
"MessageFileNameFormat": "{id}_{mediaid}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Configuration - Folder Settings
|
||||||
|
|
||||||
|
## FolderPerPaidPost
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: A folder will be created for each paid post (containing all the media for that post) if set to `true`.
|
||||||
|
When set to `false`, paid post media will be downloaded into the `Posts/Paid` folder.
|
||||||
|
|
||||||
|
## FolderPerPost
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: A folder will be created for each post (containing all the media for that post) if set to `true`.
|
||||||
|
When set to `false`, post media will be downloaded into the `Posts/Free` folder.
|
||||||
|
|
||||||
|
## FolderPerPaidMessage
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: A folder will be created for each paid message (containing all the media for that message) if set to `true`.
|
||||||
|
When set to `false`, paid message media will be downloaded into the `Messages/Paid` folder.
|
||||||
|
|
||||||
|
## FolderPerMessage
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: A folder will be created for each message (containing all the media for that message) if set to `true`.
|
||||||
|
When set to `false`, message media will be downloaded into the `Messages/Free` folder.
|
||||||
|
|
||||||
|
# Configuration - Subscription Settings
|
||||||
|
|
||||||
|
## IncludeExpiredSubscriptions
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: If set to `true`, expired subscriptions will appear in the user list under the "Custom" menu option.
|
||||||
|
|
||||||
|
## IncludeRestrictedSubscriptions
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: If set to `true`, media from restricted creators will be downloaded. If set to `false`, restricted creators will be ignored.
|
||||||
|
|
||||||
|
## IgnoredUsersListName
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `""`
|
||||||
|
|
||||||
|
Allowed values: The name of a list of users you have created on OnlyFans or `""`
|
||||||
|
|
||||||
|
Description: When set to the name of a list, users in the list will be ignored when scraping content.
|
||||||
|
If set to `""` (or an invalid list name), no users will be ignored when scraping content.
|
||||||
|
|
||||||
|
# Configuration - Interaction Settings
|
||||||
|
|
||||||
|
## NonInteractiveMode
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: If set to `true`, the program will run without any input from the user. It will scrape all users automatically
|
||||||
|
(unless [NonInteractiveModeListName](#noninteractivemodelistname) or [NonInteractiveModePurchasedTab](#noninteractivemodepurchasedtab) are configured).
|
||||||
|
If set to `false`, the default behaviour will apply, and you will be able to choose an option from the menu.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
If NonInteractiveMode is enabled, you will be unable to authenticate OF-DL using the standard authentication method.
|
||||||
|
Before you can run OF-DL in NonInteractiveMode, you must either
|
||||||
|
|
||||||
|
1. Generate an auth.json file by running OF-DL with NonInteractiveMode disabled and authenticating OF-DL using the standard method **OR**
|
||||||
|
2. Generate an auth.json file by using a [legacy authentication method](/docs/config/auth#legacy-methods)
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## NonInteractiveModeListName
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `""`
|
||||||
|
|
||||||
|
Allowed values: The name of a list of users you have created on OnlyFans or `""`
|
||||||
|
|
||||||
|
Description: When set to the name of a list, non-interactive mode will download media from the list of users instead of all
|
||||||
|
users (when [NonInteractiveMode](#noninteractivemode) is set to `true`). If set to `""`, all users will be scraped
|
||||||
|
(unless [NonInteractiveModePurchasedTab](#noninteractivemodepurchasedtab) is configured).
|
||||||
|
|
||||||
|
## NonInteractiveModePurchasedTab
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: When set to `true`, non-interactive mode will only download content from the Purchased tab
|
||||||
|
(when [NonInteractiveMode](#noninteractivemode) is set to `true`). If set to `false`, all users will be scraped
|
||||||
|
(unless [NonInteractiveModeListName](#noninteractivemodelistname) is configured).
|
||||||
|
|
||||||
|
# Configuration - Performance Settings
|
||||||
|
|
||||||
|
## Timeout
|
||||||
|
|
||||||
|
Type: `integer`
|
||||||
|
|
||||||
|
Default: `-1`
|
||||||
|
|
||||||
|
Allowed values: Any positive integer or `-1`
|
||||||
|
|
||||||
|
Description: You won't need to set this, but if you see errors about the configured timeout of 100 seconds elapsing then
|
||||||
|
you could set this to be more than 100. It is recommended that you leave this as the default value.
|
||||||
|
|
||||||
|
## LimitDownloadRate
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Allowed values: `true`, `false`
|
||||||
|
|
||||||
|
Description: If set to `true`, the download rate will be limited to the value set in [DownloadLimitInMbPerSec](#downloadlimitinmbpersec).
|
||||||
|
|
||||||
|
## DownloadLimitInMbPerSec
|
||||||
|
|
||||||
|
Type: `integer`
|
||||||
|
|
||||||
|
Default: `4`
|
||||||
|
|
||||||
|
Allowed values: Any positive integer
|
||||||
|
|
||||||
|
Description: The download rate in MB per second. This will only be used if [LimitDownloadRate](#limitdownloadrate) is set to `true`.
|
||||||
|
|
||||||
|
# Configuration - Logging/Debug Settings
|
||||||
|
|
||||||
|
## LoggingLevel
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `"Error"`
|
||||||
|
|
||||||
|
Allowed values: `"Verbose"`, `"Debug"`, `"Information"`, `"Warning"`, `"Error"`, `"Fatal"`
|
||||||
|
|
||||||
|
Description: The level of logging that will be saved to the log files in the `logs` folder.
|
||||||
|
When requesting help with an issue, it is recommended to set this to `"Verbose"` and provide the log file.
|
94
docs/docs/config/custom-filename-formats.md
Normal file
94
docs/docs/config/custom-filename-formats.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Custom Filename Formats
|
||||||
|
|
||||||
|
In the config.conf file you can now specify some custom filename formats that will be used when downloading files. I have had to add 4 new fields to the auth.json file, these are:
|
||||||
|
|
||||||
|
- PaidPostFileNameFormat
|
||||||
|
- PostFileNameFormat
|
||||||
|
- PaidMessageFileNameFormat
|
||||||
|
- MessageFileNameFormat
|
||||||
|
|
||||||
|
I have had to do it this way as the names of fields from the API responses are different in some places
|
||||||
|
so it would become a mess using 1 file format for everything, besides having separate formats can be useful if you only
|
||||||
|
want posts to have a custom format and the rest just use the default filename.
|
||||||
|
|
||||||
|
Below are the names of the fields you can use in each format:
|
||||||
|
|
||||||
|
## PaidPostFileNameFormat
|
||||||
|
|
||||||
|
`id` - Id of the post
|
||||||
|
|
||||||
|
`postedAt` - The date when the post was made yyyy-mm-dd
|
||||||
|
|
||||||
|
`mediaId` - Id of the media
|
||||||
|
|
||||||
|
`mediaCreatedAt` - The date when the media was uploaded to OnlyFans yyyy-mm-dd
|
||||||
|
|
||||||
|
`filename` - The original filename e.g 0gy8cmw5jjjs5pt487b9g_source.mp4 or 914x1706_6b211f68a4e315125ecf70137bb75d8e.jpg
|
||||||
|
|
||||||
|
`username` - The username of the creator e.g onlyfans
|
||||||
|
|
||||||
|
`text` - The text of the post
|
||||||
|
|
||||||
|
## PostFileNameFormat
|
||||||
|
|
||||||
|
`id` - Id of the post
|
||||||
|
|
||||||
|
`postedAt` - The date when the post was made yyyy-mm-dd
|
||||||
|
|
||||||
|
`mediaId` - Id of the media
|
||||||
|
|
||||||
|
`mediaCreatedAt` - The date when the media was uploaded to OnlyFans yyyy-mm-dd
|
||||||
|
|
||||||
|
`filename` - The original filename e.g 0gy8cmw5jjjs5pt487b9g_source.mp4 or 914x1706_6b211f68a4e315125ecf70137bb75d8e.jpg
|
||||||
|
|
||||||
|
`username` - The username of the creator e.g onlyfans
|
||||||
|
|
||||||
|
`text` - The text of the post
|
||||||
|
|
||||||
|
`rawText` - The text of the post
|
||||||
|
|
||||||
|
## PaidMessageFileNameFormat
|
||||||
|
|
||||||
|
`id` - Id of the message
|
||||||
|
|
||||||
|
`createdAt` - The date when the message was sent yyyy-mm-dd
|
||||||
|
|
||||||
|
`mediaId` - Id of the media
|
||||||
|
|
||||||
|
`mediaCreatedAt` - The date when the media was uploaded to OnlyFans yyyy-mm-dd
|
||||||
|
|
||||||
|
`filename` - The original filename e.g 0gy8cmw5jjjs5pt487b9g_source.mp4 or 914x1706_6b211f68a4e315125ecf70137bb75d8e.jpg
|
||||||
|
|
||||||
|
`username` - The username of the creator e.g onlyfans
|
||||||
|
|
||||||
|
`text` - The text of the message
|
||||||
|
|
||||||
|
## MessageFileNameFormat
|
||||||
|
|
||||||
|
`id` - Id of the message
|
||||||
|
|
||||||
|
`createdAt` - The date when the message was sent yyyy-mm-dd
|
||||||
|
|
||||||
|
`mediaId` - Id of the media
|
||||||
|
|
||||||
|
`mediaCreatedAt` - The date when the media was uploaded to OnlyFans yyyy-mm-dd
|
||||||
|
|
||||||
|
`filename` - The original filename e.g 0gy8cmw5jjjs5pt487b9g_source.mp4 or 914x1706_6b211f68a4e315125ecf70137bb75d8e.jpg
|
||||||
|
|
||||||
|
`username` - The username of the creator e.g onlyfans
|
||||||
|
|
||||||
|
`text` - The text of the message
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
`"PaidPostFileNameFormat": "{id}_{mediaid}_{filename}"`
|
||||||
|
|
||||||
|
`"PostFileNameFormat": "{username}_{id}_{mediaid}_{mediaCreatedAt}"`
|
||||||
|
|
||||||
|
`"PaidMessageFileNameFormat": "{id}_{mediaid}_{createdAt}"`
|
||||||
|
|
||||||
|
`"MessageFileNameFormat": "{id}_{mediaid}_{filename}"`
|
8
docs/docs/installation/_category_.json
Normal file
8
docs/docs/installation/_category_.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"label": "Installation",
|
||||||
|
"position": 1,
|
||||||
|
"link": {
|
||||||
|
"type": "generated-index",
|
||||||
|
"description": "Installation instructions for OF-DL"
|
||||||
|
}
|
||||||
|
}
|
48
docs/docs/installation/docker.md
Normal file
48
docs/docs/installation/docker.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
|
||||||
|
## Running OF-DL
|
||||||
|
|
||||||
|
To run OF-DL in a docker container, follow these steps:
|
||||||
|
|
||||||
|
1. Install Docker Desktop (Windows, macOS) or Docker Engine (Linux) and launch it
|
||||||
|
2. Open your terminal application of choice (macOS Terminal, GNOME Terminal, etc.)
|
||||||
|
3. Create two directories, one called `config` and one called `data`.
|
||||||
|
- An example might be:
|
||||||
|
```bash
|
||||||
|
mkdir -p $HOME/ofdl/config $HOME/ofdl/data
|
||||||
|
```
|
||||||
|
Adjust `$HOME/ofdl` as desired (including in the commands below) if you want the files stored elsewhere.
|
||||||
|
4. Run the following command to start the docker container:
|
||||||
|
```bash
|
||||||
|
docker run --rm -it -v $HOME/ofdl/data/:/data -v $HOME/ofdl/config/:/config -p 8080:8080 ghcr.io/sim0n00ps/of-dl:latest
|
||||||
|
```
|
||||||
|
If `config.json` and/or `rules.json` don't exist in the `config` directory, files with default values will be created when you run the docker container.
|
||||||
|
If you have your own Widevine keys, those files should be placed under `$HOME/ofdl/config/cdm/devices/chrome_1610/`.
|
||||||
|
5. OF-DL needs to be authenticated with your OnlyFans account. When prompted, open [http://localhost:8080](http://localhost:8080) in a web browser to log in to your OnlyFans account.
|
||||||
|
|
||||||
|
## Updating OF-DL
|
||||||
|
|
||||||
|
When a new version of OF-DL is released, you can download the latest docker image by executing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/sim0n00ps/of-dl:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then run the new version of OF-DL by executing the `docker run` command in the [Running OF-DL](#running-of-dl) section above.
|
||||||
|
|
||||||
|
## Building the Docker Image (Optional)
|
||||||
|
|
||||||
|
Since official docker images are provided for OF-DL through GitHub Container Registry (ghcr.io), you do not need to build the docker image yourself.
|
||||||
|
If you would like to build the docker image yourself, however, start by cloning the OF-DL repository and opening a terminal in the root directory of the repository.
|
||||||
|
Then, execute the following command while replacing `x.x.x` with the current version of OF-DL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VERSION="x.x.x" docker build --build-arg VERSION=$VERSION -t of-dl .
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then run a container using the image you just built by executing the `docker run` command in the
|
||||||
|
[Running OF-DL](#running-of-dl) section above while replacing `ghcr.io/sim0n00ps/of-dl:latest` with `of-dl`.
|
48
docs/docs/installation/linux.md
Normal file
48
docs/docs/installation/linux.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
|
||||||
|
A Linux release of OF-DL is not available at this time, however you can run OF-DL on Linux using Docker.
|
||||||
|
Please refer to the [Docker](/docs/installation/docker) page for instructions on how to run OF-DL in a Docker container.
|
||||||
|
If you do not have Docker installed, you can download it from [here](https://docs.docker.com/desktop/install/linux-install/).
|
||||||
|
If you would like to run OF-DL natively on Linux, you can build it from source by following the instructions below.
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
- Install the libicu library
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install libicu-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- Install .NET version 8
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://dot.net/v1/dotnet-install.sh
|
||||||
|
sudo bash dotnet-install.sh --architecture x64 --install-dir /usr/share/dotnet/ --runtime dotnet --version 8.0.7
|
||||||
|
```
|
||||||
|
|
||||||
|
- Clone the repo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/sim0n00ps/OF-DL.git
|
||||||
|
cd 'OF-DL'
|
||||||
|
```
|
||||||
|
|
||||||
|
- Build the project. Replace `%VERSION%` with the current version number of OF-DL (e.g. `1.7.68`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet publish -p:Version=%VERSION% -c Release
|
||||||
|
cd 'OF DL/bin/Release/net8.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
- Download the windows release as described on [here](/docs/installation/windows#installation).
|
||||||
|
- Add the `config.json` and `rules.json` files as well as the `cdm` folder to the `OF DL/bin/Release/net8.0` folder.
|
||||||
|
|
||||||
|
- Run the application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ./'OF DL'
|
||||||
|
```
|
9
docs/docs/installation/macos.md
Normal file
9
docs/docs/installation/macos.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
|
||||||
|
macOS releases of OF-DL are not available at this time, however you can run OF-DL on macOS using Docker.
|
||||||
|
Please refer to the [Docker](/docs/installation/docker) page for instructions on how to run OF-DL in a Docker container.
|
||||||
|
If you do not have Docker installed, you can download it from [here](https://docs.docker.com/desktop/install/mac-install/).
|
26
docs/docs/installation/windows.md
Normal file
26
docs/docs/installation/windows.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### FFmpeg
|
||||||
|
|
||||||
|
You will need to download FFmpeg. You can download it from [here](https://www.gyan.dev/ffmpeg/builds/).
|
||||||
|
Make sure you download `ffmpeg-release-essentials.zip`. Unzip it anywhere on your computer. You only need `ffmpeg.exe`, and you can ignore the rest.
|
||||||
|
Move `ffmpeg.exe` to the same folder as `OF DL.exe` (downloaded in the installation steps below). If you choose to move `ffmpeg.exe` to a different folder,
|
||||||
|
you will need to specify the path to `ffmpeg.exe` in the config file (see the `FFmpegPath` [config option](/docs/config/configuration#ffmpegpath)).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Navigate to the OF-DL [releases page](https://github.com/sim0n00ps/OF-DL/releases), and download the latest release zip file. The zip file will be named `OFDLVx.x.x.zip` where `x.x.x` is the version number.
|
||||||
|
2. Unzip the downloaded file. The destination folder can be anywhere on your computer, preferably somewhere where you want to download content to/already have content downloaded.
|
||||||
|
3. Your folder should contain a folder named `cdm` as well as the following files:
|
||||||
|
- OF DL.exe
|
||||||
|
- config.json
|
||||||
|
- rules.json
|
||||||
|
- e_sqlite3.dll
|
||||||
|
- ffmpeg.exe
|
||||||
|
4. Once you have done this, run OF DL.exe
|
54
docs/docs/running-the-program.md
Normal file
54
docs/docs/running-the-program.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Running the Program
|
||||||
|
|
||||||
|
Once you are happy you have filled everything in [auth.json](/docs/config/auth) correctly, you can double click OF-DL.exe and you should see a command prompt window appear, it should look something like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
It should locate `config.json`, `rules.json` and FFmpeg successfully. If anything doesn't get located
|
||||||
|
successfully, then make sure the files exist or the path is correct.
|
||||||
|
|
||||||
|
OF-DL will open a new window, if needed, to allow you to log into your OnlyFans account. The window will automatically close once
|
||||||
|
the authorization process has finished. If the auth info is correct then you should see a message in green text
|
||||||
|
`Logged In successfully as {Your Username} {Your User Id}`. However, if the authorization has failed,
|
||||||
|
then a message in red text will appear `Auth failed, please check the values in auth.json are correct, press any key to exit.`
|
||||||
|
This means you need to go back and fill in the `auth.json` file again, this will usually indicate that your `user-agent` has changed or you need to re-copy your `sess` value.
|
||||||
|
|
||||||
|
If you're logged in successfully then you will be greeted with a selection prompt. To navigate the menu the can use the ↑ & ↓ arrows and press `enter` to choose that option.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The `Select All` option will go through every account you are currently subscribed to and grab all of the media from the users.
|
||||||
|
|
||||||
|
The `List` option will show you all the lists you have created on OnlyFans and you can then select 1 or more lists to download the content of the users within those lists.
|
||||||
|
|
||||||
|
The `Custom` option allows you to select 1 or more accounts you want to scrape media from so if you only want to get media from a select number of accounts then you can do that.
|
||||||
|
To navigate the menu the can use the ↑ & ↓ arrows. You can also press keys A-Z on the keyboard whilst in the menu to easily navigate the menu and for example
|
||||||
|
pressing the letter 'c' on the keyboard will highlight the first user in the list whose username starts with the letter 'c'. To select/deselect an account,
|
||||||
|
press the space key, and after you are happy with your selection(s), press the enter key to start downloading.
|
||||||
|
|
||||||
|
The `Download Single Post` allows you to download a post from a URL, to get this URL go to any post and press the 3 dots, Copy link to post.
|
||||||
|
|
||||||
|
The `Download Single Message` allows you to download a message from a URL, to get this URL go to any message in the **purchased tab** and press the 3 dots, Copy link to message.
|
||||||
|
|
||||||
|
The `Download Purchased Tab` option will download all the media from the purchased tab in OnlyFans.
|
||||||
|
|
||||||
|
The `Edit config.json` option allows you to change the config from within the program.
|
||||||
|
|
||||||
|
The `Change logging level` option allows you to change the logging level that the program uses when writing logs to files in the `logs` folder.
|
||||||
|
|
||||||
|
The `Logout and Exit` option allows you to remove your authentication from OF-DL. This is useful if you use multiple OnlyFans accounts.
|
||||||
|
|
||||||
|
After you have made your selection the content should start downloading. Content is downloaded in this order:
|
||||||
|
|
||||||
|
1. Paid Posts
|
||||||
|
2. Posts
|
||||||
|
3. Archived
|
||||||
|
4. Streams
|
||||||
|
5. Stories
|
||||||
|
6. Highlights
|
||||||
|
7. Messages
|
||||||
|
8. Paid Messages
|
123
docs/docusaurus.config.js
Normal file
123
docs/docusaurus.config.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// @ts-check
|
||||||
|
// `@type` JSDoc annotations allow editor autocompletion and type checking
|
||||||
|
// (when paired with `@ts-check`).
|
||||||
|
// There are various equivalent ways to declare your Docusaurus config.
|
||||||
|
// See: https://docusaurus.io/docs/api/docusaurus-config
|
||||||
|
|
||||||
|
import {themes as prismThemes} from 'prism-react-renderer';
|
||||||
|
|
||||||
|
/** @type {import('@docusaurus/types').Config} */
|
||||||
|
const config = {
|
||||||
|
title: 'OF-DL',
|
||||||
|
tagline: 'A media scraper for OnlyFans with DRM video support',
|
||||||
|
favicon: 'img/logo.png',
|
||||||
|
|
||||||
|
// Set the production url of your site here
|
||||||
|
url: 'https://sim0n00ps.github.io',
|
||||||
|
// Set the /<baseUrl>/ pathname under which your site is served
|
||||||
|
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||||
|
baseUrl: '/OF-DL/',
|
||||||
|
|
||||||
|
// GitHub pages deployment config.
|
||||||
|
// If you aren't using GitHub pages, you don't need these.
|
||||||
|
organizationName: 'sim0n00ps', // Usually your GitHub org/user name.
|
||||||
|
projectName: 'OF-DL', // Usually your repo name.
|
||||||
|
|
||||||
|
onBrokenLinks: 'throw',
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
|
||||||
|
// Even if you don't use internationalization, you can use this field to set
|
||||||
|
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||||
|
// may want to replace "en" with "zh-Hans".
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en'],
|
||||||
|
},
|
||||||
|
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'@docusaurus/preset-classic',
|
||||||
|
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||||
|
({
|
||||||
|
docs: {
|
||||||
|
sidebarPath: './sidebars.js',
|
||||||
|
},
|
||||||
|
blog: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
themeConfig:
|
||||||
|
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||||
|
({
|
||||||
|
colorMode: {
|
||||||
|
respectPrefersColorScheme: true,
|
||||||
|
},
|
||||||
|
navbar: {
|
||||||
|
title: 'OF-DL',
|
||||||
|
logo: {
|
||||||
|
alt: 'OF-DL Logo',
|
||||||
|
src: 'img/logo.png',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'docSidebar',
|
||||||
|
sidebarId: 'generatedSidebar',
|
||||||
|
position: 'left',
|
||||||
|
label: 'Docs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://github.com/sim0n00ps/OF-DL',
|
||||||
|
label: 'GitHub',
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
style: 'dark',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'Docs',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Installation',
|
||||||
|
to: '/docs/installation/windows',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Configuration',
|
||||||
|
to: '/docs/config/auth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Running the Program',
|
||||||
|
to: '/docs/running-the-program',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Community',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Discord',
|
||||||
|
href: 'https://discord.com/invite/6bUW8EJ53j',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'More',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'GitHub',
|
||||||
|
href: 'https://github.com/sim0n00ps/OF-DL',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
prism: {
|
||||||
|
theme: prismThemes.github,
|
||||||
|
darkTheme: prismThemes.dracula,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
15550
docs/package-lock.json
generated
Normal file
15550
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
docs/package.json
Normal file
44
docs/package.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "of-dl",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"docusaurus": "docusaurus",
|
||||||
|
"start": "docusaurus start",
|
||||||
|
"build": "docusaurus build",
|
||||||
|
"swizzle": "docusaurus swizzle",
|
||||||
|
"deploy": "docusaurus deploy",
|
||||||
|
"clear": "docusaurus clear",
|
||||||
|
"serve": "docusaurus serve",
|
||||||
|
"write-translations": "docusaurus write-translations",
|
||||||
|
"write-heading-ids": "docusaurus write-heading-ids"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/core": "3.4.0",
|
||||||
|
"@docusaurus/preset-classic": "3.4.0",
|
||||||
|
"@mdx-js/react": "^3.0.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"prism-react-renderer": "^2.3.0",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@docusaurus/module-type-aliases": "3.4.0",
|
||||||
|
"@docusaurus/types": "3.4.0"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.5%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 3 chrome version",
|
||||||
|
"last 3 firefox version",
|
||||||
|
"last 5 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0"
|
||||||
|
}
|
||||||
|
}
|
33
docs/sidebars.js
Normal file
33
docs/sidebars.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Creating a sidebar enables you to:
|
||||||
|
- create an ordered group of docs
|
||||||
|
- render a sidebar for each doc of that group
|
||||||
|
- provide next/previous navigation
|
||||||
|
|
||||||
|
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||||
|
|
||||||
|
Create as many sidebars as you want.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||||
|
const sidebars = {
|
||||||
|
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||||
|
generatedSidebar: [{type: 'autogenerated', dirName: '.'}],
|
||||||
|
|
||||||
|
// But you can create a sidebar manually
|
||||||
|
/*
|
||||||
|
tutorialSidebar: [
|
||||||
|
'intro',
|
||||||
|
'hello',
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Tutorial',
|
||||||
|
items: ['tutorial-basics/create-a-document'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sidebars;
|
39
docs/src/pages/index.js
Normal file
39
docs/src/pages/index.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
import Layout from '@theme/Layout';
|
||||||
|
|
||||||
|
import Heading from '@theme/Heading';
|
||||||
|
import styles from './index.module.css';
|
||||||
|
|
||||||
|
function HomepageHeader() {
|
||||||
|
const {siteConfig} = useDocusaurusContext();
|
||||||
|
return (
|
||||||
|
<header className={clsx('hero hero--primary', styles.heroBanner)}>
|
||||||
|
<div className="container">
|
||||||
|
<Heading as="h1" className="hero__title">
|
||||||
|
{siteConfig.title}
|
||||||
|
</Heading>
|
||||||
|
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Link
|
||||||
|
className="button button--secondary button--lg"
|
||||||
|
to="docs/installation/windows">
|
||||||
|
Installation
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const {siteConfig} = useDocusaurusContext();
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
title={siteConfig.title}
|
||||||
|
description={siteConfig.tagline}>
|
||||||
|
<HomepageHeader />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user