Helper tool for stitching together livestream VOD segments and uploading them to YouTube!

check if fullvod exists; store config in UserConfigDir

+58 -13
+1 -1
config/config.go
··· 33 }, 34 } 35 36 - const CONFIG_FILENAME = "config.toml" 37 38 func ReadConfig(filename string) (*Config, error) { 39 cfgBytes, err := os.ReadFile(filename)
··· 33 }, 34 } 35 36 + var CONFIG_FILENAME string = "config.toml" 37 38 func ReadConfig(filename string) (*Config, error) { 39 cfgBytes, err := os.ReadFile(filename)
+35 -12
main.go
··· 28 const segmentExtension = "mkv" 29 30 func showHelp() { 31 - execSplits := strings.Split(os.Args[0], "/") 32 - execName := execSplits[len(execSplits) - 1] 33 - fmt.Printf(helpText, execName) 34 } 35 36 func main() { 37 // config 38 cfg, err := config.ReadConfig(config.CONFIG_FILENAME) 39 if err != nil { 40 log.Fatalf("Failed to read config: %v", err) 41 os.Exit(1) 42 } 43 if cfg == nil { 44 err = config.GenerateConfig(config.CONFIG_FILENAME) 45 if err != nil { 46 log.Fatalf("Failed to generate config: %v", err) ··· 57 // arguments 58 if len(os.Args) < 2 || os.Args[1] == "--help" || os.Args[1] == "-h" { 59 showHelp() 60 - os.Exit(0) 61 } 62 63 var verbose bool = false ··· 76 fallthrough 77 case "--help": 78 showHelp() 79 - os.Exit(0) 80 81 case "-v": 82 fallthrough ··· 228 os.Exit(1) 229 } 230 fmt.Printf( 231 - "\n================================\n" + 232 - "TITLE:\n%s\n\n" + 233 - "DESCRIPTION:\n%s\n" + 234 "\n================================\n", 235 title, description, 236 ) 237 } 238 239 // concatenate VOD segments into full VOD 240 - video.SizeBytes, err = vid.ConcatVideo(video, vodFiles, verbose) 241 - if err != nil { 242 - log.Fatalf("Failed to concatenate VOD segments: %v", err) 243 - os.Exit(1) 244 } 245 246 // youtube oauth flow
··· 28 const segmentExtension = "mkv" 29 30 func showHelp() { 31 + fmt.Println(helpText) 32 + os.Exit(0) 33 } 34 35 func main() { 36 // config 37 + userConfigDir, err := os.UserConfigDir() 38 + if err != nil { 39 + log.Fatalf("Could not determine user configuration directory: %v", err) 40 + os.Exit(1) 41 + } 42 + config.CONFIG_FILENAME = path.Join(userConfigDir, "vodular", "config.toml") 43 cfg, err := config.ReadConfig(config.CONFIG_FILENAME) 44 if err != nil { 45 log.Fatalf("Failed to read config: %v", err) 46 os.Exit(1) 47 } 48 if cfg == nil { 49 + err = os.MkdirAll(path.Dir(config.CONFIG_FILENAME), 0750) 50 + if err != nil { 51 + log.Fatalf("Failed to create config directory: %v", err) 52 + os.Exit(1) 53 + } 54 err = config.GenerateConfig(config.CONFIG_FILENAME) 55 if err != nil { 56 log.Fatalf("Failed to generate config: %v", err) ··· 67 // arguments 68 if len(os.Args) < 2 || os.Args[1] == "--help" || os.Args[1] == "-h" { 69 showHelp() 70 } 71 72 var verbose bool = false ··· 85 fallthrough 86 case "--help": 87 showHelp() 88 89 case "-v": 90 fallthrough ··· 236 os.Exit(1) 237 } 238 fmt.Printf( 239 + "\n================================\n\n" + 240 + "< TITLE >\n%s\n\n" + 241 + "< DESCRIPTION >\n%s\n" + 242 "\n================================\n", 243 title, description, 244 ) 245 } 246 247 // concatenate VOD segments into full VOD 248 + fullVodExists := func () bool { 249 + // check if full VOD already exists with expected duration 250 + if fullVodProbe, err := scanner.ProbeSegment(video.Filename); err != nil { 251 + var totalLength float64 = 0 252 + for _, filename := range vodFiles { 253 + probe, err := scanner.ProbeSegment(filename) 254 + if err != nil { continue } 255 + totalLength += probe.Format.Duration 256 + } 257 + return fullVodProbe.Format.Duration == totalLength 258 + } 259 + return false 260 + }() 261 + if !fullVodExists { 262 + video.SizeBytes, err = vid.ConcatVideo(video, vodFiles, verbose) 263 + if err != nil { 264 + log.Fatalf("Failed to concatenate VOD segments: %v", err) 265 + os.Exit(1) 266 + } 267 } 268 269 // youtube oauth flow
+22
scanner/scanner.go
··· 1 package scanner 2 3 import ( 4 "os" 5 "path" 6 "strings" 7 "time" 8 9 "github.com/pelletier/go-toml/v2" 10 ) 11 12 type ( ··· 24 FootageDir string `toml:"footage_dir"` 25 Uploaded bool `toml:"uploaded"` 26 Category *Category `toml:"category" comment:"(Optional) Category details, for additional credits."` 27 } 28 ) 29 ··· 45 } 46 47 return files, nil 48 } 49 50 func ReadMetadata(directory string) (*Metadata, error) {
··· 1 package scanner 2 3 import ( 4 + "encoding/json" 5 "os" 6 "path" 7 "strings" 8 "time" 9 10 "github.com/pelletier/go-toml/v2" 11 + ffmpeg_go "github.com/u2takey/ffmpeg-go" 12 ) 13 14 type ( ··· 26 FootageDir string `toml:"footage_dir"` 27 Uploaded bool `toml:"uploaded"` 28 Category *Category `toml:"category" comment:"(Optional) Category details, for additional credits."` 29 + } 30 + 31 + ffprobeFormat struct { 32 + Duration float64 `json:"duration"` 33 + Size int64 `json:"size"` 34 + } 35 + 36 + ffprobeOutput struct { 37 + Format ffprobeFormat `json:"format"` 38 } 39 ) 40 ··· 56 } 57 58 return files, nil 59 + } 60 + 61 + func ProbeSegment(filename string) (*ffprobeOutput, error) { 62 + out, err := ffmpeg_go.Probe(filename) 63 + if err != nil { return nil, err } 64 + 65 + probe := ffprobeOutput{} 66 + err = json.Unmarshal([]byte(out), probe) 67 + if err != nil { return nil, err } 68 + 69 + return &probe, nil 70 } 71 72 func ReadMetadata(directory string) (*Metadata, error) {