Merge pull request 'Replace PuppeteerSharp with Playwright' (#44) from whimsical-c4lic0/OF-DL:replace-puppeteer-with-playwright into master
Reviewed-on: #44
This commit is contained in:
commit
ba0347f86f
@ -58,7 +58,7 @@ jobs:
|
||||
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 ffprobe.exe LICENSE.ffmpeg
|
||||
zip ../OFDLV${{ steps.version.outputs.version }}.zip OF\ DL.exe e_sqlite3.dll rules.json config.conf cdm chromium-scripts ffmpeg.exe ffprobe.exe LICENSE.ffmpeg
|
||||
cd ..
|
||||
|
||||
- name: Create release and upload artifact
|
||||
|
||||
37
Dockerfile
37
Dockerfile
@ -1,10 +1,7 @@
|
||||
FROM alpine:3.23 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
|
||||
ARG VERSION
|
||||
|
||||
RUN apk --no-cache --repository community add \
|
||||
dotnet10-sdk
|
||||
|
||||
# Copy source code
|
||||
COPY ["OF DL.sln", "/src/OF DL.sln"]
|
||||
COPY ["OF DL", "/src/OF DL"]
|
||||
@ -22,21 +19,24 @@ RUN /src/out/OF\ DL --non-interactive || true && \
|
||||
mv /src/updated_config.conf /src/config.conf
|
||||
|
||||
|
||||
FROM alpine:3.23 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 \
|
||||
dotnet10-runtime \
|
||||
ffmpeg7 \
|
||||
udev \
|
||||
ttf-freefont \
|
||||
chromium \
|
||||
ffmpeg \
|
||||
supervisor \
|
||||
xvfb \
|
||||
x11vnc \
|
||||
novnc
|
||||
novnc \
|
||||
npm \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN npx playwright install-deps
|
||||
|
||||
RUN apt-get remove --purge -y npm \
|
||||
&& apt-get autoremove -y
|
||||
|
||||
# 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
|
||||
@ -55,13 +55,14 @@ 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", "--"]
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["/app/entrypoint.sh"]
|
||||
|
||||
@ -13,9 +13,9 @@
|
||||
<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="PuppeteerSharp" Version="20.2.6"/>
|
||||
<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"/>
|
||||
|
||||
@ -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")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current authentication state.
|
||||
@ -35,7 +43,7 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
|
||||
public Auth? CurrentAuth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads authentication data from disk.
|
||||
/// Loads authentication data from the disk.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The auth file path.</param>
|
||||
/// <returns>True when auth data is loaded successfully.</returns>
|
||||
@ -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<InstalledBrowser> 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<string> 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<string> GetBcToken(IPage page) =>
|
||||
await page.EvaluateExpressionAsync<string>("window.localStorage.getItem('bcTokenSha') || ''");
|
||||
private static async Task<string> GetBcToken(IPage page) =>
|
||||
await page.EvaluateAsync<string>("window.localStorage.getItem('bcTokenSha') || ''");
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the stored cookie string to only include required cookie values.
|
||||
@ -194,10 +196,10 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
|
||||
/// </summary>
|
||||
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<string, string> mappedCookies = (await page.GetCookiesAsync())
|
||||
Dictionary<string, string> 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<string>("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)
|
||||
|
||||
@ -36,7 +36,7 @@ public interface IAuthService
|
||||
Task<UserEntities.User?> ValidateAuthAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Logs out by deleting chrome-data and auth.json.
|
||||
/// Logs out by deleting chromium-data and auth.json.
|
||||
/// </summary>
|
||||
void Logout();
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@ -23,9 +23,9 @@
|
||||
<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="PuppeteerSharp" Version="20.2.6"/>
|
||||
<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"/>
|
||||
@ -49,6 +49,9 @@
|
||||
<None Update="rules.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="chromium-scripts/stealth.min.js">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -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[/]");
|
||||
|
||||
14
OF DL/chromium-scripts/CREATING_STEALTH_SCRIPT.md
Normal file
14
OF DL/chromium-scripts/CREATING_STEALTH_SCRIPT.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Stealth Script Creation
|
||||
|
||||
## Requirements
|
||||
|
||||
- NodeJS (with npx CLI tool)
|
||||
|
||||
## Instructions
|
||||
|
||||
- Open a terminal in this directory (OF DL/chromium-scripts)
|
||||
- Run `npx -y extract-stealth-evasions`
|
||||
|
||||
## 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).
|
||||
7
OF DL/chromium-scripts/stealth.min.js
vendored
Normal file
7
OF DL/chromium-scripts/stealth.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -2,6 +2,7 @@
|
||||
|
||||
mkdir -p /config/cdm/devices/chrome_1610
|
||||
mkdir -p /config/logs/
|
||||
mkdir -p /config/chromium
|
||||
|
||||
if [ ! -f /config/config.conf ] && [ ! -f /config/config.json ]; then
|
||||
cp /default-config/config.conf /config/config.conf
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user