first working version!
This commit is contained in:
parent
1bba7ef03d
commit
84de96df31
9 changed files with 689 additions and 80 deletions
273
main.go
273
main.go
|
|
@ -4,11 +4,19 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
toml "github.com/pelletier/go-toml/v2"
|
||||
"google.golang.org/api/option"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/youtube/v3"
|
||||
|
||||
"arimelody.space/live-vod-uploader/scanner"
|
||||
vid "arimelody.space/live-vod-uploader/video"
|
||||
yt "arimelody.space/live-vod-uploader/youtube"
|
||||
)
|
||||
|
||||
type (
|
||||
|
|
@ -23,99 +31,210 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
var DEFAULT_TAGS = []string{
|
||||
"ari melody",
|
||||
"ari melody LIVE",
|
||||
"livestream",
|
||||
"vtuber",
|
||||
"twitch",
|
||||
"gaming",
|
||||
"let's play",
|
||||
"full VOD",
|
||||
"VOD",
|
||||
"stream",
|
||||
"archive",
|
||||
}
|
||||
const CONFIG_FILENAME = "config.toml"
|
||||
|
||||
const (
|
||||
CATEGORY_GAMING = "20"
|
||||
)
|
||||
func showHelp() {
|
||||
execSplits := strings.Split(os.Args[0], "/")
|
||||
execName := execSplits[len(execSplits) - 1]
|
||||
fmt.Printf(
|
||||
"usage: %s [options] [directory]\n\n" +
|
||||
"options:\n" +
|
||||
"\t-h, --help: Show this help message.\n" +
|
||||
"\t-v, --verbose: Show verbose logging output.\n" +
|
||||
"\t--init: Initialise `directory` as a VOD directory.\n",
|
||||
execName)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Printf("usage: %s <video ID>\n", os.Args[0])
|
||||
if len(os.Args) < 2 || os.Args[1] == "--help" || os.Args[1] == "-h" {
|
||||
showHelp()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// videoID := os.Args[1]
|
||||
var directory string
|
||||
var initDirectory bool = false
|
||||
var verbose bool = false
|
||||
|
||||
cfgBytes, err := os.ReadFile("config.toml")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fatal: failed to read config file: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
for i, arg := range os.Args {
|
||||
if i == 0 { continue }
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
switch arg {
|
||||
|
||||
case "-h":
|
||||
fallthrough
|
||||
case "--help":
|
||||
showHelp()
|
||||
os.Exit(0)
|
||||
|
||||
case "--init":
|
||||
initDirectory = true
|
||||
|
||||
case "-v":
|
||||
fallthrough
|
||||
case "--verbose":
|
||||
verbose = true
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown option `%s`\n", arg)
|
||||
os.Exit(1)
|
||||
|
||||
}
|
||||
} else {
|
||||
directory = arg
|
||||
}
|
||||
}
|
||||
|
||||
cfg := Config{}
|
||||
cfgBytes, err := os.ReadFile(CONFIG_FILENAME)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read config file: %v", err)
|
||||
|
||||
tomlBytes, err := toml.Marshal(&cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal json: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = os.WriteFile(CONFIG_FILENAME, tomlBytes, 0o644)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write config file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Printf("New config file created. Please edit this before running again!")
|
||||
os.Exit(0)
|
||||
}
|
||||
err = toml.Unmarshal(cfgBytes, &cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fatal: failed to parse config: %s\n", err.Error())
|
||||
log.Fatalf("Failed to parse config: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if initDirectory {
|
||||
dirInfo, err := os.Stat(directory)
|
||||
if err != nil {
|
||||
if err == os.ErrNotExist {
|
||||
log.Fatalf("No such directory: %s", directory)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Fatalf("Failed to open directory: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if !dirInfo.IsDir() {
|
||||
log.Fatalf("Not a directory: %s", directory)
|
||||
os.Exit(1)
|
||||
}
|
||||
dirEntry, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open directory: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, entry := range dirEntry {
|
||||
if !entry.IsDir() && entry.Name() == "metadata.toml" {
|
||||
log.Printf("Directory `%s` already initialised", directory)
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
|
||||
defaultMetadata := scanner.DefaultMetadata()
|
||||
metadataStr, _ := toml.Marshal(defaultMetadata)
|
||||
err = os.WriteFile(path.Join(directory, "metadata.toml"), metadataStr, 0o644)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write to file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Printf("Directory successfully initialised")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
metadata, err := scanner.FetchMetadata(directory)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
vodFiles, err := scanner.FetchVideos(metadata.FootageDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch VOD filenames: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(vodFiles) == 0 {
|
||||
log.Fatal("Directory contained no VOD files (expecting .mkv)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", "\t")
|
||||
fmt.Printf("Directory metadata: ")
|
||||
enc.Encode(metadata)
|
||||
fmt.Printf("\nVOD files available: ")
|
||||
enc.Encode(vodFiles)
|
||||
}
|
||||
|
||||
video, err := yt.BuildVideo(metadata)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to build video template: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if verbose {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
fmt.Printf("\nVideo template: ")
|
||||
enc.Encode(video)
|
||||
|
||||
title, err := yt.BuildTitle(video)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to build video title: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
description, err := yt.BuildDescription(video)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to build video description: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(
|
||||
"\nTITLE: %s\nDESCRIPTION: %s",
|
||||
title, description,
|
||||
)
|
||||
}
|
||||
|
||||
err = vid.ConcatVideo(video, vodFiles)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to concatenate VOD files: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// okay actual youtube stuff now
|
||||
|
||||
// TODO: tidy up oauth flow with localhost webserver
|
||||
ctx := context.Background()
|
||||
service, err := youtube.NewService(
|
||||
ctx,
|
||||
option.WithScopes(youtube.YoutubeUploadScope),
|
||||
option.WithAPIKey(cfg.Google.ApiKey),
|
||||
)
|
||||
config := &oauth2.Config{
|
||||
ClientID: cfg.Google.ClientID,
|
||||
ClientSecret: cfg.Google.ClientSecret,
|
||||
Endpoint: google.Endpoint,
|
||||
Scopes: []string{ youtube.YoutubeScope },
|
||||
RedirectURL: "http://localhost:8090",
|
||||
}
|
||||
verifier := oauth2.GenerateVerifier()
|
||||
url := config.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
|
||||
log.Printf("Visit URL to initiate OAuth2: %s", url)
|
||||
|
||||
var code string
|
||||
fmt.Print("Enter OAuth2 code: ")
|
||||
if _, err := fmt.Scan(&code); err != nil {
|
||||
log.Fatalf("Failed to read oauth2 code: %v", err)
|
||||
}
|
||||
|
||||
token, err := config.Exchange(ctx, code, oauth2.VerifierOption(verifier))
|
||||
log.Printf("Token expires on %s\n", token.Expiry.Format("02 Jan 2006"))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fatal: failed to create youtube service: %s\n", err.Error())
|
||||
log.Fatalf("Could not exchange OAuth2 code: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
videoService := youtube.NewVideosService(service)
|
||||
|
||||
// get video by ID
|
||||
{
|
||||
// call := service.Videos.List([]string{
|
||||
// "snippet", "contentDetails", "statistics", "status",
|
||||
// }).Id(videoID)
|
||||
// res, err := call.Do()
|
||||
// if err != nil {
|
||||
// fmt.Fprintf(os.Stderr, "fatal: failed to request videos list: %s\n", err.Error())
|
||||
// os.Exit(1)
|
||||
// }
|
||||
|
||||
// data, err := json.MarshalIndent(res, "", " ")
|
||||
// if err != nil {
|
||||
// fmt.Fprintf(os.Stderr, "fatal: failed to marshal json: %s\n", err.Error())
|
||||
// os.Exit(1)
|
||||
// }
|
||||
|
||||
// fmt.Println(string(data))
|
||||
}
|
||||
|
||||
call := videoService.Insert([]string{
|
||||
"snippet", "status",
|
||||
}, &youtube.Video{
|
||||
Snippet: &youtube.VideoSnippet{
|
||||
Title: "Untitled Video",
|
||||
Description: "No description",
|
||||
Tags: DEFAULT_TAGS,
|
||||
CategoryId: CATEGORY_GAMING, // gaming
|
||||
},
|
||||
}).NotifySubscribers(false)
|
||||
// TODO: call.Media()
|
||||
video, err := call.Do()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fatal: failed to upload video: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(video, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fatal: failed to marshal video data json: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println(string(data))
|
||||
yt.UploadVideo(ctx, token, video)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue