Compare commits
2 commits
335802a3d7
...
0b11c9d450
Author | SHA1 | Date | |
---|---|---|---|
0b11c9d450 | |||
c331f549fa |
6 changed files with 91 additions and 25 deletions
32
README.md
Normal file
32
README.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Luzifer / publish-vod
|
||||||
|
|
||||||
|
Imagine having multiple archives for your VoDs. Imagine uploading the VoD to each of them manually. Eventually you write a script doing the legwork but that script only uploads sequentially and you always need to edit the script.
|
||||||
|
|
||||||
|
Or you create a config for this tool and start one command which then concurrently uploads the VoD to all the configured targets.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
|
||||||
|
uploaders:
|
||||||
|
|
||||||
|
- name: Example SFTP
|
||||||
|
type: sftp
|
||||||
|
settings:
|
||||||
|
host: hostname:port
|
||||||
|
path: /where/to/put/the/file
|
||||||
|
user: someoneimportant
|
||||||
|
|
||||||
|
- name: Example Youtube
|
||||||
|
type: youtube
|
||||||
|
settings:
|
||||||
|
channel: the ID of your channel found in its link
|
||||||
|
description: |
|
||||||
|
What to put in the description field
|
||||||
|
vaultKey: secret/youtube-creds
|
||||||
|
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The `youtube` uploader relies on [Vault](https://www.vaultproject.io/) to store the oauth-token and the client credentials. Create a Google Cloud Platform project, enable the Youtube Data API, create a new application ("OAuth 2.0 Client IDs") with Type "Desktop App", put the client ID and client secret into the keys `clientId` and `clientSecret` in that Vault key. Run `publish-vod --validate-only` to authorize the application. This will automatically store the token into the Vault key.
|
49
main.go
49
main.go
|
@ -20,6 +20,7 @@ var (
|
||||||
cfg = struct {
|
cfg = struct {
|
||||||
Config string `flag:"config,c" default:"config.yaml" description:"Configuration for the uploaders"`
|
Config string `flag:"config,c" default:"config.yaml" description:"Configuration for the uploaders"`
|
||||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||||
|
ValidateOnly bool `flag:"validate-only" default:"false" description:"Only execute validation and prepare functions, do not upload"`
|
||||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
|
@ -52,32 +53,16 @@ func main() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rconfig.Args()) < 2 { //nolint:gomnd
|
|
||||||
logrus.Fatal("Usage: publish-vod <filename>")
|
|
||||||
}
|
|
||||||
|
|
||||||
configFile, err := config.Load(cfg.Config)
|
configFile, err := config.Load(cfg.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("loading config")
|
logrus.WithError(err).Fatal("loading config")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
fileName = rconfig.Args()[1]
|
wg sync.WaitGroup
|
||||||
wg sync.WaitGroup
|
|
||||||
)
|
)
|
||||||
|
|
||||||
f, err := os.Open(fileName)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("opening VoD")
|
|
||||||
}
|
|
||||||
defer f.Close() //nolint:errcheck // File is closed by process exit
|
|
||||||
|
|
||||||
stat, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("getting VoD stat")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
barPool = pb.NewPool()
|
barPool = pb.NewPool()
|
||||||
longestPrefix int
|
longestPrefix int
|
||||||
|
@ -88,15 +73,43 @@ func main() {
|
||||||
if u == nil {
|
if u == nil {
|
||||||
logrus.Fatalf("unknown uploader %q", c.Type)
|
logrus.Fatalf("unknown uploader %q", c.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = u.ValidateConfig(c.Settings); err != nil {
|
if err = u.ValidateConfig(c.Settings); err != nil {
|
||||||
logrus.WithError(err).Fatalf("validating config entry %q", c.Name)
|
logrus.WithError(err).Fatalf("validating config entry %q", c.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = u.Prepare(ctx, uploader.UploaderOpts{
|
||||||
|
Name: c.Name,
|
||||||
|
Config: c.Settings,
|
||||||
|
}); err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("preparing uploader %q", c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
if l := len(c.Name); l > longestPrefix {
|
if l := len(c.Name); l > longestPrefix {
|
||||||
longestPrefix = l
|
longestPrefix = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.ValidateOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rconfig.Args()) < 2 { //nolint:gomnd
|
||||||
|
logrus.Fatal("Usage: publish-vod <filename>")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := rconfig.Args()[1]
|
||||||
|
f, err := os.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("opening VoD")
|
||||||
|
}
|
||||||
|
defer f.Close() //nolint:errcheck // File is closed by process exit
|
||||||
|
|
||||||
|
stat, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("getting VoD stat")
|
||||||
|
}
|
||||||
|
|
||||||
for i := range configFile.Uploaders {
|
for i := range configFile.Uploaders {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Uploader interface {
|
Uploader interface {
|
||||||
|
Prepare(ctx context.Context, opts UploaderOpts) error
|
||||||
UploadFile(ctx context.Context, opts UploaderOpts) error
|
UploadFile(ctx context.Context, opts UploaderOpts) error
|
||||||
ValidateConfig(config *fieldcollection.FieldCollection) error
|
ValidateConfig(config *fieldcollection.FieldCollection) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ var (
|
||||||
_ uploader.Uploader = Uploader{}
|
_ uploader.Uploader = Uploader{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (Uploader) Prepare(context.Context, uploader.UploaderOpts) error { return nil }
|
||||||
|
|
||||||
func (Uploader) UploadFile(ctx context.Context, opts uploader.UploaderOpts) error {
|
func (Uploader) UploadFile(ctx context.Context, opts uploader.UploaderOpts) error {
|
||||||
socket := os.Getenv("SSH_AUTH_SOCK")
|
socket := os.Getenv("SSH_AUTH_SOCK")
|
||||||
conn, err := net.Dial("unix", socket)
|
conn, err := net.Dial("unix", socket)
|
||||||
|
|
|
@ -27,26 +27,44 @@ var (
|
||||||
_ uploader.Uploader = Uploader{}
|
_ uploader.Uploader = Uploader{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (Uploader) Prepare(ctx context.Context, opts uploader.UploaderOpts) error {
|
||||||
|
_, ts, err := oauth4youtube.GetOAuth2Client(ctx, opts.Config.MustString("vaultKey", nil))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting oauth credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := ts.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting token to store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := vaultoauth2.SaveTokenToVault(opts.Config.MustString("vaultKey", nil), t); err != nil {
|
||||||
|
return fmt.Errorf("storing token to vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (Uploader) UploadFile(ctx context.Context, opts uploader.UploaderOpts) error {
|
func (Uploader) UploadFile(ctx context.Context, opts uploader.UploaderOpts) error {
|
||||||
client, ts, err := oauth4youtube.GetOAuth2Client(ctx, opts.Config.MustString("vaultKey", nil))
|
client, ts, err := oauth4youtube.GetOAuth2Client(ctx, opts.Config.MustString("vaultKey", nil))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("getting oauth credentials")
|
return fmt.Errorf("getting oauth credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
t, err := ts.Token()
|
t, err := ts.Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("getting token to store back")
|
logrus.WithError(err).Error("getting token to store back")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := vaultoauth2.SaveTokenToVault(opts.Config.MustString("vaultKey", nil), t); err != nil {
|
if err := vaultoauth2.SaveTokenToVault(opts.Config.MustString("vaultKey", nil), t); err != nil {
|
||||||
logrus.WithError(err).Fatal("storing token back to vault")
|
logrus.WithError(err).Error("storing token back to vault")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
service, err := youtube.NewService(ctx, option.WithHTTPClient(client))
|
service, err := youtube.NewService(ctx, option.WithHTTPClient(client))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("creating YouTube client")
|
return fmt.Errorf("creating Youtube client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
upload := &youtube.Video{
|
upload := &youtube.Video{
|
||||||
|
@ -78,7 +96,7 @@ func (Uploader) UploadFile(ctx context.Context, opts uploader.UploaderOpts) erro
|
||||||
Media(progressReader).
|
Media(progressReader).
|
||||||
Do()
|
Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("inserting video")
|
return fmt.Errorf("inserting video: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.FinalMessage("Video uploaded: https://youtu.be/%s", resp.Id)
|
opts.FinalMessage("Video uploaded: https://youtu.be/%s", resp.Id)
|
||||||
|
|
|
@ -28,7 +28,7 @@ func GetOAuth2Client(ctx context.Context, vaultKey string) (client *http.Client,
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
Endpoint: google.Endpoint,
|
Endpoint: google.Endpoint,
|
||||||
RedirectURL: "http://127.0.0.1:8000",
|
RedirectURL: "http://127.0.0.1:65285",
|
||||||
Scopes: []string{youtube.YoutubeUploadScope},
|
Scopes: []string{youtube.YoutubeUploadScope},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ func GetOAuth2Client(ctx context.Context, vaultKey string) (client *http.Client,
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { codeChan <- r.URL.Query().Get("code") })
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { codeChan <- r.URL.Query().Get("code") })
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: ":8000",
|
Addr: ":65285",
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
ReadHeaderTimeout: time.Second,
|
ReadHeaderTimeout: time.Second,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue