package main import ( "context" "encoding/json" "fmt" "log" "os" "path" "strings" toml "github.com/pelletier/go-toml/v2" "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 ( Config struct { Google GoogleConfig `toml:"google"` } GoogleConfig struct { ApiKey string `toml:"api_key"` ClientID string `toml:"client_id"` ClientSecret string `toml:"client_secret"` } ) const CONFIG_FILENAME = "config.toml" 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 || os.Args[1] == "--help" || os.Args[1] == "-h" { showHelp() os.Exit(0) } var directory string var initDirectory bool = false var verbose bool = false 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 { 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() 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 { log.Fatalf("Could not exchange OAuth2 code: %v", err) os.Exit(1) } yt.UploadVideo(ctx, token, video) }