Skip to content

Commit 42dd840

Browse files
committed
Bump version to 0.9.9
1 parent 7fe0bbd commit 42dd840

6 files changed

Lines changed: 62 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.9.9] - 2026-03-26
11+
12+
### Fixed
13+
- Video downloads now preserve the original file format (e.g. .mov files no longer get served as .mp4).
14+
- Correct Content-Type headers for all video formats (MP4, MOV, AVI, WebM, MKV) on both the share page and project page.
15+
16+
### Changed
17+
- Increased maximum upload size limit from 100 GB to 1000 GB.
18+
- Improved Docker entrypoint to avoid unnecessary `chown` on node_modules during user setup.
19+
20+
### Documentation
21+
- Added system requirements section to the Installation wiki (CPU, RAM, disk, SSD recommendation).
22+
- Added CPU thread allocation reference to the Configuration wiki.
23+
- Updated screenshots for v0.9.8.
24+
25+
### Dependencies
26+
- Updated Next.js, BullMQ, ioredis, SimpleWebAuthn, file-type, isomorphic-dompurify, and eslint-config-next to latest minor versions.
27+
1028
## [0.9.8] - 2026-03-22
1129

1230
### Security

docs/wiki/Home.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ViTransfer Wiki
22

3-
Welcome to the ViTransfer documentation wiki. This content reflects the application capabilities and behavior as of version **0.9.8**.
3+
Welcome to the ViTransfer documentation wiki. This content reflects the application capabilities and behavior as of version **0.9.9**.
44

55
The canonical wiki lives on GitHub: https://github.com/MansiVisuals/ViTransfer/wiki
66
This folder (`docs/wiki`) is a mirror of those pages for easy versioning in the repo.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vitransfer",
3-
"version": "0.9.8",
3+
"version": "0.9.9",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",

src/app/api/content/[token]/route.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { verifyVideoAccessToken, detectHotlinking, trackVideoAccess, logSecurity
33
import { getRedis } from '@/lib/redis'
44
import { prisma } from '@/lib/db'
55
import { createReadStream, existsSync, statSync, ReadStream } from 'fs'
6-
import { getFilePath, sanitizeFilenameForHeader } from '@/lib/storage'
6+
import { getFilePath, sanitizeFilenameForHeader, getVideoContentType } from '@/lib/storage'
77
import { rateLimit } from '@/lib/rate-limit'
88
import { getClientIpAddress } from '@/lib/utils'
99
import { getAuthContext } from '@/lib/auth'
@@ -202,7 +202,7 @@ export async function GET(
202202
const originalPath = video.originalStoragePath
203203
let filePath: string | null = null
204204
let filename: string | null = null
205-
let contentType = 'video/mp4'
205+
let contentType = getVideoContentType(video.originalFileName || '')
206206

207207
// Handle asset download
208208
if (assetId && isDownload) {
@@ -278,12 +278,12 @@ export async function GET(
278278
// Use asset filename if available, otherwise generate from video info
279279
const rawFilename = filename || (video.approved
280280
? video.originalFileName
281-
: `${video.project.title.replace(/[^a-z0-9]/gi, '_')}_${verifiedToken.quality}.mp4`)
281+
: `${video.project.title.replace(/[^a-z0-9]/gi, '_')}_${verifiedToken.quality}${(video.originalFileName || '.mp4').slice((video.originalFileName || '.mp4').lastIndexOf('.'))}`)
282282
const sanitizedFilename = sanitizeFilenameForHeader(rawFilename)
283283

284-
// For non-asset streams, determine Content-Type based on quality
284+
// For non-asset downloads, use original file's content type
285285
if (!assetId) {
286-
contentType = isThumbnail ? 'image/jpeg' : 'video/mp4'
286+
contentType = isThumbnail ? 'image/jpeg' : getVideoContentType(video.originalFileName || '')
287287
}
288288

289289
const trackDownloadOnce = async () => {
@@ -371,9 +371,15 @@ export async function GET(
371371
const fileStream = createReadStream(fullPath, { start, end, highWaterMark: STREAM_HIGH_WATER_MARK_BYTES })
372372
const readableStream = createWebReadableStream(fileStream)
373373

374-
// For non-asset streams, determine Content-Type based on quality
374+
// For non-asset streams, determine Content-Type
375375
if (!assetId) {
376-
contentType = isThumbnail ? 'image/jpeg' : 'video/mp4'
376+
if (isThumbnail) {
377+
contentType = 'image/jpeg'
378+
} else if (filePath === originalPath) {
379+
contentType = getVideoContentType(video.originalFileName || '')
380+
} else {
381+
contentType = 'video/mp4' // transcoded previews are always mp4
382+
}
377383
}
378384

379385
return new NextResponse(readableStream, {
@@ -395,9 +401,15 @@ export async function GET(
395401
const fileStream = createReadStream(fullPath, { highWaterMark: STREAM_HIGH_WATER_MARK_BYTES })
396402
const readableStream = createWebReadableStream(fileStream)
397403

398-
// For non-asset streams, determine Content-Type based on quality
404+
// For non-asset streams, determine Content-Type
399405
if (!assetId) {
400-
contentType = isThumbnail ? 'image/jpeg' : 'video/mp4'
406+
if (isThumbnail) {
407+
contentType = 'image/jpeg'
408+
} else if (filePath === originalPath) {
409+
contentType = getVideoContentType(video.originalFileName || '')
410+
} else {
411+
contentType = 'video/mp4' // transcoded previews are always mp4
412+
}
401413
}
402414

403415
return new NextResponse(readableStream, {

src/app/api/videos/[id]/download/route.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server'
22
import { prisma } from '@/lib/db'
3-
import { getFilePath, sanitizeFilenameForHeader } from '@/lib/storage'
3+
import { getFilePath, sanitizeFilenameForHeader, getVideoContentType } from '@/lib/storage'
44
import { verifyProjectAccess } from '@/lib/project-access'
55
import { rateLimit } from '@/lib/rate-limit'
66
import { getConfiguredLocale, loadLocaleMessages } from '@/i18n/locale'
@@ -89,6 +89,7 @@ export async function GET(
8989
// Use the original filename from the database, guard against missing values
9090
const originalFilename = video.originalFileName || 'video.mp4'
9191
const safeFilename = sanitizeFilenameForHeader(originalFilename)
92+
const contentType = getVideoContentType(originalFilename)
9293

9394
const range = request.headers.get('range')
9495

@@ -119,7 +120,7 @@ export async function GET(
119120
return new NextResponse(readableStream, {
120121
status: 206,
121122
headers: {
122-
'Content-Type': 'video/mp4',
123+
'Content-Type': contentType,
123124
'Content-Disposition': `attachment; filename="${safeFilename}"`,
124125
'Content-Length': chunkSize.toString(),
125126
'Content-Range': `bytes ${start}-${end}/${stat.size}`,
@@ -147,7 +148,7 @@ export async function GET(
147148
// Return file with proper headers for download
148149
return new NextResponse(readableStream, {
149150
headers: {
150-
'Content-Type': 'video/mp4',
151+
'Content-Type': contentType,
151152
'Content-Disposition': `attachment; filename="${safeFilename}"`,
152153
'Content-Length': stat.size.toString(),
153154
'Accept-Ranges': 'bytes',

src/lib/storage.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,23 @@ export function getFilePath(filePath: string): string {
119119
return validatePath(filePath)
120120
}
121121

122+
/**
123+
* Derive video Content-Type from filename extension
124+
*/
125+
const VIDEO_MIME_MAP: Record<string, string> = {
126+
'.mp4': 'video/mp4',
127+
'.mov': 'video/quicktime',
128+
'.avi': 'video/x-msvideo',
129+
'.webm': 'video/webm',
130+
'.mkv': 'video/x-matroska',
131+
}
132+
133+
export function getVideoContentType(filename: string): string {
134+
if (!filename) return 'video/mp4'
135+
const ext = filename.toLowerCase().slice(filename.lastIndexOf('.'))
136+
return VIDEO_MIME_MAP[ext] || 'video/mp4'
137+
}
138+
122139
/**
123140
* Sanitize filename for Content-Disposition header
124141
* Prevents CRLF injection and other header injection attacks

0 commit comments

Comments
 (0)