diff --git a/OF DL.Core/OF DL.Core.csproj b/OF DL.Core/OF DL.Core.csproj
index 2ef833a..ed0b30d 100644
--- a/OF DL.Core/OF DL.Core.csproj
+++ b/OF DL.Core/OF DL.Core.csproj
@@ -13,9 +13,9 @@
+
-
diff --git a/OF DL.Core/Services/AuthService.cs b/OF DL.Core/Services/AuthService.cs
index a8b725c..a277a34 100644
--- a/OF DL.Core/Services/AuthService.cs
+++ b/OF DL.Core/Services/AuthService.cs
@@ -1,9 +1,8 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Playwright;
using Newtonsoft.Json;
using OF_DL.Models;
-using PuppeteerSharp;
-using PuppeteerSharp.BrowserData;
using Serilog;
using UserEntities = OF_DL.Models.Entities.Users;
@@ -11,8 +10,25 @@ namespace OF_DL.Services;
public class AuthService(IServiceProvider serviceProvider) : IAuthService
{
- private const int LoginTimeout = 600000; // 10 minutes
- private const int FeedLoadTimeout = 60000; // 1 minute
+ private const float LoginTimeout = 600000f; // 10 minutes
+ private const float FeedLoadTimeout = 60000f; // 1 minute
+ private const int AdditionalWaitAfterPageLoad = 3000; // 3 seconds
+
+ private readonly string _userDataDir = Path.GetFullPath("chromium-data");
+ private const string InitScriptsDirName = "chromium-scripts";
+
+ private readonly BrowserTypeLaunchPersistentContextOptions _options = new()
+ {
+ Headless = false,
+ Channel = "chromium",
+ Args =
+ [
+ "--no-sandbox",
+ "--disable-setuid-sandbox",
+ "--disable-blink-features=AutomationControlled",
+ "--disable-infobars"
+ ]
+ };
private readonly string[] _desiredCookies =
[
@@ -20,14 +36,6 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
"sess"
];
- private readonly LaunchOptions _options = new()
- {
- Headless = false,
- Channel = ChromeReleaseChannel.Stable,
- DefaultViewport = null,
- Args = ["--no-sandbox", "--disable-setuid-sandbox"],
- UserDataDir = Path.GetFullPath("chrome-data")
- };
///
/// Gets or sets the current authentication state.
@@ -35,7 +43,7 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
public Auth? CurrentAuth { get; set; }
///
- /// Loads authentication data from disk.
+ /// Loads authentication data from the disk.
///
/// The auth file path.
/// True when auth data is loaded successfully.
@@ -107,43 +115,37 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
}
}
- private async Task SetupBrowser(bool runningInDocker)
+ private Task SetupBrowser(bool runningInDocker)
{
- string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH");
- if (executablePath != null)
- {
- Log.Information(
- "OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}",
- executablePath);
- _options.ExecutablePath = executablePath;
- }
- else
- {
- BrowserFetcher browserFetcher = new();
- List installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList();
- if (installedBrowsers.Count == 0)
- {
- Log.Information("Downloading browser.");
- InstalledBrowser? downloadedBrowser = await browserFetcher.DownloadAsync();
- Log.Information("Browser downloaded. Path: {executablePath}",
- downloadedBrowser.GetExecutablePath());
- _options.ExecutablePath = downloadedBrowser.GetExecutablePath();
- }
- else
- {
- _options.ExecutablePath = installedBrowsers.First().GetExecutablePath();
- }
- }
-
if (runningInDocker)
{
Log.Information("Running in Docker. Disabling sandbox and GPU.");
- _options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"];
+ _options.Args =
+ [
+ "--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu",
+ "--disable-blink-features=AutomationControlled", "--disable-infobars"
+ ];
+
+ // If chromium is already downloaded, skip installation
+ string? playwrightBrowsersPath = Environment.GetEnvironmentVariable("PLAYWRIGHT_BROWSERS_PATH");
+ IEnumerable folders = Directory.GetDirectories(playwrightBrowsersPath ?? "/config/chromium")
+ .Where(folder => folder.Contains("chromium-"));
+
+ if (folders.Any())
+ {
+ Log.Information("chromium already downloaded. Skipping install step.");
+ return Task.CompletedTask;
+ }
}
+
+ int exitCode = Program.Main(["install", "--with-deps", "chromium"]);
+ return exitCode != 0
+ ? throw new Exception($"Playwright chromium download failed. Exited with code {exitCode}")
+ : Task.CompletedTask;
}
- private async Task GetBcToken(IPage page) =>
- await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''");
+ private static async Task GetBcToken(IPage page) =>
+ await page.EvaluateAsync("window.localStorage.getItem('bcTokenSha') || ''");
///
/// Normalizes the stored cookie string to only include required cookie values.
@@ -194,10 +196,10 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
///
public void Logout()
{
- if (Directory.Exists("chrome-data"))
+ if (Directory.Exists("chromium-data"))
{
- Log.Information("Deleting chrome-data folder");
- Directory.Delete("chrome-data", true);
+ Log.Information("Deleting chromium-data folder");
+ Directory.Delete("chromium-data", true);
}
if (File.Exists("auth.json"))
@@ -211,18 +213,23 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
{
try
{
- IBrowser? browser;
+ IBrowserContext? browser;
try
{
- browser = await Puppeteer.LaunchAsync(_options);
+ IPlaywright playwright = await Playwright.CreateAsync();
+ browser = await playwright.Chromium.LaunchPersistentContextAsync(_userDataDir, _options);
}
- catch (ProcessException e)
+ catch (Exception e)
{
- if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir))
+ if ((
+ e.Message.Contains("An error occurred trying to start process") ||
+ e.Message.Contains("The profile appears to be in use by another Chromium process")
+ ) && Directory.Exists(_userDataDir))
{
- Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again.");
- Directory.Delete(_options.UserDataDir, true);
- browser = await Puppeteer.LaunchAsync(_options);
+ Log.Error("Failed to launch browser. Deleting chromium-data directory and trying again.");
+ Directory.Delete(_userDataDir, true);
+ IPlaywright playwright = await Playwright.CreateAsync();
+ browser = await playwright.Chromium.LaunchPersistentContextAsync(_userDataDir, _options);
}
else
{
@@ -235,27 +242,39 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
throw new Exception("Could not get browser");
}
- IPage[]? pages = await browser.PagesAsync();
- IPage? page = pages.First();
+ IPage? page = browser.Pages[0];
if (page == null)
{
throw new Exception("Could not get page");
}
+ string exeDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ??
+ string.Empty;
+ string initScriptsDir = Path.Combine(exeDirectory, InitScriptsDirName);
+ if (Directory.Exists(initScriptsDir))
+ {
+ Log.Information("Loading init scripts from {initScriptsDir}", initScriptsDir);
+ foreach (string initScript in Directory.GetFiles(initScriptsDir, "*.js"))
+ {
+ Log.Debug("Loading init script {initScript}", initScript);
+ await page.AddInitScriptAsync(initScript);
+ }
+ }
+
Log.Debug("Navigating to OnlyFans.");
- await page.GoToAsync("https://onlyfans.com");
+ await page.GotoAsync("https://onlyfans.com");
Log.Debug("Waiting for user to login");
- await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout });
+ await page.WaitForSelectorAsync(".b-feed", new PageWaitForSelectorOptions { Timeout = LoginTimeout });
Log.Debug("Feed element detected (user logged in)");
- await page.ReloadAsync();
+ await page.ReloadAsync(
+ new PageReloadOptions { Timeout = FeedLoadTimeout, WaitUntil = WaitUntilState.DOMContentLoaded });
+
+ // Wait for an additional time to ensure the DOM is fully loaded
+ await Task.Delay(AdditionalWaitAfterPageLoad);
- await page.WaitForNavigationAsync(new NavigationOptions
- {
- WaitUntil = [WaitUntilNavigation.Networkidle2], Timeout = FeedLoadTimeout
- });
Log.Debug("DOM loaded. Getting BC token and cookies ...");
string xBc;
@@ -265,35 +284,40 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
}
catch (Exception e)
{
- Log.Error(e, "Error getting bcToken");
- throw new Exception("Error getting bcToken");
+ await browser.CloseAsync();
+ throw new Exception($"Error getting bcToken. {e.Message}");
}
- Dictionary mappedCookies = (await page.GetCookiesAsync())
+ Dictionary mappedCookies = (await browser.CookiesAsync())
.Where(cookie => cookie.Domain.Contains("onlyfans.com"))
.ToDictionary(cookie => cookie.Name, cookie => cookie.Value);
mappedCookies.TryGetValue("auth_id", out string? userId);
if (userId == null)
{
+ await browser.CloseAsync();
throw new Exception("Could not find 'auth_id' cookie");
}
mappedCookies.TryGetValue("sess", out string? sess);
if (sess == null)
{
+ await browser.CloseAsync();
throw new Exception("Could not find 'sess' cookie");
}
- string? userAgent = await browser.GetUserAgentAsync();
- if (userAgent == null)
+ string userAgent = await page.EvaluateAsync("navigator.userAgent");
+ if (string.IsNullOrWhiteSpace(userAgent))
{
+ await browser.CloseAsync();
throw new Exception("Could not get user agent");
}
string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key))
.Select(key => $"${key}={mappedCookies[key]};"));
+ await browser.CloseAsync();
+
return new Auth { Cookie = cookies, UserAgent = userAgent, UserId = userId, XBc = xBc };
}
catch (Exception e)
diff --git a/OF DL.Core/Services/IAuthService.cs b/OF DL.Core/Services/IAuthService.cs
index 432d13f..5078615 100644
--- a/OF DL.Core/Services/IAuthService.cs
+++ b/OF DL.Core/Services/IAuthService.cs
@@ -36,7 +36,7 @@ public interface IAuthService
Task ValidateAuthAsync();
///
- /// Logs out by deleting chrome-data and auth.json.
+ /// Logs out by deleting chromium-data and auth.json.
///
void Logout();
}
diff --git a/OF DL.Tests/Services/AuthServiceTests.cs b/OF DL.Tests/Services/AuthServiceTests.cs
index b25a576..f4dab59 100644
--- a/OF DL.Tests/Services/AuthServiceTests.cs
+++ b/OF DL.Tests/Services/AuthServiceTests.cs
@@ -29,10 +29,7 @@ public class AuthServiceTests
AuthService service = CreateService();
service.CurrentAuth = new Auth
{
- UserId = "123",
- UserAgent = "agent",
- XBc = "xbc",
- Cookie = "auth_id=123; sess=abc;"
+ UserId = "123", UserAgent = "agent", XBc = "xbc", Cookie = "auth_id=123; sess=abc;"
};
await service.SaveToFileAsync();
@@ -53,10 +50,7 @@ public class AuthServiceTests
using TempFolder temp = new();
using CurrentDirectoryScope _ = new(temp.Path);
AuthService service = CreateService();
- service.CurrentAuth = new Auth
- {
- Cookie = "auth_id=123; other=1; sess=abc"
- };
+ service.CurrentAuth = new Auth { Cookie = "auth_id=123; other=1; sess=abc" };
service.ValidateCookieString();
@@ -74,13 +68,13 @@ public class AuthServiceTests
using TempFolder temp = new();
using CurrentDirectoryScope _ = new(temp.Path);
AuthService service = CreateService();
- Directory.CreateDirectory("chrome-data");
- File.WriteAllText("chrome-data/test.txt", "x");
+ Directory.CreateDirectory("chromium-data");
+ File.WriteAllText("chromium-data/test.txt", "x");
File.WriteAllText("auth.json", "{}");
service.Logout();
- Assert.False(Directory.Exists("chrome-data"));
+ Assert.False(Directory.Exists("chromium-data"));
Assert.False(File.Exists("auth.json"));
}
diff --git a/OF DL/OF DL.csproj b/OF DL/OF DL.csproj
index 8285c32..8ace189 100644
--- a/OF DL/OF DL.csproj
+++ b/OF DL/OF DL.csproj
@@ -23,8 +23,9 @@
+
-
+
diff --git a/OF DL/Program.cs b/OF DL/Program.cs
index f43d269..2716455 100644
--- a/OF DL/Program.cs
+++ b/OF DL/Program.cs
@@ -37,8 +37,6 @@ public class Program(IServiceProvider serviceProvider)
{
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]Note: Some users have reported that \"Sign in with Google\" has not been working with the new authentication method.[/]");
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[/]");