rezo 1.0.66 → 1.0.68

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 (57) hide show
  1. package/dist/adapters/entries/curl.d.ts +5 -0
  2. package/dist/adapters/entries/fetch.d.ts +5 -0
  3. package/dist/adapters/entries/http.d.ts +5 -0
  4. package/dist/adapters/entries/http2.d.ts +5 -0
  5. package/dist/adapters/entries/react-native.d.ts +5 -0
  6. package/dist/adapters/entries/xhr.d.ts +5 -0
  7. package/dist/adapters/index.cjs +6 -6
  8. package/dist/cache/index.cjs +9 -9
  9. package/dist/crawler/crawler.cjs +26 -5
  10. package/dist/crawler/crawler.js +26 -5
  11. package/dist/crawler/index.cjs +40 -40
  12. package/dist/crawler.d.ts +10 -0
  13. package/dist/entries/crawler.cjs +4 -4
  14. package/dist/index.cjs +27 -27
  15. package/dist/index.d.ts +5 -0
  16. package/dist/internal/agents/index.cjs +10 -10
  17. package/dist/platform/browser.d.ts +5 -0
  18. package/dist/platform/bun.d.ts +5 -0
  19. package/dist/platform/deno.d.ts +5 -0
  20. package/dist/platform/node.d.ts +5 -0
  21. package/dist/platform/react-native.d.ts +5 -0
  22. package/dist/platform/worker.d.ts +5 -0
  23. package/dist/proxy/index.cjs +4 -4
  24. package/dist/proxy/manager.cjs +1 -1
  25. package/dist/proxy/manager.js +1 -1
  26. package/dist/queue/index.cjs +8 -8
  27. package/dist/queue/queue.cjs +3 -1
  28. package/dist/queue/queue.js +3 -1
  29. package/dist/responses/universal/index.cjs +11 -11
  30. package/dist/wget/asset-extractor.cjs +556 -0
  31. package/dist/wget/asset-extractor.js +553 -0
  32. package/dist/wget/asset-organizer.cjs +230 -0
  33. package/dist/wget/asset-organizer.js +227 -0
  34. package/dist/wget/download-cache.cjs +221 -0
  35. package/dist/wget/download-cache.js +218 -0
  36. package/dist/wget/downloader.cjs +607 -0
  37. package/dist/wget/downloader.js +604 -0
  38. package/dist/wget/file-writer.cjs +349 -0
  39. package/dist/wget/file-writer.js +346 -0
  40. package/dist/wget/filter-lists.cjs +1330 -0
  41. package/dist/wget/filter-lists.js +1330 -0
  42. package/dist/wget/index.cjs +633 -0
  43. package/dist/wget/index.d.ts +8486 -0
  44. package/dist/wget/index.js +614 -0
  45. package/dist/wget/link-converter.cjs +297 -0
  46. package/dist/wget/link-converter.js +294 -0
  47. package/dist/wget/progress.cjs +271 -0
  48. package/dist/wget/progress.js +266 -0
  49. package/dist/wget/resume.cjs +166 -0
  50. package/dist/wget/resume.js +163 -0
  51. package/dist/wget/robots.cjs +303 -0
  52. package/dist/wget/robots.js +300 -0
  53. package/dist/wget/types.cjs +200 -0
  54. package/dist/wget/types.js +197 -0
  55. package/dist/wget/url-filter.cjs +351 -0
  56. package/dist/wget/url-filter.js +348 -0
  57. package/package.json +6 -1
