├── .github
└── FUNDING.yml
├── assets
├── attributions.txt
└── electron.ico
├── .vscode
└── settings.json
├── forge.env.d.ts
├── .prettierrc
├── src
├── main
│ ├── types
│ │ ├── Bootstrap.ts
│ │ ├── App.ts
│ │ ├── FileConfig.ts
│ │ ├── MainInvocableMethods.ts
│ │ └── UIConfig.ts
│ ├── views
│ │ ├── editor
│ │ │ ├── renderer.ts
│ │ │ ├── editor-view-preload.ts
│ │ │ ├── index.html
│ │ │ └── EditorView.ts
│ │ └── modal
│ │ │ ├── renderer.ts
│ │ │ ├── modal-view-preload.ts
│ │ │ ├── index.html
│ │ │ └── ModalView.ts
│ ├── assets
│ │ └── help
│ │ │ ├── logger.console
│ │ │ ├── log.level.md
│ │ │ └── date.time.format.md
│ │ │ ├── logger.file.1
│ │ │ ├── log.level.md
│ │ │ ├── date.time.format.md
│ │ │ ├── log.filename.md
│ │ │ └── log.dir.md
│ │ │ ├── request
│ │ │ ├── proxy.reject.unauthorized.tls.md
│ │ │ └── proxy.url.md
│ │ │ ├── include
│ │ │ ├── all.media.variants.md
│ │ │ ├── audio.by.filename.md
│ │ │ ├── images.by.filename.md
│ │ │ └── attachments.by.filename.md
│ │ │ ├── downloader
│ │ │ ├── max.video.resolution.md
│ │ │ ├── stop.on.md
│ │ │ ├── cookie.md
│ │ │ ├── target.md
│ │ │ ├── path.to.deno.md
│ │ │ ├── use.status.cache.md
│ │ │ └── path.to.ffmpeg.md
│ │ │ ├── embed.downloader.vimeo
│ │ │ ├── helper.ytdlp.path.md
│ │ │ ├── type.md
│ │ │ └── exec.md
│ │ │ ├── patreon.dl.gui
│ │ │ └── connect.youtube.md
│ │ │ ├── output
│ │ │ ├── campaign.dir.name.format.md
│ │ │ ├── content.dir.name.format.md
│ │ │ └── media.filename.format.md
│ │ │ └── embed.downloader.youtube
│ │ │ └── exec.md
│ ├── ui
│ │ ├── helpers
│ │ │ └── Toast.ts
│ │ ├── editor
│ │ │ ├── components
│ │ │ │ ├── Common.tsx
│ │ │ │ ├── CheckboxRow.tsx
│ │ │ │ ├── HelpIcon.tsx
│ │ │ │ ├── SelectRow.tsx
│ │ │ │ └── TextInputRow.tsx
│ │ │ ├── AlertsBox.tsx
│ │ │ ├── EditorPanel.tsx
│ │ │ ├── OtherBox.tsx
│ │ │ ├── DownloadBox.tsx
│ │ │ ├── WebBrowserToolbar.tsx
│ │ │ ├── EditorToolbar.tsx
│ │ │ ├── OutputBox.tsx
│ │ │ ├── NetworkBox.tsx
│ │ │ └── LoggingBox.tsx
│ │ ├── modals
│ │ │ ├── ModalWrapper.tsx
│ │ │ ├── AboutModal.tsx
│ │ │ ├── HelpModal.tsx
│ │ │ ├── PreviewModal.tsx
│ │ │ ├── ConfirmSaveModal.tsx
│ │ │ └── WebBrowserSettingsModal.tsx
│ │ ├── styles
│ │ │ └── main.css
│ │ ├── contexts
│ │ │ ├── ConfigProvider.tsx
│ │ │ └── EditorContextProvider.tsx
│ │ └── App.tsx
│ ├── main-window-preload.ts
│ ├── util
│ │ ├── WindowState.ts
│ │ ├── Help.ts
│ │ ├── ObjectHelper.ts
│ │ ├── Config.ts
│ │ ├── RecentDocuments.ts
│ │ └── YouTubeConfigurator.ts
│ ├── mixins
│ │ ├── WebBrowserEvents.ts
│ │ ├── EditorEvents.ts
│ │ └── AppMenu.ts
│ ├── config
│ │ └── WebBrowserSettings.ts
│ ├── DownloaderConsoleLogger.ts
│ └── Constants.ts
├── server-console
│ ├── ui
│ │ ├── styles
│ │ │ └── main.css
│ │ ├── ServerConsoleApp.tsx
│ │ ├── components
│ │ │ ├── HelpIcon.tsx
│ │ │ ├── ServerErrorModal.tsx
│ │ │ ├── SelectRow.tsx
│ │ │ ├── TextInputRow.tsx
│ │ │ └── ServerFormModal.tsx
│ │ └── ServerConsoleToolbar.tsx
│ ├── renderer.ts
│ ├── Constants.ts
│ ├── index.html
│ ├── server-console-preload.ts
│ ├── util
│ │ └── Server.ts
│ ├── types
│ │ ├── Server.ts
│ │ ├── ServerConsoleEvents.ts
│ │ └── ServerConsoleInvocableMethods.ts
│ ├── config
│ │ └── Servers.ts
│ └── ServerConsoleWindow.ts
├── resources
│ ├── packaging
│ │ ├── deb
│ │ │ ├── postrm
│ │ │ └── postinst
│ │ ├── rpm
│ │ │ ├── postuninstall.sh
│ │ │ └── postinstall.sh
│ │ └── patreon-dl-gui-server-console.desktop
│ └── patreon-dl-vimeo.js
├── common
│ ├── Constants.ts
│ ├── ui
│ │ ├── index.ts
│ │ ├── Loader.tsx
│ │ ├── styles
│ │ │ └── components.css
│ │ └── components
│ │ │ ├── CustomScrollbars.tsx
│ │ │ └── ToolbarButton.tsx
│ ├── util
│ │ ├── Misc.ts
│ │ ├── FS.ts
│ │ └── WindowState.ts
│ ├── types
│ │ └── Utility.ts
│ ├── RendererAPI.ts
│ └── ProcessBase.ts
└── index.ts
├── vite.preload.config.ts
├── tsconfig.json
├── vite.renderer.config.ts
├── eslint.config.mjs
├── vite.main.config.ts
├── .gitignore
├── misc
└── rpm-spec.ejs
└── package.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: patrickkfkan
2 |
--------------------------------------------------------------------------------
/assets/attributions.txt:
--------------------------------------------------------------------------------
1 | electron.ico - icons-icons.com
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
--------------------------------------------------------------------------------
/forge.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "experimentalTernaries": true
4 | }
--------------------------------------------------------------------------------
/assets/electron.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patrickkfkan/patreon-dl-gui/HEAD/assets/electron.ico
--------------------------------------------------------------------------------
/src/main/types/Bootstrap.ts:
--------------------------------------------------------------------------------
1 | export interface BootstrapData {
2 | browserExecutablePath: string;
3 | }
4 |
--------------------------------------------------------------------------------
/src/main/views/editor/renderer.ts:
--------------------------------------------------------------------------------
1 | import { loadMainUI } from "../../../common/ui/Loader";
2 |
3 | loadMainUI();
4 |
--------------------------------------------------------------------------------
/src/server-console/ui/styles/main.css:
--------------------------------------------------------------------------------
1 | .server-list-table .rdg-cell[aria-selected="true"] {
2 | outline: none;
3 | }
4 |
--------------------------------------------------------------------------------
/src/main/views/modal/renderer.ts:
--------------------------------------------------------------------------------
1 | import { loadModalWrapper } from "../../../common/ui/Loader";
2 |
3 | loadModalWrapper();
4 |
--------------------------------------------------------------------------------
/src/server-console/renderer.ts:
--------------------------------------------------------------------------------
1 | import { loadServerConsoleUI } from "../common/ui/Loader";
2 |
3 | loadServerConsoleUI();
4 |
--------------------------------------------------------------------------------
/src/resources/packaging/deb/postrm:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | rm -f /usr/share/applications/patreon-dl-gui-server-console.desktop
3 | update-desktop-database /usr/share/applications || true
--------------------------------------------------------------------------------
/src/resources/packaging/rpm/postuninstall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | rm -f /usr/share/applications/patreon-dl-gui-server-console.desktop
3 | update-desktop-database /usr/share/applications || true
4 |
--------------------------------------------------------------------------------
/src/resources/packaging/rpm/postinstall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | install -Dm644 /usr/lib/patreon-dl-gui/resources/patreon-dl-gui-server-console.desktop /usr/share/applications/patreon-dl-gui-server-console.desktop
3 | update-desktop-database /usr/share/applications || true
--------------------------------------------------------------------------------
/src/common/Constants.ts:
--------------------------------------------------------------------------------
1 | import { app } from "electron";
2 | import envPaths from "env-paths";
3 |
4 | export const APP_DATA_PATH = envPaths(app.getName(), {
5 | suffix: ""
6 | }).data;
7 |
8 | export const APP_URL = "https://github.com/patrickkfkan/patreon-dl-gui";
9 |
--------------------------------------------------------------------------------
/src/resources/packaging/deb/postinst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | install -Dm644 /usr/lib/patreon-dl-gui/resources/patreon-dl-gui-server-console.desktop /usr/share/applications/patreon-dl-gui-server-console.desktop
4 | update-desktop-database /usr/share/applications || true
--------------------------------------------------------------------------------
/src/resources/packaging/patreon-dl-gui-server-console.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=patreon-dl-gui (Server Console)
3 | Comment=Browse content downloaded by patreon-dl
4 | Exec=/usr/bin/patreon-dl-gui --server-console
5 | Icon=patreon-dl-gui
6 | Type=Application
7 | Categories=Utility;
--------------------------------------------------------------------------------
/src/main/assets/help/logger.console/log.level.md:
--------------------------------------------------------------------------------
1 | ## Log level
2 |
3 | ---
4 |
5 | Sets the logging level.
6 |
7 | Log levels are defined in the following order:
8 |
9 | - debug
10 | - info
11 | - warn
12 | - error
13 |
14 | Messages up to the specified level will be included in the logs.
15 |
--------------------------------------------------------------------------------
/src/main/assets/help/logger.file.1/log.level.md:
--------------------------------------------------------------------------------
1 | ## Log level
2 |
3 | ---
4 |
5 | Sets the logging level.
6 |
7 | Log levels are defined in the following order:
8 |
9 | - debug
10 | - info
11 | - warn
12 | - error
13 |
14 | Messages up to the specified level will be included in the logs.
15 |
--------------------------------------------------------------------------------
/src/main/assets/help/request/proxy.reject.unauthorized.tls.md:
--------------------------------------------------------------------------------
1 | ## Reject unauthorized TLS (proxy)
2 |
3 | ---
4 |
5 | When connecting to a proxy server through SSL/TLS, this option indicates whether invalid certificates should be rejected. You should uncheck this option if your proxy server uses self-signed certs.
6 |
--------------------------------------------------------------------------------
/src/main/assets/help/include/all.media.variants.md:
--------------------------------------------------------------------------------
1 | ## All media variants
2 |
3 | ---
4 |
5 | Some media items may be provided in different sizes and formats. For example, images may have "large" and "small" variations. Enable this option to download all of them. If disabled, only the best quality variant will be downloaded.
6 |
--------------------------------------------------------------------------------
/src/main/assets/help/downloader/max.video.resolution.md:
--------------------------------------------------------------------------------
1 | ## Max video resolution
2 |
3 | The maximum video resolution to download (height in pixels).
4 |
5 | Applies to Patreon-hosted videos and embedded YouTube videos when using the built-in YouTube downloader.
6 |
7 | Set to "None" to download the highest resolution available.
8 |
--------------------------------------------------------------------------------
/src/main/ui/helpers/Toast.ts:
--------------------------------------------------------------------------------
1 | import type { TypeOptions } from "react-toastify";
2 | import { toast } from "react-toastify";
3 |
4 | export function showToast(type: TypeOptions, message: string) {
5 | toast(message, {
6 | type,
7 | autoClose: 3000,
8 | position: "bottom-center",
9 | theme: "dark"
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/src/common/ui/index.ts:
--------------------------------------------------------------------------------
1 | export type AccessibilityProps = {
2 | ariaLabel?: string | null;
3 | };
4 |
5 | export type HelpProps =
6 | | {
7 | helpTooltip?: undefined | null;
8 | helpHasMoreInfo?: undefined | null;
9 | }
10 | | {
11 | helpTooltip: string;
12 | helpHasMoreInfo?: boolean | null;
13 | };
14 |
--------------------------------------------------------------------------------
/src/main/assets/help/embed.downloader.vimeo/helper.ytdlp.path.md:
--------------------------------------------------------------------------------
1 | ## Path to yt-dlp
2 |
3 | ---
4 |
5 | The path to [yt-dlp](https://github.com/yt-dlp/yt-dlp) executable when dowloading embedded Vimeo videos through `patreon-dl`'s helper script.
6 |
7 | If not specified, `yt-dlp` will be called directly when needed, so it should be in the PATH.
8 |
--------------------------------------------------------------------------------
/src/main/assets/help/logger.file.1/date.time.format.md:
--------------------------------------------------------------------------------
1 | ## Datetime format
2 |
3 | ---
4 |
5 | The string pattern used to format the date and time of log messages.
6 |
7 | Date-time formatting is provided by https://github.com/felixge/node-dateformat. Refer to the README of that project for pattern rules.
8 |
9 | ---
10 |
11 | Default: `mmm dd HH:MM:ss`
12 |
--------------------------------------------------------------------------------
/src/main/assets/help/logger.console/date.time.format.md:
--------------------------------------------------------------------------------
1 | ## Datetime format
2 |
3 | ---
4 |
5 | The string pattern used to format the date and time of log messages.
6 |
7 | Date-time formatting is provided by https://github.com/felixge/node-dateformat. Refer to the README of that project for pattern rules.
8 |
9 | ---
10 |
11 | Default: `mmm dd HH:MM:ss`
12 |
--------------------------------------------------------------------------------
/src/common/util/Misc.ts:
--------------------------------------------------------------------------------
1 | export function getErrorString(error: unknown): string {
2 | if (error instanceof Error) {
3 | if (error.cause) {
4 | return `${error.message}: ${getErrorString(error.cause)}`;
5 | }
6 | return error.message;
7 | }
8 | else if (typeof error === 'object') {
9 | return JSON.stringify(error);
10 | }
11 | return String(error);
12 | }
--------------------------------------------------------------------------------
/src/main/types/App.ts:
--------------------------------------------------------------------------------
1 | import type { UIConfig } from "./UIConfig";
2 |
3 | export interface Editor {
4 | id: number;
5 | name: string;
6 | filePath: string | null;
7 | config: UIConfig;
8 | modified: boolean;
9 | loadAlerts?: AlertMessage[];
10 | promptOnSave: boolean;
11 | }
12 |
13 | export interface AlertMessage {
14 | type: "warn" | "error";
15 | text: string;
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/assets/help/include/audio.by.filename.md:
--------------------------------------------------------------------------------
1 | ## Audio by filename
2 |
3 | ---
4 |
5 | If specified, only audio items with filenames matching the specified pattern will be downloaded. Leave blank to download all audio files.
6 |
7 | By default, filename patterns are case-sensitive. To ignore case, start the pattern with `!`.
8 |
9 | This setting takes a glob pattern. For example, `*.wav` will download WAV files only.
10 |
--------------------------------------------------------------------------------
/src/main/assets/help/include/images.by.filename.md:
--------------------------------------------------------------------------------
1 | ## Images by filename
2 |
3 | ---
4 |
5 | If specified, only images with filenames matching the specified pattern will be downloaded. Leave blank to download all images.
6 |
7 | By default, filename patterns are case-sensitive. To ignore case, start the pattern with `!`.
8 |
9 | This setting takes a glob pattern. For example, `*.{gif,jpg,jpeg}` will download JPEGs and GIFs only.
10 |
--------------------------------------------------------------------------------
/src/main/assets/help/include/attachments.by.filename.md:
--------------------------------------------------------------------------------
1 | ## Attachments by filename
2 |
3 | ---
4 |
5 | If specified, only attachments with filenames matching the specified pattern will be downloaded. Leave blank to download all attachments.
6 |
7 | By default, filename patterns are case-sensitive. To ignore case, start the pattern with `!`.
8 |
9 | This setting takes a glob pattern. For example, `*.zip` will download ZIP files only.
10 |
--------------------------------------------------------------------------------
/src/main/main-window-preload.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from "electron";
2 | import RendererAPI from "../common/RendererAPI";
3 |
4 | const mainAPI = new RendererAPI<"main">();
5 |
6 | contextBridge.exposeInMainWorld("mainAPI", {
7 | on: mainAPI.on,
8 | emitMainEvent: mainAPI.emitMainEvent,
9 | invoke: mainAPI.invoke
10 | });
11 |
12 | declare global {
13 | interface Window {
14 | mainAPI: RendererAPI<"main">;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/server-console/Constants.ts:
--------------------------------------------------------------------------------
1 | import type { DeepRequired } from "patreon-dl";
2 | import type { ServerConsoleWindowProps } from "./ServerConsoleWindow";
3 |
4 | export const DEFAULT_SERVER_CONSOLE_WINDOW_PROPS: ServerConsoleWindowProps &
5 | DeepRequired> = {
6 | size: { width: 800, height: 480 },
7 | minSize: { width: 800, height: 480 },
8 | state: "normal",
9 | devTools: false
10 | };
11 |
--------------------------------------------------------------------------------
/src/main/views/editor/editor-view-preload.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from "electron";
2 | import RendererAPI from "../../../common/RendererAPI";
3 |
4 | const mainAPI = new RendererAPI<"main">();
5 |
6 | contextBridge.exposeInMainWorld("mainAPI", {
7 | on: mainAPI.on,
8 | emitMainEvent: mainAPI.emitMainEvent,
9 | invoke: mainAPI.invoke
10 | });
11 |
12 | declare global {
13 | interface Window {
14 | mainAPI: RendererAPI<"main">;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/views/modal/modal-view-preload.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from "electron";
2 | import RendererAPI from "../../../common/RendererAPI";
3 |
4 | const mainAPI = new RendererAPI<"main">();
5 |
6 | contextBridge.exposeInMainWorld("mainAPI", {
7 | on: mainAPI.on,
8 | emitMainEvent: mainAPI.emitMainEvent,
9 | invoke: mainAPI.invoke
10 | });
11 |
12 | declare global {
13 | interface Window {
14 | mainAPI: RendererAPI<"main">;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/assets/help/downloader/stop.on.md:
--------------------------------------------------------------------------------
1 | ## Stop condition
2 |
3 | ---
4 |
5 | When to stop the downloader.
6 |
7 | If you choose "Previously downloaded item encountered", you must also enable "Use status cache" for this setting to be effective.
8 |
9 | If you choose "Publish date of item out of specified range", you must also set the "Published" criteria under the "Include" -> "Posts" tab (or "Products", as the case may be) for this setting to be effective.
10 |
--------------------------------------------------------------------------------
/src/main/assets/help/downloader/cookie.md:
--------------------------------------------------------------------------------
1 | ## Cookie
2 |
3 | ---
4 |
5 | The cookie to use in download requests.
6 |
7 | Cookies are necessary to download locked content which you have access to through subscription / purchase.
8 |
9 | ##### Usage
10 |
11 | If not already signed into Patreon, do so in the embedded web browser. Wait for a brief moment for the cookie to be obtained.
12 |
13 | You can also enter the cookie value manually by selecting "Enter value" from the dropdown menu.
14 |
--------------------------------------------------------------------------------
/src/main/views/editor/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/server-console/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/vite.preload.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 |
3 | export default defineConfig((_env) => {
4 | return {
5 | build: {
6 | rollupOptions: {
7 | output: {
8 | format: "es",
9 | // Must output preload scripts as .mjs, otherwise electron will require() them
10 | entryFileNames: "[name].mjs",
11 | chunkFileNames: "[name].mjs",
12 | assetFileNames: "[name].[ext]"
13 | }
14 | }
15 | }
16 | };
17 | });
18 |
--------------------------------------------------------------------------------
/src/main/assets/help/downloader/target.md:
--------------------------------------------------------------------------------
1 | ## Download target
2 |
3 | ---
4 |
5 | Download targets are captured through the embedded web browser.
6 |
7 | ##### Usage
8 |
9 | In the embedded web browser, visit a Patreon page showing the contents you want to download. Supported contents include:
10 |
11 | - Posts by a creator
12 | - A single post
13 | - Posts in a collection
14 | - A shop product
15 |
16 | Wait for a brief moment after the page loads, and you should see the target description appear in the textbox.
17 |
--------------------------------------------------------------------------------
/src/main/views/modal/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/server-console/server-console-preload.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from "electron";
2 | import RendererAPI from "../common/RendererAPI";
3 |
4 | const serverConsoleAPI = new RendererAPI<"serverConsole">();
5 |
6 | contextBridge.exposeInMainWorld("serverConsoleAPI", {
7 | on: serverConsoleAPI.on,
8 | emitMainEvent: serverConsoleAPI.emitMainEvent,
9 | invoke: serverConsoleAPI.invoke
10 | });
11 |
12 | declare global {
13 | interface Window {
14 | serverConsoleAPI: RendererAPI<"serverConsole">;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "jsx": "react-jsx",
5 | "allowJs": true,
6 | "module": "esnext",
7 | "esModuleInterop": true,
8 | "noImplicitAny": true,
9 | "sourceMap": true,
10 | "baseUrl": ".",
11 | "outDir": "dist",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "strict": true,
15 | "paths": {
16 | "*": ["node_modules/*"]
17 | }
18 | },
19 | "include": ["src/**/*"],
20 | "exclude": ["node_modules", "out"]
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/assets/help/downloader/path.to.deno.md:
--------------------------------------------------------------------------------
1 | ## Path to Deno
2 |
3 | The path to [Deno](https://deno.com) executable.
4 |
5 | Deno is used by the built-in YouTube downloader to run third-party code in a sandboxed environment. It is optional but without it, the code runs without isolation, increasing the risk of security vulnerabilities such as unauthorized access, data corruption, or malicious behavior. For this reason, installing Deno is strongly recommended.
6 |
7 | Set this option if you have Deno installed and the executable is not in the system's PATH.
8 |
--------------------------------------------------------------------------------
/src/main/types/FileConfig.ts:
--------------------------------------------------------------------------------
1 | import type { FILE_CONFIG_SECTION_PROPS } from "../Constants";
2 |
3 | export type FileConfigSection = keyof typeof FILE_CONFIG_SECTION_PROPS;
4 | export type FileConfigProp =
5 | (typeof FILE_CONFIG_SECTION_PROPS)[S][number];
6 | export type FileConfigContents = {
7 | [S in FileConfigSection]: {
8 | [P in FileConfigProp]: string;
9 | };
10 | };
11 |
12 | export interface FileConfig {
13 | editorId: number;
14 | name: string;
15 | filePath: T extends "hasPath" ? string : string | null;
16 | contents: string;
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/assets/help/downloader/use.status.cache.md:
--------------------------------------------------------------------------------
1 | ## Use status cache
2 |
3 | ---
4 |
5 | If enabled, `patreon-dl` creates a file that keeps track of items that have been downloaded. In subsequent downloads, the status cache is used to quickly determine whether an item previously downloaded has changed and can thus be skipped.
6 |
7 |
8 | warning
9 | If you modify or delete any downloaded file, the status cache will be unaware of it!
10 |
11 |
--------------------------------------------------------------------------------
/src/main/util/WindowState.ts:
--------------------------------------------------------------------------------
1 | import type { MainWindowState as MainWindowState } from "../MainWindow";
2 | import type { WindowState } from "../../common/util/WindowState";
3 | import {
4 | loadLastWindowState,
5 | saveWindowState
6 | } from "../../common/util/WindowState";
7 |
8 | export function loadLastMainWindowState() {
9 | return loadLastWindowState("main", isMainWindowState);
10 | }
11 |
12 | export function saveMainWindowState(data: MainWindowState) {
13 | saveWindowState("main", data);
14 | }
15 |
16 | function isMainWindowState(data: WindowState): data is MainWindowState {
17 | return Reflect.has(data, "editorPanelWidth");
18 | }
19 |
--------------------------------------------------------------------------------
/src/common/ui/Loader.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from "react-dom/client";
2 | import App from "../../main/ui/App";
3 | import ModalWrapper from "../../main/ui/modals/ModalWrapper";
4 | import ServerConsoleApp from "../../server-console/ui/ServerConsoleApp";
5 |
6 | export function loadMainUI() {
7 | const root = createRoot(document.body);
8 | root.render();
9 | }
10 |
11 | export function loadModalWrapper() {
12 | const root = createRoot(document.body);
13 | root.render();
14 | }
15 |
16 | export function loadServerConsoleUI() {
17 | const root = createRoot(document.body);
18 | root.render();
19 | }
20 |
--------------------------------------------------------------------------------
/src/common/ui/styles/components.css:
--------------------------------------------------------------------------------
1 | .scrollbars .track-vertical {
2 | top: 0;
3 | bottom: 2px;
4 | right: 0;
5 | background: var(--bs-secondary);
6 | opacity: 0.2;
7 | border-radius: 3px;
8 | }
9 |
10 | .scrollbars .thumb-vertical {
11 | border-radius: 3px;
12 | background: var(--bs-light);
13 | opacity: 0.2;
14 | }
15 |
16 | .scrollbars .track-horizontal {
17 | left: 0;
18 | bottom: 2px;
19 | right: 0;
20 | background: var(--bs-secondary);
21 | opacity: 0.2;
22 | border-radius: 3px;
23 | }
24 |
25 | .scrollbars .thumb-horizontal {
26 | border-radius: 3px;
27 | background: var(--bs-light);
28 | opacity: 0.2;
29 | }
30 |
--------------------------------------------------------------------------------
/src/server-console/util/Server.ts:
--------------------------------------------------------------------------------
1 | import type { ServerList } from "../types/Server";
2 |
3 | export function getStartableServerListEntryIds(list: ServerList) {
4 | return list.entries
5 | .filter(
6 | (entry) =>
7 | entry.status === "stopped" ||
8 | (entry.status === "error" && entry.action === "start")
9 | )
10 | .map((entry) => entry.id);
11 | }
12 |
13 | export function getStoppableServerListEntryIds(list: ServerList) {
14 | return list.entries
15 | .filter(
16 | (entry) =>
17 | entry.status === "running" ||
18 | (entry.status === "error" && entry.action === "stop")
19 | )
20 | .map((entry) => entry.id);
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/assets/help/patreon.dl.gui/connect.youtube.md:
--------------------------------------------------------------------------------
1 | ## Connect to YouTube account
2 |
3 | ---
4 |
5 | This option is only available when you choose to use the built-in downloader for downloading embedded YouTube videos.
6 |
7 | `patreon-dl` supports downloading embedded YouTube videos or from embedded YouTube video links. In addition, if you have a YouTube Premium subscription, you can connect `patreon-dl` to your account and download videos at qualities available only to Premium accounts (e.g. "1080p Premium").
8 |
9 | To connect to your YouTube account, open the YouTube Configurator through clicking the cogs icon button or choosing from the Run menu. This option has no effect if you are not connected.
10 |
--------------------------------------------------------------------------------
/src/main/assets/help/downloader/path.to.ffmpeg.md:
--------------------------------------------------------------------------------
1 | ## Path to FFmpeg
2 |
3 | ---
4 |
5 | The path to [FFmpeg](https://ffmpeg.org) executable.
6 |
7 | FFmpeg is required when downloading:
8 |
9 | - videos that are provided only in streaming format; and
10 | - embedded YouTube videos using the built-in downloader.
11 |
12 |
13 | info
14 | Not all video downloads require FFmpeg, but you should have it installed on your system anyway.
15 |
16 |
17 | If not specified, `ffmpeg` will be called directly when needed, so it should be in the PATH.
18 |
--------------------------------------------------------------------------------
/src/main/assets/help/embed.downloader.vimeo/type.md:
--------------------------------------------------------------------------------
1 | ## Embedded Vimeo downloader
2 |
3 | ---
4 |
5 | Choose the method to download embedded Vimeo videos:
6 |
7 | ##### Use helper script
8 |
9 | `patreon-dl` provides a helper script to download embedded Vimeo content. If you choose this option, you must also install [yt-dlp](https://github.com/yt-dlp/yt-dlp). The helper script processes the embed HTML content in the Patreon post and instructs `yt-dlp` to do the actual downloading (using information obtained from the embed HTML).
10 |
11 | ##### Run external command
12 |
13 | Provide the full command to download embedded Vimeo content. Set placeholders in the command for `patreon-dl` to inject runtime values (see help details for the "External command" field).
14 |
--------------------------------------------------------------------------------
/src/main/ui/editor/components/Common.tsx:
--------------------------------------------------------------------------------
1 | import type { UIConfig, UIConfigSection } from "../../../types/UIConfig";
2 | import type { HelpProps } from "../../../../common/ui";
3 | import HelpIcon from "./HelpIcon";
4 |
5 | export type CreateHelpIconArgs = HelpProps & {
6 | config: [S, keyof UIConfig[S]];
7 | className?: string;
8 | };
9 |
10 | export function createHelpIcon(
11 | args: CreateHelpIconArgs
12 | ) {
13 | const { helpTooltip, helpHasMoreInfo, config, className } = args;
14 | if (!helpTooltip) {
15 | return null;
16 | }
17 | return (
18 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/util/Help.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, readFileSync } from "fs";
2 | import type { UIConfig, UIConfigSection } from "../types/UIConfig";
3 | import path from "path";
4 | import { dirname } from "path";
5 | import { fileURLToPath } from "url";
6 |
7 | const __dirname = dirname(fileURLToPath(import.meta.url));
8 |
9 | export function getHelpContents(
10 | section: S,
11 | prop: keyof UIConfig[S]
12 | ) {
13 | const filePath = path.resolve(
14 | __dirname,
15 | `assets/main/help/${section}/${String(prop)}.md`
16 | );
17 | if (!existsSync(filePath)) {
18 | throw Error(
19 | `Could not read help file for [${section}] ${String(prop)}: ${filePath} does not exist`
20 | );
21 | }
22 | return readFileSync(filePath).toString();
23 | }
24 |
--------------------------------------------------------------------------------
/src/server-console/types/Server.ts:
--------------------------------------------------------------------------------
1 | import type { WebServer } from "patreon-dl";
2 |
3 | export interface Server {
4 | name: string;
5 | dataDir: string;
6 | port: "auto" | "manual";
7 | portNumber: number;
8 | }
9 |
10 | export type ServerListEntry = {
11 | id: number;
12 | server: Server;
13 | } & (
14 | | {
15 | status: "starting";
16 | }
17 | | {
18 | status: "running";
19 | url: string;
20 | instance: WebServer;
21 | }
22 | | {
23 | status: "stopping";
24 | instance: WebServer;
25 | }
26 | | {
27 | status: "stopped";
28 | }
29 | | {
30 | status: "error";
31 | action: "start" | "stop";
32 | message: string;
33 | }
34 | );
35 |
36 | export interface ServerList {
37 | entries: ServerListEntry[];
38 | }
39 |
--------------------------------------------------------------------------------
/src/server-console/types/ServerConsoleEvents.ts:
--------------------------------------------------------------------------------
1 | import type { Server, ServerList } from "./Server";
2 |
3 | export type ServerConsoleRendererEvent =
4 | | "serverListUpdate"
5 | | "showAddServerForm"
6 | | "showEditServerForm"
7 | | "closeServerForm";
8 |
9 | export type ServerConsoleMainEvent = "uiReady";
10 |
11 | export type ServerConsoleRendererEventListener<
12 | E extends ServerConsoleRendererEvent
13 | > =
14 | E extends "serverListUpdate" ? (list: ServerList) => void
15 | : E extends "showAddServerForm" ? () => void
16 | : E extends "showEditServerForm" ? (server: Server) => void
17 | : E extends "closeServerForm" ? () => void
18 | : never;
19 |
20 | export type ServerConsoleMainEventListener =
21 | E extends "uiReady" ? () => void : never;
22 |
--------------------------------------------------------------------------------
/src/main/ui/modals/ModalWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { ToastContainer } from "react-toastify";
2 | import AboutModal from "./AboutModal";
3 | import ConfirmSaveModal from "./ConfirmSaveModal";
4 | import DownloaderModal from "./DownloaderModal";
5 | import HelpModal from "./HelpModal";
6 | import PreviewModal from "./PreviewModal";
7 | import YouTubeConfiguratorModal from "./YouTubeConfiguratorModal";
8 | import WebBRowserSettingsModal from "./WebBrowserSettingsModal";
9 |
10 | function ModalWrapper() {
11 | return (
12 | <>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | >
22 | );
23 | }
24 |
25 | export default ModalWrapper;
26 |
--------------------------------------------------------------------------------
/src/server-console/ui/ServerConsoleApp.tsx:
--------------------------------------------------------------------------------
1 | import "bootswatch/dist/darkly/bootstrap.min.css";
2 | import "material-icons/iconfont/material-icons.css";
3 | import "material-symbols";
4 | import "./styles/main.css";
5 | import "../../common/ui/styles/components.css";
6 | import { useEffect } from "react";
7 | import { Stack } from "react-bootstrap";
8 | import ServerListTable from "./components/ServerListTable";
9 | import ServerConsoleToolbar from "./ServerConsoleToolbar";
10 | import ServerFormModal from "./components/ServerFormModal";
11 |
12 | function ServerConsoleApp() {
13 | useEffect(() => {
14 | window.serverConsoleAPI.emitMainEvent("uiReady");
15 | }, []);
16 |
17 | return (
18 | <>
19 |
20 |
21 |
22 |
23 |
24 | >
25 | );
26 | }
27 |
28 | export default ServerConsoleApp;
29 |
--------------------------------------------------------------------------------
/src/common/ui/components/CustomScrollbars.tsx:
--------------------------------------------------------------------------------
1 | import Scrollbars from "react-custom-scrollbars-4";
2 |
3 | function CustomScrollbars({ children }: { children: React.ReactNode }) {
4 | return (
5 | (
10 |
11 | )}
12 | renderTrackVertical={(props) => (
13 |
14 | )}
15 | renderThumbHorizontal={(props) => (
16 |
17 | )}
18 | renderThumbVertical={(props) => (
19 |
20 | )}
21 | renderView={(props) => }
22 | >
23 | {children}
24 |
25 | );
26 | }
27 |
28 | export default CustomScrollbars;
29 |
--------------------------------------------------------------------------------
/src/server-console/ui/components/HelpIcon.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Tooltip } from "react-tooltip";
3 | import "react-tooltip/dist/react-tooltip.css";
4 | import _ from "lodash";
5 |
6 | interface HelpIconProps {
7 | tooltip: string;
8 | className?: string;
9 | }
10 |
11 | function HelpIcon(props: HelpIconProps) {
12 | const [id] = useState(_.uniqueId("tooltip-trigger-"));
13 | const { tooltip, className } = props;
14 |
15 | return (
16 | <>
17 |
23 | help
24 |
25 |
26 |
{tooltip}
27 |
28 | >
29 | );
30 | }
31 |
32 | export default HelpIcon;
33 |
--------------------------------------------------------------------------------
/vite.renderer.config.ts:
--------------------------------------------------------------------------------
1 | import { ConfigEnv, defineConfig } from "vite";
2 | import path from "path";
3 | import react from "@vitejs/plugin-react";
4 | import { fileURLToPath } from "url";
5 |
6 | const SRC_ROOTS = {
7 | "editor_view": "src/main/views/editor",
8 | "modal_view": "src/main/views/modal",
9 | "server_console": "src/server-console"
10 | };
11 |
12 | const __filename = fileURLToPath(import.meta.url);
13 | const __dirname = path.dirname(__filename);
14 |
15 | export default defineConfig((env) => {
16 | const { name: rendererName} = (env as ConfigEnv<"renderer">).forgeConfigSelf;
17 | const srcRoot = SRC_ROOTS[rendererName];
18 | if (!srcRoot) {
19 | throw Error(`No src root defined for renderer "${rendererName}"`);
20 | }
21 | return {
22 | root: path.resolve(__dirname, srcRoot),
23 | plugins: [react()],
24 | build: {
25 | outDir: path.resolve(__dirname, `.vite/renderer/${rendererName}`)
26 | }
27 | };
28 | });
29 |
--------------------------------------------------------------------------------
/src/main/assets/help/output/campaign.dir.name.format.md:
--------------------------------------------------------------------------------
1 | ## Campaign directory name format
2 |
3 | ---
4 |
5 | Name format of campaign directories. A format is a string pattern consisting of fields enclosed in curly braces.
6 |
7 | When you download content, a directory is created for the campaign that hosts the content. Content directories, which stores the downloaded content, are then placed under the campaign directory. If campaign info could not be obtained from content, then content directory will be created directly under `out.dir`.
8 |
9 | A format must contain at least one of the following fields:
10 |
11 | - `creator.vanity`
12 | - `creator.name`
13 | - `creator.id`
14 | - `campaign.name`
15 | - `campaign.id`
16 |
17 | Characters enclosed in square brackets followed by a question mark denote conditional separators. If the value of a field could not be obtained or is empty, the conditional separator immediately adjacent to it will be omitted from the name.
18 |
19 | ---
20 |
21 | Default: `{creator.vanity}[ - ]?{campaign.name}`
22 |
23 | Fallback: `campaign-{campaign.id}`
24 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import eslint from "@eslint/js";
3 | import reactPlugin from "eslint-plugin-react";
4 | import tseslint from "typescript-eslint";
5 |
6 | export default tseslint.config(
7 | {
8 | ignores: ["src/resources/patreon-dl-vimeo.js"]
9 | },
10 | eslint.configs.recommended,
11 | tseslint.configs.recommended,
12 | reactPlugin.configs.flat.recommended,
13 | reactPlugin.configs.flat["jsx-runtime"],
14 | {
15 | files: ["src/**/*.ts", "src/**/*.tsx"],
16 | languageOptions: {
17 | globals: {
18 | ...globals.browser,
19 | ...globals.node
20 | }
21 | },
22 | rules: {
23 | "@typescript-eslint/no-unused-vars": [
24 | "warn",
25 | {
26 | vars: "all",
27 | varsIgnorePattern: "^_",
28 | args: "after-used",
29 | argsIgnorePattern: "^_",
30 | caughtErrorsIgnorePattern: "^_"
31 | }
32 | ],
33 | "@typescript-eslint/consistent-type-imports": "error",
34 | "react/prop-types": "off"
35 | },
36 | settings: {
37 | react: {
38 | version: "detect"
39 | }
40 | }
41 | }
42 | );
43 |
--------------------------------------------------------------------------------
/src/common/util/FS.ts:
--------------------------------------------------------------------------------
1 | import type { BaseWindow } from "electron";
2 | import type electron from "electron";
3 | import { dialog } from "electron";
4 | import fs from "fs";
5 | import { APP_DATA_PATH } from "../Constants";
6 | import { getErrorString } from "./Misc";
7 |
8 | export type FSChooserResult =
9 | | {
10 | canceled: true;
11 | }
12 | | {
13 | canceled: false;
14 | filePath: string;
15 | };
16 |
17 | export async function openFSChooser(
18 | win: BaseWindow,
19 | options: electron.OpenDialogOptions
20 | ): Promise {
21 | const result = await dialog.showOpenDialog(win, options);
22 | if (result.canceled) {
23 | return {
24 | canceled: true
25 | };
26 | } else {
27 | return {
28 | canceled: false,
29 | filePath: result.filePaths[0]
30 | };
31 | }
32 | }
33 |
34 | export function ensureAppDataPath() {
35 | if (!fs.existsSync(APP_DATA_PATH)) {
36 | try {
37 | fs.mkdirSync(APP_DATA_PATH, {
38 | recursive: true
39 | });
40 | } catch (error: unknown) {
41 | console.error(
42 | `Failed to create app data path "${APP_DATA_PATH}":`,
43 | getErrorString(error)
44 | );
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/util/ObjectHelper.ts:
--------------------------------------------------------------------------------
1 | import { DateTime } from "patreon-dl";
2 |
3 | const NO_CLEAN = [DateTime];
4 |
5 | export default class ObjectHelper {
6 | static clean(
7 | obj: object,
8 | opts?: {
9 | deep?: boolean;
10 | cleanNulls?: boolean;
11 | cleanEmptyObjects?: boolean;
12 | }
13 | ) {
14 | const deep = opts?.deep || false;
15 | const cleanNulls = opts?.cleanNulls || false;
16 | const cleanEmptyObjects = opts?.cleanEmptyObjects || false;
17 |
18 | if (!obj || typeof obj !== "object") {
19 | return obj;
20 | }
21 | const result: Record = {};
22 | for (const [k, v] of Object.entries(obj)) {
23 | const skip = v === undefined || (v === null && cleanNulls);
24 | if (!skip) {
25 | if (
26 | v !== null &&
27 | typeof v === "object" &&
28 | !NO_CLEAN.find((nc) => v instanceof nc)
29 | ) {
30 | const c = deep ? this.clean(v, opts) : v;
31 | if (Object.entries(c).length > 0 || !cleanEmptyObjects) {
32 | result[k] = c;
33 | }
34 | } else {
35 | result[k] = v;
36 | }
37 | }
38 | }
39 | return Array.isArray(obj) ? Object.values(result) : result;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/mixins/WebBrowserEvents.ts:
--------------------------------------------------------------------------------
1 | import { PATREON_URL } from "../Constants";
2 | import type { MainProcessConstructor } from "../MainProcess";
3 |
4 | export function WebBrowserEventSupportMixin<
5 | TBase extends MainProcessConstructor
6 | >(Base: TBase) {
7 | return class WebBrowserEventSupportedProcess extends Base {
8 | protected registerMainEventListeners() {
9 | const callbacks = super.registerMainEventListeners();
10 | return [
11 | ...callbacks,
12 |
13 | this.handle("setWebBrowserURL", (url) => {
14 | this.win.webBrowserView?.gotoURL(url);
15 | }),
16 |
17 | this.handle("setWebBrowserURLToHome", () => {
18 | this.win.webBrowserView?.gotoURL(PATREON_URL);
19 | }),
20 |
21 | this.handle("webBrowserBack", () => {
22 | this.win.webBrowserView?.goBack();
23 | }),
24 |
25 | this.handle("webBrowserForward", () => {
26 | this.win.webBrowserView?.goForward();
27 | }),
28 |
29 | this.handle("webBrowserReload", () => {
30 | this.win.webBrowserView?.reload();
31 | }),
32 |
33 | this.on("viewBoundsChange", (bounds) => {
34 | this.win.updateViewBounds(bounds);
35 | })
36 | ];
37 | }
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/assets/help/request/proxy.url.md:
--------------------------------------------------------------------------------
1 | ## Proxy URL
2 |
3 | ---
4 |
5 | The URL of the proxy server. Supports HTTP, HTTPS, SOCKS4 and SOCKS5 protocols.
6 |
7 | The URL follows this scheme: `protocol://[username:[password]]@hostname:port`. For example:
8 |
9 | - `http://proxy.xyz:8080`
10 | - `socks5://user:password@socksproxy.xyz:1080`
11 |
12 |
13 | info
14 | Web browser sessions are persistent. This means data such as login status is preserved. When you set a proxy URL, the session will be
15 | persisted based on the URL's hostname. In other words, if you set a new proxy URL (never been used in patreon-dl-gui), the embedded web browser will start a new session.
16 |
17 |
18 |
19 | warning
20 | FFmpeg, which is required to download videos in streaming format, supports HTTP proxy only. For other types of proxy, video streams will be handled through direct connection (i.e. without proxy).
21 |