wallpaper-engine-dsh 0.1.0 → 0.1.3
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.
- package/README.md +41 -67
- package/README.zh.md +40 -68
- package/lib/client.js +648 -145
- package/lib/core.js +96 -4
- package/lib/index.js +32 -69
- package/package.json +17 -2
package/lib/core.js
CHANGED
|
@@ -272,11 +272,21 @@ export function inferType(file) {
|
|
|
272
272
|
* @returns {boolean}
|
|
273
273
|
*/
|
|
274
274
|
export function isInsideDir(root, target, platform = process.platform) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
const win = platform === 'win32';
|
|
276
|
+
// Canonicalize for comparison: win32 paths are case-insensitive and accept
|
|
277
|
+
// both separators, so normalize `/` and `\` to `\` and lowercase. This also
|
|
278
|
+
// makes the win32 semantics identical no matter which OS the code runs on
|
|
279
|
+
// (CI is Linux; the WE files themselves are Windows paths).
|
|
280
|
+
const canon = (p) => {
|
|
281
|
+
let r = resolve(String(p));
|
|
282
|
+
if (win) r = r.replace(/[\\/]/g, '\\').toLowerCase();
|
|
283
|
+
return r;
|
|
284
|
+
};
|
|
285
|
+
const r = canon(root);
|
|
286
|
+
const t = canon(target);
|
|
278
287
|
if (t === r) return true;
|
|
279
|
-
const
|
|
288
|
+
const boundary = win ? '\\' : sep;
|
|
289
|
+
const withSep = r.endsWith(boundary) ? r : r + boundary;
|
|
280
290
|
return t.startsWith(withSep);
|
|
281
291
|
}
|
|
282
292
|
|
|
@@ -497,3 +507,85 @@ export function mimeFor(absPath) {
|
|
|
497
507
|
woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf',
|
|
498
508
|
}[ext] || 'application/octet-stream';
|
|
499
509
|
}
|
|
510
|
+
|
|
511
|
+
// ── 6. Inventory assembly (pure; the host wires it to HTTP) ──────────────────
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Assemble the JSON inventory payload from a discovery result. Pure on top of
|
|
515
|
+
* the enumeration/playlist helpers, so the host's HTTP layer stays thin and
|
|
516
|
+
* this function is directly unit-testable against fixture trees.
|
|
517
|
+
*
|
|
518
|
+
* @param {{ installDir: string | null, libraryRoots: string[] }} discovery
|
|
519
|
+
* @param {object} [io] overridable IO: existsSync/readFileSync/readdirSync/
|
|
520
|
+
* statSync (default to node:fs), plus:
|
|
521
|
+
* - `mint(entry)` — mint a media/preview token for a file entry; the host
|
|
522
|
+
* provides a random-token map, tests provide a spy.
|
|
523
|
+
* - `base` — URL prefix for media/preview links (default '/we-background').
|
|
524
|
+
* @returns {{ installDir, total, portableCount, wallpapers, playlists }}
|
|
525
|
+
*/
|
|
526
|
+
export function buildInventoryFrom(discovery = {}, io = {}) {
|
|
527
|
+
const exists = io.existsSync || existsSync;
|
|
528
|
+
const read = io.readFileSync || readFileSync;
|
|
529
|
+
const mint = io.mint || (() => null);
|
|
530
|
+
const base = io.base || '/we-background';
|
|
531
|
+
|
|
532
|
+
const installDir = discovery.installDir || null;
|
|
533
|
+
const libraryRoots = Array.isArray(discovery.libraryRoots) ? discovery.libraryRoots : [];
|
|
534
|
+
const roots = wallpaperRoots(installDir, libraryRoots, { existsSync: exists });
|
|
535
|
+
const projects = enumerateWallpapers(roots, io);
|
|
536
|
+
const byId = new Map(projects.map((p) => [p.id, p]));
|
|
537
|
+
const byPath = new Map(projects.map((p) => [pathKey(p.fileAbs), p.id]));
|
|
538
|
+
|
|
539
|
+
const wallpapers = projects.map((p) => {
|
|
540
|
+
const portable = PORTABLE_KINDS.includes(p.type);
|
|
541
|
+
const hasMedia = portable && exists(p.fileAbs);
|
|
542
|
+
const hasPreview = Boolean(p.previewAbs) && exists(p.previewAbs);
|
|
543
|
+
return {
|
|
544
|
+
id: p.id,
|
|
545
|
+
title: p.title,
|
|
546
|
+
type: p.type,
|
|
547
|
+
playable: hasMedia,
|
|
548
|
+
media: hasMedia
|
|
549
|
+
? `${base}/media/${mint({ abs: p.fileAbs, rootDir: p.type === 'web' ? p.dir : null })}`
|
|
550
|
+
: null,
|
|
551
|
+
preview: hasPreview
|
|
552
|
+
? `${base}/preview/${mint({ abs: p.previewAbs, rootDir: null })}`
|
|
553
|
+
: null,
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
const playableIds = new Set(wallpapers.filter((w) => w.playable).map((w) => w.id));
|
|
557
|
+
|
|
558
|
+
let playlists = [];
|
|
559
|
+
const configPath = installDir ? join(installDir, 'config.json') : null;
|
|
560
|
+
if (configPath && exists(configPath)) {
|
|
561
|
+
try {
|
|
562
|
+
playlists = parsePlaylists(JSON.parse(read(configPath, 'utf8')));
|
|
563
|
+
} catch { /* malformed config.json → no playlists, not fatal */ }
|
|
564
|
+
}
|
|
565
|
+
const playlistView = playlists.map((playlist, index) => {
|
|
566
|
+
const ids = [];
|
|
567
|
+
const seen = new Set();
|
|
568
|
+
for (const item of playlist.items) {
|
|
569
|
+
const id = resolvePlaylistItem(item, byPath, byId);
|
|
570
|
+
if (id && !seen.has(id)) { seen.add(id); ids.push(id); }
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
id: `pl-${index}`,
|
|
574
|
+
name: playlist.name,
|
|
575
|
+
order: playlist.order,
|
|
576
|
+
delay: playlist.delay,
|
|
577
|
+
wallpaperIds: ids,
|
|
578
|
+
total: ids.length,
|
|
579
|
+
portableCount: ids.filter((id) => playableIds.has(id)).length,
|
|
580
|
+
unresolvedCount: Math.max(0, playlist.items.length - ids.length),
|
|
581
|
+
};
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
installDir,
|
|
586
|
+
total: wallpapers.length,
|
|
587
|
+
portableCount: wallpapers.filter((w) => w.playable).length,
|
|
588
|
+
wallpapers,
|
|
589
|
+
playlists: playlistView,
|
|
590
|
+
};
|
|
591
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -47,8 +47,6 @@ import * as core from './core.js';
|
|
|
47
47
|
const BASE = '/we-background';
|
|
48
48
|
/** Inventory is cached and rebuilt at most this often unless ?refresh=1. */
|
|
49
49
|
const INVENTORY_TTL_MS = 30_000;
|
|
50
|
-
/** Media kinds eligible for sub-asset serving (web wallpapers bundle files). */
|
|
51
|
-
const SUBASSET_KINDS = new Set(['web']);
|
|
52
50
|
|
|
53
51
|
/** Steam root recorded by the Windows installer; null elsewhere/on failure. */
|
|
54
52
|
function steamRegistryRoot() {
|
|
@@ -107,74 +105,24 @@ export function apply(ctx) {
|
|
|
107
105
|
// ── Inventory (cached) ──────────────────────────────────────────────────
|
|
108
106
|
let cache = null; // { at: number, payload: object }
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const portable = core.PORTABLE_KINDS.includes(p.type);
|
|
120
|
-
const hasMedia = portable && existsSync(p.fileAbs);
|
|
121
|
-
const hasPreview = Boolean(p.previewAbs) && existsSync(p.previewAbs);
|
|
122
|
-
return {
|
|
123
|
-
id: p.id,
|
|
124
|
-
title: p.title,
|
|
125
|
-
type: p.type,
|
|
126
|
-
playable: hasMedia,
|
|
127
|
-
media: hasMedia
|
|
128
|
-
? `${BASE}/media/${mint({
|
|
129
|
-
abs: p.fileAbs,
|
|
130
|
-
rootDir: SUBASSET_KINDS.has(p.type) ? p.dir : null,
|
|
131
|
-
})}`
|
|
132
|
-
: null,
|
|
133
|
-
preview: hasPreview
|
|
134
|
-
? `${BASE}/preview/${mint({ abs: p.previewAbs, rootDir: null })}`
|
|
135
|
-
: null,
|
|
136
|
-
};
|
|
137
|
-
});
|
|
138
|
-
const playableIds = new Set(wallpapers.filter((w) => w.playable).map((w) => w.id));
|
|
139
|
-
|
|
140
|
-
let playlists = [];
|
|
141
|
-
const configPath = installDir ? join(installDir, 'config.json') : null;
|
|
142
|
-
if (configPath && existsSync(configPath)) {
|
|
143
|
-
try {
|
|
144
|
-
playlists = core.parsePlaylists(JSON.parse(readFileSync(configPath, 'utf8')));
|
|
145
|
-
} catch { /* malformed config.json → no playlists, not fatal */ }
|
|
146
|
-
}
|
|
147
|
-
const playlistView = playlists.map((playlist, index) => {
|
|
148
|
-
const ids = [];
|
|
149
|
-
const seen = new Set();
|
|
150
|
-
for (const item of playlist.items) {
|
|
151
|
-
const id = core.resolvePlaylistItem(item, byPath, byId);
|
|
152
|
-
if (id && !seen.has(id)) { seen.add(id); ids.push(id); }
|
|
153
|
-
}
|
|
154
|
-
return {
|
|
155
|
-
id: `pl-${index}`,
|
|
156
|
-
name: playlist.name,
|
|
157
|
-
order: playlist.order,
|
|
158
|
-
delay: playlist.delay,
|
|
159
|
-
wallpaperIds: ids,
|
|
160
|
-
total: ids.length,
|
|
161
|
-
portableCount: ids.filter((id) => playableIds.has(id)).length,
|
|
162
|
-
unresolvedCount: Math.max(0, playlist.items.length - ids.length),
|
|
163
|
-
};
|
|
164
|
-
});
|
|
108
|
+
// Discovery is expensive (reg.exe + vdf scan) and the install layout barely
|
|
109
|
+
// changes — resolve it ONCE and reuse across builds; only an explicit
|
|
110
|
+
// refresh (or TTL expiry on the first build) forces a re-discovery.
|
|
111
|
+
let discoveryCache = null;
|
|
112
|
+
function getDiscovery(force) {
|
|
113
|
+
if (!force && discoveryCache) return discoveryCache;
|
|
114
|
+
discoveryCache = discover();
|
|
115
|
+
return discoveryCache;
|
|
116
|
+
}
|
|
165
117
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
portableCount: wallpapers.filter((w) => w.playable).length,
|
|
170
|
-
wallpapers,
|
|
171
|
-
playlists: playlistView,
|
|
172
|
-
};
|
|
118
|
+
function buildInventory(force) {
|
|
119
|
+
tokens.clear(); // re-mint: tokens from older builds stop resolving
|
|
120
|
+
return core.buildInventoryFrom(getDiscovery(force), { mint });
|
|
173
121
|
}
|
|
174
122
|
|
|
175
123
|
function getInventory(force) {
|
|
176
124
|
if (!force && cache && Date.now() - cache.at < INVENTORY_TTL_MS) return cache.payload;
|
|
177
|
-
const payload = buildInventory();
|
|
125
|
+
const payload = buildInventory(force);
|
|
178
126
|
cache = { at: Date.now(), payload };
|
|
179
127
|
return payload;
|
|
180
128
|
}
|
|
@@ -235,10 +183,18 @@ export function apply(ctx) {
|
|
|
235
183
|
kind: 'exact',
|
|
236
184
|
path: `${BASE}/inventory`,
|
|
237
185
|
handler: (req, res) => {
|
|
238
|
-
if (req.method !== 'GET'
|
|
186
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
187
|
+
sendJson(res, 405, { error: 'method not allowed' });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
239
190
|
try {
|
|
240
191
|
const url = new URL(req.url || '/', 'http://x');
|
|
241
|
-
|
|
192
|
+
const payload = JSON.stringify(getInventory(url.searchParams.get('refresh') === '1'));
|
|
193
|
+
res.statusCode = 200;
|
|
194
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
195
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
196
|
+
res.setHeader('Content-Length', String(Buffer.byteLength(payload)));
|
|
197
|
+
res.end(req.method === 'HEAD' ? undefined : payload);
|
|
242
198
|
} catch (err) {
|
|
243
199
|
sendJson(res, 500, { error: String(err && err.message ? err.message : err) });
|
|
244
200
|
}
|
|
@@ -273,7 +229,8 @@ export function apply(ctx) {
|
|
|
273
229
|
const entry = tokens.get(token);
|
|
274
230
|
if (!entry) {
|
|
275
231
|
res.statusCode = 404;
|
|
276
|
-
res.end(
|
|
232
|
+
if (req.method === 'HEAD') res.end();
|
|
233
|
+
else res.end('not found');
|
|
277
234
|
return;
|
|
278
235
|
}
|
|
279
236
|
let abs = entry.abs;
|
|
@@ -281,7 +238,8 @@ export function apply(ctx) {
|
|
|
281
238
|
// Sub-asset of a bundled web wallpaper: must stay inside its dir.
|
|
282
239
|
if (seg !== 'media' || !entry.rootDir) {
|
|
283
240
|
res.statusCode = 404;
|
|
284
|
-
res.end(
|
|
241
|
+
if (req.method === 'HEAD') res.end();
|
|
242
|
+
else res.end('not found');
|
|
285
243
|
return;
|
|
286
244
|
}
|
|
287
245
|
let rel;
|
|
@@ -304,11 +262,16 @@ export function apply(ctx) {
|
|
|
304
262
|
}));
|
|
305
263
|
}
|
|
306
264
|
|
|
265
|
+
// Warm the discovery cache now (reg.exe + vdf scan) so the first inventory
|
|
266
|
+
// request answers immediately instead of paying the discovery cost inline.
|
|
267
|
+
try { getDiscovery(false); } catch { /* non-fatal: inventory will 500 with reason */ }
|
|
268
|
+
|
|
307
269
|
return () => {
|
|
308
270
|
for (const d of disposers) { try { d(); } catch { /* ignore */ } }
|
|
309
271
|
disposers.length = 0;
|
|
310
272
|
tokens.clear();
|
|
311
273
|
cache = null;
|
|
274
|
+
discoveryCache = null;
|
|
312
275
|
};
|
|
313
276
|
}
|
|
314
277
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallpaper-engine-dsh",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Wallpaper Engine Video/Web wallpapers as the DSH web GUI background — discovery, sandboxed rendering, rotation lists, liquid-glass tuning.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -61,7 +61,10 @@
|
|
|
61
61
|
"verify": "node scripts/verify-client.mjs",
|
|
62
62
|
"test": "node --test test/core.test.mjs",
|
|
63
63
|
"prepare": "node scripts/prepare.mjs",
|
|
64
|
-
"prepublishOnly": "npm run test && npm run build && npm run verify"
|
|
64
|
+
"prepublishOnly": "npm run test && npm run build && npm run verify",
|
|
65
|
+
"release": "npm version patch -m \"chore: release v%s\" && git push --follow-tags",
|
|
66
|
+
"release:minor": "npm version minor -m \"chore: release v%s\" && git push --follow-tags",
|
|
67
|
+
"release:major": "npm version major -m \"chore: release v%s\" && git push --follow-tags"
|
|
65
68
|
},
|
|
66
69
|
"engines": {
|
|
67
70
|
"node": ">=18.17"
|
|
@@ -74,8 +77,20 @@
|
|
|
74
77
|
"react": "^18.2.0"
|
|
75
78
|
},
|
|
76
79
|
"peerDependenciesMeta": {
|
|
80
|
+
"@deepseek-ai/cordis": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@deepseek-ai/dsh-client-runtime": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
77
89
|
"@deepseek-ai/dsh-host-webserver": {
|
|
78
90
|
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"react": {
|
|
93
|
+
"optional": true
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
}
|