@x12i/pocket-videos-server 0.0.0-stage → 1.1.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/package.json +17 -4
- package/serve.js +91 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x12i/pocket-videos-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Development server for Pocket Videos. Serves the client with a content directory overlaid, including Range requests for MP3 and MP4.",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./serve.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"serve.js"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@x12i/pocket-videos": "1.1.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/serve.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Development server for Pocket Videos.
|
|
2
|
+
// node serve.js -> http://localhost:8080
|
|
3
|
+
// node serve.js 3000 -> http://localhost:3000
|
|
4
|
+
// contentDir may be one directory or a list. Earlier directories win over later
|
|
5
|
+
// ones, and all of them win over the same path in the client package, so a later
|
|
6
|
+
// app (catalog, media, manifest, icons) sits on top of the generic shell.
|
|
7
|
+
// Range requests are supported for seeking. Not for production.
|
|
8
|
+
|
|
9
|
+
import http from 'node:http';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { clientDir as packagedClientDir } from '@x12i/pocket-videos/client-dir';
|
|
14
|
+
|
|
15
|
+
const TYPES = {
|
|
16
|
+
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8',
|
|
17
|
+
'.json': 'application/json; charset=utf-8', '.webmanifest': 'application/manifest+json',
|
|
18
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
19
|
+
'.woff2': 'font/woff2', '.m4a': 'audio/mp4', '.mp3': 'audio/mpeg', '.ogg': 'audio/ogg', '.wav': 'audio/wav',
|
|
20
|
+
'.mp4': 'video/mp4', '.webm': 'video/webm', '.mov': 'video/quicktime',
|
|
21
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function inside(root, file) {
|
|
25
|
+
return file === root || file.startsWith(root + path.sep);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function locate(roots, rel, cb, index = 0) {
|
|
29
|
+
if (index >= roots.length) { cb(null); return; }
|
|
30
|
+
const root = roots[index];
|
|
31
|
+
const file = path.normalize(path.join(root, rel));
|
|
32
|
+
if (!inside(root, file)) { locate(roots, rel, cb, index + 1); return; }
|
|
33
|
+
fs.stat(file, (err, stat) => {
|
|
34
|
+
if (err || !stat.isFile()) locate(roots, rel, cb, index + 1);
|
|
35
|
+
else cb({ file, stat });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createServer({ clientDir, contentDir } = {}) {
|
|
40
|
+
if (!clientDir) throw new Error('createServer requires clientDir');
|
|
41
|
+
const contentDirs = (Array.isArray(contentDir) ? contentDir : [contentDir]).filter(Boolean);
|
|
42
|
+
const roots = [...contentDirs, clientDir].map((dir) => path.resolve(dir));
|
|
43
|
+
|
|
44
|
+
return http.createServer((req, res) => {
|
|
45
|
+
let rel;
|
|
46
|
+
try { rel = decodeURIComponent(new URL(req.url, 'http://x').pathname); } catch { res.writeHead(400).end(); return; }
|
|
47
|
+
if (rel.endsWith('/')) rel += 'index.html';
|
|
48
|
+
rel = rel.replace(/^\/+/, '');
|
|
49
|
+
if (!rel || rel.includes('\0')) { res.writeHead(400).end(); return; }
|
|
50
|
+
|
|
51
|
+
locate(roots, rel, (found) => {
|
|
52
|
+
if (!found) { res.writeHead(404, { 'Content-Type': 'text/plain' }).end('Not found'); return; }
|
|
53
|
+
const { file, stat } = found;
|
|
54
|
+
const type = TYPES[path.extname(file).toLowerCase()] || 'application/octet-stream';
|
|
55
|
+
const headers = { 'Content-Type': type, 'Accept-Ranges': 'bytes', 'Cache-Control': 'no-cache' };
|
|
56
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range || '');
|
|
57
|
+
if (range && (range[1] || range[2])) {
|
|
58
|
+
let start, end;
|
|
59
|
+
if (range[1] === '') { start = Math.max(0, stat.size - Number(range[2])); end = stat.size - 1; }
|
|
60
|
+
else { start = Number(range[1]); end = range[2] === '' ? stat.size - 1 : Math.min(Number(range[2]), stat.size - 1); }
|
|
61
|
+
if (start >= stat.size || start > end) { res.writeHead(416, { 'Content-Range': `bytes */${stat.size}` }).end(); return; }
|
|
62
|
+
res.writeHead(206, { ...headers, 'Content-Range': `bytes ${start}-${end}/${stat.size}`, 'Content-Length': end - start + 1 });
|
|
63
|
+
if (req.method === 'HEAD') { res.end(); return; }
|
|
64
|
+
fs.createReadStream(file, { start, end }).pipe(res);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
res.writeHead(200, { ...headers, 'Content-Length': stat.size });
|
|
68
|
+
if (req.method === 'HEAD') { res.end(); return; }
|
|
69
|
+
fs.createReadStream(file).pipe(res);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function defaultContentDir() {
|
|
75
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
76
|
+
const candidates = [
|
|
77
|
+
path.resolve(here, '../../demo'),
|
|
78
|
+
path.resolve(process.cwd(), 'demo'),
|
|
79
|
+
];
|
|
80
|
+
for (const dir of candidates) {
|
|
81
|
+
if (fs.existsSync(path.join(dir, 'catalog.json'))) return dir;
|
|
82
|
+
}
|
|
83
|
+
return candidates[0];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
87
|
+
if (invokedDirectly) {
|
|
88
|
+
const port = Number(process.argv[2]) || 8080;
|
|
89
|
+
createServer({ clientDir: packagedClientDir, contentDir: defaultContentDir() })
|
|
90
|
+
.listen(port, () => console.log(`Pocket Videos running at http://localhost:${port}`));
|
|
91
|
+
}
|
package/README.md
DELETED