nixamp 0.24.2 → 0.25.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.
- package/README.md +38 -0
- package/dist/captions.d.ts +12 -0
- package/dist/captions.js +29 -2
- package/dist/hash.d.ts +12 -0
- package/dist/hash.js +139 -0
- package/dist/main.js +27 -3
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +49 -1
- package/dist/media-index.d.ts +67 -0
- package/dist/media-index.js +114 -0
- package/dist/media-local.d.ts +43 -0
- package/dist/media-local.js +88 -0
- package/dist/media-page.d.ts +18 -0
- package/dist/media-page.js +146 -0
- package/dist/media.d.ts +145 -0
- package/dist/media.js +364 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +186 -1
- package/dist/transcribe.js +20 -0
- package/dist/transcript-client.d.ts +19 -0
- package/dist/transcript-client.js +24 -0
- package/package.json +1 -1
- package/src/captions.ts +32 -2
- package/src/hash.ts +139 -0
- package/src/main.ts +27 -3
- package/src/mcp.ts +49 -1
- package/src/media-index.ts +145 -0
- package/src/media-local.ts +117 -0
- package/src/media-page.ts +159 -0
- package/src/media.ts +433 -0
- package/src/server.ts +178 -1
- package/src/transcribe.ts +17 -0
- package/src/transcript-client.ts +34 -0
- package/web/dist/assets/{hls-3VKVEQE3-B3PGV4wK.js → hls-3VKVEQE3-GlzSYi0P.js} +1 -1
- package/web/dist/assets/index-DJHHB_63.js +1 -0
- package/web/dist/assets/{mpegts-CtxeYR5-.js → mpegts-KIbyW_RL.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-CBHUeoME.js → mpegts-LO6RVLD6-Fs_vbGcB.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +5 -5
- package/web/dist/assets/index-DOOjAq1w.js +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file on this machine, described for nixamp.com's record of it.
|
|
3
|
+
*
|
|
4
|
+
* The same few steps whoever meets the file: hash every byte (the OpenFile
|
|
5
|
+
* id), take the quick fingerprint (the transcript key), ask ffprobe what is
|
|
6
|
+
* inside, ask nichedb what it is, note where it is being carried, and say
|
|
7
|
+
* when this machine will look at it again. `nixamp hash` does it for a
|
|
8
|
+
* file you name, `nixamp transcribe` for the one it is writing down, and a
|
|
9
|
+
* server for a file it puts on the air. A file that changed since the
|
|
10
|
+
* machine last looked is described again as a new record, and the two
|
|
11
|
+
* records point at each other.
|
|
12
|
+
*/
|
|
13
|
+
import { basename, extname } from "node:path";
|
|
14
|
+
import { codecsOf } from "./audio.js";
|
|
15
|
+
import { checkInterval, contentHash, factsFrom, fileFacts } from "./media.js";
|
|
16
|
+
import { remember } from "./media-index.js";
|
|
17
|
+
import { keepMedia } from "./transcript-client.js";
|
|
18
|
+
import { fileFingerprint } from "./transcripts.js";
|
|
19
|
+
/** Everything this machine can say about a file, ready to keep. */
|
|
20
|
+
export async function describeFile(path, deps = {}) {
|
|
21
|
+
const now = deps.now ?? Date.now;
|
|
22
|
+
const file = fileFacts(path);
|
|
23
|
+
const id = await (deps.hash ?? contentHash)(path);
|
|
24
|
+
const fingerprint = (deps.fingerprint ?? fileFingerprint)(path);
|
|
25
|
+
const codecs = deps.tools && deps.tools.carries !== false ? await codecsOf(deps.tools, path) : null;
|
|
26
|
+
const name = codecs?.tags?.title || basename(path, extname(path));
|
|
27
|
+
let enriched = null;
|
|
28
|
+
try {
|
|
29
|
+
enriched = deps.enricher ? await deps.enricher.lookup(name) : null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
enriched = null;
|
|
33
|
+
}
|
|
34
|
+
const at = now();
|
|
35
|
+
const facts = {
|
|
36
|
+
...factsFrom(codecs, enriched),
|
|
37
|
+
fingerprint,
|
|
38
|
+
checkedAt: new Date(at).toISOString(),
|
|
39
|
+
checkAfter: new Date(at + checkInterval(file.mtimeMs, at)).toISOString(),
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
fingerprint,
|
|
44
|
+
size: file.size,
|
|
45
|
+
mtimeMs: file.mtimeMs,
|
|
46
|
+
keep: {
|
|
47
|
+
name: file.name,
|
|
48
|
+
size: file.size,
|
|
49
|
+
contentType: file.contentType,
|
|
50
|
+
updated: file.updated,
|
|
51
|
+
facts: facts,
|
|
52
|
+
...(deps.holder ? { holder: deps.holder } : {}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Keep a described file on nixamp.com and in this machine's index. Answers the store's sentence when it refused. */
|
|
57
|
+
export async function keepFile(signed, path, described, deps = {}) {
|
|
58
|
+
const got = await keepMedia(signed, described.id, described.keep, deps.fetcher ?? fetch);
|
|
59
|
+
if (!got.ok)
|
|
60
|
+
return got.error;
|
|
61
|
+
try {
|
|
62
|
+
remember(path, { id: described.id, fingerprint: described.fingerprint, size: described.size, mtimeMs: described.mtimeMs }, (deps.now ?? Date.now)(), deps.index);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// The index is a convenience; the record is what matters.
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A file that changed since it was last described: described again as a
|
|
71
|
+
* new record that says what it was, while the old record says what it
|
|
72
|
+
* became. Answers the new identity, or null when nixamp.com would not
|
|
73
|
+
* keep it.
|
|
74
|
+
*/
|
|
75
|
+
export async function refreshChanged(signed, path, before, deps = {}) {
|
|
76
|
+
const described = await describeFile(path, deps);
|
|
77
|
+
if (described.id === before.id) {
|
|
78
|
+
// Touched but not changed: the same bytes, a new modification time.
|
|
79
|
+
const failed = await keepFile(signed, path, described, deps);
|
|
80
|
+
return failed ? null : { id: described.id, fingerprint: described.fingerprint };
|
|
81
|
+
}
|
|
82
|
+
described.keep.facts = { ...(described.keep.facts ?? {}), supersedes: `sha256:${before.id}` };
|
|
83
|
+
const failed = await keepFile(signed, path, described, deps);
|
|
84
|
+
if (failed)
|
|
85
|
+
return null;
|
|
86
|
+
await keepMedia(signed, before.id, { facts: { supersededBy: `sha256:${described.id}` } }, deps.fetcher ?? fetch);
|
|
87
|
+
return { id: described.id, fingerprint: described.fingerprint };
|
|
88
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nixamp.com/hash/<id>, as a page: everything nixamp knows about one file,
|
|
3
|
+
* for a person with a browser. The same facts as the OpenFile descriptor
|
|
4
|
+
* beside it, laid out: what it is, how long, what is inside, who said what
|
|
5
|
+
* it was, where it has been carried, and the transcript in every language
|
|
6
|
+
* it has been written down in, with subtitle files to take away.
|
|
7
|
+
*
|
|
8
|
+
* Rendered on the server from a string, with the terminal's look, so a
|
|
9
|
+
* crawler and a link preview read it and nothing has to load first.
|
|
10
|
+
*/
|
|
11
|
+
import type { MediaRecord, TranscriptRef } from "./media.ts";
|
|
12
|
+
import type { TranscriptLine } from "./transcripts.ts";
|
|
13
|
+
export interface PageTranscript extends TranscriptRef {
|
|
14
|
+
/** The lines themselves, for the one transcript the page shows. */
|
|
15
|
+
shown?: TranscriptLine[];
|
|
16
|
+
}
|
|
17
|
+
/** The page, whole. `transcripts[0]` carries the lines shown; the rest are offered as files. */
|
|
18
|
+
export declare function mediaPage(record: MediaRecord, site: string, transcripts?: PageTranscript[]): string;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
function esc(text) {
|
|
2
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3
|
+
}
|
|
4
|
+
function bytes(size) {
|
|
5
|
+
if (size >= 1024 ** 3)
|
|
6
|
+
return `${(size / 1024 ** 3).toFixed(2)} GB`;
|
|
7
|
+
if (size >= 1024 ** 2)
|
|
8
|
+
return `${(size / 1024 ** 2).toFixed(1)} MB`;
|
|
9
|
+
if (size >= 1024)
|
|
10
|
+
return `${(size / 1024).toFixed(0)} KB`;
|
|
11
|
+
return `${size} B`;
|
|
12
|
+
}
|
|
13
|
+
function clock(seconds) {
|
|
14
|
+
const whole = Math.max(0, Math.floor(seconds));
|
|
15
|
+
const h = Math.floor(whole / 3600);
|
|
16
|
+
const m = Math.floor((whole % 3600) / 60);
|
|
17
|
+
const s = whole % 60;
|
|
18
|
+
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}` : `${m}:${String(s).padStart(2, "0")}`;
|
|
19
|
+
}
|
|
20
|
+
function when(iso) {
|
|
21
|
+
return iso ? esc(iso.replace("T", " ").replace(/\.\d+Z$/, " UTC")) : "";
|
|
22
|
+
}
|
|
23
|
+
const STYLE = `
|
|
24
|
+
:root { color-scheme: dark; --bg:#080c09; --panel:#0c120e; --edge:#1d2c22; --green:#4af689; --dim:#227a4a; --fg:#cfe8d8; --muted:#6d8a79; --accent:#7ef0c4; }
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
27
|
+
main { max-width: 960px; margin: 0 auto; padding: 20px 16px 48px; }
|
|
28
|
+
a { color: var(--accent); }
|
|
29
|
+
.brand { color: var(--green); font-weight: 700; letter-spacing: .14em; text-decoration: none; }
|
|
30
|
+
h1 { font-size: 20px; margin: 18px 0 4px; color: var(--green); overflow-wrap: anywhere; }
|
|
31
|
+
.id { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
|
|
32
|
+
.panel { position: relative; border: 1px solid var(--edge); border-radius: 6px; background: var(--panel); padding: 18px 12px 12px; margin-top: 16px; }
|
|
33
|
+
.panel::before { content: attr(data-title); position: absolute; top: -9px; left: 10px; padding: 0 6px; background: var(--panel); color: var(--dim); font-size: 12px; letter-spacing: .06em; }
|
|
34
|
+
table { border-collapse: collapse; width: 100%; }
|
|
35
|
+
td { padding: 3px 8px 3px 0; vertical-align: top; overflow-wrap: anywhere; }
|
|
36
|
+
td:first-child { color: var(--muted); white-space: nowrap; width: 11em; }
|
|
37
|
+
.poster { float: right; max-width: 160px; margin: 0 0 8px 12px; border: 1px solid var(--edge); border-radius: 4px; }
|
|
38
|
+
ul { margin: 0; padding-left: 1.2em; }
|
|
39
|
+
.lines { list-style: none; padding: 0; max-height: 420px; overflow-y: auto; font-size: 13px; }
|
|
40
|
+
.lines li { display: flex; gap: 8px; padding: 2px 0; border-bottom: 1px dotted var(--edge); }
|
|
41
|
+
.lines .t { color: var(--muted); flex: 0 0 auto; }
|
|
42
|
+
.chips a { display: inline-block; margin: 0 8px 6px 0; border: 1px solid var(--edge); border-radius: 999px; padding: 1px 10px; font-size: 12px; text-decoration: none; }
|
|
43
|
+
.muted { color: var(--muted); }
|
|
44
|
+
pre { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--muted); font-size: 12px; }
|
|
45
|
+
`;
|
|
46
|
+
/** The page, whole. `transcripts[0]` carries the lines shown; the rest are offered as files. */
|
|
47
|
+
export function mediaPage(record, site, transcripts = []) {
|
|
48
|
+
const base = site.replace(/\/+$/, "");
|
|
49
|
+
const f = record.facts;
|
|
50
|
+
const title = f.enrichment?.title || f.tags?.title || record.name || record.id.slice(0, 12);
|
|
51
|
+
const picture = f.enrichment?.image ?? null;
|
|
52
|
+
const url = `${base}/hash/${record.id}`;
|
|
53
|
+
const summary = f.enrichment?.summary ?? "";
|
|
54
|
+
const description = summary || `${record.name}${f.duration ? `, ${clock(f.duration)}` : ""}${transcripts.length > 0 ? `, transcribed in ${transcripts.map((one) => one.language || "its own language").join(", ")}` : ""}`;
|
|
55
|
+
const rows = [];
|
|
56
|
+
if (record.name)
|
|
57
|
+
rows.push(["File", esc(record.name)]);
|
|
58
|
+
if (record.size)
|
|
59
|
+
rows.push(["Size", bytes(record.size)]);
|
|
60
|
+
if (record.contentType)
|
|
61
|
+
rows.push(["Type", esc(record.contentType)]);
|
|
62
|
+
if (f.duration)
|
|
63
|
+
rows.push(["Length", clock(f.duration)]);
|
|
64
|
+
if (f.width && f.height)
|
|
65
|
+
rows.push(["Picture", `${f.width}×${f.height}`]);
|
|
66
|
+
if (f.codecs)
|
|
67
|
+
rows.push(["Inside", esc([f.codecs.video, f.codecs.audio, f.codecs.container].filter(Boolean).join(" / "))]);
|
|
68
|
+
if (f.tags?.artist || f.tags?.album)
|
|
69
|
+
rows.push(["Tagged", esc([f.tags.artist, f.tags.album].filter(Boolean).join(" · "))]);
|
|
70
|
+
if (record.updated)
|
|
71
|
+
rows.push(["File changed", when(record.updated)]);
|
|
72
|
+
if (f.checkedAt)
|
|
73
|
+
rows.push(["Last checked", `${when(f.checkedAt)}${f.checkAfter ? `, next ${when(f.checkAfter)}` : ""}`]);
|
|
74
|
+
if (f.fingerprint)
|
|
75
|
+
rows.push(["Fingerprint", `<span class="id">${esc(f.fingerprint)}</span>`]);
|
|
76
|
+
if (f.supersedes)
|
|
77
|
+
rows.push(["Was", `<a href="${base}/hash/${esc(f.supersedes.replace(/^sha256:/, ""))}">${esc(f.supersedes)}</a>`]);
|
|
78
|
+
if (f.supersededBy)
|
|
79
|
+
rows.push(["Became", `<a href="${base}/hash/${esc(f.supersededBy.replace(/^sha256:/, ""))}">${esc(f.supersededBy)}</a>`]);
|
|
80
|
+
rows.push(["Kept", `${when(record.createdAt)}${record.updatedAt !== record.createdAt ? `, last added to ${when(record.updatedAt)}` : ""}`]);
|
|
81
|
+
const enrichment = f.enrichment
|
|
82
|
+
? `<section class="panel" data-title="What nichedb says it is">
|
|
83
|
+
${picture ? `<img class="poster" src="${esc(picture)}" alt="" />` : ""}
|
|
84
|
+
<table>
|
|
85
|
+
<tr><td>Title</td><td>${esc(f.enrichment.title ?? "")}${f.enrichment.year ? ` (${f.enrichment.year})` : ""}</td></tr>
|
|
86
|
+
${f.enrichment.kind ? `<tr><td>Kind</td><td>${esc(f.enrichment.kind)}</td></tr>` : ""}
|
|
87
|
+
${summary ? `<tr><td>About</td><td>${esc(summary)}</td></tr>` : ""}
|
|
88
|
+
${f.enrichment.page ? `<tr><td>Page</td><td><a href="${esc(f.enrichment.page)}">${esc(f.enrichment.page)}</a></td></tr>` : ""}
|
|
89
|
+
</table>
|
|
90
|
+
</section>`
|
|
91
|
+
: "";
|
|
92
|
+
const holders = record.holders.length > 0
|
|
93
|
+
? `<section class="panel" data-title="Carried by">
|
|
94
|
+
<ul>${record.holders.map((h) => `<li><a href="${esc(h.url)}">${esc(h.name || h.url)}</a>${h.channel ? ` as <code>${esc(h.channel)}</code>` : ""} <span class="muted">${when(h.seenAt)}</span></li>`).join("")}</ul>
|
|
95
|
+
</section>`
|
|
96
|
+
: "";
|
|
97
|
+
const shown = transcripts[0];
|
|
98
|
+
const transcript = transcripts.length > 0
|
|
99
|
+
? `<section class="panel" data-title="Transcript">
|
|
100
|
+
<p class="chips">${transcripts.map((one) => {
|
|
101
|
+
const q = one.language ? `?language=${esc(one.language)}` : "";
|
|
102
|
+
return `<a href="${url}.srt${q}">${esc(one.language || "as spoken")}${one.translatedFrom ? ` (from ${esc(one.translatedFrom)})` : ""} · ${one.lines} lines${one.complete ? "" : " so far"} · SRT</a> <a href="${url}.vtt${q}">VTT</a> <a href="${url}.txt${q}">text</a>`;
|
|
103
|
+
}).join("<br />")}</p>
|
|
104
|
+
${shown?.shown && shown.shown.length > 0
|
|
105
|
+
? `<ul class="lines">${shown.shown.map((line) => `<li><span class="t">${clock(line.start)}</span><span>${esc(line.text)}</span></li>`).join("")}</ul>`
|
|
106
|
+
: ""}
|
|
107
|
+
</section>`
|
|
108
|
+
: `<section class="panel" data-title="Transcript"><p class="muted">Not written down yet. <code>nixamp transcribe FILE</code> keeps it here, and a server captioning it does too.</p></section>`;
|
|
109
|
+
return `<!doctype html>
|
|
110
|
+
<html lang="en">
|
|
111
|
+
<head>
|
|
112
|
+
<meta charset="utf-8" />
|
|
113
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
114
|
+
<title>${esc(title)} · nixamp</title>
|
|
115
|
+
<meta name="description" content="${esc(description.slice(0, 300))}" />
|
|
116
|
+
<link rel="canonical" href="${url}" />
|
|
117
|
+
<link rel="openfile" href="${url}.openfile.json" />
|
|
118
|
+
<link rel="alternate" type="application/json" href="${url}.json" />
|
|
119
|
+
<meta property="og:title" content="${esc(title)}" />
|
|
120
|
+
<meta property="og:description" content="${esc(description.slice(0, 300))}" />
|
|
121
|
+
<meta property="og:url" content="${url}" />
|
|
122
|
+
<meta property="og:type" content="${record.contentType.startsWith("video/") ? "video.other" : record.contentType.startsWith("audio/") ? "music.song" : "website"}" />
|
|
123
|
+
${picture ? `<meta property="og:image" content="${esc(picture)}" />` : ""}
|
|
124
|
+
<style>${STYLE}</style>
|
|
125
|
+
</head>
|
|
126
|
+
<body>
|
|
127
|
+
<main>
|
|
128
|
+
<a class="brand" href="${base}/">NIXAMP</a>
|
|
129
|
+
<h1>${esc(title)}</h1>
|
|
130
|
+
<div class="id">sha256:${esc(record.id)}</div>
|
|
131
|
+
<section class="panel" data-title="The file">
|
|
132
|
+
<table>${rows.map(([k, v]) => `<tr><td>${k}</td><td>${v}</td></tr>`).join("")}</table>
|
|
133
|
+
</section>
|
|
134
|
+
${enrichment}
|
|
135
|
+
${holders}
|
|
136
|
+
${transcript}
|
|
137
|
+
<section class="panel" data-title="For a program">
|
|
138
|
+
<p class="muted">This page as <a href="${url}.openfile.json">OpenFile</a> (<a href="https://logicsrc.com/docs/openfile">the specification</a>) or <a href="${url}.json">JSON</a>. Every file nixamp.com knows: <a href="${base}/.well-known/openfile.json">/.well-known/openfile.json</a>.</p>
|
|
139
|
+
<pre>nixamp hash FILE the same address for a file of yours
|
|
140
|
+
nixamp transcribe FILE and its transcript, kept here</pre>
|
|
141
|
+
</section>
|
|
142
|
+
</main>
|
|
143
|
+
</body>
|
|
144
|
+
</html>
|
|
145
|
+
`;
|
|
146
|
+
}
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Queryable } from "./follows.ts";
|
|
2
|
+
import type { Codecs } from "./audio.ts";
|
|
3
|
+
import type { Enriched } from "./enrich.ts";
|
|
4
|
+
/** What the nixamp key of a record may carry. Every part optional: a record grows. */
|
|
5
|
+
export interface MediaFacts {
|
|
6
|
+
/** The transcript store's identity for the same file. */
|
|
7
|
+
fingerprint?: string;
|
|
8
|
+
/** Seconds. */
|
|
9
|
+
duration?: number;
|
|
10
|
+
codecs?: {
|
|
11
|
+
video?: string;
|
|
12
|
+
audio?: string;
|
|
13
|
+
container?: string;
|
|
14
|
+
};
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
tags?: {
|
|
18
|
+
title?: string;
|
|
19
|
+
artist?: string;
|
|
20
|
+
album?: string;
|
|
21
|
+
};
|
|
22
|
+
/** What nichedb said it is. */
|
|
23
|
+
enrichment?: {
|
|
24
|
+
kind?: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
year?: number | null;
|
|
27
|
+
image?: string | null;
|
|
28
|
+
summary?: string | null;
|
|
29
|
+
page?: string;
|
|
30
|
+
};
|
|
31
|
+
/** The record this file became when it changed on disk, as `sha256:<hex>`. */
|
|
32
|
+
supersededBy?: string;
|
|
33
|
+
/** The record this file was before it changed. */
|
|
34
|
+
supersedes?: string;
|
|
35
|
+
/** When the machine holding it last looked, and when it will next. */
|
|
36
|
+
checkedAt?: string;
|
|
37
|
+
checkAfter?: string;
|
|
38
|
+
}
|
|
39
|
+
/** A place that has carried the file: an OpenFile holder. A nixamp server serves it over HTTP, which makes it a gateway. */
|
|
40
|
+
export interface Holder {
|
|
41
|
+
kind: "gateway" | "peer" | "seeder";
|
|
42
|
+
url: string;
|
|
43
|
+
seenAt: string;
|
|
44
|
+
/** The channel it was carried as, when it was. */
|
|
45
|
+
channel?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface MediaRecord {
|
|
49
|
+
/** The SHA-256 of the bytes, hex. */
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
size: number;
|
|
53
|
+
contentType: string;
|
|
54
|
+
/** When the file last changed, ISO. */
|
|
55
|
+
updated: string;
|
|
56
|
+
facts: MediaFacts;
|
|
57
|
+
holders: Holder[];
|
|
58
|
+
by: string;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
updatedAt: string;
|
|
61
|
+
}
|
|
62
|
+
/** How many places a record remembers carrying it. */
|
|
63
|
+
export declare const MAX_HOLDERS = 50;
|
|
64
|
+
/** The listing at /.well-known/openfile.json is this long at most. */
|
|
65
|
+
export declare const LISTING = 100;
|
|
66
|
+
/** A file changed an hour ago is looked at again in a quarter of an hour; one untouched for years, once a month. */
|
|
67
|
+
export declare const CHECK_MIN_MS: number;
|
|
68
|
+
export declare const CHECK_MAX_MS: number;
|
|
69
|
+
/** The media type a file's name suggests, or octet-stream. */
|
|
70
|
+
export declare function contentTypeOf(path: string): string;
|
|
71
|
+
/** A media id as a request names one: bare hex or `sha256:` hex; the hex, or null. */
|
|
72
|
+
export declare function mediaId(value: unknown): string | null;
|
|
73
|
+
/** The SHA-256 of every byte of a file, as a stream so a film does not sit in memory. */
|
|
74
|
+
export declare function contentHash(path: string): Promise<string>;
|
|
75
|
+
/** What the filesystem says about a file: its size, when it changed, and its type. */
|
|
76
|
+
export declare function fileFacts(path: string): {
|
|
77
|
+
name: string;
|
|
78
|
+
size: number;
|
|
79
|
+
updated: string;
|
|
80
|
+
contentType: string;
|
|
81
|
+
mtimeMs: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* How long to leave a file alone before looking at it again: a quarter of
|
|
85
|
+
* the time since it last changed, between a quarter of an hour and a
|
|
86
|
+
* month. A file being edited is looked at often; a film from 2019 is not
|
|
87
|
+
* stat'd every hour of every day for the rest of its life.
|
|
88
|
+
*/
|
|
89
|
+
export declare function checkInterval(mtimeMs: number, nowMs: number): number;
|
|
90
|
+
/** The facts ffprobe and nichedb give a server, as the record wants them. */
|
|
91
|
+
export declare function factsFrom(codecs?: Codecs | null, enriched?: Enriched | null): MediaFacts;
|
|
92
|
+
/** Facts as a request hands them in: only the keys this file names, each of the right shape. */
|
|
93
|
+
export declare function factsFromRequest(value: unknown): MediaFacts;
|
|
94
|
+
/** A holder as a request hands it in, or null. */
|
|
95
|
+
export declare function holderFrom(value: unknown): Holder | null;
|
|
96
|
+
/** Holders with one entry per address, the latest sighting kept, newest first. */
|
|
97
|
+
export declare function mergeHolders(had: Holder[], added: Holder[]): Holder[];
|
|
98
|
+
export interface MediaAsk {
|
|
99
|
+
id: string;
|
|
100
|
+
name?: string;
|
|
101
|
+
size?: number;
|
|
102
|
+
contentType?: string;
|
|
103
|
+
/** When the file last changed. */
|
|
104
|
+
updated?: string;
|
|
105
|
+
facts?: MediaFacts;
|
|
106
|
+
holder?: Holder | null;
|
|
107
|
+
by: string;
|
|
108
|
+
}
|
|
109
|
+
export declare class Media {
|
|
110
|
+
private readonly db;
|
|
111
|
+
private readonly onEvent;
|
|
112
|
+
private ready;
|
|
113
|
+
constructor(db: Queryable, onEvent?: (message: string) => void);
|
|
114
|
+
private ensure;
|
|
115
|
+
/**
|
|
116
|
+
* Keep what somebody knows about a file. A new id is a new record; a known
|
|
117
|
+
* one keeps its union: facts merge key by key with the newer winning,
|
|
118
|
+
* holders merge by address, a name or a size given replaces one that was
|
|
119
|
+
* not. The account that first kept it stays its keeper.
|
|
120
|
+
*/
|
|
121
|
+
save(ask: MediaAsk): Promise<MediaRecord | null>;
|
|
122
|
+
get(id: string): Promise<MediaRecord | null>;
|
|
123
|
+
/** The record of the file a transcript fingerprint belongs to, the latest when there are several. */
|
|
124
|
+
byFingerprint(fingerprint: string): Promise<MediaRecord | null>;
|
|
125
|
+
/** The latest records, newest first, for the listing. */
|
|
126
|
+
recent(limit?: number): Promise<MediaRecord[]>;
|
|
127
|
+
/** Never throws: a store that is having a moment costs the record, not the request. */
|
|
128
|
+
quietly<T>(what: string, run: () => Promise<T>): Promise<T | null>;
|
|
129
|
+
}
|
|
130
|
+
/** A transcript, as the record lists it: enough to pick a language and fetch it. */
|
|
131
|
+
export interface TranscriptRef {
|
|
132
|
+
language: string;
|
|
133
|
+
translatedFrom: string | null;
|
|
134
|
+
lines: number;
|
|
135
|
+
complete: boolean;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The record as an OpenFile file object: the specification's keys at the
|
|
139
|
+
* top, nixamp's own under `nixamp`. `fetch` is empty on purpose: nixamp.com
|
|
140
|
+
* has no bytes to hand out, only what it knows about them; the holders are
|
|
141
|
+
* where they were last carried.
|
|
142
|
+
*/
|
|
143
|
+
export declare function openFileOf(record: MediaRecord, site: string, transcripts?: TranscriptRef[]): Record<string, unknown>;
|
|
144
|
+
/** The publisher's descriptor: nixamp.com and its latest records. */
|
|
145
|
+
export declare function openFileListing(records: MediaRecord[], site: string): Record<string, unknown>;
|