kitowall 2.1.0 → 2.3.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.
- package/dist/cli.js +57 -9
- package/dist/core/workshop.js +441 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -118,6 +118,8 @@ Commands:
|
|
|
118
118
|
Show system logs (requests/downloads/errors)
|
|
119
119
|
logs clear Clear system logs
|
|
120
120
|
we config set-api-key <key> Save Steam Web API key (~/.config/kitowall/we.json)
|
|
121
|
+
we config get-steam-roots Show configured manual Steam roots
|
|
122
|
+
we config set-steam-roots <a,b,c> Save manual Steam roots (steam root or workshop/content/431960)
|
|
121
123
|
we search [--text <q>] [--tags <a,b>] [--sort <top|newest|trend|subscribed|updated>] [--page <n>] [--page-size <n>] [--days <n>] [--fixtures]
|
|
122
124
|
Search Wallpaper Engine workshop items (appid 431960)
|
|
123
125
|
we details <publishedfileid> [--fixtures]
|
|
@@ -127,6 +129,11 @@ Commands:
|
|
|
127
129
|
we job <job_id> Show one download job
|
|
128
130
|
we jobs [--limit <n>] List recent download jobs
|
|
129
131
|
we library List downloaded workshop items
|
|
132
|
+
we scan-steam Detect Steam workshop folders and list downloaded ids
|
|
133
|
+
we sync-steam Sync local Steam Workshop 431960 items into Kitsune downloads
|
|
134
|
+
we app-status Detect if Wallpaper Engine (AppID 431960) is installed
|
|
135
|
+
we active Show current livewallpaper authority/lock state
|
|
136
|
+
we stop [--monitor <name> | --all] Stop livewallpaper instances and restore previous services
|
|
130
137
|
we coexist enter|exit|status Temporarily stop/restore wallpaper rotation services
|
|
131
138
|
check [--namespace <ns>] [--json] Quick system check (no changes)
|
|
132
139
|
|
|
@@ -303,14 +310,26 @@ async function main() {
|
|
|
303
310
|
}
|
|
304
311
|
if (action === 'config') {
|
|
305
312
|
const sub = cleanOpt(args[2] ?? null);
|
|
306
|
-
if (sub
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
if (sub === 'set-api-key') {
|
|
314
|
+
const key = cleanOpt(args[3] ?? null);
|
|
315
|
+
if (!key)
|
|
316
|
+
throw new Error('Usage: we config set-api-key <key>');
|
|
317
|
+
(0, workshop_1.setWorkshopApiKey)(key);
|
|
318
|
+
console.log(JSON.stringify({ ok: true, updated: 'steamWebApiKey' }, null, 2));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (sub === 'get-steam-roots') {
|
|
322
|
+
console.log(JSON.stringify({ ok: true, ...(0, workshop_1.workshopGetSteamRoots)() }, null, 2));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (sub === 'set-steam-roots') {
|
|
326
|
+
const raw = cleanOpt(args[3] ?? null) ?? '';
|
|
327
|
+
const roots = raw ? raw.split(',').map(v => v.trim()).filter(Boolean) : [];
|
|
328
|
+
const out = (0, workshop_1.workshopSetSteamRoots)(roots);
|
|
329
|
+
console.log(JSON.stringify(out, null, 2));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
throw new Error('Usage: we config <set-api-key|get-steam-roots|set-steam-roots> ...');
|
|
314
333
|
}
|
|
315
334
|
if (action === 'search') {
|
|
316
335
|
const text = cleanOpt(getOptionValue(args, '--text'));
|
|
@@ -386,6 +405,35 @@ async function main() {
|
|
|
386
405
|
console.log(JSON.stringify(out, null, 2));
|
|
387
406
|
return;
|
|
388
407
|
}
|
|
408
|
+
if (action === 'scan-steam') {
|
|
409
|
+
const out = (0, workshop_1.workshopScanSteamDownloads)();
|
|
410
|
+
console.log(JSON.stringify({ ok: true, ...out }, null, 2));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (action === 'sync-steam') {
|
|
414
|
+
const out = (0, workshop_1.workshopSyncSteamDownloads)();
|
|
415
|
+
console.log(JSON.stringify(out, null, 2));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (action === 'app-status') {
|
|
419
|
+
const out = (0, workshop_1.workshopWallpaperEngineStatus)();
|
|
420
|
+
console.log(JSON.stringify(out, null, 2));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (action === 'active') {
|
|
424
|
+
const out = (0, workshop_1.workshopActiveStatus)();
|
|
425
|
+
console.log(JSON.stringify(out, null, 2));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (action === 'stop') {
|
|
429
|
+
const monitor = cleanOpt(getOptionValue(args, '--monitor'));
|
|
430
|
+
const out = await (0, workshop_1.workshopStop)({
|
|
431
|
+
monitor,
|
|
432
|
+
all: args.includes('--all') || !monitor
|
|
433
|
+
});
|
|
434
|
+
console.log(JSON.stringify(out, null, 2));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
389
437
|
if (action === 'coexist') {
|
|
390
438
|
const sub = cleanOpt(args[2] ?? null);
|
|
391
439
|
if (sub === 'enter') {
|
|
@@ -405,7 +453,7 @@ async function main() {
|
|
|
405
453
|
}
|
|
406
454
|
throw new Error('Usage: we coexist <enter|exit|status>');
|
|
407
455
|
}
|
|
408
|
-
throw new Error('Usage: we <config|search|details|download|job|jobs|library|run-job|coexist> ...');
|
|
456
|
+
throw new Error('Usage: we <config|search|details|download|job|jobs|library|scan-steam|sync-steam|app-status|active|stop|run-job|coexist> ...');
|
|
409
457
|
}
|
|
410
458
|
// Regular commands (need config/state)
|
|
411
459
|
const config = (0, config_1.loadConfig)();
|
package/dist/core/workshop.js
CHANGED
|
@@ -4,12 +4,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.setWorkshopApiKey = setWorkshopApiKey;
|
|
7
|
+
exports.workshopGetSteamRoots = workshopGetSteamRoots;
|
|
8
|
+
exports.workshopSetSteamRoots = workshopSetSteamRoots;
|
|
9
|
+
exports.workshopWallpaperEngineStatus = workshopWallpaperEngineStatus;
|
|
10
|
+
exports.workshopScanSteamDownloads = workshopScanSteamDownloads;
|
|
11
|
+
exports.workshopSyncSteamDownloads = workshopSyncSteamDownloads;
|
|
7
12
|
exports.workshopSearch = workshopSearch;
|
|
8
13
|
exports.workshopDetails = workshopDetails;
|
|
9
14
|
exports.workshopQueueDownload = workshopQueueDownload;
|
|
15
|
+
exports.workshopActiveStatus = workshopActiveStatus;
|
|
16
|
+
exports.workshopSetActive = workshopSetActive;
|
|
10
17
|
exports.workshopCoexistenceEnter = workshopCoexistenceEnter;
|
|
11
18
|
exports.workshopCoexistenceExit = workshopCoexistenceExit;
|
|
12
19
|
exports.workshopCoexistenceStatus = workshopCoexistenceStatus;
|
|
20
|
+
exports.workshopStop = workshopStop;
|
|
13
21
|
exports.workshopRunJob = workshopRunJob;
|
|
14
22
|
exports.workshopGetJob = workshopGetJob;
|
|
15
23
|
exports.workshopListJobs = workshopListJobs;
|
|
@@ -59,6 +67,7 @@ function ensureWePaths(paths) {
|
|
|
59
67
|
(0, fs_1.ensureDir)(paths.runtime);
|
|
60
68
|
(0, fs_1.ensureDir)(node_path_1.default.join(paths.cache, 'search'));
|
|
61
69
|
(0, fs_1.ensureDir)(node_path_1.default.join(paths.steamcmd, 'logs'));
|
|
70
|
+
(0, fs_1.ensureDir)(node_path_1.default.join(paths.root, 'coexistence', 'snapshots'));
|
|
62
71
|
}
|
|
63
72
|
function getWeConfigPath() {
|
|
64
73
|
return node_path_1.default.join(node_os_1.default.homedir(), '.config', 'kitowall', 'we.json');
|
|
@@ -74,6 +83,16 @@ function readWeConfig() {
|
|
|
74
83
|
return {};
|
|
75
84
|
}
|
|
76
85
|
}
|
|
86
|
+
function normalizePath(input) {
|
|
87
|
+
const raw = clean(input) ?? '';
|
|
88
|
+
if (!raw)
|
|
89
|
+
return '';
|
|
90
|
+
if (raw === '~')
|
|
91
|
+
return node_os_1.default.homedir();
|
|
92
|
+
if (raw.startsWith('~/'))
|
|
93
|
+
return node_path_1.default.join(node_os_1.default.homedir(), raw.slice(2));
|
|
94
|
+
return raw;
|
|
95
|
+
}
|
|
77
96
|
function setWorkshopApiKey(apiKey) {
|
|
78
97
|
const key = clean(apiKey);
|
|
79
98
|
if (!key)
|
|
@@ -84,6 +103,20 @@ function setWorkshopApiKey(apiKey) {
|
|
|
84
103
|
(0, fs_1.writeJson)(p, current);
|
|
85
104
|
return { ok: true };
|
|
86
105
|
}
|
|
106
|
+
function workshopGetSteamRoots() {
|
|
107
|
+
const cfg = readWeConfig();
|
|
108
|
+
const steamRoots = Array.isArray(cfg.steamRoots)
|
|
109
|
+
? cfg.steamRoots.map(v => normalizePath(String(v))).filter(Boolean)
|
|
110
|
+
: [];
|
|
111
|
+
return { steam_roots: Array.from(new Set(steamRoots)) };
|
|
112
|
+
}
|
|
113
|
+
function workshopSetSteamRoots(roots) {
|
|
114
|
+
const cfg = readWeConfig();
|
|
115
|
+
const normalized = roots.map(v => normalizePath(String(v))).filter(Boolean);
|
|
116
|
+
cfg.steamRoots = Array.from(new Set(normalized));
|
|
117
|
+
(0, fs_1.writeJson)(getWeConfigPath(), cfg);
|
|
118
|
+
return { ok: true, steam_roots: cfg.steamRoots };
|
|
119
|
+
}
|
|
87
120
|
function getSteamWebApiKey() {
|
|
88
121
|
const envKey = clean(process.env.STEAM_WEB_API_KEY) ?? clean(process.env.KITOWALL_STEAM_WEB_API_KEY);
|
|
89
122
|
if (envKey)
|
|
@@ -322,6 +355,281 @@ function writeMetaFile(meta) {
|
|
|
322
355
|
cached_at: now()
|
|
323
356
|
});
|
|
324
357
|
}
|
|
358
|
+
function normalizeWallpaperType(raw) {
|
|
359
|
+
const t = clean(raw)?.toLowerCase();
|
|
360
|
+
if (!t)
|
|
361
|
+
return 'unknown';
|
|
362
|
+
if (t === 'video')
|
|
363
|
+
return 'video';
|
|
364
|
+
if (t === 'scene')
|
|
365
|
+
return 'scene';
|
|
366
|
+
if (t === 'web')
|
|
367
|
+
return 'web';
|
|
368
|
+
if (t === 'application')
|
|
369
|
+
return 'application';
|
|
370
|
+
return 'unknown';
|
|
371
|
+
}
|
|
372
|
+
function detectAudioReactiveFromText(text) {
|
|
373
|
+
return /(audio|spectrum|visualizer|now[\s_-]?playing|media[\s_-]?integration|fft|bass|reactive)/i.test(text);
|
|
374
|
+
}
|
|
375
|
+
function readProjectInfo(dir) {
|
|
376
|
+
const projectPath = node_path_1.default.join(dir, 'project.json');
|
|
377
|
+
if (!node_fs_1.default.existsSync(projectPath)) {
|
|
378
|
+
return { type: 'unknown', audioReactive: false };
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const raw = node_fs_1.default.readFileSync(projectPath, 'utf8');
|
|
382
|
+
const json = JSON.parse(raw);
|
|
383
|
+
const type = normalizeWallpaperType(json.type);
|
|
384
|
+
const title = clean(json.title) ?? '';
|
|
385
|
+
const audioReactive = detectAudioReactiveFromText(`${title}\n${raw}`);
|
|
386
|
+
return { type, audioReactive };
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return { type: 'unknown', audioReactive: false };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function readLibraryFoldersVdf(vdfPath) {
|
|
393
|
+
if (!node_fs_1.default.existsSync(vdfPath))
|
|
394
|
+
return [];
|
|
395
|
+
const raw = node_fs_1.default.readFileSync(vdfPath, 'utf8');
|
|
396
|
+
const out = [];
|
|
397
|
+
const lines = raw.split('\n');
|
|
398
|
+
for (const line of lines) {
|
|
399
|
+
const m = line.match(/"path"\s+"([^"]+)"/);
|
|
400
|
+
if (!m || !m[1])
|
|
401
|
+
continue;
|
|
402
|
+
const candidate = m[1].replace(/\\\\/g, '\\');
|
|
403
|
+
out.push(candidate);
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
function getDefaultSteamRoots() {
|
|
408
|
+
const home = node_os_1.default.homedir();
|
|
409
|
+
const roots = [
|
|
410
|
+
node_path_1.default.join(home, '.steam', 'steam'),
|
|
411
|
+
node_path_1.default.join(home, '.local', 'share', 'Steam'),
|
|
412
|
+
node_path_1.default.join(home, '.var', 'app', 'com.valvesoftware.Steam', '.local', 'share', 'Steam')
|
|
413
|
+
];
|
|
414
|
+
return Array.from(new Set(roots));
|
|
415
|
+
}
|
|
416
|
+
function getManualSteamRoots() {
|
|
417
|
+
const cfg = readWeConfig();
|
|
418
|
+
if (!Array.isArray(cfg.steamRoots))
|
|
419
|
+
return [];
|
|
420
|
+
return cfg.steamRoots.map(v => normalizePath(String(v))).filter(Boolean);
|
|
421
|
+
}
|
|
422
|
+
function detectSteamappsDirs() {
|
|
423
|
+
const steamappsSet = new Set();
|
|
424
|
+
const candidateRoots = [...getDefaultSteamRoots(), ...getManualSteamRoots()];
|
|
425
|
+
for (const root of candidateRoots) {
|
|
426
|
+
const p = normalizePath(root);
|
|
427
|
+
if (!p || !node_fs_1.default.existsSync(p))
|
|
428
|
+
continue;
|
|
429
|
+
if (node_path_1.default.basename(p) === 'steamapps' && node_fs_1.default.existsSync(p)) {
|
|
430
|
+
steamappsSet.add(p);
|
|
431
|
+
}
|
|
432
|
+
const directSteamapps = node_path_1.default.join(p, 'steamapps');
|
|
433
|
+
if (node_fs_1.default.existsSync(directSteamapps)) {
|
|
434
|
+
steamappsSet.add(directSteamapps);
|
|
435
|
+
}
|
|
436
|
+
const marker = `${node_path_1.default.sep}steamapps${node_path_1.default.sep}workshop${node_path_1.default.sep}content${node_path_1.default.sep}${WE_APP_ID}`;
|
|
437
|
+
const idx = p.indexOf(marker);
|
|
438
|
+
if (idx >= 0) {
|
|
439
|
+
const steamapps = p.slice(0, idx + `${node_path_1.default.sep}steamapps`.length);
|
|
440
|
+
if (node_fs_1.default.existsSync(steamapps))
|
|
441
|
+
steamappsSet.add(steamapps);
|
|
442
|
+
}
|
|
443
|
+
const markerNoSteamapps = `${node_path_1.default.sep}workshop${node_path_1.default.sep}content${node_path_1.default.sep}${WE_APP_ID}`;
|
|
444
|
+
const idx2 = p.indexOf(markerNoSteamapps);
|
|
445
|
+
if (idx2 >= 0) {
|
|
446
|
+
const steamappsMaybe = p.slice(0, idx2);
|
|
447
|
+
if (node_path_1.default.basename(steamappsMaybe) === 'steamapps' && node_fs_1.default.existsSync(steamappsMaybe)) {
|
|
448
|
+
steamappsSet.add(steamappsMaybe);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const baseSteamapps = Array.from(steamappsSet);
|
|
453
|
+
for (const steamapps of baseSteamapps) {
|
|
454
|
+
const vdf = node_path_1.default.join(steamapps, 'libraryfolders.vdf');
|
|
455
|
+
for (const lib of readLibraryFoldersVdf(vdf)) {
|
|
456
|
+
const candidate = node_path_1.default.join(lib, 'steamapps');
|
|
457
|
+
if (node_fs_1.default.existsSync(candidate))
|
|
458
|
+
steamappsSet.add(candidate);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return Array.from(steamappsSet);
|
|
462
|
+
}
|
|
463
|
+
function toWorkshopAppContentDir(inputPath) {
|
|
464
|
+
const p = normalizePath(inputPath);
|
|
465
|
+
if (!p)
|
|
466
|
+
return '';
|
|
467
|
+
const direct = p;
|
|
468
|
+
const asSteamRoot = node_path_1.default.join(p, 'steamapps', 'workshop', 'content', String(WE_APP_ID));
|
|
469
|
+
const asSteamappsRoot = node_path_1.default.join(p, 'workshop', 'content', String(WE_APP_ID));
|
|
470
|
+
const asContentRoot = node_path_1.default.join(p, String(WE_APP_ID));
|
|
471
|
+
if (node_fs_1.default.existsSync(direct) && direct.endsWith(`${node_path_1.default.sep}${WE_APP_ID}`))
|
|
472
|
+
return direct;
|
|
473
|
+
if (node_fs_1.default.existsSync(direct) && direct.includes(`${node_path_1.default.sep}workshop${node_path_1.default.sep}content${node_path_1.default.sep}${WE_APP_ID}`))
|
|
474
|
+
return direct;
|
|
475
|
+
if (node_fs_1.default.existsSync(asSteamRoot))
|
|
476
|
+
return asSteamRoot;
|
|
477
|
+
if (node_fs_1.default.existsSync(asSteamappsRoot))
|
|
478
|
+
return asSteamappsRoot;
|
|
479
|
+
if (node_fs_1.default.existsSync(asContentRoot))
|
|
480
|
+
return asContentRoot;
|
|
481
|
+
return '';
|
|
482
|
+
}
|
|
483
|
+
function detectSteamWorkshopContentDirs() {
|
|
484
|
+
const discovered = new Set();
|
|
485
|
+
for (const root of getManualSteamRoots()) {
|
|
486
|
+
const fromManual = toWorkshopAppContentDir(root);
|
|
487
|
+
if (fromManual && node_fs_1.default.existsSync(fromManual))
|
|
488
|
+
discovered.add(fromManual);
|
|
489
|
+
}
|
|
490
|
+
for (const steamapps of detectSteamappsDirs()) {
|
|
491
|
+
const candidate = node_path_1.default.join(steamapps, 'workshop', 'content', String(WE_APP_ID));
|
|
492
|
+
if (node_fs_1.default.existsSync(candidate)) {
|
|
493
|
+
discovered.add(candidate);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return Array.from(discovered);
|
|
497
|
+
}
|
|
498
|
+
function workshopWallpaperEngineStatus() {
|
|
499
|
+
const manifests = [];
|
|
500
|
+
const steamapps = detectSteamappsDirs();
|
|
501
|
+
for (const dir of steamapps) {
|
|
502
|
+
const manifest = node_path_1.default.join(dir, `appmanifest_${WE_APP_ID}.acf`);
|
|
503
|
+
if (node_fs_1.default.existsSync(manifest))
|
|
504
|
+
manifests.push(manifest);
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
ok: true,
|
|
508
|
+
installed: manifests.length > 0,
|
|
509
|
+
manifests,
|
|
510
|
+
steamapps
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function findPreviewCandidate(dir) {
|
|
514
|
+
if (!node_fs_1.default.existsSync(dir))
|
|
515
|
+
return undefined;
|
|
516
|
+
const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
517
|
+
const preferred = ['preview.gif', 'preview.webm', 'preview.mp4', 'preview.jpg', 'preview.png', 'preview.webp'];
|
|
518
|
+
for (const name of preferred) {
|
|
519
|
+
const full = node_path_1.default.join(dir, name);
|
|
520
|
+
if (node_fs_1.default.existsSync(full) && node_fs_1.default.statSync(full).isFile())
|
|
521
|
+
return full;
|
|
522
|
+
}
|
|
523
|
+
for (const entry of entries) {
|
|
524
|
+
if (!entry.isFile())
|
|
525
|
+
continue;
|
|
526
|
+
const lower = entry.name.toLowerCase();
|
|
527
|
+
if (lower.startsWith('preview.') || lower.startsWith('thumbnail.')) {
|
|
528
|
+
return node_path_1.default.join(dir, entry.name);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
function workshopScanSteamDownloads() {
|
|
534
|
+
const sources = detectSteamWorkshopContentDirs();
|
|
535
|
+
const idsSet = new Set();
|
|
536
|
+
for (const source of sources) {
|
|
537
|
+
if (!node_fs_1.default.existsSync(source))
|
|
538
|
+
continue;
|
|
539
|
+
const entries = node_fs_1.default.readdirSync(source, { withFileTypes: true });
|
|
540
|
+
for (const entry of entries) {
|
|
541
|
+
if (!entry.isDirectory())
|
|
542
|
+
continue;
|
|
543
|
+
const id = clean(entry.name);
|
|
544
|
+
if (!id)
|
|
545
|
+
continue;
|
|
546
|
+
idsSet.add(id);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const ids = Array.from(idsSet).sort();
|
|
550
|
+
return { sources, count: ids.length, ids };
|
|
551
|
+
}
|
|
552
|
+
function workshopSyncSteamDownloads() {
|
|
553
|
+
const app = workshopWallpaperEngineStatus();
|
|
554
|
+
if (!app.installed) {
|
|
555
|
+
throw new Error('Wallpaper Engine (AppID 431960) is not installed in Steam. Install it first to sync downloads.');
|
|
556
|
+
}
|
|
557
|
+
const paths = getWePaths();
|
|
558
|
+
ensureWePaths(paths);
|
|
559
|
+
const sources = detectSteamWorkshopContentDirs();
|
|
560
|
+
let imported = 0;
|
|
561
|
+
let skipped = 0;
|
|
562
|
+
const seen = new Set();
|
|
563
|
+
for (const source of sources) {
|
|
564
|
+
if (!node_fs_1.default.existsSync(source))
|
|
565
|
+
continue;
|
|
566
|
+
const entries = node_fs_1.default.readdirSync(source, { withFileTypes: true });
|
|
567
|
+
for (const entry of entries) {
|
|
568
|
+
if (!entry.isDirectory())
|
|
569
|
+
continue;
|
|
570
|
+
const id = clean(entry.name);
|
|
571
|
+
if (!id || seen.has(id))
|
|
572
|
+
continue;
|
|
573
|
+
seen.add(id);
|
|
574
|
+
const sourceDir = node_path_1.default.join(source, id);
|
|
575
|
+
const targetDir = node_path_1.default.join(paths.downloads, id);
|
|
576
|
+
if (node_fs_1.default.existsSync(targetDir)) {
|
|
577
|
+
skipped += 1;
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
node_fs_1.default.cpSync(sourceDir, targetDir, { recursive: true });
|
|
581
|
+
imported += 1;
|
|
582
|
+
}
|
|
583
|
+
const metadataFile = node_path_1.default.join(paths.metadata, `${id}.json`);
|
|
584
|
+
const info = readProjectInfo(sourceDir);
|
|
585
|
+
if (!node_fs_1.default.existsSync(metadataFile)) {
|
|
586
|
+
const previewCandidate = findPreviewCandidate(sourceDir);
|
|
587
|
+
let thumbLocal;
|
|
588
|
+
if (previewCandidate && node_fs_1.default.existsSync(previewCandidate)) {
|
|
589
|
+
const ext = node_path_1.default.extname(previewCandidate) || '.jpg';
|
|
590
|
+
const previewDir = node_path_1.default.join(paths.previews, id);
|
|
591
|
+
(0, fs_1.ensureDir)(previewDir);
|
|
592
|
+
thumbLocal = node_path_1.default.join(previewDir, `thumb${ext}`);
|
|
593
|
+
if (!node_fs_1.default.existsSync(thumbLocal)) {
|
|
594
|
+
node_fs_1.default.copyFileSync(previewCandidate, thumbLocal);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
writeMetaFile({
|
|
598
|
+
id,
|
|
599
|
+
title: `Wallpaper ${id}`,
|
|
600
|
+
tags: [],
|
|
601
|
+
preview_thumb_local: thumbLocal,
|
|
602
|
+
author_name: 'Steam Workshop',
|
|
603
|
+
time_updated: Math.floor(Date.now() / 1000),
|
|
604
|
+
wallpaper_type: info.type,
|
|
605
|
+
audio_reactive: info.audioReactive
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
try {
|
|
610
|
+
const current = JSON.parse(node_fs_1.default.readFileSync(metadataFile, 'utf8'));
|
|
611
|
+
if (!current.wallpaper_type || current.wallpaper_type === 'unknown' || current.audio_reactive === undefined) {
|
|
612
|
+
writeMetaFile({
|
|
613
|
+
...current,
|
|
614
|
+
wallpaper_type: current.wallpaper_type ?? info.type,
|
|
615
|
+
audio_reactive: current.audio_reactive ?? info.audioReactive
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
// ignore malformed metadata
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
ok: true,
|
|
627
|
+
sources,
|
|
628
|
+
imported,
|
|
629
|
+
skipped,
|
|
630
|
+
total: imported + skipped
|
|
631
|
+
};
|
|
632
|
+
}
|
|
325
633
|
async function workshopSearch(input) {
|
|
326
634
|
const paths = getWePaths();
|
|
327
635
|
ensureWePaths(paths);
|
|
@@ -561,6 +869,78 @@ async function copyDownloadedContent(job) {
|
|
|
561
869
|
function snapshotFile(paths) {
|
|
562
870
|
return node_path_1.default.join(paths.runtime, 'coexistence.snapshot.json');
|
|
563
871
|
}
|
|
872
|
+
function snapshotDir(paths) {
|
|
873
|
+
return node_path_1.default.join(paths.root, 'coexistence', 'snapshots');
|
|
874
|
+
}
|
|
875
|
+
function activeLockFile(paths) {
|
|
876
|
+
return node_path_1.default.join(paths.runtime, 'active.lock');
|
|
877
|
+
}
|
|
878
|
+
function activeStateFile(paths) {
|
|
879
|
+
return node_path_1.default.join(paths.runtime, 'active.json');
|
|
880
|
+
}
|
|
881
|
+
function readActiveState() {
|
|
882
|
+
const paths = getWePaths();
|
|
883
|
+
ensureWePaths(paths);
|
|
884
|
+
const p = activeStateFile(paths);
|
|
885
|
+
if (!node_fs_1.default.existsSync(p))
|
|
886
|
+
return null;
|
|
887
|
+
try {
|
|
888
|
+
return JSON.parse(node_fs_1.default.readFileSync(p, 'utf8'));
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function writeActiveState(state) {
|
|
895
|
+
const paths = getWePaths();
|
|
896
|
+
ensureWePaths(paths);
|
|
897
|
+
(0, fs_1.writeJson)(activeStateFile(paths), state);
|
|
898
|
+
node_fs_1.default.writeFileSync(activeLockFile(paths), `${state.mode}:${state.started_at}\n`, 'utf8');
|
|
899
|
+
}
|
|
900
|
+
function clearActiveState() {
|
|
901
|
+
const paths = getWePaths();
|
|
902
|
+
ensureWePaths(paths);
|
|
903
|
+
try {
|
|
904
|
+
node_fs_1.default.unlinkSync(activeStateFile(paths));
|
|
905
|
+
}
|
|
906
|
+
catch {
|
|
907
|
+
// ignore
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
node_fs_1.default.unlinkSync(activeLockFile(paths));
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
// ignore
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function killPid(pid) {
|
|
917
|
+
if (!pid || !Number.isFinite(pid) || pid <= 1)
|
|
918
|
+
return false;
|
|
919
|
+
try {
|
|
920
|
+
process.kill(pid, 'SIGTERM');
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function workshopActiveStatus() {
|
|
928
|
+
const paths = getWePaths();
|
|
929
|
+
ensureWePaths(paths);
|
|
930
|
+
const state = readActiveState();
|
|
931
|
+
const lock = node_fs_1.default.existsSync(activeLockFile(paths));
|
|
932
|
+
return {
|
|
933
|
+
ok: true,
|
|
934
|
+
active: lock && !!state,
|
|
935
|
+
lock_path: activeLockFile(paths),
|
|
936
|
+
state_path: activeStateFile(paths),
|
|
937
|
+
state: state ?? undefined
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function workshopSetActive(state) {
|
|
941
|
+
writeActiveState(state);
|
|
942
|
+
return { ok: true, active: true, state };
|
|
943
|
+
}
|
|
564
944
|
async function isUnitActive(unit) {
|
|
565
945
|
try {
|
|
566
946
|
const out = await (0, exec_1.run)('systemctl', ['--user', 'show', unit, '--property', 'ActiveState', '--value']);
|
|
@@ -587,8 +967,11 @@ async function workshopCoexistenceEnter() {
|
|
|
587
967
|
// best effort
|
|
588
968
|
}
|
|
589
969
|
}
|
|
590
|
-
|
|
591
|
-
|
|
970
|
+
const snapshotId = String(now());
|
|
971
|
+
const snap = { id: snapshotId, ts: now(), active };
|
|
972
|
+
(0, fs_1.writeJson)(node_path_1.default.join(snapshotDir(paths), `${snapshotId}.json`), snap);
|
|
973
|
+
(0, fs_1.writeJson)(snapshotFile(paths), { id: snapshotId, ts: snap.ts });
|
|
974
|
+
return { ok: true, stopped: active, snapshot: active, snapshot_id: snapshotId };
|
|
592
975
|
}
|
|
593
976
|
async function workshopCoexistenceExit() {
|
|
594
977
|
const paths = getWePaths();
|
|
@@ -597,7 +980,19 @@ async function workshopCoexistenceExit() {
|
|
|
597
980
|
if (!node_fs_1.default.existsSync(snapPath))
|
|
598
981
|
return { ok: true, restored: [] };
|
|
599
982
|
const raw = JSON.parse(node_fs_1.default.readFileSync(snapPath, 'utf8'));
|
|
600
|
-
|
|
983
|
+
let active = Array.isArray(raw.active) ? raw.active.map(v => String(v)) : [];
|
|
984
|
+
if ((!active || active.length === 0) && raw.id) {
|
|
985
|
+
const historical = node_path_1.default.join(snapshotDir(paths), `${raw.id}.json`);
|
|
986
|
+
if (node_fs_1.default.existsSync(historical)) {
|
|
987
|
+
try {
|
|
988
|
+
const snap = JSON.parse(node_fs_1.default.readFileSync(historical, 'utf8'));
|
|
989
|
+
active = Array.isArray(snap.active) ? snap.active.map(v => String(v)) : [];
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
active = [];
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
601
996
|
const restored = [];
|
|
602
997
|
for (const unit of active) {
|
|
603
998
|
try {
|
|
@@ -630,6 +1025,42 @@ async function workshopCoexistenceStatus() {
|
|
|
630
1025
|
}
|
|
631
1026
|
return { ok: true, snapshot, current };
|
|
632
1027
|
}
|
|
1028
|
+
async function workshopStop(options) {
|
|
1029
|
+
const state = readActiveState();
|
|
1030
|
+
const hadActive = !!state;
|
|
1031
|
+
const stopped = [];
|
|
1032
|
+
if (state?.instances) {
|
|
1033
|
+
if (options?.monitor) {
|
|
1034
|
+
const inst = state.instances[options.monitor];
|
|
1035
|
+
if (killPid(inst?.pid))
|
|
1036
|
+
stopped.push(options.monitor);
|
|
1037
|
+
delete state.instances[options.monitor];
|
|
1038
|
+
if (Object.keys(state.instances).length > 0 && !options?.all) {
|
|
1039
|
+
writeActiveState(state);
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
clearActiveState();
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
for (const [monitor, inst] of Object.entries(state.instances)) {
|
|
1047
|
+
if (killPid(inst?.pid))
|
|
1048
|
+
stopped.push(monitor);
|
|
1049
|
+
}
|
|
1050
|
+
clearActiveState();
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
else if (options?.all) {
|
|
1054
|
+
clearActiveState();
|
|
1055
|
+
}
|
|
1056
|
+
const coexist = await workshopCoexistenceExit();
|
|
1057
|
+
return {
|
|
1058
|
+
ok: true,
|
|
1059
|
+
stopped_instances: stopped,
|
|
1060
|
+
restored_units: coexist.restored,
|
|
1061
|
+
had_active: hadActive
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
633
1064
|
async function workshopRunJob(jobId) {
|
|
634
1065
|
const job = readJob(jobId);
|
|
635
1066
|
if (!job)
|
|
@@ -728,6 +1159,13 @@ function workshopLibrary() {
|
|
|
728
1159
|
meta = undefined;
|
|
729
1160
|
}
|
|
730
1161
|
}
|
|
1162
|
+
const info = readProjectInfo(p);
|
|
1163
|
+
if (meta) {
|
|
1164
|
+
meta.wallpaper_type = meta.wallpaper_type ?? info.type;
|
|
1165
|
+
if (meta.audio_reactive === undefined) {
|
|
1166
|
+
meta.audio_reactive = info.audioReactive;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
731
1169
|
items.push({
|
|
732
1170
|
id,
|
|
733
1171
|
path: p,
|