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"
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
|
"google.golang.org/api/option"
|
||||||
"google.golang.org/api/youtube/v3"
|
"google.golang.org/api/youtube/v3"
|
||||||
|
|
||||||
"arimelody.space/vodular/config"
|
"arimelody.space/vodular/config"
|
||||||
|
|
@ -35,28 +36,26 @@ func showHelp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
// config
|
// config
|
||||||
userConfigDir, err := os.UserConfigDir()
|
userConfigDir, err := os.UserConfigDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Could not determine user configuration directory: %v", err)
|
log.Fatalf("Could not determine user configuration directory: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
config.CONFIG_FILENAME = path.Join(userConfigDir, "vodular", "config.toml")
|
config.CONFIG_FILENAME = path.Join(userConfigDir, "vodular", "config.toml")
|
||||||
cfg, err := config.ReadConfig(config.CONFIG_FILENAME)
|
cfg, err := config.ReadConfig(config.CONFIG_FILENAME)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to read config: %v", err)
|
log.Fatalf("Failed to read config: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
err = os.MkdirAll(path.Dir(config.CONFIG_FILENAME), 0750)
|
err = os.MkdirAll(path.Dir(config.CONFIG_FILENAME), 0750)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to create config directory: %v", err)
|
log.Fatalf("Failed to create config directory: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
err = config.GenerateConfig(config.CONFIG_FILENAME)
|
err = config.GenerateConfig(config.CONFIG_FILENAME)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to generate config: %v", err)
|
log.Fatalf("Failed to generate config: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"New config file created (%s). " +
|
"New config file created (%s). " +
|
||||||
|
|
@ -135,7 +134,6 @@ func main() {
|
||||||
err = config.WriteConfig(cfg, config.CONFIG_FILENAME)
|
err = config.WriteConfig(cfg, config.CONFIG_FILENAME)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to write config: %v", err)
|
log.Fatalf("Failed to write config: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
log.Println("Logged out successfully.")
|
log.Println("Logged out successfully.")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|
@ -146,21 +144,18 @@ func main() {
|
||||||
err = scanner.InitialiseDirectory(directory)
|
err = scanner.InitialiseDirectory(directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to initialise directory: %v", err)
|
log.Fatalf("Failed to initialise directory: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
log.Printf(
|
log.Fatalf(
|
||||||
"Directory successfully initialised. " +
|
"Directory successfully initialised. " +
|
||||||
"Be sure to update %s before uploading!",
|
"Be sure to update %s before uploading!",
|
||||||
scanner.METADATA_FILENAME,
|
scanner.METADATA_FILENAME,
|
||||||
)
|
)
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// good to have early on
|
// good to have early on
|
||||||
templates, err := yt.FetchTemplates(path.Join(userConfigDir, "vodular", "templates"))
|
templates, err := yt.FetchTemplates(path.Join(userConfigDir, "vodular", "templates"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to fetch templates: %v", err)
|
log.Fatalf("Failed to fetch templates: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// read directory metadata
|
// read directory metadata
|
||||||
|
|
@ -168,27 +163,14 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
log.Fatalf("Directory does not exist: %s", directory)
|
log.Fatalf("Directory does not exist: %s", directory)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
log.Fatalf("Failed to fetch VOD metadata: %v", err)
|
log.Fatalf("Failed to fetch VOD metadata: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if metadata == nil {
|
if metadata == nil {
|
||||||
log.Fatal(
|
log.Fatal(
|
||||||
"Directory contained no metadata. " +
|
"Directory contained no metadata. " +
|
||||||
"Use `--init` to initialise this directory.",
|
"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
|
// default footage directory
|
||||||
|
|
@ -201,14 +183,12 @@ func main() {
|
||||||
vodFiles, err := scanner.ScanSegments(footageDir, SEGMENT_EXTENSION)
|
vodFiles, err := scanner.ScanSegments(footageDir, SEGMENT_EXTENSION)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to fetch VOD filenames: %v", err)
|
log.Fatalf("Failed to fetch VOD filenames: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if len(vodFiles) == 0 {
|
if len(vodFiles) == 0 {
|
||||||
log.Fatalf(
|
log.Fatalf(
|
||||||
"Directory contained no VOD files (expecting .%s)",
|
"Directory contained no VOD files (expecting .%s)",
|
||||||
SEGMENT_EXTENSION,
|
SEGMENT_EXTENSION,
|
||||||
)
|
)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if verbose {
|
if verbose {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
|
@ -220,39 +200,35 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// scan for thumbnail
|
// scan for thumbnail
|
||||||
var thumbnail *yt.Thumbnail
|
var thumbnail *yt.ThumbnailMetadata
|
||||||
thumbnailPath, thumbnailSizeBytes, err := scanner.ScanThumbnail(directory)
|
thumbnailPath, thumbnailSizeBytes, err := scanner.ScanThumbnail(directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to fetch thumbnail: %v", err)
|
log.Fatalf("Failed to fetch thumbnail: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
} else {
|
} else {
|
||||||
thumbnail = &yt.Thumbnail{
|
thumbnail = &yt.ThumbnailMetadata{
|
||||||
Filepath: thumbnailPath,
|
Filepath: thumbnailPath,
|
||||||
SizeBytes: thumbnailSizeBytes,
|
SizeBytes: thumbnailSizeBytes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// build video template for upload
|
// build video template for upload
|
||||||
video, err := yt.BuildVideo(metadata)
|
videoMeta, err := yt.BuildVideo(metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to build video template: %v", err)
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("Failed to build video title: %v", err)
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("Failed to build video description: %v", err)
|
log.Fatalf("Failed to build video description: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if len(title) > 100 {
|
if len(title) > 100 {
|
||||||
log.Fatalf(
|
log.Fatalf(
|
||||||
"Video title length exceeds %d characters (%d). YouTube may reject this!",
|
"Video title length exceeds %d characters (%d). YouTube may reject this!",
|
||||||
MAX_TITLE_LEN,
|
MAX_TITLE_LEN,
|
||||||
len(video.Title),
|
len(videoMeta.Title),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if len(description) > 5000 {
|
if len(description) > 5000 {
|
||||||
|
|
@ -265,7 +241,7 @@ func main() {
|
||||||
if verbose {
|
if verbose {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
fmt.Printf("\nVideo template: ")
|
fmt.Printf("\nVideo template: ")
|
||||||
enc.Encode(video)
|
enc.Encode(videoMeta)
|
||||||
|
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"\n================================\n\n" +
|
"\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
|
// check if full VOD already exists with expected duration
|
||||||
fullVodExists := false
|
fullVodExists := false
|
||||||
fullVodProbe, err := scanner.ProbeSegment(video.Filepath)
|
fullVodProbe, err := scanner.ProbeSegment(videoMeta.Filepath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
video.SizeBytes = fullVodProbe.Format.Size
|
videoMeta.SizeBytes = fullVodProbe.Format.Size
|
||||||
var totalLength float64 = 0
|
var totalLength float64 = 0
|
||||||
|
|
||||||
for _, filename := range vodFiles {
|
for _, filename := range vodFiles {
|
||||||
|
|
@ -297,53 +346,20 @@ func main() {
|
||||||
log.Print("Full VOD appears to already exist- uploading this file...")
|
log.Print("Full VOD appears to already exist- uploading this file...")
|
||||||
} else {
|
} else {
|
||||||
// concatenate VOD segments into full VOD
|
// 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 {
|
if err != nil {
|
||||||
log.Fatalf("Failed to concatenate VOD segments: %v", err)
|
log.Fatalf("Failed to concatenate VOD segments: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Print("Dry run: Skipping VOD concatenation")
|
log.Print("Dry run: Skipping VOD concatenation")
|
||||||
}
|
}
|
||||||
|
|
||||||
// youtube oauth flow
|
if !dryRun {
|
||||||
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) {
|
|
||||||
// okay actually upload now!
|
// 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 {
|
if err != nil {
|
||||||
log.Fatalf("Failed to upload video: %v", err)
|
log.Fatalf("Failed to upload video: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if verbose {
|
if verbose {
|
||||||
jsonString, err := json.MarshalIndent(ytVideo, "", " ")
|
jsonString, err := json.MarshalIndent(ytVideo, "", " ")
|
||||||
|
|
@ -354,9 +370,14 @@ func main() {
|
||||||
}
|
}
|
||||||
log.Print("Video uploaded successfully!")
|
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
|
// update metadata to reflect VOD is uploaded
|
||||||
// TODO: rather than a boolean flag, link to actual video
|
metadata.UploadURL = yt.VIDEO_URL_BASE + ytVideo.Id
|
||||||
metadata.Uploaded = true
|
|
||||||
err = scanner.WriteMetadata(directory, metadata)
|
err = scanner.WriteMetadata(directory, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to update metadata: %v", err)
|
log.Fatalf("Failed to update metadata: %v", err)
|
||||||
|
|
@ -364,7 +385,7 @@ func main() {
|
||||||
|
|
||||||
// delete full VOD after upload, if requested
|
// delete full VOD after upload, if requested
|
||||||
if deleteFullVod {
|
if deleteFullVod {
|
||||||
err = os.Remove(video.Filepath)
|
err = os.Remove(videoMeta.Filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to delete full VOD: %v", err)
|
log.Fatalf("Failed to delete full VOD: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ type (
|
||||||
Date toml.LocalDate `toml:"date"`
|
Date toml.LocalDate `toml:"date"`
|
||||||
Tags []string `toml:"tags"`
|
Tags []string `toml:"tags"`
|
||||||
FootageDir string `toml:"footage_dir"`
|
FootageDir string `toml:"footage_dir"`
|
||||||
Uploaded bool `toml:"uploaded"`
|
Uploaded bool `toml:"uploaded"` // deprecated, held for backwards compatibility
|
||||||
|
UploadURL string `toml:"upload_url"`
|
||||||
Category *Category `toml:"category" comment:"(Optional) Category details, for additional credits."`
|
Category *Category `toml:"category" comment:"(Optional) Category details, for additional credits."`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, error) {
|
func ConcatVideo(video *youtube.VideoMetadata, vodFiles []string, verbose bool) (int64, error) {
|
||||||
fileListPath := path.Join(
|
fileListPath := path.Join(
|
||||||
path.Dir(video.Filepath),
|
path.Dir(video.Filepath),
|
||||||
"files.txt",
|
"files.txt",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package youtube
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -13,7 +14,6 @@ import (
|
||||||
|
|
||||||
"arimelody.space/vodular/scanner"
|
"arimelody.space/vodular/scanner"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"google.golang.org/api/option"
|
|
||||||
"google.golang.org/api/youtube/v3"
|
"google.golang.org/api/youtube/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ type (
|
||||||
Url string
|
Url string
|
||||||
}
|
}
|
||||||
|
|
||||||
Video struct {
|
VideoMetadata struct {
|
||||||
Title string
|
Title string
|
||||||
Category *Category
|
Category *Category
|
||||||
Part int
|
Part int
|
||||||
|
|
@ -45,13 +45,15 @@ type (
|
||||||
SizeBytes int64
|
SizeBytes int64
|
||||||
}
|
}
|
||||||
|
|
||||||
Thumbnail struct {
|
ThumbnailMetadata struct {
|
||||||
Filepath string
|
Filepath string
|
||||||
SizeBytes int64
|
SizeBytes int64
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func BuildVideo(metadata *scanner.Metadata) (*Video, error) {
|
const VIDEO_URL_BASE string = "https://www.youtube.com/watch?v="
|
||||||
|
|
||||||
|
func BuildVideo(metadata *scanner.Metadata) (*VideoMetadata, error) {
|
||||||
var category *Category = nil
|
var category *Category = nil
|
||||||
if metadata.Category != nil {
|
if metadata.Category != nil {
|
||||||
category = &Category{
|
category = &Category{
|
||||||
|
|
@ -64,7 +66,7 @@ func BuildVideo(metadata *scanner.Metadata) (*Video, error) {
|
||||||
if !ok { category.Type = CATEGORY_ENTERTAINMENT }
|
if !ok { category.Type = CATEGORY_ENTERTAINMENT }
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Video{
|
return &VideoMetadata{
|
||||||
Title: metadata.Title,
|
Title: metadata.Title,
|
||||||
Category: category,
|
Category: category,
|
||||||
Part: metadata.Part,
|
Part: metadata.Part,
|
||||||
|
|
@ -155,7 +157,7 @@ func FetchTemplates(templateDir string) (*Template, error) {
|
||||||
if titleFile, err := os.ReadFile(titlePath); err == nil {
|
if titleFile, err := os.ReadFile(titlePath); err == nil {
|
||||||
tmpl.Title, err = titleTemplate.Parse(string(titleFile))
|
tmpl.Title, err = titleTemplate.Parse(string(titleFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse title template: %v", err)
|
return nil, fmt.Errorf("parse title template: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !os.IsNotExist(err) { return nil, err }
|
if !os.IsNotExist(err) { return nil, err }
|
||||||
|
|
@ -176,7 +178,7 @@ func FetchTemplates(templateDir string) (*Template, error) {
|
||||||
if descriptionFile, err := os.ReadFile(descriptionPath); err == nil {
|
if descriptionFile, err := os.ReadFile(descriptionPath); err == nil {
|
||||||
tmpl.Description, err = descriptionTemplate.Parse(string(descriptionFile))
|
tmpl.Description, err = descriptionTemplate.Parse(string(descriptionFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse description template: %v", err)
|
return nil, fmt.Errorf("parse description template: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !os.IsNotExist(err) { return nil, err }
|
if !os.IsNotExist(err) { return nil, err }
|
||||||
|
|
@ -195,7 +197,7 @@ func FetchTemplates(templateDir string) (*Template, error) {
|
||||||
return &tmpl, nil
|
return &tmpl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildTemplate(video *Video, tmpl *template.Template) (string, error) {
|
func BuildTemplate(video *VideoMetadata, tmpl *template.Template) (string, error) {
|
||||||
out := &bytes.Buffer{}
|
out := &bytes.Buffer{}
|
||||||
|
|
||||||
var category *MetaCategory
|
var category *MetaCategory
|
||||||
|
|
@ -220,26 +222,18 @@ func BuildTemplate(video *Video, tmpl *template.Template) (string, error) {
|
||||||
func UploadVideo(
|
func UploadVideo(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tokenSource oauth2.TokenSource,
|
tokenSource oauth2.TokenSource,
|
||||||
video *Video,
|
service *youtube.Service,
|
||||||
thumbnail *Thumbnail,
|
video *VideoMetadata,
|
||||||
templates *Template,
|
templates *Template,
|
||||||
) (*youtube.Video, error) {
|
) (*youtube.Video, error) {
|
||||||
|
if templates == nil { return nil, fmt.Errorf("templates cannot be nil") }
|
||||||
|
if templates.Title == nil { return nil, fmt.Errorf("title template cannot be nil") }
|
||||||
|
if templates.Description == nil { return nil, fmt.Errorf("description template cannot be nil") }
|
||||||
|
|
||||||
title, err := BuildTemplate(video, templates.Title)
|
title, err := BuildTemplate(video, templates.Title)
|
||||||
if err != nil { return nil, fmt.Errorf("failed to build title: %v", err) }
|
if err != nil { return nil, fmt.Errorf("build title: %v", err) }
|
||||||
description, err := BuildTemplate(video, templates.Description)
|
description, err := BuildTemplate(video, templates.Description)
|
||||||
if err != nil { return nil, fmt.Errorf("failed to build description: %v", err) }
|
if err != nil { return nil, fmt.Errorf("build description: %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)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// upload video
|
|
||||||
|
|
||||||
videoService := youtube.NewVideosService(service)
|
videoService := youtube.NewVideosService(service)
|
||||||
|
|
||||||
|
|
@ -263,6 +257,7 @@ func UploadVideo(
|
||||||
PrivacyStatus: "private",
|
PrivacyStatus: "private",
|
||||||
},
|
},
|
||||||
}).NotifySubscribers(false)
|
}).NotifySubscribers(false)
|
||||||
|
videoInsertCall.Context(ctx)
|
||||||
|
|
||||||
videoFile, err := os.Open(video.Filepath)
|
videoFile, err := os.Open(video.Filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -285,30 +280,81 @@ func UploadVideo(
|
||||||
return nil, 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
|
return ytVideo, err
|
||||||
}
|
}
|
||||||
thumbnailSetCall.Media(thumbnailFile)
|
|
||||||
|
|
||||||
log.Println("Uploading thumbnail...")
|
func UpdateMetadata(
|
||||||
|
ctx context.Context,
|
||||||
|
tokenSource oauth2.TokenSource,
|
||||||
|
service *youtube.Service,
|
||||||
|
metadata *VideoMetadata,
|
||||||
|
videoId string,
|
||||||
|
templates *Template,
|
||||||
|
) error {
|
||||||
|
if templates == nil { return fmt.Errorf("templates cannot be nil") }
|
||||||
|
if templates.Title == nil { return fmt.Errorf("title template cannot be nil") }
|
||||||
|
if templates.Description == nil { return fmt.Errorf("description template cannot be nil") }
|
||||||
|
|
||||||
|
metadataService := youtube.NewVideosService(service)
|
||||||
|
videoFetchCall := metadataService.List([]string{"id", "snippet"})
|
||||||
|
videoFetchCall.Context(ctx)
|
||||||
|
videoFetchCall.Id(videoId)
|
||||||
|
videoList, err := videoFetchCall.Do()
|
||||||
|
if err != nil { return fmt.Errorf("fetch video: %v", err) }
|
||||||
|
if len(videoList.Items) == 0 { return errors.New("video not found") }
|
||||||
|
|
||||||
|
var video *youtube.Video
|
||||||
|
for _, v := range videoList.Items {
|
||||||
|
if v.Id == videoId {
|
||||||
|
video = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if video == nil { return errors.New("Could not find video. Was it deleted?") }
|
||||||
|
if video.Snippet == nil { return errors.New("video snippet is nil") }
|
||||||
|
|
||||||
|
video.Snippet.Title, err = BuildTemplate(metadata, templates.Title)
|
||||||
|
if err != nil { return fmt.Errorf("build title: %v", err) }
|
||||||
|
video.Snippet.Description, err = BuildTemplate(metadata, templates.Description)
|
||||||
|
if err != nil { return fmt.Errorf("build description: %v", err) }
|
||||||
|
video.Snippet.Tags = metadata.Tags
|
||||||
|
video.Snippet.CategoryId = videoYtCategory[metadata.Category.Type]
|
||||||
|
|
||||||
|
metadataSetCall := metadataService.Update([]string{"snippet"}, video)
|
||||||
|
metadataSetCall.Context(ctx)
|
||||||
|
if video, err = metadataSetCall.Do(); err != nil {
|
||||||
|
return fmt.Errorf("set metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadThumbnail(
|
||||||
|
ctx context.Context,
|
||||||
|
tokenSource oauth2.TokenSource,
|
||||||
|
service *youtube.Service,
|
||||||
|
videoId string,
|
||||||
|
thumbnail *ThumbnailMetadata,
|
||||||
|
) error {
|
||||||
|
if thumbnail == nil {
|
||||||
|
return fmt.Errorf("thumbnail cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnailService := youtube.NewThumbnailsService(service)
|
||||||
|
thumbnailSetCall := thumbnailService.Set(videoId)
|
||||||
|
thumbnailSetCall.Context(ctx)
|
||||||
|
thumbnailFile, err := os.Open(thumbnail.Filepath)
|
||||||
|
if err != nil { return fmt.Errorf("open thumbnail: %v", err) }
|
||||||
|
thumbnailSetCall.Media(thumbnailFile)
|
||||||
|
|
||||||
thumbnailSetCall.ProgressUpdater(func(current, total int64) {
|
thumbnailSetCall.ProgressUpdater(func(current, total int64) {
|
||||||
if total == 0 { total = thumbnail.SizeBytes }
|
if total == 0 { total = thumbnail.SizeBytes }
|
||||||
fmt.Printf("\t(%.2f%%)\n", float64(current) / float64(total) * 100)
|
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()
|
_, err = thumbnailSetCall.Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to upload thumbnail: %v\n", err)
|
log.Printf("Failed to upload thumbnail: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return ytVideo, err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue