patreon-dl 3.7.1 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +15 -0
  2. package/dist/browse/api/CampaignAPIMixin.d.ts +1 -1
  3. package/dist/browse/api/ContentAPIMixin.d.ts +3 -3
  4. package/dist/browse/api/FilterAPIMixin.d.ts +1 -1
  5. package/dist/browse/api/index.d.ts +10 -10
  6. package/dist/browse/db/CampaignDBMixin.d.ts +3 -3
  7. package/dist/browse/db/ContentDBMixin.d.ts +13 -13
  8. package/dist/browse/db/ContentDBMixin.js +1 -1
  9. package/dist/browse/db/ContentDBMixin.js.map +1 -1
  10. package/dist/browse/db/MediaDBMixin.js +1 -1
  11. package/dist/browse/db/MediaDBMixin.js.map +1 -1
  12. package/dist/browse/db/index.d.ts +16 -16
  13. package/dist/browse/web/assets/index-CtZbx-Du.js +218 -0
  14. package/dist/browse/web/index.html +1 -1
  15. package/dist/cli/CommandLineParser.d.ts +4 -0
  16. package/dist/cli/CommandLineParser.js +65 -32
  17. package/dist/cli/CommandLineParser.js.map +1 -1
  18. package/dist/cli/helper/PostList.d.ts +6 -0
  19. package/dist/cli/helper/PostList.js +106 -0
  20. package/dist/cli/helper/PostList.js.map +1 -0
  21. package/dist/cli/index.js +73 -53
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/downloaders/Bootstrap.d.ts +5 -0
  24. package/dist/downloaders/Bootstrap.js +10 -0
  25. package/dist/downloaders/Bootstrap.js.map +1 -1
  26. package/dist/downloaders/Downloader.d.ts +4 -4
  27. package/dist/downloaders/Downloader.js +6 -59
  28. package/dist/downloaders/Downloader.js.map +1 -1
  29. package/dist/downloaders/IncludeCriteriaHelper.d.ts +25 -0
  30. package/dist/downloaders/IncludeCriteriaHelper.js +160 -0
  31. package/dist/downloaders/IncludeCriteriaHelper.js.map +1 -0
  32. package/dist/downloaders/InitialData.js +20 -6
  33. package/dist/downloaders/InitialData.js.map +1 -1
  34. package/dist/downloaders/PostDownloader.js +67 -103
  35. package/dist/downloaders/PostDownloader.js.map +1 -1
  36. package/dist/downloaders/PostsFetcher.js +7 -5
  37. package/dist/downloaders/PostsFetcher.js.map +1 -1
  38. package/dist/downloaders/ProductDownloader.js +31 -21
  39. package/dist/downloaders/ProductDownloader.js.map +1 -1
  40. package/dist/downloaders/task/YouTubeDownloadTask.js +1 -3
  41. package/dist/downloaders/task/YouTubeDownloadTask.js.map +1 -1
  42. package/dist/parsers/PageParser.js +7 -2
  43. package/dist/parsers/PageParser.js.map +1 -1
  44. package/dist/parsers/PostParser.d.ts +4 -1
  45. package/dist/parsers/PostParser.js +54 -5
  46. package/dist/parsers/PostParser.js.map +1 -1
  47. package/dist/utils/Fetcher.d.ts +1 -0
  48. package/dist/utils/Fetcher.js +5 -0
  49. package/dist/utils/Fetcher.js.map +1 -1
  50. package/dist/utils/URLHelper.d.ts +4 -0
  51. package/dist/utils/URLHelper.js +17 -0
  52. package/dist/utils/URLHelper.js.map +1 -1
  53. package/dist/utils/YouTubeCredentialsCapturer.js +1 -1
  54. package/dist/utils/YouTubeCredentialsCapturer.js.map +1 -1
  55. package/dist/utils/logging/ConsoleLogger.js +5 -0
  56. package/dist/utils/logging/ConsoleLogger.js.map +1 -1
  57. package/dist/utils/yt/InnertubeLoader.d.ts +4 -6
  58. package/dist/utils/yt/InnertubeLoader.js +14 -14
  59. package/dist/utils/yt/InnertubeLoader.js.map +1 -1
  60. package/dist/utils/yt/PoToken.js +53 -27
  61. package/dist/utils/yt/PoToken.js.map +1 -1
  62. package/package.json +12 -8
  63. package/dist/browse/web/assets/index-B1h15ViZ.js +0 -218
