diff --git a/main.go b/main.go index c4092c5..67cd1c2 100644 --- a/main.go +++ b/main.go @@ -216,6 +216,17 @@ func main() { enc.Encode(vodFiles) } + // scan for thumbnail + thumbnailPath, thumbnailSizeBytes, err := scanner.ScanThumbnail(directory) + if err != nil { + log.Fatalf("Failed to fetch thumbnail: %v", err) + os.Exit(1) + } + thumbnail := &yt.Thumbnail{ + Filepath: thumbnailPath, + SizeBytes: thumbnailSizeBytes, + } + // build video template for upload video, err := yt.BuildVideo(metadata) if err != nil { @@ -263,7 +274,7 @@ func main() { // concatenate VOD segments into full VOD fullVodExists := func () bool { // check if full VOD already exists with expected duration - fullVodProbe, err := scanner.ProbeSegment(video.Filename) + fullVodProbe, err := scanner.ProbeSegment(video.Filepath) if err != nil { return false } video.SizeBytes = fullVodProbe.Format.Size var totalLength float64 = 0 @@ -317,7 +328,7 @@ func main() { } // okay actually upload now! - ytVideo, err := yt.UploadVideo(ctx, tokenSource, video, templates) + ytVideo, err := yt.UploadVideo(ctx, tokenSource, video, thumbnail, templates) if err != nil { log.Fatalf("Failed to upload video: %v", err) os.Exit(1) @@ -341,7 +352,7 @@ func main() { // delete full VOD after upload, if requested if deleteFullVod { - err = os.Remove(video.Filename) + err = os.Remove(video.Filepath) if err != nil { log.Fatalf("Failed to delete full VOD: %v", err) } diff --git a/scanner/scanner.go b/scanner/scanner.go index eb5c420..606f269 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -2,6 +2,7 @@ package scanner import ( "encoding/json" + "fmt" "os" "path" "strconv" @@ -60,6 +61,18 @@ func ScanSegments(directory string, extension string) ([]string, error) { return files, nil } +func ScanThumbnail(directory string) (string, int64, error) { + thumbnailPath := path.Join(directory, "thumbnail.png") + stat, err := os.Stat(thumbnailPath) + if err != nil { + return "", 0, err + } + if stat.IsDir() { + return "", 0, fmt.Errorf("thumbnail.png is a directory") + } + return thumbnailPath, stat.Size(), nil +} + func ProbeSegment(filename string) (*FFprobeOutput, error) { out, err := ffmpeg_go.Probe(filename) if err != nil { return nil, err } diff --git a/video/video.go b/video/video.go index 4d13987..a9f54f1 100644 --- a/video/video.go +++ b/video/video.go @@ -7,6 +7,7 @@ import ( "os" "path" "strconv" + "strings" "arimelody.space/vodular/youtube" ffmpeg "github.com/u2takey/ffmpeg-go" @@ -23,15 +24,15 @@ type ( func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, error) { fileListPath := path.Join( - path.Dir(video.Filename), + path.Dir(video.Filepath), "files.txt", ) totalDuration := float64(0.0) - fileListString := "" + fileListString := strings.Builder{} for _, file := range vodFiles { - fileListString += fmt.Sprintf("file '%s'\n", file) - jsonProbe, err := ffmpeg.Probe(path.Join(path.Dir(video.Filename), file)) + fmt.Fprintf(&fileListString, "file '%s'\n", file) + jsonProbe, err := ffmpeg.Probe(path.Join(path.Dir(video.Filepath), file)) if err != nil { return 0, fmt.Errorf("failed to probe file `%s`: %v", file, err) } @@ -45,7 +46,7 @@ func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, } err := os.WriteFile( fileListPath, - []byte(fileListString), + []byte(fileListString.String()), 0644, ) if err != nil { @@ -55,7 +56,7 @@ func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, stream := ffmpeg.Input(fileListPath, ffmpeg.KwArgs{ "f": "concat", "safe": "0", - }).Output(video.Filename, ffmpeg.KwArgs{ + }).Output(video.Filepath, ffmpeg.KwArgs{ "c": "copy", }).OverWriteOutput() if verbose { stream = stream.ErrorToStdOut() } @@ -70,7 +71,7 @@ func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, // not the end of the world; move along } - fileInfo, err := os.Stat(video.Filename) + fileInfo, err := os.Stat(video.Filepath) if err != nil { return 0, fmt.Errorf("failed to read output file: %v", err) } return fileInfo.Size(), nil diff --git a/youtube/youtube.go b/youtube/youtube.go index f3f1873..5f0dc0f 100644 --- a/youtube/youtube.go +++ b/youtube/youtube.go @@ -41,7 +41,12 @@ type ( Part int Date time.Time Tags []string - Filename string + Filepath string + SizeBytes int64 + } + + Thumbnail struct { + Filepath string SizeBytes int64 } ) @@ -65,7 +70,7 @@ func BuildVideo(metadata *scanner.Metadata) (*Video, error) { Part: metadata.Part, Date: metadata.Date.AsTime(time.UTC), Tags: metadata.Tags, - Filename: path.Join( + Filepath: path.Join( metadata.FootageDir, fmt.Sprintf( "%s-fullvod.mkv", @@ -216,6 +221,7 @@ func UploadVideo( ctx context.Context, tokenSource oauth2.TokenSource, video *Video, + thumbnail *Thumbnail, templates *Template, ) (*youtube.Video, error) { title, err := BuildTemplate(video, templates.Title) @@ -233,6 +239,8 @@ func UploadVideo( return nil, err } + // upload video + videoService := youtube.NewVideosService(service) categoryId := YT_CATEGORY_ENTERTAINMENT @@ -242,7 +250,7 @@ func UploadVideo( } } - call := videoService.Insert([]string{ + videoInsertCall := videoService.Insert([]string{ "snippet", "status", }, &youtube.Video{ Snippet: &youtube.VideoSnippet{ @@ -256,26 +264,49 @@ func UploadVideo( }, }).NotifySubscribers(false) - file, err := os.Open(video.Filename) + videoFile, err := os.Open(video.Filepath) if err != nil { - log.Fatalf("Failed to open file: %v\n", err) + log.Fatalf("Failed to open video: %v\n", err) return nil, err } - call.Media(file) + videoInsertCall.Media(videoFile) log.Println("Uploading video...") - call.ProgressUpdater(func(current, total int64) { + videoInsertCall.ProgressUpdater(func(current, total int64) { // for some reason, this only returns 0. // instead, we pull the file size from the ffmpeg output directly. if total == 0 { total = video.SizeBytes } - fmt.Printf("Uploading... (%.2f%%)\n", float64(current) / float64(total) * 100) + fmt.Printf("\t(%.2f%%)\n", float64(current) / float64(total) * 100) }) - ytVideo, err := call.Do() + ytVideo, err := videoInsertCall.Do() if err != nil { log.Fatalf("Failed to upload video: %v\n", err) return nil, err } + // upload thumbnail + + thumbnailService := youtube.NewThumbnailsService(service) + thumbnailSetCall := thumbnailService.Set(ytVideo.Id) + thumbnailFile, err := os.Open(thumbnail.Filepath) + if err != nil { + log.Fatalf("Failed to open thumbnail: %v\n", err) + return nil, err + } + thumbnailSetCall.Media(thumbnailFile) + + log.Println("Uploading thumbnail...") + + thumbnailSetCall.ProgressUpdater(func(current, total int64) { + if total == 0 { total = thumbnail.SizeBytes } + fmt.Printf("\t(%.2f%%)\n", float64(current) / float64(total) * 100) + }) + // kinda don't care about the response here- so long as it works! + _, err = thumbnailSetCall.Do() + if err != nil { + log.Printf("Failed to upload thumbnail: %v\n", err) + } + return ytVideo, err }