Add project files.

This commit is contained in:
sim0n00ps 2025-05-03 00:17:19 +01:00
commit d9b49bd6bc
107 changed files with 32993 additions and 0 deletions

9
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}
}
}

View 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
View 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();
}
}
}

View 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; }
}
}
}

View 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
View 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;
}
}

View 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
View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}
}

View 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; }
}
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}
}

View 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; }
}
}
}

View 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>();
}
}

View 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; }
}
}
}

View 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; }
}
}

View 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
View 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
}

View 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>();
}
}

View 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; }
}
}
}

View 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>();
}
}

View 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>();
}
}

View 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>();
}
}

View 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; }
}
}
}

View 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();
}
}

View 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>();
}
}

View 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";
}
}
}

View 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; }
}
}
}

View 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; }
}
}
}

View 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>();
}
}

View 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; }
}
}
}

View File

@ -0,0 +1,8 @@
namespace OF_DL.Entities
{
[AttributeUsage(AttributeTargets.Property)]
internal class ToggleableConfigAttribute : Attribute
{
}
}

191
OF DL/Entities/User.cs Normal file
View 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; }
}
}
}

View 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,
}

View 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,
}
}

View 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
}
}

View 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

File diff suppressed because it is too large Load Diff

166
OF DL/Helpers/AuthHelper.cs Normal file
View 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;
}
}
}

View 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
View 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();
}
}
}
}

View 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);
}
}
}

File diff suppressed because it is too large Load Diff

View 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}");
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

50
OF DL/OF DL.csproj Normal file
View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}
}

Binary file not shown.

189
OF DL/Utils.cs Normal file
View 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();
}
}
}
}
}

View 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
View 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
View 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;
}*/

View 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();
}
}
}

View 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";
}
}

View 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()}";
}
}
}

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

8
OF DL/rules.json Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
20.16.0

41
docs/README.md Normal file
View 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
View File

@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

View 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
View 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:
![Image of dev tools panel showing network tab](/img/network_tab.png)
Click on one of the requests you see in the list, and scroll down until you find the 'Request Headers' section.
![Image of dev tools panel showing request headers](/img/request_headers.png)
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
View 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!

View 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.

View 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}"`

View File

@ -0,0 +1,8 @@
{
"label": "Installation",
"position": 1,
"link": {
"type": "generated-index",
"description": "Installation instructions for OF-DL"
}
}

View 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`.

View 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'
```

View 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/).

View 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

View 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:
![CLI welcome banner](/img/welcome_banner.png)
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.
![CLI main menu](/img/cli_menu.png)
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
View 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

File diff suppressed because it is too large Load Diff

44
docs/package.json Normal file
View 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
View 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
View 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