feat: add multilingual subtitle tracks

This commit is contained in:
Nice Try
2026-06-26 14:34:10 +00:00
parent 7ce7941712
commit 22871f6679
13 changed files with 486 additions and 89 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ const liveGroups: FeatureGroup[] = [
items: [
'Bookmarks with timestamp notes',
'Jump-to-time bookmark playback',
'Subtitle support for .srt and .vtt',
'Multi-language subtitle tracks for .vtt/.srt files',
'Inline document viewing for lessons',
],
},
+4 -2
View File
@@ -182,7 +182,8 @@ export default function ScanPage() {
Course Name 1/
01 - Introduction/
01 - Introduction.mp4
01 - Introduction.srt
01 - Introduction.en.vtt
01 - Introduction.es.vtt
02 - Overview.pdf
03 - Notes.txt
02 - Advanced/
@@ -204,7 +205,8 @@ export default function ScanPage() {
</pre>
<div className="mt-3 space-y-2 text-sm text-muted-foreground">
<p><strong>How the scanner reads it:</strong> course folder module folder lesson files.</p>
<p><strong>Matching rule:</strong> subtitle files should share the same base name as the video, like <code className="bg-background px-1.5 py-0.5 rounded">lesson.mp4</code> + <code className="bg-background px-1.5 py-0.5 rounded">lesson.srt</code>.</p>
<p><strong>Matching rule:</strong> subtitle files can either share the exact video base name, like <code className="bg-background px-1.5 py-0.5 rounded">lesson.mp4</code> + <code className="bg-background px-1.5 py-0.5 rounded">lesson.vtt</code>, or use language suffixes for multiple tracks, like <code className="bg-background px-1.5 py-0.5 rounded">lesson.en.vtt</code> + <code className="bg-background px-1.5 py-0.5 rounded">lesson.es.vtt</code>.</p>
<p><strong>Subtitle formats:</strong> <code className="bg-background px-1.5 py-0.5 rounded">.vtt</code> is preferred for browser playback; <code className="bg-background px-1.5 py-0.5 rounded">.srt</code> files are detected and listed as best-effort tracks.</p>
<p><strong>Sorting:</strong> numeric prefixes like <code className="bg-background px-1.5 py-0.5 rounded">01</code>, <code className="bg-background px-1.5 py-0.5 rounded">02</code>, <code className="bg-background px-1.5 py-0.5 rounded">10</code> keep lessons in learning order.</p>
</div>
</CardContent>
+27 -2
View File
@@ -21,6 +21,15 @@ import Link from 'next/link'
import { formatDuration, formatTime, cn } from '@/lib/utils'
import { getCourseDisplayName } from '@/lib/course-display'
interface SubtitleTrackType {
id?: string
src: string
lang: string
label: string
format?: string
isDefault: boolean
}
interface LessonType {
id: string
title: string
@@ -33,6 +42,7 @@ interface LessonType {
filePath: string
thumbnail: string | null
subtitlePath: string | null
subtitles?: SubtitleTrackType[]
progress?: { completed: boolean; position: number; lastWatched: string } | null
quiz?: {
version: 1
@@ -118,13 +128,28 @@ function markCompleteFn(lessonId: string, courseId: string, moduleId: string, po
}
function VideoContent({ lesson, lessonProgress, onSave, onEnd, onTimeUpdate, seekRequest }: {
lesson: { id: string; filePath: string; thumbnail: string | null; duration: number | null; subtitlePath: string | null }
lesson: { id: string; filePath: string; thumbnail: string | null; duration: number | null; subtitlePath: string | null; subtitles?: SubtitleTrackType[] }
lessonProgress: { completed: boolean; position: number; lastWatched: string }
onSave: (position: number) => void
onEnd: () => void
onTimeUpdate: (time: number) => void
seekRequest: { time: number; nonce: number } | null
}) {
const subtitleTracks = React.useMemo(() => {
if (lesson.subtitles && lesson.subtitles.length > 0) {
return lesson.subtitles.map(track => ({
src: '/api/files/' + track.src,
srcLang: track.lang,
label: track.label,
default: track.isDefault,
}))
}
return lesson.subtitlePath
? [{ src: '/api/files/' + lesson.subtitlePath, srcLang: 'en', label: 'English', default: true }]
: []
}, [lesson.subtitlePath, lesson.subtitles])
return (
<VideoPlayer
src={'/api/files/' + lesson.filePath}
@@ -135,7 +160,7 @@ function VideoContent({ lesson, lessonProgress, onSave, onEnd, onTimeUpdate, see
onProgressSave={onSave}
onEnded={onEnd}
seekRequest={seekRequest}
subtitles={lesson.subtitlePath ? '/api/files/' + lesson.subtitlePath : undefined}
subtitles={subtitleTracks}
/>
)
}
+6
View File
@@ -22,6 +22,12 @@ export default async function WatchPage({ params, searchParams }: WatchPageProps
progress: {
where: { userId: 'local-user' },
},
subtitles: {
orderBy: [
{ isDefault: 'desc' },
{ label: 'asc' },
],
},
},
})