package youtube import ( "bytes" "context" "fmt" "log" "os" "path" "strings" "text/template" "time" "arimelody.space/vodular/scanner" "golang.org/x/oauth2" "google.golang.org/api/option" "google.golang.org/api/youtube/v3" ) const ( YT_CATEGORY_GAMING = "20" YT_CATEGORY_ENTERTAINMENT = "24" ) type CategoryType int const ( CATEGORY_GAME CategoryType = iota CATEGORY_ENTERTAINMENT ) type ( Category struct { Name string Type CategoryType Url string } Video struct { Title string Category *Category Part int Date time.Time Tags []string Filename string SizeBytes int64 } ) func BuildVideo(metadata *scanner.Metadata) (*Video, error) { videoDate, err := time.Parse("2006-01-02", metadata.Date) if err != nil { return nil, fmt.Errorf("failed to parse date from metadata: %v", err) } var category *Category = nil if metadata.Category != nil { category = &Category{ Name: metadata.Category.Name, Type: CATEGORY_ENTERTAINMENT, Url: metadata.Category.Url, } var ok bool category.Type, ok = videoCategoryStringTypes[metadata.Category.Type] if !ok { category.Type = CATEGORY_ENTERTAINMENT } } return &Video{ Title: metadata.Title, Category: category, Part: metadata.Part, Date: videoDate, Tags: metadata.Tags, Filename: path.Join( metadata.FootageDir, fmt.Sprintf( "%s-fullvod.mkv", videoDate.Format("2006-01-02"), )), }, nil } type ( MetaCategory struct { Name string Type string Url string } Metadata struct { Title string Date time.Time Category *MetaCategory Part int } Template struct { Title *template.Template Description *template.Template Tags []string } ) var videoCategoryTypeStrings = map[CategoryType]string{ CATEGORY_GAME: "gaming", CATEGORY_ENTERTAINMENT: "entertainment", } var videoCategoryStringTypes = map[string]CategoryType{ "gaming": CATEGORY_GAME, "entertainment": CATEGORY_ENTERTAINMENT, } var videoYtCategory = map[CategoryType]string { CATEGORY_GAME: YT_CATEGORY_GAMING, CATEGORY_ENTERTAINMENT: YT_CATEGORY_ENTERTAINMENT, } var templateDir = "templates" var tagsPath = path.Join(templateDir, "tags.txt") var titlePath = path.Join(templateDir, "title.txt") var descriptionPath = path.Join(templateDir, "description.txt") const defaultTitleTemplate = "{{.Title}} - {{FormatTime .Date \"02 Jan 2006\"}}" const defaultDescriptionTemplate = "Streamed on {{FormatTime .Date \"02 January 2006\"}}" var templateFuncs = template.FuncMap{ "FormatTime": func (time time.Time, format string) string { return time.Format(format) }, "ToLower": func (str string) string { return strings.ToLower(str) }, "ToUpper": func (str string) string { return strings.ToUpper(str) }, } func FetchTemplates() (*Template, error) { tmpl := Template{} // tags if tagsFile, err := os.ReadFile(tagsPath); err == nil { tmpl.Tags = strings.Split(string(tagsFile), "\n") } else { if !os.IsNotExist(err) { return nil, err } log.Fatalf( "%s not found. No default tags will be used.", tagsPath, ) tmpl.Tags = []string{} } // title titleTemplate := template.New("title").Funcs(templateFuncs) if titleFile, err := os.ReadFile(titlePath); err == nil { tmpl.Title, err = titleTemplate.Parse(string(titleFile)) if err != nil { return nil, fmt.Errorf("failed to parse title template: %v", err) } } else { if !os.IsNotExist(err) { return nil, err } log.Fatalf( "%s not found. Falling back to default template:\n%s", titlePath, defaultTitleTemplate, ) tmpl.Title, err = titleTemplate.Parse(defaultTitleTemplate) if err != nil { panic(err) } os.WriteFile(titlePath, []byte(defaultTitleTemplate), 0644) } // description descriptionTemplate := template.New("description").Funcs(templateFuncs,) if descriptionFile, err := os.ReadFile(descriptionPath); err == nil { tmpl.Description, err = descriptionTemplate.Parse(string(descriptionFile)) if err != nil { return nil, fmt.Errorf("failed to parse description template: %v", err) } } else { if !os.IsNotExist(err) { return nil, err } log.Fatalf( "%s not found. Falling back to default template:\n%s", descriptionPath, defaultDescriptionTemplate, ) tmpl.Description, err = descriptionTemplate.Parse(defaultDescriptionTemplate) if err != nil { panic(err) } os.WriteFile(descriptionPath, []byte(defaultDescriptionTemplate), 0644) } return &tmpl, nil } func BuildTemplate(video *Video, tmpl *template.Template) (string, error) { out := &bytes.Buffer{} var category *MetaCategory if video.Category != nil { category = &MetaCategory{ Name: video.Category.Name, Type: videoCategoryTypeStrings[video.Category.Type], Url: video.Category.Url, } } err := tmpl.Execute(out, Metadata{ Title: video.Title, Date: video.Date, Category: category, Part: video.Part, }) return strings.TrimSpace(out.String()), err } func UploadVideo( ctx context.Context, tokenSource oauth2.TokenSource, video *Video, templates *Template, ) (*youtube.Video, error) { title, err := BuildTemplate(video, templates.Title) if err != nil { return nil, fmt.Errorf("failed to build title: %v", err) } description, err := BuildTemplate(video, templates.Description) if err != nil { return nil, fmt.Errorf("failed to build description: %v", err) } service, err := youtube.NewService( ctx, option.WithScopes(youtube.YoutubeUploadScope), option.WithTokenSource(tokenSource), ) if err != nil { log.Fatalf("Failed to create youtube service: %v\n", err) return nil, err } videoService := youtube.NewVideosService(service) categoryId := YT_CATEGORY_ENTERTAINMENT if video.Category != nil { if cid, ok := videoYtCategory[video.Category.Type]; ok { categoryId = cid } } call := videoService.Insert([]string{ "snippet", "status", }, &youtube.Video{ Snippet: &youtube.VideoSnippet{ Title: title, Description: description, Tags: append(templates.Tags, video.Tags...), CategoryId: categoryId, // gaming }, Status: &youtube.VideoStatus{ PrivacyStatus: "private", }, }).NotifySubscribers(false) file, err := os.Open(video.Filename) if err != nil { log.Fatalf("Failed to open file: %v\n", err) return nil, err } call.Media(file) log.Println("Uploading video...") call.ProgressUpdater(func(current, total int64) { // for some reason, this only returns 0. // instead, we pull the file size from the ffmpeg output directly. if total == 0 { total = video.SizeBytes } fmt.Printf("Uploading... (%.2f%%)\n", float64(current) / float64(total) * 100) }) ytVideo, err := call.Do() if err != nil { log.Fatalf("Failed to upload video: %v\n", err) return nil, err } return ytVideo, err }