Compare commits
3 commits
c961aa3b6c
...
21b7a3926b
| Author | SHA1 | Date | |
|---|---|---|---|
| 21b7a3926b | |||
| 75534da702 | |||
| 0abafd26c5 |
6 changed files with 167 additions and 153 deletions
42
README.md
42
README.md
|
|
@ -1,7 +1,12 @@
|
||||||
# Vodular
|
# Vodular
|
||||||
This tool stitches together livestream VOD segments (in `.mkv`format) and automatically uploads them to YouTube, complete with customisable metadata such as titles, descriptions, tags, and a thumbnail!
|
This tool stitches together livestream VOD segments (in `.mkv` format) and
|
||||||
|
automatically uploads them to YouTube, complete with customisable metadata such
|
||||||
|
as titles, descriptions, tags, and a thumbnail!
|
||||||
|
|
||||||
I built this to greatly simplify the process of getting my full-quality livestream VODs onto YouTube, and I'm open-sourcing it in the hopes that it helps someone else with their workflow. As such, personal forks are welcome and encouraged!
|
I built this to greatly simplify the process of getting my full-quality
|
||||||
|
livestream VODs onto YouTube, and I'm open-sourcing it in the hopes that it
|
||||||
|
helps someone else with their workflow. As such, personal forks are welcome and
|
||||||
|
encouraged!
|
||||||
|
|
||||||
## Quick Jump
|
## Quick Jump
|
||||||
- [Basic Usage](#basic-usage)
|
- [Basic Usage](#basic-usage)
|
||||||
|
|
@ -9,40 +14,40 @@ I built this to greatly simplify the process of getting my full-quality livestre
|
||||||
- [Templates](#templates)
|
- [Templates](#templates)
|
||||||
|
|
||||||
## Basic usage
|
## Basic usage
|
||||||
1. Run the tool for the first time to generate a starter configuration file:
|
1. Run `vodular` for the first time to generate a starter configuration file:
|
||||||
```sh
|
|
||||||
$ vodular
|
|
||||||
New config file created (config.toml). Please edit this file before running again!
|
|
||||||
```
|
|
||||||
The directory which holds your configuration file and templates varies,
|
The directory which holds your configuration file and templates varies,
|
||||||
depending on platform:
|
depending on platform:
|
||||||
- **Linux:** `~/.config/vodular/templates`
|
- **Linux:** `~/.config/vodular/templates`
|
||||||
- **macOS:** `~/Library/Application Support/vodular/templates`
|
- **macOS:** `~/Library/Application Support/vodular/templates`
|
||||||
- **Windows:** `%AppData%/vodular/templates`
|
- **Windows:** `%AppData%/vodular/templates`
|
||||||
|
|
||||||
2. Edit your configuration file as necessary (You will need to create a [YouTube Data API v3](https://developers.google.com/youtube/v3) service and provide its credentials here).
|
2. Edit your configuration file as necessary (You will need to create a
|
||||||
**IMPORTANT:** `config.toml` contains very sensitive credentials. Do not share this file with anyone.
|
[YouTube Data API v3](https://developers.google.com/youtube/v3) service and
|
||||||
|
provide its credentials here).
|
||||||
|
**IMPORTANT:** `config.toml` contains very sensitive credentials. Do not share
|
||||||
|
this file with anyone.
|
||||||
|
|
||||||
3. Initialise a VOD directory:
|
3. Initialise a VOD directory:
|
||||||
```sh
|
```sh
|
||||||
$ vodular --init /path/to/vod
|
vodular --init /path/to/vod
|
||||||
Directory successfully initialised. Be sure to update metadata.toml before uploading!
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This directory should contain:
|
This directory should contain:
|
||||||
- A `metadata.toml` file
|
- A `metadata.toml` file
|
||||||
- Your footage files, either at the root of the directory or in a specified subdirectory
|
- Your footage files, either at the root of the directory or in a specified
|
||||||
|
subdirectory
|
||||||
- Your thumbnail, specifically named `thumbnail.png`
|
- Your thumbnail, specifically named `thumbnail.png`
|
||||||
|
|
||||||
4. Modify your newly-created `metadata.toml` to your liking.
|
4. Modify your newly-created `metadata.toml` to your liking.
|
||||||
|
|
||||||
5. Upload a VOD!
|
5. Upload a VOD!
|
||||||
```sh
|
```sh
|
||||||
# `--deleteAfter` deletes the redundant full VOD export afterwards
|
vodular /path/to/vod
|
||||||
vodular --deleteAfter /path/to/vod
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**NOTE:** On first run, you will be prompted to sign in to YouTube with the channel you wish to upload to. To sign out, simply run `vodular --logout`.
|
**NOTE:** On first run, you will be prompted to log into YouTube with the
|
||||||
|
channel you wish to upload to. To log out, simply run `vodular --logout`.
|
||||||
|
|
||||||
## VOD Metadata
|
## VOD Metadata
|
||||||
When `--init`ialising a directory, a `metadata.toml` file is created. This is a
|
When `--init`ialising a directory, a `metadata.toml` file is created. This is a
|
||||||
|
|
@ -56,13 +61,12 @@ title = 'Untitled Stream'
|
||||||
part = 0
|
part = 0
|
||||||
# The date of the stream
|
# The date of the stream
|
||||||
date = '2026-01-28'
|
date = '2026-01-28'
|
||||||
# (Optional) Additional tags to add to this VOD's metadata.
|
# (Optional) VOD-specific tags. Added to global tags template.
|
||||||
tags = ['livestream', 'VOD']
|
tags = ['livestream', 'VOD']
|
||||||
# (Optional) Footage directory override, for more complex directory structures.
|
# (Optional) Footage directory override, for more complex directory structures.
|
||||||
footage_dir = 'footage'
|
footage_dir = 'footage'
|
||||||
# Set to `true` by the tool when the VOD has been uploaded successfully.
|
# On successful upload, updated to a URL to the video.
|
||||||
# Prevents future uploads unless `--force` is used.
|
upload_url = false
|
||||||
uploaded = false
|
|
||||||
|
|
||||||
# (Optional) Category details, for additional credits.
|
# (Optional) Category details, for additional credits.
|
||||||
[category]
|
[category]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
"github.com/pelletier/go-toml/v2"
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
@ -19,7 +18,6 @@ type (
|
||||||
Host string `toml:"host" comment:"Address to host OAuth2 redirect flow"`
|
Host string `toml:"host" comment:"Address to host OAuth2 redirect flow"`
|
||||||
RedirectUri string `toml:"redirect_uri" comment:"URI to use in Google OAuth2 flow"`
|
RedirectUri string `toml:"redirect_uri" comment:"URI to use in Google OAuth2 flow"`
|
||||||
Google GoogleConfig `toml:"google"`
|
Google GoogleConfig `toml:"google"`
|
||||||
Token *oauth2.Token `toml:"token" comment:"This section is filled in automatically on a successful authentication flow."`
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
221
main.go
221
main.go
|
|
@ -30,14 +30,59 @@ const SEGMENT_EXTENSION = "mkv"
|
||||||
const MAX_TITLE_LEN = 100
|
const MAX_TITLE_LEN = 100
|
||||||
const MAX_DESCRIPTION_LEN = 5000
|
const MAX_DESCRIPTION_LEN = 5000
|
||||||
|
|
||||||
func showHelp() {
|
|
||||||
fmt.Println(helpText)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var verbose bool = false
|
||||||
|
var initDirectory bool = false
|
||||||
|
var logout bool = false
|
||||||
|
var keepConcatVOD bool = false
|
||||||
|
var forceUpload bool = false
|
||||||
|
var dryRun bool = false
|
||||||
|
var directory string = ""
|
||||||
|
|
||||||
|
for i, arg := range os.Args {
|
||||||
|
if i == 0 { continue }
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
switch arg {
|
||||||
|
|
||||||
|
case "-h":
|
||||||
|
fallthrough
|
||||||
|
case "--help":
|
||||||
|
log.Fatal(helpText)
|
||||||
|
|
||||||
|
case "-v":
|
||||||
|
fallthrough
|
||||||
|
case "--verbose":
|
||||||
|
verbose = true
|
||||||
|
|
||||||
|
case "--dry-run":
|
||||||
|
dryRun = true
|
||||||
|
|
||||||
|
case "-f":
|
||||||
|
fallthrough
|
||||||
|
case "--force":
|
||||||
|
forceUpload = true
|
||||||
|
|
||||||
|
case "--init":
|
||||||
|
initDirectory = true
|
||||||
|
|
||||||
|
case "--keep-vod":
|
||||||
|
keepConcatVOD = true
|
||||||
|
|
||||||
|
case "--logout":
|
||||||
|
logout = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown option `%s`\n", arg)
|
||||||
|
os.Exit(1)
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
directory = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// config
|
// config
|
||||||
userConfigDir, err := os.UserConfigDir()
|
userConfigDir, err := os.UserConfigDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -49,12 +94,10 @@ func main() {
|
||||||
log.Fatalf("Failed to read config: %v", err)
|
log.Fatalf("Failed to read config: %v", err)
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
err = os.MkdirAll(path.Dir(config.CONFIG_FILENAME), 0750)
|
if err = os.MkdirAll(path.Dir(config.CONFIG_FILENAME), 0750); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create config directory: %v", err)
|
log.Fatalf("Failed to create config directory: %v", err)
|
||||||
}
|
}
|
||||||
err = config.GenerateConfig(config.CONFIG_FILENAME)
|
if err = config.GenerateConfig(config.CONFIG_FILENAME); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to generate config: %v", err)
|
log.Fatalf("Failed to generate config: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf(
|
log.Printf(
|
||||||
|
|
@ -65,60 +108,15 @@ func main() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// arguments
|
oauthTokenFilepath := path.Join(userConfigDir, "vodular", "youtube-auth.json")
|
||||||
if len(os.Args) < 2 || os.Args[1] == "--help" || os.Args[1] == "-h" {
|
var token *oauth2.Token
|
||||||
showHelp()
|
if data, err := os.ReadFile(oauthTokenFilepath); err != nil {
|
||||||
}
|
if !os.IsNotExist(err) {
|
||||||
|
log.Fatalf("Failed to open %s: %v", oauthTokenFilepath, err)
|
||||||
var verbose bool = false
|
|
||||||
var initDirectory bool = false
|
|
||||||
var logout bool = false
|
|
||||||
var deleteFullVod bool = false
|
|
||||||
var forceUpload bool = false
|
|
||||||
var dryRun bool = false
|
|
||||||
var directory string = "."
|
|
||||||
|
|
||||||
for i, arg := range os.Args {
|
|
||||||
if i == 0 { continue }
|
|
||||||
if strings.HasPrefix(arg, "-") {
|
|
||||||
switch arg {
|
|
||||||
|
|
||||||
case "-h":
|
|
||||||
fallthrough
|
|
||||||
case "--help":
|
|
||||||
showHelp()
|
|
||||||
|
|
||||||
case "-v":
|
|
||||||
fallthrough
|
|
||||||
case "--verbose":
|
|
||||||
verbose = true
|
|
||||||
|
|
||||||
case "--init":
|
|
||||||
initDirectory = true
|
|
||||||
|
|
||||||
case "--logout":
|
|
||||||
logout = true
|
|
||||||
|
|
||||||
case "-d":
|
|
||||||
fallthrough
|
|
||||||
case "--deleteAfter":
|
|
||||||
deleteFullVod = true
|
|
||||||
|
|
||||||
case "-f":
|
|
||||||
fallthrough
|
|
||||||
case "--force":
|
|
||||||
forceUpload = true
|
|
||||||
|
|
||||||
case "--dry-run":
|
|
||||||
dryRun = true
|
|
||||||
|
|
||||||
default:
|
|
||||||
fmt.Fprintf(os.Stderr, "Unknown option `%s`\n", arg)
|
|
||||||
os.Exit(1)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
directory = arg
|
if err := json.Unmarshal(data, &token); err != nil {
|
||||||
|
log.Fatalf("Failed to parse YouTube auth file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,19 +128,16 @@ func main() {
|
||||||
|
|
||||||
// logout (--logout)
|
// logout (--logout)
|
||||||
if logout {
|
if logout {
|
||||||
cfg.Token = nil
|
if token == nil { log.Fatal("Not logged in.") }
|
||||||
err = config.WriteConfig(cfg, config.CONFIG_FILENAME)
|
if err := os.Remove(oauthTokenFilepath); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("Failed to delete %s: %v", oauthTokenFilepath, err)
|
||||||
log.Fatalf("Failed to write config: %v", err)
|
|
||||||
}
|
}
|
||||||
log.Println("Logged out successfully.")
|
log.Fatalf("Logged out successfully.")
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialising directory (--init)
|
// initialising directory (--init)
|
||||||
if initDirectory {
|
if initDirectory {
|
||||||
err = scanner.InitialiseDirectory(directory)
|
if err = scanner.InitialiseDirectory(directory); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to initialise directory: %v", err)
|
log.Fatalf("Failed to initialise directory: %v", err)
|
||||||
}
|
}
|
||||||
log.Fatalf(
|
log.Fatalf(
|
||||||
|
|
@ -152,6 +147,8 @@ func main() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// good to have early on
|
// good to have early on
|
||||||
templates, err := yt.FetchTemplates(path.Join(userConfigDir, "vodular", "templates"))
|
templates, err := yt.FetchTemplates(path.Join(userConfigDir, "vodular", "templates"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -193,10 +190,18 @@ func main() {
|
||||||
if verbose {
|
if verbose {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetIndent("", "\t")
|
enc.SetIndent("", "\t")
|
||||||
fmt.Printf("Directory metadata: ")
|
log.Print("[ Metadata ]")
|
||||||
enc.Encode(metadata)
|
log.Printf("Title: %s", metadata.Title)
|
||||||
fmt.Printf("\nVOD files available: ")
|
log.Printf("Part: %d", metadata.Part)
|
||||||
enc.Encode(vodFiles)
|
log.Printf("Date: %s", metadata.Date.String())
|
||||||
|
log.Printf(
|
||||||
|
"Category: `%s` - %s (%s)",
|
||||||
|
metadata.Category.Type,
|
||||||
|
metadata.Category.Name,
|
||||||
|
metadata.Category.Url,
|
||||||
|
)
|
||||||
|
log.Printf("Tags: %s", strings.Join(metadata.Tags, ", "))
|
||||||
|
log.Printf("VOD files: [ %s ]", strings.Join(vodFiles, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// scan for thumbnail
|
// scan for thumbnail
|
||||||
|
|
@ -239,19 +244,13 @@ func main() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if verbose {
|
if verbose {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
log.Print()
|
||||||
fmt.Printf("\nVideo template: ")
|
log.Printf("[ TITLE ]\n\n%s\n\n", title)
|
||||||
enc.Encode(videoMeta)
|
log.Printf("[ DESCRIPTION ]\n\n%s\n\n", description)
|
||||||
|
|
||||||
fmt.Printf(
|
|
||||||
"\n================================\n\n" +
|
|
||||||
"< TITLE >\n%s\n\n" +
|
|
||||||
"< DESCRIPTION >\n%s\n" +
|
|
||||||
"\n================================\n",
|
|
||||||
title, description,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// youtube oauth flow
|
// youtube oauth flow
|
||||||
oauth2Config := &oauth2.Config{
|
oauth2Config := &oauth2.Config{
|
||||||
ClientID: cfg.Google.ClientID,
|
ClientID: cfg.Google.ClientID,
|
||||||
|
|
@ -260,16 +259,17 @@ func main() {
|
||||||
Scopes: []string{ youtube.YoutubeScope },
|
Scopes: []string{ youtube.YoutubeScope },
|
||||||
RedirectURL: cfg.RedirectUri,
|
RedirectURL: cfg.RedirectUri,
|
||||||
}
|
}
|
||||||
var token *oauth2.Token
|
|
||||||
if cfg.Token != nil {
|
if token == nil {
|
||||||
token = cfg.Token
|
|
||||||
} else {
|
|
||||||
token, err = oauth.GenerateToken(&ctx, oauth2Config, cfg)
|
token, err = oauth.GenerateToken(&ctx, oauth2Config, cfg)
|
||||||
if err != nil {
|
if err != nil { log.Fatalf("OAuth flow failed: %v", err) }
|
||||||
log.Fatalf("OAuth flow failed: %v", err)
|
tokenFile, err := os.OpenFile(oauthTokenFilepath, os.O_RDWR | os.O_CREATE, 0600)
|
||||||
|
if err != nil { log.Fatalf("Failed to open %s: %v", oauthTokenFilepath, err) }
|
||||||
|
if err := json.NewEncoder(tokenFile).Encode(token); err != nil {
|
||||||
|
log.Fatalf("Failed to write %s: %v", oauthTokenFilepath, err)
|
||||||
}
|
}
|
||||||
cfg.Token = token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenSource := oauth2Config.TokenSource(ctx, token)
|
tokenSource := oauth2Config.TokenSource(ctx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to create OAuth2 token source: %v", err)
|
log.Fatalf("Failed to create OAuth2 token source: %v", err)
|
||||||
|
|
@ -278,6 +278,8 @@ func main() {
|
||||||
log.Fatalf("Failed to save OAuth token: %v", err)
|
log.Fatalf("Failed to save OAuth token: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
service, err := youtube.NewService(
|
service, err := youtube.NewService(
|
||||||
ctx,
|
ctx,
|
||||||
option.WithScopes(youtube.YoutubeUploadScope),
|
option.WithScopes(youtube.YoutubeUploadScope),
|
||||||
|
|
@ -287,6 +289,8 @@ func main() {
|
||||||
log.Fatalf("Failed to create youtube service: %v\n", err)
|
log.Fatalf("Failed to create youtube service: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// skip uploading if already done
|
// skip uploading if already done
|
||||||
if (metadata.Uploaded || len(metadata.UploadURL) > 0) && !forceUpload {
|
if (metadata.Uploaded || len(metadata.UploadURL) > 0) && !forceUpload {
|
||||||
if len(metadata.UploadURL) > 0 {
|
if len(metadata.UploadURL) > 0 {
|
||||||
|
|
@ -325,12 +329,23 @@ func main() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
videoMeta.Filepath = path.Join(metadata.FootageDir, vodFiles[0])
|
||||||
|
|
||||||
|
// no need to concat if VOD is already one whole file
|
||||||
|
if len(vodFiles) > 1 {
|
||||||
|
videoMeta.Filepath = path.Join(
|
||||||
|
metadata.FootageDir,
|
||||||
|
fmt.Sprintf("%s-fullvod.mkv", metadata.Date.String()),
|
||||||
|
)
|
||||||
|
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
// check if full VOD already exists with expected duration
|
// check if full VOD already exists with expected duration
|
||||||
fullVodExists := false
|
concatVodExists := false
|
||||||
fullVodProbe, err := scanner.ProbeSegment(videoMeta.Filepath)
|
concatVodProbe, err := scanner.ProbeSegment(videoMeta.Filepath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
videoMeta.SizeBytes = fullVodProbe.Format.Size
|
videoMeta.SizeBytes = concatVodProbe.Format.Size
|
||||||
var totalLength float64 = 0
|
var totalLength float64 = 0
|
||||||
|
|
||||||
for _, filename := range vodFiles {
|
for _, filename := range vodFiles {
|
||||||
|
|
@ -339,10 +354,10 @@ func main() {
|
||||||
totalLength += probe.Format.Duration
|
totalLength += probe.Format.Duration
|
||||||
}
|
}
|
||||||
// full VOD may exist, but not be complete (interrupted concat)
|
// full VOD may exist, but not be complete (interrupted concat)
|
||||||
fullVodExists = math.Abs(fullVodProbe.Format.Duration - totalLength) < float64(0.1)
|
concatVodExists = math.Abs(concatVodProbe.Format.Duration - totalLength) < float64(0.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fullVodExists {
|
if concatVodExists {
|
||||||
log.Print("Full VOD appears to already exist- uploading this file...")
|
log.Print("Full VOD appears to already exist- uploading this file...")
|
||||||
} else {
|
} else {
|
||||||
// concatenate VOD segments into full VOD
|
// concatenate VOD segments into full VOD
|
||||||
|
|
@ -354,6 +369,9 @@ func main() {
|
||||||
} else {
|
} else {
|
||||||
log.Print("Dry run: Skipping VOD concatenation")
|
log.Print("Dry run: Skipping VOD concatenation")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
// okay actually upload now!
|
// okay actually upload now!
|
||||||
|
|
@ -378,15 +396,14 @@ func main() {
|
||||||
|
|
||||||
// update metadata to reflect VOD is uploaded
|
// update metadata to reflect VOD is uploaded
|
||||||
metadata.UploadURL = yt.VIDEO_URL_BASE + ytVideo.Id
|
metadata.UploadURL = yt.VIDEO_URL_BASE + ytVideo.Id
|
||||||
err = scanner.WriteMetadata(directory, metadata)
|
if err = scanner.WriteMetadata(directory, metadata); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to update metadata: %v", err)
|
log.Fatalf("Failed to update metadata: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete full VOD after upload, if requested
|
// delete concatenated VOD after upload, unless specified otherwise
|
||||||
if deleteFullVod {
|
// if len(vodFiles) == 1, the full VOD is the *only* VOD. do not delete this!!!
|
||||||
err = os.Remove(videoMeta.Filepath)
|
if len(vodFiles) > 1 && !keepConcatVOD {
|
||||||
if err != nil {
|
if err = os.Remove(videoMeta.Filepath); err != nil {
|
||||||
log.Fatalf("Failed to delete full VOD: %v", err)
|
log.Fatalf("Failed to delete full VOD: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ func GenerateToken(
|
||||||
oauth2.AccessTypeOffline,
|
oauth2.AccessTypeOffline,
|
||||||
oauth2.S256ChallengeOption(verifier),
|
oauth2.S256ChallengeOption(verifier),
|
||||||
)
|
)
|
||||||
fmt.Printf("\nSign in to YouTube: %s\n\n", url)
|
log.Printf("Sign in to YouTube: %s\n\n", url)
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
if err := server.ListenAndServe(); err != http.ErrServerClosed {
|
if err := server.ListenAndServe(); err != http.ErrServerClosed {
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@ USAGE: vodular [options] [directory]
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
-h, --help: Show this help message.
|
-h, --help: Show this help message.
|
||||||
-v, --verbose: Show verbose logging output.
|
-v, --verbose: Show verbose logging output.
|
||||||
--init: Initialise `directory` as a VOD directory.
|
|
||||||
--logout: Logs out of the current YouTube account.
|
|
||||||
-d, --deleteAfter: Deletes the full VOD after upload.
|
|
||||||
--dry-run: Don't upload or modify any files (Useful with -v)
|
--dry-run: Don't upload or modify any files (Useful with -v)
|
||||||
-f, --force: Force uploading the VOD, even if it already exists.
|
-f, --force: Force uploading the VOD, even if it already exists.
|
||||||
|
--init: Initialise `directory` as a VOD directory.
|
||||||
|
--keep-vod: Retains the concatenated VOD (if generated) after upload.
|
||||||
|
--logout: Logs out of the current YouTube account.
|
||||||
|
|
||||||
SOURCE: https://codeberg.org/arimelody/vodular
|
SOURCE: https://codeberg.org/arimelody/vodular
|
||||||
ISSUES: https://codeberg.org/arimelody/vodular/issues
|
ISSUES: https://codeberg.org/arimelody/vodular/issues
|
||||||
|
|
|
||||||
|
|
@ -72,12 +72,6 @@ func BuildVideo(metadata *scanner.Metadata) (*VideoMetadata, error) {
|
||||||
Part: metadata.Part,
|
Part: metadata.Part,
|
||||||
Date: metadata.Date.AsTime(time.UTC),
|
Date: metadata.Date.AsTime(time.UTC),
|
||||||
Tags: metadata.Tags,
|
Tags: metadata.Tags,
|
||||||
Filepath: path.Join(
|
|
||||||
metadata.FootageDir,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"%s-fullvod.mkv",
|
|
||||||
metadata.Date.String(),
|
|
||||||
)),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue