discovery-media-player 0.1.0 → 0.1.1

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.
@@ -82,3 +82,49 @@ describe("le player répond sans plateforme", () => {
82
82
  expect(await ctx.identity.canManageShares(null, "create")).toBe(false);
83
83
  });
84
84
  });
85
+
86
+ // LA PREMIÈRE MINUTE DE QUELQU'UN QUI DÉCOUVRE LE PROJET.
87
+ //
88
+ // Écrit après avoir regardé un premier utilisateur suivre le README : dossier vide, nom d'exemple
89
+ // tapé tel quel, et un « Impossible d'afficher ce document » rouge — le même message qu'un fichier
90
+ // absent, qu'un dossier vide et qu'un format non géré. Trois causes, une phrase, aucune piste.
91
+ // La racine du serveur montre désormais ce qu'il y a à lire.
92
+ describe("page d'accueil du mode dossier", () => {
93
+ const fs = require("node:fs");
94
+ const os = require("node:os");
95
+ const p = require("node:path");
96
+
97
+ it("liste ce qui est affichable, et rien d'autre", async () => {
98
+ const dir = fs.realpathSync(fs.mkdtempSync(p.join(os.tmpdir(), "player-home-")));
99
+ fs.writeFileSync(p.join(dir, "rapport.pdf"), "%PDF-1.4");
100
+ fs.writeFileSync(p.join(dir, "plan.png"), "x");
101
+ fs.writeFileSync(p.join(dir, "notes.docx"), "x"); // format non géré : ne pas le proposer
102
+ fs.writeFileSync(p.join(dir, ".cache"), "x"); // fichier caché
103
+ fs.mkdirSync(p.join(dir, "archives")); // dossier
104
+
105
+ const { pageAccueil } = require("../serve.js");
106
+ const html = await pageAccueil(dir);
107
+ expect(html).toContain('href="/preview/rapport.pdf"');
108
+ expect(html).toContain('href="/preview/plan.png"');
109
+ expect(html).not.toContain("notes.docx");
110
+ expect(html).not.toContain(".cache");
111
+ expect(html).not.toContain("archives");
112
+ });
113
+
114
+ it("dit qu'il est vide plutôt que de laisser deviner", async () => {
115
+ const dir = fs.realpathSync(fs.mkdtempSync(p.join(os.tmpdir(), "player-vide-")));
116
+ const { pageAccueil } = require("../serve.js");
117
+ expect(await pageAccueil(dir)).toContain("Aucun document affichable");
118
+ });
119
+
120
+ // Un nom de fichier est écrit par quelqu'un d'autre — ici l'exploitant, mais la règle ne se
121
+ // relâche pas selon la confiance qu'on a dans la source.
122
+ it("échappe les noms de fichiers", async () => {
123
+ const dir = fs.realpathSync(fs.mkdtempSync(p.join(os.tmpdir(), "player-xss-")));
124
+ fs.writeFileSync(p.join(dir, '<img src=x onerror=alert(1)>.pdf'), "%PDF");
125
+ const { pageAccueil } = require("../serve.js");
126
+ const html = await pageAccueil(dir);
127
+ expect(html).not.toContain("<img src=x");
128
+ expect(html).toContain("&lt;img");
129
+ });
130
+ });
package/bin/serve.js CHANGED
@@ -14,6 +14,7 @@
14
14
 
15
15
  const http = require("node:http");
16
16
  const path = require("node:path");
17
+ const fs = require("node:fs/promises");
17
18
  const { pathToFileURL } = require("node:url");
18
19
  const player = require("../server/handler");
19
20
  const { createStandaloneContext } = require("../context/standalone");
@@ -49,6 +50,58 @@ function versParametres(url) {
49
50
  return q;
50
51
  }
51
52
 
