feat(author): Extract and display chapters from VIDEO_TS folders

Added chapter extraction from VIDEO_TS folders when importing:
- New extractChaptersFromVideoTS() function to parse chapters from VOB files
- Automatically loads chapters when VIDEO_TS folder is dropped
- Displays chapters in Chapters tab with "VIDEO_TS Chapters" source label
- Uses ffprobe to extract chapter info from main title VOB file

Chapters are now fully imported and visible in the UI when loading
VIDEO_TS folders, preserving the original DVD chapter structure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stu Leak 2025-12-30 21:54:30 -05:00
parent 75a8b900e8
commit cc16352098
2 changed files with 48 additions and 0 deletions

View File

@ -406,6 +406,8 @@ func buildChaptersTab(state *appState) fyne.CanvasObject {
sourceLabel.SetText("Source: Embedded chapters")
case "scenes":
sourceLabel.SetText("Source: Scene detection")
case "videots":
sourceLabel.SetText("Source: VIDEO_TS chapters")
default:
sourceLabel.SetText("Source: Chapters")
}
@ -981,6 +983,8 @@ func (s *appState) authorChapterSummary() (int, string) {
return len(s.authorChapters), "Embedded Chapters"
case "scenes":
return len(s.authorChapters), "Scene Chapters"
case "videots":
return len(s.authorChapters), "VIDEO_TS Chapters"
default:
return len(s.authorChapters), "Chapters"
}
@ -1076,6 +1080,28 @@ func (s *appState) loadEmbeddedChapters(path string) {
}
}
func (s *appState) loadVideoTSChapters(videoTSPath string) {
chapters, err := extractChaptersFromVideoTS(videoTSPath)
if err != nil || len(chapters) == 0 {
// No chapters found, clear if previously set
if s.authorChapterSource == "videots" {
s.authorChapters = nil
s.authorChapterSource = ""
s.updateAuthorSummary()
if s.authorChaptersRefresh != nil {
s.authorChaptersRefresh()
}
}
return
}
s.authorChapters = chapters
s.authorChapterSource = "videots"
s.updateAuthorSummary()
if s.authorChaptersRefresh != nil {
s.authorChaptersRefresh()
}
}
func chaptersFromClips(clips []authorClip) []authorChapter {
if len(clips) == 0 {
return nil
@ -1222,6 +1248,27 @@ func extractChaptersFromFile(path string) ([]authorChapter, error) {
return chapters, nil
}
func extractChaptersFromVideoTS(videoTSPath string) ([]authorChapter, error) {
// Try to find the main title VOB files
// Usually VTS_01_1.VOB contains the main content
vobFiles, err := filepath.Glob(filepath.Join(videoTSPath, "VTS_*_1.VOB"))
if err != nil || len(vobFiles) == 0 {
return nil, fmt.Errorf("no VOB files found in VIDEO_TS")
}
// Sort to get the first title set (usually the main feature)
sort.Strings(vobFiles)
mainVOB := vobFiles[0]
// Try to extract chapters from the main VOB using ffprobe
chapters, err := extractChaptersFromFile(mainVOB)
if err != nil {
return nil, err
}
return chapters, nil
}
func chaptersToDVDAuthor(chapters []authorChapter) string {
if len(chapters) == 0 {
return ""

View File

@ -10483,6 +10483,7 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
s.authorClips = nil
s.authorFile = nil
s.authorOutputType = "iso"
s.loadVideoTSChapters(videoTSPath)
s.showAuthorView()
return
}