Compare commits

...

29 Commits

Author SHA1 Message Date
8ce53b5be9 Tweaked publishing script 2025-05-13 22:21:04 +02:00
637bac5f61 Added logic to save list of blocked users. 2025-05-13 22:21:04 +02:00
8fd17feba5 Added logic to reset chat read state after downloading messages 2025-05-13 22:21:04 +02:00
3cc3c49b0f Improved subscription loading and null handling 2025-05-13 22:21:03 +02:00
3ac8360a09 HttpClient tweaks 2025-05-13 22:21:03 +02:00
fed6fc77a8 Added earningId to Subscribe model 2025-05-13 22:21:03 +02:00
e723680a96 Improved DB connection creation with delayed retry, and connection caching 2025-05-13 22:21:03 +02:00
233de7c9e5 Extended command line args for NonInteractive 2025-05-13 22:21:02 +02:00
a893653be1 Added exiting if other process is detected, to avoid overlapping runs 2025-05-13 22:21:02 +02:00
2e8789897e Added "x of y" count to "Scraping Data For" console outputs. 2025-05-13 22:21:02 +02:00
fd2c6ad6cd Config and project tweaks, plus publish script 2025-05-13 22:21:02 +02:00
5488d9f80c Fixed async usage. 2025-05-13 22:20:44 +02:00
3a944c112d Merge branch 'master' of https://git.ofdl.tools/sim0n00ps/OF-DL 2025-05-11 22:35:36 +01:00
5e433f6568 Tweaks to docs site 2025-05-11 22:35:33 +01:00
cd60d3092d Update README.md 2025-05-11 20:20:49 +00:00
b36ecd4f5b user mkdocs-material 2025-05-11 20:55:13 +01:00
f5ca6d8eb2 Change publish-docs.yml 2025-05-11 20:53:30 +01:00
69be3607a0 Add MkDocs 2025-05-11 20:25:00 +01:00
0a34f81510 Add CNAME for ofdl.tools 2025-05-11 00:16:29 +01:00
Gitea Actions
8106f690e0 Auto-deploy Docusaurus site to .gitea/pages [skip ci] 2025-05-10 22:22:13 +00:00
Gitea Actions
442cc646d6 Auto-deploy Docusaurus site to .gitea/pages [skip ci] 2025-05-10 22:20:05 +00:00
Gitea Actions
e77f8abff6 Auto-deploy Docusaurus site to .gitea/pages [skip ci] 2025-05-10 22:12:24 +00:00
ec751480e1 Add publish-docs.yml 2025-05-10 23:10:12 +01:00
8578c40c20 Revert DRM Video Resolution 2025-05-08 17:34:58 +01:00
b12ef22406 Fix dynamicrules 2025-05-08 00:04:41 +01:00
6a42dbe53e Remove whitespace 2025-05-06 15:57:09 -05:00
44890f51ee Add default value to video resolution config option 2025-05-06 15:57:03 -05:00
7f2849e5fd Restore version check using gitea API 2025-05-06 15:56:55 -05:00
Melithine
21d0e37bda Fix references to GitHub to point at Gitea instead. 2025-05-06 11:37:12 -07:00
47 changed files with 749 additions and 16067 deletions

View File

@ -5,5 +5,12 @@ root = true
charset = utf-8
indent_style = space
indent_size = 4
tab_width = 4
end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{sln,csproj,xml,json,config}]
indent_size = 2
indent_style = space
tab_width = 2

View File

@ -0,0 +1,36 @@
name: Publish docs
on:
push:
tags:
- 'OFDLV*'
paths:
- 'docs/**'
- '.gitea/workflows/publish-docs.yml'
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install MkDocs
run: |
pip install mkdocs-material
- name: Build site
run: |
mkdocs build --clean
- name: Deploy site
run: |
sudo rm -rf /var/www/mkdocs/*
sudo cp -r site/* /var/www/mkdocs/
sudo chown -R www-data:www-data /var/www/mkdocs/

5
.gitignore vendored
View File

@ -368,3 +368,8 @@ FodyWeavers.xsd
# Allow node_modules inside custom actions
!.gitea-actions/**/node_modules/
# venv
venv/
Publish/

View File

@ -0,0 +1,7 @@
namespace OF_DL.Entities.Chats
{
public class ChatCollection
{
public Dictionary<int, Chats.Chat> Chats { get; set; } = [];
}
}

View File

@ -0,0 +1,20 @@
namespace OF_DL.Entities.Chats
{
public class Chats
{
public List<Chat> list { get; set; }
public bool hasMore { get; set; }
public int nextOffset { get; set; }
public class Chat
{
public User withUser { get; set; }
public int unreadMessagesCount { get; set; }
}
public class User
{
public int id { get; set; }
}
}
}

View File

@ -103,6 +103,11 @@ namespace OF_DL.Entities
[JsonConverter(typeof(StringEnumConverter))]
public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source;
public string[] NonInteractiveSpecificUsers { get; set; } = [];
public string[] NonInteractiveSpecificLists { get; set; } = [];
public bool OutputBlockedUsers { get; set; }
}
public class CreatorConfig : IFileNameFormatConfig

View File

@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace OF_DL.Entities;
public class LatestReleaseAPIResponse
{
[JsonProperty(PropertyName = "tag_name")]
public string TagName { get; set; } = "";
}

View File

@ -98,6 +98,7 @@ namespace OF_DL.Entities
public object id { get; set; }
public int? userId { get; set; }
public int? subscriberId { get; set; }
public long? earningId { get; set; }
public DateTime? date { get; set; }
public int? duration { get; set; }
public DateTime? startDate { get; set; }

View File

