├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── src ├── browse │ ├── web │ │ ├── assets │ │ │ └── styles │ │ │ │ ├── App.scss │ │ │ │ ├── CampaignContent.scss │ │ │ │ ├── _mixins.scss │ │ │ │ ├── LightGalleryItem.scss │ │ │ │ ├── SidebarTrigger.scss │ │ │ │ ├── ProductList.scss │ │ │ │ ├── FilterModal.scss │ │ │ │ ├── RewardCard.scss │ │ │ │ ├── Slider.scss │ │ │ │ ├── PostCard.scss │ │ │ │ ├── FilterModalButton.scss │ │ │ │ ├── CampaignCard.scss │ │ │ │ ├── CustomScrollbars.scss │ │ │ │ ├── CollectionCard.scss │ │ │ │ ├── FadeContent.scss │ │ │ │ ├── CampaignHeader.scss │ │ │ │ ├── SliderArrow.scss │ │ │ │ ├── ProductCard.scss │ │ │ │ ├── PostContent.scss │ │ │ │ ├── CollectionBanner.scss │ │ │ │ ├── Sidebar.scss │ │ │ │ ├── MediaGallery.scss │ │ │ │ └── MediaGrid.scss │ │ ├── main.tsx │ │ ├── index.html │ │ ├── utils │ │ │ └── RawDataExtractor.ts │ │ ├── components │ │ │ ├── MediaImage.tsx │ │ │ ├── SidebarTrigger.tsx │ │ │ ├── ShowingText.tsx │ │ │ ├── SliderArrow.tsx │ │ │ ├── RewardCard.tsx │ │ │ ├── CustomScrollbars.tsx │ │ │ ├── Theme.tsx │ │ │ ├── CollectionBanner.tsx │ │ │ ├── CommentsPanel.tsx │ │ │ ├── FadeContent.tsx │ │ │ ├── LightGalleryItem.tsx │ │ │ ├── CollectionCard.tsx │ │ │ ├── SearchInputBox.tsx │ │ │ ├── PageInputButton.tsx │ │ │ ├── ProductList.tsx │ │ │ └── CampaignCard.tsx │ │ ├── layouts │ │ │ ├── MainLayout.tsx │ │ │ ├── CampaignLayout.tsx │ │ │ └── CollectionLayout.tsx │ │ ├── contexts │ │ │ ├── MainContentScrollProvider.tsx │ │ │ └── GlobalModalsProvider.tsx │ │ ├── pages │ │ │ ├── ProductContent.tsx │ │ │ └── CampaignHome.tsx │ │ └── App.tsx │ ├── types │ │ ├── Settings.ts │ │ ├── Campaign.ts │ │ ├── Media.ts │ │ ├── Filter.ts │ │ └── Content.ts │ ├── db │ │ ├── updaters │ │ │ ├── DBUpdater_1_1_0.ts │ │ │ └── DBUpdater_1_2_0.ts │ │ ├── EnvDBMixin.ts │ │ ├── CollectionFTS.ts │ │ └── Update.ts │ ├── api │ │ ├── MediaAPIMixin.ts │ │ ├── index.ts │ │ ├── CampaignAPIMixin.ts │ │ └── SettingsAPIMixin.ts │ └── server │ │ └── handler │ │ ├── CampaignAPIRequesthandler.ts │ │ ├── BaseHandler.ts │ │ └── SettingsAPIRequestHandler.ts ├── types │ └── argv-split │ │ └── index.d.ts ├── entities │ ├── List.ts │ ├── index.ts │ ├── Reward.ts │ ├── Comment.ts │ ├── User.ts │ ├── Campaign.ts │ ├── Product.ts │ └── Downloadable.ts ├── downloaders │ ├── task │ │ ├── index.ts │ │ └── DownloadTaskBatchEvent.ts │ ├── index.ts │ ├── templates │ │ ├── ProductInfo.ts │ │ ├── CollectionInfo.ts │ │ ├── CommentInfo.ts │ │ ├── PostInfo.ts │ │ └── CampaignInfo.ts │ └── DownloaderEvent.ts ├── utils │ ├── logging │ │ ├── index.ts │ │ ├── Logger.ts │ │ └── ChainLogger.ts │ ├── index.ts │ ├── WebUtils.ts │ ├── ThmbnailFilenameResolver.ts │ ├── PackageInfo.ts │ ├── Sleeper.ts │ ├── ObjectHelper.ts │ └── FetcherProgressMonitor.ts ├── index.ts └── cli │ └── server │ └── ServerCLIOptions.ts ├── vite-env.d.ts ├── tsconfig.eslint.json ├── bin ├── patreon-dl.js └── patreon-dl-server.js ├── typedoc.json ├── docs └── api │ ├── type-aliases │ ├── NoDeepTypes.md │ ├── Tier.md │ ├── LogLevel.md │ ├── CommentReply.md │ ├── DownloaderType.md │ ├── DenoInstallStatus.md │ ├── FileExistsAction.md │ ├── ImageType.md │ ├── UserIdOrVanityParam.md │ ├── DownloaderEvent.md │ ├── YouTubePostEmbed.md │ ├── DownloadTaskStatus.md │ ├── StopOnCondition.md │ ├── DownloadTaskBatchEvent.md │ ├── DownloaderConfig.md │ ├── GetCampaignParams.md │ ├── DownloaderEventPayloadOf.md │ ├── MediaItem.md │ ├── DownloadTaskBatchEventPayloadOf.md │ ├── DeepRequired.md │ ├── Downloadable.md │ ├── DateTimeConstructorArgs.md │ ├── DownloaderBootstrapData.md │ ├── DownloadTaskSkipReason.md │ ├── URLAnalysis.md │ ├── FileLoggerConfig.md │ ├── FileLoggerOptions.md │ ├── FileLoggerGetPathInfoParams.md │ ├── DownloaderInit.md │ ├── ImageMediaItem.md │ └── LinkedAttachment.md │ ├── variables │ ├── SITE_URL.md │ └── DEFAULT_WEB_SERVER_PORT.md │ ├── functions │ ├── isDenoInstalled.md │ └── createProxyAgent.md │ ├── interfaces │ ├── DownloaderStartParams.md │ ├── EmbedDownloader.md │ ├── ProxyOptions.md │ ├── ServerFileLoggerOptions.md │ ├── YouTubeCredentialsPendingInfo.md │ ├── PostTag.md │ ├── BootstrapData.md │ ├── ProxyAgentInfo.md │ ├── LogEntry.md │ ├── DownloaderFileLoggerInit.md │ ├── WebServerConfig.md │ ├── PostDownloaderContext.md │ ├── DownloaderFileLoggerOptions.md │ ├── ProductDownloaderBootstrapData.md │ ├── Downloaded.md │ ├── PostDownloaderBootstrapData.md │ ├── MediaLike.md │ ├── AttachmentMediaItem.md │ ├── ConsoleLoggerOptions.md │ ├── FileMediaItem.md │ ├── Comment.md │ ├── DummyMediaItem.md │ ├── IDownloadTask.md │ ├── AudioMediaItem.md │ ├── SingleImageMediaItem.md │ ├── DownloadProgress.md │ ├── PostThumbnailMediaItem.md │ ├── CampaignCoverPhotoMediaItem.md │ ├── PostCoverImageMediaItem.md │ ├── Reward.md │ └── Collection.md │ ├── enumerations │ ├── FileLoggerType.md │ ├── PostSortOrder.md │ └── TargetSkipReason.md │ └── classes │ ├── Logger.md │ ├── FetcherError.md │ ├── DownloadTaskError.md │ ├── DateTime.md │ └── WebServer.md ├── tsconfig.json ├── vite.config.js └── NOTICE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: patrickkfkan 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | test/ 4 | .vscode/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ** 2 | 3 | !dist/** 4 | !bin/** 5 | !README.md 6 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/App.scss: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } -------------------------------------------------------------------------------- /vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// -------------------------------------------------------------------------------- /src/types/argv-split/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "argv-split" { 2 | export default function split(str: string): string[]; 3 | } -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts", "src/**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } -------------------------------------------------------------------------------- /src/browse/web/assets/styles/CampaignContent.scss: -------------------------------------------------------------------------------- 1 | .campaign-content { 2 | width: 100%; 3 | 4 | &--post { 5 | max-width: 40.5em; 6 | } 7 | } -------------------------------------------------------------------------------- /bin/patreon-dl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import PatreonDownloaderCLI from '../dist/cli/index.js'; 4 | 5 | (new PatreonDownloaderCLI()).start(); 6 | -------------------------------------------------------------------------------- /src/entities/List.ts: -------------------------------------------------------------------------------- 1 | export interface List { 2 | url: string; // API URL 3 | items: T[]; 4 | total: number | null; 5 | nextURL: string | null; 6 | } 7 | -------------------------------------------------------------------------------- /bin/patreon-dl-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import PatreonDownloaderCLI from '../dist/cli/server/index.js'; 4 | 5 | (new PatreonDownloaderCLI()).start(); 6 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin line-clamp($lines: 1) { 2 | display: -webkit-box; 3 | -webkit-box-orient: vertical; 4 | -webkit-line-clamp: $lines; 5 | overflow: hidden; 6 | } -------------------------------------------------------------------------------- /src/browse/web/assets/styles/LightGalleryItem.scss: -------------------------------------------------------------------------------- 1 | .light-gallery-item { 2 | position: relative; 3 | 4 | &__badge { 5 | position: absolute; 6 | font-size: 0.85em; 7 | top: 1em; 8 | left: 1em; 9 | z-index: 1001; 10 | } 11 | } -------------------------------------------------------------------------------- /src/browse/web/assets/styles/SidebarTrigger.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .sidebar-trigger { 4 | text-decoration: none; 5 | color: var(--bs-body-color); 6 | 7 | &:hover { 8 | color: var(--bs-link-hover-color) 9 | } 10 | 11 | transition: color 150ms; 12 | } -------------------------------------------------------------------------------- /src/downloaders/task/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DownloadTaskBatchEvent.js'; 2 | export { IDownloadTask, DownloadTaskStatus, DownloadProgress, DownloadTaskError, DownloadTaskSkipReason } from './DownloadTask.js'; 3 | export { IDownloadTaskBatch } from './DownloadTaskBatch.js'; 4 | -------------------------------------------------------------------------------- /src/browse/web/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import { BrowserRouter } from 'react-router'; 3 | import App from './App'; 4 | 5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/ProductList.scss: -------------------------------------------------------------------------------- 1 | .product-list { 2 | --card-width: 300px; 3 | --gap: 16px; 4 | 5 | width: 100%; 6 | display: flex; 7 | flex-wrap: wrap; 8 | gap: var(--gap); 9 | 10 | &__card-wrapper { 11 | width: var(--card-width); 12 | flex-grow: 1; 13 | 14 | &--fixed { 15 | flex-grow: 0; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/utils/logging/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ChainLogger } from './ChainLogger.js'; 2 | export { default as ConsoleLogger } from './ConsoleLogger.js'; 3 | export { default as FileLogger } from './FileLogger.js'; 4 | export { default as Logger, LogLevel, LogEntry } from './Logger.js'; 5 | 6 | export * from './ConsoleLogger.js'; 7 | export * from './FileLogger.js'; 8 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin": ["typedoc-plugin-markdown", "typedoc-plugin-rename-defaults"], 3 | "excludePrivate": true, 4 | "excludeProtected": true, 5 | "excludeExternals": true, 6 | "excludeInternal": true, 7 | "excludeTags": ["@privateRemarks"], 8 | "entryPoints": ["src/index.ts"], 9 | "readme": "none", 10 | "out": "docs/api" 11 | } 12 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/FilterModal.scss: -------------------------------------------------------------------------------- 1 | .filter-modal { 2 | &__section { 3 | transition: height 150ms ease, margin-bottom 150ms ease; 4 | 5 | &--hidden { 6 | overflow: hidden; 7 | height: 0 !important; 8 | margin-bottom: 0 !important; 9 | } 10 | } 11 | 12 | &__section:not(:last-of-type) { 13 | margin-bottom: 2rem; 14 | } 15 | } -------------------------------------------------------------------------------- /docs/api/type-aliases/NoDeepTypes.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / NoDeepTypes 6 | 7 | # Type Alias: NoDeepTypes 8 | 9 | > **NoDeepTypes** = [`DateTime`](../classes/DateTime.md) 10 | 11 | Defined in: [src/utils/Misc.ts:7](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Misc.ts#L7) 12 | -------------------------------------------------------------------------------- /docs/api/type-aliases/Tier.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / Tier 6 | 7 | # Type Alias: Tier 8 | 9 | > **Tier** = `Pick`\<[`Reward`](../interfaces/Reward.md), `"id"` \| `"title"`\> 10 | 11 | Defined in: [src/entities/Reward.ts:16](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L16) 12 | -------------------------------------------------------------------------------- /docs/api/type-aliases/LogLevel.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / LogLevel 6 | 7 | # Type Alias: LogLevel 8 | 9 | > **LogLevel** = `"info"` \| `"debug"` \| `"warn"` \| `"error"` 10 | 11 | Defined in: [src/utils/logging/Logger.ts:1](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/Logger.ts#L1) 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './downloaders/index.js'; 2 | export * from './downloaders/task/index.js'; 3 | export * from './entities/index.js'; 4 | export * from './utils/index.js'; 5 | export * from './utils/logging/index.js'; 6 | 7 | export * from './browse/server/WebServer.js'; 8 | 9 | import { default as PatreonDownloader } from './downloaders/Downloader.js'; 10 | export default PatreonDownloader; 11 | -------------------------------------------------------------------------------- /docs/api/variables/SITE_URL.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / SITE\_URL 6 | 7 | # Variable: SITE\_URL 8 | 9 | > `const` **SITE\_URL**: `"https://www.patreon.com"` = `'https://www.patreon.com'` 10 | 11 | Defined in: [src/utils/URLHelper.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/URLHelper.ts#L5) 12 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Campaign.js'; 2 | export { Downloadable, Downloaded } from './Downloadable.js'; 3 | export * from './MediaItem.js'; 4 | export { Post, PostEmbed, YouTubePostEmbed, LinkedAttachment, Collection, PostTag } from './Post.js'; 5 | export { Product } from './Product.js'; 6 | export { Comment, CommentReply } from './Comment.js'; 7 | export * from './Reward.js'; 8 | export * from './User.js'; -------------------------------------------------------------------------------- /src/browse/types/Settings.ts: -------------------------------------------------------------------------------- 1 | export interface BrowseTheme { 2 | name: string; 3 | value: string; 4 | stylesheets: string[]; 5 | } 6 | 7 | export interface BrowseSettings { 8 | theme: string; 9 | listItemsPerPage: number; 10 | galleryItemsPerPage: number; 11 | } 12 | 13 | export interface BrowseSettingOptions { 14 | themes: BrowseTheme[]; 15 | listItemsPerPage: number[]; 16 | galleryItemsPerPage: number[]; 17 | } -------------------------------------------------------------------------------- /docs/api/type-aliases/CommentReply.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / CommentReply 6 | 7 | # Type Alias: CommentReply 8 | 9 | > **CommentReply** = `Omit`\<[`Comment`](../interfaces/Comment.md), `"replyCount"` \| `"replies"`\> 10 | 11 | Defined in: [src/entities/Comment.ts:18](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L18) 12 | -------------------------------------------------------------------------------- /docs/api/variables/DEFAULT_WEB_SERVER_PORT.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DEFAULT\_WEB\_SERVER\_PORT 6 | 7 | # Variable: DEFAULT\_WEB\_SERVER\_PORT 8 | 9 | > `const` **DEFAULT\_WEB\_SERVER\_PORT**: `3000` = `3000` 10 | 11 | Defined in: [src/browse/server/WebServer.ts:11](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L11) 12 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloaderType.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloaderType 6 | 7 | # Type Alias: DownloaderType 8 | 9 | > **DownloaderType** = [`Product`](../interfaces/Product.md) \| [`Post`](../interfaces/Post.md) 10 | 11 | Defined in: [src/downloaders/Bootstrap.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L5) 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Use Node.js 12 | uses: actions/setup-node@v4 13 | with: 14 | cache: 'npm' 15 | - name: Install dependencies 16 | run: npm ci 17 | - name: Run linter 18 | run: npm run lint 19 | - name: Build project 20 | run: npm run build 21 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DenoInstallStatus.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DenoInstallStatus 6 | 7 | # Type Alias: DenoInstallStatus 8 | 9 | > **DenoInstallStatus** = \{ `installed`: `true`; `version`: `string`; \} \| \{ `error`: `Error`; `installed`: `false`; \} 10 | 11 | Defined in: [src/utils/Misc.ts:49](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Misc.ts#L49) 12 | -------------------------------------------------------------------------------- /src/entities/Reward.ts: -------------------------------------------------------------------------------- 1 | import { type Downloadable } from './Downloadable.js'; 2 | 3 | export interface Reward { 4 | type: 'reward'; 5 | id: string; 6 | title: string | null; 7 | description: string | null; 8 | amount: string | null; 9 | createdAt: string | null; 10 | publishedAt: string | null; 11 | editedAt: string | null; 12 | image: Downloadable | null; 13 | url: string | null; 14 | } 15 | 16 | export type Tier = Pick; 17 | -------------------------------------------------------------------------------- /docs/api/type-aliases/FileExistsAction.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / FileExistsAction 6 | 7 | # Type Alias: FileExistsAction 8 | 9 | > **FileExistsAction** = `"overwrite"` \| `"skip"` \| `"saveAsCopy"` \| `"saveAsCopyIfNewer"` 10 | 11 | Defined in: [src/downloaders/DownloaderOptions.ts:9](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderOptions.ts#L9) 12 | -------------------------------------------------------------------------------- /docs/api/type-aliases/ImageType.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / ImageType 6 | 7 | # Type Alias: ImageType 8 | 9 | > **ImageType** = `"single"` \| `"default"` \| `"campaignCoverPhoto"` \| `"postCoverImage"` \| `"postThumbnail"` \| `"collectionThumbnail"` 10 | 11 | Defined in: [src/entities/MediaItem.ts:8](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L8) 12 | -------------------------------------------------------------------------------- /docs/api/type-aliases/UserIdOrVanityParam.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / UserIdOrVanityParam 6 | 7 | # Type Alias: UserIdOrVanityParam 8 | 9 | > **UserIdOrVanityParam** = \{ `userId`: `string`; `vanity?`: `never`; \} \| \{ `userId?`: `never`; `vanity`: `string`; \} 10 | 11 | Defined in: [src/entities/User.ts:18](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/User.ts#L18) 12 | -------------------------------------------------------------------------------- /src/downloaders/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Bootstrap.js'; 2 | export * from './Downloader.js'; 3 | export * from './DownloaderEvent.js'; 4 | export { FileExistsAction, StopOnCondition, DownloaderInit, DownloaderOptions, DownloaderIncludeOptions, EmbedDownloader, ProxyOptions, getDefaultDownloaderOptions } from './DownloaderOptions.js'; 5 | export { default as PostDownloader, PostDownloaderContext } from './PostDownloader.js'; 6 | export { default as ProductDownloader } from './ProductDownloader.js'; 7 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloaderEvent.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloaderEvent 6 | 7 | # Type Alias: DownloaderEvent 8 | 9 | > **DownloaderEvent** = `"fetchBegin"` \| `"targetBegin"` \| `"targetEnd"` \| `"phaseBegin"` \| `"phaseEnd"` \| `"end"` 10 | 11 | Defined in: [src/downloaders/DownloaderEvent.ts:6](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderEvent.ts#L6) 12 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/RewardCard.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .reward-card { 4 | width: 100%; 5 | height: 100%; 6 | 7 | &__image { 8 | max-height: 10em; 9 | object-fit: contain; 10 | } 11 | 12 | &__title { 13 | @include mixins.line-clamp(); 14 | 15 | font-weight: bold; 16 | } 17 | 18 | &__content { 19 | font-size: 0.9em; 20 | 21 | p { 22 | margin-bottom: 0; 23 | } 24 | 25 | ul { 26 | padding-left: 1em; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/browse/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | patreon-dl 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/api/type-aliases/YouTubePostEmbed.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / YouTubePostEmbed 6 | 7 | # Type Alias: YouTubePostEmbed 8 | 9 | > **YouTubePostEmbed** = [`PostEmbed`](../interfaces/PostEmbed.md) & `object` 10 | 11 | Defined in: [src/entities/Post.ts:150](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L150) 12 | 13 | ## Type declaration 14 | 15 | ### provider 16 | 17 | > **provider**: `"YouTube"` 18 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloadTaskStatus.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloadTaskStatus 6 | 7 | # Type Alias: DownloadTaskStatus 8 | 9 | > **DownloadTaskStatus** = `"pending"` \| `"pending-retry"` \| `"downloading"` \| `"error"` \| `"completed"` \| `"aborted"` \| `"skipped"` 10 | 11 | Defined in: [src/downloaders/task/DownloadTask.ts:71](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L71) 12 | -------------------------------------------------------------------------------- /docs/api/type-aliases/StopOnCondition.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / StopOnCondition 6 | 7 | # Type Alias: StopOnCondition 8 | 9 | > **StopOnCondition** = `"never"` \| `"previouslyDownloaded"` \| `"publishDateOutOfRange"` \| `"postPreviouslyDownloaded"` \| `"postPublishDateOutOfRange"` 10 | 11 | Defined in: [src/downloaders/DownloaderOptions.ts:10](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderOptions.ts#L10) 12 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { DeepRequired, NoDeepTypes, isDenoInstalled, DenoInstallStatus } from './Misc.js'; 2 | export { default as DateTime } from './DateTime.js'; 3 | export * from './DateTime.js'; 4 | export { default as YouTubeCredentialsCapturer } from './YouTubeCredentialsCapturer.js'; 5 | export * from './YouTubeCredentialsCapturer.js'; 6 | export { default as URLHelper } from './URLHelper.js'; 7 | export * from './URLHelper.js'; 8 | export { FetcherError } from './Fetcher.js'; 9 | export { createProxyAgent, ProxyAgentInfo } from './Proxy.js'; -------------------------------------------------------------------------------- /src/browse/web/assets/styles/Slider.scss: -------------------------------------------------------------------------------- 1 | // Make react-slick slide elements the same height 2 | // https://github.com/akiran/react-slick/issues/1539 3 | .slider--h100 { 4 | .slick-track { 5 | display: flex !important; 6 | 7 | .slick-slide { 8 | height: auto; 9 | display: flex !important; 10 | 11 | > div { 12 | display: flex; 13 | width :100%; 14 | } 15 | } 16 | } 17 | } 18 | 19 | .slider { 20 | &__dots li button::before { 21 | color: var(--bs-body-color) !important; 22 | } 23 | } -------------------------------------------------------------------------------- /src/browse/web/assets/styles/PostCard.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .post-card { 4 | overflow: hidden; 5 | 6 | &__inline-media-wrapper { 7 | text-align: center; 8 | 9 | img { 10 | max-width: 100%; 11 | } 12 | } 13 | 14 | &__inline-media-caption, 15 | &__external-embed-caption { 16 | font-size: smaller; 17 | } 18 | 19 | &__external-embed-wrapper { 20 | text-align: center; 21 | } 22 | 23 | &__external-embed { 24 | > iframe { 25 | width: 100%; 26 | height: 100%; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/entities/Comment.ts: -------------------------------------------------------------------------------- 1 | import { type List } from "./List.js"; 2 | import { type User } from "./User.js"; 3 | 4 | export type CommentList = List; 5 | export type CommentReplyList = List; 6 | 7 | export interface Comment { 8 | type: 'comment'; 9 | id: string; 10 | body: string; 11 | commenter: User | null; 12 | createdAt: string | null; 13 | isByCreator: boolean; 14 | replyCount: number; 15 | replies: Array; 16 | } 17 | 18 | export type CommentReply = Omit; 19 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/FilterModalButton.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .filter-modal-button { 4 | display: flex; 5 | align-items: center; 6 | white-space: nowrap; 7 | 8 | &--show::before { 9 | content: "\e152"; // filter_list 10 | font-family: "Material Icons"; 11 | font-size: 1.2em; 12 | margin-right: 0.2em; 13 | } 14 | 15 | &--clear { 16 | &::before { 17 | content: "\e5cd"; // close 18 | font-family: "Material Icons"; 19 | font-size: 1.2em; 20 | margin-right: 0.2em; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloadTaskBatchEvent.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloadTaskBatchEvent 6 | 7 | # Type Alias: DownloadTaskBatchEvent 8 | 9 | > **DownloadTaskBatchEvent** = `"taskStart"` \| `"taskProgress"` \| `"taskComplete"` \| `"taskError"` \| `"taskAbort"` \| `"taskSkip"` \| `"taskSpawn"` \| `"complete"` 10 | 11 | Defined in: [src/downloaders/task/DownloadTaskBatchEvent.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTaskBatchEvent.ts#L3) 12 | -------------------------------------------------------------------------------- /src/browse/db/updaters/DBUpdater_1_1_0.ts: -------------------------------------------------------------------------------- 1 | import { type Database } from "better-sqlite3"; 2 | import type Logger from '../../../utils/logging/Logger.js'; 3 | import { type DBUpdater } from "../Update.js"; 4 | 5 | const TARGET_VERSION = '1.1.0'; 6 | 7 | function update(_db: Database, _currentVersion: string, _logger?: Logger | null) { 8 | // Only indexes added in v1.1.0, which should have been created in Init, so 9 | // nothing to do here. 10 | return Promise.resolve(); 11 | } 12 | 13 | export const DBUpdater_1_1_0: DBUpdater = { 14 | targetVersion: TARGET_VERSION, 15 | update 16 | }; -------------------------------------------------------------------------------- /docs/api/functions/isDenoInstalled.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / isDenoInstalled 6 | 7 | # Function: isDenoInstalled() 8 | 9 | > **isDenoInstalled**(`pathToDeno?`): [`DenoInstallStatus`](../type-aliases/DenoInstallStatus.md) 10 | 11 | Defined in: [src/utils/Misc.ts:96](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Misc.ts#L96) 12 | 13 | ## Parameters 14 | 15 | ### pathToDeno? 16 | 17 | `string` 18 | 19 | ## Returns 20 | 21 | [`DenoInstallStatus`](../type-aliases/DenoInstallStatus.md) 22 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/CampaignCard.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .campaign-card { 4 | overflow: hidden; 5 | 6 | &__avatar { 7 | height: 6.5em; 8 | aspect-ratio: 1; 9 | object-fit: cover; 10 | } 11 | 12 | &__title { 13 | @include mixins.line-clamp(); 14 | color: var(--bs-link-color); 15 | font-weight: bold; 16 | } 17 | 18 | &__creation-name { 19 | @include mixins.line-clamp(); 20 | } 21 | 22 | &__count-icon { 23 | font-size: 1.2em !important; 24 | margin-right: 0.3em; 25 | } 26 | 27 | &__count-text { 28 | font-size: 1em; 29 | } 30 | } -------------------------------------------------------------------------------- /src/browse/web/assets/styles/CustomScrollbars.scss: -------------------------------------------------------------------------------- 1 | .custom-scrollbars { 2 | &__track-vertical { 3 | top: 0; 4 | bottom: 0; 5 | right: 0; 6 | background: var(--bs-secondary); 7 | opacity: 0.2; 8 | border-radius: 3px; 9 | } 10 | 11 | &__track-horizontal { 12 | left: 0; 13 | right: 0; 14 | bottom: 0; 15 | background: var(--bs-secondary); 16 | opacity: 0.2; 17 | border-radius: 3px; 18 | } 19 | 20 | &__thumb-vertical, 21 | &__thumb-horizontal { 22 | border-radius: 3px; 23 | background: var(--bs-light); 24 | opacity: 0.2; 25 | } 26 | } -------------------------------------------------------------------------------- /src/browse/web/assets/styles/CollectionCard.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .collection-card { 4 | overflow: hidden; 5 | 6 | &__thumbnail > img { 7 | height: 6.5em; 8 | aspect-ratio: 1; 9 | object-fit: cover; 10 | } 11 | 12 | &__title { 13 | @include mixins.line-clamp(); 14 | color: var(--bs-link-color); 15 | font-weight: bold; 16 | } 17 | 18 | &__description { 19 | @include mixins.line-clamp(); 20 | } 21 | 22 | &__count-icon { 23 | font-size: 1.2em !important; 24 | margin-right: 0.3em; 25 | } 26 | 27 | &__count-text { 28 | font-size: 1em; 29 | } 30 | } -------------------------------------------------------------------------------- /src/entities/User.ts: -------------------------------------------------------------------------------- 1 | import { type Downloadable } from './Downloadable.js'; 2 | import { type SingleImageMediaItem } from './MediaItem.js'; 3 | 4 | export interface User { 5 | type: 'user'; 6 | id: string; 7 | firstName: string; 8 | lastName: string; 9 | fullName: string; 10 | createdAt: string | null; 11 | image: Downloadable; 12 | thumbnail: Downloadable; 13 | url: string; 14 | vanity: string | null; 15 | raw: object; 16 | } 17 | 18 | export type UserIdOrVanityParam = { userId: string; vanity?: never } | { userId?: never; vanity: string }; 19 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloaderConfig.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloaderConfig 6 | 7 | # Type Alias: DownloaderConfig\ 8 | 9 | > **DownloaderConfig**\<`T`\> = [`DownloaderInit`](DownloaderInit.md) & [`DownloaderBootstrapData`](DownloaderBootstrapData.md)\<`T`\> 10 | 11 | Defined in: [src/downloaders/Downloader.ts:31](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Downloader.ts#L31) 12 | 13 | ## Type Parameters 14 | 15 | ### T 16 | 17 | `T` *extends* [`DownloaderType`](DownloaderType.md) 18 | -------------------------------------------------------------------------------- /docs/api/type-aliases/GetCampaignParams.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / GetCampaignParams 6 | 7 | # Type Alias: GetCampaignParams 8 | 9 | > **GetCampaignParams** = `string` \| \{ `campaignId?`: `never`; `userId`: `string`; `vanity?`: `never`; \} \| \{ `campaignId?`: `never`; `userId?`: `never`; `vanity`: `string`; \} \| \{ `campaignId`: `string`; `userId?`: `never`; `vanity?`: `never`; \} 10 | 11 | Defined in: [src/downloaders/Downloader.ts:39](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Downloader.ts#L39) 12 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloaderEventPayloadOf.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloaderEventPayloadOf 6 | 7 | # Type Alias: DownloaderEventPayloadOf\ 8 | 9 | > **DownloaderEventPayloadOf**\<`T`\> = [`DownloaderEventPayload`](../interfaces/DownloaderEventPayload.md)\[`T`\] 10 | 11 | Defined in: [src/downloaders/DownloaderEvent.ts:68](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderEvent.ts#L68) 12 | 13 | ## Type Parameters 14 | 15 | ### T 16 | 17 | `T` *extends* [`DownloaderEvent`](DownloaderEvent.md) 18 | -------------------------------------------------------------------------------- /docs/api/interfaces/DownloaderStartParams.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloaderStartParams 6 | 7 | # Interface: DownloaderStartParams 8 | 9 | Defined in: [src/downloaders/Downloader.ts:35](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Downloader.ts#L35) 10 | 11 | ## Properties 12 | 13 | ### signal? 14 | 15 | > `optional` **signal**: `AbortSignal` 16 | 17 | Defined in: [src/downloaders/Downloader.ts:36](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Downloader.ts#L36) 18 | -------------------------------------------------------------------------------- /docs/api/functions/createProxyAgent.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / createProxyAgent 6 | 7 | # Function: createProxyAgent() 8 | 9 | > **createProxyAgent**(`options`): `null` \| [`ProxyAgentInfo`](../interfaces/ProxyAgentInfo.md) 10 | 11 | Defined in: [src/utils/Proxy.ts:14](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Proxy.ts#L14) 12 | 13 | ## Parameters 14 | 15 | ### options 16 | 17 | [`DownloaderOptions`](../interfaces/DownloaderOptions.md) 18 | 19 | ## Returns 20 | 21 | `null` \| [`ProxyAgentInfo`](../interfaces/ProxyAgentInfo.md) 22 | -------------------------------------------------------------------------------- /src/browse/db/updaters/DBUpdater_1_2_0.ts: -------------------------------------------------------------------------------- 1 | import { type Database } from "better-sqlite3"; 2 | import type Logger from '../../../utils/logging/Logger.js'; 3 | import { type DBUpdater } from "../Update.js"; 4 | import { buildPostFTS } from "../PostFTS.js"; 5 | import { buildProductFTS } from "../ProductFTS.js"; 6 | 7 | const TARGET_VERSION = '1.2.0'; 8 | 9 | function update(_db: Database, _currentVersion: string, _logger?: Logger | null) { 10 | buildPostFTS(_db, _logger); 11 | buildProductFTS(_db, _logger); 12 | return Promise.resolve(); 13 | } 14 | 15 | export const DBUpdater_1_2_0: DBUpdater = { 16 | targetVersion: TARGET_VERSION, 17 | update 18 | }; -------------------------------------------------------------------------------- /docs/api/type-aliases/MediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / MediaItem 6 | 7 | # Type Alias: MediaItem 8 | 9 | > **MediaItem** = [`ImageMediaItem`](ImageMediaItem.md)\<`any`\> \| [`VideoMediaItem`](../interfaces/VideoMediaItem.md) \| [`AudioMediaItem`](../interfaces/AudioMediaItem.md) \| [`FileMediaItem`](../interfaces/FileMediaItem.md) \| [`AttachmentMediaItem`](../interfaces/AttachmentMediaItem.md) \| [`DummyMediaItem`](../interfaces/DummyMediaItem.md) 10 | 11 | Defined in: [src/entities/MediaItem.ts:142](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L142) 12 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloadTaskBatchEventPayloadOf.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloadTaskBatchEventPayloadOf 6 | 7 | # Type Alias: DownloadTaskBatchEventPayloadOf\ 8 | 9 | > **DownloadTaskBatchEventPayloadOf**\<`T`\> = [`DownloadTaskBatchEventPayload`](../interfaces/DownloadTaskBatchEventPayload.md)\[`T`\] 10 | 11 | Defined in: [src/downloaders/task/DownloadTaskBatchEvent.ts:50](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTaskBatchEvent.ts#L50) 12 | 13 | ## Type Parameters 14 | 15 | ### T 16 | 17 | `T` *extends* [`DownloadTaskBatchEvent`](DownloadTaskBatchEvent.md) 18 | -------------------------------------------------------------------------------- /src/utils/logging/Logger.ts: -------------------------------------------------------------------------------- 1 | export type LogLevel = 'info' | 'debug' | 'warn' | 'error'; 2 | 3 | export interface LogEntry { 4 | level: LogLevel, 5 | originator?: string, 6 | message: any[] 7 | } 8 | 9 | export default abstract class Logger { 10 | abstract log(entry: LogEntry): void; 11 | 12 | end(): Promise { 13 | return Promise.resolve(); 14 | } 15 | } 16 | 17 | export function commonLog( 18 | logger: Logger | null | undefined, 19 | level: LogLevel, 20 | originator: string | null | undefined, 21 | ...message: any[]) { 22 | 23 | if (logger) { 24 | logger.log({ 25 | level, 26 | originator: originator || undefined, 27 | message 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/entities/Campaign.ts: -------------------------------------------------------------------------------- 1 | import { type Downloadable } from './Downloadable.js'; 2 | import { type CampaignCoverPhotoMediaItem, type DefaultImageMediaItem } from './MediaItem.js'; 3 | import { type Reward } from './Reward.js'; 4 | import { type User } from './User.js'; 5 | 6 | export interface Campaign { 7 | type: 'campaign'; 8 | id: string; 9 | name: string; 10 | createdAt: string | null; 11 | publishedAt: string | null; 12 | avatarImage: Downloadable; 13 | coverPhoto: Downloadable; 14 | summary: string | null; 15 | url: string | null; 16 | currency: string | null; 17 | rewards: Reward[]; 18 | creator: User | null; 19 | raw: object; 20 | } 21 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DeepRequired.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DeepRequired 6 | 7 | # Type Alias: DeepRequired\ 8 | 9 | > **DeepRequired**\<`T`, `E`\> = `T` *extends* `E` ? `T` : `T` *extends* \[infer I\] ? \[`DeepRequired`\<`I`\>\] : `T` *extends* infer I[] ? `DeepRequired`\<`I`\>[] : `T` *extends* `object` ? `{ [P in keyof T]-?: DeepRequired }` : `T` *extends* `undefined` ? `never` : `T` 10 | 11 | Defined in: [src/utils/Misc.ts:10](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Misc.ts#L10) 12 | 13 | ## Type Parameters 14 | 15 | ### T 16 | 17 | `T` 18 | 19 | ### E 20 | 21 | `E` = [`NoDeepTypes`](NoDeepTypes.md) 22 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/FadeContent.scss: -------------------------------------------------------------------------------- 1 | .fade-content { 2 | --fade-content-height: 480px; 3 | --fade-content-bg: var(--bs-card-bg); 4 | 5 | position: relative; 6 | 7 | &__body { 8 | overflow: hidden; 9 | position: relative; 10 | transition: max-height 0.3s ease; 11 | max-height: var(--fade-content-height); 12 | 13 | &--expanded { 14 | max-height: none; 15 | } 16 | } 17 | 18 | &__overlay { 19 | position: absolute; 20 | bottom: 0; 21 | left: 0; 22 | width: 100%; 23 | height: 50px; 24 | background: linear-gradient(to bottom, transparent, var(--fade-content-bg)); 25 | pointer-events: none; 26 | } 27 | 28 | &__toggle { 29 | margin-top: 0.5rem; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/browse/web/utils/RawDataExtractor.ts: -------------------------------------------------------------------------------- 1 | import { type Product, type Campaign } from "../../../entities"; 2 | import ObjectHelper from "../../../utils/ObjectHelper.js"; 3 | 4 | export default class RawDataExtractor { 5 | static getCampaignCreationName(campaign: Campaign) { 6 | return ObjectHelper.getProperty(campaign, 'raw.attributes.creation_name'); 7 | } 8 | 9 | static getProductRichTextDescription(product: Product): string | null { 10 | const rich = ObjectHelper.getProperty(product, 'raw.data.attributes.description_rich_text'); 11 | if (rich) { 12 | return rich; 13 | } 14 | if (product.description) { 15 | return product.description.replaceAll('\n', '
'); 16 | } 17 | return null; 18 | } 19 | } -------------------------------------------------------------------------------- /docs/api/type-aliases/Downloadable.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / Downloadable 6 | 7 | # Type Alias: Downloadable\ 8 | 9 | > **Downloadable**\<`T`\> = `T` & `object` 10 | 11 | Defined in: [src/entities/Downloadable.ts:21](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Downloadable.ts#L21) 12 | 13 | ## Type declaration 14 | 15 | ### downloaded? 16 | 17 | > `optional` **downloaded**: [`Downloaded`](../interfaces/Downloaded.md) 18 | 19 | ## Type Parameters 20 | 21 | ### T 22 | 23 | `T` *extends* [`MediaItem`](MediaItem.md) \| [`PostEmbed`](../interfaces/PostEmbed.md) = [`MediaItem`](MediaItem.md) \| [`PostEmbed`](../interfaces/PostEmbed.md) 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "compilerOptions": { 4 | "target": "es2021", 5 | "module": "ESNext", 6 | "jsx": "react-jsx", 7 | "outDir": "dist", 8 | "declaration": true, 9 | "esModuleInterop": true, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "inlineSources": true, 13 | "strict": true, 14 | "strictPropertyInitialization": false, 15 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 16 | "typeRoots": [ 17 | "node_modules/@types", 18 | "src/types" 19 | ], 20 | "paths": { 21 | "argv-split": ["./src/types/argv-split"] 22 | } 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "src/types/**", 27 | "src/browse/web/**/*" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DateTimeConstructorArgs.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DateTimeConstructorArgs 6 | 7 | # Type Alias: DateTimeConstructorArgs 8 | 9 | > **DateTimeConstructorArgs** = `object` & \{ `gmt?`: `string`; `hour`: `number`; `minute`: `number`; `second?`: `number`; \} \| \{ `gmt?`: `undefined`; `hour?`: `undefined`; `minute?`: `undefined`; `second?`: `undefined`; \} 10 | 11 | Defined in: [src/utils/DateTime.ts:1](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/DateTime.ts#L1) 12 | 13 | ## Type declaration 14 | 15 | ### day 16 | 17 | > **day**: `number` 18 | 19 | ### month 20 | 21 | > **month**: `number` 22 | 23 | ### year 24 | 25 | > **year**: `number` 26 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloaderBootstrapData.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloaderBootstrapData 6 | 7 | # Type Alias: DownloaderBootstrapData\ 8 | 9 | > **DownloaderBootstrapData**\<`T`\> = `T`\[`"type"`\] *extends* `"product"` ? [`ProductDownloaderBootstrapData`](../interfaces/ProductDownloaderBootstrapData.md) : `T`\[`"type"`\] *extends* `"post"` ? [`PostDownloaderBootstrapData`](../interfaces/PostDownloaderBootstrapData.md) : `never` 10 | 11 | Defined in: [src/downloaders/Bootstrap.ts:47](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L47) 12 | 13 | ## Type Parameters 14 | 15 | ### T 16 | 17 | `T` *extends* [`DownloaderType`](DownloaderType.md) 18 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/CampaignHeader.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .campaign-header { 4 | 5 | &__cover { 6 | max-height: 10em; 7 | width: 100%; 8 | object-fit: cover; 9 | } 10 | 11 | &__avatar { 12 | height: 8em; 13 | aspect-ratio: 1; 14 | object-fit: cover; 15 | } 16 | 17 | &__title { 18 | @include mixins.line-clamp(); 19 | 20 | font-weight: bold; 21 | } 22 | 23 | &__creation-name { 24 | @include mixins.line-clamp(); 25 | } 26 | 27 | &__nav { 28 | border-bottom: 1px solid var(--bs-secondary); 29 | 30 | a { 31 | padding-bottom: 0.5em; 32 | position: relative; 33 | top: 1px; 34 | 35 | &.active { 36 | border-bottom: 2px solid var(--bs-link-color); 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/browse/web/assets/styles/SliderArrow.scss: -------------------------------------------------------------------------------- 1 | // react-slick's .slick-prev / .slick-next sets a bunch of conflicting styles. 2 | // Need to override them. 3 | 4 | .slider-arrow { 5 | width: auto !important; 6 | height: auto !important; 7 | color: var(--bs-btn-color) !important; 8 | background: var(--bs-btn-bg) !important; 9 | font-size: inherit !important; 10 | 11 | &__icon { 12 | font-size: 2em; 13 | } 14 | 15 | &:hover { 16 | color: var(--bs-btn-hover-color) !important; 17 | background-color: var(--bs-btn-hover-bg) !important; 18 | } 19 | 20 | &.slick-prev::before, 21 | &.slick-next::before { 22 | content: none !important; 23 | } 24 | 25 | &--prev { 26 | left: -2.5em; 27 | } 28 | 29 | &--next { 30 | right: -2.5em; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloadTaskSkipReason.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloadTaskSkipReason 6 | 7 | # Type Alias: DownloadTaskSkipReason 8 | 9 | > **DownloadTaskSkipReason** = `object` & \{ `existingDestFilePath`: `string`; `name`: `"destFileExists"`; \} \| \{ `destFilename`: `string`; `itemType`: `"image"` \| `"audio"` \| `"attachment"`; `name`: `"includeMediaByFilenameUnfulfilled"`; `pattern`: `string`; \} \| \{ `name`: `"dependentTaskNotCompleted"`; \} \| \{ `name`: `"other"`; \} 10 | 11 | Defined in: [src/downloaders/task/DownloadTask.ts:25](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L25) 12 | 13 | ## Type declaration 14 | 15 | ### message 16 | 17 | > **message**: `string` 18 | -------------------------------------------------------------------------------- /docs/api/type-aliases/URLAnalysis.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / URLAnalysis 6 | 7 | # Type Alias: URLAnalysis 8 | 9 | > **URLAnalysis** = \{ `productId`: `string`; `slug`: `string`; `type`: `"product"`; \} \| \{ `filters?`: `Record`\<`string`, `any`\>; `type`: `"postsByUser"`; `vanity`: `string`; \} \| \{ `filters?`: `Record`\<`string`, `any`\>; `type`: `"postsByUserId"`; `userId`: `string`; \} \| \{ `collectionId`: `string`; `filters?`: `Record`\<`string`, `any`\>; `type`: `"postsByCollection"`; \} \| \{ `postId`: `string`; `slug?`: `string`; `type`: `"post"`; \} \| \{ `type`: `"shop"`; `vanity`: `string`; \} 10 | 11 | Defined in: [src/utils/URLHelper.ts:270](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/URLHelper.ts#L270) 12 | -------------------------------------------------------------------------------- /src/browse/api/MediaAPIMixin.ts: -------------------------------------------------------------------------------- 1 | import { type APIConstructor } from "."; 2 | import { type GetMediaListParams, type MediaList, type MediaListSortBy } from "../types/Media.js"; 3 | import { type ContentType } from "../types/Content.js"; 4 | 5 | const DEFAULT_MEDIA_LIST_SIZE = 10; 6 | const DEFAULT_MEDIA_LIST_SORT_BY: MediaListSortBy = 'latest'; 7 | 8 | export function MediaAPIMixin(Base: TBase) { 9 | return class MediaAPI extends Base { 10 | getMediaList(params: GetMediaListParams): MediaList { 11 | const { sortBy = DEFAULT_MEDIA_LIST_SORT_BY, limit = DEFAULT_MEDIA_LIST_SIZE, offset = 0 } = params; 12 | return this.db.getMediaList({ 13 | ...params, 14 | sortBy, 15 | limit, 16 | offset, 17 | }); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/browse/web/components/MediaImage.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, type ImgHTMLAttributes, useCallback } from 'react'; 2 | 3 | interface MediaImageProps extends Omit, 'src'> { 4 | mediaId: string; 5 | thumbnail?: boolean; 6 | } 7 | 8 | const MediaImage = forwardRef((props, ref) => { 9 | const { mediaId, thumbnail = false, ...rest } = props; 10 | 11 | const src = `/media/${mediaId}${ thumbnail ? '?t=1' : ''}`; 12 | 13 | const handleError = useCallback((e: React.SyntheticEvent) => { 14 | e.currentTarget.style.display = 'none'; 15 | }, []); 16 | 17 | return ( 18 | 24 | ) 25 | }); 26 | 27 | export default MediaImage; -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import svgr from "vite-plugin-svgr"; 4 | import { viteStaticCopy } from "vite-plugin-static-copy"; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | react(), 9 | svgr(), 10 | // Themes 11 | viteStaticCopy({ 12 | targets: [ 13 | { 14 | src: "../../../node_modules/bootstrap/dist/*", 15 | dest: "themes/bootstrap/default", 16 | }, 17 | { 18 | src: "../../../node_modules/bootswatch/dist/*", 19 | dest: "themes/bootswatch", 20 | } 21 | ] 22 | }) 23 | ], 24 | root: 'src/browse/web', 25 | build: { 26 | outDir: '../../../dist/browse/web' 27 | }, 28 | resolve: { 29 | alias: { 30 | path: 'path-browserify', 31 | }, 32 | }, 33 | }); -------------------------------------------------------------------------------- /src/downloaders/templates/ProductInfo.ts: -------------------------------------------------------------------------------- 1 | import { type Product } from '../../entities/Product.js'; 2 | 3 | const PRODUCT_INFO_TEMPLATE = 4 | `Product 5 | -------- 6 | ID: {product.id} 7 | Name: {product.name} 8 | Description: {product.description} 9 | Price: {product.price} 10 | Published: {product.publishedAt} 11 | URL: {product.url} 12 | `; 13 | 14 | export function generateProductSummary(product: Product) { 15 | const productInfo = PRODUCT_INFO_TEMPLATE 16 | .replaceAll('{product.id}', product.id) 17 | .replaceAll('{product.name}', product.name || '') 18 | .replaceAll('{product.description}', product.description || '') 19 | .replaceAll('{product.price}', product.price || '') 20 | .replaceAll('{product.publishedAt}', product.publishedAt || '') 21 | .replaceAll('{product.url}', product.url || ''); 22 | 23 | return productInfo; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/WebUtils.ts: -------------------------------------------------------------------------------- 1 | export default class WebUtils { 2 | static getPaginationParams(url: string, defaultItemsPerPage: number) { 3 | const urlObj = new URL(url); 4 | const p = urlObj.searchParams.get('p') || 1; 5 | const n = urlObj.searchParams.get('n') || defaultItemsPerPage; 6 | const page = Number(p); 7 | const itemsPerPage = Number(n); 8 | if (isNaN(page)) { 9 | throw TypeError('Invalid param "p"'); 10 | } 11 | if (page <= 0) { 12 | throw RangeError(`Invalid value "${page}" for param "p"`); 13 | } 14 | if (isNaN(itemsPerPage)) { 15 | throw TypeError('Invalid param "n"'); 16 | } 17 | if (itemsPerPage <= 0) { 18 | throw RangeError(`Invalid value "${itemsPerPage}" for param "n"`); 19 | } 20 | return { 21 | limit: itemsPerPage, 22 | offset: (page - 1) * itemsPerPage 23 | }; 24 | } 25 | } -------------------------------------------------------------------------------- /docs/api/enumerations/FileLoggerType.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / FileLoggerType 6 | 7 | # Enumeration: FileLoggerType 8 | 9 | Defined in: [src/utils/logging/FileLogger.ts:12](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L12) 10 | 11 | ## Enumeration Members 12 | 13 | ### Downloader 14 | 15 | > **Downloader**: `"downloader"` 16 | 17 | Defined in: [src/utils/logging/FileLogger.ts:13](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L13) 18 | 19 | *** 20 | 21 | ### Server 22 | 23 | > **Server**: `"server"` 24 | 25 | Defined in: [src/utils/logging/FileLogger.ts:14](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L14) 26 | -------------------------------------------------------------------------------- /docs/api/interfaces/EmbedDownloader.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / EmbedDownloader 6 | 7 | # Interface: EmbedDownloader 8 | 9 | Defined in: [src/downloaders/DownloaderOptions.ts:54](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderOptions.ts#L54) 10 | 11 | ## Properties 12 | 13 | ### exec 14 | 15 | > **exec**: `string` 16 | 17 | Defined in: [src/downloaders/DownloaderOptions.ts:56](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderOptions.ts#L56) 18 | 19 | *** 20 | 21 | ### provider 22 | 23 | > **provider**: `string` 24 | 25 | Defined in: [src/downloaders/DownloaderOptions.ts:55](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderOptions.ts#L55) 26 | -------------------------------------------------------------------------------- /docs/api/type-aliases/FileLoggerConfig.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / FileLoggerConfig 6 | 7 | # Type Alias: FileLoggerConfig\ 8 | 9 | > **FileLoggerConfig**\<`T`\> = [`DeepRequired`](DeepRequired.md)\<[`FileLoggerOptions`](FileLoggerOptions.md)\<`T`\>\> & `object` 10 | 11 | Defined in: [src/utils/logging/FileLogger.ts:36](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L36) 12 | 13 | ## Type declaration 14 | 15 | ### created 16 | 17 | > **created**: `Date` 18 | 19 | ### logDir 20 | 21 | > **logDir**: `string` 22 | 23 | ### logFilename 24 | 25 | > **logFilename**: `string` 26 | 27 | ### logFilePath 28 | 29 | > **logFilePath**: `string` 30 | 31 | ## Type Parameters 32 | 33 | ### T 34 | 35 | `T` *extends* [`FileLoggerType`](../enumerations/FileLoggerType.md) 36 | -------------------------------------------------------------------------------- /docs/api/type-aliases/FileLoggerOptions.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / FileLoggerOptions 6 | 7 | # Type Alias: FileLoggerOptions\ 8 | 9 | > **FileLoggerOptions**\<`T`\> = [`ConsoleLoggerOptions`](../interfaces/ConsoleLoggerOptions.md) & `T` *extends* [`Downloader`](../enumerations/FileLoggerType.md#downloader) ? [`DownloaderFileLoggerOptions`](../interfaces/DownloaderFileLoggerOptions.md) : `T` *extends* [`Server`](../enumerations/FileLoggerType.md#server) ? [`ServerFileLoggerOptions`](../interfaces/ServerFileLoggerOptions.md) : `never` 10 | 11 | Defined in: [src/utils/logging/FileLogger.ts:17](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L17) 12 | 13 | ## Type Parameters 14 | 15 | ### T 16 | 17 | `T` *extends* [`FileLoggerType`](../enumerations/FileLoggerType.md) 18 | -------------------------------------------------------------------------------- /docs/api/type-aliases/FileLoggerGetPathInfoParams.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / FileLoggerGetPathInfoParams 6 | 7 | # Type Alias: FileLoggerGetPathInfoParams\ 8 | 9 | > **FileLoggerGetPathInfoParams**\<`T`\> = `T` *extends* [`Downloader`](../enumerations/FileLoggerType.md#downloader) ? `Pick`\<[`FileLoggerOptions`](FileLoggerOptions.md)\<`T`\>, `"init"` \| `"logDir"` \| `"logFilename"` \| `"logLevel"`\> : `T` *extends* [`Server`](../enumerations/FileLoggerType.md#server) ? `Pick`\<[`FileLoggerOptions`](FileLoggerOptions.md)\<`T`\>, `"logFilePath"`\> : `never` 10 | 11 | Defined in: [src/utils/logging/FileLogger.ts:44](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L44) 12 | 13 | ## Type Parameters 14 | 15 | ### T 16 | 17 | `T` *extends* [`FileLoggerType`](../enumerations/FileLoggerType.md) 18 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/ProductCard.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .product-card { 4 | width: 100%; 5 | 6 | &--compact { 7 | .product-card__description { 8 | display: none; 9 | } 10 | 11 | .product-card__title { 12 | @include mixins.line-clamp(2); 13 | color: var(--bs-link-color); 14 | } 15 | } 16 | 17 | &__media-nav { 18 | display: flex; 19 | gap: 1.5em; 20 | border-bottom: 1px solid var(--bs-secondary); 21 | } 22 | 23 | &__media-nav a { 24 | position: relative; 25 | top: 1px; 26 | padding: 0.5em 0; 27 | color: var(--bs-link-color); 28 | 29 | &:hover { 30 | color: var(--bs-link-hover-color); 31 | } 32 | 33 | &.active { 34 | color: var(--bs-link-color); 35 | border-bottom: 2px solid var(--bs-link-color); 36 | 37 | &:hover { 38 | color: var(--bs-link-hover-color); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /docs/api/interfaces/ProxyOptions.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / ProxyOptions 6 | 7 | # Interface: ProxyOptions 8 | 9 | Defined in: [src/downloaders/DownloaderOptions.ts:49](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderOptions.ts#L49) 10 | 11 | ## Properties 12 | 13 | ### rejectUnauthorizedTLS? 14 | 15 | > `optional` **rejectUnauthorizedTLS**: `boolean` 16 | 17 | Defined in: [src/downloaders/DownloaderOptions.ts:51](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderOptions.ts#L51) 18 | 19 | *** 20 | 21 | ### url 22 | 23 | > **url**: `string` 24 | 25 | Defined in: [src/downloaders/DownloaderOptions.ts:50](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderOptions.ts#L50) 26 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/PostContent.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .post-nav { 4 | &__previous, 5 | &__next { 6 | display: flex; 7 | align-items: center; 8 | width: calc(50% - 2rem); 9 | 10 | &--fill { 11 | width: calc(90% - 2rem); 12 | } 13 | } 14 | 15 | &__previous-label, 16 | &__next-label { 17 | width: calc(100% - 1.7rem); 18 | @include mixins.line-clamp(2); 19 | } 20 | 21 | &__next-label { 22 | text-align: right; 23 | } 24 | 25 | &__next { 26 | justify-content: end; 27 | } 28 | 29 | &__previous::before { 30 | content: "\eac3"; // keyboard_double_arrow_left 31 | font-family: "Material Icons"; 32 | font-size: 1.2em; 33 | margin-right: 0.5em; 34 | } 35 | 36 | &__next::after { 37 | content: "\eac9"; // keyboard_double_arrow_right 38 | font-family: "Material Icons"; 39 | font-size: 1.2em; 40 | margin-left: 0.5em; 41 | } 42 | } -------------------------------------------------------------------------------- /src/browse/web/components/SidebarTrigger.tsx: -------------------------------------------------------------------------------- 1 | import "../assets/styles/SidebarTrigger.scss"; 2 | import { useState } from "react"; 3 | import { Button, Offcanvas } from "react-bootstrap"; 4 | import Sidebar from "./Sidebar"; 5 | 6 | function SidebarTrigger() { 7 | const [show, setShow] = useState(false); 8 | 9 | return ( 10 | <> 11 | 18 | 19 | setShow(false)} placement="start"> 20 | 21 | setShow(false)}/> 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default SidebarTrigger; -------------------------------------------------------------------------------- /src/browse/web/components/ShowingText.tsx: -------------------------------------------------------------------------------- 1 | interface ShowingTextProps { 2 | total: number; 3 | page: number; 4 | itemsPerPage: number; 5 | subject: { 6 | singular: string; 7 | plural: string; 8 | } | string; 9 | } 10 | 11 | function ShowingText(props: ShowingTextProps) { 12 | const { total, page, itemsPerPage: limit } = props; 13 | 14 | if (total > limit) { 15 | const offset = (page - 1) * limit; 16 | const start = offset + 1; 17 | const end = Math.min(offset + limit, total); 18 | const subject = typeof props.subject === 'string' ? props.subject : props.subject.plural; 19 | return `Showing ${start} - ${end} of ${total} ${subject}`; 20 | } else { 21 | const subject = typeof props.subject === 'string' ? props.subject 22 | : total === 1 ? props.subject.singular 23 | : props.subject.plural; 24 | return `Total ${total} ${subject}`; 25 | } 26 | } 27 | 28 | export default ShowingText; -------------------------------------------------------------------------------- /docs/api/interfaces/ServerFileLoggerOptions.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / ServerFileLoggerOptions 6 | 7 | # Interface: ServerFileLoggerOptions 8 | 9 | Defined in: [src/utils/logging/FileLogger.ts:31](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L31) 10 | 11 | ## Properties 12 | 13 | ### fileExistsAction? 14 | 15 | > `optional` **fileExistsAction**: `"append"` \| `"overwrite"` 16 | 17 | Defined in: [src/utils/logging/FileLogger.ts:33](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L33) 18 | 19 | *** 20 | 21 | ### logFilePath 22 | 23 | > **logFilePath**: `string` 24 | 25 | Defined in: [src/utils/logging/FileLogger.ts:32](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L32) 26 | -------------------------------------------------------------------------------- /src/browse/web/components/SliderArrow.tsx: -------------------------------------------------------------------------------- 1 | import "../assets/styles/SliderArrow.scss"; 2 | import { Button }from "react-bootstrap"; 3 | 4 | interface ArrowProps { 5 | type: 'prev' | 'next'; 6 | className?: string; 7 | style?: React.CSSProperties; 8 | onClick?: () => void; 9 | } 10 | 11 | /** 12 | * Provides custom arrow for react-slick slider 13 | * @param props 14 | * @returns 15 | */ 16 | 17 | function SliderArrow(props: ArrowProps) { 18 | const { type, className, style, onClick } = props; 19 | const iconName = type === 'prev' ? 'chevron_left' : 'chevron_right'; 20 | 21 | return ( 22 | 30 | ) 31 | } 32 | 33 | export default SliderArrow; -------------------------------------------------------------------------------- /docs/api/type-aliases/DownloaderInit.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloaderInit 6 | 7 | # Type Alias: DownloaderInit 8 | 9 | > **DownloaderInit** = [`DeepRequired`](DeepRequired.md)\<`Pick`\<[`DownloaderOptions`](../interfaces/DownloaderOptions.md), `"outDir"` \| `"useStatusCache"` \| `"stopOn"` \| `"pathToFFmpeg"` \| `"pathToYouTubeCredentials"` \| `"pathToDeno"` \| `"dirNameFormat"` \| `"filenameFormat"` \| `"include"` \| `"request"` \| `"fileExistsAction"` \| `"embedDownloaders"` \| `"dryRun"`\>\> & `object` 10 | 11 | Defined in: [src/downloaders/DownloaderOptions.ts:93](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderOptions.ts#L93) 12 | 13 | ## Type declaration 14 | 15 | ### cookie? 16 | 17 | > `optional` **cookie**: `string` 18 | 19 | ### maxVideoResolution? 20 | 21 | > `optional` **maxVideoResolution**: `number` \| `null` 22 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/CollectionBanner.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .collection-banner { 4 | overflow: hidden; 5 | border: none; 6 | border-radius: 0; 7 | max-width: 40.5em; 8 | 9 | &__thumbnail { 10 | z-index: 100; // This is to keep thumbnail above the fade-content overlay 11 | } 12 | 13 | &__thumbnail > img { 14 | border-radius: var(--bs-border-radius); 15 | height: 8em; 16 | aspect-ratio: 1; 17 | object-fit: cover; 18 | } 19 | 20 | &__title { 21 | @include mixins.line-clamp(); 22 | color: var(--bs-link-color); 23 | font-size: 1.2rem; 24 | font-weight: bold; 25 | } 26 | 27 | .fade-content__toggle { 28 | margin-bottom: 0.5em; 29 | } 30 | } 31 | 32 | .collection-banner--nodesc { 33 | .collection-banner__thumbnail > img { 34 | height: 4em; 35 | } 36 | 37 | .collection-banner__title { 38 | flex: 1 auto; 39 | align-items: center; 40 | font-size: 1.5rem; 41 | } 42 | } -------------------------------------------------------------------------------- /docs/api/interfaces/YouTubeCredentialsPendingInfo.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / YouTubeCredentialsPendingInfo 6 | 7 | # Interface: YouTubeCredentialsPendingInfo 8 | 9 | Defined in: [src/utils/YouTubeCredentialsCapturer.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/YouTubeCredentialsCapturer.ts#L4) 10 | 11 | ## Properties 12 | 13 | ### code 14 | 15 | > **code**: `string` 16 | 17 | Defined in: [src/utils/YouTubeCredentialsCapturer.ts:6](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/YouTubeCredentialsCapturer.ts#L6) 18 | 19 | *** 20 | 21 | ### verificationURL 22 | 23 | > **verificationURL**: `string` 24 | 25 | Defined in: [src/utils/YouTubeCredentialsCapturer.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/YouTubeCredentialsCapturer.ts#L5) 26 | -------------------------------------------------------------------------------- /src/browse/types/Campaign.ts: -------------------------------------------------------------------------------- 1 | import { type Campaign } from "../../entities/index.js"; 2 | 3 | export type CampaignListSortBy = 4 | 'a-z' | 5 | 'z-a' | 6 | 'most_content' | 7 | 'most_media' | 8 | 'last_downloaded'; 9 | 10 | export type GetCampaignParams = { 11 | id: string; 12 | vanity?: never; 13 | withCounts?: boolean; 14 | } | { 15 | id?: never; 16 | vanity: string; 17 | withCounts?: boolean; 18 | } 19 | 20 | export interface GetCampaignListParams { 21 | sortBy?: CampaignListSortBy; 22 | limit?: number; 23 | offset?: number; 24 | } 25 | 26 | export interface CampaignList { 27 | campaigns: (Campaign & { 28 | postCount: number; 29 | productCount: number; 30 | mediaCount: number; 31 | collectionCount: number; 32 | })[]; 33 | total: number; 34 | } 35 | 36 | export interface CampaignWithCounts extends Campaign { 37 | postCount: number; 38 | collectionCount: number; 39 | productCount: number; 40 | mediaCount: number; 41 | } 42 | -------------------------------------------------------------------------------- /src/downloaders/templates/CollectionInfo.ts: -------------------------------------------------------------------------------- 1 | import { type Collection } from '../../entities/Post.js'; 2 | 3 | const COLLECTION_INFO_TEMPLATE = 4 | `Collection 5 | ---------- 6 | ID: {collection.id} 7 | Title: {collection.title} 8 | Description: {collection.description} 9 | Number of Posts: {collection.numPosts} 10 | Created: {collection.createdAt} 11 | Last Edited: {collection.editedAt} 12 | `; 13 | 14 | export function generateCollectionSummary(collection: Collection) { 15 | const collectionInfo = COLLECTION_INFO_TEMPLATE 16 | .replaceAll('{collection.id}', collection.id) 17 | .replaceAll('{collection.title}', collection.title || '') 18 | .replaceAll('{collection.description}', collection.description || '') 19 | .replaceAll('{collection.numPosts}', String(collection.numPosts ?? '')) 20 | .replaceAll('{collection.createdAt}', collection.createdAt || '') 21 | .replaceAll('{collection.editedAt}', collection.editedAt || '') 22 | 23 | return collectionInfo; 24 | } -------------------------------------------------------------------------------- /src/utils/ThmbnailFilenameResolver.ts: -------------------------------------------------------------------------------- 1 | import { type Response } from 'undici'; 2 | import FilenameResolver from './FllenameResolver.js'; 3 | import URLHelper from './URLHelper.js'; 4 | import { type DownloadableWithThumbnail } from '../entities/Downloadable.js'; 5 | import FSHelper from './FSHelper.js'; 6 | 7 | export default class ThumbnailFilenameResolver extends FilenameResolver { 8 | 9 | constructor(target: T) { 10 | super(target, target.thumbnailURL); 11 | } 12 | 13 | resolve(response: Response) { 14 | /** 15 | * Obtain `extension` from (in order of priority): 16 | * 1. Response headers 17 | * 2. Src URL 18 | */ 19 | const resFilenameParts = this.getFilenamePartsFromResponse(response); 20 | return FSHelper.createFilename({ 21 | name: this.target.id, 22 | ext: resFilenameParts.ext || URLHelper.getExtensionFromURL(this.srcURL) 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/api/interfaces/PostTag.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / PostTag 6 | 7 | # Interface: PostTag 8 | 9 | Defined in: [src/entities/Post.ts:165](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L165) 10 | 11 | ## Properties 12 | 13 | ### id 14 | 15 | > **id**: `string` 16 | 17 | Defined in: [src/entities/Post.ts:166](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L166) 18 | 19 | *** 20 | 21 | ### type 22 | 23 | > **type**: `"postTag"` 24 | 25 | Defined in: [src/entities/Post.ts:167](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L167) 26 | 27 | *** 28 | 29 | ### value 30 | 31 | > **value**: `string` 32 | 33 | Defined in: [src/entities/Post.ts:168](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L168) 34 | -------------------------------------------------------------------------------- /docs/api/interfaces/BootstrapData.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / BootstrapData 6 | 7 | # Interface: BootstrapData 8 | 9 | Defined in: [src/downloaders/Bootstrap.ts:7](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L7) 10 | 11 | ## Extended by 12 | 13 | - [`ProductDownloaderBootstrapData`](ProductDownloaderBootstrapData.md) 14 | - [`PostDownloaderBootstrapData`](PostDownloaderBootstrapData.md) 15 | 16 | ## Properties 17 | 18 | ### targetURL 19 | 20 | > **targetURL**: `string` 21 | 22 | Defined in: [src/downloaders/Bootstrap.ts:9](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L9) 23 | 24 | *** 25 | 26 | ### type 27 | 28 | > **type**: `string` 29 | 30 | Defined in: [src/downloaders/Bootstrap.ts:8](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L8) 31 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/Sidebar.scss: -------------------------------------------------------------------------------- 1 | @use "./mixins"; 2 | 3 | .sidebar { 4 | width: 100%; 5 | height: 100vh; 6 | border-radius: 0; 7 | 8 | &__header { 9 | background: var(--bs-card-bg); 10 | } 11 | 12 | &__brand a { 13 | color: inherit; 14 | } 15 | 16 | &__section-title { 17 | color: var(--bs-body-color); 18 | font-weight: bold; 19 | font-size: 0.9em; 20 | } 21 | 22 | &__main { 23 | display: flex; 24 | flex-direction: column; 25 | } 26 | 27 | &__link { 28 | display: flex; 29 | align-items: center; 30 | color: var(--bs-body-color); 31 | 32 | &:hover { 33 | color: var(--bs-link-hover-color); 34 | } 35 | 36 | transition: color 150ms; 37 | } 38 | 39 | &__link-icon { 40 | width: 1.5em; 41 | height: 1.5em; 42 | object-fit: cover; 43 | 44 | &--svg { 45 | fill: var(--bs-body-color); 46 | } 47 | } 48 | 49 | &__link-text { 50 | @include mixins.line-clamp(); 51 | 52 | font-size: 0.9em; 53 | } 54 | } -------------------------------------------------------------------------------- /docs/api/interfaces/ProxyAgentInfo.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / ProxyAgentInfo 6 | 7 | # Interface: ProxyAgentInfo 8 | 9 | Defined in: [src/utils/Proxy.ts:6](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Proxy.ts#L6) 10 | 11 | ## Properties 12 | 13 | ### agent 14 | 15 | > **agent**: `Dispatcher` 16 | 17 | Defined in: [src/utils/Proxy.ts:9](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Proxy.ts#L9) 18 | 19 | *** 20 | 21 | ### protocol 22 | 23 | > **protocol**: `"http"` \| `"https"` \| `"socks4"` \| `"socks5"` 24 | 25 | Defined in: [src/utils/Proxy.ts:7](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Proxy.ts#L7) 26 | 27 | *** 28 | 29 | ### proxyURL 30 | 31 | > **proxyURL**: `string` 32 | 33 | Defined in: [src/utils/Proxy.ts:8](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Proxy.ts#L8) 34 | -------------------------------------------------------------------------------- /src/utils/logging/ChainLogger.ts: -------------------------------------------------------------------------------- 1 | import Logger, { type LogEntry } from '../../utils/logging/Logger.js'; 2 | 3 | export default class ChainLogger extends Logger { 4 | 5 | #loggers: Logger[]; 6 | 7 | constructor(loggers?: Logger[]) { 8 | super(); 9 | this.#loggers = loggers || []; 10 | } 11 | 12 | add(logger: Logger) { 13 | this.#loggers.push(logger); 14 | } 15 | 16 | remove(logger: Logger) { 17 | const index = this.#loggers.findIndex((l) => l === logger); 18 | if (index >= 0) { 19 | this.#loggers.splice(index, 1); 20 | } 21 | } 22 | 23 | clear() { 24 | this.#loggers = []; 25 | } 26 | 27 | log(entry: LogEntry): void { 28 | for (const logger of this.#loggers) { 29 | logger.log(entry); 30 | } 31 | } 32 | 33 | async end(): Promise { 34 | const endPromises = this.#loggers.map(async (logger) => { 35 | try { 36 | await logger.end(); 37 | } 38 | catch (_error: unknown) { 39 | // Do nothing 40 | } 41 | }); 42 | await Promise.all(endPromises); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/api/interfaces/LogEntry.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / LogEntry 6 | 7 | # Interface: LogEntry 8 | 9 | Defined in: [src/utils/logging/Logger.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/Logger.ts#L3) 10 | 11 | ## Properties 12 | 13 | ### level 14 | 15 | > **level**: [`LogLevel`](../type-aliases/LogLevel.md) 16 | 17 | Defined in: [src/utils/logging/Logger.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/Logger.ts#L4) 18 | 19 | *** 20 | 21 | ### message 22 | 23 | > **message**: `any`[] 24 | 25 | Defined in: [src/utils/logging/Logger.ts:6](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/Logger.ts#L6) 26 | 27 | *** 28 | 29 | ### originator? 30 | 31 | > `optional` **originator**: `string` 32 | 33 | Defined in: [src/utils/logging/Logger.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/Logger.ts#L5) 34 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | NOTICE 2 | 3 | This project includes the following third-party software: 4 | 5 | - LightGallery (https://www.lightgalleryjs.com/) 6 | Copyright © 2016–2025 7 | Licensed under the GNU General Public License v3.0 (GPLv3) 8 | Developed by Srinivas Tamada and contributors. 9 | Source: https://github.com/sachinchoolur/lightGallery 10 | 11 | - Bottleneck (https://github.com/SGrondin/bottleneck) 12 | Licensed under the BSD 2-Clause License 13 | 14 | - Fast-Copy (https://github.com/planttheidea/fast-copy) 15 | Licensed under the GNU General Public License v3.0 (GPLv3) 16 | 17 | - argv-split (https://github.com/75lb/argv-split) 18 | Licensed under the GNU General Public License v2.0 (GPLv2) 19 | 20 | All other dependencies are used under permissive licenses such as MIT, Apache-2.0, or ISC, and do not require attribution beyond preservation of their license terms in `node_modules`. 21 | 22 | This project is open source and licensed under the MIT License. 23 | In accordance with the terms of the above licenses, this NOTICE file provides attribution and license information for included third-party software. 24 | -------------------------------------------------------------------------------- /docs/api/type-aliases/ImageMediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / ImageMediaItem 6 | 7 | # Type Alias: ImageMediaItem\ 8 | 9 | > **ImageMediaItem**\<`T`\> = `T` *extends* `"single"` ? [`SingleImageMediaItem`](../interfaces/SingleImageMediaItem.md) : `T` *extends* `"default"` ? [`DefaultImageMediaItem`](../interfaces/DefaultImageMediaItem.md) : `T` *extends* `"campaignCoverPhoto"` ? [`CampaignCoverPhotoMediaItem`](../interfaces/CampaignCoverPhotoMediaItem.md) : `T` *extends* `"postCoverImage"` ? [`PostCoverImageMediaItem`](../interfaces/PostCoverImageMediaItem.md) : `T` *extends* `"postThumbnail"` ? [`PostThumbnailMediaItem`](../interfaces/PostThumbnailMediaItem.md) : `T` *extends* `"collectionThumbnail"` ? [`CollectionThumbnailMediaItem`](../interfaces/CollectionThumbnailMediaItem.md) : `never` 10 | 11 | Defined in: [src/entities/MediaItem.ts:90](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L90) 12 | 13 | ## Type Parameters 14 | 15 | ### T 16 | 17 | `T` *extends* [`ImageType`](ImageType.md) 18 | -------------------------------------------------------------------------------- /src/browse/web/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Row, Col, Stack } from "react-bootstrap"; 2 | import { Link, Outlet } from "react-router"; 3 | import Sidebar from "../components/Sidebar"; 4 | import { ScrollProvider } from "../contexts/MainContentScrollProvider"; 5 | import SidebarTrigger from "../components/SidebarTrigger"; 6 | 7 | function MainLayout() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | patreon-dl 20 |
21 |
22 | 23 |
24 | 25 |
26 |
27 | ) 28 | } 29 | 30 | export default MainLayout; -------------------------------------------------------------------------------- /docs/api/enumerations/PostSortOrder.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / PostSortOrder 6 | 7 | # Enumeration: PostSortOrder 8 | 9 | Defined in: [src/utils/URLHelper.ts:264](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/URLHelper.ts#L264) 10 | 11 | ## Enumeration Members 12 | 13 | ### CollectionOrder 14 | 15 | > **CollectionOrder**: `"collection_order"` 16 | 17 | Defined in: [src/utils/URLHelper.ts:267](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/URLHelper.ts#L267) 18 | 19 | *** 20 | 21 | ### PublisedAtDesc 22 | 23 | > **PublisedAtDesc**: `"-published_at"` 24 | 25 | Defined in: [src/utils/URLHelper.ts:265](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/URLHelper.ts#L265) 26 | 27 | *** 28 | 29 | ### PublishedAtAsc 30 | 31 | > **PublishedAtAsc**: `"published_at"` 32 | 33 | Defined in: [src/utils/URLHelper.ts:266](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/URLHelper.ts#L266) 34 | -------------------------------------------------------------------------------- /docs/api/interfaces/DownloaderFileLoggerInit.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloaderFileLoggerInit 6 | 7 | # Interface: DownloaderFileLoggerInit 8 | 9 | Defined in: [src/utils/logging/FileLogger.ts:68](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L68) 10 | 11 | ## Properties 12 | 13 | ### date? 14 | 15 | > `optional` **date**: `Date` 16 | 17 | Defined in: [src/utils/logging/FileLogger.ts:71](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L71) 18 | 19 | *** 20 | 21 | ### outDir? 22 | 23 | > `optional` **outDir**: `string` 24 | 25 | Defined in: [src/utils/logging/FileLogger.ts:70](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L70) 26 | 27 | *** 28 | 29 | ### targetURL 30 | 31 | > **targetURL**: `string` 32 | 33 | Defined in: [src/utils/logging/FileLogger.ts:69](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L69) 34 | -------------------------------------------------------------------------------- /src/browse/types/Media.ts: -------------------------------------------------------------------------------- 1 | import { type Campaign, type Post, type Product, type Tier } from "../../entities/index.js"; 2 | import { type ContentType } from "./Content.js"; 3 | 4 | export type MediaListSortBy = 'latest' | 'oldest'; 5 | 6 | export type GetMediaListParams = { 7 | campaign?: Campaign | string; 8 | sourceType?: T; 9 | isViewable?: boolean; 10 | datePublished?: string; // 'YYYY' or 'YYYY-mm' (e.g. '2025-06') 11 | sortBy?: MediaListSortBy; 12 | limit?: number; 13 | offset?: number; 14 | } & 15 | ( 16 | T extends 'post' ? { 17 | tiers?: Tier[] | string[]; 18 | } 19 | : T extends 'product' ? {} 20 | : never 21 | ); 22 | 23 | export interface MediaListItem { 24 | id: string; 25 | mediaType: 'image' | 'video' | 'audio' | 'pdf'; 26 | mimeType: string | null; 27 | thumbnail: { 28 | path: string; 29 | width: number | null; 30 | height: number | null; 31 | } | null; 32 | source: T extends 'post' ? Post : T extends 'product' ? Product : Post | Product; 33 | } 34 | 35 | export interface MediaList { 36 | items: MediaListItem[]; 37 | total: number; 38 | } -------------------------------------------------------------------------------- /src/browse/web/components/RewardCard.tsx: -------------------------------------------------------------------------------- 1 | import "../assets/styles/RewardCard.scss"; 2 | import { Card, Stack } from "react-bootstrap"; 3 | import { type Reward } from "../../../entities"; 4 | 5 | interface RewardCardProps { 6 | reward: Reward; 7 | } 8 | 9 | function RewardCard(props: RewardCardProps) { 10 | const { reward } = props; 11 | 12 | return ( 13 | 14 | 15 | { 16 | reward.image?.downloaded?.path ? ( 17 | 18 | ) : null 19 | } 20 | 21 | 22 |
{reward.title}
23 |
{reward.amount || ''}
24 |
25 |
26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default RewardCard; -------------------------------------------------------------------------------- /docs/api/interfaces/WebServerConfig.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / WebServerConfig 6 | 7 | # Interface: WebServerConfig 8 | 9 | Defined in: [src/browse/server/WebServer.ts:13](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L13) 10 | 11 | ## Properties 12 | 13 | ### dataDir? 14 | 15 | > `optional` **dataDir**: `string` 16 | 17 | Defined in: [src/browse/server/WebServer.ts:14](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L14) 18 | 19 | *** 20 | 21 | ### logger? 22 | 23 | > `optional` **logger**: `null` \| [`Logger`](../classes/Logger.md) 24 | 25 | Defined in: [src/browse/server/WebServer.ts:16](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L16) 26 | 27 | *** 28 | 29 | ### port? 30 | 31 | > `optional` **port**: `null` \| `number` 32 | 33 | Defined in: [src/browse/server/WebServer.ts:15](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L15) 34 | -------------------------------------------------------------------------------- /src/downloaders/task/DownloadTaskBatchEvent.ts: -------------------------------------------------------------------------------- 1 | import { type DownloadProgress, type DownloadTaskError, type IDownloadTask, type DownloadTaskSkipReason } from './DownloadTask.js'; 2 | 3 | export type DownloadTaskBatchEvent = 4 | 'taskStart' | 5 | 'taskProgress' | 6 | 'taskComplete' | 7 | 'taskError' | 8 | 'taskAbort' | 9 | 'taskSkip' | 10 | 'taskSpawn' | 11 | 'complete'; 12 | 13 | export interface DownloadTaskBatchEventPayload { 14 | 15 | 'taskStart': { 16 | task: IDownloadTask; 17 | }; 18 | 19 | 'taskProgress': { 20 | task: IDownloadTask; 21 | progress: DownloadProgress | null; 22 | }; 23 | 24 | 'taskComplete': { 25 | task: IDownloadTask; 26 | }; 27 | 28 | 'taskError': { 29 | error: DownloadTaskError; 30 | willRetry: boolean; 31 | }; 32 | 33 | 'taskAbort': { 34 | task: IDownloadTask; 35 | }; 36 | 37 | 'taskSkip': { 38 | task: IDownloadTask; 39 | reason: DownloadTaskSkipReason; 40 | }; 41 | 42 | 'taskSpawn': { 43 | origin: IDownloadTask; 44 | spawn: IDownloadTask; 45 | }; 46 | 47 | 'complete': {}; 48 | } 49 | 50 | export type DownloadTaskBatchEventPayloadOf = DownloadTaskBatchEventPayload[T]; 51 | -------------------------------------------------------------------------------- /docs/api/interfaces/PostDownloaderContext.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / PostDownloaderContext 6 | 7 | # Interface: PostDownloaderContext 8 | 9 | Defined in: [src/downloaders/PostDownloader.ts:24](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/PostDownloader.ts#L24) 10 | 11 | ## Properties 12 | 13 | ### keepDBOpen? 14 | 15 | > `optional` **keepDBOpen**: `boolean` 16 | 17 | Defined in: [src/downloaders/PostDownloader.ts:27](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/PostDownloader.ts#L27) 18 | 19 | *** 20 | 21 | ### keepLoggerOpen? 22 | 23 | > `optional` **keepLoggerOpen**: `boolean` 24 | 25 | Defined in: [src/downloaders/PostDownloader.ts:26](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/PostDownloader.ts#L26) 26 | 27 | *** 28 | 29 | ### skipSaveCampaign? 30 | 31 | > `optional` **skipSaveCampaign**: `boolean` 32 | 33 | Defined in: [src/downloaders/PostDownloader.ts:25](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/PostDownloader.ts#L25) 34 | -------------------------------------------------------------------------------- /docs/api/classes/Logger.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / Logger 6 | 7 | # Class: `abstract` Logger 8 | 9 | Defined in: [src/utils/logging/Logger.ts:9](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/Logger.ts#L9) 10 | 11 | ## Extended by 12 | 13 | - [`ChainLogger`](ChainLogger.md) 14 | - [`ConsoleLogger`](ConsoleLogger.md) 15 | 16 | ## Constructors 17 | 18 | ### Constructor 19 | 20 | > **new Logger**(): `Logger` 21 | 22 | #### Returns 23 | 24 | `Logger` 25 | 26 | ## Methods 27 | 28 | ### end() 29 | 30 | > **end**(): `Promise`\<`void`\> 31 | 32 | Defined in: [src/utils/logging/Logger.ts:12](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/Logger.ts#L12) 33 | 34 | #### Returns 35 | 36 | `Promise`\<`void`\> 37 | 38 | *** 39 | 40 | ### log() 41 | 42 | > `abstract` **log**(`entry`): `void` 43 | 44 | Defined in: [src/utils/logging/Logger.ts:10](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/Logger.ts#L10) 45 | 46 | #### Parameters 47 | 48 | ##### entry 49 | 50 | [`LogEntry`](../interfaces/LogEntry.md) 51 | 52 | #### Returns 53 | 54 | `void` 55 | -------------------------------------------------------------------------------- /src/browse/types/Filter.ts: -------------------------------------------------------------------------------- 1 | export interface FilterOption { 2 | title: string; 3 | isDefault?: boolean; 4 | value: string | null; 5 | } 6 | 7 | export type FilterSearchParams = string; 8 | 9 | export interface FilterSection { 10 | title?: string; 11 | searchParam: S; 12 | displayHint: 'pill' | 'pill_small' | 'list'; 13 | options: FilterOption[]; 14 | enableCondition?: { 15 | searchParam: S; 16 | condition: 'is' | 'not'; 17 | value: string; 18 | }; 19 | } 20 | 21 | export type PostFilterSearchParams = 22 | 'post_types' | 23 | 'is_viewable' | 24 | 'tier_ids' | 25 | 'sort_by' | 26 | 'date_published' | 27 | 'search' | 28 | 'tag_id'; 29 | 30 | export type ProductFilterSearchParams = 31 | 'is_viewable' | 32 | 'sort_by' | 33 | 'date_published' | 34 | 'search'; 35 | 36 | export type MediaFilterSearchParams = 37 | 'source_type' | 38 | 'is_viewable' | 39 | 'tier_ids' | 40 | 'sort_by' | 41 | 'date_published'; 42 | 43 | export interface FilterData { 44 | sections: FilterSection[]; 45 | external?: { 46 | searchParam: S; 47 | }[] 48 | }; 49 | 50 | export interface Filter { 51 | options: { 52 | searchParam: S; 53 | value: string | null; 54 | }[]; 55 | } -------------------------------------------------------------------------------- /src/browse/web/assets/styles/MediaGallery.scss: -------------------------------------------------------------------------------- 1 | .lg-video-poster { 2 | object-fit: contain; 3 | } 4 | 5 | .media-gallery { 6 | display: flex; 7 | flex-wrap: wrap; 8 | gap: var(--media-gallery-gap); 9 | 10 | &__thumbnail-wrapper { 11 | flex-grow: 1; 12 | width: auto; 13 | position: relative; 14 | height: var(--media-gallery-row-height); 15 | border: var(--media-gallery-thumbnail-border) solid var(--bs-border-color); 16 | border-radius: var(--bs-border-radius); 17 | overflow: hidden; 18 | cursor: pointer; 19 | 20 | &--video::before { 21 | content: '\e1c4'; 22 | font-family: 'Material Icons Outlined'; 23 | font-size: 4em; 24 | position: absolute; 25 | top: 50%; 26 | left: 50%; 27 | transform: translate(-50%, -50%); 28 | color: #fff; 29 | transition: opacity 150ms ease-in-out; 30 | opacity: 0.8; 31 | } 32 | 33 | &--video:hover::before { 34 | opacity: 1; 35 | } 36 | 37 | &--empty { 38 | width: var(--media-gallery-row-height); 39 | } 40 | } 41 | 42 | &__thumbnail { 43 | width: 100%; 44 | height: 100%; 45 | object-fit: cover; 46 | } 47 | 48 | &__source-link { 49 | color: inherit; 50 | 51 | &:hover { 52 | text-decoration: none !important; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/browse/web/contexts/MainContentScrollProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useCallback, useContext, useRef } from "react"; 2 | import CustomScrollbars from "../components/CustomScrollbars"; 3 | import type Scrollbars from "react-custom-scrollbars-4"; 4 | 5 | interface MainContentScrollProviderProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | interface MainContentScrollContextValue { 10 | scrollTo: (x: number, y: number) => void; 11 | } 12 | 13 | const MainContentScrollContext = createContext({} as MainContentScrollContextValue); 14 | 15 | function MainContentScrollProvider(props: MainContentScrollProviderProps) { 16 | const { children } = props; 17 | const scrollbarsRef = useRef(null); 18 | 19 | const scrollTo = useCallback((x: number, y: number) => { 20 | if (scrollbarsRef.current) { 21 | scrollbarsRef.current.scrollLeft(x); 22 | scrollbarsRef.current.scrollTop(y); 23 | } 24 | }, []); 25 | 26 | return ( 27 | 28 | 29 | {children} 30 | 31 | 32 | ); 33 | }; 34 | 35 | const useScroll = () => useContext(MainContentScrollContext); 36 | 37 | export { useScroll, MainContentScrollProvider as ScrollProvider }; -------------------------------------------------------------------------------- /src/utils/PackageInfo.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import ObjectHelper from './ObjectHelper.js'; 3 | 4 | export interface PackageInfo { 5 | name: string; 6 | version: string; 7 | description: string; 8 | author: string; 9 | repository: string; 10 | banner: string; 11 | } 12 | 13 | let info: PackageInfo | null = null; 14 | 15 | export function getPackageInfo() { 16 | if (info === null) { 17 | try { 18 | const packageURL = new URL('../../package.json', import.meta.url); 19 | const json = JSON.parse(fs.readFileSync(packageURL).toString()); 20 | info = { 21 | name: json.name || '', 22 | version: json.version || '', 23 | description: json.description || '', 24 | author: json.author || '', 25 | repository: ObjectHelper.getProperty(json, 'repository.url') || '', 26 | banner: json.name && json.version && json.description ? 27 | `${json.name} v${json.version} ${json.description}` : '' 28 | }; 29 | } 30 | catch (error) { 31 | console.error('Failed to read package.json:', error instanceof Error ? error.message : error); 32 | info = { 33 | name: '', 34 | version: '', 35 | description: '', 36 | author: '', 37 | repository: '', 38 | banner: '' 39 | }; 40 | } 41 | } 42 | return info; 43 | } 44 | -------------------------------------------------------------------------------- /src/browse/web/components/CustomScrollbars.tsx: -------------------------------------------------------------------------------- 1 | import { type ForwardedRef, forwardRef } from "react"; 2 | import "../assets/styles/CustomScrollbars.scss"; 3 | import Scrollbars from "react-custom-scrollbars-4"; 4 | 5 | interface CustomScrollbarsProps { 6 | viewClassName?: string; 7 | children: React.ReactNode; 8 | } 9 | 10 | const CustomScrollbars = forwardRef((props: CustomScrollbarsProps, ref: ForwardedRef) => { 11 | const { viewClassName, children } = props; 12 | 13 | return ( 14 | ( 20 |
21 | )} 22 | renderTrackVertical={(props) => ( 23 |
24 | )} 25 | renderThumbHorizontal={(props) => ( 26 |
27 | )} 28 | renderThumbVertical={(props) => ( 29 |
30 | )} 31 | renderView={(props) =>
} 32 | > 33 | {children} 34 | 35 | ); 36 | }); 37 | 38 | export default CustomScrollbars; 39 | -------------------------------------------------------------------------------- /docs/api/classes/FetcherError.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / FetcherError 6 | 7 | # Class: FetcherError 8 | 9 | Defined in: [src/utils/Fetcher.ts:33](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Fetcher.ts#L33) 10 | 11 | ## Extends 12 | 13 | - `Error` 14 | 15 | ## Constructors 16 | 17 | ### Constructor 18 | 19 | > **new FetcherError**(`message`, `url`, `method`): `FetcherError` 20 | 21 | Defined in: [src/utils/Fetcher.ts:38](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Fetcher.ts#L38) 22 | 23 | #### Parameters 24 | 25 | ##### message 26 | 27 | `string` 28 | 29 | ##### url 30 | 31 | `string` 32 | 33 | ##### method 34 | 35 | `string` 36 | 37 | #### Returns 38 | 39 | `FetcherError` 40 | 41 | #### Overrides 42 | 43 | `Error.constructor` 44 | 45 | ## Properties 46 | 47 | ### method 48 | 49 | > **method**: `string` 50 | 51 | Defined in: [src/utils/Fetcher.ts:36](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Fetcher.ts#L36) 52 | 53 | *** 54 | 55 | ### url 56 | 57 | > **url**: `string` 58 | 59 | Defined in: [src/utils/Fetcher.ts:35](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/Fetcher.ts#L35) 60 | -------------------------------------------------------------------------------- /src/browse/web/pages/ProductContent.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useParams } from "react-router"; 3 | import { Container, Row, Col } from "react-bootstrap"; 4 | import { useAPI } from "../contexts/APIProvider"; 5 | import { type Product } from "../../../entities"; 6 | import ProductCard from "../components/ProductCard"; 7 | 8 | function ProductContent() { 9 | const {id: productId} = useParams(); 10 | const { api } = useAPI(); 11 | const [product, setContent] = useState(null); 12 | 13 | useEffect(() => { 14 | if (!productId) { 15 | return; 16 | } 17 | const abortController = new AbortController(); 18 | void (async () => { 19 | const product = await api.getProduct(productId); 20 | if (!abortController.signal.aborted) { 21 | setContent(product); 22 | } 23 | })(); 24 | 25 | return () => abortController.abort(); 26 | }, [api, productId]); 27 | 28 | if (!product) { 29 | return null; 30 | } 31 | 32 | return ( 33 | 34 | 35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 | ) 43 | } 44 | 45 | export default ProductContent; -------------------------------------------------------------------------------- /src/entities/Product.ts: -------------------------------------------------------------------------------- 1 | import { type Campaign } from './Campaign.js'; 2 | import { type List } from './List.js'; 3 | import { type Downloadable } from './Downloadable.js'; 4 | 5 | export type ProductList = List; 6 | 7 | // Known productType values 8 | export const ProductType = { 9 | DigitalCommerce: 'digital_commerce', 10 | Post: 'post', 11 | Collection: 'collection' 12 | } as const; 13 | 14 | export interface Product { 15 | type: 'product'; 16 | id: string; 17 | /** 18 | * If `undefined`, assume `ProductType.DigitalCommerce`. 19 | * 20 | * @since 3.5.0 21 | */ 22 | productType?: string; 23 | /** 24 | * ID of entity referenced according to `productType` (post or collection) 25 | * 26 | * @since 3.5.0 27 | */ 28 | referencedEntityId?: string; 29 | isAccessible: boolean; 30 | name: string | null; 31 | description: string | null; 32 | /** 33 | * `description` converted to plain text. 34 | * Used by FTS. 35 | * @since 3.5.0 36 | */ 37 | descriptionText?: string | null; 38 | price: string | null; 39 | publishedAt: string | null; 40 | // The URL depends on `productType`: 41 | // `ProductType.Post`: post URL. 42 | // `ProductType.Collection`: collection URL. 43 | // `ProductType.DigitalCommerce`: product URL. 44 | url: string; 45 | previewMedia: Downloadable[]; 46 | contentMedia: Downloadable[]; 47 | campaign: Campaign | null; 48 | raw: object; 49 | } 50 | -------------------------------------------------------------------------------- /src/browse/web/contexts/GlobalModalsProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useCallback, useContext, useState } from "react"; 2 | import BrowseSettingsModal from "../components/BrowseSettingsModal"; 3 | 4 | interface GlobalModalsProviderProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | interface GlobalModalsContextValue { 9 | showBrowseSettingsModal: () => void; 10 | closeBrowseSettingsModal: () => void; 11 | } 12 | 13 | const GlobalModalsContext = createContext({} as GlobalModalsContextValue); 14 | 15 | function GlobalModalsProvider(props: GlobalModalsProviderProps) { 16 | const { children } = props; 17 | const [ browseSettingsModalVisible, setBrowseSettingsModalVisible ] = useState(false); 18 | 19 | const showBrowseSettingsModal = useCallback(() => { 20 | setBrowseSettingsModalVisible(true); 21 | }, []); 22 | 23 | const closeBrowseSettingsModal = useCallback(() => { 24 | setBrowseSettingsModalVisible(false); 25 | }, []) 26 | 27 | return ( 28 | 34 | {children} 35 | 39 | 40 | ); 41 | }; 42 | 43 | const useGlobalModals = () => useContext(GlobalModalsContext); 44 | 45 | export { useGlobalModals, GlobalModalsProvider }; -------------------------------------------------------------------------------- /src/downloaders/templates/CommentInfo.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from 'os'; 2 | import { type Comment, type CommentReply } from '../../entities/Comment.js'; 3 | 4 | const POST_COMMENT_TEMPLATE = 5 | ` 6 | {comment.commenter} {comment.date} 7 | {---} 8 | {comment.body} 9 | 10 | `; 11 | 12 | export function generatePostCommentsSummary(comments: Comment[] | CommentReply[], indent = '') { 13 | let result = ''; 14 | for (const comment of comments) { 15 | const snippet = POST_COMMENT_TEMPLATE 16 | .replaceAll('{comment.commenter}', comment.commenter?.fullName || '') 17 | .replaceAll('{comment.date}', comment.createdAt || '') 18 | .replaceAll('{comment.body}', comment.body); 19 | 20 | const lines = snippet.split(EOL); 21 | const formatted = lines.map((l, i) => { 22 | let fl = l; 23 | if (l.includes('{---}') && i > 0) { 24 | const previousLine = lines[i - 1]; 25 | if (previousLine) { 26 | const sep = '-'.repeat(previousLine.length); 27 | fl = l.replaceAll('{---}', sep); 28 | } 29 | } 30 | return `${indent}${fl}`; 31 | }) 32 | .join(EOL); 33 | 34 | result += formatted; 35 | 36 | if (isComment(comment) && comment.replies.length > 0) { 37 | result += generatePostCommentsSummary(comment.replies, ' '); 38 | } 39 | } 40 | return result; 41 | } 42 | 43 | function isComment(data: Comment | CommentReply): data is Comment { 44 | return Reflect.has(data, 'replies'); 45 | } 46 | -------------------------------------------------------------------------------- /src/browse/web/components/Theme.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useReducer } from "react"; 2 | import { useBrowseSettings } from "../contexts/BrowseSettingsProvider"; 3 | 4 | const stylesheetsReducer = (currentStylesheets: string[] | null, stylesheets: string[] | null) => { 5 | if (currentStylesheets && stylesheets) { 6 | const isEqual = JSON.stringify(currentStylesheets.sort()) === JSON.stringify(stylesheets.sort()); 7 | return isEqual ? currentStylesheets : stylesheets; 8 | } 9 | return stylesheets; 10 | } 11 | 12 | function Theme() { 13 | const { settings, options } = useBrowseSettings(); 14 | const [ stylesheets, setStylesheets ] = useReducer(stylesheetsReducer, null); 15 | 16 | useEffect(() => { 17 | const stylesheets = options.themes.find( 18 | (theme) => theme.value === settings.theme)?.stylesheets || null; 19 | setStylesheets(stylesheets); 20 | }, [settings, options ]); 21 | 22 | useEffect(() => { 23 | if (!stylesheets || stylesheets.length === 0) { 24 | return; 25 | } 26 | const links = document.querySelectorAll('link[id^="theme-stylesheet-"]'); 27 | links.forEach((link) => link.remove()); 28 | stylesheets.forEach((sheet, i) => { 29 | const link = document.createElement("link"); 30 | link.id = `theme-stylesheet-${i}`; 31 | link.rel = "stylesheet"; 32 | link.href = sheet; 33 | document.head.prepend(link); 34 | }) 35 | }, [stylesheets]); 36 | 37 | return null; 38 | } 39 | 40 | export default Theme; 41 | -------------------------------------------------------------------------------- /src/browse/server/handler/CampaignAPIRequesthandler.ts: -------------------------------------------------------------------------------- 1 | import { type Request, type Response } from 'express'; 2 | import { type Logger } from '../../../utils/logging'; 3 | import { type APIInstance } from '../../api'; 4 | import Basehandler from './BaseHandler.js'; 5 | import { type CampaignListSortBy } from '../../types/Campaign.js'; 6 | 7 | const DEFAULT_ITEMS_PER_PAGE = 20; 8 | 9 | export default class CampaignAPIRequestHandler extends Basehandler { 10 | name = 'CampaignAPIRequestHandler'; 11 | 12 | #api: APIInstance; 13 | 14 | constructor(api: APIInstance, logger?: Logger | null) { 15 | super(logger); 16 | this.#api = api; 17 | } 18 | 19 | handleListRequest(req: Request, res: Response) { 20 | const { limit, offset } = this.getPaginationParams(req, DEFAULT_ITEMS_PER_PAGE); 21 | const sortBy = this.getQueryParamValue( 22 | req, 23 | 'sort_by', 24 | ['a-z', 'z-a', 'most_content', 'most_media', 'last_downloaded'], 25 | 'a-z' 26 | ); 27 | const list = this.#api.getCampaignList({ 28 | sortBy, 29 | limit, 30 | offset 31 | }); 32 | res.json(list); 33 | } 34 | 35 | handleGetRequest(req: Request, res: Response, id: string) { 36 | const withCounts = req.query['with_counts'] ? this.getQueryParamValue<'true' | 'false'>( 37 | req, 38 | 'with_counts', 39 | ['true', 'false'] 40 | ) === 'true' ? true : false : undefined; 41 | res.json(this.#api.getCampaign({id, withCounts})); 42 | } 43 | } -------------------------------------------------------------------------------- /src/browse/server/handler/BaseHandler.ts: -------------------------------------------------------------------------------- 1 | import { type Request } from "express"; 2 | import { type UnionToTuple } from "../../../utils/Misc.js"; 3 | import {type LogLevel} from "../../../utils/logging/Logger.js"; 4 | import type Logger from "../../../utils/logging/Logger.js"; 5 | import { commonLog } from "../../../utils/logging/Logger.js"; 6 | import WebUtils from "../../../utils/WebUtils.js"; 7 | 8 | export default abstract class Basehandler { 9 | 10 | abstract name: string; 11 | #logger?: Logger | null; 12 | 13 | constructor(logger?: Logger | null) { 14 | this.#logger = logger; 15 | } 16 | 17 | getPaginationParams(req: Request, defaultItemsPerPage: number) { 18 | const url = `${req.protocol}://${req.get("host")}${req.originalUrl}`; 19 | return WebUtils.getPaginationParams(url, defaultItemsPerPage); 20 | } 21 | 22 | getQueryParamValue( 23 | req: Request, 24 | param: string, 25 | allowedValues: UnionToTuple, 26 | defaultValue?: T 27 | ): T { 28 | const value = req.query[param]; 29 | if (!value) { 30 | if (!defaultValue) { 31 | throw Error(`Invalid params: "${param}" missing`); 32 | } 33 | return defaultValue; 34 | } 35 | if (!allowedValues.includes(value)) { 36 | throw Error(`Invalid value "${value as string}" for param "${param}"`); 37 | } 38 | return value as T; 39 | } 40 | 41 | protected log(level: LogLevel, ...msg: any[]) { 42 | commonLog(this.#logger, level, this.name, ...msg); 43 | } 44 | } -------------------------------------------------------------------------------- /docs/api/interfaces/DownloaderFileLoggerOptions.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloaderFileLoggerOptions 6 | 7 | # Interface: DownloaderFileLoggerOptions 8 | 9 | Defined in: [src/utils/logging/FileLogger.ts:24](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L24) 10 | 11 | ## Properties 12 | 13 | ### fileExistsAction? 14 | 15 | > `optional` **fileExistsAction**: `"append"` \| `"overwrite"` 16 | 17 | Defined in: [src/utils/logging/FileLogger.ts:28](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L28) 18 | 19 | *** 20 | 21 | ### init 22 | 23 | > **init**: [`DownloaderFileLoggerInit`](DownloaderFileLoggerInit.md) 24 | 25 | Defined in: [src/utils/logging/FileLogger.ts:25](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L25) 26 | 27 | *** 28 | 29 | ### logDir? 30 | 31 | > `optional` **logDir**: `string` 32 | 33 | Defined in: [src/utils/logging/FileLogger.ts:26](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L26) 34 | 35 | *** 36 | 37 | ### logFilename? 38 | 39 | > `optional` **logFilename**: `string` 40 | 41 | Defined in: [src/utils/logging/FileLogger.ts:27](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/FileLogger.ts#L27) 42 | -------------------------------------------------------------------------------- /docs/api/interfaces/ProductDownloaderBootstrapData.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / ProductDownloaderBootstrapData 6 | 7 | # Interface: ProductDownloaderBootstrapData 8 | 9 | Defined in: [src/downloaders/Bootstrap.ts:12](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L12) 10 | 11 | ## Extends 12 | 13 | - [`BootstrapData`](BootstrapData.md) 14 | 15 | ## Properties 16 | 17 | ### productFetch 18 | 19 | > **productFetch**: \{ `productId`: `string`; `type`: `"single"`; \} \| \{ `campaignId?`: `string`; `type`: `"byShop"`; `vanity`: `string`; \} 20 | 21 | Defined in: [src/downloaders/Bootstrap.ts:14](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L14) 22 | 23 | *** 24 | 25 | ### targetURL 26 | 27 | > **targetURL**: `string` 28 | 29 | Defined in: [src/downloaders/Bootstrap.ts:9](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L9) 30 | 31 | #### Inherited from 32 | 33 | [`BootstrapData`](BootstrapData.md).[`targetURL`](BootstrapData.md#targeturl) 34 | 35 | *** 36 | 37 | ### type 38 | 39 | > **type**: `"product"` 40 | 41 | Defined in: [src/downloaders/Bootstrap.ts:13](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L13) 42 | 43 | #### Overrides 44 | 45 | [`BootstrapData`](BootstrapData.md).[`type`](BootstrapData.md#type) 46 | -------------------------------------------------------------------------------- /docs/api/interfaces/Downloaded.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / Downloaded 6 | 7 | # Interface: Downloaded 8 | 9 | Defined in: [src/entities/Downloadable.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Downloadable.ts#L4) 10 | 11 | ## Properties 12 | 13 | ### mimeType? 14 | 15 | > `optional` **mimeType**: `null` \| `string` 16 | 17 | Defined in: [src/entities/Downloadable.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Downloadable.ts#L5) 18 | 19 | *** 20 | 21 | ### path? 22 | 23 | > `optional` **path**: `null` \| `string` 24 | 25 | Defined in: [src/entities/Downloadable.ts:9](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Downloadable.ts#L9) 26 | 27 | Path of downloaded file, relative to out directory. 28 | 29 | *** 30 | 31 | ### thumbnail? 32 | 33 | > `optional` **thumbnail**: `object` 34 | 35 | Defined in: [src/entities/Downloadable.ts:10](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Downloadable.ts#L10) 36 | 37 | #### height? 38 | 39 | > `optional` **height**: `null` \| `number` 40 | 41 | #### mimeType? 42 | 43 | > `optional` **mimeType**: `null` \| `string` 44 | 45 | #### path? 46 | 47 | > `optional` **path**: `null` \| `string` 48 | 49 | Path of downloaded thumbnail, relative to out directory. 50 | 51 | #### width? 52 | 53 | > `optional` **width**: `null` \| `number` 54 | -------------------------------------------------------------------------------- /src/browse/web/components/CollectionBanner.tsx: -------------------------------------------------------------------------------- 1 | import "../assets/styles/CollectionBanner.scss"; 2 | import { type Collection } from "../../../entities/Post"; 3 | import { Card, Stack } from "react-bootstrap"; 4 | import MediaImage from "./MediaImage"; 5 | import FadeContent from "./FadeContent"; 6 | 7 | interface CollectionBannerProps { 8 | collection: Collection; 9 | } 10 | 11 | function CollectionBanner({ collection }: CollectionBannerProps) { 12 | 13 | const thumbnailId = collection.thumbnail?.downloaded?.path ? collection.thumbnail.id : null; 14 | const nodesc = !collection.description; 15 | 16 | return ( 17 | 18 | 19 | 20 | {thumbnailId && ( 21 |
22 | 23 |
24 | )} 25 | 26 |
27 | {collection.title} 28 |
29 | { 30 | collection.description && ( 31 |
32 | {collection.description} 33 |
34 | ) 35 | } 36 |
37 |
38 |
39 |
40 | ); 41 | } 42 | 43 | export default CollectionBanner; -------------------------------------------------------------------------------- /src/browse/web/pages/CampaignHome.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useAPI } from "../contexts/APIProvider"; 3 | import { useNavigate, useParams } from "react-router"; 4 | import { type CampaignWithCounts } from "../../types/Campaign"; 5 | 6 | function CampaignHome() { 7 | const { id: campaignId } = useParams(); 8 | const navigate = useNavigate(); 9 | const { api } = useAPI(); 10 | const [campaign, setCampaign] = useState(null); 11 | 12 | useEffect(() => { 13 | if (!campaignId) { 14 | return; 15 | } 16 | const abortController = new AbortController(); 17 | void (async () => { 18 | const campaign = await api.getCampaign({ id: campaignId, withCounts: true }); 19 | if (!abortController.signal.aborted) { 20 | setCampaign(campaign); 21 | } 22 | })(); 23 | 24 | return () => abortController.abort(); 25 | }, [api, campaignId]); 26 | 27 | useEffect(() => { 28 | if (!campaign) { 29 | return; 30 | } 31 | void (async () => { 32 | let p = ''; 33 | if (campaign.postCount > 0) { 34 | p = `posts`; 35 | } 36 | else if (campaign.productCount > 0) { 37 | p = 'shop'; 38 | } 39 | else if (campaign.mediaCount > 0) { 40 | p = 'media'; 41 | } 42 | else { 43 | p = 'about'; 44 | } 45 | await navigate(`/campaigns/${campaign.id}/${p}`, { replace: true }); 46 | return; 47 | })(); 48 | }, [api, campaign, navigate]); 49 | 50 | return null; 51 | } 52 | 53 | export default CampaignHome; 54 | -------------------------------------------------------------------------------- /src/browse/web/layouts/CampaignLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Row, Col } from "react-bootstrap"; 2 | import { Outlet, useParams } from "react-router"; 3 | import { useAPI } from "../contexts/APIProvider"; 4 | import { useEffect, useState } from "react"; 5 | import CampaignHeader from "../components/CampaignHeader"; 6 | import { type CampaignWithCounts } from "../../types/Campaign"; 7 | 8 | function CampaignLayout() { 9 | const { id: campaignId } = useParams(); 10 | 11 | if (!campaignId) { 12 | return null; 13 | } 14 | const { api } = useAPI(); 15 | const [campaign, setCampaign] = useState(null); 16 | 17 | useEffect(() => { 18 | const abortController = new AbortController(); 19 | void (async () => { 20 | const campaign = await api.getCampaign({ id: campaignId, withCounts: true }); 21 | if (!abortController.signal.aborted) { 22 | setCampaign(campaign); 23 | }; 24 | })(); 25 | 26 | return () => abortController.abort(); 27 | }, [api, campaignId]); 28 | 29 | if (!campaign) { 30 | return null; 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ) 47 | } 48 | 49 | export default CampaignLayout; -------------------------------------------------------------------------------- /src/entities/Downloadable.ts: -------------------------------------------------------------------------------- 1 | import { type DefaultImageMediaItem, type SingleImageMediaItem, type VideoMediaItem, type MediaItem } from './MediaItem.js'; 2 | import { type PostEmbed, type YouTubePostEmbed } from './Post.js'; 3 | 4 | export interface Downloaded { 5 | mimeType?: string | null; 6 | /** 7 | * Path of downloaded file, relative to out directory. 8 | */ 9 | path?: string | null; 10 | thumbnail?: { 11 | /** 12 | * Path of downloaded thumbnail, relative to out directory. 13 | */ 14 | path?: string | null; 15 | mimeType?: string | null; 16 | width?: number | null; 17 | height?: number | null; 18 | } 19 | } 20 | 21 | export type Downloadable = T & { 22 | downloaded?: Downloaded 23 | }; 24 | 25 | export type DownloadableWithThumbnail = 26 | SingleImageMediaItem | 27 | DefaultImageMediaItem | 28 | VideoMediaItem; 29 | 30 | export function isDownloadableWithThumbnail(item: Downloadable): item is DownloadableWithThumbnail & { thumbnailURL: string; } { 31 | return ((item.type === 'image' && (item.imageType === 'single' || item.imageType === 'default')) || 32 | (item.type === 'video') || 33 | (item.type === 'audio') || 34 | isEmbed(item)) && !!item.thumbnailURL; 35 | } 36 | 37 | 38 | export function isYouTubeEmbed(embed: Downloadable): embed is Downloadable { 39 | return embed.provider === 'YouTube'; 40 | } 41 | 42 | export function isEmbed(data: Downloadable): data is Downloadable { 43 | return data.type === 'videoEmbed' || data.type === 'linkEmbed' || data.type === 'unknownEmbed'; 44 | } 45 | -------------------------------------------------------------------------------- /docs/api/classes/DownloadTaskError.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloadTaskError 6 | 7 | # Class: DownloadTaskError 8 | 9 | Defined in: [src/downloaders/task/DownloadTask.ts:13](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L13) 10 | 11 | ## Extends 12 | 13 | - `Error` 14 | 15 | ## Constructors 16 | 17 | ### Constructor 18 | 19 | > **new DownloadTaskError**(`message`, `task`, `cause?`): `DownloadTaskError` 20 | 21 | Defined in: [src/downloaders/task/DownloadTask.ts:17](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L17) 22 | 23 | #### Parameters 24 | 25 | ##### message 26 | 27 | `string` 28 | 29 | ##### task 30 | 31 | [`IDownloadTask`](../interfaces/IDownloadTask.md) 32 | 33 | ##### cause? 34 | 35 | `Error` 36 | 37 | #### Returns 38 | 39 | `DownloadTaskError` 40 | 41 | #### Overrides 42 | 43 | `Error.constructor` 44 | 45 | ## Properties 46 | 47 | ### cause? 48 | 49 | > `optional` **cause**: `Error` 50 | 51 | Defined in: [src/downloaders/task/DownloadTask.ts:15](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L15) 52 | 53 | #### Overrides 54 | 55 | `Error.cause` 56 | 57 | *** 58 | 59 | ### task 60 | 61 | > **task**: [`IDownloadTask`](../interfaces/IDownloadTask.md) 62 | 63 | Defined in: [src/downloaders/task/DownloadTask.ts:14](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L14) 64 | -------------------------------------------------------------------------------- /docs/api/type-aliases/LinkedAttachment.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / LinkedAttachment 6 | 7 | # Type Alias: LinkedAttachment 8 | 9 | > **LinkedAttachment** = `object` 10 | 11 | Defined in: [src/entities/Post.ts:142](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L142) 12 | 13 | ## Properties 14 | 15 | ### downloadable? 16 | 17 | > `optional` **downloadable**: [`Downloadable`](Downloadable.md)\<[`AttachmentMediaItem`](../interfaces/AttachmentMediaItem.md)\> 18 | 19 | Defined in: [src/entities/Post.ts:147](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L147) 20 | 21 | *** 22 | 23 | ### mediaId 24 | 25 | > **mediaId**: `string` 26 | 27 | Defined in: [src/entities/Post.ts:146](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L146) 28 | 29 | *** 30 | 31 | ### postId 32 | 33 | > **postId**: `string` 34 | 35 | Defined in: [src/entities/Post.ts:145](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L145) 36 | 37 | *** 38 | 39 | ### type 40 | 41 | > **type**: `"linkedAttachment"` 42 | 43 | Defined in: [src/entities/Post.ts:143](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L143) 44 | 45 | *** 46 | 47 | ### url 48 | 49 | > **url**: `string` 50 | 51 | Defined in: [src/entities/Post.ts:144](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L144) 52 | -------------------------------------------------------------------------------- /src/utils/Sleeper.ts: -------------------------------------------------------------------------------- 1 | export default class Sleeper { 2 | 3 | #timeout: NodeJS.Timeout | null; 4 | #resolve: (() => void) | null; 5 | #signal?: AbortSignal; 6 | #abortHandler: (() => void) | null; 7 | 8 | #startParams: { ms: number; signal?: AbortSignal }; 9 | 10 | constructor() { 11 | this.#timeout = null; 12 | this.#resolve = null; 13 | this.#abortHandler = null; 14 | } 15 | 16 | static getInstance(ms: number, signal?: AbortSignal) { 17 | const sleeper = new Sleeper(); 18 | sleeper.#startParams = { ms, signal }; 19 | return sleeper; 20 | } 21 | 22 | start() { 23 | const { ms, signal } = this.#startParams; 24 | return new Promise((resolve, reject) => { 25 | if (signal) { 26 | this.#signal = signal; 27 | this.#abortHandler = () => reject(new Error('Aborted')); 28 | signal.addEventListener('abort', this.#abortHandler, { once: true }); 29 | } 30 | this.#resolve = resolve; 31 | this.#timeout = setTimeout(() => this.wake(), ms); 32 | }) 33 | .finally(() => { 34 | this.#clear(); 35 | }); 36 | } 37 | 38 | destroy() { 39 | this.#clear(); 40 | } 41 | 42 | #clear(resolve = false) { 43 | if (this.#timeout) { 44 | clearTimeout(this.#timeout); 45 | this.#timeout = null; 46 | } 47 | if (this.#signal && this.#abortHandler) { 48 | this.#signal.removeEventListener('abort', this.#abortHandler); 49 | this.#abortHandler = null; 50 | this.#signal = undefined; 51 | } 52 | if (this.#resolve && resolve) { 53 | this.#resolve(); 54 | } 55 | this.#resolve = null; 56 | } 57 | 58 | wake() { 59 | this.#clear(true); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docs/api/enumerations/TargetSkipReason.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / TargetSkipReason 6 | 7 | # Enumeration: TargetSkipReason 8 | 9 | Defined in: [src/downloaders/DownloaderEvent.ts:15](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderEvent.ts#L15) 10 | 11 | ## Enumeration Members 12 | 13 | ### AlreadyDownloaded 14 | 15 | > **AlreadyDownloaded**: `1` 16 | 17 | Defined in: [src/downloaders/DownloaderEvent.ts:17](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderEvent.ts#L17) 18 | 19 | *** 20 | 21 | ### Inaccessible 22 | 23 | > **Inaccessible**: `0` 24 | 25 | Defined in: [src/downloaders/DownloaderEvent.ts:16](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderEvent.ts#L16) 26 | 27 | *** 28 | 29 | ### NotInTier 30 | 31 | > **NotInTier**: `3` 32 | 33 | Defined in: [src/downloaders/DownloaderEvent.ts:19](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderEvent.ts#L19) 34 | 35 | *** 36 | 37 | ### PublishDateOutOfRange 38 | 39 | > **PublishDateOutOfRange**: `4` 40 | 41 | Defined in: [src/downloaders/DownloaderEvent.ts:20](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderEvent.ts#L20) 42 | 43 | *** 44 | 45 | ### UnmetMediaTypeCriteria 46 | 47 | > **UnmetMediaTypeCriteria**: `2` 48 | 49 | Defined in: [src/downloaders/DownloaderEvent.ts:18](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/DownloaderEvent.ts#L18) 50 | -------------------------------------------------------------------------------- /src/browse/web/components/CommentsPanel.tsx: -------------------------------------------------------------------------------- 1 | import { type Comment, type CommentReply } from "../../../entities/Comment"; 2 | import { Stack, Badge } from "react-bootstrap"; 3 | 4 | interface CommentRowProps { 5 | comment: Comment | CommentReply; 6 | } 7 | 8 | function hasReplies(comment: Comment | CommentReply): comment is Comment { 9 | return Reflect.has(comment, 'replies'); 10 | } 11 | 12 | function CommentRow(props: CommentRowProps) { 13 | const { comment } = props; 14 | return ( 15 | 16 | 17 |
18 | {comment.commenter?.fullName || ''} 19 | {comment.isByCreator ? CREATOR : null} 20 |
21 | 22 | {comment.createdAt ? new Date(comment.createdAt).toLocaleString() : ''} 23 | 24 |
25 |
26 | {comment.body} 27 |
28 | { 29 | hasReplies(comment) && comment.replies.length > 0 ? ( 30 |
31 | 32 |
33 | ) 34 | : null 35 | } 36 |
37 | ) 38 | } 39 | 40 | function CommentsPanel(props: { comments: (Comment | CommentReply)[] }) { 41 | const { comments } = props; 42 | if (comments.length === 0) { 43 | return null; 44 | } 45 | const rows = comments.map((comment) => ); 46 | return ( 47 | 48 | {rows} 49 | 50 | ) 51 | } 52 | 53 | export default CommentsPanel; -------------------------------------------------------------------------------- /src/downloaders/templates/PostInfo.ts: -------------------------------------------------------------------------------- 1 | import { type Post, type PostEmbed } from '../../entities/Post.js'; 2 | 3 | const POST_INFO_TEMPLATE = 4 | `Post 5 | -------- 6 | ID: {post.id} 7 | Type: {post.type} 8 | Title: {post.title} 9 | Teaser: {post.teaser} 10 | Content: {post.content} 11 | Published: {post.publishedAt} 12 | Last Edited: {post.editedAt} 13 | URL: {post.url} 14 | `; 15 | 16 | const POST_EMBED_TEMPLATE = 17 | `Embed 18 | -------- 19 | Type: {embed.type} 20 | Description: {embed.description} 21 | HTML: {embed.html} 22 | Provider: {embed.provider} 23 | provider URL: {embed.providerURL} 24 | Subject: {embed.subject} 25 | URL: {embed.url} 26 | `; 27 | 28 | export function generatePostSummary(post: Post) { 29 | const postInfo = POST_INFO_TEMPLATE 30 | .replaceAll('{post.id}', post.id) 31 | .replaceAll('{post.type}', post.postType) 32 | .replaceAll('{post.title}', post.title || '') 33 | .replaceAll('{post.teaser}', post.teaserText || '') 34 | .replaceAll('{post.content}', post.content || '') 35 | .replaceAll('{post.publishedAt}', post.publishedAt || '') 36 | .replaceAll('{post.editedAt}', post.editedAt || '') 37 | .replaceAll('{post.url}', post.url || ''); 38 | 39 | return postInfo; 40 | } 41 | 42 | export function generatePostEmbedSummary(embed: PostEmbed) { 43 | const embedInfo = POST_EMBED_TEMPLATE 44 | .replaceAll('{embed.type}', embed.type) 45 | .replaceAll('{embed.description}', embed.description || '') 46 | .replaceAll('{embed.html}', embed.html || '') 47 | .replaceAll('{embed.provider}', embed.provider || '') 48 | .replaceAll('{embed.providerURL}', embed.providerURL || '') 49 | .replaceAll('{embed.subject}', embed.subject || '') 50 | .replaceAll('{embed.url}', embed.url || ''); 51 | 52 | return embedInfo; 53 | } 54 | -------------------------------------------------------------------------------- /docs/api/interfaces/PostDownloaderBootstrapData.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / PostDownloaderBootstrapData 6 | 7 | # Interface: PostDownloaderBootstrapData 8 | 9 | Defined in: [src/downloaders/Bootstrap.ts:24](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L24) 10 | 11 | ## Extends 12 | 13 | - [`BootstrapData`](BootstrapData.md) 14 | 15 | ## Properties 16 | 17 | ### postFetch 18 | 19 | > **postFetch**: \{ `postId`: `string`; `type`: `"single"`; \} \| \{ `campaignId?`: `string`; `filters?`: `Record`\<`string`, `any`\>; `type`: `"byUser"`; `vanity`: `string`; \} \| \{ `campaignId?`: `string`; `filters?`: `Record`\<`string`, `any`\>; `type`: `"byUserId"`; `userId`: `string`; \} \| \{ `campaignId?`: `string`; `collectionId`: `string`; `filters?`: `Record`\<`string`, `any`\>; `type`: `"byCollection"`; \} 20 | 21 | Defined in: [src/downloaders/Bootstrap.ts:26](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L26) 22 | 23 | *** 24 | 25 | ### targetURL 26 | 27 | > **targetURL**: `string` 28 | 29 | Defined in: [src/downloaders/Bootstrap.ts:9](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L9) 30 | 31 | #### Inherited from 32 | 33 | [`BootstrapData`](BootstrapData.md).[`targetURL`](BootstrapData.md#targeturl) 34 | 35 | *** 36 | 37 | ### type 38 | 39 | > **type**: `"post"` 40 | 41 | Defined in: [src/downloaders/Bootstrap.ts:25](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/Bootstrap.ts#L25) 42 | 43 | #### Overrides 44 | 45 | [`BootstrapData`](BootstrapData.md).[`type`](BootstrapData.md#type) 46 | -------------------------------------------------------------------------------- /src/downloaders/DownloaderEvent.ts: -------------------------------------------------------------------------------- 1 | import { type Campaign } from '../entities/Campaign.js'; 2 | import { type Collection, type Post } from '../entities/Post.js'; 3 | import { type Product } from '../entities/Product.js'; 4 | import { type IDownloadTaskBatch } from './task/DownloadTaskBatch.js'; 5 | 6 | export type DownloaderEvent = 7 | 'fetchBegin' | 8 | 'targetBegin' | 9 | 'targetEnd' | 10 | 'phaseBegin' | 11 | 'phaseEnd' | 12 | 'end' 13 | ; 14 | 15 | export enum TargetSkipReason { 16 | Inaccessible = 0, 17 | AlreadyDownloaded = 1, 18 | UnmetMediaTypeCriteria = 2, 19 | NotInTier = 3, 20 | PublishDateOutOfRange = 4 21 | } 22 | 23 | export interface DownloaderEventPayload { 24 | 25 | 'fetchBegin': { 26 | targetType: 'product' | 'products' | 'post' | 'posts'; 27 | } 28 | 29 | 'targetBegin': { 30 | target: Campaign | Product | Post | Collection; 31 | }; 32 | 33 | 'targetEnd': { 34 | target: Campaign | Product | Post | Collection; 35 | } & ({ 36 | isSkipped: false; 37 | } | { 38 | isSkipped: true; 39 | skipReason: TargetSkipReason; 40 | skipMessage: string; 41 | }); 42 | 43 | 'phaseBegin': { 44 | target: Campaign | Product | Post | Collection; 45 | } & ({ 46 | phase: 'saveInfo' | 'saveMedia'; 47 | } | { 48 | phase: 'batchDownload'; 49 | batch: IDownloadTaskBatch; 50 | }); 51 | 52 | 'phaseEnd': { 53 | target: Campaign | Product | Post | Collection; 54 | phase: 'saveInfo' | 'saveMedia' | 'batchDownload'; 55 | }; 56 | 57 | 'end': { 58 | aborted: true; 59 | error?: undefined; 60 | message: string; 61 | } | { 62 | aborted: false; 63 | error?: any; 64 | message: string; 65 | }; 66 | } 67 | 68 | export type DownloaderEventPayloadOf = DownloaderEventPayload[T]; 69 | -------------------------------------------------------------------------------- /src/browse/api/index.ts: -------------------------------------------------------------------------------- 1 | import _sanitizeHTML from 'sanitize-html'; 2 | import type Logger from '../../utils/logging/Logger.js'; 3 | import { commonLog, type LogLevel } from '../../utils/logging/Logger.js'; 4 | import { CampaignAPIMixin } from './CampaignAPIMixin.js'; 5 | import { type DBInstance } from '../db'; 6 | import { ContentAPIMixin } from './ContentAPIMixin.js'; 7 | import { SettingsAPIMixin } from './SettingsAPIMixin.js'; 8 | import { MediaAPIMixin } from './MediaAPIMixin.js'; 9 | import { FilterAPIMixin } from './FilterAPIMixin.js'; 10 | 11 | export type APIConstructor = new (...args: any[]) => APIBase; 12 | export type APIInstance = InstanceType; 13 | 14 | const SANITIZE_HTML_OPTIONS = { 15 | allowedTags: _sanitizeHTML.defaults.allowedTags.concat(['img']), 16 | allowedAttributes: { 17 | ..._sanitizeHTML.defaults.allowedAttributes, 18 | '*': ['class'] 19 | } 20 | }; 21 | 22 | export class APIBase { 23 | name = 'API'; 24 | 25 | db: DBInstance; 26 | logger?: Logger | null; 27 | 28 | constructor(db: DBInstance, logger?: Logger | null) { 29 | this.db = db; 30 | this.logger = logger; 31 | } 32 | 33 | static getInstance(db: DBInstance, logger?: Logger | null) { 34 | return new API(db, logger); 35 | } 36 | 37 | sanitizeHTML(html: string) { 38 | return _sanitizeHTML(html, SANITIZE_HTML_OPTIONS); 39 | } 40 | 41 | log(level: LogLevel, ...msg: any[]) { 42 | const limiterStopOnError = msg.find( 43 | (m) => m instanceof Error && m.message === 'LimiterStopOnError' 44 | ); 45 | if (limiterStopOnError) { 46 | return; 47 | } 48 | commonLog(this.logger, level, this.name, ...msg); 49 | } 50 | } 51 | 52 | const API = FilterAPIMixin(MediaAPIMixin(SettingsAPIMixin(ContentAPIMixin(CampaignAPIMixin(APIBase))))); 53 | 54 | export default API; 55 | -------------------------------------------------------------------------------- /src/browse/api/CampaignAPIMixin.ts: -------------------------------------------------------------------------------- 1 | import { type APIConstructor } from "."; 2 | import { type Campaign } from "../../entities/index.js"; 3 | import { type CampaignList, type CampaignListSortBy, type CampaignWithCounts, type GetCampaignListParams, type GetCampaignParams } from "../types/Campaign.js"; 4 | 5 | 6 | const DEFAULT_CAMPAIGN_LIST_SIZE = 10; 7 | const DEFAULT_CAMPAIGN_LIST_SORT_BY: CampaignListSortBy = 'a-z'; 8 | 9 | 10 | export function CampaignAPIMixin(Base: TBase) { 11 | return class CampaignAPI extends Base { 12 | getCampaignList(params: GetCampaignListParams): CampaignList { 13 | const { sortBy = DEFAULT_CAMPAIGN_LIST_SORT_BY, limit = DEFAULT_CAMPAIGN_LIST_SIZE, offset = 0 } = params; 14 | const list = this.db.getCampaignList({ 15 | sortBy, 16 | limit, 17 | offset 18 | }); 19 | for (const campaign of list.campaigns) { 20 | this.#sanitizeCampaign(campaign); 21 | } 22 | return list; 23 | } 24 | 25 | getCampaign(params: GetCampaignParams & { withCounts: true }): CampaignWithCounts | null; 26 | getCampaign(params: GetCampaignParams & { withCounts?: false }): Campaign | null; 27 | getCampaign(params: GetCampaignParams): Campaign | CampaignWithCounts | null; 28 | getCampaign(params: GetCampaignParams) { 29 | const campaign = this.db.getCampaign(params); 30 | if (campaign) { 31 | this.#sanitizeCampaign(campaign); 32 | } 33 | return campaign; 34 | } 35 | 36 | #sanitizeCampaign(campaign: Campaign) { 37 | if (campaign.summary) { 38 | campaign.summary = this.sanitizeHTML(campaign.summary); 39 | } 40 | for (const reward of campaign.rewards) { 41 | if (reward.description) { 42 | reward.description = this.sanitizeHTML(reward.description); 43 | } 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/browse/server/handler/SettingsAPIRequestHandler.ts: -------------------------------------------------------------------------------- 1 | import { type Request, type Response } from 'express'; 2 | import { type Logger } from '../../../utils/logging'; 3 | import { type APIInstance } from '../../api'; 4 | import Basehandler from './BaseHandler.js'; 5 | import { type BrowseSettings } from '../../types/Settings.js'; 6 | 7 | export default class SettingsAPIRequestHandler extends Basehandler { 8 | name = 'SettingsAPIRequestHandler'; 9 | 10 | #api: APIInstance; 11 | 12 | constructor(api: APIInstance, logger?: Logger | null) { 13 | super(logger); 14 | this.#api = api; 15 | } 16 | 17 | handleGetBrowseSettingsRequest(_req: Request, res: Response) { 18 | res.json(this.#api.getBrowseSettings()); 19 | } 20 | 21 | handleBrowseSettingOptionsRequest(_req: Request, res: Response) { 22 | res.json(this.#api.getBrowseSettingOptions()); 23 | } 24 | 25 | handleSaveBrowseSettingsRequest(req: Request, res: Response) { 26 | this.#api.saveBrowseSettings(this.#retrieveBrowseSettings(req)); 27 | res.sendStatus(200); 28 | } 29 | 30 | #retrieveBrowseSettings(req: Request) { 31 | const body = req.body; 32 | if (this.#isBrowseSettings(body)) { 33 | return body; 34 | } 35 | throw Error('Invalid browse settings data'); 36 | } 37 | 38 | #isBrowseSettings(data: any): data is BrowseSettings { 39 | if (typeof data !== 'object' || !data) { 40 | return false; 41 | } 42 | const hasValidThemeValue = Reflect.has(data, 'theme') && typeof data.theme === 'string'; 43 | const hasValidListItemsPerPageValue = Reflect.has(data, 'listItemsPerPage') && typeof data.listItemsPerPage === 'number'; 44 | const hasValidGalleryItemsPerPageValue = Reflect.has(data, 'galleryItemsPerPage') && typeof data.galleryItemsPerPage === 'number'; 45 | return hasValidThemeValue && hasValidListItemsPerPageValue && hasValidGalleryItemsPerPageValue; 46 | } 47 | } -------------------------------------------------------------------------------- /src/browse/web/components/FadeContent.tsx: -------------------------------------------------------------------------------- 1 | import '../assets/styles/FadeContent.scss'; 2 | import { useState, useRef, useEffect, useCallback } from 'react'; 3 | 4 | interface FadeContentProps { 5 | children: React.ReactNode; 6 | maxHeight?: number; 7 | } 8 | 9 | const FadeContent = ({ children, maxHeight = 480 }: FadeContentProps) => { 10 | const [expanded, setExpanded] = useState(false); 11 | const [showToggle, setShowToggle] = useState(false); 12 | const contentRef = useRef(null); 13 | 14 | useEffect(() => { 15 | if (contentRef.current) { 16 | const isOverflowing = contentRef.current.scrollHeight > maxHeight; 17 | setShowToggle(isOverflowing); 18 | if (!isOverflowing) { 19 | setExpanded(true); 20 | } 21 | } 22 | }, [children, maxHeight]); 23 | 24 | const handleToggleClick = useCallback( 25 | (e: React.MouseEvent) => { 26 | e.preventDefault(); 27 | setExpanded(!expanded); 28 | }, 29 | [expanded] 30 | ); 31 | 32 | return ( 33 |
34 |
38 | {children} 39 | {!expanded &&
} 40 |
41 | {showToggle && ( 42 |
43 | 51 |
52 | )} 53 |
54 | ); 55 | }; 56 | 57 | export default FadeContent; 58 | -------------------------------------------------------------------------------- /docs/api/interfaces/MediaLike.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / MediaLike 6 | 7 | # Interface: MediaLike 8 | 9 | Defined in: [src/entities/MediaItem.ts:1](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L1) 10 | 11 | ## Extended by 12 | 13 | - [`SingleImageMediaItem`](SingleImageMediaItem.md) 14 | - [`DefaultImageMediaItem`](DefaultImageMediaItem.md) 15 | - [`CampaignCoverPhotoMediaItem`](CampaignCoverPhotoMediaItem.md) 16 | - [`PostCoverImageMediaItem`](PostCoverImageMediaItem.md) 17 | - [`PostThumbnailMediaItem`](PostThumbnailMediaItem.md) 18 | - [`CollectionThumbnailMediaItem`](CollectionThumbnailMediaItem.md) 19 | - [`VideoMediaItem`](VideoMediaItem.md) 20 | - [`AudioMediaItem`](AudioMediaItem.md) 21 | - [`FileMediaItem`](FileMediaItem.md) 22 | - [`AttachmentMediaItem`](AttachmentMediaItem.md) 23 | - [`DummyMediaItem`](DummyMediaItem.md) 24 | 25 | ## Properties 26 | 27 | ### filename 28 | 29 | > **filename**: `null` \| `string` 30 | 31 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4) 32 | 33 | *** 34 | 35 | ### id 36 | 37 | > **id**: `string` 38 | 39 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3) 40 | 41 | *** 42 | 43 | ### mimeType 44 | 45 | > **mimeType**: `null` \| `string` 46 | 47 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5) 48 | 49 | *** 50 | 51 | ### type 52 | 53 | > **type**: `string` 54 | 55 | Defined in: [src/entities/MediaItem.ts:2](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L2) 56 | -------------------------------------------------------------------------------- /docs/api/classes/DateTime.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DateTime 6 | 7 | # Class: DateTime 8 | 9 | Defined in: [src/utils/DateTime.ts:17](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/DateTime.ts#L17) 10 | 11 | ## Constructors 12 | 13 | ### Constructor 14 | 15 | > **new DateTime**(`args`): `DateTime` 16 | 17 | Defined in: [src/utils/DateTime.ts:32](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/DateTime.ts#L32) 18 | 19 | #### Parameters 20 | 21 | ##### args 22 | 23 | [`DateTimeConstructorArgs`](../type-aliases/DateTimeConstructorArgs.md) 24 | 25 | #### Returns 26 | 27 | `DateTime` 28 | 29 | ## Properties 30 | 31 | ### FORMAT 32 | 33 | > `static` **FORMAT**: `string` = `'yyyy-MM-dd [hh:mm[:ss] [GMT]]'` 34 | 35 | Defined in: [src/utils/DateTime.ts:19](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/DateTime.ts#L19) 36 | 37 | ## Methods 38 | 39 | ### toString() 40 | 41 | > **toString**(): `string` 42 | 43 | Defined in: [src/utils/DateTime.ts:113](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/DateTime.ts#L113) 44 | 45 | #### Returns 46 | 47 | `string` 48 | 49 | *** 50 | 51 | ### valueOf() 52 | 53 | > **valueOf**(): `Date` 54 | 55 | Defined in: [src/utils/DateTime.ts:109](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/DateTime.ts#L109) 56 | 57 | #### Returns 58 | 59 | `Date` 60 | 61 | *** 62 | 63 | ### from() 64 | 65 | > `static` **from**(`value`): `DateTime` 66 | 67 | Defined in: [src/utils/DateTime.ts:71](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/DateTime.ts#L71) 68 | 69 | #### Parameters 70 | 71 | ##### value 72 | 73 | `string` | `number` 74 | 75 | #### Returns 76 | 77 | `DateTime` 78 | -------------------------------------------------------------------------------- /src/browse/web/layouts/CollectionLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Row, Col } from "react-bootstrap"; 2 | import { Outlet, useParams } from "react-router"; 3 | import { useAPI } from "../contexts/APIProvider"; 4 | import { useEffect, useState } from "react"; 5 | import CampaignHeader from "../components/CampaignHeader"; 6 | import { type CampaignWithCounts } from "../../types/Campaign"; 7 | import CollectionBanner from "../components/CollectionBanner"; 8 | import { type Collection } from "../../../entities/Post"; 9 | 10 | function CollectionLayout() { 11 | const { id: collectionId } = useParams(); 12 | 13 | if (!collectionId) { 14 | return null; 15 | } 16 | const { api } = useAPI(); 17 | const [campaign, setCampaign] = useState(null); 18 | const [collection, setCollection] = useState(null); 19 | 20 | useEffect(() => { 21 | const abortController = new AbortController(); 22 | void (async () => { 23 | const { campaignId, collection } = await api.getCollection(collectionId); 24 | const campaign = await api.getCampaign({ id: campaignId, withCounts: true }); 25 | if (!abortController.signal.aborted) { 26 | setCampaign(campaign); 27 | setCollection(collection); 28 | }; 29 | })(); 30 | 31 | return () => abortController.abort(); 32 | }, [api, collectionId]); 33 | 34 | if (!campaign) { 35 | return null; 36 | } 37 | 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {collection && } 48 | 49 | 50 | 51 | 52 | ) 53 | } 54 | 55 | export default CollectionLayout; -------------------------------------------------------------------------------- /src/utils/ObjectHelper.ts: -------------------------------------------------------------------------------- 1 | import DateTime from "./DateTime.js"; 2 | 3 | const NO_CLEAN = [ DateTime ]; 4 | 5 | export default class ObjectHelper { 6 | 7 | static getProperty(obj: any, prop: string, required = false) { 8 | const props = prop.split('.'); 9 | let v = obj; 10 | while (props.length > 0) { 11 | const p = props.shift() as string; 12 | if (v && typeof v === 'object') { 13 | v = v[p]; 14 | if (v === undefined) { 15 | if (required) { 16 | throw new ObjectPropertyNotFoundError(prop); 17 | } 18 | return v; 19 | } 20 | } 21 | else if (required) { 22 | throw new ObjectPropertyNotFoundError(prop); 23 | } 24 | } 25 | return v; 26 | } 27 | 28 | static clean(obj: any, opts?: { 29 | deep?: boolean; 30 | cleanNulls?: boolean; 31 | cleanEmptyObjects?: boolean; 32 | }) { 33 | const deep = opts?.deep || false; 34 | const cleanNulls = opts?.cleanNulls || false; 35 | const cleanEmptyObjects = opts?.cleanEmptyObjects || false; 36 | 37 | if (!obj || typeof obj !== 'object') { 38 | return obj; 39 | } 40 | const result: any = {}; 41 | for (const [ k, v ] of Object.entries(obj)) { 42 | const skip = v === undefined || (v === null && cleanNulls); 43 | if (!skip) { 44 | if (v !== null && typeof v === 'object' && !NO_CLEAN.find((nc) => v instanceof nc)) { 45 | const c = deep ? this.clean(v, opts) : v; 46 | if (Object.entries(c).length > 0 || !cleanEmptyObjects) { 47 | result[k] = c; 48 | } 49 | } 50 | else { 51 | result[k] = v; 52 | } 53 | } 54 | } 55 | return Array.isArray(obj) ? Object.values(result) : result; 56 | } 57 | } 58 | 59 | class ObjectPropertyNotFoundError extends Error { 60 | 61 | prop: string; 62 | 63 | constructor(prop: string) { 64 | super(); 65 | this.name = 'ObjectPropertyNotFoundError'; 66 | this.prop = prop; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/api/interfaces/AttachmentMediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / AttachmentMediaItem 6 | 7 | # Interface: AttachmentMediaItem 8 | 9 | Defined in: [src/entities/MediaItem.ts:125](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L125) 10 | 11 | ## Extends 12 | 13 | - [`MediaLike`](MediaLike.md) 14 | 15 | ## Properties 16 | 17 | ### downloadURL 18 | 19 | > **downloadURL**: `null` \| `string` 20 | 21 | Defined in: [src/entities/MediaItem.ts:127](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L127) 22 | 23 | *** 24 | 25 | ### filename 26 | 27 | > **filename**: `null` \| `string` 28 | 29 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4) 30 | 31 | #### Inherited from 32 | 33 | [`MediaLike`](MediaLike.md).[`filename`](MediaLike.md#filename) 34 | 35 | *** 36 | 37 | ### id 38 | 39 | > **id**: `string` 40 | 41 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3) 42 | 43 | #### Inherited from 44 | 45 | [`MediaLike`](MediaLike.md).[`id`](MediaLike.md#id) 46 | 47 | *** 48 | 49 | ### mimeType 50 | 51 | > **mimeType**: `null` \| `string` 52 | 53 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5) 54 | 55 | #### Inherited from 56 | 57 | [`MediaLike`](MediaLike.md).[`mimeType`](MediaLike.md#mimetype) 58 | 59 | *** 60 | 61 | ### type 62 | 63 | > **type**: `"attachment"` 64 | 65 | Defined in: [src/entities/MediaItem.ts:126](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L126) 66 | 67 | #### Overrides 68 | 69 | [`MediaLike`](MediaLike.md).[`type`](MediaLike.md#type) 70 | -------------------------------------------------------------------------------- /src/browse/web/components/LightGalleryItem.tsx: -------------------------------------------------------------------------------- 1 | import "../assets/styles/LightGalleryItem.scss"; 2 | import { Badge, Card } from "react-bootstrap"; 3 | 4 | export interface LightGalleryItemProps { 5 | id: string; 6 | href?: string; 7 | dataSrc?: string; 8 | dataVideo?: string; 9 | dataPoster?: string; 10 | dataIframe?: boolean; 11 | dataSubHTML?: string; 12 | thumbnailURL?: string; 13 | classNamePrefix: string; 14 | style?: React.CSSProperties; 15 | badge?: string; 16 | } 17 | 18 | function LightGalleryItem(props: LightGalleryItemProps) { 19 | const { 20 | href, 21 | dataSrc, 22 | dataVideo, 23 | dataPoster, 24 | dataIframe, 25 | dataSubHTML, 26 | thumbnailURL, 27 | classNamePrefix, 28 | style, 29 | badge 30 | } = props; 31 | let extraClassName = dataVideo ? `${classNamePrefix}__thumbnail-wrapper--video` : ''; 32 | if (!thumbnailURL) { 33 | extraClassName += ` ${classNamePrefix}__thumbnail-wrapper--empty`; 34 | } 35 | return ( 36 | { 46 | thumbnailURL ? ( 47 | 51 | ) 52 | : ( 53 | 54 | description 55 | 56 | ) 57 | } 58 | { 59 | badge ? ( 60 | 61 | {badge} 62 | 63 | ) : null 64 | } 65 | 66 | ) 67 | } 68 | 69 | export default LightGalleryItem; -------------------------------------------------------------------------------- /docs/api/interfaces/ConsoleLoggerOptions.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / ConsoleLoggerOptions 6 | 7 | # Interface: ConsoleLoggerOptions 8 | 9 | Defined in: [src/utils/logging/ConsoleLogger.ts:15](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/ConsoleLogger.ts#L15) 10 | 11 | ## Properties 12 | 13 | ### color? 14 | 15 | > `optional` **color**: `boolean` 16 | 17 | Defined in: [src/utils/logging/ConsoleLogger.ts:25](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/ConsoleLogger.ts#L25) 18 | 19 | *** 20 | 21 | ### dateTimeFormat? 22 | 23 | > `optional` **dateTimeFormat**: `string` 24 | 25 | Defined in: [src/utils/logging/ConsoleLogger.ts:24](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/ConsoleLogger.ts#L24) 26 | 27 | *** 28 | 29 | ### enabled? 30 | 31 | > `optional` **enabled**: `boolean` 32 | 33 | Defined in: [src/utils/logging/ConsoleLogger.ts:16](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/ConsoleLogger.ts#L16) 34 | 35 | *** 36 | 37 | ### include? 38 | 39 | > `optional` **include**: `object` 40 | 41 | Defined in: [src/utils/logging/ConsoleLogger.ts:18](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/ConsoleLogger.ts#L18) 42 | 43 | #### dateTime? 44 | 45 | > `optional` **dateTime**: `boolean` 46 | 47 | #### errorStack? 48 | 49 | > `optional` **errorStack**: `boolean` 50 | 51 | #### level? 52 | 53 | > `optional` **level**: `boolean` 54 | 55 | #### originator? 56 | 57 | > `optional` **originator**: `boolean` 58 | 59 | *** 60 | 61 | ### logLevel? 62 | 63 | > `optional` **logLevel**: [`LogLevel`](../type-aliases/LogLevel.md) 64 | 65 | Defined in: [src/utils/logging/ConsoleLogger.ts:17](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/utils/logging/ConsoleLogger.ts#L17) 66 | -------------------------------------------------------------------------------- /docs/api/classes/WebServer.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / WebServer 6 | 7 | # Class: WebServer 8 | 9 | Defined in: [src/browse/server/WebServer.ts:19](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L19) 10 | 11 | ## Constructors 12 | 13 | ### Constructor 14 | 15 | > **new WebServer**(`config`): `WebServer` 16 | 17 | Defined in: [src/browse/server/WebServer.ts:30](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L30) 18 | 19 | #### Parameters 20 | 21 | ##### config 22 | 23 | [`WebServerConfig`](../interfaces/WebServerConfig.md) 24 | 25 | #### Returns 26 | 27 | `WebServer` 28 | 29 | ## Properties 30 | 31 | ### name 32 | 33 | > **name**: `string` = `'WebServer'` 34 | 35 | Defined in: [src/browse/server/WebServer.ts:20](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L20) 36 | 37 | ## Methods 38 | 39 | ### getConfig() 40 | 41 | > **getConfig**(): [`WebServerConfig`](../interfaces/WebServerConfig.md) 42 | 43 | Defined in: [src/browse/server/WebServer.ts:118](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L118) 44 | 45 | #### Returns 46 | 47 | [`WebServerConfig`](../interfaces/WebServerConfig.md) 48 | 49 | *** 50 | 51 | ### start() 52 | 53 | > **start**(): `Promise`\<`void`\> 54 | 55 | Defined in: [src/browse/server/WebServer.ts:39](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L39) 56 | 57 | #### Returns 58 | 59 | `Promise`\<`void`\> 60 | 61 | *** 62 | 63 | ### stop() 64 | 65 | > **stop**(): `undefined` \| `Promise`\<`void`\> 66 | 67 | Defined in: [src/browse/server/WebServer.ts:82](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/browse/server/WebServer.ts#L82) 68 | 69 | #### Returns 70 | 71 | `undefined` \| `Promise`\<`void`\> 72 | -------------------------------------------------------------------------------- /src/browse/web/components/CollectionCard.tsx: -------------------------------------------------------------------------------- 1 | import "../assets/styles/CollectionCard.scss"; 2 | import { Link } from "react-router"; 3 | import { Card, Stack } from "react-bootstrap"; 4 | import MediaImage from "./MediaImage"; 5 | import { type Collection } from "../../../entities/Post"; 6 | 7 | interface CollectionCardProps { 8 | collection: Collection; 9 | } 10 | 11 | const COUNT_ICONS: Partial> = { 12 | numPosts: 'article', 13 | }; 14 | 15 | function CollectionCard(props: CollectionCardProps) { 16 | const { collection } = props; 17 | 18 | const thumbnailId = collection.thumbnail?.downloaded?.path ? collection.thumbnail.id : null; 19 | 20 | return ( 21 | 22 | 23 | {thumbnailId && ( 24 |
25 | 26 |
27 | )} 28 | 29 |
30 | 31 | {collection.title} 32 | 33 |
34 | { 35 | collection.description ? ( 36 |
37 | {collection.description} 38 |
39 | ) : null 40 | } 41 | 45 | 46 | {COUNT_ICONS['numPosts']} 47 | {collection.numPosts || 0} 48 | 49 | 50 |
51 |
52 |
53 | ) 54 | } 55 | 56 | export default CollectionCard; -------------------------------------------------------------------------------- /src/browse/api/SettingsAPIMixin.ts: -------------------------------------------------------------------------------- 1 | import { type APIConstructor } from "."; 2 | import { type BrowseSettingOptions, type BrowseSettings, type BrowseTheme } from "../types/Settings.js"; 3 | 4 | const BROWSE_SETTINGS_ENV_KEY = 'browse_settings'; 5 | 6 | const THEMES: BrowseTheme[] = [ 7 | { 8 | name: 'Default', 9 | value: 'default', 10 | stylesheets: ['/themes/bootstrap/default/css/bootstrap.min.css'] 11 | }, 12 | ...[ 13 | 'brite', 14 | 'cerulean', 15 | 'cosmo', 16 | 'cyborg', 17 | 'darkly', 18 | 'flatly', 19 | 'journal', 20 | 'litera', 21 | 'lumen', 22 | 'lux', 23 | 'materia', 24 | 'minty', 25 | 'morph', 26 | 'pulse', 27 | 'quartz', 28 | 'sandstone', 29 | 'simplex', 30 | 'sketchy', 31 | 'slate', 32 | 'solar', 33 | 'spacelab', 34 | 'superhero', 35 | 'united', 36 | 'vapor', 37 | 'yeti', 38 | 'zephyr' 39 | ].map((bootswatchThemeName) => ({ 40 | name: bootswatchThemeName.charAt(0).toUpperCase() + bootswatchThemeName.slice(1), 41 | value: bootswatchThemeName, 42 | stylesheets: [`/themes/bootswatch/${bootswatchThemeName}/bootstrap.min.css`] 43 | })) 44 | ] 45 | 46 | const DEFAULT_BROWSE_SETTINGS: BrowseSettings = { 47 | theme: THEMES[0].value, 48 | listItemsPerPage: 20, 49 | galleryItemsPerPage: 100 50 | }; 51 | 52 | export function SettingsAPIMixin(Base: TBase) { 53 | return class SettingsAPI extends Base { 54 | getBrowseSettings() { 55 | const settings = this.db.getEnvValue(BROWSE_SETTINGS_ENV_KEY); 56 | if (settings) { 57 | return { 58 | ...DEFAULT_BROWSE_SETTINGS, 59 | ...settings 60 | }; 61 | } 62 | return {...DEFAULT_BROWSE_SETTINGS}; 63 | } 64 | 65 | saveBrowseSettings(settings: BrowseSettings) { 66 | return this.db.saveEnvValue(BROWSE_SETTINGS_ENV_KEY, settings); 67 | } 68 | 69 | getBrowseSettingOptions(): BrowseSettingOptions { 70 | return { 71 | themes: THEMES, 72 | listItemsPerPage: [10, 20, 30, 50], 73 | galleryItemsPerPage: [50, 100, 150, 200] 74 | }; 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/browse/db/EnvDBMixin.ts: -------------------------------------------------------------------------------- 1 | import { type DBConstructor } from '.'; 2 | 3 | export type EnvDBConstructor = new ( 4 | ...args: any[] 5 | ) => InstanceType>>; 6 | 7 | export function EnvDBMixin(Base: TBase) { 8 | return class EnvDB extends Base { 9 | saveEnvValue(key: string, value: any) { 10 | this.log('debug', `Save env value for "${key}" to DB`); 11 | const exists = this.checkEnvExists(key); 12 | if (!exists) { 13 | this.run( 14 | ` 15 | INSERT INTO env ( 16 | env_key, 17 | value 18 | ) 19 | VALUES (?, ?) 20 | `, 21 | [ 22 | key, 23 | JSON.stringify(value) 24 | ] 25 | ); 26 | } else { 27 | this.log('debug', `Env value for "${key}" already exists in DB - update record`); 28 | this.run(` 29 | UPDATE env 30 | SET 31 | value = ? 32 | WHERE env_key = ? 33 | `, 34 | [ 35 | JSON.stringify(value), 36 | key 37 | ] 38 | ); 39 | } 40 | } 41 | 42 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters 43 | getEnvValue(key: string): T | null { 44 | this.log('debug', `Get env value for "${key}" from DB`); 45 | const result = this.get( 46 | `SELECT value FROM env WHERE env_key = ?`, 47 | [key] 48 | ); 49 | return result ? JSON.parse(result.value) : null; 50 | } 51 | 52 | checkEnvExists(key: string) { 53 | this.log('debug', `Check if env value for "${key}" exists in DB`); 54 | try { 55 | const result = this.get( 56 | ` 57 | SELECT COUNT(*) as count 58 | FROM env 59 | WHERE 60 | env_key = ? 61 | `, 62 | [ key ] 63 | ); 64 | return result.count > 0; 65 | } catch (error) { 66 | this.log( 67 | 'error', 68 | `Failed to check if env value for "${key}" exist in DB:`, 69 | error 70 | ); 71 | return false; 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/browse/web/components/SearchInputBox.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useCallback, useImperativeHandle, useState } from "react"; 2 | import { Button, Form, InputGroup } from "react-bootstrap"; 3 | 4 | export type SearchInputBoxOnConfirmListener = (value: string) => void; 5 | 6 | export interface SearchInputBoxHandle { 7 | onConfirm: (listener: SearchInputBoxOnConfirmListener | null) => void; 8 | setInput: (value: string) => void; 9 | } 10 | 11 | interface SearchInputBoxProps { 12 | placeholder?: string; 13 | onConfirm?: SearchInputBoxOnConfirmListener | null; 14 | } 15 | 16 | const SearchInputBox = forwardRef((props, ref) => { 17 | const [ onConfirm, setOnConfirmListener ] = useState(() => props.onConfirm || null); 18 | const [input, setInput] = useState(''); 19 | 20 | const handleChange = useCallback( 21 | (e: React.ChangeEvent) => { 22 | setInput(e.target.value); 23 | }, 24 | [] 25 | ); 26 | 27 | const confirm = useCallback(() => { 28 | if (!onConfirm) { 29 | return; 30 | } 31 | onConfirm(input); 32 | }, [input, onConfirm]); 33 | 34 | const handleKeyDown = useCallback( 35 | (e: React.KeyboardEvent) => { 36 | if (e.key === 'Enter') { 37 | confirm(); 38 | } 39 | }, 40 | [confirm] 41 | ); 42 | 43 | useImperativeHandle(ref, () => ({ 44 | onConfirm: (listener: SearchInputBoxOnConfirmListener | null) => { 45 | setOnConfirmListener(() => listener); 46 | }, 47 | setInput: (value: string) => { 48 | setInput(value); 49 | } 50 | })); 51 | 52 | return ( 53 | 54 | 61 | 68 | 69 | ) 70 | }); 71 | 72 | export default SearchInputBox; -------------------------------------------------------------------------------- /src/cli/server/ServerCLIOptions.ts: -------------------------------------------------------------------------------- 1 | import { type DeepPartial } from '../../utils/Misc.js'; 2 | import CLIOptionValidator from '../CLIOptionValidator.js'; 3 | import { type ConsoleLoggerOptions } from '../../utils/logging/ConsoleLogger.js'; 4 | import { type FileLoggerOptions, type FileLoggerType } from '../../utils/logging/FileLogger.js'; 5 | import { type WebServerConfig } from '../../browse/server/WebServer.js'; 6 | import ServerCommandLineParser, { type ServerCommandLineParseResult } from './ServerCommandLineParser.js'; 7 | 8 | export interface ServerCLIOptions extends DeepPartial> { 9 | consoleLogger: ConsoleLoggerOptions; 10 | fileLogger?: FileLoggerOptions; 11 | } 12 | 13 | export type ServerCLIOptionParserEntry = { 14 | src: 'cli' 15 | key: string; 16 | value?: string; 17 | } 18 | 19 | export function getServerCLIOptions(): ServerCLIOptions { 20 | const commandLineOptions = ServerCommandLineParser.parse(); 21 | 22 | const { consoleLogger, fileLogger } = getServerCLILoggerOptions(commandLineOptions); 23 | 24 | const options: ServerCLIOptions = { 25 | dataDir: CLIOptionValidator.validateString(commandLineOptions.dataDir), 26 | port: CLIOptionValidator.validateNumber(commandLineOptions.port), 27 | consoleLogger, 28 | fileLogger 29 | }; 30 | 31 | return options; 32 | } 33 | 34 | export function getServerCLILoggerOptions(commandLineOptions?: ServerCommandLineParseResult) { 35 | if (!commandLineOptions) { 36 | commandLineOptions = ServerCommandLineParser.parse(); 37 | } 38 | 39 | const logLevel = CLIOptionValidator.validateString(commandLineOptions.logLevel, 'info', 'debug', 'warn', 'error', 'none'); 40 | const enabled = logLevel !== 'none'; 41 | const consoleLogger = { 42 | enabled, 43 | logLevel: logLevel !== 'none' ? logLevel : undefined, 44 | }; 45 | let fileLogger: FileLoggerOptions | undefined = undefined; 46 | const logFilePath = CLIOptionValidator.validateString(commandLineOptions.logFile); 47 | if (logFilePath) { 48 | fileLogger = { 49 | logLevel: consoleLogger.logLevel, 50 | logFilePath 51 | }; 52 | } 53 | return { 54 | consoleLogger, 55 | fileLogger 56 | }; 57 | } -------------------------------------------------------------------------------- /src/utils/FetcherProgressMonitor.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import type Progress from './Progress'; 3 | 4 | export type FetcherProgress = { 5 | speed: number; 6 | percentage?: number; 7 | transferred: number; 8 | length?: number; 9 | remaining?: number; 10 | eta?: number; 11 | runtime: number; 12 | destFilename: string; 13 | destFilePath: string; 14 | }; 15 | 16 | export default class FetcherProgressMonitor extends EventEmitter { 17 | 18 | #progress: Progress; 19 | #destFilename: string; 20 | #destFilePath: string; 21 | 22 | constructor(progress: Progress, destFilename: string, destFilePath: string) { 23 | super(); 24 | this.#progress = progress; 25 | this.#destFilename = destFilename; 26 | this.#destFilePath = destFilePath; 27 | 28 | this.#progress.on('progress', this.#handleProgressStreamProgressEvent.bind(this)); 29 | } 30 | 31 | getDestFilename() { 32 | return this.#destFilename; 33 | } 34 | 35 | getDestFilePath() { 36 | return this.#destFilePath; 37 | } 38 | 39 | getProgress(): FetcherProgress { 40 | return { 41 | speed: this.#progress.speed, 42 | percentage: this.#progress.percentage, 43 | transferred: this.#progress.transferred, 44 | length: this.#progress.length, 45 | remaining: this.#progress.remaining, 46 | eta: this.#progress.eta, 47 | runtime: this.#progress.runtime, 48 | destFilename: this.#destFilename, 49 | destFilePath: this.#destFilePath 50 | }; 51 | } 52 | 53 | #handleProgressStreamProgressEvent() { 54 | this.emit('progress', this.getProgress()); 55 | } 56 | 57 | emit(event: 'progress', progress: FetcherProgress): boolean; 58 | emit(event: string | symbol, ...args: any[]): boolean { 59 | return super.emit(event, ...args); 60 | } 61 | 62 | on(event: 'progress', listener: (progress: FetcherProgress) => void): this; 63 | on(event: string | symbol, listener: (...args: any[]) => void): this { 64 | return super.on(event, listener); 65 | } 66 | 67 | once(event: 'progress', listener: (progress: FetcherProgress) => void): this; 68 | once(event: string | symbol, listener: (...args: any[]) => void): this { 69 | return super.once(event, listener); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/browse/web/components/PageInputButton.tsx: -------------------------------------------------------------------------------- 1 | import { OverlayTrigger, Popover, Form, Button, InputGroup, Stack } from "react-bootstrap"; 2 | import { useCallback, useRef, useState } from "react"; 3 | 4 | interface PageInputButtonProps { 5 | currentPage: number; 6 | totalPages: number; 7 | onChange: (page: number) => void; 8 | } 9 | 10 | function PageInputButton(props: PageInputButtonProps) { 11 | const { currentPage, totalPages, onChange } = props; 12 | const [value, setValue] = useState(currentPage); 13 | const inputRef = useRef(null); 14 | 15 | const onShow = useCallback(() => { 16 | if (inputRef.current) { 17 | inputRef.current.focus(); 18 | } 19 | }, []); 20 | 21 | const onHide = useCallback(() => { 22 | if (inputRef.current) { 23 | inputRef.current.blur(); 24 | } 25 | }, []); 26 | 27 | const handleGoClick = useCallback(() => { 28 | if (!inputRef.current) { 29 | return; 30 | } 31 | const page = Number(inputRef.current.value); 32 | if (isNaN(page) || page === currentPage) { 33 | return; 34 | } 35 | const p = Math.max(Math.min(totalPages, page), 1); 36 | onChange(p); 37 | }, [currentPage, totalPages]); 38 | 39 | return ( 40 | 48 | 49 | 50 | Go to: 51 | 52 | setValue(Number(e.target.value))} /> 60 | 61 | 62 | 63 | 64 | 65 | } 66 | > 67 | 68 | 69 | ) 70 | } 71 | 72 | export default PageInputButton; -------------------------------------------------------------------------------- /docs/api/interfaces/FileMediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / FileMediaItem 6 | 7 | # Interface: FileMediaItem 8 | 9 | Defined in: [src/entities/MediaItem.ts:119](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L119) 10 | 11 | ## Extends 12 | 13 | - [`MediaLike`](MediaLike.md) 14 | 15 | ## Properties 16 | 17 | ### createdAt 18 | 19 | > **createdAt**: `null` \| `string` 20 | 21 | Defined in: [src/entities/MediaItem.ts:121](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L121) 22 | 23 | *** 24 | 25 | ### downloadURL 26 | 27 | > **downloadURL**: `null` \| `string` 28 | 29 | Defined in: [src/entities/MediaItem.ts:122](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L122) 30 | 31 | *** 32 | 33 | ### filename 34 | 35 | > **filename**: `null` \| `string` 36 | 37 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4) 38 | 39 | #### Inherited from 40 | 41 | [`MediaLike`](MediaLike.md).[`filename`](MediaLike.md#filename) 42 | 43 | *** 44 | 45 | ### id 46 | 47 | > **id**: `string` 48 | 49 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3) 50 | 51 | #### Inherited from 52 | 53 | [`MediaLike`](MediaLike.md).[`id`](MediaLike.md#id) 54 | 55 | *** 56 | 57 | ### mimeType 58 | 59 | > **mimeType**: `null` \| `string` 60 | 61 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5) 62 | 63 | #### Inherited from 64 | 65 | [`MediaLike`](MediaLike.md).[`mimeType`](MediaLike.md#mimetype) 66 | 67 | *** 68 | 69 | ### type 70 | 71 | > **type**: `"file"` 72 | 73 | Defined in: [src/entities/MediaItem.ts:120](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L120) 74 | 75 | #### Overrides 76 | 77 | [`MediaLike`](MediaLike.md).[`type`](MediaLike.md#type) 78 | -------------------------------------------------------------------------------- /docs/api/interfaces/Comment.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / Comment 6 | 7 | # Interface: Comment 8 | 9 | Defined in: [src/entities/Comment.ts:7](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L7) 10 | 11 | ## Properties 12 | 13 | ### body 14 | 15 | > **body**: `string` 16 | 17 | Defined in: [src/entities/Comment.ts:10](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L10) 18 | 19 | *** 20 | 21 | ### commenter 22 | 23 | > **commenter**: `null` \| [`User`](User.md) 24 | 25 | Defined in: [src/entities/Comment.ts:11](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L11) 26 | 27 | *** 28 | 29 | ### createdAt 30 | 31 | > **createdAt**: `null` \| `string` 32 | 33 | Defined in: [src/entities/Comment.ts:12](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L12) 34 | 35 | *** 36 | 37 | ### id 38 | 39 | > **id**: `string` 40 | 41 | Defined in: [src/entities/Comment.ts:9](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L9) 42 | 43 | *** 44 | 45 | ### isByCreator 46 | 47 | > **isByCreator**: `boolean` 48 | 49 | Defined in: [src/entities/Comment.ts:13](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L13) 50 | 51 | *** 52 | 53 | ### replies 54 | 55 | > **replies**: [`CommentReply`](../type-aliases/CommentReply.md)[] 56 | 57 | Defined in: [src/entities/Comment.ts:15](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L15) 58 | 59 | *** 60 | 61 | ### replyCount 62 | 63 | > **replyCount**: `number` 64 | 65 | Defined in: [src/entities/Comment.ts:14](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L14) 66 | 67 | *** 68 | 69 | ### type 70 | 71 | > **type**: `"comment"` 72 | 73 | Defined in: [src/entities/Comment.ts:8](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Comment.ts#L8) 74 | -------------------------------------------------------------------------------- /src/browse/web/components/ProductList.tsx: -------------------------------------------------------------------------------- 1 | import "../assets/styles/ProductList.scss"; 2 | import { useEffect, useMemo, useRef, useState } from "react"; 3 | import { type Product } from "../../../entities"; 4 | import ProductCard from "./ProductCard"; 5 | 6 | interface ProductListProps { 7 | products: Product[]; 8 | } 9 | 10 | const MIN_CARD_WIDTH = 300; 11 | const GAP = 16; 12 | 13 | function ProductList(props: ProductListProps ) { 14 | const { products } = props; 15 | const containerRef = useRef(null); 16 | const [containerWidth, setContainerWidth] = useState(null); 17 | 18 | useEffect(() => { 19 | const observer = new ResizeObserver((entries) => { 20 | const { width } = entries[0].contentRect; 21 | setContainerWidth(width); 22 | }); 23 | 24 | if (containerRef.current) { 25 | observer.observe(containerRef.current); 26 | } 27 | 28 | return () => observer.disconnect(); 29 | }, []); 30 | 31 | const layoutProps = useMemo(() => { 32 | if (!containerWidth) { 33 | return { 34 | cardWidth: MIN_CARD_WIDTH, 35 | lastRowFirstItemIndex: 0 36 | }; 37 | } 38 | const cardsPerRow = Math.floor((containerWidth + GAP) / (MIN_CARD_WIDTH + GAP)); 39 | const cardWidth = (((containerWidth + GAP) / cardsPerRow) - GAP).toFixed(2); 40 | const lastRowFirstItemIndex = Math.floor((products.length - 1) / cardsPerRow) * cardsPerRow; 41 | return { 42 | cardWidth, 43 | lastRowFirstItemIndex 44 | } 45 | }, [containerWidth]); 46 | 47 | const { cardWidth, lastRowFirstItemIndex } = layoutProps; 48 | 49 | return ( 50 |
58 | { 59 | products.map((product, index) => ( 60 |
= lastRowFirstItemIndex ? 'product-list__card-wrapper--fixed' : ''}`} 62 | > 63 | 64 |
65 | )) 66 | } 67 |
68 | ) 69 | } 70 | 71 | export default ProductList; -------------------------------------------------------------------------------- /docs/api/interfaces/DummyMediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DummyMediaItem 6 | 7 | # Interface: DummyMediaItem 8 | 9 | Defined in: [src/entities/MediaItem.ts:136](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L136) 10 | 11 | Minimal `MediaItem` typically used to represent media-type properties of elements, such 12 | as video thumbnails and campaign avatar / cover photos. 13 | As a `MediaItem` type, and hence also a `Downloadable` type, it can be used to create 14 | `MediaFilenameResolver` and `DownloadTask` instances. 15 | 16 | ## Extends 17 | 18 | - [`MediaLike`](MediaLike.md) 19 | 20 | ## Properties 21 | 22 | ### filename 23 | 24 | > **filename**: `null` \| `string` 25 | 26 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4) 27 | 28 | #### Inherited from 29 | 30 | [`MediaLike`](MediaLike.md).[`filename`](MediaLike.md#filename) 31 | 32 | *** 33 | 34 | ### id 35 | 36 | > **id**: `string` 37 | 38 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3) 39 | 40 | #### Inherited from 41 | 42 | [`MediaLike`](MediaLike.md).[`id`](MediaLike.md#id) 43 | 44 | *** 45 | 46 | ### mimeType 47 | 48 | > **mimeType**: `null` \| `string` 49 | 50 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5) 51 | 52 | #### Inherited from 53 | 54 | [`MediaLike`](MediaLike.md).[`mimeType`](MediaLike.md#mimetype) 55 | 56 | *** 57 | 58 | ### srcURLs 59 | 60 | > **srcURLs**: `Record`\<`string`, `string` \| `null`\> 61 | 62 | Defined in: [src/entities/MediaItem.ts:139](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L139) 63 | 64 | *** 65 | 66 | ### type 67 | 68 | > **type**: `"dummy"` 69 | 70 | Defined in: [src/entities/MediaItem.ts:137](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L137) 71 | 72 | #### Overrides 73 | 74 | [`MediaLike`](MediaLike.md).[`type`](MediaLike.md#type) 75 | -------------------------------------------------------------------------------- /src/browse/web/assets/styles/MediaGrid.scss: -------------------------------------------------------------------------------- 1 | .lg-video-poster { 2 | object-fit: contain; 3 | } 4 | 5 | .media-grid { 6 | display: grid; 7 | position: relative; 8 | max-height: 32em; 9 | gap: 1px; 10 | background: var(--bs-card-border-color); 11 | border-bottom: var(--bs-card-border-color); 12 | 13 | &--1 { 14 | grid-template-rows: 1fr; 15 | grid-template-columns: 1fr; 16 | grid-template-areas: 'a'; 17 | } 18 | 19 | &--2 { 20 | grid-template-rows: 1fr; 21 | grid-template-columns: 1fr 1fr; 22 | grid-template-areas: 'a b'; 23 | } 24 | 25 | &--3 { 26 | grid-template-rows: 1fr 1fr; 27 | grid-template-columns: 1fr 1fr; 28 | grid-template-areas: 'a b' 'a c'; 29 | } 30 | 31 | &--4 { 32 | grid-template-rows: 1fr 1fr; 33 | grid-template-columns: 1fr 1fr; 34 | grid-template-areas: 'a b' 'c d'; 35 | } 36 | 37 | &__item-wrapper { 38 | width: 100%; 39 | position: relative; 40 | 41 | &:nth-child(1) { 42 | grid-area: a; 43 | } 44 | 45 | &:nth-child(2) { 46 | grid-area: b; 47 | } 48 | 49 | &:nth-child(3) { 50 | grid-area: c; 51 | } 52 | 53 | &:nth-child(4) { 54 | grid-area: d; 55 | } 56 | } 57 | 58 | &__thumbnail-backdrop { 59 | width: 100%; 60 | height: 100%; 61 | position: absolute; 62 | background-size: cover; 63 | filter: blur(1em); 64 | transform: scale(1.5); 65 | } 66 | 67 | &__thumbnail-wrapper { 68 | width: 100%; 69 | height: 100%; 70 | z-index: 1000; 71 | cursor: pointer; 72 | 73 | &--video::before { 74 | content: '\e1c4'; 75 | font-family: 'Material Icons Outlined'; 76 | font-size: 4em; 77 | position: absolute; 78 | top: 50%; 79 | left: 50%; 80 | transform: translate(-50%, -50%); 81 | color: #fff; 82 | transition: opacity 150ms ease-in-out; 83 | opacity: 0.8; 84 | } 85 | 86 | &--video:hover::before { 87 | opacity: 1; 88 | } 89 | } 90 | 91 | &__thumbnail { 92 | width: 100%; 93 | height: 100%; 94 | object-fit: contain; 95 | } 96 | 97 | &__badge { 98 | position: absolute; 99 | font-size: 1em; 100 | bottom: 1em; 101 | left: 1em; 102 | z-index: 1001; 103 | } 104 | } -------------------------------------------------------------------------------- /src/browse/web/components/CampaignCard.tsx: -------------------------------------------------------------------------------- 1 | import "../assets/styles/CampaignCard.scss"; 2 | import { Link } from "react-router"; 3 | import { Card, Stack } from "react-bootstrap"; 4 | import RawDataExtractor from "../utils/RawDataExtractor"; 5 | import { type CampaignWithCounts } from "../../types/Campaign"; 6 | import MediaImage from "./MediaImage"; 7 | 8 | interface CampaignCardProps { 9 | campaign: CampaignWithCounts; 10 | } 11 | 12 | const COUNT_ICONS: Partial> = { 13 | postCount: 'article', 14 | mediaCount: 'image', 15 | productCount: 'storefront' 16 | }; 17 | 18 | function CampaignCard(props: CampaignCardProps) { 19 | const { campaign } = props; 20 | const creationName = RawDataExtractor.getCampaignCreationName(campaign); 21 | 22 | const countElements: React.ReactElement[] = 23 | ['postCount', 'productCount', 'mediaCount'].reduce((result, key) => { 24 | if (campaign[key] > 0) { 25 | result.push(( 26 | 27 | {COUNT_ICONS[key]} 28 | {campaign[key]} 29 | 30 | )); 31 | } 32 | return result; 33 | }, []); 34 | 35 | return ( 36 | 37 | 38 | 42 | 43 |
44 | 45 | {campaign.name} 46 | 47 |
48 | { 49 | creationName ? ( 50 |
51 | {creationName} 52 |
53 | ) : null 54 | } 55 | 59 | {...countElements} 60 | 61 |
62 |
63 |
64 | ) 65 | } 66 | 67 | export default CampaignCard; -------------------------------------------------------------------------------- /docs/api/interfaces/IDownloadTask.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / IDownloadTask 6 | 7 | # Interface: IDownloadTask 8 | 9 | Defined in: [src/downloaders/task/DownloadTask.ts:87](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L87) 10 | 11 | ## Properties 12 | 13 | ### getProgress() 14 | 15 | > **getProgress**: () => `null` \| [`DownloadProgress`](DownloadProgress.md) 16 | 17 | Defined in: [src/downloaders/task/DownloadTask.ts:94](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L94) 18 | 19 | #### Returns 20 | 21 | `null` \| [`DownloadProgress`](DownloadProgress.md) 22 | 23 | *** 24 | 25 | ### id 26 | 27 | > **id**: `number` 28 | 29 | Defined in: [src/downloaders/task/DownloadTask.ts:88](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L88) 30 | 31 | *** 32 | 33 | ### resolvedDestFilename 34 | 35 | > **resolvedDestFilename**: `null` \| `string` 36 | 37 | Defined in: [src/downloaders/task/DownloadTask.ts:92](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L92) 38 | 39 | *** 40 | 41 | ### resolvedDestPath 42 | 43 | > **resolvedDestPath**: `null` \| `string` 44 | 45 | Defined in: [src/downloaders/task/DownloadTask.ts:93](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L93) 46 | 47 | *** 48 | 49 | ### retryCount 50 | 51 | > **retryCount**: `number` 52 | 53 | Defined in: [src/downloaders/task/DownloadTask.ts:91](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L91) 54 | 55 | *** 56 | 57 | ### src 58 | 59 | > **src**: `string` 60 | 61 | Defined in: [src/downloaders/task/DownloadTask.ts:89](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L89) 62 | 63 | *** 64 | 65 | ### srcEntity 66 | 67 | > **srcEntity**: [`Downloadable`](../type-aliases/Downloadable.md) 68 | 69 | Defined in: [src/downloaders/task/DownloadTask.ts:90](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L90) 70 | -------------------------------------------------------------------------------- /src/downloaders/templates/CampaignInfo.ts: -------------------------------------------------------------------------------- 1 | import { type Campaign } from '../../entities/Campaign.js'; 2 | 3 | const CAMPAIGN_INFO_TEMPLATE = 4 | `Campaign 5 | -------- 6 | ID: {campaign.id} 7 | Name: {campaign.name} 8 | Created: {campaign.createdAt} 9 | Published: {campaign.publishedAt} 10 | Summary: {campaign.summary} 11 | URL: {campaign.url} 12 | 13 | Creator 14 | ------- 15 | ID: {campaign.creator.id} 16 | Name: {campaign.creator.fullName} 17 | Created: {campaign.creator.createdAt} 18 | URL: {campaign.creator.url} 19 | `; 20 | 21 | const REWARD_INFO_TEMPLATE = 22 | `Reward: {reward.title} 23 | ------- 24 | ID: {reward.id} 25 | Description: {reward.description} 26 | Amount: {reward.amount} 27 | Created: {reward.createdAt} 28 | Published: {reward.publishedAt} 29 | URL: {reward.url} 30 | 31 | `; 32 | 33 | const COMBINED_TEMPLATE = 34 | `{campaign.info} 35 | {rewards.info} 36 | `; 37 | 38 | export function generateCampaignSummary(campaign: Campaign) { 39 | const campaignInfo = CAMPAIGN_INFO_TEMPLATE 40 | .replaceAll('{campaign.id}', campaign.id) 41 | .replaceAll('{campaign.name}', campaign.name) 42 | .replaceAll('{campaign.createdAt}', campaign.createdAt || '') 43 | .replaceAll('{campaign.publishedAt}', campaign.publishedAt || '') 44 | .replaceAll('{campaign.summary}', campaign.summary || '') 45 | .replaceAll('{campaign.url}', campaign.url || '') 46 | .replaceAll('{campaign.creator.id}', campaign.creator?.id || '') 47 | .replaceAll('{campaign.creator.fullName}', campaign.creator?.fullName || '') 48 | .replaceAll('{campaign.creator.createdAt}', campaign.creator?.createdAt || '') 49 | .replaceAll('{campaign.creator.url}', campaign.creator?.url || ''); 50 | 51 | const rewardSnippets: string[] = []; 52 | for (const reward of campaign.rewards) { 53 | const snippet = REWARD_INFO_TEMPLATE 54 | .replaceAll('{reward.id}', reward.id) 55 | .replaceAll('{reward.title}', reward.title || '') 56 | .replaceAll('{reward.description}', reward.description || '') 57 | .replaceAll('{reward.amount}', reward.amount || '') 58 | .replaceAll('{reward.createdAt}', reward.createdAt || '') 59 | .replaceAll('{reward.publishedAt}', reward.publishedAt || '') 60 | .replaceAll('{reward.url}', reward.url || ''); 61 | rewardSnippets.push(snippet); 62 | } 63 | 64 | return COMBINED_TEMPLATE 65 | .replaceAll('{campaign.info}', campaignInfo) 66 | .replaceAll('{rewards.info}', rewardSnippets.join('')); 67 | } 68 | -------------------------------------------------------------------------------- /src/browse/db/CollectionFTS.ts: -------------------------------------------------------------------------------- 1 | import { type Database } from "better-sqlite3"; 2 | 3 | const COLLECTION_FTS_SOURCE_DELETE_SQL = `DELETE FROM collection_fts_source WHERE collection_id = old.collection_id;`; 4 | const COLLECTION_FTS_SOURCE_INSERT_SQL = ` 5 | INSERT INTO collection_fts_source(collection_id, title, body) 6 | VALUES( 7 | new.collection_id, 8 | json_extract(new.details, '$.title'), 9 | json_extract(new.details, '$.description') 10 | ); 11 | `; 12 | const COLLECTION_FTS_DELETE_SQL = `DELETE FROM collection_fts WHERE rowid = old.fts_rowid;`; 13 | const COLLECTION_FTS_INSERT_SQL = ` 14 | INSERT INTO collection_fts(rowid, title, body) 15 | VALUES ( 16 | new.fts_rowid, 17 | new.title, 18 | new.body 19 | ); 20 | `; 21 | const COLLECTION_FTS_INIT = ` 22 | CREATE TABLE IF NOT EXISTS "collection_fts_source" ( 23 | "fts_rowid" INTEGER, 24 | "collection_id" TEXT NOT NULL, 25 | "title" TEXT, 26 | "body" TEXT, 27 | PRIMARY KEY("fts_rowid"), 28 | FOREIGN KEY("collection_id") REFERENCES "collection"("collection_id") 29 | ); 30 | 31 | CREATE VIRTUAL TABLE IF NOT EXISTS collection_fts USING fts5( 32 | title, 33 | body, 34 | content = 'collection_fts_source', 35 | content_rowid = 'fts_rowid' 36 | ); 37 | 38 | CREATE TRIGGER IF NOT EXISTS collection_ai AFTER INSERT ON collection BEGIN 39 | ${COLLECTION_FTS_SOURCE_INSERT_SQL} 40 | END; 41 | 42 | CREATE TRIGGER IF NOT EXISTS collection_au AFTER UPDATE ON collection BEGIN 43 | ${COLLECTION_FTS_SOURCE_DELETE_SQL} 44 | ${COLLECTION_FTS_SOURCE_INSERT_SQL} 45 | END; 46 | 47 | CREATE TRIGGER IF NOT EXISTS collection_ad AFTER DELETE ON collection BEGIN 48 | ${COLLECTION_FTS_SOURCE_DELETE_SQL} 49 | END; 50 | 51 | CREATE TRIGGER IF NOT EXISTS collection_fts_source_ai AFTER INSERT ON collection_fts_source BEGIN 52 | ${COLLECTION_FTS_INSERT_SQL} 53 | END; 54 | 55 | CREATE TRIGGER IF NOT EXISTS collection_fts_source_bu BEFORE UPDATE ON collection_fts_source BEGIN 56 | ${COLLECTION_FTS_DELETE_SQL} 57 | END; 58 | 59 | CREATE TRIGGER IF NOT EXISTS collection_fts_source_au AFTER UPDATE ON collection_fts_source BEGIN 60 | ${COLLECTION_FTS_INSERT_SQL} 61 | END; 62 | 63 | CREATE TRIGGER IF NOT EXISTS collection_fts_source_bd BEFORE DELETE ON collection_fts_source BEGIN 64 | ${COLLECTION_FTS_DELETE_SQL} 65 | END; 66 | `; 67 | 68 | export function initDBCollectionFTS(db: Database) { 69 | db.exec(COLLECTION_FTS_INIT); 70 | } -------------------------------------------------------------------------------- /src/browse/web/App.tsx: -------------------------------------------------------------------------------- 1 | import "material-icons/iconfont/material-icons.css"; 2 | import "./assets/styles/App.scss"; 3 | import { APIProvider } from "./contexts/APIProvider"; 4 | import { Routes, Route } from 'react-router'; 5 | import CampaignList from "./pages/CampaignList"; 6 | import MainLayout from './layouts/MainLayout'; 7 | import CampaignContent from './pages/CampaignContent'; 8 | import CampaignLayout from './layouts/CampaignLayout'; 9 | import AboutCampaign from './pages/AboutCampaign'; 10 | import CampaignHome from './pages/CampaignHome'; 11 | import PostContent from './pages/PostContent'; 12 | import { BrowseSettingsProvider } from './contexts/BrowseSettingsProvider'; 13 | import Theme from "./components/Theme"; 14 | import { GlobalModalsProvider } from "./contexts/GlobalModalsProvider"; 15 | import CampaignMedia from "./pages/CampaignMedia"; 16 | import ProductContent from "./pages/ProductContent"; 17 | import CollectionList from "./pages/CollectionList"; 18 | import CollectionLayout from "./layouts/CollectionLayout"; 19 | 20 | function App() { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | } > 28 | } /> 29 | } /> 30 | }> 31 | } /> 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | } /> 37 | 38 | } /> 39 | } /> 40 | }> 41 | } /> 42 | 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /docs/api/interfaces/AudioMediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / AudioMediaItem 6 | 7 | # Interface: AudioMediaItem 8 | 9 | Defined in: [src/entities/MediaItem.ts:112](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L112) 10 | 11 | ## Extends 12 | 13 | - [`MediaLike`](MediaLike.md) 14 | 15 | ## Properties 16 | 17 | ### createdAt 18 | 19 | > **createdAt**: `null` \| `string` 20 | 21 | Defined in: [src/entities/MediaItem.ts:114](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L114) 22 | 23 | *** 24 | 25 | ### filename 26 | 27 | > **filename**: `null` \| `string` 28 | 29 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4) 30 | 31 | #### Inherited from 32 | 33 | [`MediaLike`](MediaLike.md).[`filename`](MediaLike.md#filename) 34 | 35 | *** 36 | 37 | ### id 38 | 39 | > **id**: `string` 40 | 41 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3) 42 | 43 | #### Inherited from 44 | 45 | [`MediaLike`](MediaLike.md).[`id`](MediaLike.md#id) 46 | 47 | *** 48 | 49 | ### mimeType 50 | 51 | > **mimeType**: `null` \| `string` 52 | 53 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5) 54 | 55 | #### Inherited from 56 | 57 | [`MediaLike`](MediaLike.md).[`mimeType`](MediaLike.md#mimetype) 58 | 59 | *** 60 | 61 | ### thumbnailURL 62 | 63 | > **thumbnailURL**: `null` \| `string` 64 | 65 | Defined in: [src/entities/MediaItem.ts:116](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L116) 66 | 67 | *** 68 | 69 | ### type 70 | 71 | > **type**: `"audio"` 72 | 73 | Defined in: [src/entities/MediaItem.ts:113](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L113) 74 | 75 | #### Overrides 76 | 77 | [`MediaLike`](MediaLike.md).[`type`](MediaLike.md#type) 78 | 79 | *** 80 | 81 | ### url 82 | 83 | > **url**: `null` \| `string` 84 | 85 | Defined in: [src/entities/MediaItem.ts:115](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L115) 86 | -------------------------------------------------------------------------------- /docs/api/interfaces/SingleImageMediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / SingleImageMediaItem 6 | 7 | # Interface: SingleImageMediaItem 8 | 9 | Defined in: [src/entities/MediaItem.ts:16](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L16) 10 | 11 | ## Extends 12 | 13 | - [`MediaLike`](MediaLike.md) 14 | 15 | ## Properties 16 | 17 | ### filename 18 | 19 | > **filename**: `null` \| `string` 20 | 21 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4) 22 | 23 | #### Inherited from 24 | 25 | [`MediaLike`](MediaLike.md).[`filename`](MediaLike.md#filename) 26 | 27 | *** 28 | 29 | ### id 30 | 31 | > **id**: `string` 32 | 33 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3) 34 | 35 | #### Inherited from 36 | 37 | [`MediaLike`](MediaLike.md).[`id`](MediaLike.md#id) 38 | 39 | *** 40 | 41 | ### imageType 42 | 43 | > **imageType**: `"single"` 44 | 45 | Defined in: [src/entities/MediaItem.ts:18](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L18) 46 | 47 | *** 48 | 49 | ### imageURL 50 | 51 | > **imageURL**: `null` \| `string` 52 | 53 | Defined in: [src/entities/MediaItem.ts:19](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L19) 54 | 55 | *** 56 | 57 | ### mimeType 58 | 59 | > **mimeType**: `null` \| `string` 60 | 61 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5) 62 | 63 | #### Inherited from 64 | 65 | [`MediaLike`](MediaLike.md).[`mimeType`](MediaLike.md#mimetype) 66 | 67 | *** 68 | 69 | ### thumbnailURL 70 | 71 | > **thumbnailURL**: `null` \| `string` 72 | 73 | Defined in: [src/entities/MediaItem.ts:20](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L20) 74 | 75 | *** 76 | 77 | ### type 78 | 79 | > **type**: `"image"` 80 | 81 | Defined in: [src/entities/MediaItem.ts:17](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L17) 82 | 83 | #### Overrides 84 | 85 | [`MediaLike`](MediaLike.md).[`type`](MediaLike.md#type) 86 | -------------------------------------------------------------------------------- /docs/api/interfaces/DownloadProgress.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / DownloadProgress 6 | 7 | # Interface: DownloadProgress 8 | 9 | Defined in: [src/downloaders/task/DownloadTask.ts:41](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L41) 10 | 11 | ## Properties 12 | 13 | ### destFilename 14 | 15 | > **destFilename**: `string` 16 | 17 | Defined in: [src/downloaders/task/DownloadTask.ts:42](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L42) 18 | 19 | *** 20 | 21 | ### destFilePath 22 | 23 | > **destFilePath**: `string` 24 | 25 | Defined in: [src/downloaders/task/DownloadTask.ts:43](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L43) 26 | 27 | *** 28 | 29 | ### length? 30 | 31 | > `optional` **length**: `number` 32 | 33 | Defined in: [src/downloaders/task/DownloadTask.ts:45](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L45) 34 | 35 | *** 36 | 37 | ### lengthDownloaded 38 | 39 | > **lengthDownloaded**: `number` 40 | 41 | Defined in: [src/downloaders/task/DownloadTask.ts:46](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L46) 42 | 43 | *** 44 | 45 | ### lengthUnit 46 | 47 | > **lengthUnit**: `string` 48 | 49 | Defined in: [src/downloaders/task/DownloadTask.ts:44](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L44) 50 | 51 | *** 52 | 53 | ### percent? 54 | 55 | > `optional` **percent**: `number` 56 | 57 | Defined in: [src/downloaders/task/DownloadTask.ts:47](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L47) 58 | 59 | *** 60 | 61 | ### sizeDownloaded 62 | 63 | > **sizeDownloaded**: `number` 64 | 65 | Defined in: [src/downloaders/task/DownloadTask.ts:48](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L48) 66 | 67 | *** 68 | 69 | ### speed 70 | 71 | > **speed**: `number` 72 | 73 | Defined in: [src/downloaders/task/DownloadTask.ts:49](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/downloaders/task/DownloadTask.ts#L49) 74 | -------------------------------------------------------------------------------- /docs/api/interfaces/PostThumbnailMediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / PostThumbnailMediaItem 6 | 7 | # Interface: PostThumbnailMediaItem 8 | 9 | Defined in: [src/entities/MediaItem.ts:63](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L63) 10 | 11 | ## Extends 12 | 13 | - [`MediaLike`](MediaLike.md) 14 | 15 | ## Properties 16 | 17 | ### filename 18 | 19 | > **filename**: `null` \| `string` 20 | 21 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4) 22 | 23 | #### Inherited from 24 | 25 | [`MediaLike`](MediaLike.md).[`filename`](MediaLike.md#filename) 26 | 27 | *** 28 | 29 | ### id 30 | 31 | > **id**: `string` 32 | 33 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3) 34 | 35 | #### Inherited from 36 | 37 | [`MediaLike`](MediaLike.md).[`id`](MediaLike.md#id) 38 | 39 | *** 40 | 41 | ### imageType 42 | 43 | > **imageType**: `"postThumbnail"` 44 | 45 | Defined in: [src/entities/MediaItem.ts:65](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L65) 46 | 47 | *** 48 | 49 | ### imageURLs 50 | 51 | > **imageURLs**: `object` 52 | 53 | Defined in: [src/entities/MediaItem.ts:66](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L66) 54 | 55 | #### default 56 | 57 | > **default**: `null` \| `string` 58 | 59 | #### large 60 | 61 | > **large**: `null` \| `string` 62 | 63 | #### large2 64 | 65 | > **large2**: `null` \| `string` 66 | 67 | #### square 68 | 69 | > **square**: `null` \| `string` 70 | 71 | *** 72 | 73 | ### mimeType 74 | 75 | > **mimeType**: `null` \| `string` 76 | 77 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5) 78 | 79 | #### Inherited from 80 | 81 | [`MediaLike`](MediaLike.md).[`mimeType`](MediaLike.md#mimetype) 82 | 83 | *** 84 | 85 | ### type 86 | 87 | > **type**: `"image"` 88 | 89 | Defined in: [src/entities/MediaItem.ts:64](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L64) 90 | 91 | #### Overrides 92 | 93 | [`MediaLike`](MediaLike.md).[`type`](MediaLike.md#type) 94 | -------------------------------------------------------------------------------- /docs/api/interfaces/CampaignCoverPhotoMediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / CampaignCoverPhotoMediaItem 6 | 7 | # Interface: CampaignCoverPhotoMediaItem 8 | 9 | Defined in: [src/entities/MediaItem.ts:39](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L39) 10 | 11 | ## Extends 12 | 13 | - [`MediaLike`](MediaLike.md) 14 | 15 | ## Properties 16 | 17 | ### filename 18 | 19 | > **filename**: `null` \| `string` 20 | 21 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4) 22 | 23 | #### Inherited from 24 | 25 | [`MediaLike`](MediaLike.md).[`filename`](MediaLike.md#filename) 26 | 27 | *** 28 | 29 | ### id 30 | 31 | > **id**: `string` 32 | 33 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3) 34 | 35 | #### Inherited from 36 | 37 | [`MediaLike`](MediaLike.md).[`id`](MediaLike.md#id) 38 | 39 | *** 40 | 41 | ### imageType 42 | 43 | > **imageType**: `"campaignCoverPhoto"` 44 | 45 | Defined in: [src/entities/MediaItem.ts:41](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L41) 46 | 47 | *** 48 | 49 | ### imageURLs 50 | 51 | > **imageURLs**: `object` 52 | 53 | Defined in: [src/entities/MediaItem.ts:42](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L42) 54 | 55 | #### large 56 | 57 | > **large**: `null` \| `string` 58 | 59 | #### medium 60 | 61 | > **medium**: `null` \| `string` 62 | 63 | #### small 64 | 65 | > **small**: `null` \| `string` 66 | 67 | #### xlarge 68 | 69 | > **xlarge**: `null` \| `string` 70 | 71 | #### xsmall 72 | 73 | > **xsmall**: `null` \| `string` 74 | 75 | *** 76 | 77 | ### mimeType 78 | 79 | > **mimeType**: `null` \| `string` 80 | 81 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5) 82 | 83 | #### Inherited from 84 | 85 | [`MediaLike`](MediaLike.md).[`mimeType`](MediaLike.md#mimetype) 86 | 87 | *** 88 | 89 | ### type 90 | 91 | > **type**: `"image"` 92 | 93 | Defined in: [src/entities/MediaItem.ts:40](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L40) 94 | 95 | #### Overrides 96 | 97 | [`MediaLike`](MediaLike.md).[`type`](MediaLike.md#type) 98 | -------------------------------------------------------------------------------- /docs/api/interfaces/PostCoverImageMediaItem.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / PostCoverImageMediaItem 6 | 7 | # Interface: PostCoverImageMediaItem 8 | 9 | Defined in: [src/entities/MediaItem.ts:51](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L51) 10 | 11 | ## Extends 12 | 13 | - [`MediaLike`](MediaLike.md) 14 | 15 | ## Properties 16 | 17 | ### filename 18 | 19 | > **filename**: `null` \| `string` 20 | 21 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4) 22 | 23 | #### Inherited from 24 | 25 | [`MediaLike`](MediaLike.md).[`filename`](MediaLike.md#filename) 26 | 27 | *** 28 | 29 | ### id 30 | 31 | > **id**: `string` 32 | 33 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3) 34 | 35 | #### Inherited from 36 | 37 | [`MediaLike`](MediaLike.md).[`id`](MediaLike.md#id) 38 | 39 | *** 40 | 41 | ### imageType 42 | 43 | > **imageType**: `"postCoverImage"` 44 | 45 | Defined in: [src/entities/MediaItem.ts:53](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L53) 46 | 47 | *** 48 | 49 | ### imageURLs 50 | 51 | > **imageURLs**: `object` 52 | 53 | Defined in: [src/entities/MediaItem.ts:54](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L54) 54 | 55 | #### default 56 | 57 | > **default**: `null` \| `string` 58 | 59 | #### large 60 | 61 | > **large**: `null` \| `string` 62 | 63 | #### thumb 64 | 65 | > **thumb**: `null` \| `string` 66 | 67 | #### thumbSquare 68 | 69 | > **thumbSquare**: `null` \| `string` 70 | 71 | #### thumbSquareLarge 72 | 73 | > **thumbSquareLarge**: `null` \| `string` 74 | 75 | *** 76 | 77 | ### mimeType 78 | 79 | > **mimeType**: `null` \| `string` 80 | 81 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5) 82 | 83 | #### Inherited from 84 | 85 | [`MediaLike`](MediaLike.md).[`mimeType`](MediaLike.md#mimetype) 86 | 87 | *** 88 | 89 | ### type 90 | 91 | > **type**: `"image"` 92 | 93 | Defined in: [src/entities/MediaItem.ts:52](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L52) 94 | 95 | #### Overrides 96 | 97 | [`MediaLike`](MediaLike.md).[`type`](MediaLike.md#type) 98 | -------------------------------------------------------------------------------- /src/browse/db/Update.ts: -------------------------------------------------------------------------------- 1 | import type Database from 'better-sqlite3'; 2 | import semver from 'semver'; 3 | import path from 'path'; 4 | import dateFormat from 'dateformat'; 5 | import type Logger from '../../utils/logging/Logger.js'; 6 | import { DBUpdater_1_1_0 } from './updaters/DBUpdater_1_1_0.js'; 7 | import { DBUpdater_1_2_0 } from './updaters/DBUpdater_1_2_0.js'; 8 | import { commonLog } from '../../utils/logging/Logger.js'; 9 | 10 | export interface DBUpdater { 11 | targetVersion: string; 12 | update: (db: Database.Database, currentVersion: string, logger?: Logger | null) => Promise; 13 | } 14 | 15 | const updaters: DBUpdater[] = [ 16 | DBUpdater_1_1_0, 17 | DBUpdater_1_2_0 18 | ]; 19 | 20 | function getUpdaterByClosestHigherVersion(currentVersion: string) { 21 | const higher = updaters.filter(up => semver.gt(up.targetVersion, currentVersion)); 22 | if (higher.length === 0) return null; 23 | return higher.sort((a, b) => semver.compare(a.targetVersion, b.targetVersion))[0]; 24 | } 25 | 26 | 27 | export async function updateDB(db: Database.Database, currentVersion: string, logger?: Logger | null, firstRun = true) { 28 | const updater = getUpdaterByClosestHigherVersion(currentVersion); 29 | 30 | if (!updater) { 31 | return; 32 | } 33 | if (firstRun) { 34 | const dbFile = db.name; 35 | const bakFile = path.resolve(path.dirname(dbFile), `db-backup-v${currentVersion}-${dateFormat(new Date(), 'yyyymmdd-HH_MM_ss')}.sqlite`); 36 | commonLog( 37 | logger, 38 | 'info', 39 | 'DB', 40 | `DB needs updating. Current DB will be backed up to "${bakFile}"` 41 | ); 42 | commonLog( 43 | logger, 44 | 'info', 45 | 'DB', 46 | 'Backing up DB...' 47 | ) 48 | await db.backup(bakFile); 49 | } 50 | commonLog( 51 | logger, 52 | 'info', 53 | 'DB', 54 | `Update v${currentVersion} -> v${updater.targetVersion}` 55 | ); 56 | try { 57 | db.exec('BEGIN TRANSACTION;'); 58 | await updater.update(db, currentVersion, logger); 59 | db.prepare(`UPDATE env SET value = ? WHERE env_key = ?`).run( 60 | updater.targetVersion, 61 | 'db_schema_version' 62 | ); 63 | db.exec('COMMIT;'); 64 | } 65 | catch (error: any) { 66 | let rollbackError: any = null; 67 | try { 68 | db.exec('ROLLBACK;'); 69 | } catch (_rollbackError) { 70 | rollbackError = _rollbackError; 71 | } 72 | 73 | throw Error(`Failed to update DB from v${currentVersion} -> v${updater.targetVersion}${ rollbackError ? ' (rollback failed)' : '' }:`, { cause: error }); 74 | } 75 | 76 | return updateDB(db, updater.targetVersion, logger, false); 77 | } -------------------------------------------------------------------------------- /docs/api/interfaces/Reward.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / Reward 6 | 7 | # Interface: Reward 8 | 9 | Defined in: [src/entities/Reward.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L3) 10 | 11 | ## Properties 12 | 13 | ### amount 14 | 15 | > **amount**: `null` \| `string` 16 | 17 | Defined in: [src/entities/Reward.ts:8](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L8) 18 | 19 | *** 20 | 21 | ### createdAt 22 | 23 | > **createdAt**: `null` \| `string` 24 | 25 | Defined in: [src/entities/Reward.ts:9](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L9) 26 | 27 | *** 28 | 29 | ### description 30 | 31 | > **description**: `null` \| `string` 32 | 33 | Defined in: [src/entities/Reward.ts:7](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L7) 34 | 35 | *** 36 | 37 | ### editedAt 38 | 39 | > **editedAt**: `null` \| `string` 40 | 41 | Defined in: [src/entities/Reward.ts:11](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L11) 42 | 43 | *** 44 | 45 | ### id 46 | 47 | > **id**: `string` 48 | 49 | Defined in: [src/entities/Reward.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L5) 50 | 51 | *** 52 | 53 | ### image 54 | 55 | > **image**: `null` \| [`Downloadable`](../type-aliases/Downloadable.md) 56 | 57 | Defined in: [src/entities/Reward.ts:12](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L12) 58 | 59 | *** 60 | 61 | ### publishedAt 62 | 63 | > **publishedAt**: `null` \| `string` 64 | 65 | Defined in: [src/entities/Reward.ts:10](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L10) 66 | 67 | *** 68 | 69 | ### title 70 | 71 | > **title**: `null` \| `string` 72 | 73 | Defined in: [src/entities/Reward.ts:6](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L6) 74 | 75 | *** 76 | 77 | ### type 78 | 79 | > **type**: `"reward"` 80 | 81 | Defined in: [src/entities/Reward.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L4) 82 | 83 | *** 84 | 85 | ### url 86 | 87 | > **url**: `null` \| `string` 88 | 89 | Defined in: [src/entities/Reward.ts:13](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Reward.ts#L13) 90 | -------------------------------------------------------------------------------- /src/browse/types/Content.ts: -------------------------------------------------------------------------------- 1 | import { type Campaign, type Comment, type Post, type Product, type Tier } from "../../entities/index.js"; 2 | import { type PostTag, type Collection } from "../../entities/Post.js"; 3 | 4 | export type ContentListSortBy = 'a-z' | 'z-a' | 'latest' | 'oldest'; 5 | export type ContentType = 'post' | 'product'; 6 | 7 | export interface PostWithComments extends Post { 8 | comments: Comment[] | null; 9 | } 10 | 11 | export type GetContentListParams = 12 | { 13 | campaign?: Campaign | string; 14 | type?: T; 15 | isViewable?: boolean; 16 | datePublished?: string; // 'YYYY' or 'YYYY-mm' (e.g. '2025-06') 17 | limit?: number; 18 | offset?: number; 19 | } & 20 | ( 21 | T extends 'post' ? { 22 | postTypes?: string[]; 23 | tiers?: Tier[] | string[]; 24 | collection?: Collection | string; 25 | tag?: PostTag | string; 26 | } 27 | : T extends 'product' ? {} 28 | : never 29 | ) & ( 30 | { 31 | search: string; 32 | sortBy?: ContentListSortBy | 'best_match'; 33 | } | { 34 | search?: undefined; 35 | sortBy?: ContentListSortBy; 36 | } 37 | ); 38 | 39 | export interface ContentList { 40 | items: ( 41 | T extends 'post' ? PostWithComments 42 | : T extends 'product' ? Product 43 | : PostWithComments | Product 44 | )[]; 45 | total: number; 46 | } 47 | 48 | export type GetContentContext = Omit, 'limit' | 'offset'>; 49 | 50 | export type GetPreviousNextContentResult = 51 | T extends 'post' ? { 52 | previous: PostWithComments | null; 53 | next: PostWithComments | null; 54 | } 55 | : T extends 'product' ? { 56 | previous: Product | null; 57 | next: Product | null; 58 | } 59 | : never; 60 | 61 | export type CollectionListSortBy = 'a-z' | 'z-a' | 'last_created' | 'last_updated'; 62 | 63 | export interface GetCollectionListParams { 64 | campaign: Campaign | string; 65 | search?: string; 66 | sortBy?: CollectionListSortBy; 67 | limit?: number; 68 | offset?: number; 69 | } 70 | 71 | export interface CollectionList { 72 | collections: Collection[]; 73 | total: number; 74 | } 75 | 76 | export interface GetPostTagListParams { 77 | campaign: Campaign | string; 78 | } 79 | 80 | export interface PostTagList { 81 | tags: PostTag[]; 82 | total: number; 83 | } 84 | 85 | export type SearchContentParams = { 86 | campaign?: Campaign | string; 87 | query: string; 88 | } & ({ 89 | type: 'post'; 90 | collection?: Collection; 91 | sortBy: ContentListSortBy | 'best_match'; 92 | } | { 93 | type: 'product'; 94 | sortBy: ContentListSortBy | 'best_match'; 95 | } | { 96 | type: 'collection'; 97 | sortBy: CollectionListSortBy | 'best_match'; 98 | }) -------------------------------------------------------------------------------- /docs/api/interfaces/Collection.md: -------------------------------------------------------------------------------- 1 | [**patreon-dl**](../README.md) 2 | 3 | *** 4 | 5 | [patreon-dl](../README.md) / Collection 6 | 7 | # Interface: Collection 8 | 9 | Defined in: [src/entities/Post.ts:152](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L152) 10 | 11 | ## Properties 12 | 13 | ### createdAt 14 | 15 | > **createdAt**: `null` \| `string` 16 | 17 | Defined in: [src/entities/Post.ts:157](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L157) 18 | 19 | *** 20 | 21 | ### description 22 | 23 | > **description**: `null` \| `string` 24 | 25 | Defined in: [src/entities/Post.ts:156](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L156) 26 | 27 | *** 28 | 29 | ### editedAt 30 | 31 | > **editedAt**: `null` \| `string` 32 | 33 | Defined in: [src/entities/Post.ts:158](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L158) 34 | 35 | *** 36 | 37 | ### id 38 | 39 | > **id**: `string` 40 | 41 | Defined in: [src/entities/Post.ts:153](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L153) 42 | 43 | *** 44 | 45 | ### numPosts 46 | 47 | > **numPosts**: `null` \| `number` 48 | 49 | Defined in: [src/entities/Post.ts:159](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L159) 50 | 51 | *** 52 | 53 | ### postIds 54 | 55 | > **postIds**: `null` \| `string`[] 56 | 57 | Defined in: [src/entities/Post.ts:160](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L160) 58 | 59 | *** 60 | 61 | ### raw 62 | 63 | > **raw**: `object` 64 | 65 | Defined in: [src/entities/Post.ts:162](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L162) 66 | 67 | *** 68 | 69 | ### thumbnail 70 | 71 | > **thumbnail**: `null` \| [`Downloadable`](../type-aliases/Downloadable.md)\<[`CollectionThumbnailMediaItem`](CollectionThumbnailMediaItem.md)\> 72 | 73 | Defined in: [src/entities/Post.ts:161](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L161) 74 | 75 | *** 76 | 77 | ### title 78 | 79 | > **title**: `null` \| `string` 80 | 81 | Defined in: [src/entities/Post.ts:155](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L155) 82 | 83 | *** 84 | 85 | ### type 86 | 87 | > **type**: `"collection"` 88 | 89 | Defined in: [src/entities/Post.ts:154](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/Post.ts#L154) 90 | --------------------------------------------------------------------------------