2025-11-05 21:22:15 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-01-28 12:50:11 +00:00
|
|
|
_ "embed"
|
2025-11-05 21:22:15 +00:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
2026-01-28 10:48:14 +00:00
|
|
|
"log"
|
2026-01-31 04:04:32 +00:00
|
|
|
"math"
|
2025-11-05 21:22:15 +00:00
|
|
|
"os"
|
2026-01-28 10:48:14 +00:00
|
|
|
"path"
|
|
|
|
|
"strings"
|
2025-11-05 21:22:15 +00:00
|
|
|
|
2026-01-28 10:48:14 +00:00
|
|
|
"golang.org/x/oauth2"
|
|
|
|
|
"golang.org/x/oauth2/google"
|
2026-06-28 15:37:36 +01:00
|
|
|
"google.golang.org/api/option"
|
2025-11-05 21:22:15 +00:00
|
|
|
"google.golang.org/api/youtube/v3"
|
2026-01-28 10:48:14 +00:00
|
|
|
|
2026-01-30 18:53:09 +00:00
|
|
|
"arimelody.space/vodular/config"
|
2026-06-28 13:40:08 +01:00
|
|
|
"arimelody.space/vodular/oauth"
|
2026-01-30 18:53:09 +00:00
|
|
|
"arimelody.space/vodular/scanner"
|
|
|
|
|
vid "arimelody.space/vodular/video"
|
|
|
|
|
yt "arimelody.space/vodular/youtube"
|
2025-11-05 21:22:15 +00:00
|
|
|
)
|
|
|
|
|
|
2026-01-28 12:50:11 +00:00
|
|
|
//go:embed res/help.txt
|
|
|
|
|
var helpText string
|
2026-01-28 10:48:14 +00:00
|
|
|
|
2026-02-01 17:08:16 +00:00
|
|
|
const SEGMENT_EXTENSION = "mkv"
|
|
|
|
|
const MAX_TITLE_LEN = 100
|
|
|
|
|
const MAX_DESCRIPTION_LEN = 5000
|
2026-01-30 14:14:54 +00:00
|
|
|
|
2025-11-05 21:22:15 +00:00
|
|
|
func main() {
|
2026-06-28 15:37:36 +01:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
2026-01-28 10:48:14 +00:00
|
|
|
var verbose bool = false
|
2026-01-28 12:50:11 +00:00
|
|
|
var initDirectory bool = false
|
2026-01-30 18:53:09 +00:00
|
|
|
var logout bool = false
|
2026-06-28 16:37:42 +01:00
|
|
|
var keepConcatVOD bool = false
|
2026-01-28 12:50:11 +00:00
|
|
|
var forceUpload bool = false
|
2026-06-28 13:35:06 +01:00
|
|
|
var dryRun bool = false
|
2026-06-28 16:37:42 +01:00
|
|
|
var directory string = ""
|
2026-01-28 10:48:14 +00:00
|
|
|
|
|
|
|
|
for i, arg := range os.Args {
|
|
|
|
|
if i == 0 { continue }
|
|
|
|
|
if strings.HasPrefix(arg, "-") {
|
|
|
|
|
switch arg {
|
|
|
|
|
|
|
|
|
|
case "-h":
|
|
|
|
|
fallthrough
|
|
|
|
|
case "--help":
|
2026-06-28 16:37:42 +01:00
|
|
|
log.Fatal(helpText)
|
2026-01-28 10:48:14 +00:00
|
|
|
|
|
|
|
|
case "-v":
|
|
|
|
|
fallthrough
|
|
|
|
|
case "--verbose":
|
|
|
|
|
verbose = true
|
|
|
|
|
|
2026-06-28 16:37:42 +01:00
|
|
|
case "--dry-run":
|
|
|
|
|
dryRun = true
|
2026-01-28 12:50:11 +00:00
|
|
|
|
|
|
|
|
case "-f":
|
|
|
|
|
fallthrough
|
|
|
|
|
case "--force":
|
|
|
|
|
forceUpload = true
|
|
|
|
|
|
2026-06-28 16:37:42 +01:00
|
|
|
case "--init":
|
|
|
|
|
initDirectory = true
|
|
|
|
|
|
|
|
|
|
case "--keep-vod":
|
|
|
|
|
keepConcatVOD = true
|
|
|
|
|
|
|
|
|
|
case "--logout":
|
|
|
|
|
logout = true
|
2026-06-28 13:35:06 +01:00
|
|
|
|
2026-01-28 10:48:14 +00:00
|
|
|
default:
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Unknown option `%s`\n", arg)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
directory = arg
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-05 21:22:15 +00:00
|
|
|
|
2026-06-28 16:59:41 +01:00
|
|
|
// config
|
|
|
|
|
userConfigDir, err := os.UserConfigDir()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Could not determine user configuration directory: %v", err)
|
|
|
|
|
}
|
|
|
|
|
config.CONFIG_FILENAME = path.Join(userConfigDir, "vodular", "config.toml")
|
|
|
|
|
cfg, err := config.ReadConfig(config.CONFIG_FILENAME)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to read config: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
if err = os.MkdirAll(path.Dir(config.CONFIG_FILENAME), 0750); err != nil {
|
|
|
|
|
log.Fatalf("Failed to create config directory: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err = config.GenerateConfig(config.CONFIG_FILENAME); err != nil {
|
|
|
|
|
log.Fatalf("Failed to generate config: %v", err)
|
|
|
|
|
}
|
|
|
|
|
log.Printf(
|
|
|
|
|
"New config file created (%s). " +
|
|
|
|
|
"Please edit this file before running again!",
|
|
|
|
|
config.CONFIG_FILENAME,
|
|
|
|
|
)
|
|
|
|
|
os.Exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oauthTokenFilepath := path.Join(userConfigDir, "vodular", "youtube-auth.json")
|
|
|
|
|
var token *oauth2.Token
|
|
|
|
|
if data, err := os.ReadFile(oauthTokenFilepath); err != nil {
|
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
|
log.Fatalf("Failed to open %s: %v", oauthTokenFilepath, err)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if err := json.Unmarshal(data, &token); err != nil {
|
|
|
|
|
log.Fatalf("Failed to parse YouTube auth file: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 19:23:35 +00:00
|
|
|
if !strings.HasPrefix(directory, "/") {
|
|
|
|
|
if wd, err := os.Getwd(); err != nil {
|
|
|
|
|
directory = path.Join(directory, wd)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 18:53:09 +00:00
|
|
|
// logout (--logout)
|
|
|
|
|
if logout {
|
2026-06-28 16:59:41 +01:00
|
|
|
if token == nil { log.Fatal("Not logged in.") }
|
|
|
|
|
if err := os.Remove(oauthTokenFilepath); err != nil {
|
|
|
|
|
log.Fatalf("Failed to delete %s: %v", oauthTokenFilepath, err)
|
2026-01-30 14:14:54 +00:00
|
|
|
}
|
2026-06-28 16:37:42 +01:00
|
|
|
log.Fatalf("Logged out successfully.")
|
2026-01-28 12:50:11 +00:00
|
|
|
}
|
2025-11-05 21:22:15 +00:00
|
|
|
|
2026-01-28 12:50:11 +00:00
|
|
|
// initialising directory (--init)
|
2026-01-28 10:48:14 +00:00
|
|
|
if initDirectory {
|
2026-06-28 16:59:41 +01:00
|
|
|
if err = scanner.InitialiseDirectory(directory); err != nil {
|
2026-01-28 12:50:11 +00:00
|
|
|
log.Fatalf("Failed to initialise directory: %v", err)
|
2026-01-28 10:48:14 +00:00
|
|
|
}
|
2026-06-28 15:37:36 +01:00
|
|
|
log.Fatalf(
|
2026-01-30 14:14:54 +00:00
|
|
|
"Directory successfully initialised. " +
|
|
|
|
|
"Be sure to update %s before uploading!",
|
|
|
|
|
scanner.METADATA_FILENAME,
|
|
|
|
|
)
|
2026-01-28 10:48:14 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-28 16:59:41 +01:00
|
|
|
|
|
|
|
|
|
2026-01-30 18:53:09 +00:00
|
|
|
// good to have early on
|
2026-01-31 03:10:28 +00:00
|
|
|
templates, err := yt.FetchTemplates(path.Join(userConfigDir, "vodular", "templates"))
|
2026-01-30 18:53:09 +00:00
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to fetch templates: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 12:50:11 +00:00
|
|
|
// read directory metadata
|
|
|
|
|
metadata, err := scanner.ReadMetadata(directory)
|
2025-11-05 21:22:15 +00:00
|
|
|
if err != nil {
|
2026-01-30 19:23:35 +00:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
log.Fatalf("Directory does not exist: %s", directory)
|
|
|
|
|
}
|
2026-01-28 10:48:14 +00:00
|
|
|
log.Fatalf("Failed to fetch VOD metadata: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if metadata == nil {
|
2026-01-28 12:50:11 +00:00
|
|
|
log.Fatal(
|
|
|
|
|
"Directory contained no metadata. " +
|
|
|
|
|
"Use `--init` to initialise this directory.",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 14:14:54 +00:00
|
|
|
// default footage directory
|
2026-06-20 21:23:38 +01:00
|
|
|
footageDir := directory
|
|
|
|
|
if len(metadata.FootageDir) >= 0 && !strings.HasPrefix(metadata.FootageDir, "/") {
|
|
|
|
|
footageDir = path.Join(directory, metadata.FootageDir)
|
2026-01-30 14:14:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 12:50:11 +00:00
|
|
|
// scan for VOD segments
|
2026-06-20 21:23:38 +01:00
|
|
|
vodFiles, err := scanner.ScanSegments(footageDir, SEGMENT_EXTENSION)
|
2026-01-28 10:48:14 +00:00
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to fetch VOD filenames: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(vodFiles) == 0 {
|
2026-01-28 12:50:11 +00:00
|
|
|
log.Fatalf(
|
|
|
|
|
"Directory contained no VOD files (expecting .%s)",
|
2026-02-01 17:08:16 +00:00
|
|
|
SEGMENT_EXTENSION,
|
2026-01-28 12:50:11 +00:00
|
|
|
)
|
2025-11-05 21:22:15 +00:00
|
|
|
}
|
2026-01-28 10:48:14 +00:00
|
|
|
if verbose {
|
|
|
|
|
enc := json.NewEncoder(os.Stdout)
|
|
|
|
|
enc.SetIndent("", "\t")
|
2026-06-28 16:22:36 +01:00
|
|
|
log.Print("[ Metadata ]")
|
|
|
|
|
log.Printf("Title: %s", metadata.Title)
|
|
|
|
|
log.Printf("Part: %d", metadata.Part)
|
|
|
|
|
log.Printf("Date: %s", metadata.Date.String())
|
|
|
|
|
log.Printf(
|
|
|
|
|
"Category: `%s` - %s (%s)",
|
|
|
|
|
metadata.Category.Type,
|
|
|
|
|
metadata.Category.Name,
|
|
|
|
|
metadata.Category.Url,
|
|
|
|
|
)
|
|
|
|
|
log.Printf("Tags: %s", strings.Join(metadata.Tags, ", "))
|
|
|
|
|
log.Printf("VOD files: [ %s ]", strings.Join(vodFiles, ", "))
|
2026-01-28 10:48:14 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 18:28:50 +01:00
|
|
|
// scan for thumbnail
|
2026-06-28 15:37:36 +01:00
|
|
|
var thumbnail *yt.ThumbnailMetadata
|
2026-06-23 18:28:50 +01:00
|
|
|
thumbnailPath, thumbnailSizeBytes, err := scanner.ScanThumbnail(directory)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to fetch thumbnail: %v", err)
|
2026-06-23 19:07:51 +01:00
|
|
|
} else {
|
2026-06-28 15:37:36 +01:00
|
|
|
thumbnail = &yt.ThumbnailMetadata{
|
2026-06-23 19:07:51 +01:00
|
|
|
Filepath: thumbnailPath,
|
|
|
|
|
SizeBytes: thumbnailSizeBytes,
|
|
|
|
|
}
|
2026-06-23 18:28:50 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 12:50:11 +00:00
|
|
|
// build video template for upload
|
2026-06-28 15:37:36 +01:00
|
|
|
videoMeta, err := yt.BuildVideo(metadata)
|
2025-11-05 21:22:15 +00:00
|
|
|
if err != nil {
|
2026-01-28 10:48:14 +00:00
|
|
|
log.Fatalf("Failed to build video template: %v", err)
|
2025-11-05 21:22:15 +00:00
|
|
|
}
|
2026-06-28 15:37:36 +01:00
|
|
|
title, err := yt.BuildTemplate(videoMeta, templates.Title)
|
2026-02-01 17:08:16 +00:00
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to build video title: %v", err)
|
|
|
|
|
}
|
2026-06-28 15:37:36 +01:00
|
|
|
description, err := yt.BuildTemplate(videoMeta, templates.Description)
|
2026-02-01 17:08:16 +00:00
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to build video description: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(title) > 100 {
|
|
|
|
|
log.Fatalf(
|
|
|
|
|
"Video title length exceeds %d characters (%d). YouTube may reject this!",
|
|
|
|
|
MAX_TITLE_LEN,
|
2026-06-28 15:37:36 +01:00
|
|
|
len(videoMeta.Title),
|
2026-02-01 17:08:16 +00:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if len(description) > 5000 {
|
|
|
|
|
log.Fatalf(
|
|
|
|
|
"Video description length exceeds %d characters (%d). YouTube may reject this!",
|
|
|
|
|
MAX_DESCRIPTION_LEN,
|
|
|
|
|
len(description),
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-01-28 10:48:14 +00:00
|
|
|
if verbose {
|
2026-06-28 16:22:36 +01:00
|
|
|
log.Print()
|
|
|
|
|
log.Printf("[ TITLE ]\n\n%s\n\n", title)
|
|
|
|
|
log.Printf("[ DESCRIPTION ]\n\n%s\n\n", description)
|
2026-01-28 10:48:14 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-28 16:59:41 +01:00
|
|
|
|
|
|
|
|
|
2026-01-28 12:50:11 +00:00
|
|
|
// youtube oauth flow
|
2026-01-30 15:55:57 +00:00
|
|
|
oauth2Config := &oauth2.Config{
|
|
|
|
|
ClientID: cfg.Google.ClientID,
|
|
|
|
|
ClientSecret: cfg.Google.ClientSecret,
|
|
|
|
|
Endpoint: google.Endpoint,
|
|
|
|
|
Scopes: []string{ youtube.YoutubeScope },
|
|
|
|
|
RedirectURL: cfg.RedirectUri,
|
|
|
|
|
}
|
2026-06-28 16:59:41 +01:00
|
|
|
|
|
|
|
|
if token == nil {
|
2026-06-28 13:40:08 +01:00
|
|
|
token, err = oauth.GenerateToken(&ctx, oauth2Config, cfg)
|
2026-06-28 16:59:41 +01:00
|
|
|
if err != nil { log.Fatalf("OAuth flow failed: %v", err) }
|
|
|
|
|
tokenFile, err := os.OpenFile(oauthTokenFilepath, os.O_RDWR | os.O_CREATE, 0600)
|
|
|
|
|
if err != nil { log.Fatalf("Failed to open %s: %v", oauthTokenFilepath, err) }
|
|
|
|
|
if err := json.NewEncoder(tokenFile).Encode(token); err != nil {
|
|
|
|
|
log.Fatalf("Failed to write %s: %v", oauthTokenFilepath, err)
|
2026-01-30 14:14:54 +00:00
|
|
|
}
|
2026-01-30 15:55:57 +00:00
|
|
|
}
|
2026-06-28 16:59:41 +01:00
|
|
|
|
2026-01-30 15:55:57 +00:00
|
|
|
tokenSource := oauth2Config.TokenSource(ctx, token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to create OAuth2 token source: %v", err)
|
2026-06-28 15:37:36 +01:00
|
|
|
}
|
|
|
|
|
if err = config.WriteConfig(cfg, config.CONFIG_FILENAME); err != nil {
|
|
|
|
|
log.Fatalf("Failed to save OAuth token: %v", err)
|
2026-01-30 15:55:57 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-28 16:59:41 +01:00
|
|
|
|
|
|
|
|
|
2026-06-28 15:37:36 +01:00
|
|
|
service, err := youtube.NewService(
|
|
|
|
|
ctx,
|
|
|
|
|
option.WithScopes(youtube.YoutubeUploadScope),
|
|
|
|
|
option.WithTokenSource(tokenSource),
|
|
|
|
|
)
|
2026-01-30 15:55:57 +00:00
|
|
|
if err != nil {
|
2026-06-28 15:37:36 +01:00
|
|
|
log.Fatalf("Failed to create youtube service: %v\n", err)
|
2026-01-28 12:50:11 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-28 16:59:41 +01:00
|
|
|
|
|
|
|
|
|
2026-06-28 15:37:36 +01:00
|
|
|
// skip uploading if already done
|
|
|
|
|
if (metadata.Uploaded || len(metadata.UploadURL) > 0) && !forceUpload {
|
|
|
|
|
if len(metadata.UploadURL) > 0 {
|
|
|
|
|
log.Printf("VOD has already been uploaded: %s", metadata.UploadURL)
|
|
|
|
|
} else {
|
|
|
|
|
log.Println("VOD has already been uploaded.")
|
|
|
|
|
}
|
|
|
|
|
if !dryRun {
|
|
|
|
|
log.Printf(
|
|
|
|
|
"To upload anyways, use --force or update %s.",
|
|
|
|
|
scanner.METADATA_FILENAME,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(metadata.UploadURL) > 0 {
|
|
|
|
|
if !dryRun {
|
|
|
|
|
videoId := strings.TrimPrefix(metadata.UploadURL, yt.VIDEO_URL_BASE)
|
|
|
|
|
if videoId == metadata.UploadURL {
|
|
|
|
|
log.Fatal("can't parse video ID from upload URL")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Println("Updating metadata...")
|
|
|
|
|
if err = yt.UpdateMetadata(ctx, tokenSource, service, videoMeta, videoId, templates); err != nil {
|
|
|
|
|
log.Printf("update metadata: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Println("Uploading thumbnail...")
|
|
|
|
|
if err = yt.UploadThumbnail(ctx, tokenSource, service, videoId, thumbnail); err != nil {
|
|
|
|
|
log.Printf("upload thumbnail: %v", err)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log.Print("Dry run: Skipping metadata update")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
os.Exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 16:59:41 +01:00
|
|
|
|
|
|
|
|
|
2026-06-28 16:22:36 +01:00
|
|
|
videoMeta.Filepath = path.Join(metadata.FootageDir, vodFiles[0])
|
|
|
|
|
|
|
|
|
|
// no need to concat if VOD is already one whole file
|
|
|
|
|
if len(vodFiles) > 1 {
|
|
|
|
|
videoMeta.Filepath = path.Join(
|
|
|
|
|
metadata.FootageDir,
|
|
|
|
|
fmt.Sprintf("%s-fullvod.mkv", metadata.Date.String()),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if !dryRun {
|
|
|
|
|
// check if full VOD already exists with expected duration
|
|
|
|
|
concatVodExists := false
|
|
|
|
|
concatVodProbe, err := scanner.ProbeSegment(videoMeta.Filepath)
|
|
|
|
|
if err == nil {
|
|
|
|
|
videoMeta.SizeBytes = concatVodProbe.Format.Size
|
|
|
|
|
var totalLength float64 = 0
|
|
|
|
|
|
|
|
|
|
for _, filename := range vodFiles {
|
|
|
|
|
probe, err := scanner.ProbeSegment(path.Join(footageDir, filename))
|
|
|
|
|
if err != nil { continue }
|
|
|
|
|
totalLength += probe.Format.Duration
|
|
|
|
|
}
|
|
|
|
|
// full VOD may exist, but not be complete (interrupted concat)
|
|
|
|
|
concatVodExists = math.Abs(concatVodProbe.Format.Duration - totalLength) < float64(0.1)
|
2026-06-28 15:37:36 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-28 16:22:36 +01:00
|
|
|
if concatVodExists {
|
|
|
|
|
log.Print("Full VOD appears to already exist- uploading this file...")
|
|
|
|
|
} else {
|
|
|
|
|
// concatenate VOD segments into full VOD
|
|
|
|
|
videoMeta.SizeBytes, err = vid.ConcatVideo(videoMeta, vodFiles, verbose)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to concatenate VOD segments: %v", err)
|
|
|
|
|
}
|
2026-06-28 15:37:36 +01:00
|
|
|
}
|
2026-06-28 16:22:36 +01:00
|
|
|
} else {
|
|
|
|
|
log.Print("Dry run: Skipping VOD concatenation")
|
2026-06-28 15:37:36 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 16:59:41 +01:00
|
|
|
|
|
|
|
|
|
2026-06-28 15:37:36 +01:00
|
|
|
if !dryRun {
|
2026-06-28 13:35:06 +01:00
|
|
|
// okay actually upload now!
|
2026-06-28 15:37:36 +01:00
|
|
|
ytVideo, err := yt.UploadVideo(ctx, tokenSource, service, videoMeta, templates)
|
2026-01-28 12:50:11 +00:00
|
|
|
if err != nil {
|
2026-06-28 13:35:06 +01:00
|
|
|
log.Fatalf("Failed to upload video: %v", err)
|
2026-01-28 12:50:11 +00:00
|
|
|
}
|
2026-06-28 13:35:06 +01:00
|
|
|
if verbose {
|
|
|
|
|
jsonString, err := json.MarshalIndent(ytVideo, "", " ")
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to marshal video data json: %v", err)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println(string(jsonString))
|
|
|
|
|
}
|
|
|
|
|
log.Print("Video uploaded successfully!")
|
2026-01-28 12:50:11 +00:00
|
|
|
|
2026-06-28 15:37:36 +01:00
|
|
|
// set the thumbnail
|
|
|
|
|
log.Println("Uploading thumbnail...")
|
|
|
|
|
if err = yt.UploadThumbnail(ctx, tokenSource, service, ytVideo.Id, thumbnail); err != nil {
|
|
|
|
|
log.Printf("Failed to upload thumbnail: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 13:35:06 +01:00
|
|
|
// update metadata to reflect VOD is uploaded
|
2026-06-28 15:37:36 +01:00
|
|
|
metadata.UploadURL = yt.VIDEO_URL_BASE + ytVideo.Id
|
2026-06-28 16:59:41 +01:00
|
|
|
if err = scanner.WriteMetadata(directory, metadata); err != nil {
|
2026-06-28 13:35:06 +01:00
|
|
|
log.Fatalf("Failed to update metadata: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 16:37:42 +01:00
|
|
|
// delete concatenated VOD after upload, unless specified otherwise
|
2026-06-28 16:22:36 +01:00
|
|
|
// if len(vodFiles) == 1, the full VOD is the *only* VOD. do not delete this!!!
|
2026-06-28 16:37:42 +01:00
|
|
|
if len(vodFiles) > 1 && !keepConcatVOD {
|
2026-06-28 16:22:36 +01:00
|
|
|
if err = os.Remove(videoMeta.Filepath); err != nil {
|
2026-06-28 13:35:06 +01:00
|
|
|
log.Fatalf("Failed to delete full VOD: %v", err)
|
|
|
|
|
}
|
2026-01-28 12:50:11 +00:00
|
|
|
}
|
2026-06-28 13:35:06 +01:00
|
|
|
} else {
|
|
|
|
|
log.Print("Dry run: Skipping video upload")
|
2026-01-28 12:50:11 +00:00
|
|
|
}
|
|
|
|
|
}
|