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/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, platform?: string }} [io]
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, platform?: string }} [io]
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, existsSync?: (p: string) => boolean }} [io]
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
- let type = typeof o.type === 'string' ? o.type.toLowerCase() : inferType(o.file);
312
- if (!KINDS.includes(type)) type = 'scene';
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[], statSync?: (p: string) => { isDirectory(): boolean } }} [io]
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/empty header, serve the whole body (200)
468
- * error → unsatisfiable or malformed, answer 416
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 { error: true };
484
+ if (!m) return null; // wrong unit / multi-range / garbage → ignore (200)
476
485
  const [, a, b] = m;
477
- if (a === '' && b === '') return { error: true };
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 { error: true };
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 { error: true };
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), 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.
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 = projects.map((p) => {
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
- return {
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
- ? `${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
- });
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) => {