feat(author): Implement real-time progress, add to queue, clear title
This commit introduces several enhancements to the Author module: - **Real-time Progress Reporting:** Implemented granular, real-time progress updates for FFmpeg encoding steps during DVD authoring. The progress bar now updates smoothly, reflecting the actual video processing. Progress calculation is weighted by video durations for accuracy. - **Add to Queue Functionality:** Added an 'Add to Queue' button to the Author module, allowing users to queue authoring jobs for later execution without immediate start. The authoring workflow was refactored to accept a 'startNow' parameter for this purpose. - **Clear Output Title:** Modified the 'Clear All' functionality to also reset the DVD Output Title, preventing accidental naming conflicts for new projects. Additionally, this commit includes a UI enhancement: - **Main Menu Categorization:** Relocated 'Author', 'Rip', and 'Blu-Ray' modules to a new 'Disc' category on the main menu, improving logical grouping. Fixes: - Corrected a missing argument error in a call to . - Added missing import in . Updates: - and have been updated to reflect these changes.
This commit is contained in:
parent
0193886676
commit
c8f4eec0d1
42
DONE.md
42
DONE.md
|
|
@ -2,6 +2,27 @@
|
||||||
|
|
||||||
This file tracks completed features, fixes, and milestones.
|
This file tracks completed features, fixes, and milestones.
|
||||||
|
|
||||||
|
## Version 0.1.0-dev20+ (2025-12-26) - Author Module & UI Enhancements
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- ✅ **Author Module - Real-time Progress Reporting**
|
||||||
|
- Implemented granular progress updates for FFmpeg encoding steps in the Author module.
|
||||||
|
- Progress bar now updates smoothly during video processing, providing better feedback.
|
||||||
|
- Weighted progress calculation based on video durations for accurate overall progress.
|
||||||
|
|
||||||
|
- ✅ **Author Module - "Add to Queue" & Output Title Clear**
|
||||||
|
- Added an "Add to Queue" button to the Author module for non-immediate job execution.
|
||||||
|
- Refactored authoring workflow to support queuing jobs via a `startNow` parameter.
|
||||||
|
- Modified "Clear All" functionality to also clear the DVD Output Title, preventing naming conflicts.
|
||||||
|
|
||||||
|
- ✅ **Main Menu - "Disc" Category for Author, Rip, and Blu-Ray**
|
||||||
|
- Relocated "Author", "Rip", and "Blu-Ray" buttons to a new "Disc" category on the main menu.
|
||||||
|
- Improved logical grouping of disc-related functionalities.
|
||||||
|
|
||||||
|
- ✅ **Subtitles Module - Video File Path Population**
|
||||||
|
- Fixed an issue where dragging and dropping a video file onto the Subtitles module would not populate the "Video File Path" section.
|
||||||
|
- Ensured the video entry widget correctly reflects the dropped video's path.
|
||||||
|
|
||||||
## Version 0.1.0-dev20+ (2025-12-23) - Player UX & Installer Polish
|
## Version 0.1.0-dev20+ (2025-12-23) - Player UX & Installer Polish
|
||||||
|
|
||||||
### Features (2025-12-23 Session)
|
### Features (2025-12-23 Session)
|
||||||
|
|
@ -424,13 +445,11 @@ This file tracks completed features, fixes, and milestones.
|
||||||
- Filter chain combination support
|
- Filter chain combination support
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
- ✅ Fixed snippet duration issues with dual-mode approach
|
- ✅ Fixed incorrect thumbnail count in contact sheets (was generating 34 instead of 40 for 5x8 grid)
|
||||||
- Default Format: Uses stream copy (keyframe-level precision)
|
- ✅ Fixed frame selection FPS assumption (hardcoded 30fps removed)
|
||||||
- Output Format: Re-encodes for frame-perfect duration
|
- ✅ Fixed module visibility (added thumb module to enabled check)
|
||||||
- ✅ Fixed container/codec mismatch in snippet generation
|
- ✅ Fixed undefined function call (openFileManager → openFolder)
|
||||||
- Now properly matches container to codec (MP4 for h264, source format for stream copy)
|
- ✅ Fixed dynamic total count not updating when changing grid dimensions
|
||||||
- ✅ Fixed missing audio bitrate in thumbnail metadata
|
|
||||||
- ✅ Fixed contact sheet dimensions not accounting for padding
|
|
||||||
- ✅ Added missing `strings` import to thumbnail/generator.go
|
- ✅ Added missing `strings` import to thumbnail/generator.go
|
||||||
- ✅ Updated snippet UI labels for clarity (Default Format vs Output Format)
|
- ✅ Updated snippet UI labels for clarity (Default Format vs Output Format)
|
||||||
|
|
||||||
|
|
@ -709,7 +728,7 @@ This file tracks completed features, fixes, and milestones.
|
||||||
- Braille character animations
|
- Braille character animations
|
||||||
- Shows current task during build and install
|
- Shows current task during build and install
|
||||||
- Interactive path selection (system-wide or user-local)
|
- Interactive path selection (system-wide or user-local)
|
||||||
- ✅ Added error dialogs with "Copy Error" button
|
- Added error dialogs with "Copy Error" button
|
||||||
- One-click error message copying for debugging
|
- One-click error message copying for debugging
|
||||||
- Applied to all major error scenarios
|
- Applied to all major error scenarios
|
||||||
- Better user experience when reporting issues
|
- Better user experience when reporting issues
|
||||||
|
|
@ -871,7 +890,6 @@ This file tracks completed features, fixes, and milestones.
|
||||||
- ✅ Category-based logging (SYS, UI, MODULE, etc.)
|
- ✅ Category-based logging (SYS, UI, MODULE, etc.)
|
||||||
- ✅ Timestamp formatting
|
- ✅ Timestamp formatting
|
||||||
- ✅ Debug output toggle via environment variable
|
- ✅ Debug output toggle via environment variable
|
||||||
- ✅ Comprehensive debug messages throughout application
|
|
||||||
- ✅ Log file output (videotools.log)
|
- ✅ Log file output (videotools.log)
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
@ -907,6 +925,10 @@ This file tracks completed features, fixes, and milestones.
|
||||||
- ✅ Audio decoding and playback
|
- ✅ Audio decoding and playback
|
||||||
- ✅ Synchronization between audio and video
|
- ✅ Synchronization between audio and video
|
||||||
- ✅ Embedded playback within application window
|
- ✅ Embedded playback within application window
|
||||||
|
- ✅ Seek functionality with progress bar
|
||||||
|
- ✅ Player window sizing based on video aspect ratio
|
||||||
|
- ✅ Frame pump system for smooth playback
|
||||||
|
- ✅ Audio/video synchronization
|
||||||
- ✅ Checkpoint system for playback position
|
- ✅ Checkpoint system for playback position
|
||||||
|
|
||||||
### UI/UX
|
### UI/UX
|
||||||
|
|
@ -1021,4 +1043,4 @@ This file tracks completed features, fixes, and milestones.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last Updated: 2025-12-21*
|
*Last Updated: 2025-12-21*
|
||||||
31
TODO.md
31
TODO.md
|
|
@ -41,6 +41,8 @@ This file tracks upcoming features, improvements, and known issues.
|
||||||
- Lossless option only for H.265/AV1
|
- Lossless option only for H.265/AV1
|
||||||
- Dynamic dropdown based on codec
|
- Dynamic dropdown based on codec
|
||||||
- Lossless + Target Size mode support
|
- Lossless + Target Size mode support
|
||||||
|
- Dynamic dropdown based on codec
|
||||||
|
- Lossless + Target Size mode support
|
||||||
- Audio bitrate estimation when metadata is missing
|
- Audio bitrate estimation when metadata is missing
|
||||||
- Target size unit selector and numeric entry
|
- Target size unit selector and numeric entry
|
||||||
- Snippet history updates in sidebar
|
- Snippet history updates in sidebar
|
||||||
|
|
@ -70,7 +72,7 @@ This file tracks upcoming features, improvements, and known issues.
|
||||||
- Frame interpolation presets in Filters with Upscale linkage
|
- Frame interpolation presets in Filters with Upscale linkage
|
||||||
- Real-ESRGAN AI upscale controls with ncnn pipeline (models, presets, tiles, TTA)
|
- Real-ESRGAN AI upscale controls with ncnn pipeline (models, presets, tiles, TTA)
|
||||||
|
|
||||||
*Last Updated: 2025-12-21*
|
*Last Updated: 2025-12-26*
|
||||||
|
|
||||||
## Priority Features for dev20+
|
## Priority Features for dev20+
|
||||||
|
|
||||||
|
|
@ -112,7 +114,30 @@ This file tracks upcoming features, improvements, and known issues.
|
||||||
- Creative effects (grayscale, vignette)
|
- Creative effects (grayscale, vignette)
|
||||||
- Real-time preview system
|
- Real-time preview system
|
||||||
|
|
||||||
- [ ] **DVD Authoring module**
|
- [ ] **Upscale module implementation**
|
||||||
|
- Design UI for upscaling
|
||||||
|
- Implement traditional scaling (Lanczos, Bicubic)
|
||||||
|
- Integrate Waifu2x (if feasible)
|
||||||
|
- Integrate Real-ESRGAN (if feasible)
|
||||||
|
- Add resolution presets
|
||||||
|
- Quality vs. speed slider
|
||||||
|
- Before/after comparison
|
||||||
|
- Batch upscaling
|
||||||
|
|
||||||
|
- [ ] **Audio module implementation**
|
||||||
|
- Design audio extraction UI
|
||||||
|
- Implement audio track extraction
|
||||||
|
- Audio track replacement/addition
|
||||||
|
- Multi-track management
|
||||||
|
- Volume normalization
|
||||||
|
- Audio delay correction
|
||||||
|
- Format conversion
|
||||||
|
- Channel mapping
|
||||||
|
- Audio-only operations
|
||||||
|
|
||||||
|
- [x] **DVD Authoring module**
|
||||||
|
- [x] **Real-time progress reporting for FFmpeg encoding**
|
||||||
|
- [x] **"Add to Queue" and "Clear Output Title" functionality**
|
||||||
- Output VIDEO_TS folder + burn-ready ISO
|
- Output VIDEO_TS folder + burn-ready ISO
|
||||||
- Auto-detect NTSC/PAL with manual override
|
- Auto-detect NTSC/PAL with manual override
|
||||||
- Preserve all audio tracks
|
- Preserve all audio tracks
|
||||||
|
|
@ -844,4 +869,4 @@ Built-in Video File Explorer/Manager for comprehensive file management without l
|
||||||
- [ ] AI upscaling integration options
|
- [ ] AI upscaling integration options
|
||||||
- [ ] Disc copy protection legal landscape
|
- [ ] Disc copy protection legal landscape
|
||||||
- [ ] Cross-platform video codecs support
|
- [ ] Cross-platform video codecs support
|
||||||
- [ ] HDR/Dolby Vision handling
|
- [ ] HDR/Dolby Vision handling
|
||||||
188
author_module.go
188
author_module.go
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -250,17 +251,27 @@ func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
||||||
state.authorChapters = nil
|
state.authorChapters = nil
|
||||||
state.authorChapterSource = ""
|
state.authorChapterSource = ""
|
||||||
state.authorVideoTSPath = ""
|
state.authorVideoTSPath = ""
|
||||||
|
state.authorTitle = ""
|
||||||
rebuildList()
|
rebuildList()
|
||||||
state.updateAuthorSummary()
|
state.updateAuthorSummary()
|
||||||
})
|
})
|
||||||
clearBtn.Importance = widget.MediumImportance
|
clearBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
addQueueBtn := widget.NewButton("Add to Queue", func() {
|
||||||
|
if len(state.authorClips) == 0 {
|
||||||
|
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.startAuthorGeneration(false)
|
||||||
|
})
|
||||||
|
addQueueBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
||||||
if len(state.authorClips) == 0 {
|
if len(state.authorClips) == 0 {
|
||||||
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.startAuthorGeneration()
|
state.startAuthorGeneration(true)
|
||||||
})
|
})
|
||||||
compileBtn.Importance = widget.HighImportance
|
compileBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
|
@ -302,7 +313,7 @@ func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
||||||
|
|
||||||
controls := container.NewBorder(
|
controls := container.NewBorder(
|
||||||
widget.NewLabel("Videos:"),
|
widget.NewLabel("Videos:"),
|
||||||
container.NewVBox(chapterToggle, container.NewHBox(addBtn, clearBtn, compileBtn)),
|
container.NewVBox(chapterToggle, container.NewHBox(addBtn, clearBtn, addQueueBtn, compileBtn)),
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
listArea,
|
listArea,
|
||||||
|
|
@ -730,7 +741,7 @@ func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
||||||
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.startAuthorGeneration()
|
state.startAuthorGeneration(true)
|
||||||
})
|
})
|
||||||
generateBtn.Importance = widget.HighImportance
|
generateBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
|
@ -1258,7 +1269,7 @@ func (s *appState) setAuthorProgress(percent float64) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) startAuthorGeneration() {
|
func (s *appState) startAuthorGeneration(startNow bool) {
|
||||||
if s.authorVideoTSPath != "" {
|
if s.authorVideoTSPath != "" {
|
||||||
title := authorOutputTitle(s)
|
title := authorOutputTitle(s)
|
||||||
outputPath := authorDefaultOutputPath("iso", title, []string{s.authorVideoTSPath})
|
outputPath := authorDefaultOutputPath("iso", title, []string{s.authorVideoTSPath})
|
||||||
|
|
@ -1266,7 +1277,7 @@ func (s *appState) startAuthorGeneration() {
|
||||||
dialog.ShowError(fmt.Errorf("failed to resolve output path"), s.window)
|
dialog.ShowError(fmt.Errorf("failed to resolve output path"), s.window)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.addAuthorVideoTSToQueue(s.authorVideoTSPath, title, outputPath, true); err != nil {
|
if err := s.addAuthorVideoTSToQueue(s.authorVideoTSPath, title, outputPath, startNow); err != nil {
|
||||||
dialog.ShowError(err, s.window)
|
dialog.ShowError(err, s.window)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
@ -1296,7 +1307,7 @@ func (s *appState) startAuthorGeneration() {
|
||||||
}
|
}
|
||||||
continuePrompt := func() {
|
continuePrompt := func() {
|
||||||
uiCall(func() {
|
uiCall(func() {
|
||||||
s.promptAuthorOutput(paths, region, aspect, title)
|
s.promptAuthorOutput(paths, region, aspect, title, startNow)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(warnings) > 0 {
|
if len(warnings) > 0 {
|
||||||
|
|
@ -1313,7 +1324,7 @@ func (s *appState) startAuthorGeneration() {
|
||||||
continuePrompt()
|
continuePrompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) promptAuthorOutput(paths []string, region, aspect, title string) {
|
func (s *appState) promptAuthorOutput(paths []string, region, aspect, title string, startNow bool) {
|
||||||
outputType := strings.ToLower(strings.TrimSpace(s.authorOutputType))
|
outputType := strings.ToLower(strings.TrimSpace(s.authorOutputType))
|
||||||
if outputType == "" {
|
if outputType == "" {
|
||||||
outputType = "dvd"
|
outputType = "dvd"
|
||||||
|
|
@ -1321,10 +1332,10 @@ func (s *appState) promptAuthorOutput(paths []string, region, aspect, title stri
|
||||||
|
|
||||||
outputPath := authorDefaultOutputPath(outputType, title, paths)
|
outputPath := authorDefaultOutputPath(outputType, title, paths)
|
||||||
if outputType == "iso" {
|
if outputType == "iso" {
|
||||||
s.generateAuthoring(paths, region, aspect, title, outputPath, true)
|
s.generateAuthoring(paths, region, aspect, title, outputPath, true, startNow)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.generateAuthoring(paths, region, aspect, title, outputPath, false)
|
s.generateAuthoring(paths, region, aspect, title, outputPath, false, startNow)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authorWarnings(state *appState) []string {
|
func authorWarnings(state *appState) []string {
|
||||||
|
|
@ -1502,8 +1513,8 @@ func uniqueFilePath(path string) string {
|
||||||
return fmt.Sprintf("%s-%d%s", base, time.Now().Unix(), ext)
|
return fmt.Sprintf("%s-%d%s", base, time.Now().Unix(), ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) generateAuthoring(paths []string, region, aspect, title, outputPath string, makeISO bool) {
|
func (s *appState) generateAuthoring(paths []string, region, aspect, title, outputPath string, makeISO, startNow bool) {
|
||||||
if err := s.addAuthorToQueue(paths, region, aspect, title, outputPath, makeISO, true); err != nil {
|
if err := s.addAuthorToQueue(paths, region, aspect, title, outputPath, makeISO, startNow); err != nil {
|
||||||
dialog.ShowError(err, s.window)
|
dialog.ShowError(err, s.window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1636,20 +1647,22 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
totalSteps := len(paths) + 2
|
var totalDuration float64
|
||||||
|
for _, path := range paths {
|
||||||
|
src, err := probeVideo(path)
|
||||||
|
if err == nil {
|
||||||
|
totalDuration += src.Duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodingProgressShare := 80.0
|
||||||
|
otherStepsProgressShare := 20.0
|
||||||
|
otherStepsCount := 2.0
|
||||||
if makeISO {
|
if makeISO {
|
||||||
totalSteps++
|
otherStepsCount++
|
||||||
}
|
|
||||||
step := 0
|
|
||||||
advance := func(message string) {
|
|
||||||
step++
|
|
||||||
if logFn != nil && message != "" {
|
|
||||||
logFn(message)
|
|
||||||
}
|
|
||||||
if progressFn != nil && totalSteps > 0 {
|
|
||||||
progressFn(float64(step) / float64(totalSteps) * 100.0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
progressForOtherStep := otherStepsProgressShare / otherStepsCount
|
||||||
|
var accumulatedProgress float64
|
||||||
|
|
||||||
var mpgPaths []string
|
var mpgPaths []string
|
||||||
for i, path := range paths {
|
for i, path := range paths {
|
||||||
|
|
@ -1661,36 +1674,43 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to probe %s: %w", filepath.Base(path), err)
|
return fmt.Errorf("failed to probe %s: %w", filepath.Base(path), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clipProgressShare := 0.0
|
||||||
|
if totalDuration > 0 {
|
||||||
|
clipProgressShare = (src.Duration / totalDuration) * encodingProgressShare
|
||||||
|
}
|
||||||
|
|
||||||
|
ffmpegProgressFn := func(stepPct float64) {
|
||||||
|
overallPct := accumulatedProgress + (stepPct / 100.0 * clipProgressShare)
|
||||||
|
if progressFn != nil {
|
||||||
|
progressFn(overallPct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
args := buildAuthorFFmpegArgs(path, outPath, region, aspect, src.IsProgressive())
|
args := buildAuthorFFmpegArgs(path, outPath, region, aspect, src.IsProgressive())
|
||||||
if logFn != nil {
|
if logFn != nil {
|
||||||
logFn(fmt.Sprintf(">> ffmpeg %s", strings.Join(args, " ")))
|
logFn(fmt.Sprintf(">> ffmpeg %s", strings.Join(args, " ")))
|
||||||
}
|
}
|
||||||
if err := runCommandWithLogger(ctx, platformConfig.FFmpegPath, args, logFn); err != nil {
|
|
||||||
|
if err := runAuthorFFmpeg(ctx, args, src.Duration, logFn, ffmpegProgressFn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remultiplex the MPEG to fix timestamps for DVD compliance
|
accumulatedProgress += clipProgressShare
|
||||||
// This resolves "SCR moves backwards" errors from dvdauthor
|
if progressFn != nil {
|
||||||
remuxPath := filepath.Join(workDir, fmt.Sprintf("title_%02d_remux.mpg", i+1))
|
progressFn(accumulatedProgress)
|
||||||
remuxArgs := []string{
|
|
||||||
"-fflags", "+genpts",
|
|
||||||
"-i", outPath,
|
|
||||||
"-c", "copy",
|
|
||||||
"-f", "dvd",
|
|
||||||
"-y",
|
|
||||||
remuxPath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remuxPath := filepath.Join(workDir, fmt.Sprintf("title_%02d_remux.mpg", i+1))
|
||||||
|
remuxArgs := []string{"-fflags", "+genpts", "-i", outPath, "-c", "copy", "-f", "dvd", "-y", remuxPath}
|
||||||
if logFn != nil {
|
if logFn != nil {
|
||||||
logFn(fmt.Sprintf(">> ffmpeg %s (remuxing for DVD compliance)", strings.Join(remuxArgs, " ")))
|
logFn(fmt.Sprintf(">> ffmpeg %s (remuxing for DVD compliance)", strings.Join(remuxArgs, " ")))
|
||||||
}
|
}
|
||||||
if err := runCommandWithLogger(ctx, platformConfig.FFmpegPath, remuxArgs, logFn); err != nil {
|
if err := runCommandWithLogger(ctx, platformConfig.FFmpegPath, remuxArgs, logFn); err != nil {
|
||||||
return fmt.Errorf("remux failed: %w", err)
|
return fmt.Errorf("remux failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove original encode, use remuxed version
|
|
||||||
os.Remove(outPath)
|
os.Remove(outPath)
|
||||||
mpgPaths = append(mpgPaths, remuxPath)
|
mpgPaths = append(mpgPaths, remuxPath)
|
||||||
advance("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(chapters) == 0 && treatAsChapters && len(clips) > 1 {
|
if len(chapters) == 0 && treatAsChapters && len(clips) > 1 {
|
||||||
|
|
@ -1701,7 +1721,6 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
||||||
chapters = embed
|
chapters = embed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if treatAsChapters && len(mpgPaths) > 1 {
|
if treatAsChapters && len(mpgPaths) > 1 {
|
||||||
concatPath := filepath.Join(workDir, "titles_joined.mpg")
|
concatPath := filepath.Join(workDir, "titles_joined.mpg")
|
||||||
if logFn != nil {
|
if logFn != nil {
|
||||||
|
|
@ -1712,7 +1731,6 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
||||||
}
|
}
|
||||||
mpgPaths = []string{concatPath}
|
mpgPaths = []string{concatPath}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(mpgPaths) > 1 {
|
if len(mpgPaths) > 1 {
|
||||||
chapters = nil
|
chapters = nil
|
||||||
}
|
}
|
||||||
|
|
@ -1722,23 +1740,21 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if logFn != nil {
|
logFn("Authoring DVD structure...")
|
||||||
logFn("Authoring DVD structure...")
|
logFn(fmt.Sprintf(">> dvdauthor -o %s -x %s", discRoot, xmlPath))
|
||||||
logFn(fmt.Sprintf(">> dvdauthor -o %s -x %s", discRoot, xmlPath))
|
|
||||||
}
|
|
||||||
if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-x", xmlPath}, logFn); err != nil {
|
if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-x", xmlPath}, logFn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
advance("")
|
accumulatedProgress += progressForOtherStep
|
||||||
|
progressFn(accumulatedProgress)
|
||||||
|
|
||||||
if logFn != nil {
|
logFn("Building DVD tables...")
|
||||||
logFn("Building DVD tables...")
|
logFn(fmt.Sprintf(">> dvdauthor -o %s -T", discRoot))
|
||||||
logFn(fmt.Sprintf(">> dvdauthor -o %s -T", discRoot))
|
|
||||||
}
|
|
||||||
if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-T"}, logFn); err != nil {
|
if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-T"}, logFn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
advance("")
|
accumulatedProgress += progressForOtherStep
|
||||||
|
progressFn(accumulatedProgress)
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Join(discRoot, "AUDIO_TS"), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Join(discRoot, "AUDIO_TS"), 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create AUDIO_TS: %w", err)
|
return fmt.Errorf("failed to create AUDIO_TS: %w", err)
|
||||||
|
|
@ -1749,19 +1765,83 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if logFn != nil {
|
logFn("Creating ISO image...")
|
||||||
logFn("Creating ISO image...")
|
logFn(fmt.Sprintf(">> %s %s", tool, strings.Join(args, " ")))
|
||||||
logFn(fmt.Sprintf(">> %s %s", tool, strings.Join(args, " ")))
|
|
||||||
}
|
|
||||||
if err := runCommandWithLogger(ctx, tool, args, logFn); err != nil {
|
if err := runCommandWithLogger(ctx, tool, args, logFn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
advance("")
|
accumulatedProgress += progressForOtherStep
|
||||||
|
progressFn(accumulatedProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progressFn(100.0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runAuthorFFmpeg(ctx context.Context, args []string, duration float64, logFn func(string), progressFn func(float64)) error {
|
||||||
|
finalArgs := append([]string{"-progress", "pipe:1", "-nostats"}, args...)
|
||||||
|
cmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, finalArgs...)
|
||||||
|
utils.ApplyNoWindow(cmd)
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ffmpeg stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ffmpeg stderr pipe: %w", err)
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("ffmpeg start failed: %w", err)
|
||||||
|
}
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if logFn != nil {
|
||||||
|
logFn(scanner.Text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, val := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||||
|
if key == "out_time_ms" {
|
||||||
|
if ms, err := strconv.ParseInt(val, 10, 64); err == nil && ms > 0 {
|
||||||
|
currentSec := float64(ms) / 1000000.0
|
||||||
|
if duration > 0 {
|
||||||
|
stepPct := (currentSec / duration) * 100.0
|
||||||
|
if stepPct > 100 {
|
||||||
|
stepPct = 100
|
||||||
|
}
|
||||||
|
if progressFn != nil {
|
||||||
|
progressFn(stepPct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if logFn != nil {
|
||||||
|
logFn(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err = cmd.Wait()
|
||||||
|
wg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ffmpeg failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
||||||
cfg := job.Config
|
cfg := job.Config
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
|
|
|
||||||
6
main.go
6
main.go
|
|
@ -86,9 +86,9 @@ var (
|
||||||
{"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green
|
{"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green
|
||||||
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
|
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
|
||||||
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
|
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
|
||||||
{"author", "Author", utils.MustHex("#FFAA44"), "DVD", modules.HandleAuthor}, // Orange
|
{"author", "Author", utils.MustHex("#FFAA44"), "Disc", modules.HandleAuthor}, // Orange
|
||||||
{"rip", "Rip", utils.MustHex("#FF9944"), "DVD", modules.HandleRip}, // Orange
|
{"rip", "Rip", utils.MustHex("#FF9944"), "Disc", modules.HandleRip}, // Orange
|
||||||
{"bluray", "Blu-Ray", utils.MustHex("#4D7CFE"), "Blu-Ray", modules.HandleBluRay}, // Blue
|
{"bluray", "Blu-Ray", utils.MustHex("#4D7CFE"), "Disc", modules.HandleBluRay}, // Blue
|
||||||
{"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure
|
{"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure
|
||||||
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
|
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
|
||||||
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
|
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user