includio-cms 0.13.4 → 0.14.0
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 +41 -0
- package/ROADMAP.md +12 -2
- package/dist/admin/api/handler.js +4 -0
- package/dist/admin/api/media-gc.js +14 -3
- package/dist/admin/api/system-info.d.ts +2 -0
- package/dist/admin/api/system-info.js +9 -0
- package/dist/admin/api/transcode-videos.d.ts +2 -0
- package/dist/admin/api/transcode-videos.js +32 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +401 -3
- package/dist/core/fields/structuredToHtml.js +20 -1
- package/dist/core/server/fields/resolveImageFields.js +37 -2
- package/dist/core/server/media/operations/batchTranscodeVideos.d.ts +23 -0
- package/dist/core/server/media/operations/batchTranscodeVideos.js +104 -0
- package/dist/core/server/media/operations/deleteMediaFile.js +16 -1
- package/dist/core/server/media/operations/getDiskUsage.d.ts +24 -0
- package/dist/core/server/media/operations/getDiskUsage.js +103 -0
- package/dist/core/server/media/operations/getSystemInfo.d.ts +27 -0
- package/dist/core/server/media/operations/getSystemInfo.js +90 -0
- package/dist/core/server/media/operations/replaceFile.js +10 -0
- package/dist/core/server/media/operations/uploadFile.js +2 -0
- package/dist/core/server/media/styles/ffmpeg/generateVideoStyle.d.ts +7 -0
- package/dist/core/server/media/styles/ffmpeg/generateVideoStyle.js +59 -0
- package/dist/core/server/media/styles/operations/generateDefaultVideoStyles.d.ts +9 -0
- package/dist/core/server/media/styles/operations/generateDefaultVideoStyles.js +88 -0
- package/dist/db-postgres/index.js +113 -0
- package/dist/db-postgres/schema/index.d.ts +1 -0
- package/dist/db-postgres/schema/index.js +1 -0
- package/dist/db-postgres/schema/videoStyle.d.ts +228 -0
- package/dist/db-postgres/schema/videoStyle.js +18 -0
- package/dist/files-local/index.d.ts +5 -1
- package/dist/files-local/index.js +5 -2
- package/dist/files-local/transcode.d.ts +27 -0
- package/dist/files-local/transcode.js +130 -0
- package/dist/files-local/video.d.ts +1 -0
- package/dist/files-local/video.js +27 -5
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/sveltekit/components/video.svelte +15 -1
- package/dist/sveltekit/utils/media.js +2 -2
- package/dist/types/adapters/db.d.ts +37 -1
- package/dist/types/cms.d.ts +16 -0
- package/dist/types/fields.d.ts +12 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/media.d.ts +15 -0
- package/dist/updates/0.14.0/index.d.ts +2 -0
- package/dist/updates/0.14.0/index.js +36 -0
- package/dist/updates/index.js +2 -1
- package/package.json +1 -1
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,47 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.14.0 — 2026-03-24
|
|
7
|
+
|
|
8
|
+
Video transcoding & optimization
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Auto-transcode uploaded videos to mp4 (h264) and webm (vp9) in background
|
|
12
|
+
- Video styles system with status tracking (pending/processing/done/failed)
|
|
13
|
+
- Admin maintenance: video transcoding card with batch transcode, purge & retranscode, SSE progress
|
|
14
|
+
- Configurable video transcoding: formats, max resolution, CRF quality, concurrency
|
|
15
|
+
- Skip logic: skip already-optimized mp4 h264 at 1080p or below, skip files over 500MB
|
|
16
|
+
- Frontend video serves multiple source elements (webm, mp4, original fallback)
|
|
17
|
+
- System info endpoint: CMS version, Node, PostgreSQL, ffmpeg (codec checks), sharp, OS diagnostics
|
|
18
|
+
- Disk usage endpoint: breakdown per category (originals by type, image styles, video styles, posters)
|
|
19
|
+
|
|
20
|
+
### Migration
|
|
21
|
+
|
|
22
|
+
```sql
|
|
23
|
+
CREATE TABLE IF NOT EXISTS video_styles (
|
|
24
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
25
|
+
media_file_id UUID NOT NULL REFERENCES media_file(id) ON DELETE CASCADE,
|
|
26
|
+
name TEXT NOT NULL,
|
|
27
|
+
url TEXT NOT NULL,
|
|
28
|
+
width INTEGER,
|
|
29
|
+
height INTEGER,
|
|
30
|
+
format TEXT NOT NULL,
|
|
31
|
+
codec TEXT,
|
|
32
|
+
file_size INTEGER,
|
|
33
|
+
mime_type TEXT NOT NULL,
|
|
34
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
35
|
+
error TEXT,
|
|
36
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE UNIQUE INDEX IF NOT EXISTS video_styles_unique_key
|
|
40
|
+
ON video_styles (media_file_id, name);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Notes
|
|
44
|
+
|
|
45
|
+
Video transcoding requires ffmpeg with libx264 and libvpx-vp9 codecs. Graceful degradation if ffmpeg is unavailable.
|
|
46
|
+
|
|
6
47
|
## 0.13.4 — 2026-03-24
|
|
7
48
|
|
|
8
49
|
Video poster regeneration, filename sanitization
|
package/ROADMAP.md
CHANGED
|
@@ -246,14 +246,24 @@
|
|
|
246
246
|
- [x] `[chore]` `[P1]` Extracted video processing to reusable module <!-- files: src/lib/files-local/video.ts, src/lib/files-local/index.ts -->
|
|
247
247
|
- [x] `[fix]` `[P1]` Filename sanitization normalizes Unicode to NFC <!-- files: src/lib/files-local/sanitizeFilename.ts -->
|
|
248
248
|
|
|
249
|
-
## 0.14.0 —
|
|
249
|
+
## 0.14.0 — Video transcoding
|
|
250
|
+
|
|
251
|
+
- [x] `[feature]` `[P1]` Video styles system — auto-transcode to mp4 h264 + webm vp9 <!-- files: src/lib/db-postgres/schema/videoStyle.ts, src/lib/files-local/transcode.ts -->
|
|
252
|
+
- [x] `[feature]` `[P1]` Background transcoding pipeline on upload with skip logic <!-- files: src/lib/core/server/media/styles/operations/generateDefaultVideoStyles.ts -->
|
|
253
|
+
- [x] `[feature]` `[P1]` Admin maintenance: video transcoding card (batch, purge, SSE progress) <!-- files: src/lib/admin/client/maintenance/maintenance-page.svelte -->
|
|
254
|
+
- [x] `[feature]` `[P2]` Frontend multi-source video delivery (webm, mp4, original) <!-- files: src/lib/sveltekit/components/video.svelte -->
|
|
255
|
+
- [x] `[feature]` `[P2]` Configurable video transcoding in MediaConfig <!-- files: src/lib/types/cms.ts -->
|
|
256
|
+
- [x] `[feature]` `[P2]` System info endpoint — CMS version, Node, PostgreSQL, ffmpeg, sharp, OS <!-- files: src/lib/admin/api/system-info.ts, src/lib/core/server/media/operations/getSystemInfo.ts -->
|
|
257
|
+
- [x] `[feature]` `[P2]` Disk usage endpoint — breakdown per category (originals, image styles, video styles, posters) <!-- files: src/lib/admin/api/system-info.ts, src/lib/core/server/media/operations/getDiskUsage.ts -->
|
|
258
|
+
|
|
259
|
+
## 0.15.0 — SEO module
|
|
250
260
|
|
|
251
261
|
- [ ] `[feature]` `[P1]` SERP preview + character limits for title/description <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
|
|
252
262
|
- [ ] `[feature]` `[P1]` Global SEO settings
|
|
253
263
|
- [ ] `[feature]` `[P1]` Dedicated frontend SEO components <!-- files: src/lib/sveltekit/components/seo.svelte -->
|
|
254
264
|
- [ ] `[feature]` `[P2]` Sitemap generation
|
|
255
265
|
|
|
256
|
-
## 0.
|
|
266
|
+
## 0.16.0 — WCAG/ATAG compliance
|
|
257
267
|
|
|
258
268
|
- [ ] `[chore]` `[P0]` Full WCAG/ATAG audit
|
|
259
269
|
- [ ] `[feature]` `[P0]` Accessibility rework based on audit findings
|
|
@@ -7,7 +7,9 @@ import * as acceptInviteHandlers from './accept-invite.js';
|
|
|
7
7
|
import * as mediaGcHandlers from './media-gc.js';
|
|
8
8
|
import * as generateStylesHandlers from './generate-styles.js';
|
|
9
9
|
import * as regeneratePostersHandlers from './regenerate-posters.js';
|
|
10
|
+
import * as transcodeVideosHandlers from './transcode-videos.js';
|
|
10
11
|
import * as uploadLimitHandlers from './upload-limit.js';
|
|
12
|
+
import * as systemInfoHandlers from './system-info.js';
|
|
11
13
|
import { requireAuth } from '../remote/middleware/auth.js';
|
|
12
14
|
import { getCMS } from '../../core/cms.js';
|
|
13
15
|
import { lookup } from 'mrmime';
|
|
@@ -21,7 +23,9 @@ export function createAdminApiHandler(options) {
|
|
|
21
23
|
'media-gc': mediaGcHandlers,
|
|
22
24
|
'generate-styles': generateStylesHandlers,
|
|
23
25
|
'regenerate-posters': regeneratePostersHandlers,
|
|
26
|
+
'transcode-videos': transcodeVideosHandlers,
|
|
24
27
|
'upload-limit': uploadLimitHandlers,
|
|
28
|
+
'system-info': systemInfoHandlers,
|
|
25
29
|
...options?.extraRoutes
|
|
26
30
|
};
|
|
27
31
|
const privateMediaGet = async (event) => {
|
|
@@ -3,13 +3,15 @@ import { purgeAllImageStyles } from '../../core/server/media/operations/purgeIma
|
|
|
3
3
|
import { getReconciliationReport, deleteOrphanedDiskFiles } from '../../core/server/media/operations/reconcileMedia.js';
|
|
4
4
|
import { getStylesStatus } from '../../core/server/media/styles/operations/batchGenerateStyles.js';
|
|
5
5
|
import { getVideoPosterStatus } from '../../core/server/media/operations/batchRegenerateVideoPosters.js';
|
|
6
|
+
import { getVideoTranscodeStatus, purgeVideoStyles } from '../../core/server/media/operations/batchTranscodeVideos.js';
|
|
6
7
|
import { json } from '@sveltejs/kit';
|
|
7
8
|
export const GET = async ({ url }) => {
|
|
8
9
|
requireRole('admin');
|
|
9
|
-
const [stylesStatus, report, videoPosterStatus] = await Promise.all([
|
|
10
|
+
const [stylesStatus, report, videoPosterStatus, videoTranscodeStatus] = await Promise.all([
|
|
10
11
|
getStylesStatus(),
|
|
11
12
|
getReconciliationReport(),
|
|
12
|
-
getVideoPosterStatus()
|
|
13
|
+
getVideoPosterStatus(),
|
|
14
|
+
getVideoTranscodeStatus()
|
|
13
15
|
]);
|
|
14
16
|
return json({
|
|
15
17
|
imageStylesCount: stylesStatus.existingStyles,
|
|
@@ -20,7 +22,12 @@ export const GET = async ({ url }) => {
|
|
|
20
22
|
missingDiskRecords: report.missingDisk,
|
|
21
23
|
videosCount: videoPosterStatus.videosCount,
|
|
22
24
|
videosWithPosters: videoPosterStatus.videosWithPosters,
|
|
23
|
-
videosMissingPosters: videoPosterStatus.videosMissingPosters
|
|
25
|
+
videosMissingPosters: videoPosterStatus.videosMissingPosters,
|
|
26
|
+
videoStylesCount: videoTranscodeStatus.videoStylesCount,
|
|
27
|
+
videoStylesExpected: videoTranscodeStatus.videoStylesExpected,
|
|
28
|
+
videoStylesDone: videoTranscodeStatus.videoStylesDone,
|
|
29
|
+
videoStylesPending: videoTranscodeStatus.videoStylesPending,
|
|
30
|
+
videoStylesFailed: videoTranscodeStatus.videoStylesFailed
|
|
24
31
|
});
|
|
25
32
|
};
|
|
26
33
|
export const DELETE = async ({ url }) => {
|
|
@@ -34,5 +41,9 @@ export const DELETE = async ({ url }) => {
|
|
|
34
41
|
const result = await deleteOrphanedDiskFiles();
|
|
35
42
|
return json(result);
|
|
36
43
|
}
|
|
44
|
+
if (action === 'purge-video-styles') {
|
|
45
|
+
const result = await purgeVideoStyles();
|
|
46
|
+
return json(result);
|
|
47
|
+
}
|
|
37
48
|
return json({ error: 'Unknown action' }, { status: 400 });
|
|
38
49
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { requireRole } from '../remote/middleware/auth.js';
|
|
2
|
+
import { getDiskUsage } from '../../core/server/media/operations/getDiskUsage.js';
|
|
3
|
+
import { getSystemInfo } from '../../core/server/media/operations/getSystemInfo.js';
|
|
4
|
+
import { json } from '@sveltejs/kit';
|
|
5
|
+
export const GET = async () => {
|
|
6
|
+
requireRole('admin');
|
|
7
|
+
const [diskUsage, systemInfo] = await Promise.all([getDiskUsage(), getSystemInfo()]);
|
|
8
|
+
return json({ diskUsage, systemInfo });
|
|
9
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { requireRole } from '../remote/middleware/auth.js';
|
|
2
|
+
import { batchTranscodeVideos } from '../../core/server/media/operations/batchTranscodeVideos.js';
|
|
3
|
+
export const POST = async () => {
|
|
4
|
+
requireRole('admin');
|
|
5
|
+
const abort = new AbortController();
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const stream = new ReadableStream({
|
|
8
|
+
async start(controller) {
|
|
9
|
+
try {
|
|
10
|
+
for await (const event of batchTranscodeVideos(abort.signal)) {
|
|
11
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'error', error: e instanceof Error ? e.message : String(e) })}\n\n`));
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
controller.close();
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
cancel() {
|
|
22
|
+
abort.abort();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return new Response(stream, {
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'text/event-stream',
|
|
28
|
+
'Cache-Control': 'no-cache',
|
|
29
|
+
Connection: 'keep-alive'
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
};
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
import PlayerStop from '@tabler/icons-svelte/icons/player-stop';
|
|
10
10
|
import CircleCheck from '@tabler/icons-svelte/icons/circle-check';
|
|
11
11
|
import Video from '@tabler/icons-svelte/icons/video';
|
|
12
|
+
import Transform from '@tabler/icons-svelte/icons/transform';
|
|
13
|
+
import Database from '@tabler/icons-svelte/icons/database';
|
|
14
|
+
import Server from '@tabler/icons-svelte/icons/server';
|
|
12
15
|
import Button from '../../../components/ui/button/button.svelte';
|
|
13
16
|
import * as Card from '../../../components/ui/card/index.js';
|
|
14
17
|
import { toast } from 'svelte-sonner';
|
|
@@ -23,9 +26,45 @@
|
|
|
23
26
|
videosCount: number;
|
|
24
27
|
videosWithPosters: number;
|
|
25
28
|
videosMissingPosters: number;
|
|
29
|
+
videoStylesCount: number;
|
|
30
|
+
videoStylesExpected: number;
|
|
31
|
+
videoStylesDone: number;
|
|
32
|
+
videoStylesPending: number;
|
|
33
|
+
videoStylesFailed: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DiskUsage {
|
|
37
|
+
originals: { images: number; videos: number; audio: number; pdf: number; other: number; total: number };
|
|
38
|
+
imageStyles: { count: number; bytes: number };
|
|
39
|
+
videoStyles: { count: number; bytes: number };
|
|
40
|
+
posters: { count: number; bytes: number };
|
|
41
|
+
total: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SystemInfo {
|
|
45
|
+
cmsVersion: string;
|
|
46
|
+
nodeVersion: string;
|
|
47
|
+
postgresVersion: string | null;
|
|
48
|
+
ffmpeg: { available: boolean; version: string | null; codecs: { h264: boolean; vp9: boolean } };
|
|
49
|
+
sharp: { available: boolean; version: string | null };
|
|
50
|
+
os: { platform: string; release: string; cpus: number; totalMemory: number };
|
|
51
|
+
uploads: { path: string };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface SystemInfoResponse {
|
|
55
|
+
diskUsage: DiskUsage;
|
|
56
|
+
systemInfo: SystemInfo;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatBytes(bytes: number): string {
|
|
60
|
+
if (bytes === 0) return '0 B';
|
|
61
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
62
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
63
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
26
64
|
}
|
|
27
65
|
|
|
28
66
|
let report = $state<GcReport | null>(null);
|
|
67
|
+
let sysInfo = $state<SystemInfoResponse | null>(null);
|
|
29
68
|
let loading = $state(true);
|
|
30
69
|
let purging = $state(false);
|
|
31
70
|
let cleaningOrphans = $state(false);
|
|
@@ -54,12 +93,35 @@
|
|
|
54
93
|
|
|
55
94
|
let posterPercent = $derived(posterTotal > 0 ? Math.round((posterProcessed / posterTotal) * 100) : 0);
|
|
56
95
|
|
|
96
|
+
// Video transcode state
|
|
97
|
+
let transcoding = $state(false);
|
|
98
|
+
let transTotal = $state(0);
|
|
99
|
+
let transProcessed = $state(0);
|
|
100
|
+
let transCreated = $state(0);
|
|
101
|
+
let transSkipped = $state(0);
|
|
102
|
+
let transCurrentFile = $state('');
|
|
103
|
+
let transErrors = $state(0);
|
|
104
|
+
let transAbort: AbortController | null = null;
|
|
105
|
+
let purgingVideoStyles = $state(false);
|
|
106
|
+
|
|
107
|
+
let transPercent = $derived(transTotal > 0 ? Math.round((transProcessed / transTotal) * 100) : 0);
|
|
108
|
+
|
|
109
|
+
let videoStylesMissing = $derived(
|
|
110
|
+
report ? report.videoStylesExpected - report.videoStylesDone : 0
|
|
111
|
+
);
|
|
112
|
+
|
|
57
113
|
async function loadReport() {
|
|
58
114
|
loading = true;
|
|
59
115
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
116
|
+
const [gcRes, sysRes] = await Promise.all([
|
|
117
|
+
fetch('/admin/api/media-gc'),
|
|
118
|
+
fetch('/admin/api/system-info')
|
|
119
|
+
]);
|
|
120
|
+
if (!gcRes.ok) throw new Error('Failed to load GC report');
|
|
121
|
+
report = await gcRes.json();
|
|
122
|
+
if (sysRes.ok) {
|
|
123
|
+
sysInfo = await sysRes.json();
|
|
124
|
+
}
|
|
63
125
|
} catch {
|
|
64
126
|
toast.error('Nie udało się załadować raportu');
|
|
65
127
|
} finally {
|
|
@@ -239,6 +301,92 @@
|
|
|
239
301
|
posterAbort?.abort();
|
|
240
302
|
}
|
|
241
303
|
|
|
304
|
+
async function startTranscode() {
|
|
305
|
+
transcoding = true;
|
|
306
|
+
transTotal = 0;
|
|
307
|
+
transProcessed = 0;
|
|
308
|
+
transCreated = 0;
|
|
309
|
+
transSkipped = 0;
|
|
310
|
+
transCurrentFile = '';
|
|
311
|
+
transErrors = 0;
|
|
312
|
+
transAbort = new AbortController();
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const res = await fetch('/admin/api/transcode-videos', {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
signal: transAbort.signal
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (!res.ok) throw new Error('Failed to start');
|
|
321
|
+
if (!res.body) throw new Error('No response body');
|
|
322
|
+
|
|
323
|
+
const reader = res.body.getReader();
|
|
324
|
+
const decoder = new TextDecoder();
|
|
325
|
+
let buffer = '';
|
|
326
|
+
|
|
327
|
+
while (true) {
|
|
328
|
+
const { done, value } = await reader.read();
|
|
329
|
+
if (done) break;
|
|
330
|
+
|
|
331
|
+
buffer += decoder.decode(value, { stream: true });
|
|
332
|
+
const chunks = buffer.split('\n\n');
|
|
333
|
+
buffer = chunks.pop() || '';
|
|
334
|
+
|
|
335
|
+
for (const chunk of chunks) {
|
|
336
|
+
if (!chunk.startsWith('data: ')) continue;
|
|
337
|
+
const event = JSON.parse(chunk.slice(6));
|
|
338
|
+
|
|
339
|
+
transTotal = event.total ?? transTotal;
|
|
340
|
+
transProcessed = event.processed ?? transProcessed;
|
|
341
|
+
transCreated = event.created ?? transCreated;
|
|
342
|
+
transSkipped = event.skipped ?? transSkipped;
|
|
343
|
+
transCurrentFile = event.currentFile ?? transCurrentFile;
|
|
344
|
+
|
|
345
|
+
if (event.type === 'error') {
|
|
346
|
+
transErrors++;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (event.type === 'done') {
|
|
350
|
+
const parts = [`Przetworzono ${transTotal} wariantów`];
|
|
351
|
+
if (transCreated > 0) parts.push(`utworzono ${transCreated}`);
|
|
352
|
+
if (transSkipped > 0) parts.push(`pominięto ${transSkipped}`);
|
|
353
|
+
if (transErrors > 0) parts.push(`${transErrors} błędów`);
|
|
354
|
+
toast.success(parts.join(', '));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch (e) {
|
|
359
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
360
|
+
toast.info(`Przerwano po ${transProcessed}/${transTotal} wariantów`);
|
|
361
|
+
} else {
|
|
362
|
+
toast.error('Błąd podczas transkodowania');
|
|
363
|
+
}
|
|
364
|
+
} finally {
|
|
365
|
+
transcoding = false;
|
|
366
|
+
transAbort = null;
|
|
367
|
+
await loadReport();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function cancelTranscode() {
|
|
372
|
+
transAbort?.abort();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function purgeVideoStylesAction() {
|
|
376
|
+
purgingVideoStyles = true;
|
|
377
|
+
try {
|
|
378
|
+
const res = await fetch('/admin/api/media-gc?action=purge-video-styles', { method: 'DELETE' });
|
|
379
|
+
if (!res.ok) throw new Error('Failed');
|
|
380
|
+
const data = await res.json();
|
|
381
|
+
toast.success(`Usunięto ${data.deletedCount} styli wideo (${data.filesDeleted} plików z dysku)`);
|
|
382
|
+
await loadReport();
|
|
383
|
+
} catch {
|
|
384
|
+
toast.error('Nie udało się usunąć styli wideo');
|
|
385
|
+
} finally {
|
|
386
|
+
purgingVideoStyles = false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
242
390
|
$effect(() => {
|
|
243
391
|
loadReport();
|
|
244
392
|
});
|
|
@@ -413,6 +561,108 @@
|
|
|
413
561
|
</Card.Content>
|
|
414
562
|
</Card.Root>
|
|
415
563
|
|
|
564
|
+
<!-- Video transcoding -->
|
|
565
|
+
<Card.Root>
|
|
566
|
+
<Card.Header>
|
|
567
|
+
<div class="flex items-center gap-2">
|
|
568
|
+
<Transform class="size-5" style="color: var(--primary);" />
|
|
569
|
+
<Card.Title>Transkodowanie wideo</Card.Title>
|
|
570
|
+
</div>
|
|
571
|
+
<Card.Description>
|
|
572
|
+
Warianty wideo (mp4 h264, webm vp9) zoptymalizowane do wyświetlania
|
|
573
|
+
</Card.Description>
|
|
574
|
+
</Card.Header>
|
|
575
|
+
<Card.Content>
|
|
576
|
+
<p class="mb-1 text-3xl font-bold" style="color: var(--primary);">
|
|
577
|
+
{report.videoStylesDone}
|
|
578
|
+
<span class="text-base font-normal" style="color: var(--muted-foreground);">/ {report.videoStylesExpected}</span>
|
|
579
|
+
</p>
|
|
580
|
+
<p class="mb-4 text-xs" style="color: var(--muted-foreground);">
|
|
581
|
+
{report.videosCount} filmów{report.videoStylesPending > 0 ? `, ${report.videoStylesPending} w kolejce` : ''}{report.videoStylesFailed > 0 ? `, ${report.videoStylesFailed} błędów` : ''}
|
|
582
|
+
</p>
|
|
583
|
+
|
|
584
|
+
{#if transcoding}
|
|
585
|
+
<div class="mb-4">
|
|
586
|
+
<div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
|
|
587
|
+
<span>{transProcessed}/{transTotal} wariantów</span>
|
|
588
|
+
<span>{transPercent}%</span>
|
|
589
|
+
</div>
|
|
590
|
+
<div class="h-2 w-full overflow-hidden rounded-full" style="background: var(--muted, #e5e7eb);">
|
|
591
|
+
<div
|
|
592
|
+
class="h-full rounded-full transition-all duration-300"
|
|
593
|
+
style="width: {transPercent}%; background: var(--primary);"
|
|
594
|
+
></div>
|
|
595
|
+
</div>
|
|
596
|
+
<p class="mt-1 text-xs" style="color: var(--muted-foreground);">
|
|
597
|
+
Utworzono: {transCreated}, pominięto: {transSkipped}{transErrors > 0 ? `, błędów: ${transErrors}` : ''}
|
|
598
|
+
</p>
|
|
599
|
+
<p class="mt-0.5 truncate text-xs" style="color: var(--muted-foreground);">
|
|
600
|
+
{transCurrentFile}
|
|
601
|
+
</p>
|
|
602
|
+
<Button
|
|
603
|
+
variant="outline"
|
|
604
|
+
size="sm"
|
|
605
|
+
onclick={cancelTranscode}
|
|
606
|
+
class="mt-2"
|
|
607
|
+
>
|
|
608
|
+
<PlayerStop class="size-4" />
|
|
609
|
+
Anuluj
|
|
610
|
+
</Button>
|
|
611
|
+
</div>
|
|
612
|
+
{:else if videoStylesMissing > 0 || report.videoStylesFailed > 0}
|
|
613
|
+
<div class="mb-3">
|
|
614
|
+
{#if videoStylesMissing > 0}
|
|
615
|
+
<p class="mb-2 text-sm" style="color: var(--warning, #C4893A);">
|
|
616
|
+
Brakuje {videoStylesMissing} wariantów wideo.
|
|
617
|
+
</p>
|
|
618
|
+
{/if}
|
|
619
|
+
{#if report.videoStylesFailed > 0}
|
|
620
|
+
<p class="mb-2 text-sm" style="color: var(--error, #C44B4B);">
|
|
621
|
+
{report.videoStylesFailed} wariantów nie udało się wygenerować.
|
|
622
|
+
</p>
|
|
623
|
+
{/if}
|
|
624
|
+
<Button
|
|
625
|
+
variant="default"
|
|
626
|
+
size="sm"
|
|
627
|
+
onclick={startTranscode}
|
|
628
|
+
>
|
|
629
|
+
<PlayerPlay class="size-4" />
|
|
630
|
+
Transkoduj brakujące
|
|
631
|
+
</Button>
|
|
632
|
+
</div>
|
|
633
|
+
{:else if report.videosCount > 0}
|
|
634
|
+
<div class="mb-3 flex items-center gap-1.5 text-sm" style="color: var(--success, #3A8A5C);">
|
|
635
|
+
<CircleCheck class="size-4" />
|
|
636
|
+
Wszystkie warianty wygenerowane
|
|
637
|
+
</div>
|
|
638
|
+
{:else}
|
|
639
|
+
<div class="mb-3 flex items-center gap-1.5 text-sm" style="color: var(--success, #3A8A5C);">
|
|
640
|
+
<CircleCheck class="size-4" />
|
|
641
|
+
Brak plików wideo
|
|
642
|
+
</div>
|
|
643
|
+
{/if}
|
|
644
|
+
|
|
645
|
+
{#if report.videoStylesCount > 0}
|
|
646
|
+
<Button
|
|
647
|
+
variant="destructive"
|
|
648
|
+
size="sm"
|
|
649
|
+
onclick={purgeVideoStylesAction}
|
|
650
|
+
disabled={purgingVideoStyles || transcoding}
|
|
651
|
+
>
|
|
652
|
+
{#if purgingVideoStyles}
|
|
653
|
+
<Loader2 class="size-4 animate-spin" />
|
|
654
|
+
{:else}
|
|
655
|
+
<Trash class="size-4" />
|
|
656
|
+
{/if}
|
|
657
|
+
Usuń wszystkie i transkoduj ponownie
|
|
658
|
+
</Button>
|
|
659
|
+
<p class="mt-2 text-xs" style="color: var(--text-light);">
|
|
660
|
+
Warianty zostaną odtworzone przy następnym transkodowaniu
|
|
661
|
+
</p>
|
|
662
|
+
{/if}
|
|
663
|
+
</Card.Content>
|
|
664
|
+
</Card.Root>
|
|
665
|
+
|
|
416
666
|
<!-- Orphaned disk files -->
|
|
417
667
|
<Card.Root>
|
|
418
668
|
<Card.Header>
|
|
@@ -489,6 +739,154 @@
|
|
|
489
739
|
{/if}
|
|
490
740
|
</Card.Content>
|
|
491
741
|
</Card.Root>
|
|
742
|
+
|
|
743
|
+
<!-- Disk usage -->
|
|
744
|
+
{#if sysInfo}
|
|
745
|
+
{@const du = sysInfo.diskUsage}
|
|
746
|
+
<Card.Root>
|
|
747
|
+
<Card.Header>
|
|
748
|
+
<div class="flex items-center gap-2">
|
|
749
|
+
<Database class="size-5" style="color: var(--primary);" />
|
|
750
|
+
<Card.Title>Zużycie dysku</Card.Title>
|
|
751
|
+
</div>
|
|
752
|
+
<Card.Description>
|
|
753
|
+
Rozmiar plików mediów i wygenerowanych wariantów
|
|
754
|
+
</Card.Description>
|
|
755
|
+
</Card.Header>
|
|
756
|
+
<Card.Content>
|
|
757
|
+
<p class="mb-4 text-3xl font-bold" style="color: var(--primary);">
|
|
758
|
+
{formatBytes(du.total)}
|
|
759
|
+
</p>
|
|
760
|
+
<dl class="space-y-1.5 text-sm">
|
|
761
|
+
{#if du.originals.images > 0}
|
|
762
|
+
<div class="flex justify-between">
|
|
763
|
+
<dt style="color: var(--muted-foreground);">Obrazy</dt>
|
|
764
|
+
<dd class="font-medium">{formatBytes(du.originals.images)}</dd>
|
|
765
|
+
</div>
|
|
766
|
+
{/if}
|
|
767
|
+
{#if du.originals.videos > 0}
|
|
768
|
+
<div class="flex justify-between">
|
|
769
|
+
<dt style="color: var(--muted-foreground);">Wideo</dt>
|
|
770
|
+
<dd class="font-medium">{formatBytes(du.originals.videos)}</dd>
|
|
771
|
+
</div>
|
|
772
|
+
{/if}
|
|
773
|
+
{#if du.originals.audio > 0}
|
|
774
|
+
<div class="flex justify-between">
|
|
775
|
+
<dt style="color: var(--muted-foreground);">Audio</dt>
|
|
776
|
+
<dd class="font-medium">{formatBytes(du.originals.audio)}</dd>
|
|
777
|
+
</div>
|
|
778
|
+
{/if}
|
|
779
|
+
{#if du.originals.pdf > 0}
|
|
780
|
+
<div class="flex justify-between">
|
|
781
|
+
<dt style="color: var(--muted-foreground);">PDF</dt>
|
|
782
|
+
<dd class="font-medium">{formatBytes(du.originals.pdf)}</dd>
|
|
783
|
+
</div>
|
|
784
|
+
{/if}
|
|
785
|
+
{#if du.originals.other > 0}
|
|
786
|
+
<div class="flex justify-between">
|
|
787
|
+
<dt style="color: var(--muted-foreground);">Inne</dt>
|
|
788
|
+
<dd class="font-medium">{formatBytes(du.originals.other)}</dd>
|
|
789
|
+
</div>
|
|
790
|
+
{/if}
|
|
791
|
+
{#if du.imageStyles.count > 0}
|
|
792
|
+
<div class="flex justify-between">
|
|
793
|
+
<dt style="color: var(--muted-foreground);">Style obrazów ({du.imageStyles.count})</dt>
|
|
794
|
+
<dd class="font-medium">{formatBytes(du.imageStyles.bytes)}</dd>
|
|
795
|
+
</div>
|
|
796
|
+
{/if}
|
|
797
|
+
{#if du.videoStyles.count > 0}
|
|
798
|
+
<div class="flex justify-between">
|
|
799
|
+
<dt style="color: var(--muted-foreground);">Style wideo ({du.videoStyles.count})</dt>
|
|
800
|
+
<dd class="font-medium">{formatBytes(du.videoStyles.bytes)}</dd>
|
|
801
|
+
</div>
|
|
802
|
+
{/if}
|
|
803
|
+
{#if du.posters.count > 0}
|
|
804
|
+
<div class="flex justify-between">
|
|
805
|
+
<dt style="color: var(--muted-foreground);">Postery/miniaturki ({du.posters.count})</dt>
|
|
806
|
+
<dd class="font-medium">{formatBytes(du.posters.bytes)}</dd>
|
|
807
|
+
</div>
|
|
808
|
+
{/if}
|
|
809
|
+
</dl>
|
|
810
|
+
</Card.Content>
|
|
811
|
+
</Card.Root>
|
|
812
|
+
|
|
813
|
+
<!-- System info -->
|
|
814
|
+
{@const si = sysInfo.systemInfo}
|
|
815
|
+
<Card.Root>
|
|
816
|
+
<Card.Header>
|
|
817
|
+
<div class="flex items-center gap-2">
|
|
818
|
+
<Server class="size-5" style="color: var(--primary);" />
|
|
819
|
+
<Card.Title>System</Card.Title>
|
|
820
|
+
</div>
|
|
821
|
+
<Card.Description>
|
|
822
|
+
Informacje o środowisku i zależnościach
|
|
823
|
+
</Card.Description>
|
|
824
|
+
</Card.Header>
|
|
825
|
+
<Card.Content>
|
|
826
|
+
<dl class="space-y-1.5 text-sm">
|
|
827
|
+
<div class="flex justify-between">
|
|
828
|
+
<dt style="color: var(--muted-foreground);">includio-cms</dt>
|
|
829
|
+
<dd class="font-medium">{si.cmsVersion}</dd>
|
|
830
|
+
</div>
|
|
831
|
+
<div class="flex justify-between">
|
|
832
|
+
<dt style="color: var(--muted-foreground);">Node.js</dt>
|
|
833
|
+
<dd class="font-medium">{si.nodeVersion}</dd>
|
|
834
|
+
</div>
|
|
835
|
+
<div class="flex justify-between">
|
|
836
|
+
<dt style="color: var(--muted-foreground);">PostgreSQL</dt>
|
|
837
|
+
<dd class="font-medium">{si.postgresVersion ?? 'niedostępny'}</dd>
|
|
838
|
+
</div>
|
|
839
|
+
<div class="flex justify-between">
|
|
840
|
+
<dt style="color: var(--muted-foreground);">ffmpeg</dt>
|
|
841
|
+
<dd class="font-medium">
|
|
842
|
+
{#if si.ffmpeg.available}
|
|
843
|
+
{si.ffmpeg.version}
|
|
844
|
+
{:else}
|
|
845
|
+
<span style="color: var(--error, #C44B4B);">niedostępny</span>
|
|
846
|
+
{/if}
|
|
847
|
+
</dd>
|
|
848
|
+
</div>
|
|
849
|
+
{#if si.ffmpeg.available}
|
|
850
|
+
<div class="flex justify-between">
|
|
851
|
+
<dt style="color: var(--muted-foreground);">Kodeki</dt>
|
|
852
|
+
<dd class="flex gap-1.5 font-medium">
|
|
853
|
+
<span
|
|
854
|
+
class="rounded px-1.5 py-0.5 text-xs"
|
|
855
|
+
style="background: {si.ffmpeg.codecs.h264 ? 'var(--success, #3A8A5C)' : 'var(--error, #C44B4B)'}; color: white;"
|
|
856
|
+
>h264</span>
|
|
857
|
+
<span
|
|
858
|
+
class="rounded px-1.5 py-0.5 text-xs"
|
|
859
|
+
style="background: {si.ffmpeg.codecs.vp9 ? 'var(--success, #3A8A5C)' : 'var(--error, #C44B4B)'}; color: white;"
|
|
860
|
+
>vp9</span>
|
|
861
|
+
</dd>
|
|
862
|
+
</div>
|
|
863
|
+
{/if}
|
|
864
|
+
<div class="flex justify-between">
|
|
865
|
+
<dt style="color: var(--muted-foreground);">sharp</dt>
|
|
866
|
+
<dd class="font-medium">
|
|
867
|
+
{#if si.sharp.available}
|
|
868
|
+
{si.sharp.version}
|
|
869
|
+
{:else}
|
|
870
|
+
<span style="color: var(--error, #C44B4B);">niedostępny</span>
|
|
871
|
+
{/if}
|
|
872
|
+
</dd>
|
|
873
|
+
</div>
|
|
874
|
+
<div class="flex justify-between">
|
|
875
|
+
<dt style="color: var(--muted-foreground);">OS</dt>
|
|
876
|
+
<dd class="font-medium">{si.os.platform} {si.os.release}</dd>
|
|
877
|
+
</div>
|
|
878
|
+
<div class="flex justify-between">
|
|
879
|
+
<dt style="color: var(--muted-foreground);">CPU / RAM</dt>
|
|
880
|
+
<dd class="font-medium">{si.os.cpus} rdzeni / {formatBytes(si.os.totalMemory)}</dd>
|
|
881
|
+
</div>
|
|
882
|
+
<div class="flex justify-between">
|
|
883
|
+
<dt style="color: var(--muted-foreground);">Katalog uploads</dt>
|
|
884
|
+
<dd class="max-w-[200px] truncate font-medium" title={si.uploads.path}>{si.uploads.path}</dd>
|
|
885
|
+
</div>
|
|
886
|
+
</dl>
|
|
887
|
+
</Card.Content>
|
|
888
|
+
</Card.Root>
|
|
889
|
+
{/if}
|
|
492
890
|
</div>
|
|
493
891
|
|
|
494
892
|
<!-- Refresh button -->
|
|
@@ -135,7 +135,26 @@ function renderNode(node, options) {
|
|
|
135
135
|
videoAttrs += ` width="${width}"`;
|
|
136
136
|
if (height)
|
|
137
137
|
videoAttrs += ` height="${height}"`;
|
|
138
|
-
|
|
138
|
+
// Build source elements: transcoded styles first, then original
|
|
139
|
+
let sources = '';
|
|
140
|
+
const mediaData = attrs._media;
|
|
141
|
+
if (mediaData?.videoStyles) {
|
|
142
|
+
const styles = Object.values(mediaData.videoStyles)
|
|
143
|
+
.filter((s) => s.status === 'done' && s.url);
|
|
144
|
+
// webm first, then mp4
|
|
145
|
+
styles.sort((a, b) => {
|
|
146
|
+
if (a.mimeType === 'video/webm' && b.mimeType !== 'video/webm')
|
|
147
|
+
return -1;
|
|
148
|
+
if (a.mimeType !== 'video/webm' && b.mimeType === 'video/webm')
|
|
149
|
+
return 1;
|
|
150
|
+
return 0;
|
|
151
|
+
});
|
|
152
|
+
for (const style of styles) {
|
|
153
|
+
sources += `<source src="${escapeHtml(resolveUrl(style.url, options.baseUrl))}" type="${escapeHtml(style.mimeType)}">`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
sources += `<source src="${escapeHtml(src)}">`;
|
|
157
|
+
return `<video${videoAttrs}>${sources}</video>`;
|
|
139
158
|
}
|
|
140
159
|
case 'table':
|
|
141
160
|
return `<table>${children}</table>`;
|