236 lines
5.2 KiB
Go
236 lines
5.2 KiB
Go
package youtube
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"arimelody.space/live-vod-uploader/scanner"
|
|
"golang.org/x/oauth2"
|
|
"google.golang.org/api/option"
|
|
"google.golang.org/api/youtube/v3"
|
|
)
|
|
|
|
const (
|
|
YT_CATEGORY_GAMING = "20"
|
|
YT_CATEGORY_ENTERTAINMENT = "24"
|
|
)
|
|
|
|
type CategoryType int
|
|
const (
|
|
CATEGORY_GAME CategoryType = iota
|
|
CATEGORY_ENTERTAINMENT
|
|
)
|
|
|
|
type (
|
|
Category struct {
|
|
Name string
|
|
Type CategoryType
|
|
Url string
|
|
}
|
|
|
|
Video struct {
|
|
Title string
|
|
Category *Category
|
|
Part int
|
|
Date time.Time
|
|
Tags []string
|
|
Filename string
|
|
SizeBytes int64
|
|
}
|
|
)
|
|
|
|
func BuildVideo(metadata *scanner.Metadata) (*Video, error) {
|
|
videoDate, err := time.Parse("2006-01-02", metadata.Date)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse date from metadata: %v", err)
|
|
}
|
|
|
|
var category *Category = nil
|
|
if metadata.Category != nil {
|
|
category = &Category{
|
|
Name: metadata.Category.Name,
|
|
Type: CATEGORY_ENTERTAINMENT,
|
|
Url: metadata.Category.Url,
|
|
}
|
|
var ok bool
|
|
category.Type, ok = videoCategoryStringTypes[metadata.Category.Type]
|
|
if !ok { category.Type = CATEGORY_ENTERTAINMENT }
|
|
}
|
|
|
|
return &Video{
|
|
Title: metadata.Title,
|
|
Category: category,
|
|
Part: metadata.Part,
|
|
Date: videoDate,
|
|
Tags: DefaultTags,
|
|
Filename: path.Join(
|
|
metadata.FootageDir,
|
|
fmt.Sprintf(
|
|
"%s-fullvod.mkv",
|
|
videoDate.Format("2006-01-02"),
|
|
)),
|
|
}, nil
|
|
}
|
|
|
|
type (
|
|
MetaCategory struct {
|
|
Name string
|
|
Type string
|
|
Url string
|
|
}
|
|
|
|
Metadata struct {
|
|
Title string
|
|
Date string
|
|
Category *MetaCategory
|
|
Part int
|
|
}
|
|
)
|
|
|
|
var videoCategoryTypeStrings = map[CategoryType]string{
|
|
CATEGORY_GAME: "gaming",
|
|
CATEGORY_ENTERTAINMENT: "entertainment",
|
|
}
|
|
var videoCategoryStringTypes = map[string]CategoryType{
|
|
"gaming": CATEGORY_GAME,
|
|
"entertainment": CATEGORY_ENTERTAINMENT,
|
|
}
|
|
var videoYtCategory = map[CategoryType]string {
|
|
CATEGORY_GAME: YT_CATEGORY_GAMING,
|
|
CATEGORY_ENTERTAINMENT: YT_CATEGORY_ENTERTAINMENT,
|
|
}
|
|
|
|
var DefaultTags []string
|
|
func GetDefaultTags(filepath string) ([]string, error) {
|
|
file, err := os.ReadFile(filepath)
|
|
if err != nil { return nil, err }
|
|
|
|
tags := strings.Split(string(file), "\n")
|
|
return tags, nil
|
|
}
|
|
|
|
var titleTemplate *template.Template = template.Must(
|
|
template.ParseFiles("template/title.txt"),
|
|
)
|
|
func BuildTitle(video *Video) (string, error) {
|
|
out := &bytes.Buffer{}
|
|
|
|
var category *MetaCategory
|
|
if video.Category != nil {
|
|
category = &MetaCategory{
|
|
Name: video.Category.Name,
|
|
Type: videoCategoryTypeStrings[video.Category.Type],
|
|
Url: video.Category.Url,
|
|
}
|
|
}
|
|
|
|
// TODO: give templates date format and lowercase functions
|
|
// these should not be hard-coded!
|
|
err := titleTemplate.Execute(out, Metadata{
|
|
Title: video.Title,
|
|
Date: strings.ToLower(video.Date.Format("02 Jan 2006")),
|
|
Category: category,
|
|
Part: video.Part,
|
|
})
|
|
|
|
return strings.TrimSpace(out.String()), err
|
|
}
|
|
|
|
var descriptionTemplate *template.Template = template.Must(
|
|
template.ParseFiles("template/description.txt"),
|
|
)
|
|
func BuildDescription(video *Video) (string, error) {
|
|
out := &bytes.Buffer{}
|
|
|
|
var category *MetaCategory
|
|
if video.Category != nil {
|
|
category = &MetaCategory{
|
|
Name: video.Category.Name,
|
|
Type: videoCategoryTypeStrings[video.Category.Type],
|
|
Url: video.Category.Url,
|
|
}
|
|
}
|
|
|
|
err := descriptionTemplate.Execute(out, Metadata{
|
|
Title: video.Title,
|
|
Date: strings.ToLower(video.Date.Format("02 Jan 2006")),
|
|
Category: category,
|
|
Part: video.Part,
|
|
})
|
|
|
|
return strings.TrimSpace(out.String()), err
|
|
}
|
|
|
|
func UploadVideo(ctx context.Context, token *oauth2.Token, video *Video) (*youtube.Video, error) {
|
|
title, err := BuildTitle(video)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build title: %v", err)
|
|
}
|
|
description, err := BuildDescription(video)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build description: %v", err)
|
|
}
|
|
|
|
service, err := youtube.NewService(
|
|
ctx,
|
|
option.WithScopes(youtube.YoutubeUploadScope),
|
|
option.WithTokenSource(oauth2.StaticTokenSource(token)),
|
|
)
|
|
if err != nil {
|
|
log.Fatalf("Failed to create youtube service: %v\n", err)
|
|
return nil, err
|
|
}
|
|
|
|
videoService := youtube.NewVideosService(service)
|
|
|
|
categoryId := YT_CATEGORY_ENTERTAINMENT
|
|
if video.Category != nil {
|
|
if cid, ok := videoYtCategory[video.Category.Type]; ok {
|
|
categoryId = cid
|
|
}
|
|
}
|
|
|
|
call := videoService.Insert([]string{
|
|
"snippet", "status",
|
|
}, &youtube.Video{
|
|
Snippet: &youtube.VideoSnippet{
|
|
Title: title,
|
|
Description: description,
|
|
Tags: append(DefaultTags, video.Tags...),
|
|
CategoryId: categoryId, // gaming
|
|
},
|
|
Status: &youtube.VideoStatus{
|
|
PrivacyStatus: "private",
|
|
},
|
|
}).NotifySubscribers(false)
|
|
|
|
file, err := os.Open(video.Filename)
|
|
if err != nil {
|
|
log.Fatalf("Failed to open file: %v\n", err)
|
|
return nil, err
|
|
}
|
|
call.Media(file)
|
|
|
|
log.Println("Uploading video...")
|
|
|
|
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("Uploading... (%.2f%%)\n", float64(current) / float64(total) * 100)
|
|
})
|
|
ytVideo, err := call.Do()
|
|
if err != nil {
|
|
log.Fatalf("Failed to upload video: %v\n", err)
|
|
return nil, err
|
|
}
|
|
|
|
return ytVideo, err
|
|
}
|