show description and long description
This commit is contained in:
1 parent
e5e26222ce
commit
c8e38d51f4
2 files changed
+45
-53
No files matched your search
@@ -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
@@ -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';
|
||||
|
||||
Reference in new issue
Block a user