new feature: update metadata on uploaded videos
This commit is contained in:
parent
3053c19dd0
commit
c961aa3b6c
4 changed files with 196 additions and 128 deletions
175
main.go
175
main.go
|
|
@ -13,6 +13,7 @@ import (
|
|||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/youtube/v3"
|
||||
|
||||
"arimelody.space/vodular/config"
|
||||
|
|
@ -35,28 +36,26 @@ func showHelp() {
|
|||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// config
|
||||
userConfigDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not determine user configuration directory: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
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)
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfg == nil {
|
||||
err = os.MkdirAll(path.Dir(config.CONFIG_FILENAME), 0750)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create config directory: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = config.GenerateConfig(config.CONFIG_FILENAME)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate config: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Printf(
|
||||
"New config file created (%s). " +
|
||||
|
|
@ -135,7 +134,6 @@ func main() {
|
|||
err = config.WriteConfig(cfg, config.CONFIG_FILENAME)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write config: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Println("Logged out successfully.")
|
||||
os.Exit(0)
|
||||
|
|
@ -146,21 +144,18 @@ func main() {
|
|||
err = scanner.InitialiseDirectory(directory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialise directory: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Printf(
|
||||
log.Fatalf(
|
||||
"Directory successfully initialised. " +
|
||||
"Be sure to update %s before uploading!",
|
||||
scanner.METADATA_FILENAME,
|
||||
)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// good to have early on
|
||||
templates, err := yt.FetchTemplates(path.Join(userConfigDir, "vodular", "templates"))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch templates: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// read directory metadata
|
||||
|
|
@ -168,27 +163,14 @@ func main() {
|
|||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Fatalf("Directory does not exist: %s", directory)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Fatalf("Failed to fetch VOD metadata: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if metadata == nil {
|
||||
log.Fatal(
|
||||
"Directory contained no metadata. " +
|
||||
"Use `--init` to initialise this directory.",
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// skip uploading if already done
|
||||
if metadata.Uploaded == !forceUpload {
|
||||
log.Printf(
|
||||
"VOD has already been uploaded. " +
|
||||
"Use --force to override, or update the %s.",
|
||||
scanner.METADATA_FILENAME,
|
||||
)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// default footage directory
|
||||
|
|
@ -201,14 +183,12 @@ func main() {
|
|||
vodFiles, err := scanner.ScanSegments(footageDir, SEGMENT_EXTENSION)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch VOD filenames: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(vodFiles) == 0 {
|
||||
log.Fatalf(
|
||||
"Directory contained no VOD files (expecting .%s)",
|
||||
SEGMENT_EXTENSION,
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
if verbose {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
|
|
@ -220,39 +200,35 @@ func main() {
|
|||
}
|
||||
|
||||
// scan for thumbnail
|
||||
var thumbnail *yt.Thumbnail
|
||||
var thumbnail *yt.ThumbnailMetadata
|
||||
thumbnailPath, thumbnailSizeBytes, err := scanner.ScanThumbnail(directory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch thumbnail: %v", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
thumbnail = &yt.Thumbnail{
|
||||
thumbnail = &yt.ThumbnailMetadata{
|
||||
Filepath: thumbnailPath,
|
||||
SizeBytes: thumbnailSizeBytes,
|
||||
}
|
||||
}
|
||||
|
||||
// build video template for upload
|
||||
video, err := yt.BuildVideo(metadata)
|
||||
videoMeta, err := yt.BuildVideo(metadata)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to build video template: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
title, err := yt.BuildTemplate(video, templates.Title)
|
||||
title, err := yt.BuildTemplate(videoMeta, templates.Title)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to build video title: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
description, err := yt.BuildTemplate(video, templates.Description)
|
||||
description, err := yt.BuildTemplate(videoMeta, templates.Description)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to build video description: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(title) > 100 {
|
||||
log.Fatalf(
|
||||
"Video title length exceeds %d characters (%d). YouTube may reject this!",
|
||||
MAX_TITLE_LEN,
|
||||
len(video.Title),
|
||||
len(videoMeta.Title),
|
||||
)
|
||||
}
|
||||
if len(description) > 5000 {
|
||||
|
|
@ -265,7 +241,7 @@ func main() {
|
|||
if verbose {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
fmt.Printf("\nVideo template: ")
|
||||
enc.Encode(video)
|
||||
enc.Encode(videoMeta)
|
||||
|
||||
fmt.Printf(
|
||||
"\n================================\n\n" +
|
||||
|
|
@ -276,12 +252,85 @@ func main() {
|
|||
)
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
// youtube oauth flow
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: cfg.Google.ClientID,
|
||||
ClientSecret: cfg.Google.ClientSecret,
|
||||
Endpoint: google.Endpoint,
|
||||
Scopes: []string{ youtube.YoutubeScope },
|
||||
RedirectURL: cfg.RedirectUri,
|
||||
}
|
||||
var token *oauth2.Token
|
||||
if cfg.Token != nil {
|
||||
token = cfg.Token
|
||||
} else {
|
||||
token, err = oauth.GenerateToken(&ctx, oauth2Config, cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("OAuth flow failed: %v", err)
|
||||
}
|
||||
cfg.Token = token
|
||||
}
|
||||
tokenSource := oauth2Config.TokenSource(ctx, token)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create OAuth2 token source: %v", err)
|
||||
}
|
||||
if err = config.WriteConfig(cfg, config.CONFIG_FILENAME); err != nil {
|
||||
log.Fatalf("Failed to save OAuth token: %v", err)
|
||||
}
|
||||
|
||||
service, err := youtube.NewService(
|
||||
ctx,
|
||||
option.WithScopes(youtube.YoutubeUploadScope),
|
||||
option.WithTokenSource(tokenSource),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create youtube service: %v\n", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
// check if full VOD already exists with expected duration
|
||||
fullVodExists := false
|
||||
fullVodProbe, err := scanner.ProbeSegment(video.Filepath)
|
||||
fullVodProbe, err := scanner.ProbeSegment(videoMeta.Filepath)
|
||||
if err == nil {
|
||||
video.SizeBytes = fullVodProbe.Format.Size
|
||||
videoMeta.SizeBytes = fullVodProbe.Format.Size
|
||||
var totalLength float64 = 0
|
||||
|
||||
for _, filename := range vodFiles {
|
||||
|
|
@ -297,53 +346,20 @@ func main() {
|
|||
log.Print("Full VOD appears to already exist- uploading this file...")
|
||||
} else {
|
||||
// concatenate VOD segments into full VOD
|
||||
video.SizeBytes, err = vid.ConcatVideo(video, vodFiles, verbose)
|
||||
videoMeta.SizeBytes, err = vid.ConcatVideo(videoMeta, vodFiles, verbose)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to concatenate VOD segments: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Print("Dry run: Skipping VOD concatenation")
|
||||
}
|
||||
|
||||
// youtube oauth flow
|
||||
ctx := context.Background()
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: cfg.Google.ClientID,
|
||||
ClientSecret: cfg.Google.ClientSecret,
|
||||
Endpoint: google.Endpoint,
|
||||
Scopes: []string{ youtube.YoutubeScope },
|
||||
RedirectURL: cfg.RedirectUri,
|
||||
}
|
||||
var token *oauth2.Token
|
||||
if cfg.Token != nil {
|
||||
token = cfg.Token
|
||||
} else {
|
||||
token, err = oauth.GenerateToken(&ctx, oauth2Config, cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("OAuth flow failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.Token = token
|
||||
}
|
||||
tokenSource := oauth2Config.TokenSource(ctx, token)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create OAuth2 token source: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = config.WriteConfig(cfg, config.CONFIG_FILENAME)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to save OAuth token: %v", err)
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
if !dryRun {
|
||||
// okay actually upload now!
|
||||
ytVideo, err := yt.UploadVideo(ctx, tokenSource, video, thumbnail, templates)
|
||||
ytVideo, err := yt.UploadVideo(ctx, tokenSource, service, videoMeta, templates)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to upload video: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if verbose {
|
||||
jsonString, err := json.MarshalIndent(ytVideo, "", " ")
|
||||
|
|
@ -354,9 +370,14 @@ func main() {
|
|||
}
|
||||
log.Print("Video uploaded successfully!")
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// update metadata to reflect VOD is uploaded
|
||||
// TODO: rather than a boolean flag, link to actual video
|
||||
metadata.Uploaded = true
|
||||
metadata.UploadURL = yt.VIDEO_URL_BASE + ytVideo.Id
|
||||
err = scanner.WriteMetadata(directory, metadata)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to update metadata: %v", err)
|
||||
|
|
@ -364,7 +385,7 @@ func main() {
|
|||
|
||||
// delete full VOD after upload, if requested
|
||||
if deleteFullVod {
|
||||
err = os.Remove(video.Filepath)
|
||||
err = os.Remove(videoMeta.Filepath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to delete full VOD: %v", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue