Compare commits

...

223 Commits

Author SHA1 Message Date
fc76164fb6 Merge pull request 'Fix failing test' (#158) from whimsical-c4lic0/OF-DL:add-error-logs-to-downloads into master
Reviewed-on: sim0n00ps/OF-DL#158
2026-03-02 16:27:17 +00:00
3d08f9cc75 Merge branch 'master' into add-error-logs-to-downloads 2026-03-02 16:27:10 +00:00
cac07c1a3e Fix failing test 2026-03-02 10:22:14 -06:00
30169c65b2 Merge pull request 'Add error logging to DownloadService' (#157) from whimsical-c4lic0/OF-DL:add-error-logs-to-downloads into master
Reviewed-on: sim0n00ps/OF-DL#157
2026-03-02 16:22:06 +00:00
e12f6de830 Add error logging to DownloadService 2026-03-02 10:18:53 -06:00
29571516ea Merge pull request 'Add support for DRM encrypted audio files' (#156) from whimsical-c4lic0/OF-DL:download-drm-audio into master
Reviewed-on: sim0n00ps/OF-DL#156
2026-03-02 09:35:11 +00:00
2c3d3fd3fa Merge branch 'master' into download-drm-audio 2026-03-02 09:34:59 +00:00
014195cb46 Merge pull request 'Add GUI' (#155) from whimsical-c4lic0/OF-DL:add-initial-gui into master
Reviewed-on: sim0n00ps/OF-DL#155
2026-03-02 09:34:48 +00:00
a7646e8ad6 Support downloading DRM encrypted audio files 2026-03-02 03:24:32 -06:00
7102bc34d9 Add missing cancellation tokens to download methods 2026-03-01 22:24:37 -06:00
9bc2a191f9 Fix windows clipboard issues 2026-03-01 22:09:29 -06:00
5f490a657f Add cancellationToken as needed 2026-03-01 21:40:32 -06:00
a6845c383a Adjust logging levels on clipboard logs 2026-03-01 21:39:23 -06:00
86f5aea02a Merge branch 'master' into add-initial-gui
# Conflicts:
#	OF DL.Cli/OF DL.Cli.csproj
#	OF DL.Core/Services/ApiService.cs
#	OF DL.Core/Services/DownloadService.cs
#	OF DL.Tests/OF DL.Tests.csproj
2026-03-01 21:30:42 -06:00
e7acaac7af Fix clipboard issues in docker 2026-03-01 21:25:23 -06:00
65da76ba43 Avoid using self-contained executables in docker to save storage from duplicated runtimes 2026-03-01 18:25:46 -06:00
78a6dbf300 Update dependencies 2026-03-01 18:18:54 -06:00
fd827eea18 Improve entrypoint service waiting 2026-03-01 18:18:16 -06:00
beee158d65 Bump Nuget packages 2026-03-01 23:29:46 +00:00
ce624ba573
Added exponential backoff retry handling for OF API requests in ApiService and for non‑DRM downloads in DownloadService.
•
Introduced configurable retry constants in Constants.
2026-03-01 23:24:45 +00:00
1945a592d1 Update .gitea/workflows/publish-docs.yml 2026-03-01 22:35:10 +00:00
01eb12d385 Display the auto-detected Ffmpeg and FFprobe paths as watermarks on the input fields 2026-03-01 16:00:34 -06:00
c3d793ce90 Update docs 2026-03-01 14:53:35 -06:00
7c273ed0a0 Reformat GUI code to reduce duplicated code and reduce branching via method parameters 2026-03-01 14:30:55 -06:00
43854bb771 Add cancellation token logic to missing methods, loops, and delays in ApiService, DownloadOrchestrationService, and DownloadService 2026-03-01 13:00:14 -06:00
54e092601c Remove unused code 2026-03-01 05:17:04 -06:00
02986d39dc Update AGENTS.md 2026-03-01 05:16:56 -06:00
584ce09644 Update documentation for GUI version 2026-03-01 05:04:52 -06:00
1ee4ebe865 Re-throw OperationCanceledException in ApiService 2026-03-01 03:49:21 -06:00
e2db74743f Remove random GUI docs 2026-03-01 03:19:02 -06:00
523eb9b8f1 Propogate CancellationToken to HTTP requests 2026-03-01 03:18:45 -06:00
0654c9ab09 Address IDE warnings 2026-03-01 02:54:44 -06:00
383b97f238 Fix incorrect docker download path 2026-03-01 02:49:18 -06:00
c8edf760ba Remove FFmpeg Path and FFprobe Path watermarks 2026-03-01 02:39:57 -06:00
35ec1f2bfd Simplify Dockerfile to reduce image size 2026-03-01 02:35:36 -06:00
26b98b8d31 Update publish workflow to include GUI 2026-03-01 01:11:26 -06:00
1bc47ad62b Skip chromium install step if chromium already has been installed 2026-03-01 00:23:16 -06:00
0af7066086 Add a notification modal when some users could not be selected from a list 2026-02-28 01:45:16 -06:00
d0de99a00c Remove unused EnforceGuiOnlyConfigValues function 2026-02-28 01:00:20 -06:00
4ea2a6107f Re-word DrmVideoDurationMatchThreshold config description 2026-02-28 00:56:11 -06:00
73bd188699 Add ShowScrapeSize config option to the GUI with a warning modal 2026-02-28 00:55:51 -06:00
aaed5bc906 Merge branch 'master' into add-initial-gui 2026-02-27 23:29:40 -06:00
f950dc258f Merge pull request 'Remove $ from beginning of cookie variables' (#152) from whimsical-c4lic0/OF-DL:fix-invalid-auth-files into master
Reviewed-on: sim0n00ps/OF-DL#152
2026-02-27 18:44:03 +00:00
12d7d6793e Remove $ from beining of cookie variables 2026-02-27 12:40:41 -06:00
80cda0b8b5 Merge pull request 'Add missing folders to the release' (#151) from whimsical-c4lic0/OF-DL:add-missing-files-to-release into master
Reviewed-on: sim0n00ps/OF-DL#151
2026-02-27 13:28:57 +00:00
acc2aea459 Avoid including macOS and linux playwright binaries in release zip 2026-02-27 02:14:30 -06:00
eea0af0d82 Add missing playwright files to release zip 2026-02-27 02:02:26 -06:00
6c054454ed Add cdm and chromium-scripts contents to release zip 2026-02-27 01:37:21 -06:00
2a75f5c868 Fix cancellation handling 2026-02-27 00:20:09 -06:00
b3e6ca4b5f Rename OF DL to OF DL.Cli in release workflow 2026-02-27 00:19:01 -06:00
f1d3ac7ea3 Use a logging sync to ensure GUI users are aware of any errors that occur while downloading 2026-02-27 00:01:07 -06:00
49cddd0608 Use consistent error message in GUI log 2026-02-26 22:56:24 -06:00
c035fa5721 Add global unhandled exception handler to GUI 2026-02-26 22:55:50 -06:00
6b345ea986 Update stealth script instructions to include specific instructions for GUI and CLI 2026-02-26 22:50:29 -06:00
daca54da2e Merge branch 'master' into add-initial-gui 2026-02-26 22:30:03 -06:00
b5be1f350b merge upstream 2026-02-27 04:29:37 +00:00
3749cd1568 Display a warning if a download is attempted without enabling both media types and sources 2026-02-26 22:28:40 -06:00
2a727c7121 Improve help text on config page 2026-02-26 19:10:52 -06:00
3f51f469ab Update .gitea/workflows/publish-release.yml 2026-02-27 00:25:50 +00:00
0b149e2547 Update .gitea/workflows/publish-release.yml 2026-02-26 23:55:36 +00:00
fe941bb540 merge upstream 2026-02-20 16:57:31 +00:00
ba0347f86f Merge pull request 'Replace PuppeteerSharp with Playwright' (#44) from whimsical-c4lic0/OF-DL:replace-puppeteer-with-playwright into master
Reviewed-on: sim0n00ps/OF-DL#44
2026-02-20 10:39:25 +00:00
bb04e0518a Add rules.json and stealth script to GUI project 2026-02-20 02:54:22 -06:00
e409e4a16c Fix CLI project name for docker 2026-02-20 02:52:52 -06:00
0a709f97ea Add legacy auth method to GUI, remove unnecessary activity log entries, and avoid deleting the auth.json file when auth fails 2026-02-20 00:35:19 -06:00
f536a34772 Rename OF DL to OF DL.Cli 2026-02-19 19:12:45 -06:00
2dcb9a3753 Update web link handling to copy links to clipboard when click inside docker 2026-02-19 18:52:15 -06:00
4d3ae0e19a Move OS and system environment checks to a helper class 2026-02-19 17:01:15 -06:00
3b8e575a21 Add auth flow status updates to the GUI 2026-02-19 16:36:50 -06:00
603c998ae9 Merge branch 'master' into add-initial-gui 2026-02-19 12:27:23 -06:00
d81a4d60f9 Merge remote-tracking branch 'sim0n00ps/master'
# Conflicts:
#	OF DL.Core/Services/DownloadService.cs
2026-02-19 12:26:37 -06:00
22ad1c005b Merge remote-tracking branch 'sim0n00ps/master' into replace-puppeteer-with-playwright
# Conflicts:
#	.gitea/workflows/publish-release.yml
2026-02-19 12:25:27 -06:00
40a7687606 Merge pull request 'Detect if a single post is paid or free' (#144) from whimsical-c4lic0/OF-DL:detect-free-and-paid-single-posts into master
Reviewed-on: sim0n00ps/OF-DL#144
2026-02-19 18:23:24 +00:00
ccb990675a Merge branch 'master' into detect-free-and-paid-single-posts 2026-02-19 18:22:51 +00:00
77bd5f7ed9 Merge pull request 'Fix custom filename formats for paid messages and paid posts' (#143) from whimsical-c4lic0/OF-DL:fix-custom-filename-formats into master
Reviewed-on: sim0n00ps/OF-DL#143
2026-02-19 18:22:42 +00:00
70f69fb502 Merge remote-tracking branch 'sim0n00ps/master' into fix-custom-filename-formats
# Conflicts:
#	OF DL.Core/Services/DownloadService.cs
2026-02-19 12:21:06 -06:00
e22d2b63a2 Merge remote-tracking branch 'sim0n00ps/master' into detect-free-and-paid-single-posts
# Conflicts:
#	OF DL.Core/Services/DownloadService.cs
2026-02-19 12:20:23 -06:00
4b0bd4d676 Merge pull request 'Prevent partial DRM video downloads' (#142) from whimsical-c4lic0/OF-DL:fix-partial-video-downloads into master
Reviewed-on: sim0n00ps/OF-DL#142
2026-02-19 18:03:11 +00:00
6b7cb29e2e Add GUI application to docker 2026-02-18 10:17:14 -06:00
f9089a339a Merge branch 'refs/heads/replace-puppeteer-with-playwright' into add-initial-gui
# Conflicts:
#	.gitea/workflows/publish-release.yml
2026-02-18 09:40:19 -06:00
65b25e6336 Avoid swallowing cancelation attempts via exception handling 2026-02-18 04:50:45 -06:00
8facc470f0 Merge branch 'master' into add-initial-gui 2026-02-18 04:37:08 -06:00
846b80a6c9 Merge branch 'detect-free-and-paid-single-posts'
# Conflicts:
#	OF DL.Core/Services/DownloadService.cs
2026-02-18 04:35:52 -06:00
605fbbda69 Merge branch 'fix-custom-filename-formats'
# Conflicts:
#	OF DL.Core/Services/DownloadService.cs
2026-02-18 04:35:01 -06:00
4ae09a5991 Change the default theme to dark mode 2026-02-18 04:33:32 -06:00
36dbb3de5d GUI improvements 2026-02-18 04:29:51 -06:00
a74ebc810a Update progress messaging for consistency 2026-02-18 03:52:56 -06:00
35bde51e7d Add a warning about missing CDM keys before starting downloads 2026-02-18 03:14:24 -06:00
dce7e7a6bd Detect if a single post is paid or free 2026-02-18 02:30:47 -06:00
d662d9be4d Add a download single post/message option to the GUI 2026-02-18 02:24:10 -06:00
ac4061f1ca UI improvements 2026-02-18 01:48:28 -06:00
34ad00ce03 Improve configuration layout 2026-02-18 00:04:37 -06:00
56b951ace0 Rename and organize config values 2026-02-17 22:23:43 -06:00
ac1c814633 Add help links and pages 2026-02-17 17:14:45 -06:00
e58ac7d2a6 Improve the filename format input field's appearance and validation 2026-02-17 13:58:06 -06:00
378a82548b Update linux installation docs 2026-02-17 12:37:28 -06:00
b6872a2b9e Add a dark theme option to the GUI 2026-02-17 02:10:27 -06:00
da40f3d0c5 Add FFprobe Path and DRM Video Duration Match Threshold to the config page 2026-02-17 01:36:57 -06:00
fccee9a520 Fix warnings 2026-02-17 01:14:07 -06:00
f4479a77ba Merge branch 'fix-partial-video-downloads' into add-initial-gui
# Conflicts:
#	OF DL.Core/Services/DownloadService.cs
2026-02-16 16:01:24 -06:00
661b61be66 Merge branch 'fix-custom-filename-formats' into add-initial-gui 2026-02-16 15:59:16 -06:00
03dd66a842 Fix custom filename formats for paid messages and posts, and fix creator config empty strings 2026-02-16 02:56:03 -06:00
7667939eba Add CLI flag for hiding private info for demo use 2026-02-14 15:43:51 -06:00
162811b267 Creator config modal improvements 2026-02-14 12:41:01 -06:00
85e299db41 Update GUI so that the stop button works quicker and more reliably 2026-02-14 12:30:31 -06:00
35f7d98112 Add a scrollview around the creator configs 2026-02-14 11:10:41 -06:00
7cccdd58a0 Config updates 2026-02-14 01:34:57 -06:00
5af26156c7 Separate OF DL and OF DL GUI projects 2026-02-13 15:17:22 -06:00
712f11dc4b UI improvements to the configuration page 2026-02-13 13:38:04 -06:00
ec8bf47de5 Initial demo GUI 2026-02-13 03:38:44 -06:00
edc3d771d1 Fix misleading wording in download summary messages 2026-02-13 00:53:41 -06:00
b4aac13bc6 Compare downloaded DRM video durations against the duration reported by the MPD to ensure complete downloads 2026-02-13 00:51:57 -06:00
15a5a1d5f1 Merge branch 'master' into replace-puppeteer-with-playwright 2026-02-13 00:33:05 +00:00
aee920a9f1 Merge pull request 'Major refactor' (#141) from whimsical-c4lic0/OF-DL:refactor-architecture into master
Reviewed-on: sim0n00ps/OF-DL#141
2026-02-13 00:21:58 +00:00
5f945aadfa Fix failing test 2026-02-12 10:57:01 -06:00
f16ad7f138 Improve CLI output to show counts of media 2026-02-12 10:48:44 -06:00
fdb6d55454 Fix ffmpeg logs path on windows 2026-02-12 09:50:59 -06:00
c2ab3dd79f Update work with recent major refactor 2026-02-11 13:20:06 -06:00
de97336f6c Merge branch 'refactor-architecture' into replace-puppeteer-with-playwright
# Conflicts:
#	Dockerfile
#	OF DL/Helpers/AuthHelper.cs
#	OF DL/OF DL.csproj
#	OF DL/Program.cs
2026-02-11 12:37:26 -06:00
fe0c81b2ac Remove BOM 2026-02-11 12:28:53 -06:00
3503fe1eb4 Fix capitalization in status message 2026-02-10 21:07:50 -06:00
6e0b4eba85 Upgrade dotnet from 8 to 10 2026-02-10 18:01:53 -06:00
f5a8c27cd1 Fix builds 2026-02-10 17:57:20 -06:00
a9a4c2ee20 Add checks for valid auth data before making API calls for media 2026-02-10 16:40:29 -06:00
94e135f168 Add additional AI generated unit tests 2026-02-10 16:14:31 -06:00
e7fd0ee138 Update AGENTS.md 2026-02-10 12:46:25 -06:00
d9825ae62b Add AI generated tests for ApiService and DownloadService 2026-02-10 12:44:52 -06:00
9794eacbc9 Update AGENTS.md to include testing and execution commands 2026-02-10 12:07:18 -06:00
4afa10186c Add generated docs to the .gitignore file 2026-02-10 12:03:34 -06:00
04004c7084 Refactor services 2026-02-10 12:01:33 -06:00
70738fd4ae Add unit tests for the mappers 2026-02-10 11:18:42 -06:00
e184df906f Add header comments and extract duplicated exception logging to a helper function 2026-02-10 10:30:05 -06:00
4889be1890 Update services to use coding and naming standards 2026-02-10 09:52:59 -06:00
4a218a3efe Fix remaining compiler warnings 2026-02-10 09:10:59 -06:00
85c9bc1f57 Fix docker builds 2026-02-10 03:06:15 -06:00
487de58274 Address compiler warning 2026-02-10 02:08:51 -06:00
f7f1fad92d Update AGENTS.md with updated models namespaces 2026-02-10 00:19:21 -06:00
ed06a5e514 Delete unused classes 2026-02-09 23:56:25 -06:00
974b0d4d7a Organize remaining model classes into similar namespaces 2026-02-09 23:56:09 -06:00
9766636d04 Extract repeated mapper methods into a common mapper class 2026-02-09 23:14:52 -06:00
ff431a377d Create OF DL.Core project to contain all the application logic for future GUI development 2026-02-09 22:39:23 -06:00
9fe84e9d9f Add AGENTS.md 2026-02-09 22:27:34 -06:00
4c680a40b5 Remove application logic from Program and continue to fix compiler warnings 2026-02-09 13:59:41 -06:00
17af1e8dfe Address compiler warnings 2026-02-09 04:48:21 -06:00
44a9fb1fcd Reduce duplicated code and simplify download media methods 2026-02-09 04:34:38 -06:00
a8b2acaad6 Fix custom filename format configs 2026-02-09 04:31:27 -06:00
a57af4042f Refactor remaining entities 2026-02-09 01:10:05 -06:00
fee9ca1e97 Update XML comments 2026-02-09 00:55:54 -06:00
407419a819 Replace string.Empty with "" 2026-02-09 00:55:28 -06:00
cd5c22d862 Refactor Subscriptions entities into DTOs and application entities with standardized naming conventions and default values 2026-02-09 00:46:19 -06:00
a9b135636b Refactor Users entities into DTOs and application entities with standardized naming conventions and default values 2026-02-09 00:31:52 -06:00
40ccf7aa62 Refactor Streams entities into DTOs and application entities with standardized naming conventions and default values 2026-02-09 00:10:09 -06:00
849fbbc919 Refactor Stories entities into DTOs and application entities with standardized naming conventions and default values 2026-02-08 23:27:36 -06:00
6c60509398 Refactor Purchased entities into DTOs and application entities with standardized naming conventions and default values 2026-02-08 22:58:05 -06:00
3c307bf7de Refactor Posts entities into DTOs and application entities with standardized naming conventions and default values 2026-02-08 21:04:09 -06:00
d8794ee219 Refactor Message entities into DTOs and application entities with standardized naming conventions and default values 2026-02-08 19:15:32 -06:00
911f98bc25 Refactor List entities into DTOs and application entities with standardized naming conventions and default values 2026-02-08 17:44:50 -06:00
2e3f17945e Refactor Highlight entities into DTOs and application entities with standardized naming conventions and default values 2026-02-08 16:16:38 -06:00
f243471b29 Remove BOM from files 2026-02-08 15:45:40 -06:00
35712da12d Refactor Archived entities into DTOs and application entities with standardized naming conventions and default values 2026-02-08 15:29:42 -06:00
50217a7642 Rename Entities to Models 2026-02-08 14:06:13 -06:00
6784ba0a18 Autoformat the entire solution 2026-02-06 01:42:57 -06:00
7af7bd8cfa Remove license header template and c++ rules 2026-02-06 01:42:01 -06:00
e9ab485188 Remove utils class in favor of a couple private functions 2026-02-06 01:35:50 -06:00
5df13775f0 Fix incorrect namespaces 2026-02-06 01:34:57 -06:00
86ee476dc5 Remove unused imports 2026-02-06 01:31:43 -06:00
7f1cd03f2f Update .editorconfig with additional rules from dotnet/runtime 2026-02-06 01:28:54 -06:00
4711c53746 Replace helper classes with services 2026-02-06 00:59:07 -06:00
d7bae3e260 Move config and logging into services 2026-02-05 02:21:05 -06:00
6c00f1a6aa Add helpers as services 2026-02-05 01:04:50 -06:00
a566cd0b71 Add dependency injection for config 2026-02-05 00:22:22 -06:00
e106fa2242 Document the stealth script creation process 2026-01-04 22:41:34 -06:00
568a071658 Merge branch 'master' of https://git.ofdl.tools/whimsical-c4lic0/OF-DL into replace-puppeteer-with-playwright 2026-01-04 22:30:33 -06:00
43fb74067c Remove checks for "upload" in media urls to stop media being excluded incorrectly 2026-01-04 00:59:29 +00:00
1c0536e766 Merge pull request 'Remove the quotes around the boolean DisableBrowserAuth config option' (#126) from whimsical-c4lic0/OF-DL:fix-incorrect-config-type into master
Reviewed-on: sim0n00ps/OF-DL#126
2026-01-03 00:58:53 +00:00
cdca0d5a57 Merge pull request 'Upgrade puppeteer sharp to fix 1.9.18 browser based auth in docker' (#125) from whimsical-c4lic0/OF-DL:upgrade-puppeteer-sharp into master
Reviewed-on: sim0n00ps/OF-DL#125
2026-01-03 00:58:43 +00:00
f4d1094c57 Remove the quotes around the boolean DisableBrowserAuth config option 2026-01-02 14:22:46 -06:00
d64012b9f1 Upgrade puppeteer sharp to fix issue with newer versions of chromium 2026-01-02 14:00:34 -06:00
bd0a2b6de6 Download Single Paid Message also download Preview Media 2026-01-01 23:16:18 +00:00
d5aa568afb Merge pull request 'fix: force media to empty list when null to avoid object deref error during purchased media processing' (#105) from nyc_tk/OF-DL:fix-purchased-media-obj-deref-error into master
Reviewed-on: sim0n00ps/OF-DL#105
2026-01-01 19:41:35 +00:00
4a07c03fc5 Merge pull request 'fix: updating Docker for ffmpeg 7.0 - requires Alpine v3.23 community packages' (#103) from nyc_tk/OF-DL:docker-update-ffmpeg into master
Reviewed-on: sim0n00ps/OF-DL#103
2026-01-01 19:40:48 +00:00
fc4ecf9b5e Force media to empty list when null to avoid object deref error 2025-12-15 23:12:35 -05:00
fcd287027f Updating Docker for ffmpeg 7.0 - requires Alpine v3.23 community packages 2025-12-15 22:24:14 -05:00
167d6640e3 Move DisableBrowserAuth to Auth 2025-12-14 18:47:18 +00:00
e9786f2341 Merge pull request 'Add additional logging for ffmpeg and prevent NRE errors from obscuring the actual errors from ffmpeg.' (#97) from ffmpeg_additional_logging into master
Reviewed-on: sim0n00ps/OF-DL#97
2025-12-14 15:39:10 +00:00
Melithine
0eae466368 Add additional logging for ffmpeg and prevent NRE errors from obscuring the actual errors from ffmpeg. 2025-12-13 19:55:59 -08:00
7bd5971695 Merge branch 'master' into replace-puppeteer-with-playwright 2025-12-14 02:29:11 +00:00
616aaef1c8 Merge pull request 'Add doc site and Discord invite after the startup banner.' (#89) from header_update into master
Reviewed-on: sim0n00ps/OF-DL#89
Reviewed-by: whimsical-c4lic0 <whimsical-c4lic0@noreply.localhost>
2025-12-13 19:30:56 +00:00
34f055b00e Merge pull request 'Fix 32-bit integers' (#94) from whimsical-c4lic0/OF-DL:fix-32-bit-integers into master
Reviewed-on: sim0n00ps/OF-DL#94
Reviewed-by: melithine <melithine@noreply.localhost>
2025-12-13 19:30:27 +00:00
e7e1556b3c Convert all sizes from int to long 2025-12-13 02:57:58 -06:00
74def34f96 Convert all ids from int to long 2025-12-13 02:52:15 -06:00
5c57178f5b Merge branch 'replace-puppeteer-with-playwright' of https://git.ofdl.tools/whimsical-c4lic0/OF-DL into replace-puppeteer-with-playwright 2025-12-13 02:09:33 -06:00
0572844ca8 Download chromium during runtime in docker (like windows) 2025-12-13 02:07:56 -06:00
Melithine
19730436d4 Add doc site and Discord invite after the startup banner. 2025-12-10 07:32:26 -08:00
e4eb6c0507 Merge branch 'master' into replace-puppeteer-with-playwright 2025-12-02 17:26:25 +00:00
eca38116fa Update docs/config/configuration.md 2025-12-02 00:02:50 +00:00
f6a9cbd305 Update docs/config/all-configuration-options.md 2025-12-02 00:00:36 +00:00
f75fa4e04c Merge pull request 'Add a 30s timeout to the Gitea version check.' (#84) from version_timeout into master
Reviewed-on: sim0n00ps/OF-DL#84
2025-12-01 22:47:50 +00:00
af5622ba82 Merge pull request 'Add ffmpeg version to startup output and log file.' (#83) from ffmpeg_version into master
Reviewed-on: sim0n00ps/OF-DL#83
2025-12-01 22:47:32 +00:00
0bf8dfa8ab Merge pull request 'fix: object reference not set by adding retry for GetDecryptionKeyOFDL' (#82) from TeenagerNeedsHelpQQ/OF-DL:master into master
Reviewed-on: sim0n00ps/OF-DL#82
2025-12-01 22:47:20 +00:00
d79733ec24 Merge branch 'master' into replace-puppeteer-with-playwright 2025-12-01 21:33:27 +00:00
Melithine
e98c5f74cf Add a 30s timeout to the Gitea version check. 2025-11-29 14:45:57 -08:00
Melithine
181ea8eef1 Add ffmpeg version to startup output and log file. 2025-11-29 13:46:43 -08:00
2bddedb644 fix: object reference not set by adding retry for GetDecryptionKeyOFDL 2025-11-29 15:01:21 +01:00
a4d8676f2e Merge remote-tracking branch 'origin/master' into replace-puppeteer-with-playwright 2025-11-05 14:26:48 -06:00
c147a19a0a Merge pull request 'Fix purchased tab API endpoints' (#72) from whimsical-c4lic0/OF-DL:fix-purchased-tab into master
Reviewed-on: sim0n00ps/OF-DL#72
2025-11-05 09:14:01 +00:00
18fe2580ad Fix purchased tab API endpoints 2025-11-04 18:18:29 -06:00
f501a7e806 Merge remote-tracking branch 'origin/master' into replace-puppeteer-with-playwright 2025-10-31 17:43:27 -05:00
34e6eb1d2b Merge pull request 'feat: Add option to store raw post text without sanitization (Fix #52)' (#53) from wer/OF-DL:master into master
Reviewed-on: sim0n00ps/OF-DL#53
2025-10-07 13:05:15 +00:00
78f6f1e611 Merge pull request 'fix: add widevine request retry logic to work around ratelimits' (#47) from ddirty830/OF-DL:fix-widevine-cloudflare-retry into master
Reviewed-on: sim0n00ps/OF-DL#47
2025-10-07 13:05:02 +00:00
ec88e6e783 Merge pull request 'fix: update post/message api calls due to changes' (#61) from ddirty830/OF-DL:fix-message-downloading into master
Reviewed-on: sim0n00ps/OF-DL#61
2025-10-07 13:04:34 +00:00
0761b28c72 fix: update post/message api calls due to changes 2025-10-06 15:20:55 -05:00
Grey Lee
3e7fd45589 Add DisableTextSanitization config option and update related logic 2025-09-13 17:41:22 +08:00
2b2206a0b4 merge upstream 2025-08-18 15:25:14 +00:00
2c8dbb04ed fix: add widevine request retry logic to work around ratelimits 2025-08-17 12:41:35 -05:00
824d88a6b8 Merge pull request 'Re-order the categories in the docs' (#39) from whimsical-c4lic0/OF-DL:docs-tweak into master
Reviewed-on: sim0n00ps/OF-DL#39
2025-08-08 19:31:16 +00:00
3ef7895007 Replace PuppeteerSharp with Playwright 2025-07-30 17:30:37 -05:00
64ff74f753 Re-order the categories in the docs 2025-07-14 15:07:29 -05:00
37fae9185a Fix links 2025-06-18 01:50:41 -05:00
1572c1eee8 Add docs for DisableBrowserAuth config option 2025-06-18 01:50:32 -05:00
eaefd033aa Remove unused metadata from docusaurus 2025-06-18 01:40:04 -05:00
473b8d0ef3 Add config sections docs page 2025-06-18 01:37:59 -05:00
Melithine
d83ad2ec54 Update errors to point at new docs site. 2025-05-18 09:16:05 -07:00
349 changed files with 28646 additions and 14025 deletions

View File

@ -1,9 +1,186 @@
# Editor configuration, see https://editorconfig.org
# editorconfig.org
# top-most EditorConfig file
root = true
# Default settings:
# A newline ending every file
# Use 4 spaces as indentation
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[project.json]
indent_size = 2
# Generated code
[*{_AssemblyInfo.cs,.notsupported.cs}]
generated_code = true
# C# files
[*.cs]
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = one_less_than_current
# Modifier preferences
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion
# avoid this. unless absolutely necessary
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
# Types: use keywords instead of BCL types, and permit var only when the type is clear
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = false:none
csharp_style_var_elsewhere = false:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# static fields should have s_ prefix
dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields
dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style
dotnet_naming_symbols.static_fields.applicable_kinds = field
dotnet_naming_symbols.static_fields.required_modifiers = static
dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected
dotnet_naming_style.static_prefix_style.required_prefix = s_
dotnet_naming_style.static_prefix_style.capitalization = camel_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
# Code style defaults
csharp_using_directive_placement = outside_namespace:suggestion
dotnet_sort_system_directives_first = true
csharp_prefer_braces = true:silent
csharp_preserve_single_line_blocks = true:none
csharp_preserve_single_line_statements = false:none
csharp_prefer_static_local_function = true:suggestion
csharp_prefer_simple_using_statement = false:none
csharp_style_prefer_switch_expression = true:suggestion
dotnet_style_readonly_field = true:suggestion
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
csharp_prefer_simple_default_expression = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_methods = true:silent
csharp_style_expression_bodied_constructors = true:silent
csharp_style_expression_bodied_operators = true:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = true:silent
# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
# Null checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Other features
csharp_style_prefer_index_operator = false:none
csharp_style_prefer_range_operator = false:none
csharp_style_pattern_local_over_anonymous_function = false:none
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = do_not_ignore
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Xml project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
indent_size = 2
[*.{csproj,vbproj,proj,nativeproj,locproj}]
charset = utf-8
# Xml build files
[*.builds]
indent_size = 2
# Xml files
[*.{xml,stylecop,resx,ruleset}]
indent_size = 2
# Xml config files
[*.{props,targets,config,nuspec}]
indent_size = 2
# YAML config files
[*.{yml,yaml}]
indent_size = 2
# Shell scripts
[*.sh]
end_of_line = lf
[*.{cmd,bat}]
end_of_line = crlf

5
.gitattributes vendored
View File

@ -3,6 +3,11 @@
###############################################################################
* text=auto
###############################################################################
# Shell scripts should use LF line endings (avoid /bin/sh^M issues in containers)
###############################################################################
*.sh text eol=lf
###############################################################################
# Set default behavior for command prompt diff.
#

View File

@ -4,10 +4,6 @@ on:
push:
tags:
- 'OFDLV*'
paths:
- 'docs/**'
- '.gitea/workflows/publish-docs.yml'
workflow_dispatch:
jobs:
build-and-deploy:

View File

@ -29,35 +29,54 @@ jobs:
- name: Build for Windows and Linux
run: |
dotnet publish -p:Version=${{ steps.version.outputs.version }} \
dotnet publish "OF DL.Cli/OF DL.Cli.csproj" -p:Version=${{ steps.version.outputs.version }} \
-p:PackageVersion=${{ steps.version.outputs.version }} \
-p:WarningLevel=0 -c Release -r win-x86 \
--self-contained true -p:PublishSingleFile=true -o outwin
--self-contained true -p:PublishSingleFile=true -o outwin-cli
dotnet publish -p:Version=${{ steps.version.outputs.version }} \
dotnet publish "OF DL.Gui/OF DL.Gui.csproj" -p:Version=${{ steps.version.outputs.version }} \
-p:PackageVersion=${{ steps.version.outputs.version }} \
-p:WarningLevel=0 -c Release -r win-x86 \
--self-contained true -p:PublishSingleFile=true -o outwin-gui
dotnet publish "OF DL.Cli/OF DL.Cli.csproj" -p:Version=${{ steps.version.outputs.version }} \
-p:PackageVersion=${{ steps.version.outputs.version }} \
-p:WarningLevel=0 -c Release -r linux-x64 \
--self-contained true -p:PublishSingleFile=true -o outlin
--self-contained true -p:PublishSingleFile=true -o outlin-cli
- name: Copy and patch extra files
run: |
cp ./OF\ DL/rules.json outwin/
chmod +x ./outlin/OF\ DL
cd outwin
cd outlin-cli
chmod +x "OF DL.Cli"
echo "➤ Running OF DL binary (timeout)"
timeout --preserve-status --kill-after=5s 30s ../outlin/OF\ DL --non-interactive || true
timeout --preserve-status --kill-after=5s 30s "./OF DL.Cli" --non-interactive || true
echo "➤ Binary finished"
echo "➤ Combine OF DL.Cli and OF DL.Gui into a single release folder"
cd ..
mv outwin-gui outwin
cd outwin
mv "../outwin-cli/OF DL.Cli.exe" .
mv ../outlin-cli/config.conf .
rm *.pdb
mv "OF DL.Gui.exe" "OF DL.exe"
mv "OF DL.Cli.exe" "OF DL - Classic.exe"
echo "➤ Remove unneeded playwright binaries"
rm -rf .playwright/node/darwin*
rm -rf .playwright/node/linux*
echo "➤ Creating folder for CDM"
mkdir -p cdm/devices/chrome_1610
echo "➤ Copying ffmpeg from user folder"
echo "➤ Copying ffmpeg and ffprobe from user folder"
cp /home/rhys/ffmpeg/ffmpeg-7.1.1-essentials_build/bin/ffmpeg.exe .
cp /home/rhys/ffmpeg/ffmpeg-7.1.1-essentials_build/bin/ffprobe.exe .
cp /home/rhys/ffmpeg/ffmpeg-7.1.1-essentials_build/LICENSE LICENSE.ffmpeg
echo "➤ Creating release zip"
zip ../OFDLV${{ steps.version.outputs.version }}.zip OF\ DL.exe e_sqlite3.dll rules.json config.conf cdm ffmpeg.exe LICENSE.ffmpeg
zip -r ../OFDLV${{ steps.version.outputs.version }}.zip .playwright *
cd ..
- name: Create release and upload artifact

9
.gitignore vendored
View File

@ -371,3 +371,12 @@ FodyWeavers.xsd
# venv
venv/
# Generated docs
/site
# Builds
/outwin
/outwin-cli
/outwin-gui
/outlin-cli

123
AGENTS.md Normal file
View File

@ -0,0 +1,123 @@
# AGENTS.md
## Purpose
OF DL (OF-DL) is a C# (`net10.0`) app suite with:
- A modern Avalonia GUI
- A classic CLI
- Shared core services for auth, API calls, downloads, DRM handling, and metadata storage
## Architecture at a glance
1. `AuthService` loads `auth.json` or runs browser login and saves auth.
2. `ApiService` signs OnlyFans API requests with dynamic rules.
3. `DownloadOrchestrationService` selects creators/lists and coordinates download jobs.
4. `DownloadService` downloads/decrypts media and records metadata via `DBService`.
## Key directories
- `OF DL.Cli/`: CLI-specific UI/helpers
- `OF DL.Gui/`: Avalonia UI, view models, windows
- `OF DL.Core/Services/`: business logic (auth/api/download/config/startup/db/logging)
- `OF DL.Core/Models/`: DTOs, entities, config/auth/download/startup models, mappers
- `OF DL.Core/Widevine/`: Widevine CDM logic
- `docs/`: MkDocs source
- `docker/`: container entrypoint/runtime config
## Files and entry points to check first
- `OF DL.Gui/ViewModels/MainWindowViewModel.cs`
- `OF DL.Gui/Views/MainWindow.axaml`
- `OF DL.Core/Services/ApiService.cs`
- `OF DL.Core/Services/AuthService.cs`
- `OF DL.Core/Services/ConfigService.cs`
- `OF DL.Core/Services/DownloadOrchestrationService.cs`
- `OF DL.Core/Services/DownloadService.cs`
- `OF DL.Core/Services/StartupService.cs`
- `OF DL.Core/Services/DBService.cs`
- `OF DL.Core/Helpers/EnvironmentHelper.cs`
## Runtime files (relative to working directory)
- `config.conf` (primary config)
- `auth.json` (saved auth)
- `rules.json` (dynamic rules fallback)
- `users.db` (global user index)
- `chromium-data/` (browser profile for auth)
- `cdm/` (Widevine device files)
Default download root when `DownloadPath` is blank:
- `__user_data__/sites/OnlyFans/{username}`
## Commands
Build GUI:
```bash
dotnet build "OF DL.Gui/OF DL.Gui.csproj"
```
Build CLI:
```bash
dotnet build "OF DL.Cli/OF DL.Cli.csproj"
```
Tests:
```bash
dotnet test "OF DL.Tests/OF DL.Tests.csproj"
```
Coverage (optional):
```bash
dotnet test "OF DL.Tests/OF DL.Tests.csproj" --collect:"XPlat Code Coverage"
```
## High-impact technical details
Dynamic rules and request signing (`ApiService.GetDynamicHeaders`):
- Remote rules URL: `https://git.ofdl.tools/sim0n00ps/dynamic-rules/raw/branch/main/rules.json`
- Falls back to local `rules.json`
- Signed headers include `app-token`, `sign`, `time`, `user-id`, `user-agent`, `x-bc`, `cookie`
DRM decryption:
- Preferred: local CDM device files under `cdm/devices/chrome_1610/`
- Fallback path exists when CDM files are missing (`ofdl.tools/WV`)
- Primary flow is in `DownloadService` + DRM helpers in `ApiService`
## Documentation update rules
Update docs whenever user-facing behavior changes.
- Config shape/options changed: update
- `docs/config/configuration.md`
- `docs/config/all-configuration-options.md`
- `docs/config/custom-filename-formats.md` (if filename tokens/formats changed)
- Auth/login behavior changed: update `docs/config/auth.md`
- GUI/CLI workflow changed: update
- `docs/operation/modern-version.md`
- `docs/operation/classic-version.md` (if applicable)
- Docker runtime/paths changed: update `docs/installation/docker.md`
## Coding style essentials
Follow `.editorconfig`. Most important rules:
- 4 spaces (2 for XML/YAML/project files), no tabs
- C# braces on new lines
- Prefer predefined types (`int`, `string`)
- `using` directives outside namespace, `System` first
- Private fields `_camelCase`; private static `s_` prefix
## Agent guardrails
- Prefer small, targeted changes in the service/viewmodel responsible for the behavior.
- Keep GUI and CLI behavior aligned when changes affect both.
- Do not manually edit generated docs in `site/`.
- If you add new significant workflow/service structure, update this file.

View File

@ -1,66 +1,72 @@
FROM alpine:3.20 AS build
FROM mcr.microsoft.com/dotnet/sdk:10.0 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"]
COPY ["OF DL.Cli", "/src/OF DL.Cli"]
COPY ["OF DL.Core", "/src/OF DL.Core"]
COPY ["OF DL.Gui", "/src/OF DL.Gui"]
WORKDIR "/src"
# Build release
RUN dotnet publish -p:WarningLevel=0 -p:Version=$VERSION -c Release --self-contained true -p:PublishSingleFile=true -o out
RUN dotnet publish "OF DL.Gui/OF DL.Gui.csproj" -p:WarningLevel=0 -p:Version=$VERSION -c Release -o outgui \
&& dotnet publish "OF DL.Cli/OF DL.Cli.csproj" -p:WarningLevel=0 -p:Version=$VERSION -c Release -o outcli \
# Generate default config.conf files
RUN /src/out/OF\ DL --non-interactive || true && \
&& /src/outcli/OF\ DL.Cli --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
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS final
# Install dependencies
RUN apk --no-cache --repository community add \
bash \
RUN apt-get update \
&& apt-get install -y \
tini \
dotnet8-runtime \
ffmpeg \
udev \
ttf-freefont \
chromium \
supervisor \
xvfb \
x11vnc \
novnc
novnc \
npm \
openbox \
xclip \
&& npx playwright install-deps \
&& apt-get remove --purge -y npm \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
# 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
&& 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
&& mkdir -p /data /config /config/logs /default-config
# Copy release
COPY --from=build /src/out /app
COPY --from=build /src/outgui /app
ARG cli_src_dll="/src/outcli/*.dll"
ARG cli_src_app="/src/outcli/OF DL.Cli"
ARG cli_target="/app/OF DL.Cli"
COPY --from=build ${cli_src_dll} ${cli_target}
COPY --from=build ${cli_src_app} ${cli_target}
# Copy default configuration files
COPY --from=build /src/config.conf /default-config
COPY --from=build ["/src/OF DL/rules.json", "/default-config"]
COPY --from=build ["/src/OF DL.Cli/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 \
ENV DEBIAN_FRONTEND="noninteractive" \
DISPLAY=:0.0 \
DISPLAY_WIDTH=1366 \
DISPLAY_HEIGHT=768 \
OFDL_PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
OFDL_DOCKER=true
OFDL_DOCKER=true \
PLAYWRIGHT_BROWSERS_PATH=/config/chromium
EXPOSE 8080
WORKDIR /config
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/app/entrypoint.sh"]
ENTRYPOINT ["/usr/bin/tini", "--", "/app/entrypoint.sh"]
CMD []

View File

@ -0,0 +1,136 @@
using OF_DL.Models.Downloads;
using OF_DL.Services;
using Spectre.Console;
namespace OF_DL.CLI;
/// <summary>
/// Spectre.Console implementation of IDownloadEventHandler.
/// Handles all CLI-specific display logic for downloads.
/// </summary>
public class SpectreDownloadEventHandler : IDownloadEventHandler
{
public CancellationToken CancellationToken { get; } = CancellationToken.None;
public async Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work)
{
TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
await AnsiConsole.Status()
.StartAsync($"[red]{Markup.Escape(statusMessage)}[/]",
async ctx =>
{
try
{
SpectreStatusReporter reporter = new(ctx);
T result = await work(reporter);
tcs.TrySetResult(result);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
return await tcs.Task;
}
public async Task<T> WithProgressAsync<T>(string description, long maxValue, bool showSize,
Func<IProgressReporter, Task<T>> work)
{
TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
await AnsiConsole.Progress()
.Columns(GetProgressColumns(showSize))
.StartAsync(async ctx =>
{
try
{
ProgressTask task = ctx.AddTask($"[red]{Markup.Escape(description)}[/]", false);
task.MaxValue = maxValue;
task.StartTask();
SpectreProgressReporter progressReporter = new(task);
T result = await work(progressReporter);
tcs.TrySetResult(result);
task.StopTask();
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
return await tcs.Task;
}
public void OnContentFound(string contentType, int mediaCount, int objectCount) =>
AnsiConsole.Markup($"[red]Found {mediaCount} Media from {objectCount} {Markup.Escape(contentType)}\n[/]");
public void OnNoContentFound(string contentType) =>
AnsiConsole.Markup($"[red]Found 0 {Markup.Escape(contentType)}\n[/]");
public void OnDownloadComplete(string contentType, DownloadResult result) =>
AnsiConsole.Markup(
$"[red]{Markup.Escape(contentType)} Media Already Downloaded: {result.ExistingDownloads} New {Markup.Escape(contentType)} Media Downloaded: {result.NewDownloads}[/]\n");
public void OnUserStarting(string username) =>
AnsiConsole.Markup($"[red]\nScraping Data for {Markup.Escape(username)}\n[/]");
public void OnUserComplete(string username, CreatorDownloadResult result)
{
AnsiConsole.Markup("\n");
AnsiConsole.Write(new BreakdownChart()
.FullSize()
.AddItem("Paid Posts", result.PaidPostCount, Color.Red)
.AddItem("Posts", result.PostCount, Color.Blue)
.AddItem("Archived", result.ArchivedCount, Color.Green)
.AddItem("Streams", result.StreamsCount, Color.Purple)
.AddItem("Stories", result.StoriesCount, Color.Yellow)
.AddItem("Highlights", result.HighlightsCount, Color.Orange1)
.AddItem("Messages", result.MessagesCount, Color.LightGreen)
.AddItem("Paid Messages", result.PaidMessagesCount, Color.Aqua));
AnsiConsole.Markup("\n");
}
public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount)
{
AnsiConsole.Markup("\n");
AnsiConsole.Write(new BreakdownChart()
.FullSize()
.AddItem("Paid Posts", paidPostCount, Color.Red)
.AddItem("Paid Messages", paidMessagesCount, Color.Aqua));
AnsiConsole.Markup("\n");
}
public void OnScrapeComplete(TimeSpan elapsed) =>
AnsiConsole.Markup($"[green]Scrape Completed in {elapsed.TotalMinutes:0.00} minutes\n[/]");
public void OnMessage(string message) => AnsiConsole.Markup($"[red]{Markup.Escape(message)}\n[/]");
private static ProgressColumn[] GetProgressColumns(bool showScrapeSize)
{
List<ProgressColumn> progressColumns;
if (showScrapeSize)
{
progressColumns =
[
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new DownloadedColumn(),
new RemainingTimeColumn()
];
}
else
{
progressColumns =
[
new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn()
];
}
return progressColumns.ToArray();
}
}

View File

@ -0,0 +1,16 @@
using OF_DL.Services;
using Spectre.Console;
namespace OF_DL.CLI;
/// <summary>
/// Implementation of IProgressReporter that uses Spectre.Console's ProgressTask for CLI output.
/// </summary>
public class SpectreProgressReporter(ProgressTask task, CancellationToken cancellationToken = default) : IProgressReporter
{
private readonly ProgressTask _task = task ?? throw new ArgumentNullException(nameof(task));
public CancellationToken CancellationToken { get; } = cancellationToken;
public void ReportProgress(long increment) => _task.Increment(increment);
}

View File

@ -0,0 +1,17 @@
using OF_DL.Services;
using Spectre.Console;
namespace OF_DL.CLI;
/// <summary>
/// Implementation of IStatusReporter that uses Spectre.Console's StatusContext for CLI output.
/// </summary>
public class SpectreStatusReporter(StatusContext ctx) : IStatusReporter
{
public void ReportStatus(string message)
{
ctx.Status($"[red]{message}[/]");
ctx.Spinner(Spinner.Known.Dots);
ctx.SpinnerStyle(Style.Parse("blue"));
}
}

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.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>
<ProjectReference Include="..\OF DL.Core\OF DL.Core.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Akka" Version="1.5.61" />
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1"/>
<PackageReference Include="HtmlAgilityPack" Version="1.12.4"/>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3"/>
<PackageReference Include="Microsoft.Playwright" Version="1.58.0"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4"/>
<PackageReference Include="protobuf-net" Version="3.2.56"/>
<PackageReference Include="Serilog" Version="4.3.1"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1"/>
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0"/>
<PackageReference Include="System.Reactive" Version="6.1.0"/>
<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="rules.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="chromium-scripts/stealth.min.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

911
OF DL.Cli/Program.cs Normal file
View File

@ -0,0 +1,911 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using OF_DL.CLI;
using OF_DL.Models;
using OF_DL.Enumerations;
using OF_DL.Helpers;
using OF_DL.Models.Config;
using OF_DL.Models.Downloads;
using OF_DL.Models.Entities.Users;
using OF_DL.Services;
using Serilog;
using Spectre.Console;
namespace OF_DL;
public class Program(IServiceProvider serviceProvider)
{
private async Task LoadAuthFromBrowser()
{
IAuthService authService = serviceProvider.GetRequiredService<IAuthService>();
// Show the initial message
AnsiConsole.MarkupLine("[yellow]Downloading dependencies. Please wait ...[/]");
// Show instructions based on the environment
await Task.Delay(5000);
if (EnvironmentHelper.IsRunningInDocker())
{
AnsiConsole.MarkupLine(
"[yellow]In your web browser, navigate to the port forwarded from your docker container.[/]");
AnsiConsole.MarkupLine(
"[yellow]For instance, if your docker run command included \"-p 8080:8080\", open your web browser to \"http://localhost:8080\".[/]");
AnsiConsole.MarkupLine(
"[yellow]Once on that webpage, please use it to log in to your OF account. Do not navigate away from the page.[/]");
}
else
{
AnsiConsole.MarkupLine(
"[yellow]In the new window that has opened, please log in to your OF account. Do not close the window or tab. Do not navigate away from the page.[/]\n");
AnsiConsole.MarkupLine(
"[yellow]If you use this method or encounter other issues while logging in, use one of the legacy authentication methods documented here:[/]");
AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]");
}
// Load auth from browser using the service
bool success = await authService.LoadFromBrowserAsync();
if (!success || authService.CurrentAuth == null)
{
AnsiConsole.MarkupLine(
"\n[red]Authentication failed. Be sure to log into to OF using the new window that opened automatically.[/]");
AnsiConsole.MarkupLine(
"[red]The window will close automatically when the authentication process is finished.[/]");
AnsiConsole.MarkupLine(
"[red]If the problem persists, you may want to try using a legacy authentication method documented here:[/]\n");
AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]");
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
Log.Error("auth invalid after attempt to get auth from browser");
Environment.Exit(2);
}
await authService.SaveToFileAsync();
}
public static async Task Main(string[] args)
{
AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red));
AnsiConsole.Markup("Documentation: [link]https://docs.ofdl.tools/[/]\n");
AnsiConsole.Markup("Discord server: [link]https://discord.com/invite/6bUW8EJ53j[/]\n\n");
ServiceCollection services = await ConfigureServices(args);
ServiceProvider serviceProvider = services.BuildServiceProvider();
Program program = serviceProvider.GetRequiredService<Program>();
await program.RunAsync();
}
private static async Task<ServiceCollection> ConfigureServices(string[] args)
{
// Set up dependency injection with LoggingService and ConfigService
ServiceCollection services = new();
services.AddSingleton<ILoggingService, LoggingService>();
services.AddSingleton<IConfigService, ConfigService>();
ServiceProvider tempServiceProvider = services.BuildServiceProvider();
ILoggingService loggingService = tempServiceProvider.GetRequiredService<ILoggingService>();
IConfigService configService = tempServiceProvider.GetRequiredService<IConfigService>();
if (!await configService.LoadConfigurationAsync(args))
{
AnsiConsole.MarkupLine("\n[red]config.conf is not valid, check your syntax![/]\n");
if (!configService.IsCliNonInteractive)
{
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
Console.ReadKey();
}
Environment.Exit(3);
}
AnsiConsole.Markup("[green]config.conf located successfully!\n[/]");
// Set up full dependency injection with loaded config
services = [];
services.AddSingleton(loggingService);
services.AddSingleton(configService);
services.AddSingleton<IAuthService, AuthService>();
services.AddSingleton<IApiService, ApiService>();
services.AddSingleton<IDbService, DbService>();
services.AddSingleton<IDownloadService, DownloadService>();
services.AddSingleton<IFileNameService, FileNameService>();
services.AddSingleton<IStartupService, StartupService>();
services.AddSingleton<IDownloadOrchestrationService, DownloadOrchestrationService>();
services.AddSingleton<Program>();
return services;
}
private async Task RunAsync()
{
IConfigService configService = serviceProvider.GetRequiredService<IConfigService>();
IAuthService authService = serviceProvider.GetRequiredService<IAuthService>();
IStartupService startupService = serviceProvider.GetRequiredService<IStartupService>();
IDownloadOrchestrationService orchestrationService =
serviceProvider.GetRequiredService<IDownloadOrchestrationService>();
try
{
// Version check
VersionCheckResult versionResult = await startupService.CheckVersionAsync();
DisplayVersionResult(versionResult);
// Environment validation
StartupResult startupResult = await startupService.ValidateEnvironmentAsync();
DisplayStartupResult(startupResult);
if (!startupResult.IsWindowsVersionValid)
{
Console.Write(
"This appears to be running on an older version of Windows which is not supported.\n\n");
Console.Write(
"OF-DL requires Windows 10 or higher when being run on Windows. Your reported version is: {0}\n\n",
startupResult.OsVersionString);
if (!configService.CurrentConfig.NonInteractiveMode)
{
Console.Write("Press any key to continue.\n");
Console.ReadKey();
}
Environment.Exit(1);
}
if (!startupResult.FfmpegFound)
{
if (!configService.CurrentConfig.NonInteractiveMode)
{
AnsiConsole.Markup(
"[red]Cannot locate FFmpeg; please modify config.conf with the correct path. Press any key to exit.[/]");
Console.ReadKey();
}
else
{
AnsiConsole.Markup(
"[red]Cannot locate FFmpeg; please modify config.conf with the correct path.[/]");
}
Environment.Exit(4);
}
if (!startupResult.FfprobeFound)
{
if (!configService.CurrentConfig.NonInteractiveMode)
{
AnsiConsole.Markup(
"[red]Cannot locate FFprobe; please modify config.conf with the correct path. Press any key to exit.[/]");
Console.ReadKey();
}
else
{
AnsiConsole.Markup(
"[red]Cannot locate FFprobe; please modify config.conf with the correct path.[/]");
}
Environment.Exit(4);
}
// Auth flow
await HandleAuthFlow(authService, configService);
// Validate cookie string
authService.ValidateCookieString();
// rules.json validation
DisplayRulesJsonResult(startupResult, configService);
// NonInteractiveMode
if (configService.CurrentConfig.NonInteractiveMode)
{
configService.CurrentConfig.NonInteractiveMode = true;
Log.Debug("NonInteractiveMode = true");
}
// Validate auth via API
User? validate = await authService.ValidateAuthAsync();
if (validate == null || (validate.Name == null && validate.Username == null))
{
Log.Error("Auth failed");
authService.CurrentAuth = null;
if (!configService.CurrentConfig.NonInteractiveMode &&
!configService.CurrentConfig.DisableBrowserAuth)
{
await LoadAuthFromBrowser();
}
if (authService.CurrentAuth == null)
{
AnsiConsole.MarkupLine(
"\n[red]Auth failed. Please try again or use other authentication methods detailed here:[/]\n");
AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth[/]\n");
if (!configService.CurrentConfig.NonInteractiveMode)
{
Console.WriteLine("\nPress any key to exit.");
Console.ReadKey();
}
Environment.Exit(2);
}
}
AnsiConsole.Markup(
$"[green]Logged In successfully as {(!string.IsNullOrEmpty(validate?.Name) ? validate.Name : "Unknown Name")} {(!string.IsNullOrEmpty(validate?.Username) ? validate.Username : "Unknown Username")}\n[/]");
// Main download loop
await DownloadAllData(orchestrationService, configService, startupResult);
}
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);
}
if (!configService.CurrentConfig.NonInteractiveMode)
{
Console.WriteLine("\nPress any key to exit.");
Console.ReadKey();
}
Environment.Exit(5);
}
}
private async Task DownloadAllData(
IDownloadOrchestrationService orchestrationService,
IConfigService configService,
StartupResult startupResult)
{
Config config = configService.CurrentConfig;
SpectreDownloadEventHandler eventHandler = new();
Log.Debug("Calling DownloadAllData");
do
{
DateTime startTime = DateTime.Now;
UserListResult userListResult = await orchestrationService.GetAvailableUsersAsync();
Dictionary<string, long> users = userListResult.Users;
Dictionary<string, long> lists = userListResult.Lists;
if (userListResult.IgnoredListError != null)
{
AnsiConsole.Markup($"[red]{Markup.Escape(userListResult.IgnoredListError)}\n[/]");
}
KeyValuePair<bool, Dictionary<string, long>> hasSelectedUsersKVP;
if (config.NonInteractiveMode && config.NonInteractiveModePurchasedTab)
{
hasSelectedUsersKVP = new KeyValuePair<bool, Dictionary<string, long>>(true,
new Dictionary<string, long> { { "PurchasedTab", 0 } });
}
else if (config.NonInteractiveMode && string.IsNullOrEmpty(config.NonInteractiveModeListName))
{
hasSelectedUsersKVP = new KeyValuePair<bool, Dictionary<string, long>>(true, users);
}
else if (config.NonInteractiveMode && !string.IsNullOrEmpty(config.NonInteractiveModeListName))
{
ListUserSelectionResult listSelectionResult =
await orchestrationService.GetUsersForListAsync(config.NonInteractiveModeListName, users, lists);
Dictionary<string, long> selectedUsers = listSelectionResult.SelectedUsers;
hasSelectedUsersKVP = new KeyValuePair<bool, Dictionary<string, long>>(true, selectedUsers);
}
else
{
(bool IsExit, Dictionary<string, long>? selectedUsers) userSelectionResult =
await HandleUserSelection(users, lists);
config = configService.CurrentConfig;
hasSelectedUsersKVP = new KeyValuePair<bool, Dictionary<string, long>>(userSelectionResult.IsExit,
userSelectionResult.selectedUsers ?? []);
}
if (hasSelectedUsersKVP.Key &&
hasSelectedUsersKVP.Value.ContainsKey("SinglePost"))
{
await HandleSinglePostDownload(orchestrationService, users, startupResult, eventHandler);
}
else if (hasSelectedUsersKVP.Key &&
hasSelectedUsersKVP.Value.ContainsKey("PurchasedTab"))
{
await orchestrationService.DownloadPurchasedTabAsync(users,
startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler);
DateTime endTime = DateTime.Now;
eventHandler.OnScrapeComplete(endTime - startTime);
}
else if (hasSelectedUsersKVP.Key &&
hasSelectedUsersKVP.Value.ContainsKey("SingleMessage"))
{
await HandleSingleMessageDownload(orchestrationService, users, startupResult, eventHandler);
}
else if (hasSelectedUsersKVP.Key &&
!hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged"))
{
foreach (KeyValuePair<string, long> user in hasSelectedUsersKVP.Value)
{
string path = orchestrationService.ResolveDownloadPath(user.Key);
Log.Debug($"Download path: {path}");
await orchestrationService.DownloadCreatorContentAsync(
user.Key, user.Value, path, users,
startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing,
eventHandler);
}
DateTime endTime = DateTime.Now;
eventHandler.OnScrapeComplete(endTime - startTime);
}
else if (hasSelectedUsersKVP.Key &&
hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged"))
{
// Config was changed, loop will re-read
}
else
{
break;
}
} while (!config.NonInteractiveMode);
}
private async Task HandleSinglePostDownload(
IDownloadOrchestrationService orchestrationService,
Dictionary<string, long> users,
StartupResult startupResult,
IDownloadEventHandler eventHandler)
{
AnsiConsole.Markup(
"[red]To find an individual post URL, click on the ... at the top right corner of the post and select 'Copy link to post'.\n\nTo return to the main menu, enter 'back' or 'exit' when prompted for the URL.\n\n[/]");
string postUrl = AnsiConsole.Prompt(
new TextPrompt<string>("[red]Please enter a post URL: [/]")
.ValidationErrorMessage("[red]Please enter a valid post URL[/]")
.Validate(url =>
{
Log.Debug($"Single Post URL: {url}");
Regex regex = new("https://onlyfans\\.com/[0-9]+/[A-Za-z0-9]+", RegexOptions.IgnoreCase);
if (regex.IsMatch(url))
{
return ValidationResult.Success();
}
if (url == "" || url == "exit" || url == "back")
{
return ValidationResult.Success();
}
Log.Error("Post URL invalid");
return ValidationResult.Error("[red]Please enter a valid post URL[/]");
}));
if (postUrl != "" && postUrl != "exit" && postUrl != "back")
{
long postId = Convert.ToInt64(postUrl.Split("/")[3]);
string username = postUrl.Split("/")[4];
Log.Debug($"Single Post ID: {postId}");
Log.Debug($"Single Post Creator: {username}");
if (users.ContainsKey(username))
{
string path = orchestrationService.ResolveDownloadPath(username);
Log.Debug($"Download path: {path}");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
AnsiConsole.Markup($"[red]Created folder for {Markup.Escape(username)}\n[/]");
Log.Debug($"Created folder for {username}");
}
else
{
AnsiConsole.Markup($"[red]Folder for {Markup.Escape(username)} already created\n[/]");
}
IDbService dbService = serviceProvider.GetRequiredService<IDbService>();
await dbService.CreateDb(path);
await orchestrationService.DownloadSinglePostAsync(username, postId, path, users,
startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler);
}
}
}
private async Task HandleSingleMessageDownload(
IDownloadOrchestrationService orchestrationService,
Dictionary<string, long> users,
StartupResult startupResult,
IDownloadEventHandler eventHandler)
{
AnsiConsole.Markup(
"[red]To find an individual message URL, note that you can only do so for PPV messages that you have unlocked. Go the main OnlyFans timeline, click on the Purchased tab, find the relevant message, click on the ... at the top right corner of the message, and select 'Copy link to message'. For all other messages, you cannot scrape them individually, you must scrape all messages from that creator.\n\nTo return to the main menu, enter 'back' or 'exit' when prompted for the URL.\n\n[/]");
string messageUrl = AnsiConsole.Prompt(
new TextPrompt<string>("[red]Please enter a message URL: [/]")
.ValidationErrorMessage("[red]Please enter a valid message URL[/]")
.Validate(url =>
{
Log.Debug($"Single Paid Message URL: {url}");
Regex regex = new("https://onlyfans\\.com/my/chats/chat/[0-9]+/\\?firstId=[0-9]+$",
RegexOptions.IgnoreCase);
if (regex.IsMatch(url))
{
return ValidationResult.Success();
}
if (url == "" || url == "back" || url == "exit")
{
return ValidationResult.Success();
}
Log.Error("Message URL invalid");
return ValidationResult.Error("[red]Please enter a valid message URL[/]");
}));
if (messageUrl != "" && messageUrl != "exit" && messageUrl != "back")
{
long messageId = Convert.ToInt64(messageUrl.Split("?firstId=")[1]);
long userId = Convert.ToInt64(messageUrl.Split("/")[6]);
Log.Debug($"Message ID: {messageId}");
Log.Debug($"User ID: {userId}");
string? username = await orchestrationService.ResolveUsernameAsync(userId);
Log.Debug("Content creator: {Username}", username);
if (username == null)
{
Log.Error("Could not resolve username for user ID: {userId}", userId);
AnsiConsole.MarkupLine("[red]Could not resolve username for user ID[/]");
return;
}
string path = orchestrationService.ResolveDownloadPath(username);
Log.Debug($"Download path: {path}");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
AnsiConsole.Markup($"[red]Created folder for {Markup.Escape(username)}\n[/]");
Log.Debug($"Created folder for {username}");
}
else
{
AnsiConsole.Markup($"[red]Folder for {Markup.Escape(username)} already created\n[/]");
Log.Debug($"Folder for {username} already created");
}
IDbService dbService = serviceProvider.GetRequiredService<IDbService>();
await dbService.CreateDb(path);
await orchestrationService.DownloadSinglePaidMessageAsync(username, messageId, path, users,
startupResult.ClientIdBlobMissing, startupResult.DevicePrivateKeyMissing, eventHandler);
}
}
public async Task<(bool IsExit, Dictionary<string, long>? selectedUsers)> HandleUserSelection(
Dictionary<string, long> users, Dictionary<string, long> lists)
{
IConfigService configService = serviceProvider.GetRequiredService<IConfigService>();
IAuthService authService = serviceProvider.GetRequiredService<IAuthService>();
IApiService apiService = serviceProvider.GetRequiredService<IApiService>();
ILoggingService loggingService = serviceProvider.GetRequiredService<ILoggingService>();
bool hasSelectedUsers = false;
Dictionary<string, long> selectedUsers = new();
Config currentConfig = configService.CurrentConfig;
while (!hasSelectedUsers)
{
List<string> mainMenuOptions = GetMainMenuOptions(users, lists);
string mainMenuSelection = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title(
"[red]Select Accounts to Scrape | Select All = All Accounts | List = Download content from users on List | Custom = Specific Account(s)[/]")
.AddChoices(mainMenuOptions)
);
switch (mainMenuSelection)
{
case "[red]Select All[/]":
selectedUsers = users;
hasSelectedUsers = true;
break;
case "[red]List[/]":
while (true)
{
MultiSelectionPrompt<string> listSelectionPrompt = new();
listSelectionPrompt.Title = "[red]Select List[/]";
listSelectionPrompt.PageSize = 10;
listSelectionPrompt.AddChoice("[red]Go Back[/]");
foreach (string key in lists.Keys.Select(k => $"[red]{k}[/]").ToList())
{
listSelectionPrompt.AddChoice(key);
}
List<string> listSelection = AnsiConsole.Prompt(listSelectionPrompt);
if (listSelection.Contains("[red]Go Back[/]"))
{
break;
}
hasSelectedUsers = true;
List<string> listUsernames = new();
foreach (string item in listSelection)
{
long listId = lists[item.Replace("[red]", "").Replace("[/]", "")];
List<string> usernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? [];
foreach (string user in usernames)
{
listUsernames.Add(user);
}
}
selectedUsers = users.Where(x => listUsernames.Contains($"{x.Key}"))
.ToDictionary(x => x.Key, x => x.Value);
AnsiConsole.Markup(string.Format("[red]Downloading from List(s): {0}[/]",
string.Join(", ", listSelection)));
break;
}
break;
case "[red]Custom[/]":
while (true)
{
MultiSelectionPrompt<string> selectedNamesPrompt = new();
selectedNamesPrompt.MoreChoicesText("[grey](Move up and down to reveal more choices)[/]");
selectedNamesPrompt.InstructionsText(
"[grey](Press <space> to select, <enter> to accept)[/]\n[grey](Press A-Z to easily navigate the list)[/]");
selectedNamesPrompt.Title("[red]Select users[/]");
selectedNamesPrompt.PageSize(10);
selectedNamesPrompt.AddChoice("[red]Go Back[/]");
foreach (string key in users.Keys.OrderBy(k => k).Select(k => $"[red]{k}[/]").ToList())
{
selectedNamesPrompt.AddChoice(key);
}
List<string> userSelection = AnsiConsole.Prompt(selectedNamesPrompt);
if (userSelection.Contains("[red]Go Back[/]"))
{
break;
}
hasSelectedUsers = true;
selectedUsers = users.Where(x => userSelection.Contains($"[red]{x.Key}[/]"))
.ToDictionary(x => x.Key, x => x.Value);
break;
}
break;
case "[red]Download Single Post[/]":
return (true, new Dictionary<string, long> { { "SinglePost", 0 } });
case "[red]Download Single Paid Message[/]":
return (true, new Dictionary<string, long> { { "SingleMessage", 0 } });
case "[red]Download Purchased Tab[/]":
return (true, new Dictionary<string, long> { { "PurchasedTab", 0 } });
case "[red]Edit config.conf[/]":
while (true)
{
List<(string Name, bool Value)> toggleableProps = configService.GetToggleableProperties();
List<(string choice, bool isSelected)> choices = new() { ("[red]Go Back[/]", false) };
foreach ((string Name, bool Value) prop in toggleableProps)
{
choices.Add(($"[red]{prop.Name}[/]", prop.Value));
}
MultiSelectionPrompt<string> multiSelectionPrompt = new MultiSelectionPrompt<string>()
.Title("[red]Edit config.conf[/]")
.PageSize(25);
foreach ((string choice, bool isSelected) choice in choices)
{
multiSelectionPrompt.AddChoices(choice.choice, selectionItem =>
{
if (choice.isSelected)
{
selectionItem.Select();
}
});
}
List<string> configOptions = AnsiConsole.Prompt(multiSelectionPrompt);
if (configOptions.Contains("[red]Go Back[/]"))
{
break;
}
// Extract plain names from selections
List<string> selectedNames = configOptions
.Select(o => o.Replace("[red]", "").Replace("[/]", ""))
.ToList();
bool configChanged = configService.ApplyToggleableSelections(selectedNames);
await configService.SaveConfigurationAsync();
currentConfig = configService.CurrentConfig;
if (configChanged)
{
return (true, new Dictionary<string, long> { { "ConfigChanged", 0 } });
}
break;
}
break;
case "[red]Change logging level[/]":
while (true)
{
List<(string choice, bool isSelected)> choices = [("[red]Go Back[/]", false)];
foreach (string name in typeof(LoggingLevel).GetEnumNames())
{
string itemLabel = $"[red]{name}[/]";
choices.Add(new ValueTuple<string, bool>(itemLabel,
name == loggingService.GetCurrentLoggingLevel().ToString()));
}
SelectionPrompt<string> selectionPrompt = new SelectionPrompt<string>()
.Title("[red]Select logging level[/]")
.PageSize(25);
foreach ((string choice, bool isSelected) choice in choices)
{
selectionPrompt.AddChoice(choice.choice);
}
string levelOption = AnsiConsole.Prompt(selectionPrompt);
if (levelOption.Contains("[red]Go Back[/]"))
{
break;
}
levelOption = levelOption.Replace("[red]", "").Replace("[/]", "");
LoggingLevel newLogLevel =
(LoggingLevel)Enum.Parse(typeof(LoggingLevel), levelOption, true);
Log.Debug($"Logging level changed to: {levelOption}");
Config newConfig = currentConfig;
newConfig.LoggingLevel = newLogLevel;
currentConfig = newConfig;
configService.UpdateConfig(newConfig);
await configService.SaveConfigurationAsync();
break;
}
break;
case "[red]Logout and Exit[/]":
authService.Logout();
return (false, null);
case "[red]Exit[/]":
return (false, null);
}
}
return (true, selectedUsers);
}
public static List<string> GetMainMenuOptions(Dictionary<string, long> users, Dictionary<string, long> lists)
{
if (lists.Count > 0)
{
return new List<string>
{
"[red]Select All[/]",
"[red]List[/]",
"[red]Custom[/]",
"[red]Download Single Post[/]",
"[red]Download Single Paid Message[/]",
"[red]Download Purchased Tab[/]",
"[red]Edit config.conf[/]",
"[red]Change logging level[/]",
"[red]Logout and Exit[/]",
"[red]Exit[/]"
};
}
return new List<string>
{
"[red]Select All[/]",
"[red]Custom[/]",
"[red]Download Single Post[/]",
"[red]Download Single Paid Message[/]",
"[red]Download Purchased Tab[/]",
"[red]Edit config.conf[/]",
"[red]Change logging level[/]",
"[red]Logout and Exit[/]",
"[red]Exit[/]"
};
}
private async Task HandleAuthFlow(IAuthService authService, IConfigService configService)
{
if (await authService.LoadFromFileAsync())
{
AnsiConsole.Markup("[green]auth.json located successfully!\n[/]");
}
else if (File.Exists("auth.json"))
{
Log.Information("Auth file found but could not be deserialized");
if (configService.CurrentConfig.NonInteractiveMode)
{
AnsiConsole.MarkupLine(
"\n[red]auth.json has invalid JSON syntax. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n");
AnsiConsole.MarkupLine(
"[red]You may also want to try using the browser extension which is documented here:[/]\n");
AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]");
Environment.Exit(2);
}
if (!configService.CurrentConfig.DisableBrowserAuth)
{
await LoadAuthFromBrowser();
}
else
{
ShowAuthMissingError(configService.CurrentConfig.NonInteractiveMode);
}
}
else
{
if (configService.CurrentConfig.NonInteractiveMode)
{
ShowAuthMissingError(configService.CurrentConfig.NonInteractiveMode);
}
else if (!configService.CurrentConfig.DisableBrowserAuth)
{
await LoadAuthFromBrowser();
}
else
{
ShowAuthMissingError(configService.CurrentConfig.NonInteractiveMode);
}
}
}
private static void ShowAuthMissingError(bool nonInteractiveMode)
{
AnsiConsole.MarkupLine(
"\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n");
AnsiConsole.MarkupLine(
"[red]You may also want to try using the browser extension which is documented here:[/]\n");
AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]");
if (!nonInteractiveMode)
{
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
Console.ReadKey();
}
Environment.Exit(2);
}
private static void DisplayVersionResult(VersionCheckResult result)
{
if (result.TimedOut)
{
AnsiConsole.Markup("[yellow]Version check timed out after 30 seconds.\n[/]");
return;
}
if (result.CheckFailed)
{
AnsiConsole.Markup("[yellow]Failed to verify that OF-DL is up-to-date.\n[/]");
return;
}
if (result.LocalVersion == null || result.LatestVersion == null)
{
// Debug mode or no version info
AnsiConsole.Markup("[yellow]Running in Debug/Local mode. Version check skipped.\n[/]");
return;
}
if (result.IsUpToDate)
{
AnsiConsole.Markup("[green]You are running OF-DL version " +
$"{result.LocalVersion.Major}.{result.LocalVersion.Minor}.{result.LocalVersion.Build}\n[/]");
AnsiConsole.Markup("[green]Latest Release version: " +
$"{result.LatestVersion.Major}.{result.LatestVersion.Minor}.{result.LatestVersion.Build}\n[/]");
}
else
{
AnsiConsole.Markup("[red]You are running OF-DL version " +
$"{result.LocalVersion.Major}.{result.LocalVersion.Minor}.{result.LocalVersion.Build}\n[/]");
AnsiConsole.Markup("[red]Please update to the current release, " +
$"{result.LatestVersion.Major}.{result.LatestVersion.Minor}.{result.LatestVersion.Build}: [link=https://git.ofdl.tools/sim0n00ps/OF-DL/releases]https://git.ofdl.tools/sim0n00ps/OF-DL/releases[/]\n[/]");
}
}
private static void DisplayStartupResult(StartupResult result)
{
// OS
if (result is { IsWindowsVersionValid: true, OsVersionString: not null } &&
EnvironmentHelper.IsRunningOnWindows())
{
AnsiConsole.Markup("[green]Valid version of Windows found.\n[/]");
}
// FFmpeg
if (result.FfmpegFound)
{
AnsiConsole.Markup(
result is { FfmpegPathAutoDetected: true, FfmpegPath: not null }
? $"[green]FFmpeg located successfully. Path auto-detected: {Markup.Escape(result.FfmpegPath)}\n[/]"
: "[green]FFmpeg located successfully\n[/]");
AnsiConsole.Markup(result.FfmpegVersion != null
? $"[green]ffmpeg version detected as {Markup.Escape(result.FfmpegVersion)}[/]\n"
: "[yellow]ffmpeg version could not be parsed[/]\n");
}
// FFprobe
if (result.FfprobeFound)
{
AnsiConsole.Markup(
result is { FfprobePathAutoDetected: true, FfprobePath: not null }
? $"[green]FFprobe located successfully. Path auto-detected: {Markup.Escape(result.FfprobePath)}\n[/]"
: "[green]FFprobe located successfully\n[/]");
AnsiConsole.Markup(result.FfprobeVersion != null
? $"[green]FFprobe version detected as {Markup.Escape(result.FfprobeVersion)}[/]\n"
: "[yellow]FFprobe version could not be parsed[/]\n");
}
// Widevine
if (!result.ClientIdBlobMissing)
{
AnsiConsole.Markup("[green]device_client_id_blob located successfully![/]\n");
}
if (!result.DevicePrivateKeyMissing)
{
AnsiConsole.Markup("[green]device_private_key located successfully![/]\n");
}
if (result.ClientIdBlobMissing || result.DevicePrivateKeyMissing)
{
AnsiConsole.Markup(
"[yellow]device_client_id_blob and/or device_private_key missing, https://ofdl.tools/ will be used instead for DRM protected videos\n[/]");
}
}
private static void DisplayRulesJsonResult(StartupResult result, IConfigService configService)
{
if (result.RulesJsonExists)
{
if (result.RulesJsonValid)
{
AnsiConsole.Markup("[green]rules.json located successfully!\n[/]");
}
else
{
AnsiConsole.MarkupLine("\n[red]rules.json is not valid, check your JSON syntax![/]\n");
AnsiConsole.MarkupLine("[red]Please ensure you are using the latest version of the software.[/]\n");
Log.Error("rules.json processing failed: {Error}", result.RulesJsonError);
if (!configService.CurrentConfig.NonInteractiveMode)
{
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
Console.ReadKey();
}
Environment.Exit(2);
}
}
}
}

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

View File

@ -0,0 +1,16 @@
# Stealth Script Creation
## Requirements
- NodeJS (with npx CLI tool)
## Instructions
- Open a terminal in this directory (OF DL.Cli/chromium-scripts)
- Run `npx -y extract-stealth-evasions`
- Copy the `stealth.js` file into the other chromium-scripts directory as well (`OF DL.Gui/chromium-scripts/`)
## References
See the readme.md file and source code for the stealth
script [here](https://github.com/berstend/puppeteer-extra/tree/master/packages/extract-stealth-evasions).

File diff suppressed because one or more lines are too long

41
OF DL.Cli/rules.json Normal file
View File

@ -0,0 +1,41 @@
{
"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
]
}

View File

@ -0,0 +1,29 @@
using System.Security.Cryptography;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Macs;
using Org.BouncyCastle.Crypto.Parameters;
namespace OF_DL.Crypto;
public class CryptoUtils
{
public static byte[] GetHMACSHA256Digest(byte[] data, byte[] key) => 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(key);
mac.Init(keyParam);
mac.BlockUpdate(data, 0, data.Length);
byte[] outBytes = new byte[16];
mac.DoFinal(outBytes, 0);
return outBytes;
}
}

View File

@ -0,0 +1,127 @@
using System.Security.Cryptography;
namespace OF_DL.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)
{
byte 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)
{
T[] 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;
}
// Commented out since the condition will always be false while emLen = 256 and hLen = 20
// 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", StringComparison.Ordinal) / 2;
byte[] unpadded = db[(hLen + onePos + 1)..];
return unpadded;
}
private static byte[] MGF1(byte[] seed, int maskLen)
{
SHA1 hobj = SHA1.Create();
int hLen = hobj.HashSize / 8;
List<byte> T = new();
for (int i = 0; i < (int)Math.Ceiling(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,7 @@
namespace OF_DL.Enumerations;
public enum CustomFileNameOption
{
ReturnOriginal,
ReturnEmpty
}

View File

@ -0,0 +1,7 @@
namespace OF_DL.Enumerations;
public enum DownloadDateSelection
{
before,
after
}

View File

@ -0,0 +1,34 @@
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,12 @@
namespace OF_DL.Enumerations;
public enum MediaType
{
PaidPosts = 10,
Posts = 20,
Archived = 30,
Stories = 40,
Highlights = 50,
Messages = 60,
PaidMessages = 70
}

View File

@ -0,0 +1,9 @@
// ReSharper disable InconsistentNaming
namespace OF_DL.Enumerations;
public enum Theme
{
light,
dark
}

View File

@ -0,0 +1,8 @@
namespace OF_DL.Enumerations;
public enum VideoResolution
{
_240,
_720,
source
}

View File

@ -0,0 +1,32 @@
namespace OF_DL.Helpers;
public static class Constants
{
public const string DiscordInviteUrl = "https://discord.com/invite/6bUW8EJ53j";
public const string DocumentationUrl = "https://docs.ofdl.tools/";
public const string LegacyAuthDocumentationUrl = "https://docs.ofdl.tools/config/auth/#legacy-methods";
public const string ApiUrl = "https://onlyfans.com/api2/v2";
public const int ApiPageSize = 50;
public const int WidevineRetryDelay = 10;
public const int WidevineMaxRetries = 3;
public const int DrmDownloadMaxRetries = 3;
public const int ApiRetryMaxAttempts = 5;
public const int ApiRetryBaseDelayMs = 500;
public const int ApiRetryMaxDelayMs = 8000;
public const int DownloadRetryMaxAttempts = 4;
public const int DownloadRetryBaseDelayMs = 1000;
public const int DownloadRetryMaxDelayMs = 15000;
}

View File

@ -0,0 +1,14 @@
namespace OF_DL.Helpers;
public static class EnvironmentHelper
{
private const string DockerEnvironmentVariableName = "OFDL_DOCKER";
public static bool IsRunningInDocker()
{
string? dockerValue = Environment.GetEnvironmentVariable(DockerEnvironmentVariableName);
return string.Equals(dockerValue, "true", StringComparison.OrdinalIgnoreCase);
}
public static bool IsRunningOnWindows() => OperatingSystem.IsWindows();
}

View File

@ -0,0 +1,26 @@
using Serilog;
namespace OF_DL.Helpers;
internal static class ExceptionLoggerHelper
{
/// <summary>
/// Logs an exception to the console and Serilog with inner exception details.
/// </summary>
/// <param name="ex">The exception to log.</param>
public static void LogException(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)
{
return;
}
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);
}
}

View File

@ -0,0 +1,60 @@
using Newtonsoft.Json;
using OF_DL.Models.OfdlApi;
using Serilog;
namespace OF_DL.Helpers;
public static class VersionHelper
{
private const string Url = "https://git.ofdl.tools/api/v1/repos/sim0n00ps/OF-DL/releases/latest";
private static readonly HttpClient s_httpClient = new();
public static async Task<string?> GetLatestReleaseTag(CancellationToken cancellationToken = default)
{
Log.Debug("Calling GetLatestReleaseTag");
try
{
HttpResponseMessage response = await s_httpClient.GetAsync(Url, cancellationToken);
if (!response.IsSuccessStatusCode)
{
Log.Debug("GetLatestReleaseTag did not return a Success Status Code");
return null;
}
string body = await response.Content.ReadAsStringAsync(cancellationToken);
Log.Debug("GetLatestReleaseTag API Response: {Body}", body);
LatestReleaseApiResponse? versionCheckResponse =
JsonConvert.DeserializeObject<LatestReleaseApiResponse>(body);
if (versionCheckResponse != null && versionCheckResponse.TagName != "")
{
return versionCheckResponse.TagName;
}
Log.Debug("GetLatestReleaseTag did not return a valid tag name");
return null;
}
catch (OperationCanceledException)
{
throw; // Rethrow timeout exceptions to be handled by the caller
}
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 null;
}
}

21
OF DL.Core/Models/Auth.cs Normal file
View File

@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace OF_DL.Models;
public class Auth
{
[JsonProperty(PropertyName = "USER_ID")]
public string? UserId { get; set; } = "";
[JsonProperty(PropertyName = "USER_AGENT")]
public string? UserAgent { get; set; } = "";
[JsonProperty(PropertyName = "X_BC")] public string? XBc { get; set; } = "";
[JsonProperty(PropertyName = "COOKIE")]
public string? Cookie { get; set; } = "";
[JsonIgnore]
[JsonProperty(PropertyName = "FFMPEG_PATH")]
public string? FfmpegPath { get; set; } = "";
}

View File

@ -0,0 +1,168 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using OF_DL.Enumerations;
using Serilog;
namespace OF_DL.Models.Config;
public class Config : 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; }
[ToggleableConfig] public bool IncludeRestrictedSubscriptions { get; set; }
[ToggleableConfig] public bool SkipAds { get; set; }
public string? DownloadPath { get; set; } = "";
[ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; }
public int? Timeout { get; set; } = -1;
[ToggleableConfig] public bool FolderPerPaidPost { get; set; }
[ToggleableConfig] public bool FolderPerPost { get; set; }
[ToggleableConfig] public bool FolderPerPaidMessage { get; set; }
[ToggleableConfig] public bool FolderPerMessage { get; set; }
[ToggleableConfig] public bool LimitDownloadRate { get; set; }
public int DownloadLimitInMbPerSec { get; set; } = 4;
// Indicates if you want to download only on specific dates.
[ToggleableConfig] public bool DownloadOnlySpecificDates { get; set; }
// 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; }
[ToggleableConfig] public bool DownloadPostsIncrementally { get; set; }
public bool NonInteractiveMode { get; set; }
public string NonInteractiveModeListName { get; set; } = "";
[ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; }
public string? FFmpegPath { get; set; } = "";
public string? FFprobePath { get; set; } = "";
[ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; }
public Dictionary<string, CreatorConfig> CreatorConfigs { get; set; } = new();
[ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; }
public string IgnoredUsersListName { get; set; } = "";
[JsonConverter(typeof(StringEnumConverter))]
public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error;
[JsonConverter(typeof(StringEnumConverter))]
public Theme Theme { get; set; } = Theme.dark;
[ToggleableConfig] public bool HideMissingCdmKeysWarning { get; set; }
[ToggleableConfig] public bool HideShowScrapeSizeWarning { get; set; }
[ToggleableConfig] public bool IgnoreOwnMessages { get; set; }
[ToggleableConfig] public bool DisableBrowserAuth { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source;
public double DrmVideoDurationMatchThreshold { get; set; } = 0.98;
// When enabled, post/message text is stored as-is without XML stripping.
[ToggleableConfig] public bool DisableTextSanitization { get; set; }
public string? PaidPostFileNameFormat { get; set; } = "";
public string? PostFileNameFormat { get; set; } = "";
public string? PaidMessageFileNameFormat { get; set; } = "";
public string? MessageFileNameFormat { get; set; } = "";
public IFileNameFormatConfig GetCreatorFileNameFormatConfig(string username)
{
FileNameFormatConfig combinedFilenameFormatConfig = new()
{
PaidPostFileNameFormat = PaidPostFileNameFormat,
PostFileNameFormat = PostFileNameFormat,
PaidMessageFileNameFormat = PaidMessageFileNameFormat,
MessageFileNameFormat = MessageFileNameFormat
};
if (CreatorConfigs.TryGetValue(username, out CreatorConfig? creatorConfig))
{
if (!string.IsNullOrEmpty(creatorConfig.PaidPostFileNameFormat))
{
combinedFilenameFormatConfig.PaidPostFileNameFormat = creatorConfig.PaidPostFileNameFormat;
}
if (!string.IsNullOrEmpty(creatorConfig.PostFileNameFormat))
{
combinedFilenameFormatConfig.PostFileNameFormat = creatorConfig.PostFileNameFormat;
}
if (!string.IsNullOrEmpty(creatorConfig.PaidMessageFileNameFormat))
{
combinedFilenameFormatConfig.PaidMessageFileNameFormat = creatorConfig.PaidMessageFileNameFormat;
}
if (!string.IsNullOrEmpty(creatorConfig.MessageFileNameFormat))
{
combinedFilenameFormatConfig.MessageFileNameFormat = creatorConfig.MessageFileNameFormat;
}
}
Log.Debug("PaidMessageFilenameFormat: {CombinedConfigPaidMessageFileNameFormat}",
combinedFilenameFormatConfig.PaidMessageFileNameFormat);
Log.Debug("PostFileNameFormat: {CombinedConfigPostFileNameFormat}",
combinedFilenameFormatConfig.PostFileNameFormat);
Log.Debug("MessageFileNameFormat: {CombinedConfigMessageFileNameFormat}",
combinedFilenameFormatConfig.MessageFileNameFormat);
Log.Debug("PaidPostFileNameFormat: {CombinedConfigPaidPostFileNameFormat}",
combinedFilenameFormatConfig.PaidPostFileNameFormat);
return combinedFilenameFormatConfig;
}
private class ShortDateConverter : IsoDateTimeConverter
{
public ShortDateConverter() => DateTimeFormat = "yyyy-MM-dd";
}
}

View File

@ -0,0 +1,12 @@
namespace OF_DL.Models.Config;
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,12 @@
namespace OF_DL.Models.Config;
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,12 @@
namespace OF_DL.Models.Config;
public interface IFileNameFormatConfig
{
string? PaidPostFileNameFormat { get; set; }
string? PostFileNameFormat { get; set; }
string? PaidMessageFileNameFormat { get; set; }
string? MessageFileNameFormat { get; set; }
}

View File

@ -0,0 +1,4 @@
namespace OF_DL.Models.Config;
[AttributeUsage(AttributeTargets.Property)]
internal class ToggleableConfigAttribute : Attribute;

View File

@ -0,0 +1,36 @@
namespace OF_DL.Models.Downloads;
public class CreatorDownloadResult
{
public int PaidPostCount { get; set; }
public int PostCount { get; set; }
public int ArchivedCount { get; set; }
public int StreamsCount { get; set; }
public int StoriesCount { get; set; }
public int HighlightsCount { get; set; }
public int MessagesCount { get; set; }
public int PaidMessagesCount { get; set; }
}
public class UserListResult
{
public Dictionary<string, long> Users { get; set; } = new();
public Dictionary<string, long> Lists { get; set; } = new();
public string? IgnoredListError { get; set; }
}
public class ListUserSelectionResult
{
public Dictionary<string, long> SelectedUsers { get; set; } = new();
public List<string> UnavailableUsernames { get; set; } = [];
}

View File

@ -0,0 +1,37 @@
namespace OF_DL.Models.Downloads;
/// <summary>
/// Represents the result of a download operation.
/// </summary>
public class DownloadResult
{
/// <summary>
/// Total number of media items processed.
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Number of newly downloaded media items.
/// </summary>
public int NewDownloads { get; set; }
/// <summary>
/// Number of media items that were already downloaded.
/// </summary>
public int ExistingDownloads { get; set; }
/// <summary>
/// The type of media downloaded (e.g., "Posts", "Messages", "Stories", etc.).
/// </summary>
public string MediaType { get; set; } = string.Empty;
/// <summary>
/// Indicates whether the download operation was successful.
/// </summary>
public bool Success { get; set; } = true;
/// <summary>
/// Optional error message if the download failed.
/// </summary>
public string? ErrorMessage { get; set; }
}

View File

@ -0,0 +1,17 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Archived;
public class ArchivedDto
{
[JsonProperty("list")] public List<ListItemDto> List { get; set; } = [];
[JsonProperty("hasMore")] public bool HasMore { get; set; }
[JsonProperty("headMarker")] public string HeadMarker { get; set; } = "";
[JsonProperty("tailMarker")] public string TailMarker { get; set; } = "";
[JsonProperty("counters")] public CountersDto Counters { get; set; } = new();
}

View File

@ -0,0 +1,11 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Archived;
public class InfoDto
{
[JsonProperty("source")] public SourceDto Source { get; set; } = new();
[JsonProperty("preview")] public PreviewDto Preview { get; set; } = new();
}

View File

@ -0,0 +1,96 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
using OF_DL.Utils;
namespace OF_DL.Models.Dtos.Archived;
public class LinkedPostDto
{
private string _rawText = "";
[JsonProperty("responseType")] public string ResponseType { get; set; } = "";
[JsonProperty("id")] public long? Id { get; set; }
[JsonProperty("postedAt")] public DateTime? PostedAt { get; set; }
[JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = "";
[JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new();
[JsonProperty("author")] public AuthorDto Author { get; set; } = new();
[JsonProperty("text")] public string Text { get; set; } = "";
[JsonProperty("rawText")]
public string RawText
{
get
{
if (string.IsNullOrEmpty(_rawText))
{
_rawText = XmlUtils.EvaluateInnerText(Text);
}
return _rawText;
}
set => _rawText = value;
}
[JsonProperty("lockedText")] public bool? LockedText { get; set; }
[JsonProperty("isFavorite")] public bool? IsFavorite { get; set; }
[JsonProperty("canReport")] public bool? CanReport { get; set; }
[JsonProperty("canDelete")] public bool? CanDelete { get; set; }
[JsonProperty("canComment")] public bool? CanComment { get; set; }
[JsonProperty("canEdit")] public bool? CanEdit { get; set; }
[JsonProperty("isPinned")] public bool? IsPinned { get; set; }
[JsonProperty("favoritesCount")] public int? FavoritesCount { get; set; }
[JsonProperty("mediaCount")] public int? MediaCount { get; set; }
[JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; }
[JsonProperty("voting")] public object Voting { get; set; } = new();
[JsonProperty("isOpened")] public bool? IsOpened { get; set; }
[JsonProperty("canToggleFavorite")] public bool? CanToggleFavorite { get; set; }
[JsonProperty("streamId")] public object StreamId { get; set; } = new();
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("hasVoting")] public bool? HasVoting { get; set; }
[JsonProperty("isAddedToBookmarks")] public bool? IsAddedToBookmarks { get; set; }
[JsonProperty("isArchived")] public bool? IsArchived { get; set; }
[JsonProperty("isPrivateArchived")] public bool? IsPrivateArchived { get; set; }
[JsonProperty("isDeleted")] public bool? IsDeleted { get; set; }
[JsonProperty("hasUrl")] public bool? HasUrl { get; set; }
[JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; }
[JsonProperty("cantCommentReason")] public string CantCommentReason { get; set; } = "";
[JsonProperty("commentsCount")] public int? CommentsCount { get; set; }
[JsonProperty("mentionedUsers")] public List<object> MentionedUsers { get; set; } = [];
[JsonProperty("linkedUsers")] public List<object> LinkedUsers { get; set; } = [];
[JsonProperty("media")] public List<MediumDto> Media { get; set; } = [];
[JsonProperty("canViewMedia")] public bool? CanViewMedia { get; set; }
[JsonProperty("preview")] public List<object> Preview { get; set; } = [];
}

View File

@ -0,0 +1,97 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
using OF_DL.Utils;
namespace OF_DL.Models.Dtos.Archived;
public class ListItemDto
{
private string _rawText = "";
[JsonProperty("responseType")] public string ResponseType { get; set; } = "";
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("postedAt")] public DateTime PostedAt { get; set; }
[JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = "";
[JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new();
[JsonProperty("author")] public AuthorDto Author { get; set; } = new();
[JsonProperty("text")] public string Text { get; set; } = "";
[JsonProperty("rawText")]
public string RawText
{
get
{
if (string.IsNullOrEmpty(_rawText))
{
_rawText = XmlUtils.EvaluateInnerText(Text);
}
return _rawText;
}
set => _rawText = value;
}
[JsonProperty("lockedText")] public bool? LockedText { get; set; }
[JsonProperty("isFavorite")] public bool? IsFavorite { get; set; }
[JsonProperty("canReport")] public bool? CanReport { get; set; }
[JsonProperty("canDelete")] public bool? CanDelete { get; set; }
[JsonProperty("canComment")] public bool? CanComment { get; set; }
[JsonProperty("canEdit")] public bool? CanEdit { get; set; }
[JsonProperty("isPinned")] public bool? IsPinned { get; set; }
[JsonProperty("favoritesCount")] public int? FavoritesCount { get; set; }
[JsonProperty("mediaCount")] public int? MediaCount { get; set; }
[JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; }
[JsonProperty("voting")] public object Voting { get; set; } = new();
[JsonProperty("isOpened")] public bool IsOpened { get; set; }
[JsonProperty("canToggleFavorite")] public bool? CanToggleFavorite { get; set; }
[JsonProperty("streamId")] public object StreamId { get; set; } = new();
[JsonProperty("price")] public string Price { get; set; } = "";
[JsonProperty("hasVoting")] public bool? HasVoting { get; set; }
[JsonProperty("isAddedToBookmarks")] public bool? IsAddedToBookmarks { get; set; }
[JsonProperty("isArchived")] public bool IsArchived { get; set; }
[JsonProperty("isPrivateArchived")] public bool? IsPrivateArchived { get; set; }
[JsonProperty("isDeleted")] public bool? IsDeleted { get; set; }
[JsonProperty("hasUrl")] public bool? HasUrl { get; set; }
[JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; }
[JsonProperty("commentsCount")] public int? CommentsCount { get; set; }
[JsonProperty("mentionedUsers")] public List<object> MentionedUsers { get; set; } = [];
[JsonProperty("linkedUsers")] public List<object> LinkedUsers { get; set; } = [];
[JsonProperty("media")] public List<MediumDto> Media { get; set; } = [];
[JsonProperty("canViewMedia")] public bool? CanViewMedia { get; set; }
[JsonProperty("preview")] public List<object> Preview { get; set; } = [];
[JsonProperty("cantCommentReason")] public string CantCommentReason { get; set; } = "";
}

View File

@ -0,0 +1,35 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Archived;
public class MediumDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("convertedToVideo")] public bool? ConvertedToVideo { get; set; }
[JsonProperty("canView")] public bool CanView { get; set; }
[JsonProperty("hasError")] public bool? HasError { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; } = new();
[JsonProperty("info")] public InfoDto Info { get; set; } = new();
[JsonProperty("source")] public SourceDto Source { get; set; } = new();
[JsonProperty("squarePreview")] public string SquarePreview { get; set; } = "";
[JsonProperty("full")] public string Full { get; set; } = "";
[JsonProperty("preview")] public string Preview { get; set; } = "";
[JsonProperty("thumb")] public string Thumb { get; set; } = "";
[JsonProperty("files")] public FilesDto Files { get; set; } = new();
[JsonProperty("videoSources")] public VideoSourcesDto VideoSources { get; set; } = new();
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class AuthorDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("_view")] public string View { get; set; } = "";
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class AvatarThumbsDto
{
[JsonProperty("c50")] public string C50 { get; set; } = "";
[JsonProperty("c144")] public string C144 { get; set; } = "";
}

View File

@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class CountersDto
{
[JsonProperty("audiosCount")] public int? AudiosCount { get; set; }
[JsonProperty("photosCount")] public int? PhotosCount { get; set; }
[JsonProperty("videosCount")] public int? VideosCount { get; set; }
[JsonProperty("mediasCount")] public int? MediasCount { get; set; }
[JsonProperty("postsCount")] public int? PostsCount { get; set; }
[JsonProperty("streamsCount")] public int? StreamsCount { get; set; }
[JsonProperty("archivedPostsCount")] public int? ArchivedPostsCount { get; set; }
}

View File

@ -0,0 +1,13 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class DashDto
{
[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,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class DrmDto
{
[JsonProperty("manifest")] public ManifestDto Manifest { get; set; } = new();
[JsonProperty("signature")] public SignatureDto Signature { get; set; } = new();
}

View File

@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class FilesDto
{
[JsonProperty("full")] public FullDto Full { get; set; } = new();
[JsonProperty("thumb")] public ThumbDto Thumb { get; set; } = new();
[JsonProperty("preview")] public PreviewDto Preview { get; set; } = new();
[JsonProperty("squarePreview")] public SquarePreviewDto SquarePreview { get; set; } = new();
[JsonProperty("drm")] public DrmDto? Drm { get; set; }
}

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class FullDto
{
[JsonProperty("url")] public string Url { get; set; } = "";
[JsonProperty("width")] public int Width { get; set; }
[JsonProperty("height")] public int Height { get; set; }
[JsonProperty("size")] public long Size { get; set; }
[JsonProperty("sources")] public List<object> Sources { get; set; } = [];
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class HeaderSizeDto
{
[JsonProperty("width")] public int Width { get; set; }
[JsonProperty("height")] public int Height { get; set; }
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class HeaderThumbsDto
{
[JsonProperty("w480")] public string W480 { get; set; } = "";
[JsonProperty("w760")] public string W760 { get; set; } = "";
}

View File

@ -0,0 +1,13 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class HlsDto
{
[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,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class ManifestDto
{
[JsonProperty("hls")] public string Hls { get; set; } = "";
[JsonProperty("dash")] public string Dash { get; set; } = "";
}

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class PreviewDto
{
[JsonProperty("width")] public int? Width { get; set; }
[JsonProperty("height")] public int? Height { get; set; }
[JsonProperty("size")] public int? Size { get; set; }
[JsonProperty("url")] public string Url { get; set; } = "";
[JsonProperty("sources")] public SourcesDto Sources { get; set; } = new();
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class SignatureDto
{
[JsonProperty("hls")] public HlsDto Hls { get; set; } = new();
[JsonProperty("dash")] public DashDto Dash { get; set; } = new();
}

View File

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class SourceDto
{
[JsonProperty("url")] public string Url { get; set; } = "";
[JsonProperty("width")] public int Width { get; set; }
[JsonProperty("height")] public int Height { get; set; }
[JsonProperty("duration")] public int Duration { get; set; }
[JsonProperty("size")] public long Size { get; set; }
[JsonProperty("sources")] public SourcesDto Sources { get; set; } = new();
}

View File

@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class SourcesDto
{
[JsonProperty("720")] public string _720 { get; set; } = "";
[JsonProperty("240")] public string _240 { get; set; } = "";
[JsonProperty("w150")] public string W150 { get; set; } = "";
[JsonProperty("w480")] public string W480 { get; set; } = "";
}

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class SquarePreviewDto
{
[JsonProperty("url")] public string Url { get; set; } = "";
[JsonProperty("width")] public int Width { get; set; }
[JsonProperty("height")] public int Height { get; set; }
[JsonProperty("size")] public long Size { get; set; }
[JsonProperty("sources")] public SourcesDto Sources { get; set; } = new();
}

View File

@ -0,0 +1,44 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Subscriptions;
namespace OF_DL.Models.Dtos.Common;
public class SubscribedByDataDto
{
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("newPrice")] public string? NewPrice { get; set; }
[JsonProperty("regularPrice")] public string? RegularPrice { get; set; }
[JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; }
[JsonProperty("discountPercent")] public int? DiscountPercent { get; set; }
[JsonProperty("discountPeriod")] public int? DiscountPeriod { get; set; }
[JsonProperty("subscribeAt")] public DateTime? SubscribeAt { get; set; }
[JsonProperty("expiredAt")] public DateTime? ExpiredAt { get; set; }
[JsonProperty("renewedAt")] public DateTime? RenewedAt { get; set; }
[JsonProperty("discountFinishedAt")] public object? DiscountFinishedAt { get; set; } = new();
[JsonProperty("discountStartedAt")] public object? DiscountStartedAt { get; set; } = new();
[JsonProperty("status")] public string Status { get; set; } = "";
[JsonProperty("isMuted")] public bool? IsMuted { get; set; }
[JsonProperty("unsubscribeReason")] public string UnsubscribeReason { get; set; } = "";
[JsonProperty("duration")] public string Duration { get; set; } = "";
[JsonProperty("showPostsInFeed")] public bool? ShowPostsInFeed { get; set; }
[JsonProperty("subscribes")] public List<SubscribeDto> Subscribes { get; set; } = [];
[JsonProperty("hasActivePaidSubscriptions")]
public bool? HasActivePaidSubscriptions { get; set; }
}

View File

@ -0,0 +1,54 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Subscriptions;
namespace OF_DL.Models.Dtos.Common;
public class SubscribedOnDataDto
{
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("newPrice")] public string? NewPrice { get; set; }
[JsonProperty("regularPrice")] public string? RegularPrice { get; set; }
[JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; }
[JsonProperty("discountPercent")] public int? DiscountPercent { get; set; }
[JsonProperty("discountPeriod")] public int? DiscountPeriod { get; set; }
[JsonProperty("subscribeAt")] public DateTime? SubscribeAt { get; set; }
[JsonProperty("expiredAt")] public DateTime? ExpiredAt { get; set; }
[JsonProperty("renewedAt")] public DateTime? RenewedAt { get; set; }
[JsonProperty("discountFinishedAt")] public object? DiscountFinishedAt { get; set; } = new();
[JsonProperty("discountStartedAt")] public object? DiscountStartedAt { get; set; } = new();
[JsonProperty("status")] public object? Status { get; set; }
[JsonProperty("isMuted")] public bool? IsMuted { get; set; }
[JsonProperty("unsubscribeReason")] public string? UnsubscribeReason { get; set; } = "";
[JsonProperty("duration")] public string Duration { get; set; } = "";
[JsonProperty("tipsSumm")] public string? TipsSumm { get; set; }
[JsonProperty("subscribesSumm")] public string? SubscribesSumm { get; set; }
[JsonProperty("messagesSumm")] public string? MessagesSumm { get; set; }
[JsonProperty("postsSumm")] public string? PostsSumm { get; set; }
[JsonProperty("streamsSumm")] public string? StreamsSumm { get; set; }
[JsonProperty("totalSumm")] public string? TotalSumm { get; set; }
[JsonProperty("subscribes")] public List<SubscribeDto> Subscribes { get; set; } = [];
[JsonProperty("hasActivePaidSubscriptions")]
public bool? HasActivePaidSubscriptions { get; set; }
}

View File

@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class ThumbDto
{
[JsonProperty("url")] public string Url { get; set; } = "";
[JsonProperty("width")] public int Width { get; set; }
[JsonProperty("height")] public int Height { get; set; }
[JsonProperty("size")] public long Size { get; set; }
}

View File

@ -0,0 +1,8 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class VideoDto
{
[JsonProperty("mp4")] public string Mp4 { get; set; } = "";
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Common;
public class VideoSourcesDto
{
[JsonProperty("720")] public string _720 { get; set; } = "";
[JsonProperty("240")] public string _240 { get; set; } = "";
}

View File

@ -0,0 +1,22 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Highlights;
public class HighlightMediaDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("userId")] public long UserId { get; set; }
[JsonProperty("title")] public string Title { get; set; } = "";
[JsonProperty("coverStoryId")] public long CoverStoryId { get; set; }
[JsonProperty("cover")] public string Cover { get; set; } = "";
[JsonProperty("storiesCount")] public int StoriesCount { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("stories")] public List<StoryDto> Stories { get; set; } = [];
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Highlights;
public class HighlightsDto
{
[JsonProperty("list")] public List<ListItemDto> List { get; set; } = [];
[JsonProperty("hasMore")] public bool HasMore { get; set; }
}

View File

@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Highlights;
public class ListItemDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("userId")] public long UserId { get; set; }
[JsonProperty("title")] public string Title { get; set; } = "";
[JsonProperty("coverStoryId")] public long CoverStoryId { get; set; }
[JsonProperty("cover")] public string Cover { get; set; } = "";
[JsonProperty("storiesCount")] public int StoriesCount { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
}

View File

@ -0,0 +1,21 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Highlights;
public class MediumDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("convertedToVideo")] public bool ConvertedToVideo { get; set; }
[JsonProperty("canView")] public bool CanView { get; set; }
[JsonProperty("hasError")] public bool HasError { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("files")] public FilesDto Files { get; set; } = new();
}

View File

@ -0,0 +1,24 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Highlights;
public class StoryDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("userId")] public long UserId { get; set; }
[JsonProperty("isWatched")] public bool IsWatched { get; set; }
[JsonProperty("isReady")] public bool IsReady { get; set; }
[JsonProperty("media")] public List<MediumDto> Media { get; set; } = [];
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("question")] public object Question { get; set; } = new();
[JsonProperty("canLike")] public bool CanLike { get; set; }
[JsonProperty("isLiked")] public bool IsLiked { get; set; }
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class HeaderSizeDto
{
[JsonProperty("width")] public int? Width { get; set; }
[JsonProperty("height")] public int? Height { get; set; }
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class HeaderThumbsDto
{
[JsonProperty("w480")] public string W480 { get; set; } = "";
[JsonProperty("w760")] public string W760 { get; set; } = "";
}

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class ListsStateDto
{
[JsonProperty("id")] public string Id { get; set; } = "";
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("name")] public string Name { get; set; } = "";
[JsonProperty("hasUser")] public bool HasUser { get; set; }
[JsonProperty("canAddUser")] public bool CanAddUser { get; set; }
}

View File

@ -0,0 +1,38 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class SubscribeDto
{
[JsonProperty("id")] public object Id { get; set; } = new();
[JsonProperty("userId")] public long? UserId { get; set; }
[JsonProperty("subscriberId")] public int? SubscriberId { get; set; }
[JsonProperty("date")] public DateTime? Date { get; set; }
[JsonProperty("duration")] public int? Duration { get; set; }
[JsonProperty("startDate")] public DateTime? StartDate { get; set; }
[JsonProperty("expireDate")] public DateTime? ExpireDate { get; set; }
[JsonProperty("cancelDate")] public object CancelDate { get; set; } = new();
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("regularPrice")] public string? RegularPrice { get; set; }
[JsonProperty("discount")] public string? Discount { get; set; }
[JsonProperty("action")] public string Action { get; set; } = "";
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("offerStart")] public object OfferStart { get; set; } = new();
[JsonProperty("offerEnd")] public object OfferEnd { get; set; } = new();
[JsonProperty("isCurrent")] public bool? IsCurrent { get; set; }
}

View File

@ -0,0 +1,40 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class SubscribedByDataDto
{
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("newPrice")] public string? NewPrice { get; set; }
[JsonProperty("regularPrice")] public string? RegularPrice { get; set; }
[JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; }
[JsonProperty("discountPercent")] public string? DiscountPercent { get; set; }
[JsonProperty("discountPeriod")] public string? DiscountPeriod { get; set; }
[JsonProperty("subscribeAt")] public DateTime? SubscribeAt { get; set; }
[JsonProperty("expiredAt")] public DateTime? ExpiredAt { get; set; }
[JsonProperty("renewedAt")] public object RenewedAt { get; set; } = new();
[JsonProperty("discountFinishedAt")] public object DiscountFinishedAt { get; set; } = new();
[JsonProperty("discountStartedAt")] public object DiscountStartedAt { get; set; } = new();
[JsonProperty("status")] public string Status { get; set; } = "";
[JsonProperty("isMuted")] public bool? IsMuted { get; set; }
[JsonProperty("unsubscribeReason")] public string UnsubscribeReason { get; set; } = "";
[JsonProperty("duration")] public string Duration { get; set; } = "";
[JsonProperty("showPostsInFeed")] public bool? ShowPostsInFeed { get; set; }
[JsonProperty("subscribes")] public List<SubscribeDto> Subscribes { get; set; } = [];
}

View File

@ -0,0 +1,54 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class SubscribedOnDataDto
{
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("newPrice")] public string? NewPrice { get; set; }
[JsonProperty("regularPrice")] public string? RegularPrice { get; set; }
[JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; }
[JsonProperty("discountPercent")] public string? DiscountPercent { get; set; }
[JsonProperty("discountPeriod")] public string? DiscountPeriod { get; set; }
[JsonProperty("subscribeAt")] public DateTime? SubscribeAt { get; set; }
[JsonProperty("expiredAt")] public DateTime? ExpiredAt { get; set; }
[JsonProperty("renewedAt")] public object RenewedAt { get; set; } = new();
[JsonProperty("discountFinishedAt")] public object DiscountFinishedAt { get; set; } = new();
[JsonProperty("discountStartedAt")] public object DiscountStartedAt { get; set; } = new();
[JsonProperty("status")] public object Status { get; set; } = new();
[JsonProperty("isMuted")] public bool? IsMuted { get; set; }
[JsonProperty("unsubscribeReason")] public string UnsubscribeReason { get; set; } = "";
[JsonProperty("duration")] public string Duration { get; set; } = "";
[JsonProperty("tipsSumm")] public string? TipsSumm { get; set; }
[JsonProperty("subscribesSumm")] public string? SubscribesSumm { get; set; }
[JsonProperty("messagesSumm")] public string? MessagesSumm { get; set; }
[JsonProperty("postsSumm")] public string? PostsSumm { get; set; }
[JsonProperty("streamsSumm")] public string? StreamsSumm { get; set; }
[JsonProperty("totalSumm")] public string? TotalSumm { get; set; }
[JsonProperty("lastActivity")] public DateTime? LastActivity { get; set; }
[JsonProperty("recommendations")] public int? Recommendations { get; set; }
[JsonProperty("subscribes")] public List<object> Subscribes { get; set; } = [];
}

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class SubscriptionBundleDto
{
[JsonProperty("id")] public long? Id { get; set; }
[JsonProperty("discount")] public string? Discount { get; set; }
[JsonProperty("duration")] public string? Duration { get; set; }
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("canBuy")] public bool? CanBuy { get; set; }
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class UserListDto
{
[JsonProperty("list")] public List<UserListItemDto> List { get; set; } = [];
[JsonProperty("hasMore")] public bool? HasMore { get; set; }
}

View File

@ -0,0 +1,42 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class UserListItemDto
{
[JsonProperty("id")] public string Id { get; set; } = "";
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("name")] public string Name { get; set; } = "";
[JsonProperty("usersCount")] public int? UsersCount { get; set; }
[JsonProperty("postsCount")] public int? PostsCount { get; set; }
[JsonProperty("canUpdate")] public bool? CanUpdate { get; set; }
[JsonProperty("canDelete")] public bool? CanDelete { get; set; }
[JsonProperty("canManageUsers")] public bool? CanManageUsers { get; set; }
[JsonProperty("canAddUsers")] public bool? CanAddUsers { get; set; }
[JsonProperty("canPinnedToFeed")] public bool? CanPinnedToFeed { get; set; }
[JsonProperty("isPinnedToFeed")] public bool? IsPinnedToFeed { get; set; }
[JsonProperty("canPinnedToChat")] public bool? CanPinnedToChat { get; set; }
[JsonProperty("isPinnedToChat")] public bool? IsPinnedToChat { get; set; }
[JsonProperty("order")] public string Order { get; set; } = "";
[JsonProperty("direction")] public string Direction { get; set; } = "";
[JsonProperty("users")] public List<UserListUserDto> Users { get; set; } = [];
[JsonProperty("customOrderUsersIds")] public List<object> CustomOrderUsersIds { get; set; } = [];
[JsonProperty("posts")] public List<object> Posts { get; set; } = [];
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Lists;
public class UserListUserDto
{
[JsonProperty("id")] public long? Id { get; set; }
[JsonProperty("_view")] public string View { get; set; } = "";
}

View File

@ -0,0 +1,121 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Lists;
public class UsersListDto
{
[JsonProperty("view")] public string View { get; set; } = "";
[JsonProperty("avatar")] public string Avatar { get; set; } = "";
[JsonProperty("avatarThumbs")] public AvatarThumbsDto AvatarThumbs { get; set; } = new();
[JsonProperty("header")] public string Header { get; set; } = "";
[JsonProperty("headerSize")] public HeaderSizeDto HeaderSize { get; set; } = new();
[JsonProperty("headerThumbs")] public HeaderThumbsDto HeaderThumbs { get; set; } = new();
[JsonProperty("id")] public long? Id { get; set; }
[JsonProperty("name")] public string Name { get; set; } = "";
[JsonProperty("username")] public string Username { get; set; } = "";
[JsonProperty("canLookStory")] public bool? CanLookStory { get; set; }
[JsonProperty("canCommentStory")] public bool? CanCommentStory { get; set; }
[JsonProperty("hasNotViewedStory")] public bool? HasNotViewedStory { get; set; }
[JsonProperty("isVerified")] public bool? IsVerified { get; set; }
[JsonProperty("canPayInternal")] public bool? CanPayInternal { get; set; }
[JsonProperty("hasScheduledStream")] public bool? HasScheduledStream { get; set; }
[JsonProperty("hasStream")] public bool? HasStream { get; set; }
[JsonProperty("hasStories")] public bool? HasStories { get; set; }
[JsonProperty("tipsEnabled")] public bool? TipsEnabled { get; set; }
[JsonProperty("tipsTextEnabled")] public bool? TipsTextEnabled { get; set; }
[JsonProperty("tipsMin")] public int? TipsMin { get; set; }
[JsonProperty("tipsMinInternal")] public int? TipsMinInternal { get; set; }
[JsonProperty("tipsMax")] public int? TipsMax { get; set; }
[JsonProperty("canEarn")] public bool? CanEarn { get; set; }
[JsonProperty("canAddSubscriber")] public bool? CanAddSubscriber { get; set; }
[JsonProperty("subscribePrice")] public string? SubscribePrice { get; set; }
[JsonProperty("subscriptionBundles")] public List<SubscriptionBundleDto> SubscriptionBundles { get; set; } = [];
[JsonProperty("displayName")] public string DisplayName { get; set; } = "";
[JsonProperty("notice")] public string Notice { get; set; } = "";
[JsonProperty("isPaywallRequired")] public bool? IsPaywallRequired { get; set; }
[JsonProperty("unprofitable")] public bool? Unprofitable { get; set; }
[JsonProperty("listsStates")] public List<ListsStateDto> ListsStates { get; set; } = [];
[JsonProperty("isMuted")] public bool? IsMuted { get; set; }
[JsonProperty("isRestricted")] public bool? IsRestricted { get; set; }
[JsonProperty("canRestrict")] public bool? CanRestrict { get; set; }
[JsonProperty("subscribedBy")] public bool? SubscribedBy { get; set; }
[JsonProperty("subscribedByExpire")] public bool? SubscribedByExpire { get; set; }
[JsonProperty("subscribedByExpireDate")]
public DateTime? SubscribedByExpireDate { get; set; }
[JsonProperty("subscribedByAutoprolong")]
public bool? SubscribedByAutoprolong { get; set; }
[JsonProperty("subscribedIsExpiredNow")]
public bool? SubscribedIsExpiredNow { get; set; }
[JsonProperty("currentSubscribePrice")]
public string? CurrentSubscribePrice { get; set; }
[JsonProperty("subscribedOn")] public bool? SubscribedOn { get; set; }
[JsonProperty("subscribedOnExpiredNow")]
public bool? SubscribedOnExpiredNow { get; set; }
[JsonProperty("subscribedOnDuration")] public string SubscribedOnDuration { get; set; } = "";
[JsonProperty("canReport")] public bool? CanReport { get; set; }
[JsonProperty("canReceiveChatMessage")]
public bool? CanReceiveChatMessage { get; set; }
[JsonProperty("hideChat")] public bool? HideChat { get; set; }
[JsonProperty("lastSeen")] public DateTime? LastSeen { get; set; }
[JsonProperty("isPerformer")] public bool? IsPerformer { get; set; }
[JsonProperty("isRealPerformer")] public bool? IsRealPerformer { get; set; }
[JsonProperty("subscribedByData")] public SubscribedByDataDto SubscribedByData { get; set; } = new();
[JsonProperty("subscribedOnData")] public SubscribedOnDataDto SubscribedOnData { get; set; } = new();
[JsonProperty("canTrialSend")] public bool? CanTrialSend { get; set; }
[JsonProperty("isBlocked")] public bool? IsBlocked { get; set; }
[JsonProperty("promoOffers")] public List<object> PromoOffers { get; set; } = [];
}

View File

@ -0,0 +1,98 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Messages;
public class FromUserDto
{
[JsonProperty("_view")] public string ViewRaw { get; set; } = "";
[JsonProperty("view")] public string View { get; set; } = "";
[JsonProperty("avatar")] public string Avatar { get; set; } = "";
[JsonProperty("avatarThumbs")] public AvatarThumbsDto AvatarThumbs { get; set; } = new();
[JsonProperty("header")] public string Header { get; set; } = "";
[JsonProperty("headerSize")] public HeaderSizeDto HeaderSize { get; set; } = new();
[JsonProperty("headerThumbs")] public HeaderThumbsDto HeaderThumbs { get; set; } = new();
[JsonProperty("id")] public long? Id { get; set; }
[JsonProperty("name")] public string Name { get; set; } = "";
[JsonProperty("username")] public string Username { get; set; } = "";
[JsonProperty("canLookStory")] public bool CanLookStory { get; set; }
[JsonProperty("canCommentStory")] public bool CanCommentStory { get; set; }
[JsonProperty("hasNotViewedStory")] public bool HasNotViewedStory { get; set; }
[JsonProperty("isVerified")] public bool IsVerified { get; set; }
[JsonProperty("canPayInternal")] public bool CanPayInternal { get; set; }
[JsonProperty("hasScheduledStream")] public bool HasScheduledStream { get; set; }
[JsonProperty("hasStream")] public bool HasStream { get; set; }
[JsonProperty("hasStories")] public bool HasStories { get; set; }
[JsonProperty("tipsEnabled")] public bool TipsEnabled { get; set; }
[JsonProperty("tipsTextEnabled")] public bool TipsTextEnabled { get; set; }
[JsonProperty("tipsMin")] public int TipsMin { get; set; }
[JsonProperty("tipsMinInternal")] public int TipsMinInternal { get; set; }
[JsonProperty("tipsMax")] public int TipsMax { get; set; }
[JsonProperty("canEarn")] public bool CanEarn { get; set; }
[JsonProperty("canAddSubscriber")] public bool CanAddSubscriber { get; set; }
[JsonProperty("subscribePrice")] public string SubscribePrice { get; set; } = "";
[JsonProperty("subscriptionBundles")] public List<object> SubscriptionBundles { get; set; } = [];
[JsonProperty("isPaywallRequired")] public bool IsPaywallRequired { get; set; }
[JsonProperty("listsStates")] public List<ListsStateDto> ListsStates { get; set; } = [];
[JsonProperty("isRestricted")] public bool IsRestricted { get; set; }
[JsonProperty("canRestrict")] public bool CanRestrict { get; set; }
[JsonProperty("subscribedBy")] public object SubscribedBy { get; set; } = new();
[JsonProperty("subscribedByExpire")] public object SubscribedByExpire { get; set; } = new();
[JsonProperty("subscribedByExpireDate")]
public DateTime? SubscribedByExpireDate { get; set; }
[JsonProperty("subscribedByAutoprolong")]
public object SubscribedByAutoprolong { get; set; } = new();
[JsonProperty("subscribedIsExpiredNow")]
public bool SubscribedIsExpiredNow { get; set; }
[JsonProperty("currentSubscribePrice")]
public object CurrentSubscribePrice { get; set; } = new();
[JsonProperty("subscribedOn")] public object SubscribedOn { get; set; } = new();
[JsonProperty("subscribedOnExpiredNow")]
public object SubscribedOnExpiredNow { get; set; } = new();
[JsonProperty("subscribedOnDuration")] public object SubscribedOnDuration { get; set; } = new();
[JsonProperty("callPrice")] public int CallPrice { get; set; }
[JsonProperty("lastSeen")] public DateTime? LastSeen { get; set; }
[JsonProperty("canReport")] public bool CanReport { get; set; }
}

View File

@ -0,0 +1,11 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Messages;
public class InfoDto
{
[JsonProperty("source")] public SourceDto Source { get; set; } = new();
[JsonProperty("preview")] public PreviewDto Preview { get; set; } = new();
}

View File

@ -0,0 +1,66 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Messages;
public class ListItemDto
{
[JsonProperty("responseType")] public string ResponseType { get; set; } = "";
[JsonProperty("text")] public string Text { get; set; } = "";
[JsonProperty("giphyId")] public object GiphyId { get; set; } = new();
[JsonProperty("lockedText")] public bool? LockedText { get; set; }
[JsonProperty("isFree")] public bool? IsFree { get; set; }
[JsonProperty("price")] public string Price { get; set; } = "";
[JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; }
[JsonProperty("mediaCount")] public int? MediaCount { get; set; }
[JsonProperty("media")] public List<MediumDto> Media { get; set; } = [];
[JsonProperty("previews")] public List<object> Previews { get; set; } = [];
[JsonProperty("isTip")] public bool? IsTip { get; set; }
[JsonProperty("isReportedByMe")] public bool? IsReportedByMe { get; set; }
[JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; }
[JsonProperty("queueId")] public object QueueId { get; set; } = new();
[JsonProperty("fromUser")] public FromUserDto FromUser { get; set; } = new();
[JsonProperty("isFromQueue")] public bool? IsFromQueue { get; set; }
[JsonProperty("canUnsendQueue")] public bool? CanUnsendQueue { get; set; }
[JsonProperty("unsendSecondsQueue")] public int? UnsendSecondsQueue { get; set; }
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("isOpened")] public bool? IsOpened { get; set; }
[JsonProperty("isNew")] public bool? IsNew { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("changedAt")] public DateTime? ChangedAt { get; set; }
[JsonProperty("cancelSeconds")] public int? CancelSeconds { get; set; }
[JsonProperty("isLiked")] public bool? IsLiked { get; set; }
[JsonProperty("canPurchase")] public bool? CanPurchase { get; set; }
[JsonProperty("canPurchaseReason")] public string CanPurchaseReason { get; set; } = "";
[JsonProperty("canReport")] public bool? CanReport { get; set; }
[JsonProperty("canBePinned")] public bool? CanBePinned { get; set; }
[JsonProperty("isPinned")] public bool? IsPinned { get; set; }
}

View File

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Messages;
public class ListsStateDto
{
[JsonProperty("id")] public string Id { get; set; } = "";
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("name")] public string Name { get; set; } = "";
[JsonProperty("hasUser")] public bool HasUser { get; set; }
[JsonProperty("canAddUser")] public bool CanAddUser { get; set; }
[JsonProperty("cannotAddUserReason")] public string CannotAddUserReason { get; set; } = "";
}

View File

@ -0,0 +1,37 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Messages;
public class MediumDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("canView")] public bool CanView { get; set; }
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("src")] public string Src { get; set; } = "";
[JsonProperty("preview")] public string Preview { get; set; } = "";
[JsonProperty("thumb")] public string Thumb { get; set; } = "";
[JsonProperty("locked")] public object Locked { get; set; } = new();
[JsonProperty("duration")] public int? Duration { get; set; }
[JsonProperty("hasError")] public bool? HasError { get; set; }
[JsonProperty("squarePreview")] public string SquarePreview { get; set; } = "";
[JsonProperty("video")] public VideoDto Video { get; set; } = new();
[JsonProperty("videoSources")] public VideoSourcesDto VideoSources { get; set; } = new();
[JsonProperty("source")] public SourceDto Source { get; set; } = new();
[JsonProperty("info")] public InfoDto Info { get; set; } = new();
[JsonProperty("files")] public FilesDto Files { get; set; } = new();
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Messages;
public class MessagesDto
{
[JsonProperty("list")] public List<ListItemDto> List { get; set; } = [];
[JsonProperty("hasMore")] public bool HasMore { get; set; }
}

View File

@ -0,0 +1,60 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Messages;
public class SingleMessageDto
{
[JsonProperty("responseType")] public string ResponseType { get; set; } = "";
[JsonProperty("text")] public string Text { get; set; } = "";
[JsonProperty("giphyId")] public object GiphyId { get; set; } = new();
[JsonProperty("lockedText")] public bool LockedText { get; set; }
[JsonProperty("isFree")] public bool IsFree { get; set; }
[JsonProperty("price")] public double Price { get; set; }
[JsonProperty("isMediaReady")] public bool IsMediaReady { get; set; }
[JsonProperty("mediaCount")] public int MediaCount { get; set; }
[JsonProperty("media")] public List<MediumDto> Media { get; set; } = [];
[JsonProperty("previews")] public List<object> Previews { get; set; } = [];
[JsonProperty("isTip")] public bool IsTip { get; set; }
[JsonProperty("isReportedByMe")] public bool IsReportedByMe { get; set; }
[JsonProperty("isCouplePeopleMedia")] public bool IsCouplePeopleMedia { get; set; }
[JsonProperty("queueId")] public long QueueId { get; set; }
[JsonProperty("fromUser")] public FromUserDto FromUser { get; set; } = new();
[JsonProperty("isFromQueue")] public bool IsFromQueue { get; set; }
[JsonProperty("canUnsendQueue")] public bool CanUnsendQueue { get; set; }
[JsonProperty("unsendSecondsQueue")] public int UnsendSecondsQueue { get; set; }
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("isOpened")] public bool IsOpened { get; set; }
[JsonProperty("isNew")] public bool IsNew { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("changedAt")] public DateTime? ChangedAt { get; set; }
[JsonProperty("cancelSeconds")] public int CancelSeconds { get; set; }
[JsonProperty("isLiked")] public bool IsLiked { get; set; }
[JsonProperty("canPurchase")] public bool CanPurchase { get; set; }
[JsonProperty("canReport")] public bool CanReport { get; set; }
}

View File

@ -0,0 +1,11 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Posts;
public class InfoDto
{
[JsonProperty("source")] public SourceDto Source { get; set; } = new();
[JsonProperty("preview")] public PreviewDto Preview { get; set; } = new();
}

View File

@ -0,0 +1,101 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
using OF_DL.Utils;
namespace OF_DL.Models.Dtos.Posts;
public class ListItemDto
{
private string _rawText = "";
[JsonProperty("responseType")] public string ResponseType { get; set; } = "";
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("postedAt")] public DateTime PostedAt { get; set; }
[JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = "";
[JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new();
[JsonProperty("author")] public AuthorDto Author { get; set; } = new();
[JsonProperty("text")] public string Text { get; set; } = "";
[JsonProperty("rawText")]
public string RawText
{
get
{
if (string.IsNullOrEmpty(_rawText))
{
_rawText = XmlUtils.EvaluateInnerText(Text);
}
return _rawText;
}
set => _rawText = value;
}
[JsonProperty("lockedText")] public bool? LockedText { get; set; }
[JsonProperty("isFavorite")] public bool? IsFavorite { get; set; }
[JsonProperty("canReport")] public bool? CanReport { get; set; }
[JsonProperty("canDelete")] public bool? CanDelete { get; set; }
[JsonProperty("canComment")] public bool? CanComment { get; set; }
[JsonProperty("canEdit")] public bool? CanEdit { get; set; }
[JsonProperty("isPinned")] public bool? IsPinned { get; set; }
[JsonProperty("favoritesCount")] public int? FavoritesCount { get; set; }
[JsonProperty("mediaCount")] public int? MediaCount { get; set; }
[JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; }
[JsonProperty("voting")] public object Voting { get; set; } = new();
[JsonProperty("isOpened")] public bool IsOpened { get; set; }
[JsonProperty("canToggleFavorite")] public bool? CanToggleFavorite { get; set; }
[JsonProperty("streamId")] public object StreamId { get; set; } = new();
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("hasVoting")] public bool? HasVoting { get; set; }
[JsonProperty("isAddedToBookmarks")] public bool? IsAddedToBookmarks { get; set; }
[JsonProperty("isArchived")] public bool IsArchived { get; set; }
[JsonProperty("isPrivateArchived")] public bool? IsPrivateArchived { get; set; }
[JsonProperty("isDeleted")] public bool? IsDeleted { get; set; }
[JsonProperty("hasUrl")] public bool? HasUrl { get; set; }
[JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; }
[JsonProperty("cantCommentReason")] public string CantCommentReason { get; set; } = "";
[JsonProperty("votingType")] public int? VotingType { get; set; }
[JsonProperty("commentsCount")] public int? CommentsCount { get; set; }
[JsonProperty("mentionedUsers")] public List<object> MentionedUsers { get; set; } = [];
[JsonProperty("linkedUsers")] public List<object> LinkedUsers { get; set; } = [];
[JsonProperty("canVote")] public bool? CanVote { get; set; }
[JsonProperty("media")] public List<MediumDto> Media { get; set; } = [];
[JsonProperty("canViewMedia")] public bool? CanViewMedia { get; set; }
[JsonProperty("preview")] public List<object> Preview { get; set; } = [];
}

View File

@ -0,0 +1,37 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Posts;
public class MediumDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("convertedToVideo")] public bool? ConvertedToVideo { get; set; }
[JsonProperty("canView")] public bool CanView { get; set; }
[JsonProperty("hasError")] public bool? HasError { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("info")] public InfoDto? Info { get; set; }
[JsonProperty("source")] public SourceDto? Source { get; set; }
[JsonProperty("squarePreview")] public string? SquarePreview { get; set; }
[JsonProperty("full")] public string? Full { get; set; }
[JsonProperty("preview")] public string? Preview { get; set; }
[JsonProperty("thumb")] public string? Thumb { get; set; }
[JsonProperty("hasCustomPreview")] public bool? HasCustomPreview { get; set; }
[JsonProperty("files")] public FilesDto Files { get; set; } = new();
[JsonProperty("videoSources")] public VideoSourcesDto VideoSources { get; set; } = new();
}

View File

@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Posts;
public class PostDto
{
[JsonProperty("list")] public List<ListItemDto> List { get; set; } = [];
[JsonProperty("hasMore")] public bool HasMore { get; set; }
[JsonProperty("headMarker")] public string HeadMarker { get; set; } = "";
[JsonProperty("tailMarker")] public string TailMarker { get; set; } = "";
}

View File

@ -0,0 +1,99 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
using OF_DL.Utils;
namespace OF_DL.Models.Dtos.Posts;
public class SinglePostDto
{
private string _rawText = "";
[JsonProperty("responseType")] public string ResponseType { get; set; } = "";
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("postedAt")] public DateTime PostedAt { get; set; }
[JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = "";
[JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new();
[JsonProperty("author")] public AuthorDto Author { get; set; } = new();
[JsonProperty("text")] public string Text { get; set; } = "";
[JsonProperty("rawText")]
public string RawText
{
get
{
if (string.IsNullOrEmpty(_rawText))
{
_rawText = XmlUtils.EvaluateInnerText(Text);
}
return _rawText;
}
set => _rawText = value;
}
[JsonProperty("lockedText")] public bool LockedText { get; set; }
[JsonProperty("isFavorite")] public bool IsFavorite { get; set; }
[JsonProperty("canReport")] public bool CanReport { get; set; }
[JsonProperty("canDelete")] public bool CanDelete { get; set; }
[JsonProperty("canComment")] public bool CanComment { get; set; }
[JsonProperty("canEdit")] public bool CanEdit { get; set; }
[JsonProperty("isPinned")] public bool IsPinned { get; set; }
[JsonProperty("favoritesCount")] public int FavoritesCount { get; set; }
[JsonProperty("mediaCount")] public int MediaCount { get; set; }
[JsonProperty("isMediaReady")] public bool IsMediaReady { get; set; }
[JsonProperty("voting")] public object Voting { get; set; } = new();
[JsonProperty("isOpened")] public bool IsOpened { get; set; }
[JsonProperty("canToggleFavorite")] public bool CanToggleFavorite { get; set; }
[JsonProperty("streamId")] public string StreamId { get; set; } = "";
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("hasVoting")] public bool HasVoting { get; set; }
[JsonProperty("isAddedToBookmarks")] public bool IsAddedToBookmarks { get; set; }
[JsonProperty("isArchived")] public bool IsArchived { get; set; }
[JsonProperty("isPrivateArchived")] public bool IsPrivateArchived { get; set; }
[JsonProperty("isDeleted")] public bool IsDeleted { get; set; }
[JsonProperty("hasUrl")] public bool HasUrl { get; set; }
[JsonProperty("isCouplePeopleMedia")] public bool IsCouplePeopleMedia { get; set; }
[JsonProperty("commentsCount")] public int CommentsCount { get; set; }
[JsonProperty("mentionedUsers")] public List<object> MentionedUsers { get; set; } = [];
[JsonProperty("linkedUsers")] public List<object> LinkedUsers { get; set; } = [];
[JsonProperty("tipsAmount")] public string TipsAmount { get; set; } = "";
[JsonProperty("tipsAmountRaw")] public string TipsAmountRaw { get; set; } = "";
[JsonProperty("media")] public List<MediumDto> Media { get; set; } = [];
[JsonProperty("canViewMedia")] public bool CanViewMedia { get; set; }
[JsonProperty("preview")] public List<object> Preview { get; set; } = [];
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Purchased;
public class FromUserDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("_view")] public string View { get; set; } = "";
}

View File

@ -0,0 +1,72 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
using MessageDtos = OF_DL.Models.Dtos.Messages;
namespace OF_DL.Models.Dtos.Purchased;
public class ListItemDto
{
[JsonProperty("responseType")] public string ResponseType { get; set; } = "";
[JsonProperty("text")] public string Text { get; set; } = "";
[JsonProperty("giphyId")] public object GiphyId { get; set; } = new();
[JsonProperty("lockedText")] public bool? LockedText { get; set; }
[JsonProperty("isFree")] public bool? IsFree { get; set; }
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; }
[JsonProperty("mediaCount")] public int? MediaCount { get; set; }
[JsonProperty("media")] public List<MessageDtos.MediumDto>? Media { get; set; }
[JsonProperty("previews")] public List<object>? Previews { get; set; }
[JsonProperty("preview")] public List<object>? Preview { get; set; }
[JsonProperty("isTip")] public bool? IsTip { get; set; }
[JsonProperty("isReportedByMe")] public bool? IsReportedByMe { get; set; }
[JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; }
[JsonProperty("queueId")] public object QueueId { get; set; } = new();
[JsonProperty("fromUser")] public FromUserDto? FromUser { get; set; }
[JsonProperty("author")] public AuthorDto? Author { get; set; }
[JsonProperty("isFromQueue")] public bool? IsFromQueue { get; set; }
[JsonProperty("canUnsendQueue")] public bool? CanUnsendQueue { get; set; }
[JsonProperty("unsendSecondsQueue")] public int? UnsendSecondsQueue { get; set; }
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("isOpened")] public bool IsOpened { get; set; }
[JsonProperty("isNew")] public bool? IsNew { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("postedAt")] public DateTime? PostedAt { get; set; }
[JsonProperty("changedAt")] public DateTime? ChangedAt { get; set; }
[JsonProperty("cancelSeconds")] public int? CancelSeconds { get; set; }
[JsonProperty("isLiked")] public bool? IsLiked { get; set; }
[JsonProperty("canPurchase")] public bool? CanPurchase { get; set; }
[JsonProperty("canReport")] public bool? CanReport { get; set; }
[JsonProperty("isCanceled")] public bool? IsCanceled { get; set; }
[JsonProperty("isArchived")] public bool? IsArchived { get; set; }
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Purchased;
public class PurchasedDto
{
[JsonProperty("list")] public List<ListItemDto> List { get; set; } = [];
[JsonProperty("hasMore")] public bool HasMore { get; set; }
}

View File

@ -0,0 +1,21 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Stories;
public class MediumDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("convertedToVideo")] public bool ConvertedToVideo { get; set; }
[JsonProperty("canView")] public bool CanView { get; set; }
[JsonProperty("hasError")] public bool HasError { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("files")] public FilesDto Files { get; set; } = new();
}

View File

@ -0,0 +1,24 @@
using Newtonsoft.Json;
namespace OF_DL.Models.Dtos.Stories;
public class StoryDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("userId")] public long UserId { get; set; }
[JsonProperty("isWatched")] public bool IsWatched { get; set; }
[JsonProperty("isReady")] public bool IsReady { get; set; }
[JsonProperty("media")] public List<MediumDto> Media { get; set; } = [];
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("question")] public object Question { get; set; } = new();
[JsonProperty("canLike")] public bool CanLike { get; set; }
[JsonProperty("isLiked")] public bool IsLiked { get; set; }
}

View File

@ -0,0 +1,11 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Streams;
public class InfoDto
{
[JsonProperty("source")] public SourceDto Source { get; set; } = new();
[JsonProperty("preview")] public PreviewDto Preview { get; set; } = new();
}

View File

@ -0,0 +1,101 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
using OF_DL.Utils;
namespace OF_DL.Models.Dtos.Streams;
public class ListItemDto
{
private string _rawText = "";
[JsonProperty("responseType")] public string ResponseType { get; set; } = "";
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("postedAt")] public DateTime PostedAt { get; set; }
[JsonProperty("postedAtPrecise")] public string PostedAtPrecise { get; set; } = "";
[JsonProperty("expiredAt")] public object ExpiredAt { get; set; } = new();
[JsonProperty("author")] public AuthorDto Author { get; set; } = new();
[JsonProperty("text")] public string Text { get; set; } = "";
[JsonProperty("rawText")]
public string RawText
{
get
{
if (string.IsNullOrEmpty(_rawText))
{
_rawText = XmlUtils.EvaluateInnerText(Text);
}
return _rawText;
}
set => _rawText = value;
}
[JsonProperty("lockedText")] public bool? LockedText { get; set; }
[JsonProperty("isFavorite")] public bool? IsFavorite { get; set; }
[JsonProperty("canReport")] public bool? CanReport { get; set; }
[JsonProperty("canDelete")] public bool? CanDelete { get; set; }
[JsonProperty("canComment")] public bool? CanComment { get; set; }
[JsonProperty("canEdit")] public bool? CanEdit { get; set; }
[JsonProperty("isPinned")] public bool? IsPinned { get; set; }
[JsonProperty("favoritesCount")] public int? FavoritesCount { get; set; }
[JsonProperty("mediaCount")] public int? MediaCount { get; set; }
[JsonProperty("isMediaReady")] public bool? IsMediaReady { get; set; }
[JsonProperty("voting")] public object Voting { get; set; } = new();
[JsonProperty("isOpened")] public bool? IsOpened { get; set; }
[JsonProperty("canToggleFavorite")] public bool? CanToggleFavorite { get; set; }
[JsonProperty("streamId")] public int? StreamId { get; set; }
[JsonProperty("price")] public string? Price { get; set; }
[JsonProperty("hasVoting")] public bool? HasVoting { get; set; }
[JsonProperty("isAddedToBookmarks")] public bool? IsAddedToBookmarks { get; set; }
[JsonProperty("isArchived")] public bool? IsArchived { get; set; }
[JsonProperty("isPrivateArchived")] public bool? IsPrivateArchived { get; set; }
[JsonProperty("isDeleted")] public bool? IsDeleted { get; set; }
[JsonProperty("hasUrl")] public bool? HasUrl { get; set; }
[JsonProperty("isCouplePeopleMedia")] public bool? IsCouplePeopleMedia { get; set; }
[JsonProperty("cantCommentReason")] public string CantCommentReason { get; set; } = "";
[JsonProperty("commentsCount")] public int? CommentsCount { get; set; }
[JsonProperty("mentionedUsers")] public List<object> MentionedUsers { get; set; } = [];
[JsonProperty("linkedUsers")] public List<object> LinkedUsers { get; set; } = [];
[JsonProperty("tipsAmount")] public string TipsAmount { get; set; } = "";
[JsonProperty("tipsAmountRaw")] public string TipsAmountRaw { get; set; } = "";
[JsonProperty("media")] public List<MediumDto> Media { get; set; } = [];
[JsonProperty("canViewMedia")] public bool? CanViewMedia { get; set; }
[JsonProperty("preview")] public List<object> Preview { get; set; } = [];
}

