wallpaper-engine-dsh 0.1.5 → 0.1.7
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/LICENSE +21 -0
- package/README.md +6 -5
- package/README.zh.md +5 -4
- package/lib/client.js +290 -53
- package/lib/core.js +68 -52
- package/lib/index.js +184 -110
- package/package.json +7 -3
package/lib/core.js
CHANGED
|
@@ -191,11 +191,13 @@ export function weBinaryName(platform = process.platform) {
|
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
193
|
* First candidate directory that contains the WE binary, in order.
|
|
194
|
+
* Async: io methods may be sync or return promises (the host injects
|
|
195
|
+
* fs/promises so discovery never blocks the event loop).
|
|
194
196
|
* @param {string[]} candidates
|
|
195
|
-
* @param {{ existsSync?: (p: string) => boolean
|
|
196
|
-
* @returns {string | null}
|
|
197
|
+
* @param {{ existsSync?: (p: string) => boolean | Promise<boolean>, platform?: string }} [io]
|
|
198
|
+
* @returns {Promise<string | null>}
|
|
197
199
|
*/
|
|
198
|
-
export function findInstallDir(candidates, io = {}) {
|
|
200
|
+
export async function findInstallDir(candidates, io = {}) {
|
|
199
201
|
const exists = io.existsSync || existsSync;
|
|
200
202
|
const bin = weBinaryName(io.platform || process.platform);
|
|
201
203
|
const seen = new Set();
|
|
@@ -204,7 +206,7 @@ export function findInstallDir(candidates, io = {}) {
|
|
|
204
206
|
const dir = normalize(String(raw));
|
|
205
207
|
if (seen.has(dir)) continue;
|
|
206
208
|
seen.add(dir);
|
|
207
|
-
if (exists(join(dir, bin))) return dir;
|
|
209
|
+
if (await exists(join(dir, bin))) return dir;
|
|
208
210
|
}
|
|
209
211
|
return null;
|
|
210
212
|
}
|
|
@@ -237,15 +239,15 @@ export function installDirCandidates(input = {}) {
|
|
|
237
239
|
* libraryfolders.vdf, so it must be recovered by direct inspection).
|
|
238
240
|
* @param {string[]} probes
|
|
239
241
|
* @param {string[]} vdfLibraries
|
|
240
|
-
* @param {{ existsSync?: (p: string) => boolean
|
|
241
|
-
* @returns {string[]}
|
|
242
|
+
* @param {{ existsSync?: (p: string) => boolean | Promise<boolean>, platform?: string }} [io]
|
|
243
|
+
* @returns {Promise<string[]>}
|
|
242
244
|
*/
|
|
243
|
-
export function workshopLibraryRoots(probes, vdfLibraries, io = {}) {
|
|
245
|
+
export async function workshopLibraryRoots(probes, vdfLibraries, io = {}) {
|
|
244
246
|
const exists = io.existsSync || existsSync;
|
|
245
247
|
const bin = weBinaryName(io.platform || process.platform);
|
|
246
248
|
const libs = [...vdfLibraries];
|
|
247
249
|
for (const probe of probes) {
|
|
248
|
-
if (exists(join(probe, 'steamapps', 'common', 'wallpaper_engine', bin))) {
|
|
250
|
+
if (await exists(join(probe, 'steamapps', 'common', 'wallpaper_engine', bin))) {
|
|
249
251
|
libs.push(probe);
|
|
250
252
|
}
|
|
251
253
|
}
|
|
@@ -298,18 +300,20 @@ export function isInsideDir(root, target, platform = process.platform) {
|
|
|
298
300
|
* server at arbitrary files.
|
|
299
301
|
*
|
|
300
302
|
* @param {string} dir
|
|
301
|
-
* @param {{ readFileSync?: (p: string, e: string) => string
|
|
303
|
+
* @param {{ readFileSync?: (p: string, e: string) => string | Promise<string>, existsSync?: (p: string) => boolean | Promise<boolean> }} [io]
|
|
302
304
|
*/
|
|
303
|
-
export function readProject(dir, io = {}) {
|
|
305
|
+
export async function readProject(dir, io = {}) {
|
|
304
306
|
const read = io.readFileSync || readFileSync;
|
|
305
307
|
const exists = io.existsSync || existsSync;
|
|
306
308
|
const pj = join(dir, 'project.json');
|
|
307
|
-
if (!exists(pj)) return null;
|
|
309
|
+
if (!await exists(pj)) return null;
|
|
308
310
|
try {
|
|
309
|
-
const o = JSON.parse(read(pj, 'utf8'));
|
|
311
|
+
const o = JSON.parse(await read(pj, 'utf8'));
|
|
310
312
|
if (!o || typeof o !== 'object' || typeof o.file !== 'string' || !o.file.trim()) return null;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
+
// A declared-but-unknown type (typo, trailing whitespace) is not
|
|
314
|
+
// evidence of "scene": fall back to extension inference first.
|
|
315
|
+
let type = typeof o.type === 'string' ? o.type.trim().toLowerCase() : '';
|
|
316
|
+
if (!KINDS.includes(type)) type = inferType(o.file);
|
|
313
317
|
const fileAbs = resolve(dir, o.file);
|
|
314
318
|
if (!isInsideDir(dir, fileAbs)) return null;
|
|
315
319
|
let previewAbs = null;
|
|
@@ -334,19 +338,19 @@ export function readProject(dir, io = {}) {
|
|
|
334
338
|
* Enumerate every wallpaper project under the given roots (first occurrence
|
|
335
339
|
* of an id wins), sorted case-insensitively by title.
|
|
336
340
|
* @param {string[]} roots
|
|
337
|
-
* @param {{ readdirSync?: (p: string) => string[]
|
|
341
|
+
* @param {{ readdirSync?: (p: string) => string[] | Promise<string[]>, statSync?: (p: string) => { isDirectory(): boolean } | Promise<{ isDirectory(): boolean }> }} [io]
|
|
338
342
|
*/
|
|
339
|
-
export function enumerateWallpapers(roots, io = {}) {
|
|
343
|
+
export async function enumerateWallpapers(roots, io = {}) {
|
|
340
344
|
const readdir = io.readdirSync || readdirSync;
|
|
341
345
|
const stat = io.statSync || statSync;
|
|
342
346
|
const found = new Map();
|
|
343
347
|
for (const root of roots) {
|
|
344
348
|
let entries;
|
|
345
|
-
try { entries = readdir(root); } catch { continue; }
|
|
349
|
+
try { entries = await readdir(root); } catch { continue; }
|
|
346
350
|
for (const entry of entries) {
|
|
347
351
|
const dir = join(root, entry);
|
|
348
|
-
try { if (!stat(dir).isDirectory()) continue; } catch { continue; }
|
|
349
|
-
const proj = readProject(dir, io);
|
|
352
|
+
try { if (!(await stat(dir)).isDirectory()) continue; } catch { continue; }
|
|
353
|
+
const proj = await readProject(dir, io);
|
|
350
354
|
if (!proj || found.has(proj.id)) continue;
|
|
351
355
|
found.set(proj.id, proj);
|
|
352
356
|
}
|
|
@@ -360,21 +364,21 @@ export function enumerateWallpapers(roots, io = {}) {
|
|
|
360
364
|
* plus the workshop content folder of every owning library.
|
|
361
365
|
* @param {string | null} installDir
|
|
362
366
|
* @param {string[]} libraryRoots
|
|
363
|
-
* @param {{ existsSync?: (p: string) => boolean }} [io]
|
|
364
|
-
* @returns {string[]}
|
|
367
|
+
* @param {{ existsSync?: (p: string) => boolean | Promise<boolean> }} [io]
|
|
368
|
+
* @returns {Promise<string[]>}
|
|
365
369
|
*/
|
|
366
|
-
export function wallpaperRoots(installDir, libraryRoots, io = {}) {
|
|
370
|
+
export async function wallpaperRoots(installDir, libraryRoots, io = {}) {
|
|
367
371
|
const exists = io.existsSync || existsSync;
|
|
368
372
|
const roots = [];
|
|
369
373
|
if (installDir) {
|
|
370
374
|
for (const sub of ['defaultprojects', 'myprojects']) {
|
|
371
375
|
const p = join(installDir, 'projects', sub);
|
|
372
|
-
if (exists(p)) roots.push(p);
|
|
376
|
+
if (await exists(p)) roots.push(p);
|
|
373
377
|
}
|
|
374
378
|
}
|
|
375
379
|
for (const lib of libraryRoots) {
|
|
376
380
|
const ws = join(lib, 'steamapps', 'workshop', 'content', WE_APPID);
|
|
377
|
-
if (exists(ws)) roots.push(ws);
|
|
381
|
+
if (await exists(ws)) roots.push(ws);
|
|
378
382
|
}
|
|
379
383
|
return roots;
|
|
380
384
|
}
|
|
@@ -463,30 +467,36 @@ export function resolvePlaylistItem(item, byPath, byId) {
|
|
|
463
467
|
/**
|
|
464
468
|
* Parse an HTTP Range header for a resource of `size` bytes.
|
|
465
469
|
* Supports `bytes=a-b`, `bytes=a-`, `bytes=-suffix`.
|
|
470
|
+
*
|
|
471
|
+
* RFC 9110 §14.2: a server MUST ignore a Range header it does not understand
|
|
472
|
+
* (malformed, wrong unit, multi-range) and answer 200 — 416 is reserved for
|
|
473
|
+
* syntactically valid but unsatisfiable ranges.
|
|
474
|
+
*
|
|
466
475
|
* @returns {null | { start: number, end: number } | { error: true }}
|
|
467
|
-
* null → no
|
|
468
|
-
* error → unsatisfiable
|
|
476
|
+
* null → no header, or an invalid/unsupported one: ignore it, serve 200
|
|
477
|
+
* error → valid syntax but unsatisfiable for this resource: answer 416
|
|
469
478
|
* start/end → inclusive byte range, answer 206
|
|
470
479
|
*/
|
|
471
480
|
export function parseRangeHeader(header, size) {
|
|
472
481
|
if (!header || typeof header !== 'string') return null;
|
|
473
482
|
if (!Number.isFinite(size) || size < 0) return { error: true };
|
|
474
483
|
const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
475
|
-
if (!m) return
|
|
484
|
+
if (!m) return null; // wrong unit / multi-range / garbage → ignore (200)
|
|
476
485
|
const [, a, b] = m;
|
|
477
|
-
if (a === '' && b === '') return
|
|
486
|
+
if (a === '' && b === '') return null; // "bytes=-" is not a range → 200
|
|
478
487
|
let start;
|
|
479
488
|
let end;
|
|
480
489
|
if (a === '') {
|
|
481
490
|
const suffix = parseInt(b, 10);
|
|
482
|
-
if (!Number.isFinite(suffix) || suffix <= 0) return
|
|
491
|
+
if (!Number.isFinite(suffix) || suffix <= 0) return null; // "bytes=-0"
|
|
483
492
|
start = Math.max(0, size - suffix);
|
|
484
493
|
end = size - 1;
|
|
485
494
|
} else {
|
|
486
495
|
start = parseInt(a, 10);
|
|
487
496
|
end = b === '' ? size - 1 : Math.min(parseInt(b, 10), size - 1);
|
|
488
497
|
}
|
|
489
|
-
if (!Number.isFinite(start) || !Number.isFinite(end)) return
|
|
498
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
499
|
+
// Valid syntax; unsatisfiable for this resource.
|
|
490
500
|
if (size === 0 || start > end || start >= size) return { error: true };
|
|
491
501
|
return { start, end };
|
|
492
502
|
}
|
|
@@ -517,13 +527,16 @@ export function mimeFor(absPath) {
|
|
|
517
527
|
*
|
|
518
528
|
* @param {{ installDir: string | null, libraryRoots: string[] }} discovery
|
|
519
529
|
* @param {object} [io] overridable IO: existsSync/readFileSync/readdirSync/
|
|
520
|
-
* statSync (default to node:fs
|
|
521
|
-
*
|
|
522
|
-
*
|
|
530
|
+
* statSync (default to node:fs sync functions; each may also return a
|
|
531
|
+
* promise — the host injects fs/promises to keep the event loop free),
|
|
532
|
+
* plus:
|
|
533
|
+
* - `mint(entry)` — mint a media/preview token for a file entry; when it
|
|
534
|
+
* returns a falsy value (or is omitted) the URL is null, never
|
|
535
|
+
* "…/media/null".
|
|
523
536
|
* - `base` — URL prefix for media/preview links (default '/we-background').
|
|
524
|
-
* @returns {{ installDir, total, portableCount, wallpapers, playlists }}
|
|
537
|
+
* @returns {Promise<{ installDir, total, portableCount, wallpapers, playlists }>}
|
|
525
538
|
*/
|
|
526
|
-
export function buildInventoryFrom(discovery = {}, io = {}) {
|
|
539
|
+
export async function buildInventoryFrom(discovery = {}, io = {}) {
|
|
527
540
|
const exists = io.existsSync || existsSync;
|
|
528
541
|
const read = io.readFileSync || readFileSync;
|
|
529
542
|
const mint = io.mint || (() => null);
|
|
@@ -531,35 +544,38 @@ export function buildInventoryFrom(discovery = {}, io = {}) {
|
|
|
531
544
|
|
|
532
545
|
const installDir = discovery.installDir || null;
|
|
533
546
|
const libraryRoots = Array.isArray(discovery.libraryRoots) ? discovery.libraryRoots : [];
|
|
534
|
-
const roots = wallpaperRoots(installDir, libraryRoots, { existsSync: exists });
|
|
535
|
-
const projects = enumerateWallpapers(roots, io);
|
|
547
|
+
const roots = await wallpaperRoots(installDir, libraryRoots, { existsSync: exists });
|
|
548
|
+
const projects = await enumerateWallpapers(roots, io);
|
|
536
549
|
const byId = new Map(projects.map((p) => [p.id, p]));
|
|
537
550
|
const byPath = new Map(projects.map((p) => [pathKey(p.fileAbs), p.id]));
|
|
538
551
|
|
|
539
|
-
const wallpapers =
|
|
552
|
+
const wallpapers = [];
|
|
553
|
+
for (const p of projects) {
|
|
540
554
|
const portable = PORTABLE_KINDS.includes(p.type);
|
|
541
|
-
const hasMedia = portable && exists(p.fileAbs);
|
|
542
|
-
const hasPreview = Boolean(p.previewAbs) && exists(p.previewAbs);
|
|
543
|
-
|
|
555
|
+
const hasMedia = portable && await exists(p.fileAbs);
|
|
556
|
+
const hasPreview = Boolean(p.previewAbs) && await exists(p.previewAbs);
|
|
557
|
+
const mediaToken = hasMedia
|
|
558
|
+
? mint({ abs: p.fileAbs, rootDir: p.type === 'web' ? p.dir : null, key: `${p.id}:media` })
|
|
559
|
+
: null;
|
|
560
|
+
const previewToken = hasPreview
|
|
561
|
+
? mint({ abs: p.previewAbs, rootDir: null, key: `${p.id}:preview` })
|
|
562
|
+
: null;
|
|
563
|
+
wallpapers.push({
|
|
544
564
|
id: p.id,
|
|
545
565
|
title: p.title,
|
|
546
566
|
type: p.type,
|
|
547
|
-
playable: hasMedia,
|
|
548
|
-
media: hasMedia
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
? `${base}/preview/${mint({ abs: p.previewAbs, rootDir: null })}`
|
|
553
|
-
: null,
|
|
554
|
-
};
|
|
555
|
-
});
|
|
567
|
+
playable: hasMedia && Boolean(mediaToken),
|
|
568
|
+
media: hasMedia && mediaToken ? `${base}/media/${mediaToken}` : null,
|
|
569
|
+
preview: hasPreview && previewToken ? `${base}/preview/${previewToken}` : null,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
556
572
|
const playableIds = new Set(wallpapers.filter((w) => w.playable).map((w) => w.id));
|
|
557
573
|
|
|
558
574
|
let playlists = [];
|
|
559
575
|
const configPath = installDir ? join(installDir, 'config.json') : null;
|
|
560
|
-
if (configPath && exists(configPath)) {
|
|
576
|
+
if (configPath && await exists(configPath)) {
|
|
561
577
|
try {
|
|
562
|
-
playlists = parsePlaylists(JSON.parse(read(configPath, 'utf8')));
|
|
578
|
+
playlists = parsePlaylists(JSON.parse(await read(configPath, 'utf8')));
|
|
563
579
|
} catch { /* malformed config.json → no playlists, not fatal */ }
|
|
564
580
|
}
|
|
565
581
|
const playlistView = playlists.map((playlist, index) => {
|