@@ -0,0 +1,25 @@
1
+ import type { Post, Product } from "../entities/index.js";
2
+ import { type LogLevel } from "../utils/logging/Logger.js";
3
+ import type Logger from "../utils/logging/Logger.js";
4
+ import { type DownloaderConfig } from "./Downloader.js";
5
+ export type IncludeCriteriaCheckPostResult = {
6
+ ok: true;
7
+ } | {
8
+ ok: false;
9
+ reason: 'unviewable' | 'unmetMediaTypeCriteria' | 'notInTier' | 'publishDateOutOfRange';
10
+ };
11
+ export type IncludeCriteriaCheckProductResult = {
12
+ ok: true;
13
+ } | {
14
+ ok: false;
15
+ reason: 'unviewable' | 'publishDateOutOfRange';
16
+ };
17
+ export declare class IncludeCriteriaHelper {
18
+ name: 'IncludeCriteriaHelper';
19
+ protected logger?: Logger | null;
20
+ constructor(logger?: Logger | null);
21
+ checkPost(post: Post, config: DownloaderConfig<Post>): IncludeCriteriaCheckPostResult;
22
+ checkProduct(product: Product, config: DownloaderConfig<Product>): IncludeCriteriaCheckProductResult;
23
+ protected isPublishDateOutOfRange<T extends Post | Product>(entity: T, config: DownloaderConfig<T>): boolean;
24
+ protected log(level: LogLevel, ...msg: any[]): void;
25
+ }
@@ -0,0 +1,160 @@
1
+ import { isYouTubeEmbed } from "../entities/Downloadable.js";
2
+ import { commonLog } from "../utils/logging/Logger.js";
3
+ export class IncludeCriteriaHelper {
4
+ constructor(logger) {
5
+ this.logger = logger;
6
+ }
7
+ checkPost(post, config) {
8
+ // -- 1. Viewability
9
+ if (!post.isViewable && !config.include.lockedContent) {
10
+ return {
11
+ ok: false,
12
+ reason: 'unviewable'
13
+ };
14
+ }
15
+ // -- 2. Config option 'include.postsWithMediaType'
16
+ const postsWithMediaType = config.include.postsWithMediaType;
17
+ if (postsWithMediaType !== 'any') {
18
+ const hasAttachments = post.attachments.length > 0;
19
+ const hasAudio = !!post.audio || !!post.audioPreview;
20
+ const hasImages = post.images.length > 0;
21
+ const hasVideo = !!post.video || !!post.videoPreview || !!(post.embed && (post.embed.type === 'videoEmbed' || isYouTubeEmbed(post.embed)));
22
+ const isPodcast = post.postType === 'podcast';
23
+ let skip = false;
24
+ if (postsWithMediaType === 'none') {
25
+ skip = hasAttachments || hasAudio || hasImages || hasVideo;
26
+ }
27
+ else if (Array.isArray(postsWithMediaType)) {
28
+ skip = !((postsWithMediaType.includes('attachment') && hasAttachments) ||
29
+ (postsWithMediaType.includes('audio') && hasAudio) ||
30
+ (postsWithMediaType.includes('image') && hasImages) ||
31
+ (postsWithMediaType.includes('video') && hasVideo) ||
32
+ (postsWithMediaType.includes('podcast') && isPodcast && (hasAudio || hasVideo)));
33
+ }
34
+ if (skip) {
35
+ this.log('debug', 'Match criteria failed:', `config.include.postsWithMediaType: ${JSON.stringify(postsWithMediaType)} <-> post #${post.id}:`, {
36
+ hasAttachments,
37
+ hasAudio,
38
+ hasImages,
39
+ hasVideo
40
+ });
41
+ return {
42
+ ok: false,
43
+ reason: 'unmetMediaTypeCriteria'
44
+ };
45
+ }
46
+ }
47
+ // -- 3. Config option 'include.postsInTier'
48
+ const postsInTier = config.include.postsInTier;
49
+ const isAnyTier = postsInTier === 'any' || postsInTier.includes('any');
50
+ if (!isAnyTier) {
51
+ const applicableTierIds = postsInTier.filter((id) => post.campaign?.rewards.find((r) => r.id === id));
52
+ if (!post.campaign) {
53
+ this.log('warn', 'config.include.postsInTier: ignored - post missing campaign info');
54
+ }
55
+ else {
56
+ this.log('debug', 'config.include.postsInTier: applicable tier IDs:', applicableTierIds);
57
+ }
58
+ let skip = false;
59
+ if (!post.campaign) {
60
+ skip = false;
61
+ }
62
+ else if (applicableTierIds.length === 0) {
63
+ skip = true;
64
+ }
65
+ else if (!post.tiers.find((tier) => tier.id === 'patrons')) {
66
+ skip = applicableTierIds.every((id) => !post.tiers.find((tier) => tier.id === id));
67
+ }
68
+ if (skip) {
69
+ this.log('debug', 'Match criteria failed:', `config.include.postsInTier: ${JSON.stringify(applicableTierIds)} <-> post #${post.id}:`, post.tiers);
70
+ return {
71
+ ok: false,
72
+ reason: 'notInTier'
73
+ };
74
+ }
75
+ if (post.campaign) {
76
+ this.log('debug', 'Match criteria OK:', `config.include.postsInTier: ${JSON.stringify(applicableTierIds)} <-> post #${post.id}:`, post.tiers);
77
+ }
78
+ }
79
+ // -- 4. Config option 'include.postsPublished'
80
+ if (this.isPublishDateOutOfRange(post, config)) {
81
+ return {
82
+ ok: false,
83
+ reason: 'publishDateOutOfRange'
84
+ };
85
+ }
86
+ return {
87
+ ok: true
88
+ };
89
+ }
90
+ checkProduct(product, config) {
91
+ // -- 1. Viewability
92
+ if (!product.isAccessible && !config.include.lockedContent) {
93
+ return {
94
+ ok: false,
95
+ reason: 'unviewable'
96
+ };
97
+ }
98
+ // -- 2. Config option 'include.productsPublished'
99
+ if (this.isPublishDateOutOfRange(product, config)) {
100
+ return {
101
+ ok: false,
102
+ reason: 'publishDateOutOfRange'
103
+ };
104
+ }
105
+ return {
106
+ ok: true
107
+ };
108
+ }
109
+ isPublishDateOutOfRange(entity, config) {
110
+ const publishedAfter = entity.type === 'post' ? config.include.postsPublished.after : config.include.productsPublished.after;
111
+ const publishedBefore = entity.type === 'post' ? config.include.postsPublished.before : config.include.productsPublished.before;
112
+ if (publishedAfter || publishedBefore) {
113
+ const targetPublishedAt = entity.publishedAt;
114
+ let parsedPublishedAt = null;
115
+ if (!targetPublishedAt) {
116
+ this.log('warn', `config.include.productsPublished: ignored - ${entity.type} #${entity.id} missing publish date`);
117
+ }
118
+ else {
119
+ try {
120
+ parsedPublishedAt = new Date(targetPublishedAt);
121
+ }
122
+ catch (error) {
123
+ this.log('error', `Failed to parse publish date of ${entity.type} #${entity.id} ("${targetPublishedAt}"): `, error);
124
+ this.log('warn', `config.include.productsPublished: ignored - publish date of ${entity.type} #${entity.id} could not be parsed`);
125
+ }
126
+ }
127
+ let skip = false;
128
+ if (parsedPublishedAt) {
129
+ const isAfter = publishedAfter ? parsedPublishedAt.getTime() >= publishedAfter.valueOf().getTime() : true;
130
+ const isBefore = publishedBefore ? parsedPublishedAt.getTime() < publishedBefore.valueOf().getTime() : true;
131
+ skip = !isAfter || !isBefore;
132
+ let eq = null;
133
+ if (publishedAfter && publishedBefore) {
134
+ eq = `${publishedAfter.toString()} <= *${targetPublishedAt}* < ${publishedBefore.toString()}`;
135
+ }
136
+ else if (publishedAfter) {
137
+ eq = `${publishedAfter.toString()} <= *${targetPublishedAt}*`;
138
+ }
139
+ else if (publishedBefore) {
140
+ eq = `*${targetPublishedAt}* < ${publishedBefore.toString()}`;
141
+ }
142
+ if (eq) {
143
+ if (skip) {
144
+ // this.log('warn', `Skipped downloading ${entity.type} #${entity.id}: publish date out of range`);
145
+ this.log('debug', `Publish date test failed for ${entity.type} #${entity.id}: ${eq}`);
146
+ }
147
+ else {
148
+ this.log('debug', `Publish date test OK for ${entity.type} #${entity.id}: ${eq}`);
149
+ }
150
+ }
151
+ return skip;
152
+ }
153
+ }
154
+ return false;
155
+ }
156
+ log(level, ...msg) {
157
+ commonLog(this.logger, level, this.name, ...msg);
158
+ }
159
+ }
160
+ //# sourceMappingURL=IncludeCriteriaHelper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IncludeCriteriaHelper.js","sourceRoot":"","sources":["../../src/downloaders/IncludeCriteriaHelper.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Post, Product } from \"../entities/index.js\";\nimport { isYouTubeEmbed } from \"../entities/Downloadable.js\";\nimport { type LogLevel } from \"../utils/logging/Logger.js\";\nimport type Logger from \"../utils/logging/Logger.js\";\nimport { commonLog } from \"../utils/logging/Logger.js\";\nimport { type DownloaderConfig } from \"./Downloader.js\";\n\nexport type IncludeCriteriaCheckPostResult = {\n ok: true;\n} | {\n ok: false;\n reason: 'unviewable' | 'unmetMediaTypeCriteria' | 'notInTier' | 'publishDateOutOfRange';\n};\n\nexport type IncludeCriteriaCheckProductResult = {\n ok: true;\n} | {\n ok: false;\n reason: 'unviewable' | 'publishDateOutOfRange';\n};\n\n\nexport class IncludeCriteriaHelper {\n name: 'IncludeCriteriaHelper';\n\n protected logger?: Logger | null;\n\n constructor(logger?: Logger | null) {\n this.logger = logger;\n }\n\n checkPost(post: Post, config: DownloaderConfig<Post>): IncludeCriteriaCheckPostResult {\n // -- 1. Viewability\n if (!post.isViewable && !config.include.lockedContent) {\n return {\n ok: false,\n reason: 'unviewable'\n };\n }\n\n // -- 2. Config option 'include.postsWithMediaType'\n const postsWithMediaType = config.include.postsWithMediaType;\n if (postsWithMediaType !== 'any') {\n const hasAttachments = post.attachments.length > 0;\n const hasAudio = !!post.audio || !!post.audioPreview;\n const hasImages = post.images.length > 0;\n const hasVideo = !!post.video || !!post.videoPreview || !!(post.embed && (post.embed.type === 'videoEmbed' || isYouTubeEmbed(post.embed)));\n const isPodcast = post.postType === 'podcast'\n\n let skip = false;\n if (postsWithMediaType === 'none') {\n skip = hasAttachments || hasAudio || hasImages || hasVideo;\n }\n else if (Array.isArray(postsWithMediaType)) {\n skip = !(\n (postsWithMediaType.includes('attachment') && hasAttachments) ||\n (postsWithMediaType.includes('audio') && hasAudio) ||\n (postsWithMediaType.includes('image') && hasImages) ||\n (postsWithMediaType.includes('video') && hasVideo) ||\n (postsWithMediaType.includes('podcast') && isPodcast && (hasAudio || hasVideo)));\n }\n\n if (skip) {\n this.log('debug', 'Match criteria failed:', `config.include.postsWithMediaType: ${JSON.stringify(postsWithMediaType)} <-> post #${post.id}:`, {\n hasAttachments,\n hasAudio,\n hasImages,\n hasVideo\n });\n return {\n ok: false,\n reason: 'unmetMediaTypeCriteria'\n };\n }\n }\n\n // -- 3. Config option 'include.postsInTier'\n const postsInTier = config.include.postsInTier;\n const isAnyTier = postsInTier === 'any' || postsInTier.includes('any');\n if (!isAnyTier) {\n const applicableTierIds = postsInTier.filter((id) => post.campaign?.rewards.find((r) => r.id === id));\n if (!post.campaign) {\n this.log('warn', 'config.include.postsInTier: ignored - post missing campaign info');\n }\n else {\n this.log('debug', 'config.include.postsInTier: applicable tier IDs:', applicableTierIds);\n }\n let skip = false;\n if (!post.campaign) {\n skip = false;\n }\n else if (applicableTierIds.length === 0) {\n skip = true;\n }\n else if (!post.tiers.find((tier) => tier.id === 'patrons')) {\n skip = applicableTierIds.every((id) => !post.tiers.find((tier) => tier.id === id));\n }\n if (skip) {\n this.log('debug', 'Match criteria failed:', `config.include.postsInTier: ${JSON.stringify(applicableTierIds)} <-> post #${post.id}:`, post.tiers);\n return {\n ok: false,\n reason: 'notInTier'\n };\n }\n if (post.campaign) {\n this.log('debug', 'Match criteria OK:', `config.include.postsInTier: ${JSON.stringify(applicableTierIds)} <-> post #${post.id}:`, post.tiers);\n }\n }\n\n // -- 4. Config option 'include.postsPublished'\n if (this.isPublishDateOutOfRange(post, config)) {\n return {\n ok: false,\n reason: 'publishDateOutOfRange'\n };\n }\n\n return {\n ok: true\n };\n }\n\n checkProduct(product: Product, config: DownloaderConfig<Product>): IncludeCriteriaCheckProductResult {\n // -- 1. Viewability\n if (!product.isAccessible && !config.include.lockedContent) {\n return {\n ok: false,\n reason: 'unviewable'\n };\n }\n\n // -- 2. Config option 'include.productsPublished'\n if (this.isPublishDateOutOfRange(product, config)) {\n return {\n ok: false,\n reason: 'publishDateOutOfRange'\n };\n }\n\n return {\n ok: true\n };\n }\n\n protected isPublishDateOutOfRange<T extends Post | Product>(\n entity: T,\n config: DownloaderConfig<T>\n ) {\n const publishedAfter = entity.type === 'post' ? config.include.postsPublished.after : config.include.productsPublished.after;\n const publishedBefore = entity.type === 'post' ? config.include.postsPublished.before : config.include.productsPublished.before;\n if (publishedAfter || publishedBefore) {\n const targetPublishedAt = entity.publishedAt;\n let parsedPublishedAt: Date | null = null;\n if (!targetPublishedAt) {\n this.log('warn', `config.include.productsPublished: ignored - ${entity.type} #${entity.id} missing publish date`);\n }\n else {\n try {\n parsedPublishedAt = new Date(targetPublishedAt);\n }\n catch (error: any) {\n this.log('error', `Failed to parse publish date of ${entity.type} #${entity.id} (\"${targetPublishedAt}\"): `, error);\n this.log('warn', `config.include.productsPublished: ignored - publish date of ${entity.type} #${entity.id} could not be parsed`);\n }\n }\n let skip = false;\n if (parsedPublishedAt) {\n const isAfter = publishedAfter ? parsedPublishedAt.getTime() >= publishedAfter.valueOf().getTime() : true;\n const isBefore = publishedBefore ? parsedPublishedAt.getTime() < publishedBefore.valueOf().getTime() : true;\n skip = !isAfter || !isBefore;\n let eq: string | null = null;\n if (publishedAfter && publishedBefore) {\n eq = `${publishedAfter.toString()} <= *${targetPublishedAt}* < ${publishedBefore.toString()}`;\n }\n else if (publishedAfter) {\n eq = `${publishedAfter.toString()} <= *${targetPublishedAt}*`;\n }\n else if (publishedBefore) {\n eq = `*${targetPublishedAt}* < ${publishedBefore.toString()}`;\n }\n if (eq) {\n if (skip) {\n // this.log('warn', `Skipped downloading ${entity.type} #${entity.id}: publish date out of range`);\n this.log('debug', `Publish date test failed for ${entity.type} #${entity.id}: ${eq}`);\n }\n else {\n this.log('debug', `Publish date test OK for ${entity.type} #${entity.id}: ${eq}`);\n }\n }\n return skip;\n }\n }\n return false;\n }\n\n protected log(level: LogLevel, ...msg: any[]) {\n commonLog(this.logger, level, this.name, ...msg);\n }\n}"]}
@@ -37,15 +37,29 @@ export class InitialData {
37
37
  this.log('debug', `Fetch initial data from "${url}"`);
38
38
  let page;
39
39
  try {
40
- const { html, lastUrl } = await this.fetcher.get({ url, type: 'html', maxRetries: this.config.request.maxRetries, signal });
41
- page = html;
42
- if (new URL(lastUrl).pathname === '/login-sync-domains') {
43
- this.log('debug', `Detected Cloudflare challenge flow at "${lastUrl}"`);
40
+ // For custom domains (non-patreon.com), skip the cookie-bearing fetch
41
+ // sending patreon session cookies to a third-party domain can invalidate
42
+ // the session. Use Puppeteer directly instead.
43
+ const isCustomDomain = !URLHelper.isPatreonURL(url);
44
+ if (isCustomDomain) {
45
+ this.log('debug', `Custom domain detected - using Puppeteer to avoid sending cookies to third-party domain`);
44
46
  page = await __classPrivateFieldGet(this, _InitialData_instances, "m", _InitialData_fetchPageWithPuppeteer).call(this, url);
45
- // Because cookie not available to Puppeteer, we need to fetch
46
- // current user ID separately
47
47
  fetchCurrentUserIdFromAPI = !!this.config.cookie;
48
48
  }
49
+ else {
50
+ const { html, lastUrl } = await this.fetcher.get({ url, type: 'html', maxRetries: this.config.request.maxRetries, signal });
51
+ page = html;
52
+ const isCloudflareChallenge = new URL(lastUrl).pathname === '/login-sync-domains' ||
53
+ html.includes('window._cf_chl_opt') ||
54
+ html.includes('challenge-platform');
55
+ if (isCloudflareChallenge) {
56
+ this.log('debug', `Detected Cloudflare challenge flow - using Puppeteer`);
57
+ page = await __classPrivateFieldGet(this, _InitialData_instances, "m", _InitialData_fetchPageWithPuppeteer).call(this, url);
58
+ // Because cookie not available to Puppeteer, we need to fetch
59
+ // current user ID separately
60
+ fetchCurrentUserIdFromAPI = !!this.config.cookie;
61
+ }
62
+ }
49
63
  }
50
64
  catch (error) {
51
65
  if (signal?.aborted) {
@@ -1 +1 @@
1
- {"version":3,"file":"InitialData.js","sourceRoot":"","sources":["../../src/downloaders/InitialData.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,UAAU,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAG9C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,YAAY,MAAM,0BAA0B,CAAC;AAGpD,MAAM,OAAO,WAAW;IAOtB,YAAY,MAA6B,EAAE,OAAgB,EAAE,MAAsB;;QANnF,SAAI,GAAG,aAAa,CAAC;QAOnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,MAAoB;QACzC,IAAI,UAAU,GAAkB,IAAI,CAAC;QACrC,IAAI,aAAa,GAAuB,SAAS,CAAC;QAClD,IAAI,yBAAyB,GAAG,KAAK,CAAC;QACtC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM;YAC7B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,KAAK,QAAQ;YACvC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,KAAK,QAAQ;YACvC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,EAChC,CAAC;YACD,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;YAC9C,yBAAyB,GAAG,IAAI,CAAC;QACnC,CAAC;aACI,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS;YACrC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,KAAK,QAAQ;YAC1C,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,EACnC,CAAC;YACD,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC;YACjD,yBAAyB,GAAG,IAAI,CAAC;QACnC,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,4BAA4B,GAAG,GAAG,CAAC,CAAC;YACtD,IAAI,IAAI,CAAC;YACT,IAAI,CAAC;gBACH,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;gBAC5H,IAAI,GAAG,IAAI,CAAC;gBACZ,IAAI,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,KAAK,qBAAqB,EAAE,CAAC;oBACxD,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,0CAA0C,OAAO,GAAG,CAAC,CAAC;oBACxE,IAAI,GAAG,MAAM,uBAAA,IAAI,mEAAwB,MAA5B,IAAI,EAAyB,GAAG,CAAC,CAAC;oBAC/C,+DAA+D;oBAC/D,6BAA6B;oBAC7B,yBAAyB,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;gBACnD,CAAC;YACH,CAAC;YACD,OAAO,KAAK,EAAE,CAAC;gBACb,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;oBACpB,MAAM,KAAK,CAAC;gBACd,CAAC;gBACD,MAAM,CAAC,GAAG,KAAK,CAAC,qBAAqB,GAAG,GAAG,CAAC,CAAC;gBAC7C,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC;gBAChB,MAAM,CAAC,CAAC;YACV,CAAC;YACD,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC/C,IAAI,WAAW,CAAC;YAChB,IAAI,CAAC;gBACH,WAAW,GAAG,UAAU,CAAC,gBAAgB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACvD,CAAC;YACD,OAAO,KAAK,EAAE,CAAC;gBACb,MAAM,CAAC,GAAG,KAAK,CAAC,oCAAoC,GAAG,GAAG,CAAC,CAAC;gBAC5D,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC;gBAChB,MAAM,CAAC,CAAC;YACV,CAAC;YACD,UAAU,GAAG,YAAY,CAAC,WAAW,CAAC,WAAW,EAAE,gCAAgC,CAAC,CAAC;YACrF,aAAa,GAAG,YAAY,CAAC,WAAW,CAAC,WAAW,EAAE,qCAAqC,CAAC,CAAC;YAC7F,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,KAAK,CAAC,6CAA6C,GAAG,GAAG,CAAC,CAAC;YACnE,CAAC;QACH,CAAC;QACD,IAAI,yBAAyB,EAAE,CAAC;YAC9B,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,gCAAgC,CAAC,CAAC;YACpD,MAAM,iBAAiB,GAAG,SAAS,CAAC,0BAA0B,EAAE,CAAC;YACjE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;gBACvD,GAAG,EAAE,iBAAiB;gBACtB,IAAI,EAAE,MAAM;gBACZ,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU;gBAC1C,MAAM;aACP,CAAC,CAAC;YACH,aAAa,GAAG,YAAY,CAAC,WAAW,CAAC,eAAe,EAAE,SAAS,CAAC,IAAI,SAAS,CAAC;QACpF,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,8BAA8B,UAAU,uBAAuB,aAAa,GAAG,CAAC,CAAC;QACnG,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC;IACvC,CAAC;IAMS,GAAG,CAAC,KAAe,EAAE,GAAG,GAAU;QAC1C,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,CAAC;IACnD,CAAC;CACF;8EAPC,KAAK,8CAAyB,GAAW;IACvC,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;AAChD,CAAC","sourcesContent":["import PageParser from \"../parsers/PageParser.js\";\nimport { URLHelper } from \"../utils/index.js\";\nimport type Fetcher from \"../utils/Fetcher.js\";\nimport { type Logger, type LogLevel } from \"../utils/logging/index.js\";\nimport { commonLog } from \"../utils/logging/Logger.js\";\nimport ObjectHelper from \"../utils/ObjectHelper.js\";\nimport { type DownloaderConfig } from \"./Downloader.js\";\n\nexport class InitialData {\n name = 'InitialData';\n\n protected config: DownloaderConfig<any>;\n protected fetcher: Fetcher;\n protected logger?: Logger | null;\n\n constructor(config: DownloaderConfig<any>, fetcher: Fetcher, logger?: Logger | null) {\n this.config = config;\n this.fetcher = fetcher;\n this.logger = logger;\n }\n\n async get(url: string, signal?: AbortSignal) {\n let campaignId: string | null = null;\n let currentUserId: string | undefined = undefined;\n let fetchCurrentUserIdFromAPI = false;\n if (this.config.type === 'post' &&\n this.config.postFetch.type !== 'single' &&\n this.config.postFetch.type !== 'byFile' &&\n this.config.postFetch.campaignId\n ) {\n campaignId = this.config.postFetch.campaignId;\n fetchCurrentUserIdFromAPI = true;\n }\n else if (this.config.type === 'product' &&\n this.config.productFetch.type === 'byShop' &&\n this.config.productFetch.campaignId\n ) {\n campaignId = this.config.productFetch.campaignId;\n fetchCurrentUserIdFromAPI = true;\n }\n if (!campaignId) {\n this.log('debug', `Fetch initial data from \"${url}\"`);\n let page;\n try {\n const { html, lastUrl } = await this.fetcher.get({ url, type: 'html', maxRetries: this.config.request.maxRetries, signal });\n page = html;\n if (new URL(lastUrl).pathname === '/login-sync-domains') {\n this.log('debug', `Detected Cloudflare challenge flow at \"${lastUrl}\"`);\n page = await this.#fetchPageWithPuppeteer(url);\n // Because cookie not available to Puppeteer, we need to fetch \n // current user ID separately\n fetchCurrentUserIdFromAPI = !!this.config.cookie;\n }\n }\n catch (error) {\n if (signal?.aborted) {\n throw error;\n }\n const e = Error(`Error requesting \"${url}\"`);\n e.cause = error;\n throw e;\n }\n const pageParser = new PageParser(this.logger);\n let initialData;\n try {\n initialData = pageParser.parseInitialData(page, url);\n }\n catch (error) {\n const e = Error(`Error parsing initial data from \"${url}\"`);\n e.cause = error;\n throw e;\n }\n campaignId = ObjectHelper.getProperty(initialData, 'pageBootstrap.campaign.data.id');\n currentUserId = ObjectHelper.getProperty(initialData, 'commonBootstrap.currentUser.data.id');\n if (!campaignId) {\n throw Error(`Campaign ID not found in initial data of \"${url}\"`);\n }\n }\n if (fetchCurrentUserIdFromAPI) {\n this.log('debug', `Fetch current user ID from API`);\n const currentUserAPIURL = URLHelper.constructCurrentUserAPIURL();\n const { json: currentUserJson } = await this.fetcher.get({\n url: currentUserAPIURL,\n type: 'json',\n maxRetries: this.config.request.maxRetries,\n signal\n });\n currentUserId = ObjectHelper.getProperty(currentUserJson, 'data.id') || undefined;\n }\n this.log('debug', `Initial data: campaign ID '${campaignId}'; current user ID '${currentUserId}'`);\n return { campaignId, currentUserId };\n }\n\n async #fetchPageWithPuppeteer(url: string) {\n return this.fetcher.getPageWithPuppeteer(url);\n }\n\n protected log(level: LogLevel, ...msg: any[]) {\n commonLog(this.logger, level, this.name, ...msg);\n }\n}"]}
1
+ {"version":3,"file":"InitialData.js","sourceRoot":"","sources":["../../src/downloaders/InitialData.ts"],"names":[],"mappings":"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","sourcesContent":["import PageParser from \"../parsers/PageParser.js\";\nimport { URLHelper } from \"../utils/index.js\";\nimport type Fetcher from \"../utils/Fetcher.js\";\nimport { type Logger, type LogLevel } from \"../utils/logging/index.js\";\nimport { commonLog } from \"../utils/logging/Logger.js\";\nimport ObjectHelper from \"../utils/ObjectHelper.js\";\nimport { type DownloaderConfig } from \"./Downloader.js\";\n\nexport class InitialData {\n name = 'InitialData';\n\n protected config: DownloaderConfig<any>;\n protected fetcher: Fetcher;\n protected logger?: Logger | null;\n\n constructor(config: DownloaderConfig<any>, fetcher: Fetcher, logger?: Logger | null) {\n this.config = config;\n this.fetcher = fetcher;\n this.logger = logger;\n }\n\n async get(url: string, signal?: AbortSignal) {\n let campaignId: string | null = null;\n let currentUserId: string | undefined = undefined;\n let fetchCurrentUserIdFromAPI = false;\n if (this.config.type === 'post' &&\n this.config.postFetch.type !== 'single' &&\n this.config.postFetch.type !== 'byFile' &&\n this.config.postFetch.campaignId\n ) {\n campaignId = this.config.postFetch.campaignId;\n fetchCurrentUserIdFromAPI = true;\n }\n else if (this.config.type === 'product' &&\n this.config.productFetch.type === 'byShop' &&\n this.config.productFetch.campaignId\n ) {\n campaignId = this.config.productFetch.campaignId;\n fetchCurrentUserIdFromAPI = true;\n }\n if (!campaignId) {\n this.log('debug', `Fetch initial data from \"${url}\"`);\n let page;\n try {\n // For custom domains (non-patreon.com), skip the cookie-bearing fetch —\n // sending patreon session cookies to a third-party domain can invalidate\n // the session. Use Puppeteer directly instead.\n const isCustomDomain = !URLHelper.isPatreonURL(url);\n if (isCustomDomain) {\n this.log('debug', `Custom domain detected - using Puppeteer to avoid sending cookies to third-party domain`);\n page = await this.#fetchPageWithPuppeteer(url);\n fetchCurrentUserIdFromAPI = !!this.config.cookie;\n }\n else {\n const { html, lastUrl } = await this.fetcher.get({ url, type: 'html', maxRetries: this.config.request.maxRetries, signal });\n page = html;\n const isCloudflareChallenge = new URL(lastUrl).pathname === '/login-sync-domains' ||\n html.includes('window._cf_chl_opt') ||\n html.includes('challenge-platform');\n if (isCloudflareChallenge) {\n this.log('debug', `Detected Cloudflare challenge flow - using Puppeteer`);\n page = await this.#fetchPageWithPuppeteer(url);\n // Because cookie not available to Puppeteer, we need to fetch\n // current user ID separately\n fetchCurrentUserIdFromAPI = !!this.config.cookie;\n }\n }\n }\n catch (error) {\n if (signal?.aborted) {\n throw error;\n }\n const e = Error(`Error requesting \"${url}\"`);\n e.cause = error;\n throw e;\n }\n const pageParser = new PageParser(this.logger);\n let initialData;\n try {\n initialData = pageParser.parseInitialData(page, url);\n }\n catch (error) {\n const e = Error(`Error parsing initial data from \"${url}\"`);\n e.cause = error;\n throw e;\n }\n campaignId = ObjectHelper.getProperty(initialData, 'pageBootstrap.campaign.data.id');\n currentUserId = ObjectHelper.getProperty(initialData, 'commonBootstrap.currentUser.data.id');\n if (!campaignId) {\n throw Error(`Campaign ID not found in initial data of \"${url}\"`);\n }\n }\n if (fetchCurrentUserIdFromAPI) {\n this.log('debug', `Fetch current user ID from API`);\n const currentUserAPIURL = URLHelper.constructCurrentUserAPIURL();\n const { json: currentUserJson } = await this.fetcher.get({\n url: currentUserAPIURL,\n type: 'json',\n maxRetries: this.config.request.maxRetries,\n signal\n });\n currentUserId = ObjectHelper.getProperty(currentUserJson, 'data.id') || undefined;\n }\n this.log('debug', `Initial data: campaign ID '${campaignId}'; current user ID '${currentUserId}'`);\n return { campaignId, currentUserId };\n }\n\n async #fetchPageWithPuppeteer(url: string) {\n return this.fetcher.getPageWithPuppeteer(url);\n }\n\n protected log(level: LogLevel, ...msg: any[]) {\n commonLog(this.logger, level, this.name, ...msg);\n }\n}"]}
@@ -23,6 +23,7 @@ import PostsFetcher from './PostsFetcher.js';
23
23
  import CommentParser from '../parsers/CommentParser.js';
24
24
  import { generatePostCommentsSummary } from './templates/CommentInfo.js';
25
25
  import { generateCollectionSummary } from './templates/CollectionInfo.js';
26
+ import { IncludeCriteriaHelper } from './IncludeCriteriaHelper.js';
26
27
  class PostDownloader extends Downloader {
27
28
  constructor(config, db, logger, context) {
28
29
  super(config, db, logger);
@@ -69,7 +70,7 @@ class PostDownloader extends Downloader {
69
70
  signal
70
71
  });
71
72
  const { campaignId } = await postsFetcher.getInitialData(url);
72
- const postsParser = new PostParser(this.logger);
73
+ const postsParser = new PostParser(this.fetcher, this.logger);
73
74
  const res = await this.fetchCampaign(campaignId);
74
75
  if (signal?.aborted) {
75
76
  throw new Error('Aborted');
@@ -97,6 +98,9 @@ _PostDownloader_startPromise = new WeakMap(), _PostDownloader_context = new Weak
97
98
  else if (postFetch.type === 'byCollection') {
98
99
  this.log('info', `Targeting posts in collection #${postFetch.collectionId}`);
99
100
  }
101
+ else if (postFetch.type === 'byURL') {
102
+ this.log('info', `Targeting posts at '${postFetch.url}'`);
103
+ }
100
104
  else if (postFetch.type === 'byFile') {
101
105
  this.log('info', `Target post given by API data in "${postFetch.filePath}"`);
102
106
  }
@@ -132,7 +136,7 @@ _PostDownloader_startPromise = new WeakMap(), _PostDownloader_context = new Weak
132
136
  let campaignSaved = false;
133
137
  let stopConditionMet = false;
134
138
  const savedCollectionIds = [];
135
- const postsParser = new PostParser(this.logger);
139
+ const postsParser = new PostParser(this.fetcher, this.logger);
136
140
  while (postsFetcher.hasNext()) {
137
141
  const { list, aborted, error } = await postsFetcher.next();
138
142
  if (!list || aborted) {
@@ -178,7 +182,7 @@ _PostDownloader_startPromise = new WeakMap(), _PostDownloader_context = new Weak
178
182
  const { json } = await this.commonFetchAPI(postURL, signal);
179
183
  let refreshed = null;
180
184
  if (json) {
181
- refreshed = postsParser.parsePostsAPIResponse(json, postURL).items[0] || null;
185
+ refreshed = (await postsParser.parsePostsAPIResponse(json, postURL)).items[0] || null;
182
186
  if (!refreshed) {
183
187
  this.log('warn', `Refreshed post #${_post.id} but got empty value - going to use existing data`);
184
188
  }
@@ -267,6 +271,9 @@ _PostDownloader_startPromise = new WeakMap(), _PostDownloader_context = new Weak
267
271
  else if (postFetch.type === 'byCollection') {
268
272
  this.log('info', `Done downloading posts in collection #${postFetch.collectionId}`);
269
273
  }
274
+ else if (postFetch.type === 'byURL') {
275
+ this.log('info', `Done downloading posts at '${postFetch.url}'`);
276
+ }
270
277
  else if (postFetch.type === 'byFile') {
271
278
  this.log('info', `Done downloading post given in "${postFetch.filePath}"`);
272
279
  }
@@ -310,105 +317,62 @@ _PostDownloader_startPromise = new WeakMap(), _PostDownloader_context = new Weak
310
317
  const downloadPost = scope.includes('post');
311
318
  const downloadComments = scope.includes('comments');
312
319
  // Step 1: Check whether we should download post
313
- // -- 1.1 Viewability
314
- if (!post.isViewable) {
315
- if (this.config.include.lockedContent) {
316
- this.log('warn', `Post #${post.id} is not viewable by current user`);
317
- }
318
- else {
319
- this.log('warn', `Skipped downloading post #${post.id}: not viewable by current user`);
320
- this.emit('targetEnd', {
321
- target: post,
322
- isSkipped: true,
323
- skipReason: TargetSkipReason.Inaccessible,
324
- skipMessage: 'Target is not viewable by current user'
325
- });
326
- return {
327
- status: 'skippedUnviewable'
328
- };
329
- }
330
- }
331
- // -- 1.2 Config option 'include.postsWithMediaType'
332
- const postsWithMediaType = this.config.include.postsWithMediaType;
333
- if (postsWithMediaType !== 'any') {
334
- const hasAttachments = post.attachments.length > 0;
335
- const hasAudio = !!post.audio || !!post.audioPreview;
336
- const hasImages = post.images.length > 0;
337
- const hasVideo = !!post.video || !!post.videoPreview || !!(post.embed && (post.embed.type === 'videoEmbed' || isYouTubeEmbed(post.embed)));
338
- const isPodcast = post.postType === 'podcast';
339
- let skip = false;
340
- if (postsWithMediaType === 'none') {
341
- skip = hasAttachments || hasAudio || hasImages || hasVideo;
342
- }
343
- else if (Array.isArray(postsWithMediaType)) {
344
- skip = !((postsWithMediaType.includes('attachment') && hasAttachments) ||
345
- (postsWithMediaType.includes('audio') && hasAudio) ||
346
- (postsWithMediaType.includes('image') && hasImages) ||
347
- (postsWithMediaType.includes('video') && hasVideo) ||
348
- (postsWithMediaType.includes('podcast') && isPodcast && (hasAudio || hasVideo)));
349
- }
350
- if (skip) {
351
- this.log('warn', `Skipped downloading post #${post.id}: unmet media type criteria`);
352
- this.log('debug', 'Match criteria failed:', `config.include.postsWithMediaType: ${JSON.stringify(postsWithMediaType)} <-> post #${post.id}:`, {
353
- hasAttachments,
354
- hasAudio,
355
- hasImages,
356
- hasVideo
357
- });
358
- this.emit('targetEnd', {
359
- target: post,
360
- isSkipped: true,
361
- skipReason: TargetSkipReason.UnmetMediaTypeCriteria,
362
- skipMessage: 'Target does not meet media type criteria'
363
- });
364
- return {
365
- status: 'skippedUnmetMediaTypeCriteria'
366
- };
367
- }
368
- }
369
- // -- 1.3 Config option 'include.postsInTier'
370
- const postsInTier = this.config.include.postsInTier;
371
- const isAnyTier = postsInTier === 'any' || postsInTier.includes('any');
372
- if (!isAnyTier) {
373
- const applicableTierIds = postsInTier.filter((id) => post.campaign?.rewards.find((r) => r.id === id));
374
- if (!post.campaign) {
375
- this.log('warn', 'config.include.postsInTier: ignored - post missing campaign info');
376
- }
377
- else {
378
- this.log('debug', 'config.include.postsInTier: applicable tier IDs:', applicableTierIds);
379
- }
380
- let skip = false;
381
- if (!post.campaign) {
382
- skip = false;
383
- }
384
- else if (applicableTierIds.length === 0) {
385
- skip = true;
386
- }
387
- else if (!post.tiers.find((tier) => tier.id === 'patrons')) {
388
- skip = applicableTierIds.every((id) => !post.tiers.find((tier) => tier.id === id));
389
- }
390
- if (skip) {
391
- this.log('warn', `Skipped downloading post #${post.id}: not in tier`);
392
- this.log('debug', 'Match criteria failed:', `config.include.postsInTier: ${JSON.stringify(applicableTierIds)} <-> post #${post.id}:`, post.tiers);
393
- this.emit('targetEnd', {
394
- target: post,
395
- isSkipped: true,
396
- skipReason: TargetSkipReason.NotInTier,
397
- skipMessage: 'Target not in tier'
398
- });
399
- return {
400
- status: 'skippedNotInTier'
401
- };
402
- }
403
- if (post.campaign) {
404
- this.log('debug', 'Match criteria OK:', `config.include.postsInTier: ${JSON.stringify(applicableTierIds)} <-> post #${post.id}:`, post.tiers);
320
+ const criteriaHelper = new IncludeCriteriaHelper(this.logger);
321
+ const criteriaCheck = criteriaHelper.checkPost(post, this.config);
322
+ if (!criteriaCheck.ok) {
323
+ switch (criteriaCheck.reason) {
324
+ case 'unviewable': {
325
+ this.log('warn', `Skipped downloading post #${post.id}: not viewable by current user`);
326
+ this.emit('targetEnd', {
327
+ target: post,
328
+ isSkipped: true,
329
+ skipReason: TargetSkipReason.Inaccessible,
330
+ skipMessage: 'Target is not viewable by current user'
331
+ });
332
+ return {
333
+ status: 'skippedUnviewable'
334
+ };
335
+ }
336
+ case 'unmetMediaTypeCriteria': {
337
+ this.log('warn', `Skipped downloading post #${post.id}: unmet media type criteria`);
338
+ this.emit('targetEnd', {
339
+ target: post,
340
+ isSkipped: true,
341
+ skipReason: TargetSkipReason.UnmetMediaTypeCriteria,
342
+ skipMessage: 'Target does not meet media type criteria'
343
+ });
344
+ return {
345
+ status: 'skippedUnmetMediaTypeCriteria'
346
+ };
347
+ }
348
+ case 'notInTier': {
349
+ this.log('warn', `Skipped downloading post #${post.id}: not in tier`);
350
+ this.emit('targetEnd', {
351
+ target: post,
352
+ isSkipped: true,
353
+ skipReason: TargetSkipReason.NotInTier,
354
+ skipMessage: 'Target not in tier'
355
+ });
356
+ return {
357
+ status: 'skippedNotInTier'
358
+ };
359
+ }
360
+ case 'publishDateOutOfRange': {
361
+ this.log('warn', `Skipped downloading post #${post.id}: publish date out of range`);
362
+ this.emit('targetEnd', {
363
+ target: post,
364
+ isSkipped: true,
365
+ skipReason: TargetSkipReason.PublishDateOutOfRange,
366
+ skipMessage: 'Publish date out of range'
367
+ });
368
+ return {
369
+ status: 'skippedPublishDateOutOfRange'
370
+ };
371
+ }
405
372
  }
406
373
  }
407
- // -- 1.4 Config option 'include.postsPublished'
408
- if (this.isPublishDateOutOfRange(post)) {
409
- return {
410
- status: 'skippedPublishDateOutOfRange'
411
- };
374
+ if (!post.isViewable && this.config.include.lockedContent) {
375
+ this.log('warn', `Post #${post.id} is not viewable by current user`);
412
376
  }
413
377
  if (!downloadPost && downloadComments) {
414
378
  this.log('info', `Previously downloaded post #${post.id} - refresh comments only`);
@@ -590,7 +554,7 @@ _PostDownloader_startPromise = new WeakMap(), _PostDownloader_context = new Weak
590
554
  status: 'downloaded'
591
555
  };
592
556
  }, _PostDownloader_isFetchingMultiplePosts = function _PostDownloader_isFetchingMultiplePosts(postFetch) {
593
- return postFetch.type === 'byUser' || postFetch.type === 'byUserId' || postFetch.type === 'byCollection';
557
+ return postFetch.type === 'byUser' || postFetch.type === 'byUserId' || postFetch.type === 'byCollection' || postFetch.type === 'byURL';
594
558
  }, _PostDownloader_createDownloadTaskBatchForPost = async function _PostDownloader_createDownloadTaskBatchForPost(post, postDirs, signal) {
595
559
  const incPreview = this.config.include.previewMedia;
596
560
  const incContent = this.config.include.contentMedia;
@@ -755,8 +719,8 @@ _PostDownloader_startPromise = new WeakMap(), _PostDownloader_context = new Weak
755
719
  json = null;
756
720
  }
757
721
  if (json) {
758
- const parser = new PostParser();
759
- const parentPost = parser.parsePostsAPIResponse(json, apiURL).items[0];
722
+ const parser = new PostParser(this.fetcher);
723
+ const parentPost = (await parser.parsePostsAPIResponse(json, apiURL)).items[0];
760
724
  if (!parentPost) {
761
725
  this.log('error', `Error fetching linked attachment ${laStr} from parent post #${la.postId}: post data not found in response of "${apiURL}"`);
762
726
  }