404 lines
9.3 KiB
Go
404 lines
9.3 KiB
Go
// Copyright 2025 eat-pray-ai & OpenWaygate
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package caption
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/eat-pray-ai/yutu/pkg"
|
|
"github.com/eat-pray-ai/yutu/pkg/common"
|
|
"github.com/jedib0t/go-pretty/v6/table"
|
|
"google.golang.org/api/youtube/v3"
|
|
)
|
|
|
|
var (
|
|
errGetCaption = errors.New("failed to get caption")
|
|
errUpdateCaption = errors.New("failed to update caption")
|
|
errDeleteCaption = errors.New("failed to delete caption")
|
|
errInsertCaption = errors.New("failed to insert caption")
|
|
errDownloadCaption = errors.New("failed to download caption")
|
|
)
|
|
|
|
type Caption struct {
|
|
common.Fields
|
|
File string `yaml:"file" json:"file,omitempty"`
|
|
AudioTrackType string `yaml:"audio_track_type" json:"audio_track_type,omitempty"`
|
|
IsAutoSynced *bool `yaml:"is_auto_synced" json:"is_auto_synced,omitzero"`
|
|
IsCC *bool `yaml:"is_cc" json:"is_cc,omitzero"`
|
|
IsDraft *bool `yaml:"is_draft" json:"is_draft,omitzero"`
|
|
IsEasyReader *bool `yaml:"is_easy_reader" json:"is_easy_reader,omitzero"`
|
|
IsLarge *bool `yaml:"is_large" json:"is_large,omitzero"`
|
|
Language string `yaml:"language" json:"language,omitempty"`
|
|
Name string `yaml:"name" json:"name,omitempty"`
|
|
TrackKind string `yaml:"track_kind" json:"track_kind,omitempty"`
|
|
OnBehalfOf string `yaml:"on_behalf_of" json:"on_behalf_of,omitempty"`
|
|
VideoId string `yaml:"video_id" json:"video_id,omitempty"`
|
|
Tfmt string `yaml:"tfmt" json:"tfmt,omitempty"`
|
|
Tlang string `yaml:"tlang" json:"tlang,omitempty"`
|
|
}
|
|
|
|
type ICaption[T youtube.Caption] interface {
|
|
Get() ([]*T, error)
|
|
List(io.Writer) error
|
|
Insert(io.Writer) error
|
|
Update(io.Writer) error
|
|
Delete(io.Writer) error
|
|
Download(io.Writer) error
|
|
}
|
|
|
|
type Option func(*Caption)
|
|
|
|
func NewCaption(opts ...Option) ICaption[youtube.Caption] {
|
|
c := &Caption{Fields: common.Fields{}}
|
|
for _, opt := range opts {
|
|
opt(c)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func (c *Caption) Get() ([]*youtube.Caption, error) {
|
|
if err := c.EnsureService(); err != nil {
|
|
return nil, err
|
|
}
|
|
call := c.Service.Captions.List(c.Parts, c.VideoId)
|
|
if len(c.Ids) > 0 {
|
|
call = call.Id(c.Ids...)
|
|
}
|
|
if c.OnBehalfOf != "" {
|
|
call = call.OnBehalfOf(c.OnBehalfOf)
|
|
}
|
|
if c.OnBehalfOfContentOwner != "" {
|
|
call = call.OnBehalfOfContentOwner(c.OnBehalfOfContentOwner)
|
|
}
|
|
|
|
res, err := call.Do()
|
|
if err != nil {
|
|
return nil, errors.Join(errGetCaption, err)
|
|
}
|
|
|
|
return res.Items, nil
|
|
}
|
|
|
|
func (c *Caption) List(writer io.Writer) error {
|
|
captions, err := c.Get()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
common.PrintList(
|
|
c.Output, captions, writer, table.Row{"ID", "Video ID", "Name", "Language"},
|
|
func(cap *youtube.Caption) table.Row {
|
|
return table.Row{cap.Id, cap.Snippet.VideoId, cap.Snippet.Name, cap.Snippet.Language}
|
|
},
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func (c *Caption) Insert(writer io.Writer) error {
|
|
if err := c.EnsureService(); err != nil {
|
|
return err
|
|
}
|
|
file, err := pkg.Root.Open(c.File)
|
|
if err != nil {
|
|
return errors.Join(errInsertCaption, err)
|
|
}
|
|
defer func(file *os.File) {
|
|
_ = file.Close()
|
|
}(file)
|
|
|
|
caption := &youtube.Caption{
|
|
Snippet: &youtube.CaptionSnippet{
|
|
AudioTrackType: c.AudioTrackType,
|
|
IsAutoSynced: c.IsAutoSynced != nil && *c.IsAutoSynced,
|
|
IsCC: c.IsCC != nil && *c.IsCC,
|
|
IsDraft: c.IsDraft != nil && *c.IsDraft,
|
|
IsEasyReader: c.IsEasyReader != nil && *c.IsEasyReader,
|
|
IsLarge: c.IsLarge != nil && *c.IsLarge,
|
|
Language: c.Language,
|
|
Name: c.Name,
|
|
TrackKind: c.TrackKind,
|
|
VideoId: c.VideoId,
|
|
},
|
|
}
|
|
|
|
call := c.Service.Captions.Insert([]string{"snippet"}, caption).Media(file)
|
|
if c.OnBehalfOf != "" {
|
|
call = call.OnBehalfOf(c.OnBehalfOf)
|
|
}
|
|
if c.OnBehalfOfContentOwner != "" {
|
|
call = call.OnBehalfOfContentOwner(c.OnBehalfOfContentOwner)
|
|
}
|
|
|
|
res, err := call.Do()
|
|
if err != nil {
|
|
return errors.Join(errInsertCaption, err)
|
|
}
|
|
|
|
common.PrintResult(c.Output, res, writer, "Caption inserted: %s\n", res.Id)
|
|
return nil
|
|
}
|
|
|
|
func (c *Caption) Update(writer io.Writer) error {
|
|
c.Parts = []string{"snippet"}
|
|
captions, err := c.Get()
|
|
if err != nil {
|
|
return errors.Join(errUpdateCaption, err)
|
|
}
|
|
if len(captions) == 0 {
|
|
return errGetCaption
|
|
}
|
|
|
|
caption := captions[0]
|
|
if c.AudioTrackType != "" {
|
|
caption.Snippet.AudioTrackType = c.AudioTrackType
|
|
}
|
|
if c.IsAutoSynced != nil {
|
|
caption.Snippet.IsAutoSynced = *c.IsAutoSynced
|
|
caption.Snippet.ForceSendFields = append(
|
|
caption.Snippet.ForceSendFields, "IsAutoSynced",
|
|
)
|
|
}
|
|
if c.IsCC != nil {
|
|
caption.Snippet.IsCC = *c.IsCC
|
|
caption.Snippet.ForceSendFields = append(
|
|
caption.Snippet.ForceSendFields, "IsCC",
|
|
)
|
|
}
|
|
if c.IsDraft != nil {
|
|
caption.Snippet.IsDraft = *c.IsDraft
|
|
caption.Snippet.ForceSendFields = append(
|
|
caption.Snippet.ForceSendFields, "IsDraft",
|
|
)
|
|
}
|
|
if c.IsEasyReader != nil {
|
|
caption.Snippet.IsEasyReader = *c.IsEasyReader
|
|
caption.Snippet.ForceSendFields = append(
|
|
caption.Snippet.ForceSendFields, "IsEasyReader",
|
|
)
|
|
}
|
|
if c.IsLarge != nil {
|
|
caption.Snippet.IsLarge = *c.IsLarge
|
|
caption.Snippet.ForceSendFields = append(
|
|
caption.Snippet.ForceSendFields, "IsLarge",
|
|
)
|
|
}
|
|
if c.Language != "" {
|
|
caption.Snippet.Language = c.Language
|
|
}
|
|
if c.Name != "" {
|
|
caption.Snippet.Name = c.Name
|
|
}
|
|
if c.TrackKind != "" {
|
|
caption.Snippet.TrackKind = c.TrackKind
|
|
}
|
|
if c.VideoId != "" {
|
|
caption.Snippet.VideoId = c.VideoId
|
|
}
|
|
|
|
call := c.Service.Captions.Update([]string{"snippet"}, caption)
|
|
if c.File != "" {
|
|
file, err := pkg.Root.Open(c.File)
|
|
if err != nil {
|
|
return errors.Join(errUpdateCaption, err)
|
|
}
|
|
defer func(file *os.File) {
|
|
_ = file.Close()
|
|
}(file)
|
|
call = call.Media(file)
|
|
}
|
|
if c.OnBehalfOf != "" {
|
|
call = call.OnBehalfOf(c.OnBehalfOf)
|
|
}
|
|
if c.OnBehalfOfContentOwner != "" {
|
|
call = call.OnBehalfOfContentOwner(c.OnBehalfOfContentOwner)
|
|
}
|
|
|
|
res, err := call.Do()
|
|
if err != nil {
|
|
return errors.Join(errUpdateCaption, err)
|
|
}
|
|
|
|
common.PrintResult(c.Output, res, writer, "Caption updated: %s\n", res.Id)
|
|
return nil
|
|
}
|
|
|
|
func (c *Caption) Delete(writer io.Writer) error {
|
|
if err := c.EnsureService(); err != nil {
|
|
return err
|
|
}
|
|
for _, id := range c.Ids {
|
|
call := c.Service.Captions.Delete(id)
|
|
if c.OnBehalfOf != "" {
|
|
call = call.OnBehalfOf(c.OnBehalfOf)
|
|
}
|
|
if c.OnBehalfOfContentOwner != "" {
|
|
call = call.OnBehalfOfContentOwner(c.OnBehalfOfContentOwner)
|
|
}
|
|
|
|
err := call.Do()
|
|
if err != nil {
|
|
return errors.Join(errDeleteCaption, err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(writer, "Caption %s deleted\n", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Caption) Download(writer io.Writer) error {
|
|
if err := c.EnsureService(); err != nil {
|
|
return err
|
|
}
|
|
call := c.Service.Captions.Download(c.Ids[0])
|
|
if c.Tfmt != "" {
|
|
call = call.Tfmt(c.Tfmt)
|
|
}
|
|
if c.Tlang != "" {
|
|
call = call.Tlang(c.Tlang)
|
|
}
|
|
if c.OnBehalfOf != "" {
|
|
call = call.OnBehalfOf(c.OnBehalfOf)
|
|
}
|
|
if c.OnBehalfOfContentOwner != "" {
|
|
call = call.OnBehalfOfContentOwner(c.OnBehalfOfContentOwner)
|
|
}
|
|
|
|
res, err := call.Download()
|
|
if err != nil {
|
|
return errors.Join(errDownloadCaption, err)
|
|
}
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(res.Body)
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return errors.Join(errDownloadCaption, err)
|
|
}
|
|
|
|
file, err := pkg.Root.OpenFile(
|
|
c.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600,
|
|
)
|
|
if err != nil {
|
|
return errors.Join(errDownloadCaption, err)
|
|
}
|
|
defer func(file *os.File) {
|
|
_ = file.Close()
|
|
}(file)
|
|
|
|
_, err = file.Write(body)
|
|
if err != nil {
|
|
return errors.Join(errDownloadCaption, err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(writer, "Caption %s downloaded to %s\n", c.Ids[0], c.File)
|
|
return nil
|
|
}
|
|
|
|
func WithFile(file string) Option {
|
|
return func(c *Caption) {
|
|
c.File = file
|
|
}
|
|
}
|
|
|
|
func WithAudioTrackType(audioTrackType string) Option {
|
|
return func(c *Caption) {
|
|
c.AudioTrackType = audioTrackType
|
|
}
|
|
}
|
|
|
|
func WithIsAutoSynced(isAutoSynced *bool) Option {
|
|
return func(c *Caption) {
|
|
if isAutoSynced != nil {
|
|
c.IsAutoSynced = isAutoSynced
|
|
}
|
|
}
|
|
}
|
|
|
|
func WithIsCC(isCC *bool) Option {
|
|
return func(c *Caption) {
|
|
if isCC != nil {
|
|
c.IsCC = isCC
|
|
}
|
|
}
|
|
}
|
|
|
|
func WithIsDraft(isDraft *bool) Option {
|
|
return func(c *Caption) {
|
|
if isDraft != nil {
|
|
c.IsDraft = isDraft
|
|
}
|
|
}
|
|
}
|
|
|
|
func WithIsEasyReader(isEasyReader *bool) Option {
|
|
return func(c *Caption) {
|
|
if isEasyReader != nil {
|
|
c.IsEasyReader = isEasyReader
|
|
}
|
|
}
|
|
}
|
|
|
|
func WithIsLarge(isLarge *bool) Option {
|
|
return func(c *Caption) {
|
|
if isLarge != nil {
|
|
c.IsLarge = isLarge
|
|
}
|
|
}
|
|
}
|
|
|
|
func WithLanguage(language string) Option {
|
|
return func(c *Caption) {
|
|
c.Language = language
|
|
}
|
|
}
|
|
|
|
func WithName(name string) Option {
|
|
return func(c *Caption) {
|
|
c.Name = name
|
|
}
|
|
}
|
|
|
|
func WithTrackKind(trackKind string) Option {
|
|
return func(c *Caption) {
|
|
c.TrackKind = trackKind
|
|
}
|
|
}
|
|
|
|
func WithOnBehalfOf(onBehalfOf string) Option {
|
|
return func(c *Caption) {
|
|
c.OnBehalfOf = onBehalfOf
|
|
}
|
|
}
|
|
|
|
func WithVideoId(videoId string) Option {
|
|
return func(c *Caption) {
|
|
c.VideoId = videoId
|
|
}
|
|
}
|
|
|
|
func WithTfmt(tfmt string) Option {
|
|
return func(c *Caption) {
|
|
c.Tfmt = tfmt
|
|
}
|
|
}
|
|
|
|
func WithTlang(tlang string) Option {
|
|
return func(c *Caption) {
|
|
c.Tlang = tlang
|
|
}
|
|
}
|
|
|
|
var (
|
|
WithIds = common.WithIds[*Caption]
|
|
WithParts = common.WithParts[*Caption]
|
|
WithOutput = common.WithOutput[*Caption]
|
|
WithService = common.WithService[*Caption]
|
|
|
|
WithOnBehalfOfContentOwner = common.WithOnBehalfOfContentOwner[*Caption]
|
|
)
|