Helper tool for stitching together livestream VOD segments and uploading them to YouTube!
1package scanner
2
3import (
4 "encoding/json"
5 "os"
6 "path"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/pelletier/go-toml/v2"
12 ffmpeg_go "github.com/u2takey/ffmpeg-go"
13)
14
15type (
16 Category struct {
17 Name string `toml:"name"`
18 Type string `toml:"type" comment:"Valid types: gaming, other (default: other)"`
19 Url string `toml:"url"`
20 }
21
22 Metadata struct {
23 Title string `toml:"title"`
24 Part int `toml:"part"`
25 Date string `toml:"date"`
26 Tags []string `toml:"tags"`
27 FootageDir string `toml:"footage_dir"`
28 Uploaded bool `toml:"uploaded"`
29 Category *Category `toml:"category" comment:"(Optional) Category details, for additional credits."`
30 }
31
32 FFprobeFormat struct {
33 Duration float64 `json:"duration"`
34 Size int64 `json:"size"`
35 }
36
37 FFprobeOutput struct {
38 Format FFprobeFormat `json:"format"`
39 }
40)
41
42const METADATA_FILENAME = "metadata.toml"
43
44func ScanSegments(directory string, extension string) ([]string, error) {
45 entries, err := os.ReadDir(directory)
46 if err != nil {
47 return nil, err
48 }
49
50 files := []string{}
51
52 for _, item := range entries {
53 if item.IsDir() { continue }
54 if strings.HasPrefix(item.Name(), ".") { continue }
55 if !strings.HasSuffix(item.Name(), "." + extension) { continue }
56 if strings.HasSuffix(item.Name(), "-fullvod." + extension) { continue }
57 files = append(files, item.Name())
58 }
59
60 return files, nil
61}
62
63func ProbeSegment(filename string) (*FFprobeOutput, error) {
64 out, err := ffmpeg_go.Probe(filename)
65 if err != nil { return nil, err }
66
67 type (
68 RawFFprobeFormat struct {
69 // these being strings upsets me immensely
70 Duration string `json:"duration"`
71 Size string `json:"size"`
72 }
73 RawFFprobeOutput struct {
74 Format RawFFprobeFormat `json:"format"`
75 }
76 )
77
78 probe := RawFFprobeOutput{}
79 err = json.Unmarshal([]byte(out), &probe)
80 if err != nil { return nil, err }
81
82 duration, err := strconv.ParseFloat(probe.Format.Duration, 64)
83 if err != nil { return nil, err }
84 size, err := strconv.ParseInt(probe.Format.Size, 10, 0)
85 if err != nil { return nil, err }
86
87 return &FFprobeOutput{
88 Format: FFprobeFormat{
89 Duration: duration,
90 Size: size,
91 },
92 }, nil
93}
94
95func ReadMetadata(directory string) (*Metadata, error) {
96 metadata := &Metadata{}
97 file, err := os.OpenFile(
98 path.Join(directory, METADATA_FILENAME),
99 os.O_RDONLY, os.ModePerm,
100 )
101 if err != nil { return nil, err }
102
103 err = toml.NewDecoder(file).Decode(metadata)
104 if err != nil { return nil, err }
105
106 return metadata, nil
107}
108
109func WriteMetadata(directory string, metadata *Metadata) (error) {
110 file, err := os.OpenFile(
111 path.Join(directory, METADATA_FILENAME),
112 os.O_CREATE | os.O_RDWR, 0644,
113 )
114 if err != nil { return err }
115
116 err = toml.NewEncoder(file).Encode(metadata)
117 return err
118}
119
120func DefaultMetadata() *Metadata {
121 return &Metadata{
122 Title: "Untitled Stream",
123 Date: time.Now().Format("2006-01-02"),
124 Part: 0,
125 Category: &Category{
126 Name: "Something",
127 Type: "",
128 Url: "",
129 },
130 }
131}