@ -2,6 +2,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OF_DL.Entities;
using OF_DL.Entities.Archived;
using OF_DL.Entities.Chats;
using OF_DL.Entities.Highlights;
using OF_DL.Entities.Lists;
using OF_DL.Entities.Messages;
@ -29,6 +30,8 @@ public class APIHelper : IAPIHelper
private readonly IDBHelper m_DBHelper;
private readonly IDownloadConfig downloadConfig;
private readonly Auth auth;
private HttpClient httpClient = new();
private static DateTime? cachedDynamicRulesExpiration;
private static DynamicRules? cachedDynamicRules;
@ -116,11 +119,11 @@ public class APIHelper : IAPIHelper
}
private async Task<string?> BuildHeaderAndExecuteRequests(Dictionary<string, string> getParams, string endpoint, HttpClient client)
private async Task<string?> BuildHeaderAndExecuteRequests(Dictionary<string, string> getParams, string endpoint, HttpClient client, HttpMethod? method = null)
{
Log.Debug("Calling BuildHeaderAndExecuteRequests");
HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint);
HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint, method);
using var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
string body = await response.Content.ReadAsStringAsync();
@ -131,15 +134,20 @@ public class APIHelper : IAPIHelper
}
private async Task<HttpRequestMessage> BuildHttpRequestMessage(Dictionary<string, string> getParams, string endpoint)
private async Task<HttpRequestMessage> BuildHttpRequestMessage(Dictionary<string, string> getParams, string endpoint, HttpMethod? method = null)
{
Log.Debug("Calling BuildHttpRequestMessage");
string queryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
method ??= HttpMethod.Get;
string queryParams = "";
if (getParams.Any())
queryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
Dictionary<string, string> headers = GetDynamicHeaders($"/api2/v2{endpoint}", queryParams);
HttpRequestMessage request = new(HttpMethod.Get, $"{Constants.API_URL}{endpoint}{queryParams}");
HttpRequestMessage request = new(method, $"{Constants.API_URL}{endpoint}{queryParams}");
Log.Debug($"Full request URL: {Constants.API_URL}{endpoint}{queryParams}");
@ -164,18 +172,16 @@ public class APIHelper : IAPIHelper
return input.All(char.IsDigit);
}
private static HttpClient GetHttpClient(IDownloadConfig? config = null)
private HttpClient GetHttpClient(IDownloadConfig? config = null)
{
var client = new HttpClient();
httpClient ??= new HttpClient();
if (config?.Timeout != null && config.Timeout > 0)
{
client.Timeout = TimeSpan.FromSeconds(config.Timeout.Value);
httpClient.Timeout = TimeSpan.FromSeconds(config.Timeout.Value);
}
return client;
return httpClient;
}
/// <summary>
/// this one is used during initialization only
/// if the config option is not available then no modificatiotns will be done on the getParams
@ -302,7 +308,7 @@ public class APIHelper : IAPIHelper
Log.Debug("Calling GetAllSubscrptions");
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, httpClient);
subscriptions = JsonConvert.DeserializeObject<Subscriptions>(body);
if (subscriptions != null && subscriptions.hasMore)
@ -312,7 +318,7 @@ public class APIHelper : IAPIHelper
while (true)
{
Subscriptions newSubscriptions = new();
string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
string? loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, httpClient);
if (!string.IsNullOrEmpty(loopbody) && (!loopbody.Contains("[]") || loopbody.Trim() != "[]"))
{
@ -334,11 +340,14 @@ public class APIHelper : IAPIHelper
foreach (Subscriptions.List subscription in subscriptions.list)
{
if ((!(subscription.isRestricted ?? false) || ((subscription.isRestricted ?? false) && includeRestricted))
&& !users.ContainsKey(subscription.username))
{
if (users.ContainsKey(subscription.username))
continue;
bool isRestricted = subscription.isRestricted ?? false;
bool isRestrictedButAllowed = isRestricted && includeRestricted;
if (!isRestricted || isRestrictedButAllowed)
users.Add(subscription.username, subscription.id);
}
}
return users;
@ -373,7 +382,6 @@ public class APIHelper : IAPIHelper
public async Task<Dictionary<string, int>?> GetExpiredSubscriptions(string endpoint, bool includeRestricted, IDownloadConfig config)
{
Dictionary<string, string> getParams = new()
{
{ "offset", "0" },
@ -387,6 +395,20 @@ public class APIHelper : IAPIHelper
return await GetAllSubscriptions(getParams, endpoint, includeRestricted, config);
}
public async Task<Dictionary<string, int>?> GetBlockedUsers(string endpoint, IDownloadConfig config)
{
Dictionary<string, string> getParams = new()
{
{ "offset", "0" },
{ "limit", "50" },
{ "type", "expired" },
{ "format", "infinite"}
};
Log.Debug("Calling GetBlockedUsers");
return await GetAllSubscriptions(getParams, endpoint, true, config);
}
public async Task<Dictionary<string, int>> GetLists(string endpoint, IDownloadConfig config)
{
@ -405,7 +427,7 @@ public class APIHelper : IAPIHelper
Dictionary<string, int> lists = new();
while (true)
{
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config));
if (body == null)
{
@ -470,7 +492,7 @@ public class APIHelper : IAPIHelper
while (true)
{
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config));
if (body == null)
{
break;
@ -553,7 +575,7 @@ public class APIHelper : IAPIHelper
break;
}
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config));
if (mediatype == MediaType.Stories)
@ -932,7 +954,7 @@ public class APIHelper : IAPIHelper
ref getParams,
downloadAsOf);
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config));
posts = JsonConvert.DeserializeObject<Post>(body, m_JsonSerializerSettings);
ctx.Status($"[red]Getting Posts (this may take a long time, depending on the number of Posts the creator has)\n[/] [red]Found {posts.list.Count}[/]");
ctx.Spinner(Spinner.Known.Dots);
@ -1090,7 +1112,7 @@ public class APIHelper : IAPIHelper
{ "skip_users", "all" }
};
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config));
singlePost = JsonConvert.DeserializeObject<SinglePost>(body, m_JsonSerializerSettings);
if (singlePost != null)
@ -1251,7 +1273,7 @@ public class APIHelper : IAPIHelper
ref getParams,
config.CustomDate);
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
var body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config));
streams = JsonConvert.DeserializeObject<Streams>(body, m_JsonSerializerSettings);
ctx.Status($"[red]Getting Streams\n[/] [red]Found {streams.list.Count}[/]");
ctx.Spinner(Spinner.Known.Dots);
@ -2134,11 +2156,11 @@ public class APIHelper : IAPIHelper
{
JObject user = await GetUserInfoById($"/users/list?x[]={purchase.fromUser.id}");
if(user is null)
if (user is null)
{
if (!config.BypassContentForCreatorsWhoNoLongerExist)
{
if(!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}"))
if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.fromUser.id}"))
{
purchasedTabUsers.Add($"Deleted User - {purchase.fromUser.id}", purchase.fromUser.id);
}
@ -2188,7 +2210,7 @@ public class APIHelper : IAPIHelper
{
if (!config.BypassContentForCreatorsWhoNoLongerExist)
{
if(!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}"))
if (!purchasedTabUsers.ContainsKey($"Deleted User - {purchase.author.id}"))
{
purchasedTabUsers.Add($"Deleted User - {purchase.author.id}", purchase.author.id);
}
@ -2585,6 +2607,94 @@ public class APIHelper : IAPIHelper
return null;
}
public async Task<ChatCollection> GetChats(string endpoint, IDownloadConfig config, bool onlyUnread)
{
Log.Debug($"Calling GetChats - {endpoint}");
try
{
Chats chats = new();
ChatCollection collection = new();
int limit = 60;
Dictionary<string, string> getParams = new()
{
{ "limit", $"{limit}" },
{ "offset", "0" },
{ "skip_users", "all" },
{ "order", "recent" }
};
if (onlyUnread)
getParams["filter"] = "unread";
string body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config));
chats = JsonConvert.DeserializeObject<Chats>(body, m_JsonSerializerSettings);
if (chats.hasMore)
{
getParams["offset"] = $"{chats.nextOffset}";
while (true)
{
string loopbody = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient(config));
Chats newChats = JsonConvert.DeserializeObject<Chats>(loopbody, m_JsonSerializerSettings);
chats.list.AddRange(newChats.list);
if (!newChats.hasMore)
break;
getParams["offset"] = $"{newChats.nextOffset}";
}
}
foreach (Chats.Chat chat in chats.list)
collection.Chats.Add(chat.withUser.id, chat);
return collection;
}
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;
}
public async Task MarkAsUnread(string endpoint, IDownloadConfig config)
{
Log.Debug($"Calling MarkAsUnread - {endpoint}");
try
{
var result = new { success = false };
string body = await BuildHeaderAndExecuteRequests([], endpoint, GetHttpClient(config), HttpMethod.Delete);
result = JsonConvert.DeserializeAnonymousType(body, result);
if (result?.success != true)
Console.WriteLine($"Failed to mark chat as unread! Endpoint: {endpoint}");
}
catch (Exception ex)
{
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
if (ex.InnerException != null)
{
Console.WriteLine("\nInner Exception:");
Console.WriteLine("Exception caught: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
}
}
}
public async Task<string> GetDRMMPDPSSH(string mpdUrl, string policy, string signature, string kvp)
{
@ -2703,7 +2813,7 @@ public class APIHelper : IAPIHelper
using var response = await client.SendAsync(request);
Log.Debug($"CDRM Project Response (Attempt {attempt}): {response.Content.ReadAsStringAsync().Result}");
Log.Debug($"CDRM Project Response (Attempt {attempt}): {await response.Content.ReadAsStringAsync()}");
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
@ -2844,7 +2954,7 @@ public class APIHelper : IAPIHelper
try
{
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://raw.githubusercontent.com/deviint/onlyfans-dynamic-rules/main/dynamicRules.json");
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://git.ofdl.tools/sim0n00ps/dynamic-rules/raw/branch/main/rules.json");
using var response = client.Send(request);
if (!response.IsSuccessStatusCode)

View File

@ -1,18 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OF_DL.Enumurations;
using System.IO;
using Microsoft.Data.Sqlite;
using Serilog;
using OF_DL.Entities;
using Serilog;
using System.Text;
namespace OF_DL.Helpers
{
public class DBHelper : IDBHelper
{
private static readonly Dictionary<string, SqliteConnection> _connections = [];
private readonly IDownloadConfig downloadConfig;
public DBHelper(IDownloadConfig downloadConfig)
@ -32,9 +28,7 @@ namespace OF_DL.Helpers
string dbFilePath = $"{folder}/Metadata/user_data.db";
// connect to the new database file
using SqliteConnection connection = new($"Data Source={dbFilePath}");
// open the connection
connection.Open();
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={dbFilePath}");
// create the 'medias' table
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS medias (id INTEGER NOT NULL, media_id INTEGER, post_id INTEGER NOT NULL, link VARCHAR, directory VARCHAR, filename VARCHAR, size INTEGER, api_type VARCHAR, media_type VARCHAR, preview INTEGER, linked VARCHAR, downloaded INTEGER, created_at TIMESTAMP, record_created_at TIMESTAMP, PRIMARY KEY(id), UNIQUE(media_id));", connection))
@ -139,11 +133,9 @@ namespace OF_DL.Helpers
{
try
{
using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db");
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={Directory.GetCurrentDirectory()}/users.db");
Log.Debug("Database data source: " + connection.DataSource);
connection.Open();
using (SqliteCommand cmd = new("CREATE TABLE IF NOT EXISTS users (id INTEGER NOT NULL, user_id INTEGER NOT NULL, username VARCHAR NOT NULL, PRIMARY KEY(id), UNIQUE(username));", connection))
{
await cmd.ExecuteNonQueryAsync();
@ -194,9 +186,7 @@ namespace OF_DL.Helpers
{
try
{
using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db");
connection.Open();
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={Directory.GetCurrentDirectory()}/users.db");
using (SqliteCommand checkCmd = new($"SELECT user_id, username FROM users WHERE user_id = @userId;", connection))
{
@ -247,8 +237,8 @@ namespace OF_DL.Helpers
{
try
{
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
connection.Open();
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={folder}/Metadata/user_data.db");
await EnsureCreatedAtColumnExists(connection, "messages");
using SqliteCommand cmd = new($"SELECT COUNT(*) FROM messages WHERE post_id=@post_id", connection);
cmd.Parameters.AddWithValue("@post_id", post_id);
@ -286,8 +276,8 @@ namespace OF_DL.Helpers
{
try
{
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
connection.Open();
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={folder}/Metadata/user_data.db");
await EnsureCreatedAtColumnExists(connection, "posts");
using SqliteCommand cmd = new($"SELECT COUNT(*) FROM posts WHERE post_id=@post_id", connection);
cmd.Parameters.AddWithValue("@post_id", post_id);
@ -324,8 +314,8 @@ namespace OF_DL.Helpers
{
try
{
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
connection.Open();
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={folder}/Metadata/user_data.db");
await EnsureCreatedAtColumnExists(connection, "stories");
using SqliteCommand cmd = new($"SELECT COUNT(*) FROM stories WHERE post_id=@post_id", connection);
cmd.Parameters.AddWithValue("@post_id", post_id);
@ -362,8 +352,8 @@ namespace OF_DL.Helpers
{
try
{
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
connection.Open();
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={folder}/Metadata/user_data.db");
await EnsureCreatedAtColumnExists(connection, "medias");
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM medias WHERE media_id=@media_id");
if (downloadConfig.DownloadDuplicatedMedia)
@ -400,22 +390,21 @@ namespace OF_DL.Helpers
{
try
{
bool downloaded = false;
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={folder}/Metadata/user_data.db");
using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"))
StringBuilder sql = new StringBuilder("SELECT downloaded FROM medias WHERE media_id=@media_id");
if (downloadConfig.DownloadDuplicatedMedia)
{
StringBuilder sql = new StringBuilder("SELECT downloaded FROM medias WHERE media_id=@media_id");
if(downloadConfig.DownloadDuplicatedMedia)
{
sql.Append(" and api_type=@api_type");
}
connection.Open();
using SqliteCommand cmd = new (sql.ToString(), connection);
cmd.Parameters.AddWithValue("@media_id", media_id);
cmd.Parameters.AddWithValue("@api_type", api_type);
downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync());
sql.Append(" and api_type=@api_type");
}
connection.Open();
using SqliteCommand cmd = new(sql.ToString(), connection);
cmd.Parameters.AddWithValue("@media_id", media_id);
cmd.Parameters.AddWithValue("@api_type", api_type);
bool downloaded = Convert.ToBoolean(await cmd.ExecuteScalarAsync());
return downloaded;
}
catch (Exception ex)
@ -435,8 +424,7 @@ namespace OF_DL.Helpers
public async Task UpdateMedia(string folder, long media_id, string api_type, string directory, string filename, long size, bool downloaded, DateTime created_at)
{
using SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db");
connection.Open();
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={folder}/Metadata/user_data.db");
// Construct the update command
StringBuilder sql = new StringBuilder("UPDATE medias SET directory=@directory, filename=@filename, size=@size, downloaded=@downloaded, created_at=@created_at WHERE media_id=@media_id");
@ -463,25 +451,21 @@ namespace OF_DL.Helpers
public async Task<long> GetStoredFileSize(string folder, long media_id, string api_type)
{
long size;
using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"))
{
connection.Open();
using SqliteCommand cmd = new($"SELECT size FROM medias WHERE media_id=@media_id and api_type=@api_type", connection);
cmd.Parameters.AddWithValue("@media_id", media_id);
cmd.Parameters.AddWithValue("@api_type", api_type);
size = Convert.ToInt64(await cmd.ExecuteScalarAsync());
}
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={folder}/Metadata/user_data.db");
using SqliteCommand cmd = new($"SELECT size FROM medias WHERE media_id=@media_id and api_type=@api_type", connection);
cmd.Parameters.AddWithValue("@media_id", media_id);
cmd.Parameters.AddWithValue("@api_type", api_type);
long size = Convert.ToInt64(await cmd.ExecuteScalarAsync());
return size;
}
public async Task<DateTime?> GetMostRecentPostDate(string folder)
{
DateTime? mostRecentDate = null;
using (SqliteConnection connection = new($"Data Source={folder}/Metadata/user_data.db"))
{
connection.Open();
using SqliteCommand cmd = new(@"
SqliteConnection connection = await GetAndOpenConnectionAsync($"Data Source={folder}/Metadata/user_data.db");
using SqliteCommand cmd = new(@"
SELECT
MIN(created_at) AS created_at
FROM (
@ -497,13 +481,14 @@ namespace OF_DL.Helpers
ON P.post_id = m.post_id
WHERE m.downloaded = 0
)", connection);
var scalarValue = await cmd.ExecuteScalarAsync();
if(scalarValue != null && scalarValue != DBNull.Value)
{
mostRecentDate = Convert.ToDateTime(scalarValue);
}
var scalarValue = await cmd.ExecuteScalarAsync();
if (scalarValue != null && scalarValue != DBNull.Value)
{
return Convert.ToDateTime(scalarValue);
}
return mostRecentDate;
return null;
}
private async Task EnsureCreatedAtColumnExists(SqliteConnection connection, string tableName)
@ -527,5 +512,35 @@ namespace OF_DL.Helpers
await alterCmd.ExecuteNonQueryAsync();
}
}
public static void CloseAllConnections()
{
foreach (SqliteConnection cn in _connections.Values)
{
cn?.Close();
cn?.Dispose();
}
_connections.Clear();
}
private static async Task<SqliteConnection> GetAndOpenConnectionAsync(string connectionString, int numberOfRetries = 2)
{
try
{
SqliteConnection connection = new(connectionString);
connection.Open();
return connection;
}
catch (Exception)
{
if (--numberOfRetries <= 0)
throw;
await Task.Delay(300);
return await GetAndOpenConnectionAsync(connectionString, numberOfRetries);
}
}
}
}

View File

@ -604,28 +604,31 @@ public class DownloadHelper : IDownloadHelper
decKey = decryptionKey.Substring(pos1 + 1);
}
int? streamIndex = await GetVideoStreamIndexFromMpd(url, policy, signature, kvp, downloadConfig.DownloadVideoResolution);
int streamIndex = 0;
string tempFilename = $"{folder}{path}/{filename}_source.mp4";
if (streamIndex == null)
throw new Exception($"Could not find video stream for resolution {downloadConfig.DownloadVideoResolution}");
//int? streamIndex = await GetVideoStreamIndexFromMpd(url, policy, signature, kvp, downloadConfig.DownloadVideoResolution);
string tempFilename;
//if (streamIndex == null)
// throw new Exception($"Could not find video stream for resolution {downloadConfig.DownloadVideoResolution}");
switch (downloadConfig.DownloadVideoResolution)
{
case VideoResolution.source:
tempFilename = $"{folder}{path}/{filename}_source.mp4";
break;
case VideoResolution._240:
tempFilename = $"{folder}{path}/{filename}_240.mp4";
break;
case VideoResolution._720:
tempFilename = $"{folder}{path}/{filename}_720.mp4";
break;
default:
tempFilename = $"{folder}{path}/{filename}_source.mp4";
break;
}
//string tempFilename;
//switch (downloadConfig.DownloadVideoResolution)
//{
// case VideoResolution.source:
// tempFilename = $"{folder}{path}/{filename}_source.mp4";
// break;
// case VideoResolution._240:
// tempFilename = $"{folder}{path}/{filename}_240.mp4";
// break;
// case VideoResolution._720:
// tempFilename = $"{folder}{path}/{filename}_720.mp4";
// break;
// default:
// tempFilename = $"{folder}{path}/{filename}_source.mp4";
// break;
//}
string parameters = $"-cenc_decryption_key {decKey} -headers \"Cookie:CloudFront-Policy={policy}; CloudFront-Signature={signature}; CloudFront-Key-Pair-Id={kvp}; {sess} Origin: https://onlyfans.com Referer: https://onlyfans.com User-Agent: {user_agent}\" -y -i \"{url}\" -map 0:v:{streamIndex} -map 0:a? -codec copy \"{tempFilename}\"";
@ -852,7 +855,7 @@ public class DownloadHelper : IDownloadHelper
memoryStream.Seek(0, SeekOrigin.Begin);
MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(memoryStream);
byte[] hash = await md5.ComputeHashAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
if (!avatarMD5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant()))
{
@ -895,7 +898,7 @@ public class DownloadHelper : IDownloadHelper
memoryStream.Seek(0, SeekOrigin.Begin);
MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(memoryStream);
byte[] hash = await md5.ComputeHashAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
if (!headerMD5Hashes.Contains(BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant()))
{

View File

@ -0,0 +1,52 @@
using Newtonsoft.Json;
using OF_DL.Entities;
using Serilog;
namespace OF_DL.Helpers;
public static class VersionHelper
{
public static string? GetLatestReleaseTag()
{
Log.Debug("Calling GetLatestReleaseTag");
try
{
HttpClient client = new();
HttpRequestMessage request = new(HttpMethod.Get, "https://git.ofdl.tools/api/v1/repos/sim0n00ps/OF-DL/releases/latest");
using var response = client.Send(request);
if (!response.IsSuccessStatusCode)
{
Log.Debug("GetLatestReleaseTag did not return a Success Status Code");
return null;
}
var body = response.Content.ReadAsStringAsync().Result;
Log.Debug("GetLatestReleaseTag API Response: ");
Log.Debug(body);
var versionCheckResponse = JsonConvert.DeserializeObject<LatestReleaseAPIResponse>(body);
if (versionCheckResponse == null || versionCheckResponse.TagName == "")
{
Log.Debug("GetLatestReleaseTag did not return a valid tag name");
return null;
}
return versionCheckResponse.TagName;
}
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;
}
}

View File

@ -7,6 +7,13 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ApplicationIcon>Icon\download.ico</ApplicationIcon>
<LangVersion>12</LangVersion>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
</PropertyGroup>
<PropertyGroup>
<NoWarn>CS0168;CS0219;CS0472;CS1998;CS8073;CS8600;CS8602;CS8603;CS8604;CS8605;CS8613;CS8618;CS8622;CS8625;CS8629;SYSLIB0021;AsyncFixer01;AsyncFixer02</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -19,7 +26,6 @@
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="protobuf-net" Version="3.2.46" />
<PackageReference Include="PuppeteerSharp" Version="20.1.3" />
<PackageReference Include="Serilog" Version="4.2.0" />
@ -38,12 +44,15 @@
<ItemGroup>
<None Update="auth.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
<None Update="config.conf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
<None Update="rules.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
</ItemGroup>

View File

@ -9,7 +9,6 @@ using OF_DL.Entities.Streams;
using OF_DL.Enumerations;
using OF_DL.Enumurations;
using OF_DL.Helpers;
using Octokit;
using Serilog;
using Serilog.Core;
using Serilog.Events;
@ -22,6 +21,8 @@ using static OF_DL.Entities.Messages.Messages;
using Akka.Configuration;
using System.Text;
using static Akka.Actor.ProviderSelection;
using System.Diagnostics;
using OF_DL.Entities.Chats;
namespace OF_DL;
@ -45,15 +46,15 @@ public class Program
AuthHelper authHelper = new();
Task setupBrowserTask = authHelper.SetupBrowser(runningInDocker);
Task.Delay(1000).Wait();
await Task.Delay(1000);
if (!setupBrowserTask.IsCompleted)
{
AnsiConsole.MarkupLine($"[yellow]Downloading dependencies. Please wait ...[/]");
}
setupBrowserTask.Wait();
await setupBrowserTask;
Task<Auth?> getAuthTask = authHelper.GetAuthFromBrowser();
Task.Delay(5000).Wait();
await Task.Delay(5000);
if (!getAuthTask.IsCompleted)
{
if (runningInDocker)
@ -118,13 +119,15 @@ public class Program
AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red));
ExitIfOtherProcess();
//Remove config.json and convert to config.conf
if (File.Exists("config.json"))
{
AnsiConsole.Markup("[green]config.json located successfully!\n[/]");
try
{
string jsonText = File.ReadAllText("config.json");
string jsonText = await File.ReadAllTextAsync("config.json");
var jsonConfig = JsonConvert.DeserializeObject<Entities.Config>(jsonText);
if (jsonConfig != null)
@ -220,7 +223,7 @@ public class Program
hoconConfig.AppendLine($" LoggingLevel = \"{jsonConfig.LoggingLevel.ToString().ToLower()}\"");
hoconConfig.AppendLine("}");
File.WriteAllText("config.conf", hoconConfig.ToString());
await File.WriteAllTextAsync("config.conf", hoconConfig.ToString());
File.Delete("config.json");
AnsiConsole.Markup("[green]config.conf created successfully from config.json!\n[/]");
}
@ -246,7 +249,7 @@ public class Program
AnsiConsole.Markup("[green]config.conf located successfully!\n[/]");
try
{
string hoconText = File.ReadAllText("config.conf");
string hoconText = await File.ReadAllTextAsync("config.conf");
var hoconConfig = ConfigurationFactory.ParseString(hoconText);
@ -281,7 +284,7 @@ public class Program
DownloadDateSelection = Enum.Parse<DownloadDateSelection>(hoconConfig.GetString("Download.DownloadDateSelection"), true),
CustomDate = !string.IsNullOrWhiteSpace(hoconConfig.GetString("Download.CustomDate")) ? DateTime.Parse(hoconConfig.GetString("Download.CustomDate")) : null,
ShowScrapeSize = hoconConfig.GetBoolean("Download.ShowScrapeSize"),
DownloadVideoResolution = ParseVideoResolution(hoconConfig.GetString("Download.DownloadVideoResolution")),
DownloadVideoResolution = ParseVideoResolution(hoconConfig.GetString("Download.DownloadVideoResolution", "source")),
// File Settings
PaidPostFileNameFormat = hoconConfig.GetString("File.PaidPostFileNameFormat"),
@ -474,15 +477,52 @@ public class Program
if (args is not null && args.Length > 0)
{
const string NON_INTERACTIVE_ARG = "--non-interactive";
const string NON_INTERACTIVE_ARG = "--non-interactive";
const string SPECIFIC_LISTS_ARG = "--specific-lists";
const string SPECIFIC_USERS_ARG = "--specific-users";
if (args.Any(a => NON_INTERACTIVE_ARG.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase)))
{
cliNonInteractive = true;
Log.Debug("NonInteractiveMode set via command line");
}
if (args.Any(a => NON_INTERACTIVE_ARG.Equals(NON_INTERACTIVE_ARG, StringComparison.OrdinalIgnoreCase)))
{
AnsiConsole.Markup($"[grey]Non-Interactive Mode enabled through command-line argument![/]\n");
Log.Debug("Additional arguments:");
config.NonInteractiveMode = true;
int indexOfSpecificListsArg = Array.FindIndex(args, a => a.Contains(SPECIFIC_LISTS_ARG, StringComparison.OrdinalIgnoreCase));
int indexOfSpecificUsersArg = Array.FindIndex(args, a => a.Contains(SPECIFIC_USERS_ARG, StringComparison.OrdinalIgnoreCase));
char[] separator = [','];
if (indexOfSpecificListsArg >= 0)
{
int indexOfListValues = indexOfSpecificListsArg + 1;
string[] strListValues = args.ElementAtOrDefault(indexOfListValues)?.Split(separator, StringSplitOptions.RemoveEmptyEntries) ?? [];
if (strListValues.Length > 0)
{
config.NonInteractiveSpecificLists = strListValues;
config.NonInteractiveModeListName = string.Empty;
}
}
if (indexOfSpecificUsersArg >= 0)
{
int indexOfUserValues = indexOfSpecificUsersArg + 1;
string[] strUserValues = args.ElementAtOrDefault(indexOfUserValues)?.Split(separator, StringSplitOptions.RemoveEmptyEntries) ?? [];
if (strUserValues.Length > 0)
{
config.NonInteractiveSpecificUsers = strUserValues;
}
}
}
const string OUTPUT_BLOCKED_USERS_ARG = "--output-blocked";
if (args.Any(a => OUTPUT_BLOCKED_USERS_ARG.Equals(a, StringComparison.OrdinalIgnoreCase)))
{
config.NonInteractiveMode = true;
config.OutputBlockedUsers = true;
}
Log.Debug("Additional arguments:");
foreach (string argument in args)
{
Log.Debug(argument);
@ -515,14 +555,54 @@ public class Program
}
}
#if !DEBUG
try
{
// Only run the version check if not in DEBUG mode
#if !DEBUG
Version localVersion = Assembly.GetEntryAssembly()?.GetName().Version; //Only tested with numeric values.
String? latestReleaseTag = VersionHelper.GetLatestReleaseTag();
if (latestReleaseTag == null)
{
AnsiConsole.Markup("[yellow]Failed to verify that OF-DL is up-to-date.\n[/]");
Log.Error("Failed to get the latest release tag.");
}
else
{
Version latestGiteaRelease = new Version(latestReleaseTag.Replace("OFDLV", ""));
// Compare the Versions
int versionComparison = localVersion.CompareTo(latestGiteaRelease);
if (versionComparison < 0)
{
// The version on GitHub is more up to date than this local release.
AnsiConsole.Markup("[red]You are running OF-DL version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]");
AnsiConsole.Markup("[red]Please update to the current release, " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}: [link]https://git.ofdl.tools/sim0n00ps/OF-DL/releases[/]\n[/]");
Log.Debug("Detected outdated client running version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}");
Log.Debug("Latest release version " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}");
}
else
{
// This local version is greater than the release version on GitHub.
AnsiConsole.Markup("[green]You are running OF-DL version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]");
AnsiConsole.Markup("[green]Latest Release version: " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}\n[/]");
Log.Debug("Detected client running version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}");
Log.Debug("Latest release version " + $"{latestGiteaRelease.Major}.{latestGiteaRelease.Minor}.{latestGiteaRelease.Build}");
}
}
#else
AnsiConsole.Markup("[yellow]Running in Debug/Local mode. Version check skipped.\n[/]");
Log.Debug("Running in Debug/Local mode. Version check skipped.");
#endif
}
catch (Exception e)
{
AnsiConsole.Markup("[red]Error checking latest release on GitHub:\n[/]");
Console.WriteLine(e);
Log.Error("Error checking latest release on GitHub.", e.Message);
}
AnsiConsole.Markup("[red]You are running OF-DL version " + $"{localVersion.Major}.{localVersion.Minor}.{localVersion.Build}\n[/]");
#else
AnsiConsole.Markup("[yellow]Running in Debug/Local mode. Version check skipped.\n[/]");
Log.Debug("Running in Debug/Local mode. Version check skipped.");
#endif
if (File.Exists("auth.json"))
{
@ -607,9 +687,10 @@ public class Program
AnsiConsole.Markup("[green]rules.json located successfully!\n[/]");
try
{
JsonConvert.DeserializeObject<DynamicRules>(File.ReadAllText("rules.json"));
string rulesJson = await File.ReadAllTextAsync("rules.json");
DynamicRules? dynamicRules = JsonConvert.DeserializeObject<DynamicRules>(rulesJson);
Log.Debug($"Rules.json: ");
Log.Debug(JsonConvert.SerializeObject(File.ReadAllText("rules.json"), Formatting.Indented));
Log.Debug(JsonConvert.SerializeObject(dynamicRules, Formatting.Indented));
}
catch (Exception e)
{
@ -773,7 +854,21 @@ public class Program
}
AnsiConsole.Markup($"[green]Logged In successfully as {validate.name} {validate.username}\n[/]");
await DownloadAllData(apiHelper, auth, config);
try
{
if (config.OutputBlockedUsers)
{
await DownloadBlockedUsers(apiHelper, config);
return;
}
await DownloadAllData(apiHelper, auth, config);
}
finally
{
DBHelper.CloseAllConnections();
}
}
catch (Exception ex)
{
@ -794,8 +889,29 @@ public class Program
}
}
private static async Task DownloadBlockedUsers(APIHelper m_ApiHelper, Entities.Config Config)
{
const string OUTPUT_FILE = "blocked-users.json";
private static async Task DownloadAllData(APIHelper m_ApiHelper, Auth Auth, Entities.Config Config)
Log.Debug($"Calling GetBlockedUsers");
AnsiConsole.Markup($"[red]Getting Blocked Users\n[/]");
Dictionary<string, int>? blockedUsers = await m_ApiHelper.GetBlockedUsers("/users/blocked", Config);
if (blockedUsers is null || blockedUsers.Count == 0)
{
AnsiConsole.Markup($"[red]No Blocked Users found.\n[/]");
}
else
{
AnsiConsole.Markup($"[red]Found {blockedUsers.Count} Blocked Users, saving to '{OUTPUT_FILE}'\n[/]");
string json = JsonConvert.SerializeObject(blockedUsers, Formatting.Indented);
await File.WriteAllTextAsync(OUTPUT_FILE, json);
}
}
private static async Task DownloadAllData(APIHelper m_ApiHelper, Auth Auth, Entities.Config Config)
{
DBHelper dBHelper = new DBHelper(Config);
@ -804,13 +920,21 @@ public class Program
do
{
DateTime startTime = DateTime.Now;
Dictionary<string, int> users = new();
Dictionary<string, int> activeSubs = await m_ApiHelper.GetActiveSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions, Config);
Dictionary<string, int> users = new();
Log.Debug("Subscriptions: ");
Task<Dictionary<string, int>?> taskActive = m_ApiHelper.GetActiveSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions, Config);
Task<Dictionary<string, int>?> taskExpired = Config!.IncludeExpiredSubscriptions
? m_ApiHelper.GetExpiredSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions, Config)
: Task.FromResult<Dictionary<string, int>?>([]);
foreach (KeyValuePair<string, int> activeSub in activeSubs)
{
await Task.WhenAll(taskActive, taskExpired);
Dictionary<string, int> subsActive = await taskActive ?? [];
Dictionary<string, int> subsExpired = await taskExpired ?? [];
Log.Debug("Subscriptions: ");
foreach (KeyValuePair<string, int> activeSub in subsActive)
{
if (!users.ContainsKey(activeSub.Key))
{
users.Add(activeSub.Key, activeSub.Value);
@ -820,11 +944,9 @@ public class Program
if (Config!.IncludeExpiredSubscriptions)
{
Log.Debug("Inactive Subscriptions: ");
Dictionary<string, int> expiredSubs = await m_ApiHelper.GetExpiredSubscriptions("/subscriptions/subscribes", Config.IncludeRestrictedSubscriptions, Config);
foreach (KeyValuePair<string, int> expiredSub in expiredSubs)
{
if (!users.ContainsKey(expiredSub.Key))
foreach (KeyValuePair<string, int> expiredSub in subsExpired)
{
if (!users.ContainsKey(expiredSub.Key))
{
users.Add(expiredSub.Key, expiredSub.Value);
Log.Debug($"Name: {expiredSub.Key} ID: {expiredSub.Value}");
@ -847,15 +969,39 @@ public class Program
var ignoredUsernames = await m_ApiHelper.GetListUsers($"/lists/{ignoredUsersListId}/users", Config) ?? [];
users = users.Where(x => !ignoredUsernames.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value);
}
}
}
await dBHelper.CreateUsersDB(users);
if (users.Count <= 0)
throw new InvalidOperationException("No users found!");
await dBHelper.CreateUsersDB(users);
KeyValuePair<bool, Dictionary<string, int>> hasSelectedUsersKVP;
if(Config.NonInteractiveMode && Config.NonInteractiveModePurchasedTab)
{
hasSelectedUsersKVP = new KeyValuePair<bool, Dictionary<string, int>>(true, new Dictionary<string, int> { { "PurchasedTab", 0 } });
}
else if (Config.NonInteractiveMode && string.IsNullOrEmpty(Config.NonInteractiveModeListName))
else if (Config.NonInteractiveMode && Config.NonInteractiveSpecificLists is not null && Config.NonInteractiveSpecificLists.Length > 0)
{
HashSet<string> listUsernames = [];
foreach (string listName in Config.NonInteractiveSpecificLists)
{
if (!lists.TryGetValue(listName, out int listId))
continue;
List<string> usernames = await m_ApiHelper.GetListUsers($"/lists/{listId}/users", Config);
foreach (string user in usernames)
listUsernames.Add(user);
}
users = users.Where(x => listUsernames.Contains(x.Key)).Distinct().ToDictionary(x => x.Key, x => x.Value);
hasSelectedUsersKVP = new KeyValuePair<bool, Dictionary<string, int>>(true, users);
}
else if (Config.NonInteractiveMode && Config.NonInteractiveSpecificUsers is not null && Config.NonInteractiveSpecificUsers.Length > 0)
{
HashSet<string> usernames = [.. Config.NonInteractiveSpecificUsers];
users = users.Where(u => usernames.Contains(u.Key)).ToDictionary(u => u.Key, u => u.Value);
hasSelectedUsersKVP = new KeyValuePair<bool, Dictionary<string, int>>(true, users);
}
else if (Config.NonInteractiveMode && string.IsNullOrEmpty(Config.NonInteractiveModeListName))
{
hasSelectedUsersKVP = new KeyValuePair<bool, Dictionary<string, int>>(true, users);
}
@ -985,9 +1131,11 @@ public class Program
Log.Debug($"Download path: {p}");
List<PurchasedTabCollection> purchasedTabCollections = await m_ApiHelper.GetPurchasedTab("/posts/paid", p, Config, users);
foreach(PurchasedTabCollection purchasedTabCollection in purchasedTabCollections)
int userNum = 1;
int userCount = purchasedTabCollections.Count;
foreach (PurchasedTabCollection purchasedTabCollection in purchasedTabCollections)
{
AnsiConsole.Markup($"[red]\nScraping Data for {purchasedTabCollection.Username}\n[/]");
AnsiConsole.Markup($"[red]\nScraping Data for {purchasedTabCollection.Username} ({userNum++} of {userCount})\n[/]");
string path = "";
if (!string.IsNullOrEmpty(Config.DownloadPath))
{
@ -1096,8 +1244,10 @@ public class Program
}
else if (hasSelectedUsersKVP.Key && !hasSelectedUsersKVP.Value.ContainsKey("ConfigChanged"))
{
//Iterate over each user in the list of users
foreach (KeyValuePair<string, int> user in hasSelectedUsersKVP.Value)
//Iterate over each user in the list of users
int userNum = 1;
int userCount = hasSelectedUsersKVP.Value.Count;
foreach (KeyValuePair<string, int> user in hasSelectedUsersKVP.Value)
{
int paidPostCount = 0;
int postCount = 0;
@ -1107,7 +1257,7 @@ public class Program
int highlightsCount = 0;
int messagesCount = 0;
int paidMessagesCount = 0;
AnsiConsole.Markup($"[red]\nScraping Data for {user.Key}\n[/]");
AnsiConsole.Markup($"[red]\nScraping Data for {user.Key} ({userNum++} of {userCount})\n[/]");
Log.Debug($"Scraping Data for {user.Key}");
@ -1392,6 +1542,9 @@ public class Program
{
Log.Debug($"Calling DownloadMessages - {user.Key}");
AnsiConsole.Markup($"[grey]Getting Unread Chats\n[/]");
HashSet<int> unreadChats = await GetUsersWithUnreadChats(downloadContext.ApiHelper, downloadContext.DownloadConfig);
MessageCollection messages = new MessageCollection();
await AnsiConsole.Status()
@ -1399,7 +1552,14 @@ public class Program
{
messages = await downloadContext.ApiHelper.GetMessages($"/chats/{user.Value}/messages", path, downloadContext.DownloadConfig!, ctx);
});
int oldMessagesCount = 0;
if (unreadChats.Contains(user.Value))
{
AnsiConsole.Markup($"[grey]Restoring unread state\n[/]");
await downloadContext.ApiHelper.MarkAsUnread($"/chats/{user.Value}/mark-as-read", downloadContext.DownloadConfig);
}
int oldMessagesCount = 0;
int newMessagesCount = 0;
if (messages != null && messages.Messages.Count > 0)
{
@ -2924,7 +3084,7 @@ public class Program
hoconConfig.AppendLine($" LoggingLevel = \"{newConfig.LoggingLevel.ToString().ToLower()}\"");
hoconConfig.AppendLine("}");
File.WriteAllText("config.conf", hoconConfig.ToString());
await File.WriteAllTextAsync("config.conf", hoconConfig.ToString());
string newConfigString = JsonConvert.SerializeObject(newConfig, Formatting.Indented);
@ -3148,7 +3308,18 @@ public class Program
}
}
static bool ValidateFilePath(string path)
private static async Task<HashSet<int>> GetUsersWithUnreadChats(APIHelper apiHelper, IDownloadConfig currentConfig)
{
ChatCollection chats = await apiHelper.GetChats($"/chats", currentConfig, onlyUnread: true);
var unreadChats = chats.Chats
.Where(c => c.Value.unreadMessagesCount > 0)
.ToList();
return [.. unreadChats.Select(c => c.Key)];
}
static bool ValidateFilePath(string path)
{
char[] invalidChars = System.IO.Path.GetInvalidPathChars();
char[] foundInvalidChars = path.Where(c => invalidChars.Contains(c)).ToArray();
@ -3251,4 +3422,24 @@ public class Program
return Enum.Parse<VideoResolution>("_" + value, ignoreCase: true);
}
static void ExitIfOtherProcess()
{
Assembly entryAssembly = Assembly.GetEntryAssembly();
AssemblyName entryAssemblyName = entryAssembly?.GetName();
if (entryAssemblyName?.Name is null)
return;
Process thisProcess = Process.GetCurrentProcess();
Process[] otherProcesses = [.. Process.GetProcessesByName(entryAssemblyName.Name).Where(p => p.Id != thisProcess.Id)];
if (otherProcesses.Length <= 0)
return;
AnsiConsole.Markup($"[green]Other OF DL process detected, exiting..\n[/]");
Log.Warning("Other OF DL process detected, exiting..");
Environment.Exit(0);
}
}

33
Publish_OF-DL.bat Normal file
View File

@ -0,0 +1,33 @@
@ECHO OFF
ECHO.
ECHO ==============================
ECHO == Cleaning Output ===========
ECHO ==============================
dotnet clean ".\OF DL\OF DL.csproj" -v minimal
DEL /Q /F ".\Publish"
ECHO.
ECHO ==============================
ECHO == Publishing OF-DL ==========
ECHO ==============================
dotnet publish ".\OF DL\OF DL.csproj" -o ".\Publish"
ECHO.
ECHO ==============================
ECHO == Copy to network drive? ====
ECHO ==============================
CHOICE /C yn /m "Copy published files to network drive? "
IF %ERRORLEVEL%==1 (GOTO Copy) ELSE (GOTO Exit)
:Copy
xcopy .\Publish\* p:\_Utils\OF_DL /I /Y /Q /EXCLUDE:.\excludes.txt
:Exit
ECHO.
ECHO.
PAUSE

View File

@ -7,7 +7,7 @@ Scrape all the media from an OnlyFans account
Join the discord [here](https://discord.com/invite/6bUW8EJ53j)
# Documentation
Please refer to https://sim0n00ps.github.io/OF-DL/ for instructions on:
Please refer to https://docs.ofdl.tools/ for instructions on:
- Requirements
- Installing the Program
- Running the Program

2
docs/.gitignore vendored
View File

@ -18,3 +18,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
venv/

View File

@ -1 +0,0 @@
20.16.0

View File

@ -1,41 +0,0 @@
# Website
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

View File

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

View File

@ -10,18 +10,14 @@ OF DL allows you to log in to your OnlyFans account directly. This simplifies th
When prompted by the application, log into your OnlyFans account. Do not close the opened window, tab, or navigate away to another webpage.
The new window will close automatically when the authentication process has finished.
:::warning
!!! warning
Some users have reported that "Sign in with Google" has not been working with this authentication method.
If you use the Google sign-in option to log into your OnlyFans account, use one of the [legacy authentication methods](#legacy-methods) described below.
Some users have reported that "Sign in with Google" has not been working with this authentication method.
If you use the Google sign-in option to log into your OnlyFans account, use one of the [legacy authentication methods](#legacy-methods) described below.
:::
!!! info
:::info
If you are using docker, follow the special [authentication instructions documented](/docs/installation/docker) to authenticate OF-DL
:::
If you are using docker, follow the special [authentication instructions documented](/installation/docker) to authenticate OF-DL
## Legacy Methods

View File

@ -265,7 +265,7 @@ Default: `""`
Allowed values: Any valid string
Description: Please refer to [custom filename formats](/docs/config/custom-filename-formats#paidpostfilenameformat) page to see what fields you can use.
Description: Please refer to [custom filename formats](/config/custom-filename-formats#paidpostfilenameformat) page to see what fields you can use.
## PostFileNameFormat
@ -275,7 +275,7 @@ Default: `""`
Allowed values: Any valid string
Description: Please refer to the [custom filename formats](/docs/config/custom-filename-formats#postfilenameformat) page to see what fields you can use.
Description: Please refer to the [custom filename formats](/config/custom-filename-formats#postfilenameformat) page to see what fields you can use.
## PaidMessageFileNameFormat
@ -285,7 +285,7 @@ Default: `""`
Allowed values: Any valid string
Description: Please refer to [custom filename formats](/docs/config/custom-filename-formats#paidmessagefilenameformat) page to see what fields you can use.
Description: Please refer to [custom filename formats](/config/custom-filename-formats#paidmessagefilenameformat) page to see what fields you can use.
## MessageFileNameFormat
@ -295,7 +295,7 @@ Default: `""`
Allowed values: Any valid string
Description: Please refer to [custom filename formats](/docs/config/custom-filename-formats#messagefilenameformat) page to see what fields you can use.
Description: Please refer to [custom filename formats](/config/custom-filename-formats#messagefilenameformat) page to see what fields you can use.
## RenameExistingFilesWhenCustomFormatIsSelected
@ -322,7 +322,7 @@ Description: This configuration options allows you to set file name formats for
This is useful if you want to have different file name formats for different creators. The values set here will override the global values set in the config file
(see [PaidPostFileNameFormat](#paidpostfilenameformat), [PostFileNameFormat](#postfilenameformat),
[PaidMessageFileNAmeFormat](#paidmessagefilenameformat), and [MessageFileNameFormat](#messagefilenameformat)).
For more information on the file name formats, see the [custom filename formats](/docs/config/custom-filename-formats) page.
For more information on the file name formats, see the [custom filename formats](/config/custom-filename-formats) page.
Example:
```
@ -435,15 +435,13 @@ Description: If set to `true`, the program will run without any input from the u
(unless [NonInteractiveModeListName](#noninteractivemodelistname) or [NonInteractiveModePurchasedTab](#noninteractivemodepurchasedtab) are configured).
If set to `false`, the default behaviour will apply, and you will be able to choose an option from the menu.
:::warning
!!! warning
If NonInteractiveMode is enabled, you will be unable to authenticate OF-DL using the standard authentication method.
Before you can run OF-DL in NonInteractiveMode, you must either
If NonInteractiveMode is enabled, you will be unable to authenticate OF-DL using the standard authentication method.
Before you can run OF-DL in NonInteractiveMode, you must either
1. Generate an auth.json file by running OF-DL with NonInteractiveMode disabled and authenticating OF-DL using the standard method **OR**
2. Generate an auth.json file by using a [legacy authentication method](/docs/config/auth#legacy-methods)
:::
1. Generate an auth.json file by running OF-DL with NonInteractiveMode disabled and authenticating OF-DL using the standard method **OR**
2. Generate an auth.json file by using a [legacy authentication method](/config/auth#legacy-methods)
## NonInteractiveModeListName

View File

@ -1,8 +0,0 @@
{
"label": "Configuration",
"position": 2,
"link": {
"type": "generated-index",
"description": "Configuration options and information for OF-DL"
}
}

View File

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

View File

@ -1,123 +0,0 @@
// @ts-check
// `@type` JSDoc annotations allow editor autocompletion and type checking
// (when paired with `@ts-check`).
// There are various equivalent ways to declare your Docusaurus config.
// See: https://docusaurus.io/docs/api/docusaurus-config
import {themes as prismThemes} from 'prism-react-renderer';
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'OF-DL',
tagline: 'A media scraper for OnlyFans with DRM video support',
favicon: 'img/logo.png',
// Set the production url of your site here
url: 'https://sim0n00ps.github.io',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/OF-DL/',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'sim0n00ps', // Usually your GitHub org/user name.
projectName: 'OF-DL', // Usually your repo name.
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
presets: [
[
'@docusaurus/preset-classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: './sidebars.js',
},
blog: false,
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
colorMode: {
respectPrefersColorScheme: true,
},
navbar: {
title: 'OF-DL',
logo: {
alt: 'OF-DL Logo',
src: 'img/logo.png',
},
items: [
{
type: 'docSidebar',
sidebarId: 'generatedSidebar',
position: 'left',
label: 'Docs',
},
{
href: 'https://github.com/sim0n00ps/OF-DL',
label: 'GitHub',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Docs',
items: [
{
label: 'Installation',
to: '/docs/installation/windows',
},
{
label: 'Configuration',
to: '/docs/config/auth',
},
{
label: 'Running the Program',
to: '/docs/running-the-program',
},
],
},
{
title: 'Community',
items: [
{
label: 'Discord',
href: 'https://discord.com/invite/6bUW8EJ53j',
},
],
},
{
title: 'More',
items: [
{
label: 'GitHub',
href: 'https://github.com/sim0n00ps/OF-DL',
},
],
},
],
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
}),
};
export default config;

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/img/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

8
docs/index.md Normal file
View File

@ -0,0 +1,8 @@
# Welcome to OF-DL
C# console app to download all of the media from Onlyfans accounts with DRM video downloading support.
!!! info "PLEASE READ BEFORE DOWNLOADING"
THIS TOOL CANNOT BYPASS PAYWALLS, IT CAN ONLY DOWNLOAD CONTENT YOU HAVE ACCESS TO, PLEASE DO NOT DOWNLOAD THIS TOOL THINKING YOU CAN BYPASS PAYING FOR THINGS!!!!!
Join the discord [here](https://discord.com/invite/6bUW8EJ53j)

View File

@ -18,7 +18,7 @@ To run OF-DL in a docker container, follow these steps:
Adjust `$HOME/ofdl` as desired (including in the commands below) if you want the files stored elsewhere.
4. Run the following command to start the docker container:
```bash
docker run --rm -it -v $HOME/ofdl/data/:/data -v $HOME/ofdl/config/:/config -p 8080:8080 ghcr.io/sim0n00ps/of-dl:latest
docker run --rm -it -v $HOME/ofdl/data/:/data -v $HOME/ofdl/config/:/config -p 8080:8080 git.ofdl.tools/sim0n00ps/of-dl:latest
```
If `config.json` and/or `rules.json` don't exist in the `config` directory, files with default values will be created when you run the docker container.
If you have your own Widevine keys, those files should be placed under `$HOME/ofdl/config/cdm/devices/chrome_1610/`.
@ -29,14 +29,14 @@ To run OF-DL in a docker container, follow these steps:
When a new version of OF-DL is released, you can download the latest docker image by executing:
```bash
docker pull ghcr.io/sim0n00ps/of-dl:latest
docker pull git.ofdl.tools/sim0n00ps/of-dl:latest
```
You can then run the new version of OF-DL by executing the `docker run` command in the [Running OF-DL](#running-of-dl) section above.
## Building the Docker Image (Optional)
Since official docker images are provided for OF-DL through GitHub Container Registry (ghcr.io), you do not need to build the docker image yourself.
Since official docker images are provided for OF-DL through Gitea (git.ofdl.tools), you do not need to build the docker image yourself.
If you would like to build the docker image yourself, however, start by cloning the OF-DL repository and opening a terminal in the root directory of the repository.
Then, execute the following command while replacing `x.x.x` with the current version of OF-DL:
@ -45,4 +45,4 @@ VERSION="x.x.x" docker build --build-arg VERSION=$VERSION -t of-dl .
```
You can then run a container using the image you just built by executing the `docker run` command in the
[Running OF-DL](#running-of-dl) section above while replacing `ghcr.io/sim0n00ps/of-dl:latest` with `of-dl`.
[Running OF-DL](#running-of-dl) section above while replacing `git.ofdl.tools/sim0n00ps/of-dl:latest` with `of-dl`.

View File

@ -5,7 +5,7 @@ sidebar_position: 3
# Linux
A Linux release of OF-DL is not available at this time, however you can run OF-DL on Linux using Docker.
Please refer to the [Docker](/docs/installation/docker) page for instructions on how to run OF-DL in a Docker container.
Please refer to the [Docker](/installation/docker) page for instructions on how to run OF-DL in a Docker container.
If you do not have Docker installed, you can download it from [here](https://docs.docker.com/desktop/install/linux-install/).
If you would like to run OF-DL natively on Linux, you can build it from source by following the instructions below.
@ -27,7 +27,7 @@ sudo apt-get install libicu-dev
- Clone the repo
```bash
git clone https://github.com/sim0n00ps/OF-DL.git
git clone https://git.ofdl.tools/sim0n00ps/OF-DL.git
cd 'OF-DL'
```

View File

@ -5,5 +5,5 @@ sidebar_position: 4
# macOS
macOS releases of OF-DL are not available at this time, however you can run OF-DL on macOS using Docker.
Please refer to the [Docker](/docs/installation/docker) page for instructions on how to run OF-DL in a Docker container.
Please refer to the [Docker](/installation/docker) page for instructions on how to run OF-DL in a Docker container.
If you do not have Docker installed, you can download it from [here](https://docs.docker.com/desktop/install/mac-install/).

View File

@ -11,11 +11,11 @@ sidebar_position: 1
You will need to download FFmpeg. You can download it from [here](https://www.gyan.dev/ffmpeg/builds/).
Make sure you download `ffmpeg-release-essentials.zip`. Unzip it anywhere on your computer. You only need `ffmpeg.exe`, and you can ignore the rest.
Move `ffmpeg.exe` to the same folder as `OF DL.exe` (downloaded in the installation steps below). If you choose to move `ffmpeg.exe` to a different folder,
you will need to specify the path to `ffmpeg.exe` in the config file (see the `FFmpegPath` [config option](/docs/config/configuration#ffmpegpath)).
you will need to specify the path to `ffmpeg.exe` in the config file (see the `FFmpegPath` [config option](/config/configuration#ffmpegpath)).
## Installation
1. Navigate to the OF-DL [releases page](https://github.com/sim0n00ps/OF-DL/releases), and download the latest release zip file. The zip file will be named `OFDLVx.x.x.zip` where `x.x.x` is the version number.
1. Navigate to the OF-DL [releases page](https://git.ofdl.tools/sim0n00ps/OF-DL/releases), and download the latest release zip file. The zip file will be named `OFDLVx.x.x.zip` where `x.x.x` is the version number.
2. Unzip the downloaded file. The destination folder can be anywhere on your computer, preferably somewhere where you want to download content to/already have content downloaded.
3. Your folder should contain a folder named `cdm` as well as the following files:
- OF DL.exe

15550
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +0,0 @@
{
"name": "of-dl",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/preset-classic": "3.4.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/types": "3.4.0"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}

View File

@ -4,7 +4,7 @@ sidebar_position: 3
# Running the Program
Once you are happy you have filled everything in [auth.json](/docs/config/auth) correctly, you can double click OF-DL.exe and you should see a command prompt window appear, it should look something like this:
Once you are happy you have filled everything in [auth.json](/config/auth) correctly, you can double click OF-DL.exe and you should see a command prompt window appear, it should look something like this:
![CLI welcome banner](/img/welcome_banner.png)

View File

@ -1,33 +0,0 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
generatedSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};
export default sidebars;

View File

@ -1,39 +0,0 @@
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import Heading from '@theme/Heading';
import styles from './index.module.css';
function HomepageHeader() {
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<Heading as="h1" className="hero__title">
{siteConfig.title}
</Heading>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="docs/installation/windows">
Installation
</Link>
</div>
</div>
</header>
);
}
export default function Home() {
const {siteConfig} = useDocusaurusContext();
return (
<Layout
title={siteConfig.title}
description={siteConfig.tagline}>
<HomepageHeader />
</Layout>
);
}

View File

@ -1,23 +0,0 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 769 B

2
excludes.txt Normal file
View File

@ -0,0 +1,2 @@
excludes.txt
rules.json

46
mkdocs.yml Normal file
View File

@ -0,0 +1,46 @@
site_name: OF-DL Docs
site_url: https://docs.ofdl.tools
theme:
name: material
features:
- navigation.tabs
- navigation.top
- navigation.instant
- navigation.expand
- navigation.sections
- navigation.tracking
- navigation.search.highlight
- navigation.search.suggest
- navigation.search.share
- navigation.search.suggest
- navigation.search.share
- navigation.search.suggest
- navigation.search.share
language: en
palette:
- scheme: default
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
primary: dark-blue
accent: white
- scheme: slate
toggle:
icon: material/toggle-switch
name: Switch to light mode
primary: dark-blue
accent: white
font:
text: Roboto
code: Roboto Mono
logo: img/logo.ico
favicon: img/logo.ico
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
extra:
social:
- icon: fontawesome/brands/discord
link: https://discord.com/invite/6bUW8EJ53j
copyright: "© 2025 OF-DL. All rights reserved."