patreon-dl 3.4.0 → 3.6.0

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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -2
  3. package/bin/EmbedlyDownloader.js +173 -0
  4. package/bin/patreon-dl-sprout.js +43 -0
  5. package/bin/patreon-dl-vimeo.js +22 -157
  6. package/dist/browse/api/ContentAPIMixin.d.ts +7 -1
  7. package/dist/browse/api/ContentAPIMixin.js +20 -0
  8. package/dist/browse/api/ContentAPIMixin.js.map +1 -1
  9. package/dist/browse/api/FilterAPIMixin.js +29 -2
  10. package/dist/browse/api/FilterAPIMixin.js.map +1 -1
  11. package/dist/browse/api/index.d.ts +12 -0
  12. package/dist/browse/db/CampaignDBMixin.d.ts +3 -3
  13. package/dist/browse/db/CampaignDBMixin.js +13 -1
  14. package/dist/browse/db/CampaignDBMixin.js.map +1 -1
  15. package/dist/browse/db/CollectionFTS.d.ts +2 -0
  16. package/dist/browse/db/CollectionFTS.js +68 -0
  17. package/dist/browse/db/CollectionFTS.js.map +1 -0
  18. package/dist/browse/db/ContentDBMixin.d.ts +25 -12
  19. package/dist/browse/db/ContentDBMixin.js +412 -15
  20. package/dist/browse/db/ContentDBMixin.js.map +1 -1
  21. package/dist/browse/db/Init.js +63 -5
  22. package/dist/browse/db/Init.js.map +1 -1
  23. package/dist/browse/db/MediaDBMixin.js +2 -6
  24. package/dist/browse/db/MediaDBMixin.js.map +1 -1
  25. package/dist/browse/db/PostFTS.d.ts +4 -0
  26. package/dist/browse/db/PostFTS.js +163 -0
  27. package/dist/browse/db/PostFTS.js.map +1 -0
  28. package/dist/browse/db/ProductFTS.d.ts +4 -0
  29. package/dist/browse/db/ProductFTS.js +163 -0
  30. package/dist/browse/db/ProductFTS.js.map +1 -0
  31. package/dist/browse/db/Update.js +3 -1
  32. package/dist/browse/db/Update.js.map +1 -1
  33. package/dist/browse/db/index.d.ts +26 -14
  34. package/dist/browse/db/updaters/DBUpdater_1_2_0.d.ts +2 -0
  35. package/dist/browse/db/updaters/DBUpdater_1_2_0.js +13 -0
  36. package/dist/browse/db/updaters/DBUpdater_1_2_0.js.map +1 -0
  37. package/dist/browse/server/Router.js +12 -1
  38. package/dist/browse/server/Router.js.map +1 -1
  39. package/dist/browse/server/handler/ContentAPIRequestHandler.d.ts +3 -0
  40. package/dist/browse/server/handler/ContentAPIRequestHandler.js +34 -2
  41. package/dist/browse/server/handler/ContentAPIRequestHandler.js.map +1 -1
  42. package/dist/browse/types/Campaign.d.ts +2 -0
  43. package/dist/browse/types/Campaign.js.map +1 -1
  44. package/dist/browse/types/Content.d.ts +43 -2
  45. package/dist/browse/types/Content.js.map +1 -1
  46. package/dist/browse/types/Filter.d.ts +6 -3
  47. package/dist/browse/types/Filter.js.map +1 -1
  48. package/dist/browse/web/assets/{index-b301OTnD.css → index-DjOKbT1U.css} +1 -1
  49. package/dist/browse/web/assets/index-Dw_64hkR.js +218 -0
  50. package/dist/browse/web/index.html +2 -2
  51. package/dist/cli/CLIOptions.js +20 -1
  52. package/dist/cli/CLIOptions.js.map +1 -1
  53. package/dist/cli/CommandLineParser.js +2 -1
  54. package/dist/cli/CommandLineParser.js.map +1 -1
  55. package/dist/cli/ConfigFileParser.js +8 -0
  56. package/dist/cli/ConfigFileParser.js.map +1 -1
  57. package/dist/cli/index.js +21 -4
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/downloaders/Bootstrap.d.ts +11 -1
  60. package/dist/downloaders/Bootstrap.js +14 -1
  61. package/dist/downloaders/Bootstrap.js.map +1 -1
  62. package/dist/downloaders/Downloader.d.ts +7 -3
  63. package/dist/downloaders/Downloader.js +159 -109
  64. package/dist/downloaders/Downloader.js.map +1 -1
  65. package/dist/downloaders/DownloaderEvent.d.ts +6 -6
  66. package/dist/downloaders/DownloaderEvent.js.map +1 -1
  67. package/dist/downloaders/DownloaderOptions.d.ts +14 -1
  68. package/dist/downloaders/DownloaderOptions.js +10 -0
  69. package/dist/downloaders/DownloaderOptions.js.map +1 -1
  70. package/dist/downloaders/InitialData.d.ts +16 -0
  71. package/dist/downloaders/InitialData.js +94 -0
  72. package/dist/downloaders/InitialData.js.map +1 -0
  73. package/dist/downloaders/PostDownloader.d.ts +11 -2
  74. package/dist/downloaders/PostDownloader.js +337 -277
  75. package/dist/downloaders/PostDownloader.js.map +1 -1
  76. package/dist/downloaders/PostsFetcher.d.ts +5 -5
  77. package/dist/downloaders/PostsFetcher.js +24 -75
  78. package/dist/downloaders/PostsFetcher.js.map +1 -1
  79. package/dist/downloaders/ProductDownloader.js +367 -165
  80. package/dist/downloaders/ProductDownloader.js.map +1 -1
  81. package/dist/downloaders/ProductsFetcher.d.ts +57 -0
  82. package/dist/downloaders/ProductsFetcher.js +331 -0
  83. package/dist/downloaders/ProductsFetcher.js.map +1 -0
  84. package/dist/downloaders/index.d.ts +1 -1
  85. package/dist/downloaders/index.js.map +1 -1
  86. package/dist/downloaders/task/DownloadTaskFactory.js +15 -1
  87. package/dist/downloaders/task/DownloadTaskFactory.js.map +1 -1
  88. package/dist/downloaders/templates/CollectionInfo.d.ts +2 -0
  89. package/dist/downloaders/templates/CollectionInfo.js +20 -0
  90. package/dist/downloaders/templates/CollectionInfo.js.map +1 -0
  91. package/dist/entities/Comment.d.ts +3 -3
  92. package/dist/entities/Comment.js.map +1 -1
  93. package/dist/entities/{Collection.d.ts → List.d.ts} +1 -1
  94. package/dist/entities/List.js +2 -0
  95. package/dist/entities/List.js.map +1 -0
  96. package/dist/entities/MediaItem.d.ts +17 -2
  97. package/dist/entities/MediaItem.js.map +1 -1
  98. package/dist/entities/Post.d.ts +34 -3
  99. package/dist/entities/Post.js.map +1 -1
  100. package/dist/entities/Product.d.ts +25 -0
  101. package/dist/entities/Product.js +6 -1
  102. package/dist/entities/Product.js.map +1 -1
  103. package/dist/entities/index.d.ts +1 -1
  104. package/dist/entities/index.js.map +1 -1
  105. package/dist/parsers/CommentParser.d.ts +3 -3
  106. package/dist/parsers/CommentParser.js.map +1 -1
  107. package/dist/parsers/Parser.d.ts +5 -1
  108. package/dist/parsers/Parser.js +70 -0
  109. package/dist/parsers/Parser.js.map +1 -1
  110. package/dist/parsers/PostParser.d.ts +2 -2
  111. package/dist/parsers/PostParser.js +34 -0
  112. package/dist/parsers/PostParser.js.map +1 -1
  113. package/dist/parsers/ProductParser.d.ts +3 -2
  114. package/dist/parsers/ProductParser.js +115 -49
  115. package/dist/parsers/ProductParser.js.map +1 -1
  116. package/dist/utils/FSHelper.d.ts +12 -8
  117. package/dist/utils/FSHelper.js +10 -0
  118. package/dist/utils/FSHelper.js.map +1 -1
  119. package/dist/utils/Fetcher.js +1 -1
  120. package/dist/utils/Fetcher.js.map +1 -1
  121. package/dist/utils/FilenameFormatHelper.d.ts +2 -1
  122. package/dist/utils/FilenameFormatHelper.js +14 -1
  123. package/dist/utils/FilenameFormatHelper.js.map +1 -1
  124. package/dist/utils/Misc.d.ts +2 -1
  125. package/dist/utils/Misc.js +38 -5
  126. package/dist/utils/Misc.js.map +1 -1
  127. package/dist/utils/URLHelper.d.ts +7 -0
  128. package/dist/utils/URLHelper.js +161 -3
  129. package/dist/utils/URLHelper.js.map +1 -1
  130. package/dist/utils/yt/InnertubeLoader.d.ts +1 -0
  131. package/dist/utils/yt/InnertubeLoader.js +26 -4
  132. package/dist/utils/yt/InnertubeLoader.js.map +1 -1
  133. package/dist/utils/yt/PoToken.d.ts +11 -0
  134. package/dist/utils/yt/PoToken.js +107 -0
  135. package/dist/utils/yt/PoToken.js.map +1 -0
  136. package/package.json +13 -5
  137. package/dist/browse/web/assets/index-C5gLqRAU.js +0 -209
  138. package/dist/entities/Collection.js +0 -2
  139. package/dist/entities/Collection.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Patrick Kan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -74,7 +74,7 @@ $ patreon-dl --configure-youtube
