mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
"Share on Nostr" published a kind-1 note whose body ended with a bare thumbnail
URL. Many clients only inline such a URL when a NIP-92 `imeta` entry describes
it, so the viewer often saw a plain link instead of a preview image.
- buildShareEvent now emits an `imeta` entry for the thumbnail. The media type
is inferred from the extension and OMITTED when unknown -- a wrong `m` is
worse than a missing one, since clients use it to decide whether to render
inline at all, so extensionless Blossom URLs ship url + alt only.
- inferImageMimeTypeFromUrl lives in a SEPARATE map from EXTENSION_MIME_MAP:
that map gates video upload/publish paths, and folding image types into it
would let an image pass a video check.
- shareNostrController was passing only { id, pubkey } to buildShareEvent, so
the tag could never be built; it now forwards title and thumbnail.
When a video has a NIP-71 mirror, the note instead quotes it with
`nostr:naddr1...`, which nostr clients render as a native video quote card --
the closest thing to a rich preview that does not depend on any client fetching
OpenGraph tags (most never do).
- The mirror is addressable (kind 34235/34236, author = video pubkey,
d = videoRootId), so js/nostr/mirrorPointer.js derives the coordinate with no
relay lookup, and the pointer survives the author editing the video where a
plain `nevent` would rot on the next replace.
- The pointer is only embedded after nip71MirrorService.findMirror() confirms
the mirror exists. A pointer to a missing event renders as a broken/empty
quote box, which is worse than the plain-link fallback, so a disabled
service, a throwing lookup, a 2.5s timeout or a missing videoRootId all fall
back to the thumbnail note.
- resolveMirrorKind prefers the kinds actually observed on relays and only then
falls back to the same portrait rule the mirror builder uses, so a derived
pointer matches what would be published rather than guessing independently.
- While quoting, the raw thumbnail URL is dropped from the body and the imeta
tag is skipped: the quote card already shows the thumbnail, and emitting both
rendered the same image twice. An `a` tag links the note to the coordinate.
Known limitation: a client rendering the quote card still has to play the
video, so mirrors whose imeta url is an .m3u8 hit the same HLS wall -- title and
thumbnail render, playback depends on the client.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
103 lines
2.9 KiB
JavaScript
103 lines
2.9 KiB
JavaScript
export const EXTENSION_MIME_MAP = Object.freeze(
|
|
Object.fromEntries(
|
|
Object.entries({
|
|
mp4: "video/mp4",
|
|
m4v: "video/x-m4v",
|
|
webm: "video/webm",
|
|
mkv: "video/x-matroska",
|
|
mov: "video/quicktime",
|
|
avi: "video/x-msvideo",
|
|
ogv: "video/ogg",
|
|
ogg: "video/ogg",
|
|
m3u8: "application/x-mpegurl",
|
|
mpd: "application/dash+xml",
|
|
ts: "video/mp2t",
|
|
mpg: "video/mpeg",
|
|
mpeg: "video/mpeg",
|
|
flv: "video/x-flv",
|
|
"3gp": "video/3gpp",
|
|
}).map(([extension, mimeType]) => [
|
|
extension,
|
|
typeof mimeType === "string" ? mimeType.toLowerCase() : "",
|
|
]),
|
|
),
|
|
);
|
|
|
|
export function inferMimeTypeFromUrl(url) {
|
|
if (!url || typeof url !== "string") {
|
|
return "";
|
|
}
|
|
|
|
let pathname = "";
|
|
try {
|
|
const parsed = new URL(url);
|
|
pathname = parsed.pathname || "";
|
|
} catch (err) {
|
|
const sanitized = url.split("?")[0].split("#")[0];
|
|
pathname = sanitized || "";
|
|
}
|
|
|
|
const lastSegment = pathname.split("/").pop() || "";
|
|
const match = lastSegment.match(/\.([a-z0-9]+)$/i);
|
|
if (!match) {
|
|
return "";
|
|
}
|
|
|
|
const extension = match[1].toLowerCase();
|
|
const mimeType = EXTENSION_MIME_MAP[extension];
|
|
return typeof mimeType === "string" ? mimeType : "";
|
|
}
|
|
|
|
// Image types, kept in a SEPARATE map from EXTENSION_MIME_MAP on purpose: that
|
|
// map gates video upload/publish paths (js/nostr/publishHelpers.js,
|
|
// js/nostr/videoPayloadBuilder.js), so folding image types into it would let an
|
|
// image pass a video check. Used for NIP-92 `imeta` hints on thumbnails.
|
|
export const IMAGE_EXTENSION_MIME_MAP = Object.freeze(
|
|
Object.fromEntries(
|
|
Object.entries({
|
|
jpg: "image/jpeg",
|
|
jpeg: "image/jpeg",
|
|
jfif: "image/jpeg",
|
|
png: "image/png",
|
|
gif: "image/gif",
|
|
webp: "image/webp",
|
|
avif: "image/avif",
|
|
bmp: "image/bmp",
|
|
svg: "image/svg+xml",
|
|
ico: "image/x-icon",
|
|
heic: "image/heic",
|
|
heif: "image/heif",
|
|
}).map(([extension, mimeType]) => [extension, mimeType.toLowerCase()]),
|
|
),
|
|
);
|
|
|
|
/**
|
|
* Best-effort image MIME type from a URL's extension. Returns "" when the URL
|
|
* carries no recognizable image extension — callers must treat that as "unknown"
|
|
* and omit the hint rather than guessing, since a wrong `m` value is worse than
|
|
* a missing one (clients use it to decide whether to render inline at all).
|
|
*/
|
|
export function inferImageMimeTypeFromUrl(url) {
|
|
if (!url || typeof url !== "string") {
|
|
return "";
|
|
}
|
|
|
|
let pathname = "";
|
|
try {
|
|
const parsed = new URL(url);
|
|
pathname = parsed.pathname || "";
|
|
} catch (err) {
|
|
const sanitized = url.split("?")[0].split("#")[0];
|
|
pathname = sanitized || "";
|
|
}
|
|
|
|
const lastSegment = pathname.split("/").pop() || "";
|
|
const match = lastSegment.match(/\.([a-z0-9]+)$/i);
|
|
if (!match) {
|
|
return "";
|
|
}
|
|
|
|
const mimeType = IMAGE_EXTENSION_MIME_MAP[match[1].toLowerCase()];
|
|
return typeof mimeType === "string" ? mimeType : "";
|
|
}
|