@@ -0,0 +1,230 @@
1
+ const { createHash } = require("node:crypto");
2
+ const { extname, basename, join } = require("node:path");
3
+ const DEFAULT_ASSET_FOLDERS = exports.DEFAULT_ASSET_FOLDERS = {
4
+ css: "css",
5
+ js: "js",
6
+ images: "images",
7
+ fonts: "fonts",
8
+ audio: "audio",
9
+ video: "video",
10
+ other: "assets"
11
+ };
12
+ const MIME_TO_ASSET = {
13
+ "text/css": "css",
14
+ "application/javascript": "js",
15
+ "application/x-javascript": "js",
16
+ "text/javascript": "js",
17
+ "application/ecmascript": "js",
18
+ "image/jpeg": "images",
19
+ "image/png": "images",
20
+ "image/gif": "images",
21
+ "image/webp": "images",
22
+ "image/svg+xml": "images",
23
+ "image/x-icon": "images",
24
+ "image/vnd.microsoft.icon": "images",
25
+ "image/bmp": "images",
26
+ "image/tiff": "images",
27
+ "image/avif": "images",
28
+ "font/woff": "fonts",
29
+ "font/woff2": "fonts",
30
+ "font/ttf": "fonts",
31
+ "font/otf": "fonts",
32
+ "application/font-woff": "fonts",
33
+ "application/font-woff2": "fonts",
34
+ "application/x-font-ttf": "fonts",
35
+ "application/x-font-otf": "fonts",
36
+ "application/vnd.ms-fontobject": "fonts",
37
+ "audio/mpeg": "audio",
38
+ "audio/mp3": "audio",
39
+ "audio/wav": "audio",
40
+ "audio/ogg": "audio",
41
+ "audio/webm": "audio",
42
+ "audio/aac": "audio",
43
+ "audio/flac": "audio",
44
+ "video/mp4": "video",
45
+ "video/webm": "video",
46
+ "video/ogg": "video",
47
+ "video/quicktime": "video",
48
+ "video/x-msvideo": "video",
49
+ "video/x-ms-wmv": "video"
50
+ };
51
+ const EXT_TO_ASSET = {
52
+ ".css": "css",
53
+ ".js": "js",
54
+ ".mjs": "js",
55
+ ".cjs": "js",
56
+ ".jpg": "images",
57
+ ".jpeg": "images",
58
+ ".png": "images",
59
+ ".gif": "images",
60
+ ".webp": "images",
61
+ ".svg": "images",
62
+ ".ico": "images",
63
+ ".bmp": "images",
64
+ ".tiff": "images",
65
+ ".tif": "images",
66
+ ".avif": "images",
67
+ ".woff": "fonts",
68
+ ".woff2": "fonts",
69
+ ".ttf": "fonts",
70
+ ".otf": "fonts",
71
+ ".eot": "fonts",
72
+ ".mp3": "audio",
73
+ ".wav": "audio",
74
+ ".ogg": "audio",
75
+ ".aac": "audio",
76
+ ".flac": "audio",
77
+ ".m4a": "audio",
78
+ ".mp4": "video",
79
+ ".webm": "video",
80
+ ".ogv": "video",
81
+ ".mov": "video",
82
+ ".avi": "video",
83
+ ".wmv": "video",
84
+ ".mkv": "video"
85
+ };
86
+
87
+ class AssetOrganizer {
88
+ options;
89
+ hashCache;
90
+ urlToPath;
91
+ filenameVersions;
92
+ constructor(options) {
93
+ this.options = options;
94
+ this.hashCache = new Map;
95
+ this.urlToPath = new Map;
96
+ this.filenameVersions = new Map;
97
+ }
98
+ computeHash(content) {
99
+ return createHash("md5").update(content).digest("hex");
100
+ }
101
+ getAssetFolder(mimeType, url) {
102
+ const folders = {
103
+ ...DEFAULT_ASSET_FOLDERS,
104
+ ...this.options.assetFolders
105
+ };
106
+ if (mimeType) {
107
+ const normalizedMime = mimeType.split(";")[0].trim().toLowerCase();
108
+ const assetType = MIME_TO_ASSET[normalizedMime];
109
+ if (assetType) {
110
+ return folders[assetType];
111
+ }
112
+ }
113
+ try {
114
+ const urlPath = new URL(url).pathname;
115
+ const ext = extname(urlPath).toLowerCase();
116
+ const assetType = EXT_TO_ASSET[ext];
117
+ if (assetType) {
118
+ return folders[assetType];
119
+ }
120
+ } catch {}
121
+ return folders.other;
122
+ }
123
+ shouldOrganize(mimeType, url) {
124
+ if (!this.options.organizeAssets) {
125
+ return false;
126
+ }
127
+ if (mimeType) {
128
+ const normalizedMime = mimeType.split(";")[0].trim().toLowerCase();
129
+ if (normalizedMime === "text/html" || normalizedMime === "application/xhtml+xml" || normalizedMime === "text/xml" || normalizedMime === "application/xml") {
130
+ return false;
131
+ }
132
+ }
133
+ try {
134
+ const urlPath = new URL(url).pathname;
135
+ const ext = extname(urlPath).toLowerCase();
136
+ if (ext === ".html" || ext === ".htm" || ext === ".xhtml") {
137
+ return false;
138
+ }
139
+ } catch {}
140
+ return true;
141
+ }
142
+ getOrganizedPath(url, content, mimeType) {
143
+ const existingPath = this.urlToPath.get(url);
144
+ if (existingPath) {
145
+ return {
146
+ path: existingPath,
147
+ isDuplicate: false
148
+ };
149
+ }
150
+ const hash = this.computeHash(content);
151
+ const existing = this.hashCache.get(hash);
152
+ if (existing) {
153
+ this.urlToPath.set(url, existing.path);
154
+ return {
155
+ path: existing.path,
156
+ isDuplicate: true,
157
+ originalUrl: existing.originalUrl
158
+ };
159
+ }
160
+ const folder = this.getAssetFolder(mimeType, url);
161
+ let filename = this.getFilename(url);
162
+ const basePath = join(folder, filename);
163
+ const finalPath = this.resolveCollision(basePath, hash);
164
+ this.hashCache.set(hash, {
165
+ hash,
166
+ path: finalPath,
167
+ originalUrl: url
168
+ });
169
+ this.urlToPath.set(url, finalPath);
170
+ return {
171
+ path: finalPath,
172
+ isDuplicate: false
173
+ };
174
+ }
175
+ getFilename(url) {
176
+ try {
177
+ const urlObj = new URL(url);
178
+ let pathname = urlObj.pathname;
179
+ if (pathname.endsWith("/")) {
180
+ pathname = pathname.slice(0, -1);
181
+ }
182
+ let filename = basename(pathname);
183
+ if (!filename || filename === "/") {
184
+ filename = "index";
185
+ }
186
+ const queryIndex = filename.indexOf("?");
187
+ if (queryIndex > 0) {
188
+ filename = filename.slice(0, queryIndex);
189
+ }
190
+ return filename;
191
+ } catch {
192
+ return "asset";
193
+ }
194
+ }
195
+ resolveCollision(basePath, hash) {
196
+ const existingVersion = this.filenameVersions.get(basePath);
197
+ if (existingVersion === undefined) {
198
+ this.filenameVersions.set(basePath, 1);
199
+ return basePath;
200
+ }
201
+ const ext = extname(basePath);
202
+ const nameWithoutExt = basePath.slice(0, -ext.length || undefined);
203
+ const newVersion = existingVersion + 1;
204
+ this.filenameVersions.set(basePath, newVersion);
205
+ const versionedPath = `${nameWithoutExt}_v${newVersion}${ext}`;
206
+ return versionedPath;
207
+ }
208
+ getPathForUrl(url) {
209
+ return this.urlToPath.get(url);
210
+ }
211
+ getUrlMappings() {
212
+ return new Map(this.urlToPath);
213
+ }
214
+ clear() {
215
+ this.hashCache.clear();
216
+ this.urlToPath.clear();
217
+ this.filenameVersions.clear();
218
+ }
219
+ getStats() {
220
+ return {
221
+ uniqueFiles: this.hashCache.size,
222
+ duplicatesFound: this.urlToPath.size - this.hashCache.size,
223
+ totalUrls: this.urlToPath.size
224
+ };
225
+ }
226
+ }
227
+
228
+ exports.AssetOrganizer = AssetOrganizer;
229
+ exports.default = AssetOrganizer;
230
+ module.exports = Object.assign(AssetOrganizer, exports);
@@ -0,0 +1,227 @@
1
+ import { createHash } from "node:crypto";
2
+ import { extname, basename, join } from "node:path";
3
+ export const DEFAULT_ASSET_FOLDERS = {
4
+ css: "css",
5
+ js: "js",
6
+ images: "images",
7
+ fonts: "fonts",
8
+ audio: "audio",
9
+ video: "video",
10
+ other: "assets"
11
+ };
12
+ const MIME_TO_ASSET = {
13
+ "text/css": "css",
14
+ "application/javascript": "js",
15
+ "application/x-javascript": "js",
16
+ "text/javascript": "js",
17
+ "application/ecmascript": "js",
18
+ "image/jpeg": "images",
19
+ "image/png": "images",
20
+ "image/gif": "images",
21
+ "image/webp": "images",
22
+ "image/svg+xml": "images",
23
+ "image/x-icon": "images",
24
+ "image/vnd.microsoft.icon": "images",
25
+ "image/bmp": "images",
26
+ "image/tiff": "images",
27
+ "image/avif": "images",
28
+ "font/woff": "fonts",
29
+ "font/woff2": "fonts",
30
+ "font/ttf": "fonts",
31
+ "font/otf": "fonts",
32
+ "application/font-woff": "fonts",
33
+ "application/font-woff2": "fonts",
34
+ "application/x-font-ttf": "fonts",
35
+ "application/x-font-otf": "fonts",
36
+ "application/vnd.ms-fontobject": "fonts",
37
+ "audio/mpeg": "audio",
38
+ "audio/mp3": "audio",
39
+ "audio/wav": "audio",
40
+ "audio/ogg": "audio",
41
+ "audio/webm": "audio",
42
+ "audio/aac": "audio",
43
+ "audio/flac": "audio",
44
+ "video/mp4": "video",
45
+ "video/webm": "video",
46
+ "video/ogg": "video",
47
+ "video/quicktime": "video",
48
+ "video/x-msvideo": "video",
49
+ "video/x-ms-wmv": "video"
50
+ };
51
+ const EXT_TO_ASSET = {
52
+ ".css": "css",
53
+ ".js": "js",
54
+ ".mjs": "js",
55
+ ".cjs": "js",
56
+ ".jpg": "images",
57
+ ".jpeg": "images",
58
+ ".png": "images",
59
+ ".gif": "images",
60
+ ".webp": "images",
61
+ ".svg": "images",
62
+ ".ico": "images",
63
+ ".bmp": "images",
64
+ ".tiff": "images",
65
+ ".tif": "images",
66
+ ".avif": "images",
67
+ ".woff": "fonts",
68
+ ".woff2": "fonts",
69
+ ".ttf": "fonts",
70
+ ".otf": "fonts",
71
+ ".eot": "fonts",
72
+ ".mp3": "audio",
73
+ ".wav": "audio",
74
+ ".ogg": "audio",
75
+ ".aac": "audio",
76
+ ".flac": "audio",
77
+ ".m4a": "audio",
78
+ ".mp4": "video",
79
+ ".webm": "video",
80
+ ".ogv": "video",
81
+ ".mov": "video",
82
+ ".avi": "video",
83
+ ".wmv": "video",
84
+ ".mkv": "video"
85
+ };
86
+
87
+ export class AssetOrganizer {
88
+ options;
89
+ hashCache;
90
+ urlToPath;
91
+ filenameVersions;
92
+ constructor(options) {
93
+ this.options = options;
94
+ this.hashCache = new Map;
95
+ this.urlToPath = new Map;
96
+ this.filenameVersions = new Map;
97
+ }
98
+ computeHash(content) {
99
+ return createHash("md5").update(content).digest("hex");
100
+ }
101
+ getAssetFolder(mimeType, url) {
102
+ const folders = {
103
+ ...DEFAULT_ASSET_FOLDERS,
104
+ ...this.options.assetFolders
105
+ };
106
+ if (mimeType) {
107
+ const normalizedMime = mimeType.split(";")[0].trim().toLowerCase();
108
+ const assetType = MIME_TO_ASSET[normalizedMime];
109
+ if (assetType) {
110
+ return folders[assetType];
111
+ }
112
+ }
113
+ try {
114
+ const urlPath = new URL(url).pathname;
115
+ const ext = extname(urlPath).toLowerCase();
116
+ const assetType = EXT_TO_ASSET[ext];
117
+ if (assetType) {
118
+ return folders[assetType];
119
+ }
120
+ } catch {}
121
+ return folders.other;
122
+ }
123
+ shouldOrganize(mimeType, url) {
124
+ if (!this.options.organizeAssets) {
125
+ return false;
126
+ }
127
+ if (mimeType) {
128
+ const normalizedMime = mimeType.split(";")[0].trim().toLowerCase();
129
+ if (normalizedMime === "text/html" || normalizedMime === "application/xhtml+xml" || normalizedMime === "text/xml" || normalizedMime === "application/xml") {
130
+ return false;
131
+ }
132
+ }
133
+ try {
134
+ const urlPath = new URL(url).pathname;
135
+ const ext = extname(urlPath).toLowerCase();
136
+ if (ext === ".html" || ext === ".htm" || ext === ".xhtml") {
137
+ return false;
138
+ }
139
+ } catch {}
140
+ return true;
141
+ }
142
+ getOrganizedPath(url, content, mimeType) {
143
+ const existingPath = this.urlToPath.get(url);
144
+ if (existingPath) {
145
+ return {
146
+ path: existingPath,
147
+ isDuplicate: false
148
+ };
149
+ }
150
+ const hash = this.computeHash(content);
151
+ const existing = this.hashCache.get(hash);
152
+ if (existing) {
153
+ this.urlToPath.set(url, existing.path);
154
+ return {
155
+ path: existing.path,
156
+ isDuplicate: true,
157
+ originalUrl: existing.originalUrl
158
+ };
159
+ }
160
+ const folder = this.getAssetFolder(mimeType, url);
161
+ let filename = this.getFilename(url);
162
+ const basePath = join(folder, filename);
163
+ const finalPath = this.resolveCollision(basePath, hash);
164
+ this.hashCache.set(hash, {
165
+ hash,
166
+ path: finalPath,
167
+ originalUrl: url
168
+ });
169
+ this.urlToPath.set(url, finalPath);
170
+ return {
171
+ path: finalPath,
172
+ isDuplicate: false
173
+ };
174
+ }
175
+ getFilename(url) {
176
+ try {
177
+ const urlObj = new URL(url);
178
+ let pathname = urlObj.pathname;
179
+ if (pathname.endsWith("/")) {
180
+ pathname = pathname.slice(0, -1);
181
+ }
182
+ let filename = basename(pathname);
183
+ if (!filename || filename === "/") {
184
+ filename = "index";
185
+ }
186
+ const queryIndex = filename.indexOf("?");
187
+ if (queryIndex > 0) {
188
+ filename = filename.slice(0, queryIndex);
189
+ }
190
+ return filename;
191
+ } catch {
192
+ return "asset";
193
+ }
194
+ }
195
+ resolveCollision(basePath, hash) {
196
+ const existingVersion = this.filenameVersions.get(basePath);
197
+ if (existingVersion === undefined) {
198
+ this.filenameVersions.set(basePath, 1);
199
+ return basePath;
200
+ }
201
+ const ext = extname(basePath);
202
+ const nameWithoutExt = basePath.slice(0, -ext.length || undefined);
203
+ const newVersion = existingVersion + 1;
204
+ this.filenameVersions.set(basePath, newVersion);
205
+ const versionedPath = `${nameWithoutExt}_v${newVersion}${ext}`;
206
+ return versionedPath;
207
+ }
208
+ getPathForUrl(url) {
209
+ return this.urlToPath.get(url);
210
+ }
211
+ getUrlMappings() {
212
+ return new Map(this.urlToPath);
213
+ }
214
+ clear() {
215
+ this.hashCache.clear();
216
+ this.urlToPath.clear();
217
+ this.filenameVersions.clear();
218
+ }
219
+ getStats() {
220
+ return {
221
+ uniqueFiles: this.hashCache.size,
222
+ duplicatesFound: this.urlToPath.size - this.hashCache.size,
223
+ totalUrls: this.urlToPath.size
224
+ };
225
+ }
226
+ }
227
+ export default AssetOrganizer;
@@ -0,0 +1,221 @@
1
+ const { promises: fs } = require("node:fs");
2
+ const { createHash } = require("node:crypto");
3
+ const { join, dirname } = require("node:path");
4
+ const { cwd } = require("node:process");
5
+
6
+ class DownloadCache {
7
+ outputDir;
8
+ baseUrl;
9
+ cacheDir;
10
+ cacheFile;
11
+ data = null;
12
+ dirty = false;
13
+ saveTimeout = null;
14
+ static VERSION = 1;
15
+ static CACHE_DIR = ".rezo-wget";
16
+ constructor(outputDir, baseUrl) {
17
+ this.outputDir = outputDir;
18
+ this.baseUrl = baseUrl;
19
+ this.cacheDir = join(cwd(), DownloadCache.CACHE_DIR);
20
+ const hash = this.generateCacheHash();
21
+ this.cacheFile = join(this.cacheDir, `${hash}.json`);
22
+ }
23
+ generateCacheHash() {
24
+ return createHash("md5").update(this.baseUrl).digest("hex").slice(0, 12);
25
+ }
26
+ urlHash(url) {
27
+ return createHash("md5").update(url).digest("hex");
28
+ }
29
+ async load() {
30
+ try {
31
+ await fs.mkdir(this.cacheDir, { recursive: true });
32
+ const content = await fs.readFile(this.cacheFile, "utf-8");
33
+ const data = JSON.parse(content);
34
+ if (data.version !== DownloadCache.VERSION) {
35
+ this.data = this.createEmptyCache();
36
+ return;
37
+ }
38
+ this.data = data;
39
+ } catch (error) {
40
+ this.data = this.createEmptyCache();
41
+ }
42
+ }
43
+ createEmptyCache() {
44
+ return {
45
+ version: DownloadCache.VERSION,
46
+ created: Date.now(),
47
+ updated: Date.now(),
48
+ configHash: this.generateCacheHash(),
49
+ baseUrl: this.baseUrl,
50
+ entries: {}
51
+ };
52
+ }
53
+ async check(url) {
54
+ if (!this.data) {
55
+ await this.load();
56
+ }
57
+ const key = this.urlHash(url);
58
+ const entry = this.data.entries[key];
59
+ if (!entry) {
60
+ return { cached: false, reason: "not-found" };
61
+ }
62
+ for (const filename of entry.filenames) {
63
+ const fullPath = join(this.outputDir, filename);
64
+ try {
65
+ const stat = await fs.stat(fullPath);
66
+ if (stat.size === entry.totalBytes) {
67
+ return {
68
+ cached: true,
69
+ entry,
70
+ filename
71
+ };
72
+ }
73
+ } catch {}
74
+ }
75
+ if (entry.filenames.length > 0) {
76
+ return { cached: false, reason: "file-missing", entry };
77
+ }
78
+ return { cached: false, reason: "size-mismatch", entry };
79
+ }
80
+ get(url) {
81
+ if (!this.data)
82
+ return;
83
+ return this.data.entries[this.urlHash(url)];
84
+ }
85
+ set(url, entry) {
86
+ if (!this.data) {
87
+ this.data = this.createEmptyCache();
88
+ }
89
+ const key = this.urlHash(url);
90
+ const existing = this.data.entries[key];
91
+ this.data.entries[key] = {
92
+ url,
93
+ ...entry,
94
+ filenames: existing ? [...new Set([...existing.filenames, ...entry.filenames])] : entry.filenames
95
+ };
96
+ this.data.updated = Date.now();
97
+ this.dirty = true;
98
+ this.scheduleSave();
99
+ }
100
+ addFilename(url, filename) {
101
+ if (!this.data)
102
+ return;
103
+ const key = this.urlHash(url);
104
+ const entry = this.data.entries[key];
105
+ if (entry && !entry.filenames.includes(filename)) {
106
+ entry.filenames.push(filename);
107
+ this.data.updated = Date.now();
108
+ this.dirty = true;
109
+ this.scheduleSave();
110
+ }
111
+ }
112
+ delete(url) {
113
+ if (!this.data)
114
+ return;
115
+ const key = this.urlHash(url);
116
+ if (this.data.entries[key]) {
117
+ delete this.data.entries[key];
118
+ this.data.updated = Date.now();
119
+ this.dirty = true;
120
+ this.scheduleSave();
121
+ }
122
+ }
123
+ has(url) {
124
+ if (!this.data)
125
+ return false;
126
+ return this.urlHash(url) in this.data.entries;
127
+ }
128
+ urls() {
129
+ if (!this.data)
130
+ return [];
131
+ return Object.values(this.data.entries).map((e) => e.url);
132
+ }
133
+ stats() {
134
+ if (!this.data) {
135
+ return { entries: 0, totalBytes: 0, filesCount: 0 };
136
+ }
137
+ const entries = Object.values(this.data.entries);
138
+ return {
139
+ entries: entries.length,
140
+ totalBytes: entries.reduce((sum, e) => sum + e.totalBytes, 0),
141
+ filesCount: entries.reduce((sum, e) => sum + e.filenames.length, 0)
142
+ };
143
+ }
144
+ scheduleSave() {
145
+ if (this.saveTimeout) {
146
+ clearTimeout(this.saveTimeout);
147
+ }
148
+ this.saveTimeout = setTimeout(() => this.save(), 1000);
149
+ }
150
+ async save() {
151
+ if (!this.data || !this.dirty)
152
+ return;
153
+ if (this.saveTimeout) {
154
+ clearTimeout(this.saveTimeout);
155
+ this.saveTimeout = null;
156
+ }
157
+ try {
158
+ await fs.mkdir(dirname(this.cacheFile), { recursive: true });
159
+ await fs.writeFile(this.cacheFile, JSON.stringify(this.data, null, 2), "utf-8");
160
+ this.dirty = false;
161
+ } catch (error) {
162
+ console.error("Failed to save download cache:", error);
163
+ }
164
+ }
165
+ clear() {
166
+ if (this.data) {
167
+ this.data.entries = {};
168
+ this.data.updated = Date.now();
169
+ this.dirty = true;
170
+ this.scheduleSave();
171
+ }
172
+ }
173
+ async cleanup() {
174
+ if (!this.data)
175
+ return 0;
176
+ let removed = 0;
177
+ const entries = Object.entries(this.data.entries);
178
+ for (const [key, entry] of entries) {
179
+ let hasValidFile = false;
180
+ const validFilenames = [];
181
+ for (const filename of entry.filenames) {
182
+ const fullPath = join(this.outputDir, filename);
183
+ try {
184
+ await fs.stat(fullPath);
185
+ validFilenames.push(filename);
186
+ hasValidFile = true;
187
+ } catch {}
188
+ }
189
+ if (!hasValidFile) {
190
+ delete this.data.entries[key];
191
+ removed++;
192
+ } else if (validFilenames.length !== entry.filenames.length) {
193
+ entry.filenames = validFilenames;
194
+ }
195
+ }
196
+ if (removed > 0) {
197
+ this.data.updated = Date.now();
198
+ this.dirty = true;
199
+ await this.save();
200
+ }
201
+ return removed;
202
+ }
203
+ async destroy() {
204
+ if (this.saveTimeout) {
205
+ clearTimeout(this.saveTimeout);
206
+ this.saveTimeout = null;
207
+ }
208
+ await this.save();
209
+ this.data = null;
210
+ }
211
+ get filePath() {
212
+ return this.cacheFile;
213
+ }
214
+ get dirPath() {
215
+ return this.cacheDir;
216
+ }
217
+ }
218
+
219
+ exports.DownloadCache = DownloadCache;
220
+ exports.default = DownloadCache;
221
+ module.exports = Object.assign(DownloadCache, exports);