74
74
 
75
75
  You can specify external programs to download embedded videos or from embedded links. For YouTube videos, this will replace the built-in downloader.
76
76
 
77
- See the [example config](./example-embed.conf) on how to configure an external downloader to fetch YouTube and Vimeo videos through [yt-dlp](https://github.com/yt-dlp/yt-dlp). For Vimeo videos, a [helper script](./bin/patreon-dl-vimeo.js) bundled with `patreon-dl` is used.
77
+ See the [example config](./example-embed.conf) on how to configure an external downloader to fetch YouTube, Vimeo and SproutVideo content through [yt-dlp](https://github.com/yt-dlp/yt-dlp). Helper scripts bundled with `patreon-dl` are used in the case of Vimeo and SproutVideo ([patreon-dl-vimeo.js](./bin/patreon-dl-vimeo.js) and [patreon-dl-sprout.js](./bin/patreon-dl-sprout.js) respectively).
78
78
 
79
79
  ## Installation
80
80
 
@@ -116,7 +116,12 @@ $ patreon-dl [OPTION]... URL
116
116
  #### Supported URL formats
117
117
 
118
118
  ```
119
- // Download a product
119
+ // Download products from a creator's shop
120
+ https://www.patreon.com/<creator>/shop
121
+ https://www.patreon.com/c/<creator>/shop
122
+ https://www.patreon.com/cw/<creator>/shop
123
+
124
+ // Download a single product
120
125
  https://www.patreon.com/<creator>/shop/<slug>-<product_id>
121
126
 
122
127
  // Download posts by creator
@@ -288,6 +293,21 @@ Note the URL shown in the output. Open this URL in a web browser to begin viewin
288
293
 
289
294
  ## Changelog
290
295
 
296
+ v3.6.0
297
+ - Browse: affix nav links (previous / next post) to viewport bottom if post content overflows ([patreon-dl-gui#41](https://github.com/patrickkfkan/patreon-dl-gui/issues/41))
298
+ - Fix error when Deno path contains spaces ([patreon-dl-gui#42](https://github.com/patrickkfkan/patreon-dl-gui/issues/42))
299
+ - Add SproutVideo download script ([patreon-dl-gui#43](https://github.com/patrickkfkan/patreon-dl-gui/issues/43))
300
+ - Fix YouTube download returning "auth required" error
301
+
302
+ v3.5.0
303
+ - Add support for downloading from "shop" URLs (e.g. `https://www.patreon.com/<creator>/shop`). This will download all products from a creator's shop.
304
+ - Add `productsPublished` / `products.published.after` / `products.published.before` option to set publish date criteria of products included in download.
305
+ - Since `stopOn` / `stop.on` option now also applies to products, the `postPreviouslyDownloaded` and `postPublishDateOutOfRange` values have been deprecated in favor of `previouslyDownloaded` and `publishDateOutOfRange`, respectively.
306
+ - Add Collections support. Collection info is now saved when downloading posts. This means you can browse posts by collection. ([#107](https://github.com/patrickkfkan/patreon-dl/issues/107))
307
+ - (Browse) Add search functionality ([#106](https://github.com/patrickkfkan/patreon-dl/issues/106))
308
+ - Add Tags support. Tag info is now saved when downloading posts. This means you can filter posts by tag.
309
+ - Add `include.mediaThumbnails` option
310
+
291
311
  v3.4.0
292
312
  - Fix "no posts found" on "cw" pages ([patreon-dl-gui#30](https://github.com/patrickkfkan/patreon-dl-gui/issues/30))
293
313
  - Fix YouTube streams returning 403 error ([patreon-dl-gui#31](https://github.com/patrickkfkan/patreon-dl-gui/issues/31))
@@ -0,0 +1,173 @@
1
+ import parseArgs from 'yargs-parser';
2
+ import spawn from '@patrickkfkan/cross-spawn';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * EmbedlyDownloader uses yt-dlp to download content embedded through Embedly:
7
+ * - It obtains the video URL from 'embed.html' or 'embed.url' commandline args.
8
+ * The former ("player URL") is always preferable since it is what's actually played within
9
+ * the Patreon post, and furthermore 'embed.url' sometimes returns "Page not found" (see
10
+ * issue: https://github.com/patrickkfkan/patreon-dl/issues/65) or a password-protected page.
11
+ * - The URL is passed to yt-dlp.
12
+ * - yt-dlp downloads the video from URL and saves it to 'dest.dir'. The filename is determined by the specified
13
+ * format '%(title)s.%(ext)s' (see: https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template).
14
+ * - Fallback to embed URL if player URL fails to download.
15
+ */
16
+
17
+ export default class EmbedlyDownloader {
18
+
19
+ /**
20
+ *
21
+ * @param {*} provider Name of the provider.
22
+ * @param {*} srcHostname Hostname of the embedded source URL.
23
+ */
24
+ constructor(provider, srcHostname) {
25
+ this.provider = provider;
26
+ this.srcHostname = srcHostname;
27
+ }
28
+
29
+ getPlayerURL(html) {
30
+ if (!html) {
31
+ return null;
32
+ }
33
+
34
+ const regex = /src="(\/\/cdn.embedly.com\/widgets.+?)"/g;
35
+ const match = regex.exec(html);
36
+ if (match && match[1]) {
37
+ const embedlyURL = match[1];
38
+ console.log('Found Embedly URL from embed HTML:', embedlyURL);
39
+ let embedlySrc;
40
+ try {
41
+ const urlObj = new URL(`https:${embedlyURL}`);
42
+ embedlySrc = urlObj.searchParams.get('src');
43
+ }
44
+ catch (error) {
45
+ console.error('Error parsing Embedly URL:', error);
46
+ }
47
+ try {
48
+ const embedlySrcObj = new URL(embedlySrc);
49
+ if (!this.srcHostname) {
50
+ console.log(`Got Embedly src "${embedlySrc}" - assume it is ${this.provider} player URL since no hostname was specified`);
51
+ }
52
+ else if (embedlySrcObj.hostname === this.srcHostname) {
53
+ console.log(`Got ${this.provider} player URL from Embedly src: ${embedlySrc}`);
54
+ }
55
+ else {
56
+ console.warn(`Embedly src "${embedlySrc}" does not correspond to ${this.provider} player URL`);
57
+ }
58
+ return embedlySrc;
59
+ }
60
+ catch (error) {
61
+ console.error(`Error parsing Embedly src "${embedlySrc}":`, error);
62
+ }
63
+ }
64
+
65
+ return null;
66
+ }
67
+
68
+ getCommandString(cmd, args) {
69
+ const quotedArgs = args.map((arg) => arg.includes(' ') ? `"${arg}"` : arg);
70
+ return [
71
+ cmd,
72
+ ...quotedArgs
73
+ ].join(' ');
74
+ }
75
+
76
+ async download(url, o, videoPassword, ytdlpPath, ytdlpArgs) {
77
+ let proc;
78
+ const ytdlp = ytdlpPath || 'yt-dlp';
79
+ const parsedYtdlpArgs = parseArgs(ytdlpArgs);
80
+ try {
81
+ return await new Promise((resolve, reject) => {
82
+ let settled = false;
83
+ const args = [];
84
+ if (!parsedYtdlpArgs['o'] && !parsedYtdlpArgs['output']) {
85
+ args.push('-o', o);
86
+ }
87
+ if (!parsedYtdlpArgs['referrer']) {
88
+ args.push('--add-header', 'Referer: https://patreon.com/');
89
+ }
90
+ args.push(...ytdlpArgs);
91
+ const printArgs = [...args];
92
+ if (videoPassword && !parsedYtdlpArgs['video-password']) {
93
+ args.push('--video-password', videoPassword);
94
+ printArgs.push('--video-password', '******');
95
+ }
96
+ args.push(url);
97
+ printArgs.push(url);
98
+
99
+ console.log(`Command: ${this.getCommandString(ytdlp, printArgs)}`);
100
+ proc = spawn(ytdlp, args);
101
+
102
+ proc.stdout?.on('data', (data) => {
103
+ console.log(data.toString());
104
+ });
105
+
106
+ proc.stderr?.on('data', (data_1) => {
107
+ console.error(data_1.toString());
108
+ });
109
+
110
+ proc.on('error', (err) => {
111
+ if (settled) {
112
+ return;
113
+ }
114
+ settled = true;
115
+ reject(err);
116
+ });
117
+
118
+ proc.on('exit', (code) => {
119
+ if (settled) {
120
+ return;
121
+ }
122
+ settled = true;
123
+ resolve(code);
124
+ });
125
+ });
126
+ } finally {
127
+ if (proc) {
128
+ proc.removeAllListeners();
129
+ proc.stdout?.removeAllListeners();
130
+ proc.stderr?.removeAllListeners();
131
+ }
132
+ }
133
+ }
134
+
135
+ async start() {
136
+ const args = parseArgs(process.argv.slice(2));
137
+ const {
138
+ 'o': _o,
139
+ 'embed-html': _embedHTML,
140
+ 'embed-url': _embedURL,
141
+ 'video-password': videoPassword,
142
+ 'yt-dlp': _ytdlpPath
143
+ } = args;
144
+ const o = _o?.trim() ? path.resolve(_o.trim()) : null;
145
+ const embedHTML = _embedHTML?.trim();
146
+ const embedURL = _embedURL?.trim();
147
+ const ytdlpPath = _ytdlpPath?.trim() ? path.resolve(_ytdlpPath.trim()) : null;
148
+ const ytdlpArgs = args['_'];
149
+
150
+ if (!o) {
151
+ throw Error('No output file specified');
152
+ }
153
+
154
+ if (!embedHTML && !embedURL) {
155
+ throw Error('No embed HTML or URL provided');
156
+ }
157
+
158
+ const _url = this.getPlayerURL(embedHTML) || embedURL;
159
+
160
+ if (!_url) {
161
+ throw Error(`Failed to obtain video URL`);
162
+ }
163
+
164
+ console.log(`Going to download video from "${_url}"`);
165
+ let code = await this.download(_url, o, videoPassword, ytdlpPath, ytdlpArgs);
166
+ if (code !== 0 && _url !== embedURL && embedURL) {
167
+ console.log(`Download failed - retrying with embed URL "${embedURL}"`);
168
+ return await this.download(embedURL);
169
+ }
170
+
171
+ return code;
172
+ }
173
+ }
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * External downloader for embedded SproutVideo videos. Obtains the appropriate URL to download from and
5
+ * passes it to 'yt-dlp' (https://github.com/yt-dlp/yt-dlp).
6
+ *
7
+ * Usage
8
+ * -----
9
+ * Place the following two lines in your 'patreon-dl' config file:
10
+ *
11
+ * [embed.downloader.sproutvideo]
12
+ * exec = patreon-dl-sprout -o "{dest.dir}/%(title)s.%(ext)s" --embed-html "{embed.html}" --embed-url "{embed.url}"
13
+ *
14
+ * You can append the following additional options to the exec line if necessary:
15
+ * --video-password "<password>": for password-protected videos
16
+ * --yt-dlp "</path/to/yt-dlp>": if yt-dlp is not in the PATH
17
+ *
18
+ * You can pass options directly to yt-dlp. To do so, add '--' to the end of the exec line, followed by the options.
19
+ * For example:
20
+ * exec = patreon-dl-sprout -o "{dest.dir}/%(title)s.%(ext)s" --embed-html "{embed.html}" --embed-url "{embed.url}" -- --cookies-from-browser firefox
21
+ *
22
+ * Upon encountering a post with embedded SproutVideo content, 'patreon-dl' will call this script, which in turn proceeds to download through SproutVideoDownloader
23
+ * (see the parent class EmbedlyDownloader for more info).
24
+ */
25
+
26
+ import EmbedlyDownloader from './EmbedlyDownloader.js';
27
+
28
+ class SproutVideoDownloader extends EmbedlyDownloader {
29
+ constructor() {
30
+ super('SproutVideo', 'videos.sproutvideo.com');
31
+ }
32
+ }
33
+
34
+ (async () => {
35
+ const downloader = new SproutVideoDownloader();
36
+ try {
37
+ process.exit(await downloader.start());
38
+ }
39
+ catch (error) {
40
+ console.error(error instanceof Error ? error.message : String(error));
41
+ process.exit(1);
42
+ }
43
+ })();
@@ -19,173 +19,38 @@
19
19
  * For example:
20
20
  * exec = patreon-dl-vimeo -o "{dest.dir}/%(title)s.%(ext)s" --embed-html "{embed.html}" --embed-url "{embed.url}" -- --cookies-from-browser firefox
21
21
  *
22
- * Upon encountering a post with embedded Vimeo content, 'patreon-dl' will call this script. The following then happens:
23
- * - This script obtains the video URL from 'embed.html' or 'embed.url'. The former ("player URL") is always preferable
24
- * since it is what's actually played within the Patreon post, and furthermore 'embed.url' sometimes returns
25
- * "Page not found" (see issue: https://github.com/patrickkfkan/patreon-dl/issues/65).
26
- * - The URL is passed to yt-dlp.
27
- * - yt-dlp downloads the video from URL and saves it to 'dest.dir'. The filename is determined by the specified
28
- * format '%(title)s.%(ext)s' (see: https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template).
29
- * - Fallback to embed URL if player URL fails to download.
30
- *
22
+ * Upon encountering a post with embedded Vimeo content, 'patreon-dl' will call this script, which in turn proceeds to download through VimeoDownloader
23
+ * (see the parent class EmbedlyDownloader for more info).
31
24
  */
32
25
 
33
- import parseArgs from 'yargs-parser';
34
- import spawn from '@patrickkfkan/cross-spawn';
35
- import path from 'path';
36
-
37
- function tryGetPlayerURL(html) {
38
- if (!html) {
39
- return null;
40
- }
26
+ import EmbedlyDownloader from './EmbedlyDownloader.js';
41
27
 
42
- const regex = /https:\/\/player\.vimeo\.com\/video\/\d+/g;
43
- const match = regex.exec(html);
44
- if (match && match[0]) {
45
- console.log('Found Vimeo player URL from embed HTML:', match[0]);
46
- return match[0];
28
+ class VimeoDownloader extends EmbedlyDownloader {
29
+ constructor() {
30
+ super('Vimeo', 'player.vimeo.com');
47
31
  }
48
32
 
49
- const regex2 = /src="(\/\/cdn.embedly.com\/widgets.+?)"/g;
50
- const match2 = regex2.exec(html);
51
- if (match2 && match2[1]) {
52
- const embedlyURL = match2[1];
53
- console.log('Found Embedly URL from embed HTML:', embedlyURL);
54
- let embedlySrc;
55
- try {
56
- const urlObj = new URL(`https:${embedlyURL}`);
57
- embedlySrc = urlObj.searchParams.get('src');
58
- }
59
- catch (error) {
60
- console.error('Error parsing Embedly URL:', error);
61
- }
62
- try {
63
- const embedlySrcObj = new URL(embedlySrc);
64
- if (embedlySrcObj.hostname === 'player.vimeo.com') {
65
- console.log(`Got Vimeo player URL from Embedly src: ${embedlySrc}`);
66
- }
67
- else {
68
- console.warn(`Embedly src "${embedlySrc}" does not correspond to Vimeo player URL`);
33
+ // Override
34
+ getPlayerURL(html) {
35
+ if (html) {
36
+ const regex = /https:\/\/player\.vimeo\.com\/video\/\d+/g;
37
+ const match = regex.exec(html);
38
+ if (match && match[0]) {
39
+ console.log('Found Vimeo player URL from embed HTML:', match[0]);
40
+ return match[0];
69
41
  }
70
- return embedlySrc;
71
- }
72
- catch (error) {
73
- console.error(`Error parsing Embedly src "${embedlySrc}":`, error);
74
42
  }
43
+ return super.getPlayerURL(html);
75
44
  }
76
-
77
- return null;
78
45
  }
79
46
 
80
- function getCommandString(cmd, args) {
81
- const quotedArgs = args.map((arg) => arg.includes(' ') ? `"${arg}"` : arg);
82
- return [
83
- cmd,
84
- ...quotedArgs
85
- ].join(' ');
86
- }
87
-
88
- async function download(url, o, videoPassword, ytdlpPath, ytdlpArgs) {
89
- let proc;
90
- const ytdlp = ytdlpPath || 'yt-dlp';
91
- const parsedYtdlpArgs = parseArgs(ytdlpArgs);
47
+ (async () => {
48
+ const downloader = new VimeoDownloader();
92
49
  try {
93
- return await new Promise((resolve, reject) => {
94
- let settled = false;
95
- const args = [];
96
- if (!parsedYtdlpArgs['o'] && !parsedYtdlpArgs['output']) {
97
- args.push('-o', o);
98
- }
99
- if (!parsedYtdlpArgs['referrer']) {
100
- args.push('--referer', 'https://patreon.com/');
101
- }
102
- args.push(...ytdlpArgs);
103
- const printArgs = [...args];
104
- if (videoPassword && !parsedYtdlpArgs['video-password']) {
105
- args.push('--video-password', videoPassword);
106
- printArgs.push('--video-password', '******');
107
- }
108
- args.push(url);
109
- printArgs.push(url);
110
-
111
- console.log(`Command: ${getCommandString(ytdlp, printArgs)}`);
112
- proc = spawn(ytdlp, args);
113
-
114
- proc.stdout?.on('data', (data) => {
115
- console.log(data.toString());
116
- });
117
-
118
- proc.stderr?.on('data', (data_1) => {
119
- console.error(data_1.toString());
120
- });
121
-
122
- proc.on('error', (err) => {
123
- if (settled) {
124
- return;
125
- }
126
- settled = true;
127
- reject(err);
128
- });
129
-
130
- proc.on('exit', (code) => {
131
- if (settled) {
132
- return;
133
- }
134
- settled = true;
135
- resolve(code);
136
- });
137
- });
138
- } finally {
139
- if (proc) {
140
- proc.removeAllListeners();
141
- proc.stdout?.removeAllListeners();
142
- proc.stderr?.removeAllListeners();
143
- }
50
+ process.exit(await downloader.start());
144
51
  }
145
- }
146
-
147
- const args = parseArgs(process.argv.slice(2));
148
- const {
149
- 'o': _o,
150
- 'embed-html': _embedHTML,
151
- 'embed-url': _embedURL,
152
- 'video-password': videoPassword,
153
- 'yt-dlp': _ytdlpPath
154
- } = args;
155
- const o = _o?.trim() ? path.resolve(_o.trim()) : null;
156
- const embedHTML = _embedHTML?.trim();
157
- const embedURL = _embedURL?.trim();
158
- const ytdlpPath = _ytdlpPath?.trim() ? path.resolve(_ytdlpPath.trim()) : null;
159
- const ytdlpArgs = args['_'];
160
-
161
- if (!o) {
162
- console.error('No output file specified');
163
- process.exit(1);
164
- }
165
-
166
- if (!embedHTML && !embedURL) {
167
- console.error('No embed HTML or URL provided');
168
- process.exit(1);
169
- }
170
-
171
- const url = tryGetPlayerURL(embedHTML) || embedURL;
172
-
173
- if (!url) {
174
- console.error(`Failed to obtain video URL`);
175
- process.exit(1);
176
- }
177
-
178
- async function doDownload(_url) {
179
- let code = await download(_url, o, videoPassword, ytdlpPath, ytdlpArgs);
180
- if (code !== 0 && _url !== embedURL && embedURL) {
181
- console.log(`Download failed - retrying with embed URL "${embedURL}"`);
182
- return await doDownload(embedURL);
52
+ catch (error) {
53
+ console.error(error instanceof Error ? error.message : String(error));
54
+ process.exit(1);
183
55
  }
184
- return code;
185
- }
186
-
187
- console.log(`Going to download video from "${url}"`);
188
-
189
- doDownload(url).then((code) => {
190
- process.exit(code);
191
- });
56
+ })();
@@ -1,12 +1,18 @@
1
1
  import { type APIConstructor } from ".";
2
2
  import { type Product, type Post } from "../../entities";
3
- import { type GetContentContext, type ContentType, type GetContentListParams } from "../types/Content.js";
3
+ import { type GetContentContext, type ContentType, type GetContentListParams, type GetCollectionListParams, type GetPostTagListParams } from "../types/Content.js";
4
4
  export declare function ContentAPIMixin<TBase extends APIConstructor>(Base: TBase): {
5
5
  new (...args: any[]): {
6
6
  getContentList<T extends ContentType>(params: GetContentListParams<T>): import("../types/Content.js").ContentList<T>;
7
7
  getPost(id: string): import("../types/Content.js").PostWithComments | null;
8
8
  getProduct(id: string): Product | null;
9
9
  getPreviousNextContent<T extends ContentType>(content: Post | Product, context: GetContentContext<T>): import("../types/Content.js").GetPreviousNextContentResult<T>;
10
+ getCollection(id: string): {
11
+ collection: import("../../entities").Collection;
12
+ campaignId: string;
13
+ } | null;
14
+ getCollectionList(params: GetCollectionListParams): import("../types/Content.js").CollectionList;
15
+ getPostTagList(params: GetPostTagListParams): import("../types/Content.js").PostTagList;
10
16
  "__#130@#processPostContentInlineMedia"(post: Post): void;
11
17
  name: string;
12
18
  db: import("../db").DBInstance;
@@ -8,6 +8,8 @@ import RawDataExtractor from '../web/utils/RawDataExtractor.js';
8
8
  import { URLHelper } from '../../utils/index.js';
9
9
  const DEFAULT_CONTENT_LIST_SIZE = 10;
10
10
  const DEFAULT_CONTENT_LIST_SORT_BY = 'a-z';
11
+ const DEFAULT_COLLECTION_LIST_SIZE = 10;
12
+ const DEFAULT_COLLECTION_LIST_SORT_BY = 'a-z';
11
13
  export function ContentAPIMixin(Base) {
12
14
  var _ContentAPI_instances, _ContentAPI_processPostContentInlineMedia, _a;
13
15
  return _a = class ContentAPI extends Base {
@@ -52,6 +54,24 @@ export function ContentAPIMixin(Base) {
52
54
  getPreviousNextContent(content, context) {
53
55
  return this.db.getPreviousNextContent(content, context);
54
56
  }
57
+ getCollection(id) {
58
+ return this.db.getCollection(id);
59
+ }
60
+ getCollectionList(params) {
61
+ const { search = '', sortBy = DEFAULT_COLLECTION_LIST_SORT_BY, limit = DEFAULT_COLLECTION_LIST_SIZE, offset = 0 } = params;
62
+ return this.db.getCollectionList({
63
+ campaign: params.campaign,
64
+ search,
65
+ sortBy,
66
+ limit,
67
+ offset
68
+ });
69
+ }
70
+ getPostTagList(params) {
71
+ return this.db.getPostTagList({
72
+ campaign: params.campaign,
73
+ });
74
+ }
55
75
  },
56
76
  _ContentAPI_instances = new WeakSet(),
57
77
  _ContentAPI_processPostContentInlineMedia = function _ContentAPI_processPostContentInlineMedia(post) {
@@ -1 +1 @@
1
- {"version":3,"file":"ContentAPIMixin.js","sourceRoot":"","sources":["../../../src/browse/api/ContentAPIMixin.ts"],"names":[],"mappings":";;;;;AAAA,OAAO,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,SAAS,CAAC;AAI9C,OAAO,gBAAgB,MAAM,kCAAkC,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,MAAM,yBAAyB,GAAG,EAAE,CAAC;AACrC,MAAM,4BAA4B,GAAsB,KAAK,CAAC;AAE9D,MAAM,UAAU,eAAe,CAA+B,IAAW;;IACvE,YAAO,MAAM,UAAW,SAAQ,IAAI;YAA7B;;;YAkHP,CAAC;YAjHC,cAAc,CAAwB,MAA+B;gBACnE,MAAM,EAAE,MAAM,GAAG,4BAA4B,EAAE,KAAK,GAAG,yBAAyB,EAAE,MAAM,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC;gBACxG,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,cAAc,CAAC;oBAClC,GAAG,MAAM;oBACT,MAAM;oBACN,KAAK;oBACL,MAAM;iBACP,CAAC,CAAC;gBACH,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;oBAC9B,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;wBAClB,KAAK,MAAM;4BACT,uBAAA,IAAI,wEAA+B,MAAnC,IAAI,EAAgC,IAAI,CAAC,CAAC;4BAC1C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;4BACrD,MAAM;wBACR,KAAK,SAAS,CAAC,CAAC,CAAC;4BACf,MAAM,WAAW,GAAG,gBAAgB,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAC;4BACzE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;4BACvE,MAAM;wBACR,CAAC;oBACH,CAAC;gBACH,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YAED,OAAO,CAAC,EAAU;gBAChB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;gBAC5C,IAAI,IAAI,EAAE,CAAC;oBACT,uBAAA,IAAI,wEAA+B,MAAnC,IAAI,EAAgC,IAAI,CAAC,CAAC;oBAC1C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;gBACvD,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YAED,UAAU,CAAC,EAAU;gBACnB,OAAO,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;YAC3C,CAAC;YAED,sBAAsB,CAAwB,OAAuB,EAAE,OAA6B;gBAClG,OAAO,IAAI,CAAC,EAAE,CAAC,sBAAsB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC1D,CAAC;SA0EF;;uGAxEgC,IAAU;YACvC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;YAChC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;YACzC,MAAM,oBAAoB,GAAG,IAAI,CAAC,iBAAiB,IAAI,IAAI,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,CAAC;YACzF,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,oBAAoB,CAAC,EAAE,CAAC;gBACnD,OAAO;YACT,CAAC;YAED,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YAC5B,MAAM,gBAAgB,GAAa,EAAE,CAAC;YACtC,IAAI,WAAW,GAAG,KAAK,CAAC;YAExB,IAAI,SAAS,EAAE,CAAC;gBACd,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;oBACvB,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;oBAClB,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;oBACpC,MAAM,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;oBACrF,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,UAAU,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBAC9D,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;oBAC1C,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC;yBACjB,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;yBACjB,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC;yBAClC,MAAM,CAAC,KAAK,CAAC,CAAC;oBACjB,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC;yBACzB,IAAI,CAAC,OAAO,EAAE,iCAAiC,CAAC;yBAChD,MAAM,CAAC,GAAG,CAAC,CAAC;oBACf,IAAI,CAAC,OAAO,EAAE,CAAC;wBACb,MAAM,OAAO,GAAG,0CAA0C,CAAC;wBAC3D,SAAS,CAAC,MAAM,CACd,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,iCAAiC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAC7E,CAAC;oBACJ,CAAC;oBACD,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;oBAC1B,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC;wBAClB,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBAC5B,CAAC;gBACH,CAAC,CAAC,CAAC;gBACH,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAChC,WAAW,GAAG,IAAI,CAAC;oBACnB,uCAAuC;oBACvC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;gBAChF,CAAC;YACH,CAAC;YAED,qBAAqB;YACrB,IAAI,oBAAoB,EAAE,CAAC;gBACzB,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;oBACrB,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;oBACnB,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;oBACpC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,SAAS,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;oBACzE,IAAI,SAAS,EAAE,CAAC;wBACd,IAAI,YAAgC,CAAC;wBACrC,IAAI,IAAI,CAAC,EAAE,KAAK,OAAO,EAAE,CAAC;4BACxB,MAAM,YAAY,GAAG,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,OAAO,IAAI,GAAG,CAAC,OAAO,KAAK,OAAO,CAAC,EAAE,YAAY,EAAE,UAAU,CAAC;4BACxI,YAAY,GAAG,YAAY,IAAI,UAAU,OAAO,UAAU,IAAI,CAAC,EAAE,EAAE,CAAC;wBACtE,CAAC;6BACI,CAAC;4BACJ,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE,UAAU,CAAC;4BACpF,YAAY,GAAG,YAAY,IAAI,UAAU,OAAO,EAAE,CAAC;wBACrD,CAAC;wBACD,IAAI,YAAY,EAAE,CAAC;4BACjB,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;4BAC/B,WAAW,GAAG,IAAI,CAAC;wBACrB,CAAC;oBACH,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC;YAED,IAAI,WAAW,EAAE,CAAC;gBAChB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;YAC1B,CAAC;QACH,CAAC;WACF;AACH,CAAC","sourcesContent":["import { load as cheerioLoad } from 'cheerio';\nimport { type APIConstructor } from \".\";\nimport { type Product, type Post } from \"../../entities\";\nimport { type GetContentContext, type ContentListSortBy, type ContentType, type GetContentListParams } from \"../types/Content.js\";\nimport RawDataExtractor from '../web/utils/RawDataExtractor.js';\nimport { URLHelper } from '../../utils/index.js';\n\nconst DEFAULT_CONTENT_LIST_SIZE = 10;\nconst DEFAULT_CONTENT_LIST_SORT_BY: ContentListSortBy = 'a-z';\n\nexport function ContentAPIMixin<TBase extends APIConstructor>(Base: TBase) {\n return class ContentAPI extends Base {\n getContentList<T extends ContentType>(params: GetContentListParams<T>) {\n const { sortBy = DEFAULT_CONTENT_LIST_SORT_BY, limit = DEFAULT_CONTENT_LIST_SIZE, offset = 0 } = params;\n const list = this.db.getContentList({\n ...params,\n sortBy,\n limit,\n offset,\n });\n for (const item of list.items) {\n switch (item.type) {\n case 'post':\n this.#processPostContentInlineMedia(item);\n item.content = this.sanitizeHTML(item.content || '');\n break;\n case 'product': {\n const description = RawDataExtractor.getProductRichTextDescription(item);\n item.description = description ? this.sanitizeHTML(description) : null;\n break;\n }\n }\n }\n return list;\n }\n\n getPost(id: string) {\n const post = this.db.getContent(id, 'post');\n if (post) {\n this.#processPostContentInlineMedia(post);\n post.content = this.sanitizeHTML(post.content || '');\n }\n return post;\n }\n\n getProduct(id: string) {\n return this.db.getContent(id, 'product');\n }\n\n getPreviousNextContent<T extends ContentType>(content: Post | Product, context: GetContentContext<T>) {\n return this.db.getPreviousNextContent(content, context);\n }\n\n #processPostContentInlineMedia(post: Post) {\n const html = post.content || '';\n const hasImages = post.images.length > 0;\n const hasLinkedAttachments = post.linkedAttachments && post.linkedAttachments.length > 0;\n if (!html || (!hasImages && !hasLinkedAttachments)) {\n return;\n }\n\n const $ = cheerioLoad(html);\n const replacedMediaIds: string[] = [];\n let hasModified = false;\n\n if (hasImages) {\n $('img').each((_, _el) => {\n const el = $(_el);\n const id = el.attr('data-media-id');\n const matched = id ? post.images.find(img => img.id === id && img.downloaded) : null;\n const src = matched ? `/media/${matched.id}` : el.attr('src');\n const imgEl = $('<img>').attr('src', src);\n const aEl = $('<a>')\n .attr('href', src)\n .attr('class', 'lightgallery-item')\n .append(imgEl);\n const wrapperEl = $('<div>')\n .attr('class', 'post-card__inline-media-wrapper')\n .append(aEl);\n if (!matched) {\n const caption = \"(Externally hosted - not stored locally)\";\n wrapperEl.append(\n $('<span>').attr('class', 'post-card__inline-media-caption').append(caption)\n );\n }\n el.replaceWith(wrapperEl);\n if (id && matched) {\n replacedMediaIds.push(id);\n }\n });\n if (replacedMediaIds.length > 0) {\n hasModified = true;\n // Remove images that have been inlined\n post.images = post.images.filter((img) => !replacedMediaIds.includes(img.id));\n }\n }\n\n // Linked attachments\n if (hasLinkedAttachments) {\n $('a').each((_, _el) => {\n const aEl = $(_el);\n const href = aEl.attr('href') || '';\n const { validated, ownerId, mediaId } = URLHelper.isAttachmentLink(href);\n if (validated) {\n let modifiedPath: string | undefined;\n if (post.id !== ownerId) {\n const isDownloaded = post.linkedAttachments?.find((att) => att.postId === ownerId && att.mediaId === mediaId)?.downloadable?.downloaded;\n modifiedPath = isDownloaded && `/media/${mediaId}?lapid=${post.id}`;\n }\n else {\n const isDownloaded = post.attachments.find((att) => att.id === mediaId)?.downloaded;\n modifiedPath = isDownloaded && `/media/${mediaId}`;\n }\n if (modifiedPath) {\n aEl.attr('href', modifiedPath);\n hasModified = true;\n }\n }\n });\n }\n\n if (hasModified) {\n post.content = $.html();\n }\n }\n }\n}"]}
1
+ {"version":3,"file":"ContentAPIMixin.js","sourceRoot":"","sources":["../../../src/browse/api/ContentAPIMixin.ts"],"names":[],"mappings":"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","sourcesContent":["import { load as cheerioLoad } from 'cheerio';\nimport { type APIConstructor } from \".\";\nimport { type Product, type Post } from \"../../entities\";\nimport { type GetContentContext, type ContentListSortBy, type ContentType, type GetContentListParams, type GetCollectionListParams, type CollectionListSortBy, type GetPostTagListParams } from \"../types/Content.js\";\nimport RawDataExtractor from '../web/utils/RawDataExtractor.js';\nimport { URLHelper } from '../../utils/index.js';\n\nconst DEFAULT_CONTENT_LIST_SIZE = 10;\nconst DEFAULT_CONTENT_LIST_SORT_BY: ContentListSortBy = 'a-z';\n\nconst DEFAULT_COLLECTION_LIST_SIZE = 10;\nconst DEFAULT_COLLECTION_LIST_SORT_BY: CollectionListSortBy = 'a-z';\n\nexport function ContentAPIMixin<TBase extends APIConstructor>(Base: TBase) {\n return class ContentAPI extends Base {\n getContentList<T extends ContentType>(params: GetContentListParams<T>) {\n const { sortBy = DEFAULT_CONTENT_LIST_SORT_BY, limit = DEFAULT_CONTENT_LIST_SIZE, offset = 0 } = params;\n const list = this.db.getContentList({\n ...params,\n sortBy,\n limit,\n offset,\n });\n for (const item of list.items) {\n switch (item.type) {\n case 'post':\n this.#processPostContentInlineMedia(item);\n item.content = this.sanitizeHTML(item.content || '');\n break;\n case 'product': {\n const description = RawDataExtractor.getProductRichTextDescription(item);\n item.description = description ? this.sanitizeHTML(description) : null;\n break;\n }\n }\n }\n return list;\n }\n\n getPost(id: string) {\n const post = this.db.getContent(id, 'post');\n if (post) {\n this.#processPostContentInlineMedia(post);\n post.content = this.sanitizeHTML(post.content || '');\n }\n return post;\n }\n\n getProduct(id: string) {\n return this.db.getContent(id, 'product');\n }\n\n getPreviousNextContent<T extends ContentType>(content: Post | Product, context: GetContentContext<T>) {\n return this.db.getPreviousNextContent(content, context);\n }\n\n getCollection(id: string) {\n return this.db.getCollection(id);\n }\n\n getCollectionList(params: GetCollectionListParams) {\n const {\n search = '',\n sortBy = DEFAULT_COLLECTION_LIST_SORT_BY,\n limit = DEFAULT_COLLECTION_LIST_SIZE,\n offset = 0\n } = params;\n return this.db.getCollectionList({\n campaign: params.campaign,\n search,\n sortBy,\n limit,\n offset\n });\n }\n\n getPostTagList(params: GetPostTagListParams) {\n return this.db.getPostTagList({\n campaign: params.campaign,\n });\n }\n\n #processPostContentInlineMedia(post: Post) {\n const html = post.content || '';\n const hasImages = post.images.length > 0;\n const hasLinkedAttachments = post.linkedAttachments && post.linkedAttachments.length > 0;\n if (!html || (!hasImages && !hasLinkedAttachments)) {\n return;\n }\n\n const $ = cheerioLoad(html);\n const replacedMediaIds: string[] = [];\n let hasModified = false;\n\n if (hasImages) {\n $('img').each((_, _el) => {\n const el = $(_el);\n const id = el.attr('data-media-id');\n const matched = id ? post.images.find(img => img.id === id && img.downloaded) : null;\n const src = matched ? `/media/${matched.id}` : el.attr('src');\n const imgEl = $('<img>').attr('src', src);\n const aEl = $('<a>')\n .attr('href', src)\n .attr('class', 'lightgallery-item')\n .append(imgEl);\n const wrapperEl = $('<div>')\n .attr('class', 'post-card__inline-media-wrapper')\n .append(aEl);\n if (!matched) {\n const caption = \"(Externally hosted - not stored locally)\";\n wrapperEl.append(\n $('<span>').attr('class', 'post-card__inline-media-caption').append(caption)\n );\n }\n el.replaceWith(wrapperEl);\n if (id && matched) {\n replacedMediaIds.push(id);\n }\n });\n if (replacedMediaIds.length > 0) {\n hasModified = true;\n // Remove images that have been inlined\n post.images = post.images.filter((img) => !replacedMediaIds.includes(img.id));\n }\n }\n\n // Linked attachments\n if (hasLinkedAttachments) {\n $('a').each((_, _el) => {\n const aEl = $(_el);\n const href = aEl.attr('href') || '';\n const { validated, ownerId, mediaId } = URLHelper.isAttachmentLink(href);\n if (validated) {\n let modifiedPath: string | undefined;\n if (post.id !== ownerId) {\n const isDownloaded = post.linkedAttachments?.find((att) => att.postId === ownerId && att.mediaId === mediaId)?.downloadable?.downloaded;\n modifiedPath = isDownloaded && `/media/${mediaId}?lapid=${post.id}`;\n }\n else {\n const isDownloaded = post.attachments.find((att) => att.id === mediaId)?.downloaded;\n modifiedPath = isDownloaded && `/media/${mediaId}`;\n }\n if (modifiedPath) {\n aEl.attr('href', modifiedPath);\n hasModified = true;\n }\n }\n });\n }\n\n if (hasModified) {\n post.content = $.html();\n }\n }\n }\n}"]}
@@ -134,7 +134,27 @@ export function FilterAPIMixin(Base) {
134
134
  searchParam: 'date_published',
135
135
  options: datePublishedOptions
136
136
  });
137
- return { sections };
137
+ const { tags } = this.db.getPostTagList({ campaign: campaignId });
138
+ if (tags.length > 0) {
139
+ const tagOptions = tags.map((tag) => ({
140
+ title: tag.value,
141
+ value: tag.id
142
+ }));
143
+ sections.push({
144
+ title: 'Tagged',
145
+ displayHint: 'pill_small',
146
+ searchParam: 'tag_id',
147
+ options: tagOptions
148
+ });
149
+ }
150
+ return {
151
+ sections,
152
+ external: [
153
+ {
154
+ searchParam: 'search'
155
+ }
156
+ ]
157
+ };
138
158
  }
139
159
  getProductFilterData(campaignId) {
140
160
  const productCountByYear = this.db.getContentCountByDate('product', 'year', {
@@ -208,7 +228,14 @@ export function FilterAPIMixin(Base) {
208
228
  searchParam: 'date_published',
209
229
  options: datePublishedOptions
210
230
  });
211
- return { sections };
231
+ return {
232
+ sections,
233
+ external: [
234
+ {
235
+ searchParam: 'search'
236
+ }
237
+ ]
238
+ };
212
239
  }
213
240
  getMediaFilterData(campaignId) {
214
241
  const mediaCountByTier = this.db.getMediaCountByTier(campaignId);