53
+ const AFFICHABLES = new Set([".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg"]);
54
+
55
+ /**
56
+ * Page d'accueil du mode dossier : ce qu'il y a à lire, et où le prendre.
57
+ *
58
+ * ⚠️ Écrite après avoir regardé quelqu'un essayer le projet pour la première fois. Il a suivi le
59
+ * README, ouvert `/preview/<nom>` avec le nom d'exemple, et reçu « Impossible d'afficher ce
60
+ * document » — le même message qu'un dossier vide, qu'un fichier absent et qu'un format non géré.
61
+ * Trois causes, une phrase, aucune piste. Le premier contact avec un projet ne se rejoue pas.
62
+ *
63
+ * N'existe qu'en mode dossier : c'est la racine que l'exploitant a lui-même désignée, il n'y a
64
+ * rien à divulguer qu'il ne connaisse déjà.
65
+ */
66
+ async function pageAccueil(racine) {
67
+ let fichiers = [];
68
+ try {
69
+ fichiers = (await fs.readdir(racine, { withFileTypes: true }))
70
+ .filter((e) => e.isFile() && !e.name.startsWith(".") && AFFICHABLES.has(path.extname(e.name).toLowerCase()))
71
+ .map((e) => e.name).sort();
72
+ } catch { /* racine illisible : traitée comme vide, le message le dira */ }
73
+
74
+ const echapper = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
75
+ const liste = fichiers.length
76
+ ? `<ul>${fichiers.map((n) => `<li><a href="/preview/${encodeURIComponent(n)}">${echapper(n)}</a></li>`).join("")}</ul>`
77
+ : `<p class=vide>Aucun document affichable dans <code>${echapper(racine)}</code>.<br>
78
+ Déposez-y un PDF ou une image, puis rechargez cette page.</p>`;
79
+
80
+ return `<!doctype html><html lang=fr><head><meta charset=utf-8>
81
+ <meta name=viewport content="width=device-width,initial-scale=1"><title>Discovery Media Player</title>
82
+ <style>
83
+ body{font:15px/1.6 -apple-system,system-ui,Segoe UI,Roboto,sans-serif;color:#1c1a17;background:#f5f3ef;
84
+ margin:0;display:flex;min-height:100vh;align-items:center;justify-content:center;padding:24px}
85
+ .c{width:100%;max-width:560px;background:#fff;border-radius:16px;padding:32px;box-shadow:0 18px 50px rgba(30,22,12,.10)}
86
+ h1{font-size:19px;margin:0 0 4px;letter-spacing:-.01em}
87
+ .s{color:#7c7266;font-size:13.5px;margin:0 0 22px}
88
+ ul{list-style:none;margin:0;padding:0}
89
+ li+li{border-top:1px solid #eee9e0}
90
+ a{display:block;padding:11px 12px;color:#1c1a17;text-decoration:none;border-radius:9px}
91
+ a:hover{background:#f5f2ec}
92
+ .vide{color:#7c7266;font-size:14px;background:#faf8f4;border:1px dashed #ddd4c6;border-radius:12px;padding:18px;margin:0}
93
+ code{font:12.5px ui-monospace,SFMono-Regular,Menlo,monospace;background:#f0ece4;padding:1px 5px;border-radius:5px}
94
+ .f{margin:22px 0 0;padding-top:16px;border-top:1px solid #eee9e0;color:#9a9184;font-size:12.5px}
95
+ .f a{display:inline;padding:0;color:#9a9184;text-decoration:underline}
96
+ </style></head><body><div class=c>
97
+ <h1>Discovery Media Player</h1>
98
+ <p class=s>Mode dossier — les documents servis viennent de <code>${echapper(racine)}</code>.</p>
99
+ ${liste}
100
+ <p class=f>Liens tracés, statistiques et présentation en direct demandent une base :
101
+ voir <a href="https://github.com/Juli1artha/discovery-media-player#going-further">la documentation</a>.</p>
102
+ </div></body></html>`;
103
+ }
104
+
52
105
  const serveur = http.createServer(async (req, res) => {
53
106
  const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
54
107
 
@@ -60,6 +113,13 @@ const serveur = http.createServer(async (req, res) => {
60
113
  return;
61
114
  }
62
115
 
116
+ // Mode dossier : la racine du serveur montre ce qu'il y a à lire.
117
+ if ((url.pathname === "/" || url.pathname === "/preview" || url.pathname === "/preview/") && process.env.PLAYER_LOCAL_ROOT) {
118
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
119
+ res.end(await pageAccueil(path.resolve(process.env.PLAYER_LOCAL_ROOT)));
120
+ return;
121
+ }
122
+
63
123
  const q = versParametres(url);
64
124
  const routeConnue =
65
125
  url.pathname === "/api/doc" || url.pathname === "/doc" || url.pathname === "/present" ||
@@ -112,4 +172,4 @@ if (require.main === module) serveur.listen(PORT, HOST, () => {
112
172
  console.log(` état : http://localhost:${PORT}/api/doc?contract=1`);
113
173
  });
114
174
 
115
- module.exports = { serveur, versParametres };
175
+ module.exports = { serveur, versParametres, pageAccueil };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
5
5
  "keywords": [
6
6
  "pdf-viewer",
@@ -62,11 +62,11 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@eslint/js": "^9.39.4",
65
- "esbuild": "0.21.5",
65
+ "esbuild": "^0.25.12",
66
66
  "eslint": "^9.39.4",
67
67
  "jsdom": "^25.0.1",
68
68
  "typescript": "^5.5.4",
69
69
  "typescript-eslint": "^8.46.4",
70
- "vitest": "^2.1.9"
70
+ "vitest": "^3.2.7"
71
71
  }
72
72
  }