pre-cleanup

This commit is contained in:
townandgown 2025-08-10 22:56:26 -05:00
parent 718d69c12a
commit 3c49a3fa9f
6 changed files with 151 additions and 33 deletions

View File

@ -9,12 +9,11 @@ import (
"m3u8-downloader/cmd/transfer" "m3u8-downloader/cmd/transfer"
"os" "os"
"strings" "strings"
"time"
) )
func main() { func main() {
url := flag.String("url", "", "M3U8 playlist URL") url := flag.String("url", "", "M3U8 playlist URL")
eventName := flag.String("event", time.Now().Format("2006-01-02"), "Event name") eventName := flag.String("event", "", "Event name")
debug := flag.Bool("debug", false, "Enable debug mode") debug := flag.Bool("debug", false, "Enable debug mode")
transferOnly := flag.Bool("transfer", false, "Transfer-only mode: transfer existing files without downloading") transferOnly := flag.Bool("transfer", false, "Transfer-only mode: transfer existing files without downloading")
processOnly := flag.Bool("process", false, "Process-only mode: process existing files without downloading") processOnly := flag.Bool("process", false, "Process-only mode: process existing files without downloading")

View File

@ -1,17 +1,69 @@
package transfer package transfer
import ( import (
"bufio"
"context" "context"
"fmt"
"log" "log"
"m3u8-downloader/pkg/constants" "m3u8-downloader/pkg/constants"
"m3u8-downloader/pkg/transfer" "m3u8-downloader/pkg/transfer"
"os" "os"
"os/signal" "os/signal"
"strconv"
"strings"
"syscall" "syscall"
"time" "time"
) )
func getEventDirs() ([]string, error) {
dirs, err := os.ReadDir(constants.LocalOutputDirPath)
if err != nil {
return nil, fmt.Errorf("Failed to read directory: %w", err)
}
var eventDirs []string
for _, dir := range dirs {
if dir.IsDir() {
eventDirs = append(eventDirs, dir.Name())
}
}
return eventDirs, nil
}
func RunTransferOnly(eventName string) { func RunTransferOnly(eventName string) {
// Check if NAS transfer is enabled
if !constants.EnableNASTransfer {
log.Fatal("NAS transfer is disabled in constants. Please enable it to use transfer-only mode.")
}
if eventName == "" {
events, err := getEventDirs()
if err != nil {
log.Fatalf("Failed to get event directories: %v", err)
}
if len(events) == 0 {
log.Fatal("No events found")
}
if len(events) > 1 {
fmt.Println("Multiple events found, please select one:")
for i, event := range events {
fmt.Printf("%d. %s\n", i+1, event)
}
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
index, err := strconv.Atoi(input)
if err != nil {
log.Fatalf("Failed to parse input: %v", err)
}
if index < 1 || index > len(events) {
log.Fatal("Invalid input")
}
eventName = events[index-1]
} else {
eventName = events[0]
}
}
log.Printf("Starting transfer-only mode for event: %s", eventName) log.Printf("Starting transfer-only mode for event: %s", eventName)
// Setup context and signal handling // Setup context and signal handling
@ -26,11 +78,6 @@ func RunTransferOnly(eventName string) {
cancel() cancel()
}() }()
// Check if NAS transfer is enabled
if !constants.EnableNASTransfer {
log.Fatal("NAS transfer is disabled in constants. Please enable it to use transfer-only mode.")
}
// Verify local event directory exists // Verify local event directory exists
localEventPath := constants.LocalOutputDirPath + "/" + eventName localEventPath := constants.LocalOutputDirPath + "/" + eventName
if _, err := os.Stat(localEventPath); os.IsNotExist(err) { if _, err := os.Stat(localEventPath); os.IsNotExist(err) {

View File

@ -10,7 +10,7 @@ const (
REFERRER = "https://www.flomarching.com" REFERRER = "https://www.flomarching.com"
LocalOutputDirPath = "../data/" LocalOutputDirPath = "../data/"
EnableNASTransfer = false EnableNASTransfer = true
NASOutputPath = "\\\\HomeLabNAS\\dci\\streams" NASOutputPath = "\\\\HomeLabNAS\\dci\\streams"
NASUsername = "NASAdmin" NASUsername = "NASAdmin"
NASPassword = "s3tkY6tzA&KN6M" NASPassword = "s3tkY6tzA&KN6M"
@ -23,7 +23,7 @@ const (
BatchSize = 1000 BatchSize = 1000
ManifestPath = "../data" ManifestPath = "../data"
CleanupAfterTransfer = false CleanupAfterTransfer = true
CleanupBatchSize = 1000 CleanupBatchSize = 1000
RetainLocalHours = 0 RetainLocalHours = 0
@ -31,4 +31,5 @@ const (
AutoProcess = true AutoProcess = true
ProcessingEnabled = true ProcessingEnabled = true
ProcessWorkerCount = 2 ProcessWorkerCount = 2
FFmpegPath = "ffmpeg"
) )

View File

@ -90,19 +90,16 @@ func (nt *NASService) EnsureDirectoryExists(path string) error {
} }
func (nt *NASService) EstablishConnection() error { func (nt *NASService) EstablishConnection() error {
// Extract the network path (\\server\share) from the full path
networkPath := nt.ExtractNetworkPath(nt.Config.Path) networkPath := nt.ExtractNetworkPath(nt.Config.Path)
if networkPath == "" { if networkPath == "" {
// Local path, no authentication needed return nil // local path, no network mount needed
return nil
} }
log.Printf("Establishing network connection to %s with user %s", networkPath, nt.Config.Username) log.Printf("Establishing network connection to %s with user %s", networkPath, nt.Config.Username)
// Use Windows net use command to establish authenticated connection
var cmd *exec.Cmd var cmd *exec.Cmd
if nt.Config.Username != "" && nt.Config.Password != "" { if nt.Config.Username != "" && nt.Config.Password != "" {
cmd = exec.Command("net", "use", networkPath, nt.Config.Password, "/user:"+nt.Config.Username, "/persistent:no") cmd = exec.Command("net", "use", networkPath, "/user:"+nt.Config.Username, nt.Config.Password, "/persistent:no")
} else { } else {
cmd = exec.Command("net", "use", networkPath, "/persistent:no") cmd = exec.Command("net", "use", networkPath, "/persistent:no")
} }

View File

@ -9,7 +9,10 @@ import (
"m3u8-downloader/pkg/nas" "m3u8-downloader/pkg/nas"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp" "regexp"
"runtime"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -23,7 +26,7 @@ type ProcessingService struct {
func NewProcessingService(eventName string) (*ProcessingService, error) { func NewProcessingService(eventName string) (*ProcessingService, error) {
config := &ProcessConfig{ config := &ProcessConfig{
WorkerCount: constants.ProcessWorkerCount, WorkerCount: constants.ProcessWorkerCount,
SourcePath: constants.NASOutputPath + "/" + eventName, SourcePath: constants.NASOutputPath,
DestinationPath: constants.ProcessOutputPath, DestinationPath: constants.ProcessOutputPath,
Enabled: constants.ProcessingEnabled, Enabled: constants.ProcessingEnabled,
EventName: eventName, EventName: eventName,
@ -131,10 +134,14 @@ func (ps *ProcessingService) Start(ctx context.Context) error {
return fmt.Errorf("Failed to aggregate segment info: %w", err) return fmt.Errorf("Failed to aggregate segment info: %w", err)
} }
ps.WriteConcatFile(segments) aggFile, err := ps.WriteConcatFile(segments)
if err != nil {
return fmt.Errorf("Failed to write concat file: %w", err)
}
//Feed info to ffmpeg to stitch files together //Feed info to ffmpeg to stitch files together
concatErr := ps.RunFFmpeg(ps.config.SourcePath, ps.config.DestinationPath) outPath := filepath.Join(constants.NASOutputPath, ps.config.DestinationPath, ps.config.EventName)
concatErr := ps.RunFFmpeg(aggFile, outPath)
if concatErr != nil { if concatErr != nil {
return concatErr return concatErr
} }
@ -143,7 +150,7 @@ func (ps *ProcessingService) Start(ctx context.Context) error {
} }
func (ps *ProcessingService) GetResolutions() ([]string, error) { func (ps *ProcessingService) GetResolutions() ([]string, error) {
dirs, err := os.ReadDir(ps.config.SourcePath) dirs, err := os.ReadDir(ps.config.SourcePath + "/" + ps.config.EventName)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to read source directory: %w", err) return nil, fmt.Errorf("Failed to read source directory: %w", err)
} }
@ -163,7 +170,7 @@ func (ps *ProcessingService) GetResolutions() ([]string, error) {
func (ps *ProcessingService) ParseResolutionDirectory(resolution string, ch chan<- SegmentInfo, wg *sync.WaitGroup) { func (ps *ProcessingService) ParseResolutionDirectory(resolution string, ch chan<- SegmentInfo, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
files, err := os.ReadDir(ps.config.SourcePath + "/" + resolution) files, err := os.ReadDir(ps.config.SourcePath + "/" + ps.config.EventName + "/" + resolution)
if err != nil { if err != nil {
log.Printf("Failed to read resolution directory: %v", err) log.Printf("Failed to read resolution directory: %v", err)
return return
@ -171,7 +178,10 @@ func (ps *ProcessingService) ParseResolutionDirectory(resolution string, ch chan
for _, file := range files { for _, file := range files {
if !file.IsDir() { if !file.IsDir() {
no, err := strconv.Atoi(file.Name()[7:11]) if !strings.HasSuffix(strings.ToLower(file.Name()), ".ts") {
continue
}
no, err := strconv.Atoi(file.Name()[6:10])
if err != nil { if err != nil {
log.Printf("Failed to parse segment number: %v", err) log.Printf("Failed to parse segment number: %v", err)
continue continue
@ -200,6 +210,7 @@ func (ps *ProcessingService) AggregateSegmentInfo(ch <-chan SegmentInfo) (map[in
} }
for segment := range ch { for segment := range ch {
fmt.Printf("Received segment %s in resolution %s \n", segment.Name, segment.Resolution)
current, exists := segmentMap[segment.SeqNo] current, exists := segmentMap[segment.SeqNo]
if !exists || rank[segment.Resolution] > rank[current.Resolution] { if !exists || rank[segment.Resolution] > rank[current.Resolution] {
segmentMap[segment.SeqNo] = segment segmentMap[segment.SeqNo] = segment
@ -209,30 +220,93 @@ func (ps *ProcessingService) AggregateSegmentInfo(ch <-chan SegmentInfo) (map[in
return segmentMap, nil return segmentMap, nil
} }
func (ps *ProcessingService) WriteConcatFile(segmentMap map[int]SegmentInfo) error { func (ps *ProcessingService) WriteConcatFile(segmentMap map[int]SegmentInfo) (string, error) {
f, err := os.Create(ps.config.DestinationPath + "/concat.txt") concatPath := filepath.Join(constants.NASOutputPath, constants.ProcessOutputPath, ps.config.EventName)
// Ensure the directory exists
if err := os.MkdirAll(concatPath, 0755); err != nil {
return "", fmt.Errorf("failed to create directories for concat path: %w", err)
}
concatFilePath := filepath.Join(concatPath, ps.config.EventName+".txt")
f, err := os.Create(concatFilePath)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create concat file: %w", err) return "", fmt.Errorf("failed to create concat file: %w", err)
} }
defer f.Close() defer f.Close()
for _, segment := range segmentMap { // Sort keys to preserve order
_, err = f.WriteString("file '" + ps.config.SourcePath + "/" + segment.Resolution + "/" + segment.Name + "'\n") keys := make([]int, 0, len(segmentMap))
if err != nil { for k := range segmentMap {
return fmt.Errorf("Failed to write to concat file: %w", err) keys = append(keys, k)
}
sort.Ints(keys)
for _, seq := range keys {
segment := segmentMap[seq]
filePath := filepath.Join(ps.config.SourcePath, ps.config.EventName, segment.Resolution, segment.Name)
line := fmt.Sprintf("file '%s'\n", filePath)
if _, err := f.WriteString(line); err != nil {
return "", fmt.Errorf("failed to write to concat file: %w", err)
} }
} }
return nil return concatFilePath, nil
}
func ffmpegPath() (string, error) {
var baseDir string
exePath, err := os.Executable()
if err == nil {
baseDir = filepath.Dir(exePath)
} else {
// fallback to current working directory
baseDir, err = os.Getwd()
if err != nil {
return "", err
}
}
// When running in GoLand, exePath might point to the IDE launcher,
// so optionally check if ffmpeg exists here, else fallback to CWD
ffmpeg := filepath.Join(baseDir, "bin", "ffmpeg")
if runtime.GOOS == "windows" {
ffmpeg += ".exe"
}
if _, err := os.Stat(ffmpeg); os.IsNotExist(err) {
// try cwd instead
cwd, err := os.Getwd()
if err != nil {
return "", err
}
ffmpeg = filepath.Join(cwd, "bin", "ffmpeg")
if runtime.GOOS == "windows" {
ffmpeg += ".exe"
}
}
return ffmpeg, nil
} }
func (ps *ProcessingService) RunFFmpeg(inputPath, outputPath string) error { func (ps *ProcessingService) RunFFmpeg(inputPath, outputPath string) error {
cmd := exec.Command("ffmpeg", "-f", "concat", "-safe", "0", "-i", inputPath, "-c", "copy", outputPath) fmt.Println("Running ffmpeg...")
fileOutPath := filepath.Join(outputPath, ps.config.EventName+".mp4")
fmt.Println("Input path:", inputPath)
fmt.Println("Attempting to write to: ", outputPath+"/"+ps.config.EventName+".mp4")
path, err := ffmpegPath()
if err != nil {
return err
}
cmd := exec.Command(path, "-f", "concat", "-safe", "0", "-i", inputPath, "-c", "copy", filepath.Join(outputPath, ps.config.EventName+".mp4"))
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err := cmd.Run() ffmpegErr := cmd.Run()
if err != nil { if ffmpegErr != nil {
return fmt.Errorf("Failed to run ffmpeg: %w", err) return fmt.Errorf("Failed to run ffmpeg: %w", ffmpegErr)
} }
fmt.Println("Output path:", fileOutPath)
return nil return nil
} }

View File

@ -40,7 +40,7 @@ func NewTrasferService(outputDir string, eventName string) (*TransferService, er
Enabled: constants.CleanupAfterTransfer, Enabled: constants.CleanupAfterTransfer,
RetentionPeriod: time.Duration(constants.RetainLocalHours) * time.Hour, RetentionPeriod: time.Duration(constants.RetainLocalHours) * time.Hour,
BatchSize: constants.CleanupBatchSize, BatchSize: constants.CleanupBatchSize,
CheckInterval: 1 * time.Minute, CheckInterval: constants.FileSettlingDelay,
} }
cleanup := NewCleanupService(cleanupConfig) cleanup := NewCleanupService(cleanupConfig)