View File

@ -0,0 +1,37 @@
using Newtonsoft.Json;
using OF_DL.Models.Dtos.Common;
namespace OF_DL.Models.Dtos.Streams;
public class MediumDto
{
[JsonProperty("id")] public long Id { get; set; }
[JsonProperty("type")] public string Type { get; set; } = "";
[JsonProperty("convertedToVideo")] public bool ConvertedToVideo { get; set; }
[JsonProperty("canView")] public bool CanView { get; set; }
[JsonProperty("hasError")] public bool HasError { get; set; }
[JsonProperty("createdAt")] public DateTime? CreatedAt { get; set; }
[JsonProperty("info")] public InfoDto Info { get; set; } = new();
[JsonProperty("source")] public SourceDto Source { get; set; } = new();
[JsonProperty("squarePreview")] public string SquarePreview { get; set; } = "";
[JsonProperty("full")] public string Full { get; set; } = "";
[JsonProperty("preview")] public string Preview { get; set; } = "";
[JsonProperty("thumb")] public string Thumb { get; set; } = "";
[JsonProperty("hasCustomPreview")] public bool HasCustomPreview { get; set; }
[JsonProperty("files")] public FilesDto Files { get; set; } = new();
[JsonProperty("videoSources")] public VideoSourcesDto VideoSources { get; set; } = new();
}

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