Add Rip module for DVD/ISO/VIDEO_TS
This commit is contained in:
parent
0d5670f34b
commit
faef905f18
|
|
@ -51,6 +51,12 @@ func HandleAuthor(files []string) {
|
|||
// File loading is managed in buildAuthorView()
|
||||
}
|
||||
|
||||
// HandleRip handles the rip module (placeholder)
|
||||
func HandleRip(files []string) {
|
||||
logging.Debug(logging.CatModule, "rip handler invoked with %v", files)
|
||||
fmt.Println("rip", files)
|
||||
}
|
||||
|
||||
// HandleSubtitles handles the subtitles module (placeholder)
|
||||
func HandleSubtitles(files []string) {
|
||||
logging.Debug(logging.CatModule, "subtitles handler invoked with %v", files)
|
||||
|
|
|
|||
40
main.go
40
main.go
|
|
@ -88,6 +88,7 @@ var (
|
|||
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
|
||||
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
|
||||
{"author", "Author", utils.MustHex("#FFAA44"), "Convert", modules.HandleAuthor}, // Orange
|
||||
{"rip", "Rip", utils.MustHex("#FF9944"), "Convert", modules.HandleRip}, // Orange
|
||||
{"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure
|
||||
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
|
||||
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
|
||||
|
|
@ -927,6 +928,17 @@ type appState struct {
|
|||
authorStatusLabel *widget.Label
|
||||
authorVideoTSPath string
|
||||
|
||||
// Rip module state
|
||||
ripSourcePath string
|
||||
ripOutputPath string
|
||||
ripFormat string
|
||||
ripLogText string
|
||||
ripLogEntry *widget.Entry
|
||||
ripLogScroll *container.Scroll
|
||||
ripProgress float64
|
||||
ripProgressBar *widget.ProgressBar
|
||||
ripStatusLabel *widget.Label
|
||||
|
||||
// Subtitles module state
|
||||
subtitleVideoPath string
|
||||
subtitleFilePath string
|
||||
|
|
@ -1547,7 +1559,7 @@ func (s *appState) showMainMenu() {
|
|||
Label: m.Label,
|
||||
Color: m.Color,
|
||||
Category: m.Category,
|
||||
Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect" || m.ID == "merge" || m.ID == "thumb" || m.ID == "player" || m.ID == "filters" || m.ID == "upscale" || m.ID == "author" || m.ID == "subtitles", // Enabled modules
|
||||
Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect" || m.ID == "merge" || m.ID == "thumb" || m.ID == "player" || m.ID == "filters" || m.ID == "upscale" || m.ID == "author" || m.ID == "subtitles" || m.ID == "rip", // Enabled modules
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -2285,6 +2297,8 @@ func (s *appState) showModule(id string) {
|
|||
s.showUpscaleView()
|
||||
case "author":
|
||||
s.showAuthorView()
|
||||
case "rip":
|
||||
s.showRipView()
|
||||
case "subtitles":
|
||||
s.showSubtitlesView()
|
||||
case "mainmenu":
|
||||
|
|
@ -2537,6 +2551,15 @@ func (s *appState) isSubtitleFile(path string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func firstLocalDropPath(items []fyne.URI) string {
|
||||
for _, uri := range items {
|
||||
if uri.Scheme() == "file" {
|
||||
return uri.Path()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// findVideoFiles recursively finds all video files in a directory
|
||||
func (s *appState) findVideoFiles(dir string) []string {
|
||||
var videos []string
|
||||
|
|
@ -3217,6 +3240,8 @@ func (s *appState) jobExecutor(ctx context.Context, job *queue.Job, progressCall
|
|||
return s.executeSnippetJob(ctx, job, progressCallback)
|
||||
case queue.JobTypeAuthor:
|
||||
return s.executeAuthorJob(ctx, job, progressCallback)
|
||||
case queue.JobTypeRip:
|
||||
return s.executeRipJob(ctx, job, progressCallback)
|
||||
default:
|
||||
return fmt.Errorf("unknown job type: %s", job.Type)
|
||||
}
|
||||
|
|
@ -9606,6 +9631,19 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
|||
return
|
||||
}
|
||||
|
||||
// If in rip module, accept DVD/ISO/VIDEO_TS paths
|
||||
if s.active == "rip" {
|
||||
path := firstLocalDropPath(items)
|
||||
if path == "" {
|
||||
logging.Debug(logging.CatUI, "no valid paths in dropped items")
|
||||
return
|
||||
}
|
||||
s.ripSourcePath = path
|
||||
s.ripOutputPath = defaultRipOutputPath(path, s.ripFormat)
|
||||
s.showRipView()
|
||||
return
|
||||
}
|
||||
|
||||
// If in compare module, handle up to 2 video files
|
||||
if s.active == "compare" {
|
||||
// Collect all video files from the dropped items
|
||||
|
|
|
|||
516
rip_module.go
Normal file
516
rip_module.go
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
|
||||
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
|
||||
"git.leaktechnologies.dev/stu/VideoTools/internal/ui"
|
||||
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
ripFormatLosslessMKV = "Lossless MKV (Copy)"
|
||||
ripFormatH264MKV = "H.264 MKV (CRF 18)"
|
||||
ripFormatH264MP4 = "H.264 MP4 (CRF 18)"
|
||||
)
|
||||
|
||||
func (s *appState) showRipView() {
|
||||
s.stopPreview()
|
||||
s.lastModule = s.active
|
||||
s.active = "rip"
|
||||
|
||||
if s.ripFormat == "" {
|
||||
s.ripFormat = ripFormatLosslessMKV
|
||||
}
|
||||
if s.ripStatusLabel != nil {
|
||||
s.ripStatusLabel.SetText("Ready")
|
||||
}
|
||||
s.setContent(buildRipView(s))
|
||||
}
|
||||
|
||||
func buildRipView(state *appState) fyne.CanvasObject {
|
||||
ripColor := moduleColor("rip")
|
||||
|
||||
backBtn := widget.NewButton("< BACK", func() {
|
||||
state.showMainMenu()
|
||||
})
|
||||
backBtn.Importance = widget.LowImportance
|
||||
|
||||
queueBtn := widget.NewButton("View Queue", func() {
|
||||
state.showQueue()
|
||||
})
|
||||
state.queueBtn = queueBtn
|
||||
state.updateQueueButtonLabel()
|
||||
|
||||
topBar := ui.TintedBar(ripColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
|
||||
bottomBar := moduleFooter(ripColor, layout.NewSpacer(), state.statsBar)
|
||||
|
||||
sourceEntry := widget.NewEntry()
|
||||
sourceEntry.SetPlaceHolder("Drop DVD/ISO/VIDEO_TS path here")
|
||||
sourceEntry.SetText(state.ripSourcePath)
|
||||
sourceEntry.OnChanged = func(val string) {
|
||||
state.ripSourcePath = strings.TrimSpace(val)
|
||||
state.ripOutputPath = defaultRipOutputPath(state.ripSourcePath, state.ripFormat)
|
||||
}
|
||||
|
||||
outputEntry := widget.NewEntry()
|
||||
outputEntry.SetPlaceHolder("Output path")
|
||||
outputEntry.SetText(state.ripOutputPath)
|
||||
outputEntry.OnChanged = func(val string) {
|
||||
state.ripOutputPath = strings.TrimSpace(val)
|
||||
}
|
||||
|
||||
formatSelect := widget.NewSelect([]string{ripFormatLosslessMKV, ripFormatH264MKV, ripFormatH264MP4}, func(val string) {
|
||||
state.ripFormat = val
|
||||
state.ripOutputPath = defaultRipOutputPath(state.ripSourcePath, state.ripFormat)
|
||||
outputEntry.SetText(state.ripOutputPath)
|
||||
})
|
||||
formatSelect.SetSelected(state.ripFormat)
|
||||
|
||||
statusLabel := widget.NewLabel("Ready")
|
||||
statusLabel.Wrapping = fyne.TextWrapWord
|
||||
state.ripStatusLabel = statusLabel
|
||||
|
||||
progressBar := widget.NewProgressBar()
|
||||
progressBar.SetValue(state.ripProgress / 100.0)
|
||||
state.ripProgressBar = progressBar
|
||||
|
||||
logEntry := widget.NewMultiLineEntry()
|
||||
logEntry.Wrapping = fyne.TextWrapOff
|
||||
logEntry.Disable()
|
||||
logEntry.SetText(state.ripLogText)
|
||||
state.ripLogEntry = logEntry
|
||||
logScroll := container.NewVScroll(logEntry)
|
||||
logScroll.SetMinSize(fyne.NewSize(0, 200))
|
||||
state.ripLogScroll = logScroll
|
||||
|
||||
addQueueBtn := widget.NewButton("Add Rip to Queue", func() {
|
||||
if err := state.addRipToQueue(false); err != nil {
|
||||
dialog.ShowError(err, state.window)
|
||||
return
|
||||
}
|
||||
dialog.ShowInformation("Queue", "Rip job added to queue.", state.window)
|
||||
if state.jobQueue != nil && !state.jobQueue.IsRunning() {
|
||||
state.jobQueue.Start()
|
||||
}
|
||||
})
|
||||
addQueueBtn.Importance = widget.MediumImportance
|
||||
|
||||
runNowBtn := widget.NewButton("Rip Now", func() {
|
||||
if err := state.addRipToQueue(true); err != nil {
|
||||
dialog.ShowError(err, state.window)
|
||||
return
|
||||
}
|
||||
if state.jobQueue != nil && !state.jobQueue.IsRunning() {
|
||||
state.jobQueue.Start()
|
||||
}
|
||||
dialog.ShowInformation("Rip", "Rip started! Track progress in Job Queue.", state.window)
|
||||
})
|
||||
runNowBtn.Importance = widget.HighImportance
|
||||
|
||||
controls := container.NewVBox(
|
||||
widget.NewLabelWithStyle("Source", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
ui.NewDroppable(sourceEntry, func(items []fyne.URI) {
|
||||
path := firstLocalPath(items)
|
||||
if path != "" {
|
||||
state.ripSourcePath = path
|
||||
sourceEntry.SetText(path)
|
||||
state.ripOutputPath = defaultRipOutputPath(path, state.ripFormat)
|
||||
outputEntry.SetText(state.ripOutputPath)
|
||||
}
|
||||
}),
|
||||
widget.NewLabelWithStyle("Format", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
formatSelect,
|
||||
widget.NewLabelWithStyle("Output", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
outputEntry,
|
||||
container.NewHBox(addQueueBtn, runNowBtn),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabelWithStyle("Status", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
statusLabel,
|
||||
progressBar,
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabelWithStyle("Rip Log", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
logScroll,
|
||||
)
|
||||
|
||||
return container.NewBorder(topBar, bottomBar, nil, nil, container.NewPadded(controls))
|
||||
}
|
||||
|
||||
func (s *appState) addRipToQueue(startNow bool) error {
|
||||
if s.jobQueue == nil {
|
||||
return fmt.Errorf("queue not initialized")
|
||||
}
|
||||
if strings.TrimSpace(s.ripSourcePath) == "" {
|
||||
return fmt.Errorf("set a DVD/ISO/VIDEO_TS source path")
|
||||
}
|
||||
if strings.TrimSpace(s.ripOutputPath) == "" {
|
||||
s.ripOutputPath = defaultRipOutputPath(s.ripSourcePath, s.ripFormat)
|
||||
}
|
||||
job := &queue.Job{
|
||||
Type: queue.JobTypeRip,
|
||||
Title: fmt.Sprintf("Rip DVD: %s", filepath.Base(s.ripSourcePath)),
|
||||
Description: fmt.Sprintf("Output: %s", utils.ShortenMiddle(filepath.Base(s.ripOutputPath), 40)),
|
||||
InputFile: s.ripSourcePath,
|
||||
OutputFile: s.ripOutputPath,
|
||||
Config: map[string]interface{}{
|
||||
"sourcePath": s.ripSourcePath,
|
||||
"outputPath": s.ripOutputPath,
|
||||
"format": s.ripFormat,
|
||||
},
|
||||
}
|
||||
s.resetRipLog()
|
||||
s.setRipStatus("Queued rip job...")
|
||||
s.setRipProgress(0)
|
||||
s.jobQueue.Add(job)
|
||||
if startNow && !s.jobQueue.IsRunning() {
|
||||
s.jobQueue.Start()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *appState) executeRipJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
||||
cfg := job.Config
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("rip job config missing")
|
||||
}
|
||||
sourcePath := toString(cfg["sourcePath"])
|
||||
outputPath := toString(cfg["outputPath"])
|
||||
format := toString(cfg["format"])
|
||||
if sourcePath == "" || outputPath == "" {
|
||||
return fmt.Errorf("rip job missing paths")
|
||||
}
|
||||
logFile, logPath, logErr := createRipLog(sourcePath, outputPath, format)
|
||||
if logErr != nil {
|
||||
logging.Debug(logging.CatSystem, "rip log open failed: %v", logErr)
|
||||
} else {
|
||||
job.LogPath = logPath
|
||||
defer logFile.Close()
|
||||
}
|
||||
|
||||
appendLog := func(line string) {
|
||||
if logFile != nil {
|
||||
fmt.Fprintln(logFile, line)
|
||||
}
|
||||
app := fyne.CurrentApp()
|
||||
if app != nil && app.Driver() != nil {
|
||||
app.Driver().DoFromGoroutine(func() {
|
||||
s.appendRipLog(line)
|
||||
}, false)
|
||||
}
|
||||
}
|
||||
updateProgress := func(percent float64) {
|
||||
progressCallback(percent)
|
||||
app := fyne.CurrentApp()
|
||||
if app != nil && app.Driver() != nil {
|
||||
app.Driver().DoFromGoroutine(func() {
|
||||
s.setRipProgress(percent)
|
||||
}, false)
|
||||
}
|
||||
}
|
||||
|
||||
appendLog(fmt.Sprintf("Rip started: %s", time.Now().Format(time.RFC3339)))
|
||||
appendLog(fmt.Sprintf("Source: %s", sourcePath))
|
||||
appendLog(fmt.Sprintf("Output: %s", outputPath))
|
||||
appendLog(fmt.Sprintf("Format: %s", format))
|
||||
|
||||
videoTSPath, cleanup, err := resolveVideoTSPath(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
sets, err := collectVOBSets(videoTSPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(sets) == 0 {
|
||||
return fmt.Errorf("no VOB files found in VIDEO_TS")
|
||||
}
|
||||
|
||||
set := sets[0]
|
||||
appendLog(fmt.Sprintf("Using title set: %s", set.Name))
|
||||
listFile, err := buildConcatList(set.Files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(listFile)
|
||||
|
||||
args := buildRipFFmpegArgs(listFile, outputPath, format)
|
||||
appendLog(fmt.Sprintf(">> ffmpeg %s", strings.Join(args, " ")))
|
||||
updateProgress(10)
|
||||
if err := runCommandWithLogger(ctx, platformConfig.FFmpegPath, args, appendLog); err != nil {
|
||||
return err
|
||||
}
|
||||
updateProgress(100)
|
||||
appendLog("Rip completed successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultRipOutputPath(sourcePath, format string) string {
|
||||
if sourcePath == "" {
|
||||
return ""
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
home = "."
|
||||
}
|
||||
baseDir := filepath.Join(home, "Videos", "DVD_Rips")
|
||||
name := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
|
||||
if strings.EqualFold(name, "video_ts") {
|
||||
name = filepath.Base(filepath.Dir(sourcePath))
|
||||
}
|
||||
name = sanitizeForPath(name)
|
||||
if name == "" {
|
||||
name = "dvd_rip"
|
||||
}
|
||||
ext := ".mkv"
|
||||
if format == ripFormatH264MP4 {
|
||||
ext = ".mp4"
|
||||
}
|
||||
return uniqueFilePath(filepath.Join(baseDir, name+ext))
|
||||
}
|
||||
|
||||
func createRipLog(inputPath, outputPath, format string) (*os.File, string, error) {
|
||||
base := strings.TrimSuffix(filepath.Base(outputPath), filepath.Ext(outputPath))
|
||||
if base == "" {
|
||||
base = "rip"
|
||||
}
|
||||
logPath := filepath.Join(getLogsDir(), base+"-rip"+conversionLogSuffix)
|
||||
if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil {
|
||||
return nil, logPath, fmt.Errorf("create log dir: %w", err)
|
||||
}
|
||||
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return nil, logPath, err
|
||||
}
|
||||
header := fmt.Sprintf(`VideoTools Rip Log
|
||||
Started: %s
|
||||
Source: %s
|
||||
Output: %s
|
||||
Format: %s
|
||||
|
||||
`, time.Now().Format(time.RFC3339), inputPath, outputPath, format)
|
||||
if _, err := f.WriteString(header); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, logPath, err
|
||||
}
|
||||
return f, logPath, nil
|
||||
}
|
||||
|
||||
func resolveVideoTSPath(path string) (string, func(), error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("source not found: %w", err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
if strings.EqualFold(filepath.Base(path), "VIDEO_TS") {
|
||||
return path, nil, nil
|
||||
}
|
||||
videoTS := filepath.Join(path, "VIDEO_TS")
|
||||
if info, err := os.Stat(videoTS); err == nil && info.IsDir() {
|
||||
return videoTS, nil, nil
|
||||
}
|
||||
return "", nil, fmt.Errorf("no VIDEO_TS folder found in %s", path)
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(path), ".iso") {
|
||||
tempDir, err := os.MkdirTemp(utils.TempDir(), "videotools-iso-")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
cleanup := func() {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
}
|
||||
tool, args, err := buildISOExtractCommand(path, tempDir)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return "", nil, err
|
||||
}
|
||||
if err := runCommandWithLogger(context.Background(), tool, args, nil); err != nil {
|
||||
cleanup()
|
||||
return "", nil, err
|
||||
}
|
||||
videoTS := filepath.Join(tempDir, "VIDEO_TS")
|
||||
if info, err := os.Stat(videoTS); err == nil && info.IsDir() {
|
||||
return videoTS, cleanup, nil
|
||||
}
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("VIDEO_TS not found in ISO")
|
||||
}
|
||||
return "", nil, fmt.Errorf("unsupported source: %s", path)
|
||||
}
|
||||
|
||||
func buildISOExtractCommand(isoPath, destDir string) (string, []string, error) {
|
||||
if _, err := exec.LookPath("xorriso"); err == nil {
|
||||
return "xorriso", []string{"-osirrox", "on", "-indev", isoPath, "-extract", "/VIDEO_TS", destDir}, nil
|
||||
}
|
||||
if _, err := exec.LookPath("bsdtar"); err == nil {
|
||||
return "bsdtar", []string{"-C", destDir, "-xf", isoPath, "VIDEO_TS"}, nil
|
||||
}
|
||||
return "", nil, fmt.Errorf("no ISO extraction tool found (install xorriso or bsdtar)")
|
||||
}
|
||||
|
||||
type vobSet struct {
|
||||
Name string
|
||||
Files []string
|
||||
Size int64
|
||||
}
|
||||
|
||||
func collectVOBSets(videoTS string) ([]vobSet, error) {
|
||||
entries, err := os.ReadDir(videoTS)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read VIDEO_TS: %w", err)
|
||||
}
|
||||
sets := map[string]*vobSet{}
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(name), ".vob") {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToUpper(name), "VTS_") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(strings.TrimSuffix(name, ".VOB"), "_")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
setKey := strings.Join(parts[:2], "_")
|
||||
if sets[setKey] == nil {
|
||||
sets[setKey] = &vobSet{Name: setKey}
|
||||
}
|
||||
full := filepath.Join(videoTS, name)
|
||||
info, err := os.Stat(full)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sets[setKey].Files = append(sets[setKey].Files, full)
|
||||
sets[setKey].Size += info.Size()
|
||||
}
|
||||
var result []vobSet
|
||||
for _, set := range sets {
|
||||
sort.Strings(set.Files)
|
||||
result = append(result, *set)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Size > result[j].Size
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func buildConcatList(files []string) (string, error) {
|
||||
if len(files) == 0 {
|
||||
return "", fmt.Errorf("no VOB files to concatenate")
|
||||
}
|
||||
listFile, err := os.CreateTemp(utils.TempDir(), "vt-rip-list-*.txt")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
writer := bufio.NewWriter(listFile)
|
||||
for _, f := range files {
|
||||
fmt.Fprintf(writer, "file '%s'\n", strings.ReplaceAll(f, "'", "'\\''"))
|
||||
}
|
||||
_ = writer.Flush()
|
||||
_ = listFile.Close()
|
||||
return listFile.Name(), nil
|
||||
}
|
||||
|
||||
func buildRipFFmpegArgs(listFile, outputPath, format string) []string {
|
||||
args := []string{
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
"-i", listFile,
|
||||
}
|
||||
switch format {
|
||||
case ripFormatH264MKV:
|
||||
args = append(args,
|
||||
"-c:v", "libx264",
|
||||
"-crf", "18",
|
||||
"-preset", "medium",
|
||||
"-c:a", "copy",
|
||||
)
|
||||
case ripFormatH264MP4:
|
||||
args = append(args,
|
||||
"-c:v", "libx264",
|
||||
"-crf", "18",
|
||||
"-preset", "medium",
|
||||
"-c:a", "aac",
|
||||
"-b:a", "192k",
|
||||
)
|
||||
default:
|
||||
args = append(args, "-c", "copy")
|
||||
}
|
||||
args = append(args, outputPath)
|
||||
return args
|
||||
}
|
||||
|
||||
func firstLocalPath(items []fyne.URI) string {
|
||||
for _, uri := range items {
|
||||
if uri.Scheme() == "file" {
|
||||
return uri.Path()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *appState) resetRipLog() {
|
||||
s.ripLogText = ""
|
||||
if s.ripLogEntry != nil {
|
||||
s.ripLogEntry.SetText("")
|
||||
}
|
||||
if s.ripLogScroll != nil {
|
||||
s.ripLogScroll.ScrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *appState) appendRipLog(line string) {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
return
|
||||
}
|
||||
s.ripLogText += line + "\n"
|
||||
if s.ripLogEntry != nil {
|
||||
s.ripLogEntry.SetText(s.ripLogText)
|
||||
}
|
||||
if s.ripLogScroll != nil {
|
||||
s.ripLogScroll.ScrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *appState) setRipStatus(text string) {
|
||||
if text == "" {
|
||||
text = "Ready"
|
||||
}
|
||||
if s.ripStatusLabel != nil {
|
||||
s.ripStatusLabel.SetText(text)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *appState) setRipProgress(percent float64) {
|
||||
if percent < 0 {
|
||||
percent = 0
|
||||
}
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
s.ripProgress = percent
|
||||
if s.ripProgressBar != nil {
|
||||
s.ripProgressBar.SetValue(percent / 100.0)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user