184 lines
4.7 KiB
Go
184 lines
4.7 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"path"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/cheggaaa/pb/v3"
|
||
|
"github.com/pkg/errors"
|
||
|
"github.com/sirupsen/logrus"
|
||
|
"golang.org/x/oauth2"
|
||
|
"golang.org/x/oauth2/google"
|
||
|
"google.golang.org/api/option"
|
||
|
"google.golang.org/api/youtube/v3"
|
||
|
|
||
|
"github.com/Luzifer/rconfig/v2"
|
||
|
)
|
||
|
|
||
|
const defaultDescription = `Twitch: https://twitch.tv/luziferus
|
||
|
|
||
|
Chapters:
|
||
|
0:00:00 Game1`
|
||
|
|
||
|
var (
|
||
|
cfg = struct {
|
||
|
Channel string `flag:"channel" default:"UCjsRmaAQ0IHR2CNEBqfNOSQ" description:"ID of the channel to assign the video to"`
|
||
|
Code string `flag:"code" default:"" description:"Auth-Code to create the token from"`
|
||
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||
|
VaultKey string `flag:"vault-key" default:"secret/vod2yt" description:"Where to find / store oauth data"`
|
||
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||
|
}{}
|
||
|
|
||
|
version = "dev"
|
||
|
)
|
||
|
|
||
|
func initApp() error {
|
||
|
rconfig.AutoEnv(true)
|
||
|
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
||
|
return errors.Wrap(err, "parsing cli options")
|
||
|
}
|
||
|
|
||
|
l, err := logrus.ParseLevel(cfg.LogLevel)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "parsing log-level")
|
||
|
}
|
||
|
logrus.SetLevel(l)
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func main() {
|
||
|
var err error
|
||
|
if err = initApp(); err != nil {
|
||
|
logrus.WithError(err).Fatal("initializing app")
|
||
|
}
|
||
|
|
||
|
if cfg.VersionAndExit {
|
||
|
logrus.WithField("version", version).Info("vod2yt")
|
||
|
os.Exit(0)
|
||
|
}
|
||
|
|
||
|
if len(rconfig.Args()) < 2 { //nolint:gomnd
|
||
|
logrus.Fatal("Usage: vod2yt <filename>")
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
ctx = context.Background()
|
||
|
fileName = rconfig.Args()[1]
|
||
|
)
|
||
|
|
||
|
client, ts, err := oauth(ctx)
|
||
|
if err != nil {
|
||
|
logrus.WithError(err).Fatal("getting oauth credentials")
|
||
|
}
|
||
|
|
||
|
defer func() {
|
||
|
t, err := ts.Token()
|
||
|
if err != nil {
|
||
|
logrus.WithError(err).Fatal("getting token to store back")
|
||
|
}
|
||
|
|
||
|
if err := saveTokenToVault(t); err != nil {
|
||
|
logrus.WithError(err).Fatal("storing token back to vault")
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
service, err := youtube.NewService(ctx, option.WithHTTPClient(client))
|
||
|
if err != nil {
|
||
|
logrus.WithError(err).Fatal("creating YouTube client")
|
||
|
}
|
||
|
|
||
|
upload := &youtube.Video{
|
||
|
Snippet: &youtube.VideoSnippet{
|
||
|
CategoryId: "20", // Gaming
|
||
|
ChannelId: cfg.Channel,
|
||
|
DefaultAudioLanguage: "de",
|
||
|
DefaultLanguage: "de",
|
||
|
Description: defaultDescription,
|
||
|
LiveBroadcastContent: "none",
|
||
|
Title: strings.ReplaceAll(path.Base(fileName), path.Ext(fileName), ""),
|
||
|
},
|
||
|
Status: &youtube.VideoStatus{
|
||
|
Embeddable: true,
|
||
|
License: "youtube",
|
||
|
MadeForKids: false,
|
||
|
PrivacyStatus: "private",
|
||
|
PublicStatsViewable: true,
|
||
|
},
|
||
|
RecordingDetails: &youtube.VideoRecordingDetails{
|
||
|
RecordingDate: time.Now().Format(time.RFC3339),
|
||
|
},
|
||
|
}
|
||
|
|
||
|
mediaFileStat, err := os.Stat(fileName)
|
||
|
if err != nil {
|
||
|
logrus.WithError(err).Fatal("getting media file stat")
|
||
|
}
|
||
|
|
||
|
mediaFile, err := os.Open(fileName) //#nosec:G304 // Intended to upload arbitrary file
|
||
|
if err != nil {
|
||
|
logrus.WithError(err).Fatal("opening media file")
|
||
|
}
|
||
|
defer mediaFile.Close() //nolint:errcheck // File will be closed by program exit
|
||
|
|
||
|
bar := pb.Full.Start64(mediaFileStat.Size())
|
||
|
progressReader := bar.NewProxyReader(mediaFile)
|
||
|
|
||
|
resp, err := service.Videos.
|
||
|
Insert([]string{"snippet", "status", "recordingDetails"}, upload).
|
||
|
Media(progressReader).
|
||
|
Do()
|
||
|
if err != nil {
|
||
|
logrus.WithError(err).Fatal("inserting video")
|
||
|
}
|
||
|
|
||
|
bar.Finish()
|
||
|
|
||
|
logrus.WithField("video-id", resp.Id).Info("video uploaded")
|
||
|
}
|
||
|
|
||
|
func oauth(ctx context.Context) (client *http.Client, ts oauth2.TokenSource, err error) {
|
||
|
clientID, clientSecret, err := loadClientDetailsFromVault()
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("loading client details: %w", err)
|
||
|
}
|
||
|
|
||
|
c := &oauth2.Config{
|
||
|
ClientID: clientID,
|
||
|
ClientSecret: clientSecret,
|
||
|
Endpoint: google.Endpoint,
|
||
|
RedirectURL: "http://127.0.0.1:8000",
|
||
|
Scopes: []string{youtube.YoutubeUploadScope},
|
||
|
}
|
||
|
|
||
|
t, err := loadTokenFromVault()
|
||
|
|
||
|
switch {
|
||
|
case err == nil:
|
||
|
// We've successfully loaded a token we can use below
|
||
|
|
||
|
case cfg.Code != "":
|
||
|
// We got an auth-code to exchange for a token
|
||
|
t, err = c.Exchange(ctx, cfg.Code)
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("exchanging token: %w", err)
|
||
|
}
|
||
|
|
||
|
if err = saveTokenToVault(t); err != nil {
|
||
|
return nil, nil, fmt.Errorf("storing initial token: %w", err)
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
logrus.WithError(err).Debug("token load error")
|
||
|
return nil, nil, fmt.Errorf("no token / code available, auth using following URL and restart with --code: %s", c.AuthCodeURL("offline", oauth2.AccessTypeOffline))
|
||
|
}
|
||
|
|
||
|
ts = c.TokenSource(ctx, t)
|
||
|
return oauth2.NewClient(ctx, ts), ts, nil
|
||
|
}
|