includio-cms 0.13.2 → 0.13.4
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/CHANGELOG.md +32 -0
- package/ROADMAP.md +19 -2
- package/dist/admin/api/handler.js +2 -0
- package/dist/admin/api/media-gc.js +8 -3
- package/dist/admin/api/regenerate-posters.d.ts +2 -0
- package/dist/admin/api/regenerate-posters.js +32 -0
- package/dist/admin/api/replace.js +4 -0
- package/dist/admin/api/rest/middleware/apiKey.js +7 -1
- package/dist/admin/api/upload.js +4 -0
- package/dist/admin/client/collection/collection-entries.svelte +8 -4
- package/dist/admin/client/collection/grid-view.svelte +1 -1
- package/dist/admin/client/entry/entry-header.svelte +37 -44
- package/dist/admin/client/entry/entry-header.svelte.d.ts +1 -2
- package/dist/admin/client/entry/entry-version.svelte +9 -3
- package/dist/admin/client/entry/entry.svelte +20 -1
- package/dist/admin/client/maintenance/maintenance-page.svelte +153 -0
- package/dist/admin/components/fields/seo-field.svelte +30 -16
- package/dist/admin/remote/entry.remote.js +3 -4
- package/dist/admin/state/content-language.svelte.d.ts +0 -3
- package/dist/admin/state/content-language.svelte.js +7 -11
- package/dist/admin/utils/entryLabel.js +2 -3
- package/dist/cms/runtime/api.d.ts +5 -0
- package/dist/cms/runtime/types.d.ts +13 -8
- package/dist/core/cms.js +3 -0
- package/dist/core/fields/layoutUtils.d.ts +2 -2
- package/dist/core/fields/layoutUtils.js +3 -10
- package/dist/core/server/entries/operations/get.js +2 -2
- package/dist/core/server/media/mimeBlocklist.d.ts +1 -0
- package/dist/core/server/media/mimeBlocklist.js +31 -0
- package/dist/core/server/media/operations/batchRegenerateVideoPosters.d.ts +15 -0
- package/dist/core/server/media/operations/batchRegenerateVideoPosters.js +112 -0
- package/dist/files-local/index.d.ts +1 -0
- package/dist/files-local/index.js +3 -140
- package/dist/files-local/sanitizeFilename.js +2 -1
- package/dist/files-local/video.d.ts +9 -0
- package/dist/files-local/video.js +145 -0
- package/dist/paraglide/messages/_index.d.ts +3 -36
- package/dist/paraglide/messages/_index.js +3 -71
- package/dist/paraglide/messages/hello_world.d.ts +5 -0
- package/dist/paraglide/messages/hello_world.js +33 -0
- package/dist/paraglide/messages/login_hello.d.ts +16 -0
- package/dist/paraglide/messages/login_hello.js +34 -0
- package/dist/paraglide/messages/login_please_login.d.ts +16 -0
- package/dist/paraglide/messages/login_please_login.js +34 -0
- package/dist/sveltekit/server/handle.js +8 -0
- package/dist/updates/0.13.3/index.d.ts +2 -0
- package/dist/updates/0.13.3/index.js +21 -0
- package/dist/updates/0.13.4/index.d.ts +2 -0
- package/dist/updates/0.13.4/index.js +14 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
- package/dist/admin/utils/translationStatus.d.ts +0 -17
- package/dist/admin/utils/translationStatus.js +0 -133
- package/dist/demo/reset.d.ts +0 -1
- package/dist/demo/reset.js +0 -26
- package/dist/paraglide/messages/en.d.ts +0 -5
- package/dist/paraglide/messages/en.js +0 -14
- package/dist/paraglide/messages/pl.d.ts +0 -5
- package/dist/paraglide/messages/pl.js +0 -14
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getCMS } from '../../../cms.js';
|
|
2
|
+
import { processVideo } from '../../../../files-local/video.js';
|
|
3
|
+
import { fullDir } from '../../../../files-local/index.js';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { unlink } from 'node:fs/promises';
|
|
6
|
+
const VIDEO_MIME_TYPES = [
|
|
7
|
+
'video/mp4',
|
|
8
|
+
'video/quicktime',
|
|
9
|
+
'video/webm',
|
|
10
|
+
'video/x-msvideo',
|
|
11
|
+
'video/x-matroska'
|
|
12
|
+
];
|
|
13
|
+
export async function* batchRegenerateVideoPosters(signal) {
|
|
14
|
+
const cms = getCMS();
|
|
15
|
+
const allFiles = await cms.databaseAdapter.getMediaFiles({
|
|
16
|
+
data: { mimeTypes: VIDEO_MIME_TYPES }
|
|
17
|
+
});
|
|
18
|
+
const total = allFiles.length;
|
|
19
|
+
let totalCreated = 0;
|
|
20
|
+
let totalSkipped = 0;
|
|
21
|
+
for (let i = 0; i < allFiles.length; i++) {
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
return;
|
|
24
|
+
const file = allFiles[i];
|
|
25
|
+
yield {
|
|
26
|
+
type: 'progress',
|
|
27
|
+
total,
|
|
28
|
+
processed: i,
|
|
29
|
+
created: totalCreated,
|
|
30
|
+
skipped: totalSkipped,
|
|
31
|
+
currentFile: file.name
|
|
32
|
+
};
|
|
33
|
+
// Check if poster and thumbnail already exist on disk
|
|
34
|
+
const posterFilename = file.posterUrl?.split('/').pop();
|
|
35
|
+
const thumbFilename = file.thumbnailUrl?.split('/').pop();
|
|
36
|
+
const hasPoster = posterFilename ? await fileExists(resolve(fullDir, posterFilename)) : false;
|
|
37
|
+
const hasThumb = thumbFilename ? await fileExists(resolve(fullDir, thumbFilename)) : false;
|
|
38
|
+
if (hasPoster && hasThumb) {
|
|
39
|
+
totalSkipped++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// Get video filepath
|
|
44
|
+
const videoFilename = file.url.split('/').pop();
|
|
45
|
+
if (!videoFilename) {
|
|
46
|
+
throw new Error('No video filename');
|
|
47
|
+
}
|
|
48
|
+
const videoPath = resolve(fullDir, videoFilename);
|
|
49
|
+
// Delete old poster/thumb if they exist
|
|
50
|
+
if (posterFilename) {
|
|
51
|
+
try {
|
|
52
|
+
await unlink(resolve(fullDir, posterFilename));
|
|
53
|
+
}
|
|
54
|
+
catch { /* ok */ }
|
|
55
|
+
}
|
|
56
|
+
if (thumbFilename) {
|
|
57
|
+
try {
|
|
58
|
+
await unlink(resolve(fullDir, thumbFilename));
|
|
59
|
+
}
|
|
60
|
+
catch { /* ok */ }
|
|
61
|
+
}
|
|
62
|
+
// Regenerate
|
|
63
|
+
const meta = await processVideo(videoPath, file.name, fullDir);
|
|
64
|
+
// Update DB
|
|
65
|
+
await cms.databaseAdapter.updateMediaFile({
|
|
66
|
+
id: file.id,
|
|
67
|
+
data: {
|
|
68
|
+
posterUrl: meta.posterUrl,
|
|
69
|
+
thumbnailUrl: meta.thumbnailUrl,
|
|
70
|
+
...(meta.width != null && { width: meta.width }),
|
|
71
|
+
...(meta.height != null && { height: meta.height }),
|
|
72
|
+
...(meta.duration != null && { duration: meta.duration })
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
totalCreated++;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
yield {
|
|
79
|
+
type: 'error',
|
|
80
|
+
total,
|
|
81
|
+
processed: i + 1,
|
|
82
|
+
created: totalCreated,
|
|
83
|
+
skipped: totalSkipped,
|
|
84
|
+
currentFile: file.name,
|
|
85
|
+
error: e instanceof Error ? e.message : String(e)
|
|
86
|
+
};
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
yield { type: 'done', total, processed: total, created: totalCreated, skipped: totalSkipped };
|
|
91
|
+
}
|
|
92
|
+
async function fileExists(path) {
|
|
93
|
+
try {
|
|
94
|
+
const { stat } = await import('node:fs/promises');
|
|
95
|
+
await stat(path);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function getVideoPosterStatus() {
|
|
103
|
+
const allFiles = await getCMS().databaseAdapter.getMediaFiles({
|
|
104
|
+
data: { mimeTypes: VIDEO_MIME_TYPES }
|
|
105
|
+
});
|
|
106
|
+
const videosWithPosters = allFiles.filter((f) => f.posterUrl != null).length;
|
|
107
|
+
return {
|
|
108
|
+
videosCount: allFiles.length,
|
|
109
|
+
videosWithPosters,
|
|
110
|
+
videosMissingPosters: allFiles.length - videosWithPosters
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -2,152 +2,15 @@ import { basename, extname, resolve } from 'node:path';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { readFile, writeFile, rename, unlink, mkdir, readdir } from 'node:fs/promises';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
|
-
import ffmpeg from 'fluent-ffmpeg';
|
|
6
5
|
import { fileURLToPath } from 'url';
|
|
7
6
|
import { dirname } from 'path';
|
|
8
7
|
import { sanitizeFilename } from './sanitizeFilename.js';
|
|
8
|
+
import { processVideo } from './video.js';
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = dirname(__filename);
|
|
11
11
|
global.__dirname = __dirname ?? '';
|
|
12
|
-
const fullDir = process.env.NODE_ENV === 'production' ? `/data/uploads` : `./static/uploads`;
|
|
12
|
+
export const fullDir = process.env.NODE_ENV === 'production' ? `/data/uploads` : `./static/uploads`;
|
|
13
13
|
const privateDir = process.env.NODE_ENV === 'production' ? `/data/private-uploads` : `./data/private-uploads`;
|
|
14
|
-
// Ustawienie ścieżek do ffmpeg
|
|
15
|
-
const ffmpegPath = process.env.FFMPEG_PATH || '/usr/bin/ffmpeg';
|
|
16
|
-
const ffprobePath = process.env.FFPROBE_PATH || '/usr/bin/ffprobe';
|
|
17
|
-
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
18
|
-
ffmpeg.setFfprobePath(ffprobePath);
|
|
19
|
-
function withTimeout(promise, ms, label) {
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
22
|
-
promise.then((val) => {
|
|
23
|
-
clearTimeout(timer);
|
|
24
|
-
resolve(val);
|
|
25
|
-
}, (err) => {
|
|
26
|
-
clearTimeout(timer);
|
|
27
|
-
reject(err);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
function probeVideo(filepath) {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
ffmpeg.ffprobe(filepath, (err, metadata) => {
|
|
34
|
-
if (err) {
|
|
35
|
-
reject(new Error(`ffprobe failed: ${err.message || err}`));
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const videoStream = metadata.streams.find((s) => s.codec_type === 'video');
|
|
39
|
-
const duration = metadata.format.duration ?? null;
|
|
40
|
-
resolve({
|
|
41
|
-
width: videoStream?.width ?? null,
|
|
42
|
-
height: videoStream?.height ?? null,
|
|
43
|
-
duration: duration ? Math.round(duration * 100) / 100 : null
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
function takeScreenshot(filepath, config) {
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
ffmpeg(filepath)
|
|
51
|
-
.on('error', (err) => reject(new Error(`Screenshot failed: ${err.message || err}`)))
|
|
52
|
-
.on('end', () => resolve())
|
|
53
|
-
.screenshots({
|
|
54
|
-
timestamps: [config.timestamp],
|
|
55
|
-
filename: config.filename,
|
|
56
|
-
folder: config.folder,
|
|
57
|
-
size: config.size
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
function getSafeTimestamp(duration) {
|
|
62
|
-
if (duration === null || duration <= 0)
|
|
63
|
-
return '00:00:00.000';
|
|
64
|
-
if (duration < 1)
|
|
65
|
-
return '00:00:00.000';
|
|
66
|
-
if (duration < 3) {
|
|
67
|
-
const t = duration * 0.1;
|
|
68
|
-
return formatTimestamp(t);
|
|
69
|
-
}
|
|
70
|
-
return formatTimestamp(Math.min(1, duration * 0.1));
|
|
71
|
-
}
|
|
72
|
-
function formatTimestamp(seconds) {
|
|
73
|
-
const h = Math.floor(seconds / 3600);
|
|
74
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
75
|
-
const s = seconds % 60;
|
|
76
|
-
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${s.toFixed(3).padStart(6, '0')}`;
|
|
77
|
-
}
|
|
78
|
-
let ffmpegAvailable = null;
|
|
79
|
-
async function checkFfmpegAvailable() {
|
|
80
|
-
if (ffmpegAvailable !== null)
|
|
81
|
-
return ffmpegAvailable;
|
|
82
|
-
return new Promise((resolve) => {
|
|
83
|
-
ffmpeg.ffprobe('/dev/null', (err) => {
|
|
84
|
-
if (err && (String(err).includes('ENOENT') || String(err).includes('spawn'))) {
|
|
85
|
-
console.warn('[video] ffmpeg/ffprobe not found — video thumbnails disabled');
|
|
86
|
-
ffmpegAvailable = false;
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
ffmpegAvailable = true;
|
|
90
|
-
}
|
|
91
|
-
resolve(ffmpegAvailable);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
async function processVideo(filepath, filename) {
|
|
96
|
-
if (!(await checkFfmpegAvailable())) {
|
|
97
|
-
console.warn(`[video] Skipping processing for ${filename}: ffmpeg not available`);
|
|
98
|
-
return { width: null, height: null, duration: null, thumbnailUrl: null, posterUrl: null };
|
|
99
|
-
}
|
|
100
|
-
const thumbnailFilename = `${filename}_thumb.jpg`;
|
|
101
|
-
const thumbnailUrl = `/uploads/${thumbnailFilename}`;
|
|
102
|
-
const posterFilename = `${filename}_poster.jpg`;
|
|
103
|
-
const posterUrl = `/uploads/${posterFilename}`;
|
|
104
|
-
// 1. Probe — get metadata first
|
|
105
|
-
let probe;
|
|
106
|
-
try {
|
|
107
|
-
probe = await withTimeout(probeVideo(filepath), 10_000, 'ffprobe');
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
console.warn(`[video] ffprobe failed for ${filename}:`, err);
|
|
111
|
-
return { width: null, height: null, duration: null, thumbnailUrl: null, posterUrl: null };
|
|
112
|
-
}
|
|
113
|
-
const timestamp = getSafeTimestamp(probe.duration);
|
|
114
|
-
// 2. Thumbnail (320px wide, maintain aspect ratio)
|
|
115
|
-
let finalThumbnailUrl = null;
|
|
116
|
-
try {
|
|
117
|
-
await withTimeout(takeScreenshot(filepath, {
|
|
118
|
-
timestamp,
|
|
119
|
-
filename: thumbnailFilename,
|
|
120
|
-
folder: fullDir,
|
|
121
|
-
size: '320x?'
|
|
122
|
-
}), 15_000, 'thumbnail');
|
|
123
|
-
finalThumbnailUrl = thumbnailUrl;
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
console.warn(`[video] Thumbnail failed for ${filename}:`, err);
|
|
127
|
-
}
|
|
128
|
-
// 3. Poster (up to 1280px wide, no upscale)
|
|
129
|
-
let finalPosterUrl = null;
|
|
130
|
-
const posterWidth = Math.min(1280, probe.width ?? 1280);
|
|
131
|
-
try {
|
|
132
|
-
await withTimeout(takeScreenshot(filepath, {
|
|
133
|
-
timestamp,
|
|
134
|
-
filename: posterFilename,
|
|
135
|
-
folder: fullDir,
|
|
136
|
-
size: `${posterWidth}x?`
|
|
137
|
-
}), 15_000, 'poster');
|
|
138
|
-
finalPosterUrl = posterUrl;
|
|
139
|
-
}
|
|
140
|
-
catch (err) {
|
|
141
|
-
console.warn(`[video] Poster failed for ${filename}:`, err);
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
width: probe.width,
|
|
145
|
-
height: probe.height,
|
|
146
|
-
duration: probe.duration ? Math.round(probe.duration) : null,
|
|
147
|
-
thumbnailUrl: finalThumbnailUrl,
|
|
148
|
-
posterUrl: finalPosterUrl
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
14
|
async function ensureDir(dir) {
|
|
152
15
|
try {
|
|
153
16
|
await mkdir(dir, { recursive: true });
|
|
@@ -228,7 +91,7 @@ export function local() {
|
|
|
228
91
|
else if (file.type.startsWith('video/')) {
|
|
229
92
|
type = 'video';
|
|
230
93
|
try {
|
|
231
|
-
const videoMeta = await processVideo(filepath, filename);
|
|
94
|
+
const videoMeta = await processVideo(filepath, filename, fullDir);
|
|
232
95
|
width = videoMeta.width;
|
|
233
96
|
height = videoMeta.height;
|
|
234
97
|
duration = videoMeta.duration;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Prevents path traversal, null byte injection, and other attacks.
|
|
4
4
|
*/
|
|
5
5
|
export function sanitizeFilename(name) {
|
|
6
|
-
|
|
6
|
+
// Normalize Unicode to NFC (composed form) to prevent NFD/NFC mismatch
|
|
7
|
+
let sanitized = name.normalize('NFC');
|
|
7
8
|
// Remove null bytes
|
|
8
9
|
sanitized = sanitized.replace(/\0/g, '');
|
|
9
10
|
// Remove path separators and traversal patterns
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface VideoMetadata {
|
|
2
|
+
width: number | null;
|
|
3
|
+
height: number | null;
|
|
4
|
+
duration: number | null;
|
|
5
|
+
thumbnailUrl: string | null;
|
|
6
|
+
posterUrl: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function checkFfmpegAvailable(): Promise<boolean>;
|
|
9
|
+
export declare function processVideo(filepath: string, filename: string, outputDir: string): Promise<VideoMetadata>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import ffmpeg from 'fluent-ffmpeg';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = dirname(__filename);
|
|
6
|
+
if (typeof globalThis.__dirname === 'undefined') {
|
|
7
|
+
globalThis.__dirname = __dirname ?? '';
|
|
8
|
+
}
|
|
9
|
+
// Ustawienie ścieżek do ffmpeg
|
|
10
|
+
const ffmpegPath = process.env.FFMPEG_PATH || '/usr/bin/ffmpeg';
|
|
11
|
+
const ffprobePath = process.env.FFPROBE_PATH || '/usr/bin/ffprobe';
|
|
12
|
+
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
13
|
+
ffmpeg.setFfprobePath(ffprobePath);
|
|
14
|
+
function withTimeout(promise, ms, label) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
17
|
+
promise.then((val) => {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
resolve(val);
|
|
20
|
+
}, (err) => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
reject(err);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function probeVideo(filepath) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
ffmpeg.ffprobe(filepath, (err, metadata) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
reject(new Error(`ffprobe failed: ${err.message || err}`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const videoStream = metadata.streams.find((s) => s.codec_type === 'video');
|
|
34
|
+
const duration = metadata.format.duration ?? null;
|
|
35
|
+
resolve({
|
|
36
|
+
width: videoStream?.width ?? null,
|
|
37
|
+
height: videoStream?.height ?? null,
|
|
38
|
+
duration: duration ? Math.round(duration * 100) / 100 : null
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function takeScreenshot(filepath, config) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
ffmpeg(filepath)
|
|
46
|
+
.on('error', (err) => reject(new Error(`Screenshot failed: ${err.message || err}`)))
|
|
47
|
+
.on('end', () => resolve())
|
|
48
|
+
.screenshots({
|
|
49
|
+
timestamps: [config.timestamp],
|
|
50
|
+
filename: config.filename,
|
|
51
|
+
folder: config.folder,
|
|
52
|
+
size: config.size
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function getSafeTimestamp(duration) {
|
|
57
|
+
if (duration === null || duration <= 0)
|
|
58
|
+
return '00:00:00.000';
|
|
59
|
+
if (duration < 1)
|
|
60
|
+
return '00:00:00.000';
|
|
61
|
+
if (duration < 3) {
|
|
62
|
+
const t = duration * 0.1;
|
|
63
|
+
return formatTimestamp(t);
|
|
64
|
+
}
|
|
65
|
+
return formatTimestamp(Math.min(1, duration * 0.1));
|
|
66
|
+
}
|
|
67
|
+
function formatTimestamp(seconds) {
|
|
68
|
+
const h = Math.floor(seconds / 3600);
|
|
69
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
70
|
+
const s = seconds % 60;
|
|
71
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${s.toFixed(3).padStart(6, '0')}`;
|
|
72
|
+
}
|
|
73
|
+
let ffmpegAvailable = null;
|
|
74
|
+
export async function checkFfmpegAvailable() {
|
|
75
|
+
if (ffmpegAvailable !== null)
|
|
76
|
+
return ffmpegAvailable;
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
ffmpeg.ffprobe('/dev/null', (err) => {
|
|
79
|
+
if (err && (String(err).includes('ENOENT') || String(err).includes('spawn'))) {
|
|
80
|
+
console.warn('[video] ffmpeg/ffprobe not found — video thumbnails disabled');
|
|
81
|
+
ffmpegAvailable = false;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
ffmpegAvailable = true;
|
|
85
|
+
}
|
|
86
|
+
resolve(ffmpegAvailable);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export async function processVideo(filepath, filename, outputDir) {
|
|
91
|
+
if (!(await checkFfmpegAvailable())) {
|
|
92
|
+
console.warn(`[video] Skipping processing for ${filename}: ffmpeg not available`);
|
|
93
|
+
return { width: null, height: null, duration: null, thumbnailUrl: null, posterUrl: null };
|
|
94
|
+
}
|
|
95
|
+
const thumbnailFilename = `${filename}_thumb.jpg`;
|
|
96
|
+
const thumbnailUrl = `/uploads/${thumbnailFilename}`;
|
|
97
|
+
const posterFilename = `${filename}_poster.jpg`;
|
|
98
|
+
const posterUrl = `/uploads/${posterFilename}`;
|
|
99
|
+
// 1. Probe — get metadata first
|
|
100
|
+
let probe;
|
|
101
|
+
try {
|
|
102
|
+
probe = await withTimeout(probeVideo(filepath), 10_000, 'ffprobe');
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.warn(`[video] ffprobe failed for ${filename}:`, err);
|
|
106
|
+
return { width: null, height: null, duration: null, thumbnailUrl: null, posterUrl: null };
|
|
107
|
+
}
|
|
108
|
+
const timestamp = getSafeTimestamp(probe.duration);
|
|
109
|
+
// 2. Thumbnail (320px wide, maintain aspect ratio)
|
|
110
|
+
let finalThumbnailUrl = null;
|
|
111
|
+
try {
|
|
112
|
+
await withTimeout(takeScreenshot(filepath, {
|
|
113
|
+
timestamp,
|
|
114
|
+
filename: thumbnailFilename,
|
|
115
|
+
folder: outputDir,
|
|
116
|
+
size: '320x?'
|
|
117
|
+
}), 15_000, 'thumbnail');
|
|
118
|
+
finalThumbnailUrl = thumbnailUrl;
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.warn(`[video] Thumbnail failed for ${filename}:`, err);
|
|
122
|
+
}
|
|
123
|
+
// 3. Poster (up to 1280px wide, no upscale)
|
|
124
|
+
let finalPosterUrl = null;
|
|
125
|
+
const posterWidth = Math.min(1280, probe.width ?? 1280);
|
|
126
|
+
try {
|
|
127
|
+
await withTimeout(takeScreenshot(filepath, {
|
|
128
|
+
timestamp,
|
|
129
|
+
filename: posterFilename,
|
|
130
|
+
folder: outputDir,
|
|
131
|
+
size: `${posterWidth}x?`
|
|
132
|
+
}), 15_000, 'poster');
|
|
133
|
+
finalPosterUrl = posterUrl;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.warn(`[video] Poster failed for ${filename}:`, err);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
width: probe.width,
|
|
140
|
+
height: probe.height,
|
|
141
|
+
duration: probe.duration ? Math.round(probe.duration) : null,
|
|
142
|
+
thumbnailUrl: finalThumbnailUrl,
|
|
143
|
+
posterUrl: finalPosterUrl
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
locale?: "en" | "pl";
|
|
5
|
-
}): string;
|
|
6
|
-
/**
|
|
7
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
8
|
-
*
|
|
9
|
-
* - Changing this function will be over-written by the next build.
|
|
10
|
-
*
|
|
11
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
12
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
13
|
-
*
|
|
14
|
-
* @param {{}} inputs
|
|
15
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
16
|
-
* @returns {string}
|
|
17
|
-
*/
|
|
18
|
-
declare function login_hello(inputs?: {}, options?: {
|
|
19
|
-
locale?: "en" | "pl";
|
|
20
|
-
}): string;
|
|
21
|
-
/**
|
|
22
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
23
|
-
*
|
|
24
|
-
* - Changing this function will be over-written by the next build.
|
|
25
|
-
*
|
|
26
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
27
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
28
|
-
*
|
|
29
|
-
* @param {{}} inputs
|
|
30
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
31
|
-
* @returns {string}
|
|
32
|
-
*/
|
|
33
|
-
declare function login_please_login(inputs?: {}, options?: {
|
|
34
|
-
locale?: "en" | "pl";
|
|
35
|
-
}): string;
|
|
36
|
-
export { login_hello as login.hello, login_please_login as login.please_login };
|
|
1
|
+
export * from "./hello_world.js";
|
|
2
|
+
export * from "./login_hello.js";
|
|
3
|
+
export * from "./login_please_login.js";
|
|
@@ -1,72 +1,4 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
7
|
-
*
|
|
8
|
-
* - Changing this function will be over-written by the next build.
|
|
9
|
-
*
|
|
10
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
11
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
12
|
-
*
|
|
13
|
-
* @param {{ name: NonNullable<unknown> }} inputs
|
|
14
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
15
|
-
* @returns {string}
|
|
16
|
-
*/
|
|
17
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
18
|
-
export const hello_world = (inputs, options = {}) => {
|
|
19
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
20
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
|
|
21
|
-
}
|
|
22
|
-
const locale = options.locale ?? getLocale()
|
|
23
|
-
trackMessageCall("hello_world", locale)
|
|
24
|
-
if (locale === "en") return en.hello_world(inputs)
|
|
25
|
-
return pl.hello_world(inputs)
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
29
|
-
*
|
|
30
|
-
* - Changing this function will be over-written by the next build.
|
|
31
|
-
*
|
|
32
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
33
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
34
|
-
*
|
|
35
|
-
* @param {{}} inputs
|
|
36
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
37
|
-
* @returns {string}
|
|
38
|
-
*/
|
|
39
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
40
|
-
const login_hello = (inputs = {}, options = {}) => {
|
|
41
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
42
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
|
|
43
|
-
}
|
|
44
|
-
const locale = options.locale ?? getLocale()
|
|
45
|
-
trackMessageCall("login_hello", locale)
|
|
46
|
-
if (locale === "en") return en.login_hello(inputs)
|
|
47
|
-
return pl.login_hello(inputs)
|
|
48
|
-
};
|
|
49
|
-
export { login_hello as "login.hello" }
|
|
50
|
-
/**
|
|
51
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
52
|
-
*
|
|
53
|
-
* - Changing this function will be over-written by the next build.
|
|
54
|
-
*
|
|
55
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
56
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
57
|
-
*
|
|
58
|
-
* @param {{}} inputs
|
|
59
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
60
|
-
* @returns {string}
|
|
61
|
-
*/
|
|
62
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
63
|
-
const login_please_login = (inputs = {}, options = {}) => {
|
|
64
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
65
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.login_please_login(inputs)
|
|
66
|
-
}
|
|
67
|
-
const locale = options.locale ?? getLocale()
|
|
68
|
-
trackMessageCall("login_please_login", locale)
|
|
69
|
-
if (locale === "en") return en.login_please_login(inputs)
|
|
70
|
-
return pl.login_please_login(inputs)
|
|
71
|
-
};
|
|
72
|
-
export { login_please_login as "login.please_login" }
|
|
2
|
+
export * from './hello_world.js'
|
|
3
|
+
export * from './login_hello.js'
|
|
4
|
+
export * from './login_please_login.js'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
+
|
|
4
|
+
const en_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
+
return `Hello, ${i.name} from en!`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const pl_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
9
|
+
return `Hello, ${i.name} from pl!`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
+
*
|
|
15
|
+
* - Changing this function will be over-written by the next build.
|
|
16
|
+
*
|
|
17
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
+
*
|
|
20
|
+
* @param {{ name: NonNullable<unknown> }} inputs
|
|
21
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
+
export const hello_world = (inputs, options = {}) => {
|
|
26
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
|
|
28
|
+
}
|
|
29
|
+
const locale = options.locale ?? getLocale()
|
|
30
|
+
trackMessageCall("hello_world", locale)
|
|
31
|
+
if (locale === "en") return en_hello_world(inputs)
|
|
32
|
+
return pl_hello_world(inputs)
|
|
33
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { login_hello as login.hello };
|
|
2
|
+
/**
|
|
3
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
4
|
+
*
|
|
5
|
+
* - Changing this function will be over-written by the next build.
|
|
6
|
+
*
|
|
7
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
8
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
9
|
+
*
|
|
10
|
+
* @param {{}} inputs
|
|
11
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
declare function login_hello(inputs?: {}, options?: {
|
|
15
|
+
locale?: "en" | "pl";
|
|
16
|
+
}): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
+
|
|
4
|
+
const en_login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
5
|
+
return `Welcome back`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const pl_login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
+
return `Witaj ponownie`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
+
*
|
|
15
|
+
* - Changing this function will be over-written by the next build.
|
|
16
|
+
*
|
|
17
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
+
*
|
|
20
|
+
* @param {{}} inputs
|
|
21
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
+
const login_hello = (inputs = {}, options = {}) => {
|
|
26
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
|
|
28
|
+
}
|
|
29
|
+
const locale = options.locale ?? getLocale()
|
|
30
|
+
trackMessageCall("login_hello", locale)
|
|
31
|
+
if (locale === "en") return en_login_hello(inputs)
|
|
32
|
+
return pl_login_hello(inputs)
|
|
33
|
+
};
|
|
34
|
+
export { login_hello as "login.hello" }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { login_please_login as login.please_login };
|
|
2
|
+
/**
|
|
3
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
4
|
+
*
|
|
5
|
+
* - Changing this function will be over-written by the next build.
|
|
6
|
+
*
|
|
7
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
8
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
9
|
+
*
|
|
10
|
+
* @param {{}} inputs
|
|
11
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
declare function login_please_login(inputs?: {}, options?: {
|
|
15
|
+
locale?: "en" | "pl";
|
|
16
|
+
}): string;
|