diff --git a/README.md b/README.md index 25360ea..ff53e8f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Vodular -This tool stitches together livestream VOD segments (in `.mkv`format) and automatically uploads them to YouTube, complete with customisable metadata such as titles, descriptions, tags, and a thumbnail! +This tool stitches together livestream VOD segments (in `.mkv`format) and automatically uploads them to YouTube, complete with customisable metadata such as titles, descriptions, and tags. I built this to greatly simplify the process of getting my full-quality livestream VODs onto YouTube, and I'm open-sourcing it in the hopes that it helps someone else with their workflow. As such, personal forks are welcome and encouraged! @@ -29,17 +29,11 @@ $ vodular --init /path/to/vod Directory successfully initialised. Be sure to update metadata.toml before uploading! ``` -This directory should contain: -- A `metadata.toml` file -- Your footage files, either at the root of the directory or in a specified subdirectory -- Your thumbnail, specifically named `thumbnail.png` - 4. Modify your newly-created `metadata.toml` to your liking. -5. Upload a VOD! +5. Upload a VOD (Optionally, delete the redundant full VOD export afterwards): ```sh -# `--deleteAfter` deletes the redundant full VOD export afterwards -vodular --deleteAfter /path/to/vod +$ vodular --deleteAfter /path/to/vod ``` **NOTE:** On first run, you will be prompted to sign in to YouTube with the channel you wish to upload to. To sign out, simply run `vodular --logout`. diff --git a/main.go b/main.go index 74572d1..c4092c5 100644 --- a/main.go +++ b/main.go @@ -216,19 +216,6 @@ func main() { enc.Encode(vodFiles) } - // scan for thumbnail - var thumbnail *yt.Thumbnail - thumbnailPath, thumbnailSizeBytes, err := scanner.ScanThumbnail(directory) - if err != nil { - log.Fatalf("Failed to fetch thumbnail: %v", err) - os.Exit(1) - } else { - thumbnail = &yt.Thumbnail{ - Filepath: thumbnailPath, - SizeBytes: thumbnailSizeBytes, - } - } - // build video template for upload video, err := yt.BuildVideo(metadata) if err != nil { @@ -276,7 +263,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.Filepath) + fullVodProbe, err := scanner.ProbeSegment(video.Filename) if err != nil { return false } video.SizeBytes = fullVodProbe.Format.Size var totalLength float64 = 0 @@ -330,7 +317,7 @@ func main() { } // okay actually upload now! - ytVideo, err := yt.UploadVideo(ctx, tokenSource, video, thumbnail, templates) + ytVideo, err := yt.UploadVideo(ctx, tokenSource, video, templates) if err != nil { log.Fatalf("Failed to upload video: %v", err) os.Exit(1) @@ -354,7 +341,7 @@ func main() { // delete full VOD after upload, if requested if deleteFullVod { - err = os.Remove(video.Filepath) + err = os.Remove(video.Filename) if err != nil { log.Fatalf("Failed to delete full VOD: %v", err) } diff --git a/scanner/scanner.go b/scanner/scanner.go index b124036..eb5c420 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -2,8 +2,6 @@ package scanner import ( "encoding/json" - "errors" - "fmt" "os" "path" "strconv" @@ -62,21 +60,6 @@ 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 { - if os.IsNotExist(err) { - return "", 0, err - } - 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 a9f54f1..4d13987 100644 --- a/video/video.go +++ b/video/video.go @@ -7,7 +7,6 @@ import ( "os" "path" "strconv" - "strings" "arimelody.space/vodular/youtube" ffmpeg "github.com/u2takey/ffmpeg-go" @@ -24,15 +23,15 @@ type ( func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, error) { fileListPath := path.Join( - path.Dir(video.Filepath), + path.Dir(video.Filename), "files.txt", ) totalDuration := float64(0.0) - fileListString := strings.Builder{} + fileListString := "" for _, file := range vodFiles { - fmt.Fprintf(&fileListString, "file '%s'\n", file) - jsonProbe, err := ffmpeg.Probe(path.Join(path.Dir(video.Filepath), file)) + fileListString += fmt.Sprintf("file '%s'\n", file) + jsonProbe, err := ffmpeg.Probe(path.Join(path.Dir(video.Filename), file)) if err != nil { return 0, fmt.Errorf("failed to probe file `%s`: %v", file, err) } @@ -46,7 +45,7 @@ func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, } err := os.WriteFile( fileListPath, - []byte(fileListString.String()), + []byte(fileListString), 0644, ) if err != nil { @@ -56,7 +55,7 @@ func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, stream := ffmpeg.Input(fileListPath, ffmpeg.KwArgs{ "f": "concat", "safe": "0", - }).Output(video.Filepath, ffmpeg.KwArgs{ + }).Output(video.Filename, ffmpeg.KwArgs{ "c": "copy", }).OverWriteOutput() if verbose { stream = stream.ErrorToStdOut() } @@ -71,7 +70,7 @@ func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, // not the end of the world; move along } - fileInfo, err := os.Stat(video.Filepath) + fileInfo, err := os.Stat(video.Filename) 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 96b18fc..f3f1873 100644 --- a/youtube/youtube.go +++ b/youtube/youtube.go @@ -41,12 +41,7 @@ type ( Part int Date time.Time Tags []string - Filepath string - SizeBytes int64 - } - - Thumbnail struct { - Filepath string + Filename string SizeBytes int64 } ) @@ -70,7 +65,7 @@ func BuildVideo(metadata *scanner.Metadata) (*Video, error) { Part: metadata.Part, Date: metadata.Date.AsTime(time.UTC), Tags: metadata.Tags, - Filepath: path.Join( + Filename: path.Join( metadata.FootageDir, fmt.Sprintf( "%s-fullvod.mkv", @@ -221,7 +216,6 @@ func UploadVideo( ctx context.Context, tokenSource oauth2.TokenSource, video *Video, - thumbnail *Thumbnail, templates *Template, ) (*youtube.Video, error) { title, err := BuildTemplate(video, templates.Title) @@ -239,8 +233,6 @@ func UploadVideo( return nil, err } - // upload video - videoService := youtube.NewVideosService(service) categoryId := YT_CATEGORY_ENTERTAINMENT @@ -250,7 +242,7 @@ func UploadVideo( } } - videoInsertCall := videoService.Insert([]string{ + call := videoService.Insert([]string{ "snippet", "status", }, &youtube.Video{ Snippet: &youtube.VideoSnippet{ @@ -264,51 +256,26 @@ func UploadVideo( }, }).NotifySubscribers(false) - videoFile, err := os.Open(video.Filepath) + file, err := os.Open(video.Filename) if err != nil { - log.Fatalf("Failed to open video: %v\n", err) + log.Fatalf("Failed to open file: %v\n", err) return nil, err } - videoInsertCall.Media(videoFile) + call.Media(file) log.Println("Uploading video...") - videoInsertCall.ProgressUpdater(func(current, total int64) { + call.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("\t(%.2f%%)\n", float64(current) / float64(total) * 100) + fmt.Printf("Uploading... (%.2f%%)\n", float64(current) / float64(total) * 100) }) - ytVideo, err := videoInsertCall.Do() + ytVideo, err := call.Do() if err != nil { log.Fatalf("Failed to upload video: %v\n", err) return nil, err } - if thumbnail != nil { - // upload thumbnail - - thumbnailService := youtube.NewThumbnailsService(service) - thumbnailSetCall := thumbnailService.Set(ytVideo.Id) - thumbnailFile, err := os.Open(thumbnail.Filepath) - if err != nil { - log.Printf("Failed to open thumbnail: %v\n", err) - return ytVideo, 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 }