show description and long description

This commit is contained in:
bytedream committed 2026-10-31 12:00:15 +01:00
1 parent e5e26222ce
commit c8e38d51f4
2 files changed
+45 -53

No files matched your search

+8 -4
View File
@@ -12,12 +12,12 @@ if (!video) return new Response(null, { status: 404 });
const metadata = await getVideoMetadata(video);
if (!metadata) return new Response(null, { status: 404 });
const { title, description } = metadata;
const { title, description, longDescription } = metadata;
const streamUrl = siteUrl(`video/${encodeURIComponent(video.name)}/stream`);
const thumbnailUrl = siteUrl(`video/${encodeURIComponent(video.name)}/thumbnail.jpg`);
---
<WebsiteLayout title={`${title} - Video`} description={description ?? undefined} openGraph="minimal">
<WebsiteLayout title={`${title} - Video`} description={description ?? longDescription ?? undefined} openGraph="minimal">
<Fragment slot="head">
<meta property="og:type" content="website" />
<meta property="og:image" content={thumbnailUrl} />
@@ -31,10 +31,14 @@ const thumbnailUrl = siteUrl(`video/${encodeURIComponent(video.name)}/thumbnail.
<h1 class="mt-1 text-2xl font-semibold break-words lg:text-4xl">{title}</h1>
</div>
{description && (
<p class="max-w-3xl text-base leading-relaxed whitespace-pre-line text-base-content/80">{description}</p>
)}
<VideoPlayer client:load src={streamUrl} type={video.contentType} title={title} poster={thumbnailUrl} />
{description && (
<p class="max-w-3xl text-base leading-relaxed whitespace-pre-line text-base-content/70">{description}</p>
{longDescription && (
<p class="max-w-3xl text-base leading-relaxed whitespace-pre-line text-base-content/70">{longDescription}</p>
)}
</section>
</WebsiteLayout>
+37 -49
View File
@@ -6,49 +6,8 @@ import { createHash, randomUUID } from 'node:crypto';
import { CACHE_PATH, VIDEO_PATH } from 'astro:env/server';
import { logger } from './log';
const videoTypes = [
['mp4', 'video/mp4'],
['webm', 'video/webm']
];
const execFileAsync = promisify(execFile);
export interface ResolvedVideo {
name: string;
filename: string;
path: string;
contentType: string;
size: number;
modifiedAt: Date;
}
export interface VideoRange {
start: number;
end: number;
}
export interface VideoThumbnail {
path: string;
contentType: string;
}
export interface VideoMetadata {
title: string;
description: string | null;
}
interface FFprobeStream {
codec_type?: string;
tags?: Record<string, string>;
}
interface FFprobeResult {
streams?: FFprobeStream[];
format?: {
tags?: Record<string, string>;
};
}
interface VideoMetadataCacheEntry {
modifiedAtMs: number;
size: number;
@@ -57,6 +16,14 @@ interface VideoMetadataCacheEntry {
const videoMetadataCache = new Map<string, VideoMetadataCacheEntry>();
export interface ResolvedVideo {
name: string;
filename: string;
path: string;
contentType: string;
size: number;
modifiedAt: Date;
}
export async function resolveVideo(name: string): Promise<ResolvedVideo | null> {
if (!VIDEO_PATH || !name) return null;
@@ -65,34 +32,46 @@ export async function resolveVideo(name: string): Promise<ResolvedVideo | null>
if (!safeName) return null;
for (const [extension, contentType] of videoTypes) {
const filename = `${safeName}.${extension}`;
const filename = `${safeName}.mp4`;
const filePath = path.join(directory, filename);
try {
const stats = await fs.promises.stat(filePath);
if (!stats.isFile()) continue;
if (!stats.isFile()) return null;
return {
name: safeName,
filename,
path: filePath,
contentType,
contentType: 'video/mp4',
size: stats.size,
modifiedAt: stats.mtime
};
} catch (e) {
// check if file doesn't exist
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
continue;
return null;
}
logger.warn({ error: e }, 'error while reading video file');
}
}
return null;
}
export interface VideoMetadata {
title: string;
description: string | null;
longDescription: string | null;
}
interface FFprobeResult {
streams?: {
codec_type?: string;
tags?: Record<string, string>;
}[];
format?: {
tags?: Record<string, string>;
};
}
export async function getVideoMetadata(video: ResolvedVideo): Promise<VideoMetadata | null> {
const modifiedAtMs = video.modifiedAt.getTime();
const cached = videoMetadataCache.get(video.path);
@@ -108,7 +87,7 @@ export async function getVideoMetadata(video: ResolvedVideo): Promise<VideoMetad
// prettier-ignore
[
'-v', 'error',
'-show_entries', 'format_tags=title,description,synopsis:stream=codec_type:stream_tags=title,description,synopsis',
'-show_entries', 'format_tags=title,description,synopsis,longdescription:stream=codec_type:stream_tags=title,description,synopsis,longdescription',
'-of', 'json',
video.path
],
@@ -121,7 +100,8 @@ export async function getVideoMetadata(video: ResolvedVideo): Promise<VideoMetad
metadata = {
title: tags['title'] || video.name,
description: (tags['synopsis'] || tags['longdescription'] || tags['description']) ?? null
description: tags['description'],
longDescription: tags['synopsis'],
};
} catch (e) {
logger.warn({ error: e instanceof Error ? e.message : String(e) }, 'error while reading video metadata');
@@ -137,6 +117,10 @@ export async function getVideoMetadata(video: ResolvedVideo): Promise<VideoMetad
return metadata;
}
export interface VideoThumbnail {
path: string;
contentType: string;
}
export async function getVideoThumbnail(video: ResolvedVideo): Promise<VideoThumbnail | null> {
if (!VIDEO_PATH) return null;
@@ -184,6 +168,10 @@ export async function getVideoThumbnail(video: ResolvedVideo): Promise<VideoThum
}
}
export interface VideoRange {
start: number;
end: number;
}
export function parseVideoRange(rangeHeader: string | null, size: number): VideoRange | 'invalid' | null {
if (!rangeHeader) return null;
if (size <= 0) return 'invalid';