dsh-folia 4.0.5 → 4.0.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/boot/index.js +42 -6
- package/lib/client.js +8 -0
- package/lib/routes/boot.js +12 -3
- package/package.json +1 -1
package/lib/boot/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 3. **任何异常都必须放行**。注入的脚本顶层 try/catch,超时自毁,绝不把用户挡在门外。
|
|
10
10
|
*/
|
|
11
11
|
import { createHash } from 'node:crypto';
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { extname, join } from 'node:path';
|
|
15
15
|
|
|
@@ -96,8 +96,42 @@ export function clipMetaFile(id) {
|
|
|
96
96
|
if (!String(id || '')) return null;
|
|
97
97
|
return join(bootDir(), clipStem(id) + '.json');
|
|
98
98
|
}
|
|
99
|
+
/** 旧命名规则下的路径:`<id>.mp4`(id 当年被限制成 ASCII 安全字符,所以能直接用)。 */
|
|
100
|
+
function legacyClipFile(id) {
|
|
101
|
+
const raw = String(id || '');
|
|
102
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(raw)) return null;
|
|
103
|
+
return join(bootDir(), raw + '.mp4');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 找出这个 id 实际对应的片段文件。
|
|
108
|
+
*
|
|
109
|
+
* 为什么需要"找"而不是直接算:4.0.2 把磁盘命名从 `<id>.mp4` 改成了
|
|
110
|
+
* `<id>-<hash>.mp4`(为了支持中文 slug),于是**升级前下载的片段**还叫旧名字。
|
|
111
|
+
* 列表是按文件名枚举的,所以旧文件看得见;可点「使用」时按新名字找不到 ⇒ `not-installed`。
|
|
112
|
+
* 这里按"新名字 → 旧名字"的顺序找,找到旧名字就**顺手迁移**成新名字(幂等)。
|
|
113
|
+
*/
|
|
114
|
+
export function resolveClipFile(id) {
|
|
115
|
+
const next = clipFile(id);
|
|
116
|
+
if (next && existsSync(next) && statSync(next).size > 0) return next;
|
|
117
|
+
const legacy = legacyClipFile(id);
|
|
118
|
+
if (legacy && existsSync(legacy) && statSync(legacy).size > 0) {
|
|
119
|
+
try {
|
|
120
|
+
if (next && !existsSync(next)) writeFileSync(next, readFileSync(legacy));
|
|
121
|
+
try { renameSync(legacy, next); } catch { /* 跨盘/占用:复制已经够用,旧文件留着无害 */ }
|
|
122
|
+
try { rmSync(legacy); } catch { /* ignore */ }
|
|
123
|
+
const metaPath = clipMetaFile(id);
|
|
124
|
+
if (metaPath && !existsSync(metaPath)) {
|
|
125
|
+
writeFileSync(metaPath, JSON.stringify({ id, migratedFrom: 'legacy' }, null, 2) + '\n');
|
|
126
|
+
}
|
|
127
|
+
} catch { /* 迁移失败也不影响这次使用 */ }
|
|
128
|
+
return next || legacy;
|
|
129
|
+
}
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
|
|
99
133
|
export function clipExists(id) {
|
|
100
|
-
const p =
|
|
134
|
+
const p = resolveClipFile(id);
|
|
101
135
|
return Boolean(p && existsSync(p) && statSync(p).size > 0);
|
|
102
136
|
}
|
|
103
137
|
|
|
@@ -128,11 +162,13 @@ export function listLocalClips() {
|
|
|
128
162
|
}
|
|
129
163
|
|
|
130
164
|
export function removeClip(id) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
165
|
+
const paths = [clipFile(id), legacyClipFile(id)].filter(Boolean);
|
|
166
|
+
let removed = false;
|
|
167
|
+
for (const p of paths) {
|
|
168
|
+
try { if (existsSync(p)) { rmSync(p); removed = true; } } catch { /* 继续尝试下一个 */ }
|
|
169
|
+
}
|
|
134
170
|
try { const m = clipMetaFile(id); if (m && existsSync(m)) rmSync(m); } catch { /* ignore */ }
|
|
135
|
-
return true;
|
|
171
|
+
return removed || true;
|
|
136
172
|
}
|
|
137
173
|
|
|
138
174
|
/* ── 注入的启动屏幕脚本 ─────────────────────────────────────────────────────
|
package/lib/client.js
CHANGED
|
@@ -17149,6 +17149,14 @@ window.__ModuleLoader__.load({
|
|
|
17149
17149
|
actions.push(btn(weT("使用中"), () => {}, true, false));
|
|
17150
17150
|
}
|
|
17151
17151
|
return React.createElement("div", { key: c.id, style: BOOT_STYLE.card },
|
|
17152
|
+
// 封面:站点给了真正的缩略图才显示;老条目没有就留空(不显示破图)。
|
|
17153
|
+
c.cover
|
|
17154
|
+
? React.createElement("img", {
|
|
17155
|
+
src: c.cover, alt: "", loading: "lazy", decoding: "async",
|
|
17156
|
+
style: { width: "100%", aspectRatio: "16 / 9", objectFit: "cover", borderRadius: "6px", background: "rgba(0,0,0,0.3)" },
|
|
17157
|
+
onError: (e) => { e.target.style.display = "none"; },
|
|
17158
|
+
})
|
|
17159
|
+
: null,
|
|
17152
17160
|
React.createElement("div", { style: BOOT_STYLE.title }, c.title || c.id),
|
|
17153
17161
|
React.createElement("div", { style: BOOT_STYLE.meta },
|
|
17154
17162
|
[c.durationSec ? Number(c.durationSec).toFixed(1) + "s" : null,
|
package/lib/routes/boot.js
CHANGED
|
@@ -14,7 +14,7 @@ import { join } from 'node:path';
|
|
|
14
14
|
import { sendJson } from '../json-response.js';
|
|
15
15
|
import { bodyReader } from '../http-body.js';
|
|
16
16
|
import { resolveModules } from './modules.js';
|
|
17
|
-
import { clipExists, clipFile, clipMetaFile, listLocalClips, readBoot, removeClip, writeBoot } from '../boot/index.js';
|
|
17
|
+
import { clipExists, clipFile, clipMetaFile, listLocalClips, readBoot, removeClip, resolveClipFile, writeBoot } from '../boot/index.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* 站点基址。开机动画的候选**从站点分类取**(`?category=boot-animation`),
|
|
@@ -190,6 +190,14 @@ async function fetchCandidatesUncached() {
|
|
|
190
190
|
bytes: Number(it && it.bytes) || null,
|
|
191
191
|
sha256: it && it.sha256 ? String(it.sha256) : null,
|
|
192
192
|
durationSec: it && it.durationMs ? Number(it.durationMs) / 1000 : null,
|
|
193
|
+
// 封面只认图片扩展名:站点上有几条老条目的 thumbUrl 直接指向 mp4
|
|
194
|
+
// (那时还没有缩略图功能)。把视频当 <img> 用只会得到一张破图。
|
|
195
|
+
cover: (() => {
|
|
196
|
+
const u = String((it && it.thumbUrl) || '');
|
|
197
|
+
if (!u) return null;
|
|
198
|
+
if (!/\.(jpg|jpeg|png|webp|gif|avif)(\?|#|$)/i.test(u)) return null;
|
|
199
|
+
return /^https?:/i.test(u) ? u : site + u;
|
|
200
|
+
})(),
|
|
193
201
|
faststart: true,
|
|
194
202
|
})).filter((c) => c.id && c.url);
|
|
195
203
|
if (clips.length) return { clips, error: null };
|
|
@@ -253,7 +261,7 @@ export function registerBootRoutes(webServer, c) {
|
|
|
253
261
|
if (known.has(it.id)) continue;
|
|
254
262
|
clips.push({
|
|
255
263
|
id: it.id, title: it.title, url: null, bytes: null, sha256: null,
|
|
256
|
-
durationSec: it.durationSec, faststart: true,
|
|
264
|
+
durationSec: it.durationSec, faststart: true, cover: null,
|
|
257
265
|
installed: true, localBytes: it.bytes, orphan: true,
|
|
258
266
|
});
|
|
259
267
|
}
|
|
@@ -389,7 +397,8 @@ export function registerBootRoutes(webServer, c) {
|
|
|
389
397
|
handler: async (req, res) => {
|
|
390
398
|
const boot = readBoot();
|
|
391
399
|
const id = boot.clipId;
|
|
392
|
-
|
|
400
|
+
// 旧命名的片段(升级前下载的)也要能播 —— 解析器会顺手迁移成新名字。
|
|
401
|
+
const file = resolveClipFile(id);
|
|
393
402
|
if (!file || !existsSync(file)) { res.statusCode = 404; res.end('no clip'); return; }
|
|
394
403
|
const size = statSync(file).size;
|
|
395
404
|
const range = String((req.headers && req.headers.range) || '');
|