package activity import ( "errors" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetActivity = errors.New("failed to get activity") ) type activity struct { ChannelId string `yaml:"channel_id" json:"channel_id"` Home *bool `yaml:"home" json:"home"` MaxResults int64 `yaml:"max_results" json:"max_results"` Mine *bool `yaml:"mine" json:"mine"` PublishedAfter string `yaml:"published_after" json:"published_after"` PublishedBefore string `yaml:"published_before" json:"published_before"` RegionCode string `yaml:"region_code" json:"region_code"` } type Activity interface { List([]string, string, string, io.Writer) error Get([]string) ([]*youtube.Activity, error) } type Option func(*activity) func NewActivity(opts ...Option) Activity { a := &activity{} for _, opt := range opts { opt(a) } return a } func (a *activity) Get(parts []string) ([]*youtube.Activity, error) { call := service.Activities.List(parts) if a.ChannelId != "" { call = call.ChannelId(a.ChannelId) } if a.Home != nil { call = call.Home(*a.Home) } if a.Mine != nil { call = call.Mine(*a.Mine) } call.MaxResults(a.MaxResults) if a.PublishedAfter != "" { call.PublishedAfter(a.PublishedAfter) } if a.PublishedBefore != "" { call.PublishedBefore(a.PublishedBefore) } if a.RegionCode != "" { call.RegionCode(a.RegionCode) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetActivity, err) } return res.Items, nil } func (a *activity) List( parts []string, output string, jpath string, writer io.Writer, ) error { activities, err := a.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(activities, jpath, writer) case "yaml": utils.PrintYAML(activities, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Title", "Type", "Time"}) for _, activity := range activities { tb.AppendRow( table.Row{ activity.Id, activity.Snippet.Title, activity.Snippet.Type, activity.Snippet.PublishedAt, }, ) } } return nil } func WithChannelId(channelId string) Option { return func(a *activity) { a.ChannelId = channelId } } func WithHome(home *bool) Option { return func(a *activity) { if home != nil { a.Home = home } } } func WithMaxResults(maxResults int64) Option { return func(a *activity) { if maxResults <= 0 { maxResults = 1 } a.MaxResults = maxResults } } func WithMine(mine *bool) Option { return func(a *activity) { if mine != nil { a.Mine = mine } } } func WithPublishedAfter(publishedAfter string) Option { return func(a *activity) { a.PublishedAfter = publishedAfter } } func WithPublishedBefore(publishedBefore string) Option { return func(a *activity) { a.PublishedBefore = publishedBefore } } func WithRegionCode(regionCode string) Option { return func(a *activity) { a.RegionCode = regionCode } } func WithService(svc *youtube.Service) Option { return func(_ *activity) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package auth import ( "context" "encoding/json" "fmt" "log/slog" "net" "net/http" "net/url" "os" "path/filepath" "strings" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/utils" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/option" "google.golang.org/api/youtube/v3" ) const ( createSvcFailed = "failed to create YouTube service" parseUrlFailed = "failed to parse redirect URL" stateMatchFailed = "state doesn't match, possible CSRF attack" readPromptFailed = "failed to read prompt" exchangeFailed = "failed to exchange token" listenFailed = "failed to start web server" cacheTokenFailed = "failed to cache token" parseTokenFailed = "failed to parse token" refreshTokenFailed = "failed to refresh token, please re-authenticate in cli" parseSecretFailed = "failed to parse client secret" browserOpenedHint = "Your browser has been opened to an authorization URL. yutu will resume once authorization has been provided.\n%s\n" openBrowserHint = "It seems that your browser is not open. Go to the following link in your browser:\n%s\n" manualInputHint = ` After completing the authorization flow, enter the authorization code on command line. If you end up in an error page after completing the authorization flow, and the url in the address bar is in the form of 'localhost:8216/?state=DONOT-COPY&code=COPY-THIS&scope=DONOT-COPY' ONLY 'COPY-THIS' part is the code you need to enter on command line. ` ) var ( state = utils.RandomStage() scope = []string{ youtube.YoutubeScope, youtube.YoutubeForceSslScope, youtube.YoutubeChannelMembershipsCreatorScope, } ) func (s *svc) GetService() *youtube.Service { client := s.refreshClient() service, err := youtube.NewService(s.ctx, option.WithHTTPClient(client)) if err != nil { slog.Error(createSvcFailed, "error", err) os.Exit(1) } s.service = service return s.service } func (s *svc) refreshClient() (client *http.Client) { config := s.getConfig() authedToken := &oauth2.Token{} err := json.Unmarshal([]byte(s.CacheToken), authedToken) if err != nil { client, authedToken = s.newClient(config) if s.tokenFile != "" { s.saveToken(authedToken) } return client } if !authedToken.Valid() { tokenSource := config.TokenSource(s.ctx, authedToken) authedToken, err = tokenSource.Token() if err != nil && s.tokenFile != "" { client, authedToken = s.newClient(config) s.saveToken(authedToken) return client } else if err != nil { slog.Error(refreshTokenFailed, "error", err) os.Exit(1) } if authedToken != nil && s.tokenFile != "" { s.saveToken(authedToken) } return config.Client(s.ctx, authedToken) } return config.Client(s.ctx, authedToken) } func (s *svc) newClient(config *oauth2.Config) ( client *http.Client, token *oauth2.Token, ) { authURL := config.AuthCodeURL(state, oauth2.AccessTypeOffline) token = s.getTokenFromWeb(config, authURL) client = config.Client(s.ctx, token) return } func (s *svc) getConfig() *oauth2.Config { config, err := google.ConfigFromJSON([]byte(s.Credential), scope...) if err != nil { slog.Error(parseSecretFailed, "error", err) os.Exit(1) } return config } func (s *svc) startWebServer(redirectURL string) chan string { u, err := url.Parse(redirectURL) if err != nil { slog.Error(parseUrlFailed, "url", redirectURL, "error", err) os.Exit(1) } listener, err := net.Listen("tcp", u.Host) if err != nil { slog.Error(listenFailed, "host", u.Host, "error", err) os.Exit(1) } codeCh := make(chan string) go func() { _ = http.Serve( listener, http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { return } s := r.FormValue("state") if s != state { slog.Error( stateMatchFailed, "actual", s, "expected", state, ) os.Exit(1) } code := r.FormValue("code") codeCh <- code _ = listener.Close() w.Header().Set("Content-Type", "text/plain") _, _ = fmt.Fprintf( w, "Received code: %s\r\nYou can now safely close this window.", code, ) }, ), ) }() return codeCh } func (s *svc) getCodeFromPrompt(authURL string) (code string) { fmt.Printf(openBrowserHint, authURL) fmt.Print(manualInputHint) _, err := fmt.Scan(&code) if err != nil { slog.Error(readPromptFailed, "error", err) os.Exit(1) } if strings.HasPrefix(code, "4%2F") { code = strings.Replace(code, "4%2F", "4/", 1) } return code } func (s *svc) getTokenFromWeb( config *oauth2.Config, authURL string, ) *oauth2.Token { codeCh := s.startWebServer(config.RedirectURL) var code string if err := utils.OpenURL(authURL); err == nil { fmt.Printf(browserOpenedHint, authURL) code = <-codeCh } if code == "" { code = s.getCodeFromPrompt(authURL) } slog.Debug("Authorization code generated", "code", code) token, err := config.Exchange(context.TODO(), code) if err != nil { slog.Error(exchangeFailed, "error", err) os.Exit(1) } return token } func (s *svc) saveToken(token *oauth2.Token) { dir := filepath.Dir(s.tokenFile) if err := pkg.Root.MkdirAll(dir, 0755); err != nil { slog.Error(cacheTokenFailed, "dir", dir, "error", err) os.Exit(1) } f, err := pkg.Root.OpenFile( s.tokenFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600, ) if err != nil { slog.Error(cacheTokenFailed, "file", s.tokenFile, "error", err) os.Exit(1) } defer func() { _ = f.Close() }() err = json.NewEncoder(f).Encode(token) if err != nil { slog.Error(cacheTokenFailed, "error", err) os.Exit(1) } slog.Info("Token cached to file", "file", s.tokenFile) }
package auth import ( "context" "encoding/base64" "io/fs" "log/slog" "net/http" "os" "path/filepath" "strings" "github.com/eat-pray-ai/yutu/pkg/utils" "golang.org/x/oauth2" "google.golang.org/api/youtube/v3" ) const ( readTokenFailed = "failed to read token" readSecretFailed = "failed to read client secret" authHint = "Please configure client secret as described in https://github.com/eat-pray-ai/yutu#prerequisites" ) type svc struct { Credential string `yaml:"credential" json:"credential"` CacheToken string `yaml:"cache_token" json:"cache_token"` credFile string tokenFile string service *youtube.Service ctx context.Context } type Svc interface { GetService() *youtube.Service refreshClient() *http.Client newClient(*oauth2.Config) (*http.Client, *oauth2.Token) getConfig() *oauth2.Config startWebServer(string) chan string getTokenFromWeb(*oauth2.Config, string) *oauth2.Token getCodeFromPrompt(string) string saveToken(*oauth2.Token) } type Option func(*svc) func NewY2BService(opts ...Option) Svc { s := &svc{} s.ctx = context.Background() s.credFile = "client_secret.json" for _, opt := range opts { opt(s) } return s } func WithCredential(cred string, fsys fs.FS) Option { return func(s *svc) { // cred > YUTU_CREDENTIAL envCred, ok := os.LookupEnv("YUTU_CREDENTIAL") if cred == "" && ok { cred = envCred } else if cred == "" { cred = s.credFile } // 1. cred is a file path // 2. cred is a base64 encoded string // 3. cred is a json string absCred, _ := filepath.Abs(cred) relCred, _ := filepath.Rel("/", absCred) if _, err := fs.Stat(fsys, relCred); err == nil { s.credFile = absCred credBytes, err := fs.ReadFile(fsys, relCred) if err != nil { slog.Error( readSecretFailed, "hint", authHint, "path", absCred, "error", err, ) os.Exit(1) } s.Credential = string(credBytes) return } if credB64, err := base64.StdEncoding.DecodeString(cred); err == nil { s.Credential = string(credB64) } else if utils.IsJson(cred) { s.Credential = cred } else { slog.Error(parseSecretFailed, "hint", authHint, "error", err) os.Exit(1) } } } func WithCacheToken(token string, fsys fs.FS) Option { return func(s *svc) { // token > YUTU_CACHE_TOKEN envToken, ok := os.LookupEnv("YUTU_CACHE_TOKEN") if token == "" && ok { token = envToken } else if token == "" { token = "youtube.token.json" } // 1. token is a file path // 2. token is a base64 encoded string // 3. token is a json string absToken, _ := filepath.Abs(token) relToken, _ := filepath.Rel("/", absToken) if _, err := fs.Stat(fsys, relToken); err == nil { tokenBytes, err := fs.ReadFile(fsys, relToken) if err != nil { slog.Warn(readTokenFailed, "path", absToken, "error", err) } s.tokenFile = absToken s.CacheToken = string(tokenBytes) return } else if os.IsNotExist(err) && strings.HasSuffix(token, ".json") { s.tokenFile = absToken return } if tokenB64, err := base64.StdEncoding.DecodeString(token); err == nil { s.CacheToken = string(tokenB64) } else if utils.IsJson(token) { s.CacheToken = token } else { slog.Warn(parseTokenFailed, "error", err) } } }
package caption import ( "errors" "fmt" "io" "os" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service 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 { IDs []string `yaml:"ids" json:"ids"` File string `yaml:"file" json:"file"` AudioTrackType string `yaml:"audio_track_type" json:"audio_track_type"` IsAutoSynced *bool `yaml:"is_auto_synced" json:"is_auto_synced"` IsCC *bool `yaml:"is_cc" json:"is_cc"` IsDraft *bool `yaml:"is_draft" json:"is_draft"` IsEasyReader *bool `yaml:"is_easy_reader" json:"is_easy_reader"` IsLarge *bool `yaml:"is_large" json:"is_large"` Language string `yaml:"language" json:"language"` Name string `yaml:"name" json:"name"` TrackKind string `yaml:"track_kind" json:"track_kind"` OnBehalfOf string `yaml:"on_behalf_of" json:"on_behalf_of"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` VideoId string `yaml:"video_id" json:"video_id"` Tfmt string `yaml:"tfmt" json:"tfmt"` Tlang string `yaml:"tlang" json:"tlang"` } type Caption interface { Get(parts []string) ([]*youtube.Caption, error) List(parts []string, output string, s string, writer io.Writer) error Insert(output string, s string, writer io.Writer) error Update(output string, s string, writer io.Writer) error Delete(writer io.Writer) error Download(writer io.Writer) error } type Option func(*caption) func NewCation(opts ...Option) Caption { c := &caption{} for _, opt := range opts { opt(c) } return c } func (c *caption) Get(parts []string) ([]*youtube.Caption, error) { call := service.Captions.List(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( parts []string, output string, jpath string, writer io.Writer, ) error { captions, err := c.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(captions, jpath, writer) case "yaml": utils.PrintYAML(captions, jpath, writer) case "table": tb := table.NewWriter() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Video ID", "Name", "Language"}) defer tb.Render() for _, caption := range captions { tb.AppendRow( table.Row{ caption.Id, caption.Snippet.VideoId, caption.Snippet.Name, caption.Snippet.Language, }, ) } } return nil } func (c *caption) Insert(output string, jpath string, writer io.Writer) error { file, err := pkg.Root.Open(c.File) if err != nil { return errors.Join(errInsertCaption, err) } defer file.Close() caption := &youtube.Caption{ Snippet: &youtube.CaptionSnippet{ AudioTrackType: c.AudioTrackType, IsAutoSynced: *c.IsAutoSynced, IsCC: *c.IsCC, IsDraft: *c.IsDraft, IsEasyReader: *c.IsEasyReader, IsLarge: *c.IsLarge, Language: c.Language, Name: c.Name, TrackKind: c.TrackKind, VideoId: c.VideoId, }, } call := 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) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Caption inserted: %s\n", res.Id) } return nil } func (c *caption) Update(output string, jpath string, writer io.Writer) error { captions, err := c.Get([]string{"snippet"}) 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 } if c.IsCC != nil { caption.Snippet.IsCC = *c.IsCC } if c.IsDraft != nil { caption.Snippet.IsDraft = *c.IsDraft } if c.IsEasyReader != nil { caption.Snippet.IsEasyReader = *c.IsEasyReader } if c.IsLarge != nil { caption.Snippet.IsLarge = *c.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 := 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 file.Close() 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) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Caption updated: %s\n", res.Id) } return nil } func (c *caption) Delete(writer io.Writer) error { for _, id := range c.IDs { call := 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 { call := 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 res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { return errors.Join(errDownloadCaption, err) } file, err := os.Create(c.File) if err != nil { return errors.Join(errDownloadCaption, err) } defer file.Close() _, 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 WithIDs(ids []string) Option { return func(c *caption) { c.IDs = ids } } 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 WithOnBehalfOfContentOwner(onBehalfOfContentOwner string) Option { return func(c *caption) { c.OnBehalfOfContentOwner = onBehalfOfContentOwner } } 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 } } func WithService(svc *youtube.Service) Option { return func(_ *caption) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package channel import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetChannel = errors.New("failed to get channel") errUpdateChannel = errors.New("failed to update channel") ) type channel struct { CategoryId string `yaml:"category_id" json:"category_id"` ForHandle string `yaml:"for_handle" json:"for_handle"` ForUsername string `yaml:"for_username" json:"for_username"` Hl string `yaml:"hl" json:"hl"` IDs []string `yaml:"ids" json:"ids"` ManagedByMe *bool `yaml:"managed_by_me" json:"managed_by_me"` MaxResults int64 `yaml:"max_results" json:"max_results"` Mine *bool `yaml:"mine" json:"mine"` MySubscribers *bool `yaml:"my_subscribers" json:"my_subscribers"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` Country string `yaml:"country" json:"country"` CustomUrl string `yaml:"custom_url" json:"custom_url"` DefaultLanguage string `yaml:"default_language" json:"default_language"` Description string `yaml:"description" json:"description"` Title string `yaml:"title" json:"title"` } type Channel interface { List([]string, string, string, io.Writer) error Update(string, string, io.Writer) error Get([]string) ([]*youtube.Channel, error) } type Option func(*channel) func NewChannel(opts ...Option) Channel { c := &channel{} for _, opt := range opts { opt(c) } return c } func (c *channel) Get(parts []string) ([]*youtube.Channel, error) { call := service.Channels.List(parts) if c.CategoryId != "" { call = call.CategoryId(c.CategoryId) } if c.ForHandle != "" { call = call.ForHandle(c.ForHandle) } if c.ForUsername != "" { call = call.ForUsername(c.ForUsername) } if c.Hl != "" { call = call.Hl(c.Hl) } if len(c.IDs) > 0 { call = call.Id(c.IDs...) } if c.ManagedByMe != nil { call = call.ManagedByMe(*c.ManagedByMe) } call = call.MaxResults(c.MaxResults) if c.Mine != nil { call = call.Mine(*c.Mine) } if c.MySubscribers != nil { call = call.MySubscribers(*c.MySubscribers) } if c.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(c.OnBehalfOfContentOwner) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetChannel, err) } return res.Items, nil } func (c *channel) List( parts []string, output string, jpath string, writer io.Writer, ) error { channels, err := c.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(channels, jpath, writer) case "yaml": utils.PrintYAML(channels, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Title", "Country"}) for _, channel := range channels { tb.AppendRow( table.Row{channel.Id, channel.Snippet.Title, channel.Snippet.Country}, ) } } return nil } func (c *channel) Update(output string, jpath string, writer io.Writer) error { parts := []string{"snippet"} channels, err := c.Get(parts) if err != nil { return errors.Join(errUpdateChannel, err) } if len(channels) == 0 { return errGetChannel } cha := channels[0] if c.Country != "" { cha.Snippet.Country = c.Country } if c.CustomUrl != "" { cha.Snippet.CustomUrl = c.CustomUrl } if c.DefaultLanguage != "" { cha.Snippet.DefaultLanguage = c.DefaultLanguage } if c.Description != "" { cha.Snippet.Description = c.Description } if c.Title != "" { cha.Snippet.Title = c.Title } call := service.Channels.Update(parts, cha) res, err := call.Do() if err != nil { return errors.Join(errUpdateChannel, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Channel updated: %s\n", res.Id) } return nil } func WithCategoryId(categoryId string) Option { return func(c *channel) { c.CategoryId = categoryId } } func WithForHandle(handle string) Option { return func(c *channel) { c.ForHandle = handle } } func WithForUsername(username string) Option { return func(c *channel) { c.ForUsername = username } } func WithHl(hl string) Option { return func(c *channel) { c.Hl = hl } } func WithIDs(ids []string) Option { return func(c *channel) { c.IDs = ids } } func WithChannelManagedByMe(managedByMe *bool) Option { return func(c *channel) { if managedByMe != nil { c.ManagedByMe = managedByMe } } } func WithMaxResults(maxResults int64) Option { return func(c *channel) { if maxResults <= 0 { maxResults = 1 } c.MaxResults = maxResults } } func WithMine(mine *bool) Option { return func(c *channel) { if mine != nil { c.Mine = mine } } } func WithMySubscribers(mySubscribers *bool) Option { return func(c *channel) { if mySubscribers != nil { c.MySubscribers = mySubscribers } } } func WithOnBehalfOfContentOwner(contentOwner string) Option { return func(c *channel) { c.OnBehalfOfContentOwner = contentOwner } } func WithCountry(country string) Option { return func(c *channel) { c.Country = country } } func WithCustomUrl(url string) Option { return func(c *channel) { c.CustomUrl = url } } func WithDefaultLanguage(language string) Option { return func(c *channel) { c.DefaultLanguage = language } } func WithDescription(desc string) Option { return func(c *channel) { c.Description = desc } } func WithTitle(title string) Option { return func(c *channel) { c.Title = title } } func WithService(svc *youtube.Service) Option { return func(_ *channel) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package channelBanner import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errInsertChannelBanner = errors.New("failed to insert channelBanner") ) type channelBanner struct { ChannelId string `yaml:"channel_id" json:"channel_id"` File string `yaml:"file" json:"file"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` OnBehalfOfContentOwnerChannel string `yaml:"on_behalf_of_content_owner_channel" json:"on_behalf_of_content_owner_channel"` } type ChannelBanner interface { Insert(string, string, io.Writer) error } type Option func(banner *channelBanner) func NewChannelBanner(opts ...Option) ChannelBanner { cb := &channelBanner{} for _, opt := range opts { opt(cb) } return cb } func (cb *channelBanner) Insert( output string, jpath string, writer io.Writer, ) error { file, err := pkg.Root.Open(cb.File) if err != nil { return errors.Join(errInsertChannelBanner, err) } defer file.Close() cbr := &youtube.ChannelBannerResource{} call := service.ChannelBanners.Insert(cbr).ChannelId(cb.ChannelId).Media(file) if cb.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(cb.OnBehalfOfContentOwner) } if cb.OnBehalfOfContentOwnerChannel != "" { call = call.OnBehalfOfContentOwnerChannel(cb.OnBehalfOfContentOwnerChannel) } res, err := call.Do() if err != nil { return errors.Join(errInsertChannelBanner, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "ChannelBanner inserted: %s\n", res.Url) } return nil } func WithChannelId(channelId string) Option { return func(cb *channelBanner) { cb.ChannelId = channelId } } func WithFile(file string) Option { return func(cb *channelBanner) { cb.File = file } } func WithOnBehalfOfContentOwner(onBehalfOfContentOwner string) Option { return func(cb *channelBanner) { cb.OnBehalfOfContentOwner = onBehalfOfContentOwner } } func WithOnBehalfOfContentOwnerChannel(onBehalfOfContentOwnerChannel string) Option { return func(cb *channelBanner) { cb.OnBehalfOfContentOwnerChannel = onBehalfOfContentOwnerChannel } } func WithService(svc *youtube.Service) Option { return func(_ *channelBanner) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package channelSection import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetChannelSection = errors.New("failed to get channel section") errDeleteChannelSection = errors.New("failed to delete channel section") ) type channelSection struct { IDs []string `yaml:"ids" json:"ids"` ChannelId string `yaml:"channel_id" json:"channel_id"` Hl string `yaml:"hl" json:"hl"` Mine *bool `yaml:"mine" json:"mine"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` } type ChannelSection interface { Get([]string) ([]*youtube.ChannelSection, error) List([]string, string, string, io.Writer) error Delete(writer io.Writer) error // Update() // Insert() } type Option func(*channelSection) func NewChannelSection(opts ...Option) ChannelSection { cs := &channelSection{} for _, opt := range opts { opt(cs) } return cs } func (cs *channelSection) Get(parts []string) ( []*youtube.ChannelSection, error, ) { call := service.ChannelSections.List(parts) if len(cs.IDs) > 0 { call = call.Id(cs.IDs...) } if cs.ChannelId != "" { call = call.ChannelId(cs.ChannelId) } if cs.Hl != "" { call = call.Hl(cs.Hl) } if cs.Mine != nil { call = call.Mine(*cs.Mine) } if cs.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(cs.OnBehalfOfContentOwner) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetChannelSection, err) } return res.Items, nil } func (cs *channelSection) List( parts []string, output string, jpath string, writer io.Writer, ) error { channelSections, err := cs.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(channelSections, jpath, writer) case "yaml": utils.PrintYAML(channelSections, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Channel ID", "Title"}) for _, chs := range channelSections { tb.AppendRow(table.Row{chs.Id, chs.Snippet.ChannelId, chs.Snippet.Title}) } } return nil } func (cs *channelSection) Delete(writer io.Writer) error { for _, id := range cs.IDs { call := service.ChannelSections.Delete(id) if cs.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(cs.OnBehalfOfContentOwner) } err := call.Do() if err != nil { return errors.Join(errDeleteChannelSection, err) } _, _ = fmt.Fprintf(writer, "Channel section %s deleted\n", id) } return nil } func WithIDs(ids []string) Option { return func(cs *channelSection) { cs.IDs = ids } } func WithChannelId(channelId string) Option { return func(cs *channelSection) { cs.ChannelId = channelId } } func WithHl(hl string) Option { return func(cs *channelSection) { cs.Hl = hl } } func WithMine(mine *bool) Option { return func(cs *channelSection) { if mine != nil { cs.Mine = mine } } } func WithOnBehalfOfContentOwner(onBehalfOfContentOwner string) Option { return func(cs *channelSection) { cs.OnBehalfOfContentOwner = onBehalfOfContentOwner } } func WithService(svc *youtube.Service) Option { return func(_ *channelSection) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package comment import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetComment = errors.New("failed to get comment") errMarkAsSpam = errors.New("failed to mark comment as spam") errDeleteComment = errors.New("failed to delete comment") errInsertComment = errors.New("failed to insert comment") errUpdateComment = errors.New("failed to update comment") errSetModerationStatus = errors.New("failed to set comment moderation status") ) type comment struct { IDs []string `yaml:"ids" json:"ids"` AuthorChannelId string `yaml:"author_channel_id" json:"author_channel_id"` CanRate *bool `yaml:"can_rate" json:"can_rate"` ChannelId string `yaml:"channel_id" json:"channel_id"` MaxResults int64 `yaml:"max_results" json:"max_results"` ParentId string `yaml:"parent_id" json:"parent_id"` TextFormat string `yaml:"text_format" json:"text_format"` TextOriginal string `yaml:"text_original" json:"text_original"` ModerationStatus string `yaml:"moderation_status" json:"moderation_status"` BanAuthor *bool `yaml:"ban_author" json:"ban_author"` VideoId string `yaml:"video_id" json:"video_id"` ViewerRating string `yaml:"viewer_rating" json:"viewer_rating"` } type Comment interface { Get([]string) ([]*youtube.Comment, error) List([]string, string, string, io.Writer) error Insert(string, string, io.Writer) error Update(string, string, io.Writer) error Delete(io.Writer) error MarkAsSpam(string, string, io.Writer) error SetModerationStatus(string, string, io.Writer) error } type Option func(*comment) func NewComment(opts ...Option) Comment { c := &comment{} for _, opt := range opts { opt(c) } return c } func (c *comment) Get(parts []string) ([]*youtube.Comment, error) { call := service.Comments.List(parts) if c.IDs[0] != "" { call = call.Id(c.IDs...) } call = call.MaxResults(c.MaxResults) if c.ParentId != "" { call = call.ParentId(c.ParentId) } if c.TextFormat != "" { call = call.TextFormat(c.TextFormat) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetComment, err) } return res.Items, nil } func (c *comment) List( parts []string, output string, jpath string, writer io.Writer, ) error { comments, err := c.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(comments, jpath, writer) case "yaml": utils.PrintYAML(comments, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Author", "Video ID", "Text Display"}) for _, comment := range comments { tb.AppendRow( table.Row{ comment.Id, comment.Snippet.AuthorDisplayName, comment.Snippet.VideoId, comment.Snippet.TextDisplay, }, ) } } return nil } func (c *comment) Insert(output string, jpath string, writer io.Writer) error { comment := &youtube.Comment{ Snippet: &youtube.CommentSnippet{ AuthorChannelId: &youtube.CommentSnippetAuthorChannelId{ Value: c.AuthorChannelId, }, ChannelId: c.ChannelId, ParentId: c.ParentId, TextOriginal: c.TextOriginal, VideoId: c.VideoId, }, } if c.CanRate != nil { comment.Snippet.CanRate = *c.CanRate } call := service.Comments.Insert([]string{"snippet"}, comment) res, err := call.Do() if err != nil { return errors.Join(errInsertComment, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Comment inserted: %s\n", res.Id) } return nil } func (c *comment) Update(output string, jpath string, writer io.Writer) error { comments, err := c.Get([]string{"id", "snippet"}) if err != nil { return errors.Join(errUpdateComment, err) } if len(comments) == 0 { return errGetComment } comment := comments[0] if c.CanRate != nil { comment.Snippet.CanRate = *c.CanRate } if c.TextOriginal != "" { comment.Snippet.TextOriginal = c.TextOriginal } if c.ViewerRating != "" { comment.Snippet.ViewerRating = c.ViewerRating } call := service.Comments.Update([]string{"snippet"}, comment) res, err := call.Do() if err != nil { return errors.Join(errUpdateComment, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Comment updated: %s\n", res.Id) } return nil } func (c *comment) MarkAsSpam( output string, jpath string, writer io.Writer, ) error { call := service.Comments.MarkAsSpam(c.IDs) err := call.Do() if err != nil { return errors.Join(errMarkAsSpam, err) } switch output { case "json": utils.PrintJSON(c, jpath, writer) case "yaml": utils.PrintYAML(c, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Comment marked as spam: %s\n", c.IDs) } return nil } func (c *comment) SetModerationStatus( output string, jpath string, writer io.Writer, ) error { call := service.Comments.SetModerationStatus(c.IDs, c.ModerationStatus) if c.BanAuthor != nil { call = call.BanAuthor(*c.BanAuthor) } err := call.Do() if err != nil { return errors.Join(errSetModerationStatus, err) } switch output { case "json": utils.PrintJSON(c, jpath, writer) case "yaml": utils.PrintYAML(c, jpath, writer) case "silent": default: _, _ = fmt.Fprintf( writer, "Comment moderation status set to %s: %s\n", c.ModerationStatus, c.IDs, ) } return nil } func (c *comment) Delete(writer io.Writer) error { for _, id := range c.IDs { call := service.Comments.Delete(id) err := call.Do() if err != nil { return errors.Join(errDeleteComment, err) } _, _ = fmt.Fprintf(writer, "Comment %s deleted\n", id) } return nil } func WithIDs(ids []string) Option { return func(c *comment) { c.IDs = ids } } func WithAuthorChannelId(authorChannelId string) Option { return func(c *comment) { c.AuthorChannelId = authorChannelId } } func WithCanRate(canRate *bool) Option { return func(c *comment) { if canRate != nil { c.CanRate = canRate } } } func WithChannelId(channelId string) Option { return func(c *comment) { c.ChannelId = channelId } } func WithMaxResults(maxResults int64) Option { return func(c *comment) { if maxResults <= 0 { maxResults = 1 } c.MaxResults = maxResults } } func WithParentId(parentId string) Option { return func(c *comment) { c.ParentId = parentId } } func WithTextFormat(textFormat string) Option { return func(c *comment) { c.TextFormat = textFormat } } func WithTextOriginal(textOriginal string) Option { return func(c *comment) { c.TextOriginal = textOriginal } } func WithModerationStatus(moderationStatus string) Option { return func(c *comment) { c.ModerationStatus = moderationStatus } } func WithBanAuthor(banAuthor *bool) Option { return func(c *comment) { if banAuthor != nil { c.BanAuthor = banAuthor } } } func WithVideoId(videoId string) Option { return func(c *comment) { c.VideoId = videoId } } func WithViewerRating(viewerRating string) Option { return func(c *comment) { c.ViewerRating = viewerRating } } func WithService(svc *youtube.Service) Option { return func(_ *comment) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package commentThread import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetCommentThread = errors.New("failed to get comment thread") errInsertCommentThread = errors.New("failed to insert comment thread") ) type commentThread struct { IDs []string `yaml:"ids" json:"ids"` AllThreadsRelatedToChannelId string `yaml:"all_threads_related_to_channel_id" json:"all_threads_related_to_channel_id"` AuthorChannelId string `yaml:"author_channel_id" json:"author_channel_id"` ChannelId string `yaml:"channel_id" json:"channel_id"` MaxResults int64 `yaml:"max_results" json:"max_results"` ModerationStatus string `yaml:"moderation_status" json:"moderation_status"` Order string `yaml:"order" json:"order"` SearchTerms string `yaml:"search_terms" json:"search_terms"` TextFormat string `yaml:"text_format" json:"text_format"` TextOriginal string `yaml:"text_original" json:"text_original"` VideoId string `yaml:"video_id" json:"video_id"` } type CommentThread interface { Get([]string) ([]*youtube.CommentThread, error) List([]string, string, string, io.Writer) error Insert(output string, s string, writer io.Writer) error } type Option func(*commentThread) func NewCommentThread(opts ...Option) CommentThread { c := &commentThread{} for _, opt := range opts { opt(c) } return c } func (c *commentThread) Get(parts []string) ([]*youtube.CommentThread, error) { call := service.CommentThreads.List(parts) if len(c.IDs) > 0 { call = call.Id(c.IDs...) } if c.AllThreadsRelatedToChannelId != "" { call = call.AllThreadsRelatedToChannelId(c.AllThreadsRelatedToChannelId) } if c.ChannelId != "" { call = call.ChannelId(c.ChannelId) } call = call.MaxResults(c.MaxResults) if c.ModerationStatus != "" { call = call.ModerationStatus(c.ModerationStatus) } if c.Order != "" { call = call.Order(c.Order) } if c.SearchTerms != "" { call = call.SearchTerms(c.SearchTerms) } if c.TextFormat != "" { call = call.TextFormat(c.TextFormat) } if c.VideoId != "" { call = call.VideoId(c.VideoId) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetCommentThread, err) } return res.Items, nil } func (c *commentThread) List( parts []string, output string, jpath string, writer io.Writer, ) error { commentThreads, err := c.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(commentThreads, jpath, writer) case "yaml": utils.PrintYAML(commentThreads, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Author", "Video ID", "Text Display"}) for _, cot := range commentThreads { snippet := cot.Snippet.TopLevelComment.Snippet tb.AppendRow( table.Row{ cot.Id, snippet.AuthorDisplayName, snippet.VideoId, snippet.TextDisplay, }, ) } } return nil } func (c *commentThread) Insert( output string, jpath string, writer io.Writer, ) error { ct := &youtube.CommentThread{ Snippet: &youtube.CommentThreadSnippet{ ChannelId: c.ChannelId, TopLevelComment: &youtube.Comment{ Snippet: &youtube.CommentSnippet{ AuthorChannelId: &youtube.CommentSnippetAuthorChannelId{ Value: c.AuthorChannelId, }, ChannelId: c.ChannelId, TextOriginal: c.TextOriginal, VideoId: c.VideoId, }, }, }, } res, err := service.CommentThreads.Insert([]string{"snippet"}, ct).Do() if err != nil { return errors.Join(errInsertCommentThread, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "CommentThread inserted: %s\n", res.Id) } return nil } func WithAllThreadsRelatedToChannelId(allThreadsRelatedToChannelId string) Option { return func(c *commentThread) { c.AllThreadsRelatedToChannelId = allThreadsRelatedToChannelId } } func WithAuthorChannelId(authorChannelId string) Option { return func(c *commentThread) { c.AuthorChannelId = authorChannelId } } func WithChannelId(channelId string) Option { return func(c *commentThread) { c.ChannelId = channelId } } func WithIDs(ids []string) Option { return func(c *commentThread) { c.IDs = ids } } func WithMaxResults(maxResults int64) Option { return func(c *commentThread) { if maxResults <= 0 { maxResults = 1 } c.MaxResults = maxResults } } func WithModerationStatus(moderationStatus string) Option { return func(c *commentThread) { c.ModerationStatus = moderationStatus } } func WithOrder(order string) Option { return func(c *commentThread) { c.Order = order } } func WithSearchTerms(searchTerms string) Option { return func(c *commentThread) { c.SearchTerms = searchTerms } } func WithTextFormat(textFormat string) Option { return func(c *commentThread) { c.TextFormat = textFormat } } func WithTextOriginal(textOriginal string) Option { return func(c *commentThread) { c.TextOriginal = textOriginal } } func WithVideoId(videoId string) Option { return func(c *commentThread) { c.VideoId = videoId } } func WithService(svc *youtube.Service) Option { return func(_ *commentThread) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package i18nLanguage import ( "errors" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetI18nLanguage = errors.New("failed to get i18n language") ) type i18nLanguage struct { Hl string `yaml:"hl" json:"hl"` } type I18nLanguage interface { Get([]string) ([]*youtube.I18nLanguage, error) List([]string, string, string, io.Writer) error } type Option func(*i18nLanguage) func NewI18nLanguage(opts ...Option) I18nLanguage { i := &i18nLanguage{} for _, opt := range opts { opt(i) } return i } func (i *i18nLanguage) Get(parts []string) ([]*youtube.I18nLanguage, error) { call := service.I18nLanguages.List(parts) if i.Hl != "" { call = call.Hl(i.Hl) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetI18nLanguage, err) } return res.Items, nil } func (i *i18nLanguage) List( parts []string, output string, jpath string, writer io.Writer, ) error { i18nLanguages, err := i.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(i18nLanguages, jpath, writer) case "yaml": utils.PrintYAML(i18nLanguages, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Hl", "Name"}) for _, lang := range i18nLanguages { tb.AppendRow(table.Row{lang.Id, lang.Snippet.Hl, lang.Snippet.Name}) } } return nil } func WithHl(hl string) Option { return func(i *i18nLanguage) { i.Hl = hl } } func WithService(svc *youtube.Service) Option { return func(_ *i18nLanguage) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package i18nRegion import ( "errors" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetI18nRegion = errors.New("failed to get i18n region") ) type i18nRegion struct { Hl string `yaml:"hl" json:"hl"` } type I18nRegion interface { Get([]string) ([]*youtube.I18nRegion, error) List([]string, string, string, io.Writer) error } type Option func(*i18nRegion) func NewI18nRegion(opts ...Option) I18nRegion { i := &i18nRegion{} for _, opt := range opts { opt(i) } return i } func (i *i18nRegion) Get(parts []string) ([]*youtube.I18nRegion, error) { call := service.I18nRegions.List(parts) if i.Hl != "" { call = call.Hl(i.Hl) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetI18nRegion, err) } return res.Items, nil } func (i *i18nRegion) List( parts []string, output string, jpath string, writer io.Writer, ) error { i18nRegions, err := i.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(i18nRegions, jpath, writer) case "yaml": utils.PrintYAML(i18nRegions, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Gl", "Name"}) for _, region := range i18nRegions { tb.AppendRow(table.Row{region.Id, region.Snippet.Gl, region.Snippet.Name}) } } return nil } func WithHl(hl string) Option { return func(i *i18nRegion) { i.Hl = hl } } func WithService(svc *youtube.Service) Option { return func(_ *i18nRegion) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package member import ( "errors" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetMember = errors.New("failed to get member") ) type member struct { MemberChannelId string `yaml:"member_channel_id" json:"member_channel_id"` HasAccessToLevel string `yaml:"has_access_to_level" json:"has_access_to_level"` MaxResults int64 `yaml:"max_results" json:"max_results"` Mode string `yaml:"mode" json:"mode"` } type Member interface { List([]string, string, string, io.Writer) error Get([]string) ([]*youtube.Member, error) } type Option func(*member) func NewMember(opts ...Option) Member { m := &member{} for _, opt := range opts { opt(m) } return m } func (m *member) Get(parts []string) ([]*youtube.Member, error) { call := service.Members.List(parts) if m.MemberChannelId != "" { call = call.FilterByMemberChannelId(m.MemberChannelId) } if m.HasAccessToLevel != "" { call = call.HasAccessToLevel(m.HasAccessToLevel) } call = call.MaxResults(m.MaxResults) if m.Mode != "" { call = call.Mode(m.Mode) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetMember, err) } return res.Items, nil } func (m *member) List( parts []string, output string, jpath string, writer io.Writer, ) error { members, err := m.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(members, jpath, writer) case "yaml": utils.PrintYAML(members, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"Channel ID", "Display Name"}) for _, member := range members { tb.AppendRow( table.Row{ member.Snippet.MemberDetails.ChannelId, member.Snippet.MemberDetails.DisplayName, }, ) } } return nil } func WithMemberChannelId(channelId string) Option { return func(m *member) { m.MemberChannelId = channelId } } func WithHasAccessToLevel(level string) Option { return func(m *member) { m.HasAccessToLevel = level } } func WithMaxResults(maxResults int64) Option { return func(m *member) { if maxResults <= 0 { maxResults = 1 } m.MaxResults = maxResults } } func WithMode(mode string) Option { return func(m *member) { m.Mode = mode } } func WithService(svc *youtube.Service) Option { return func(_ *member) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package membershipsLevel import ( "errors" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetMembershipsLevel = errors.New("failed to get memberships level") ) type membershipsLevel struct{} type MembershipsLevel interface { List([]string, string, string, io.Writer) error Get([]string) ([]*youtube.MembershipsLevel, error) } type Option func(*membershipsLevel) func NewMembershipsLevel(opts ...Option) MembershipsLevel { m := &membershipsLevel{} for _, opt := range opts { opt(m) } return m } func (m *membershipsLevel) Get(parts []string) ( []*youtube.MembershipsLevel, error, ) { call := service.MembershipsLevels.List(parts) res, err := call.Do() if err != nil { return nil, errors.Join(errGetMembershipsLevel, err) } return res.Items, nil } func (m *membershipsLevel) List( parts []string, output string, jpath string, writer io.Writer, ) error { membershipsLevels, err := m.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(membershipsLevels, jpath, writer) case "yaml": utils.PrintYAML(membershipsLevels, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Display Name"}) for _, ml := range membershipsLevels { tb.AppendRow(table.Row{ml.Id, ml.Snippet.LevelDetails.DisplayName}) } } return nil } func WithService(svc *youtube.Service) Option { return func(_ *membershipsLevel) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package playlist import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetPlaylist = errors.New("failed to get playlist") errInsertPlaylist = errors.New("failed to insert playlist") errUpdatePlaylist = errors.New("failed to update playlist") errDeletePlaylist = errors.New("failed to delete playlist") ) type playlist struct { IDs []string `yaml:"ids" json:"ids"` Title string `yaml:"title" json:"title"` Description string `yaml:"description" json:"description"` Hl string `yaml:"hl" json:"hl"` MaxResults int64 `yaml:"max_results" json:"max_results"` Mine *bool `yaml:"mine" json:"mine"` Tags []string `yaml:"tags" json:"tags"` Language string `yaml:"language" json:"language"` ChannelId string `yaml:"channel_id" json:"channel_id"` Privacy string `yaml:"privacy" json:"privacy"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` OnBehalfOfContentOwnerChannel string `yaml:"on_behalf_of_content_owner_channel" json:"on_behalf_of_content_owner_channel"` } type Playlist interface { List([]string, string, string, io.Writer) error Insert(string, string, io.Writer) error Update(string, string, io.Writer) error Delete(io.Writer) error Get([]string) ([]*youtube.Playlist, error) } type Option func(*playlist) func NewPlaylist(opts ...Option) Playlist { p := &playlist{} for _, opt := range opts { opt(p) } return p } func (p *playlist) Get(parts []string) ([]*youtube.Playlist, error) { call := service.Playlists.List(parts) if len(p.IDs) > 0 { call = call.Id(p.IDs...) } if p.Hl != "" { call = call.Hl(p.Hl) } if p.Mine != nil { call = call.Mine(*p.Mine) } call = call.MaxResults(p.MaxResults) if p.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(p.OnBehalfOfContentOwner) } if p.OnBehalfOfContentOwnerChannel != "" { call = call.OnBehalfOfContentOwnerChannel(p.OnBehalfOfContentOwnerChannel) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetPlaylist, err) } return res.Items, nil } func (p *playlist) List( parts []string, output string, jpath string, writer io.Writer, ) error { playlists, err := p.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(playlists, jpath, writer) case "yaml": utils.PrintYAML(playlists, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Channel ID", "Title"}) for _, pl := range playlists { tb.AppendRow(table.Row{pl.Id, pl.Snippet.ChannelId, pl.Snippet.Title}) } } return nil } func (p *playlist) Insert(output string, jpath string, writer io.Writer) error { upload := &youtube.Playlist{ Snippet: &youtube.PlaylistSnippet{ Title: p.Title, Description: p.Description, Tags: p.Tags, DefaultLanguage: p.Language, ChannelId: p.ChannelId, }, Status: &youtube.PlaylistStatus{ PrivacyStatus: p.Privacy, }, } call := service.Playlists.Insert([]string{"snippet", "status"}, upload) res, err := call.Do() if err != nil { return errors.Join(errInsertPlaylist, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Playlist inserted: %s\n", res.Id) } return nil } func (p *playlist) Update(output string, jpath string, writer io.Writer) error { playlists, err := p.Get([]string{"id", "snippet", "status"}) if err != nil { return errors.Join(errUpdatePlaylist, err) } if len(playlists) == 0 { return errGetPlaylist } playlist := playlists[0] if p.Title != "" { playlist.Snippet.Title = p.Title } if p.Description != "" { playlist.Snippet.Description = p.Description } if p.Tags != nil { playlist.Snippet.Tags = p.Tags } if p.Language != "" { playlist.Snippet.DefaultLanguage = p.Language } if p.Privacy != "" { playlist.Status.PrivacyStatus = p.Privacy } call := service.Playlists.Update([]string{"snippet", "status"}, playlist) res, err := call.Do() if err != nil { return errors.Join(errUpdatePlaylist, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Playlist updated: %s\n", res.Id) } return nil } func (p *playlist) Delete(writer io.Writer) error { for _, id := range p.IDs { call := service.Playlists.Delete(id) if p.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(p.OnBehalfOfContentOwner) } err := call.Do() if err != nil { return errors.Join(errDeletePlaylist, err) } _, _ = fmt.Fprintf(writer, "Playlist %s deleted", id) } return nil } func WithIDs(ids []string) Option { return func(p *playlist) { p.IDs = ids } } func WithTitle(title string) Option { return func(p *playlist) { p.Title = title } } func WithDescription(description string) Option { return func(p *playlist) { p.Description = description } } func WithTags(tags []string) Option { return func(p *playlist) { p.Tags = tags } } func WithLanguage(language string) Option { return func(p *playlist) { p.Language = language } } func WithChannelId(channelId string) Option { return func(p *playlist) { p.ChannelId = channelId } } func WithPrivacy(privacy string) Option { return func(p *playlist) { p.Privacy = privacy } } func WithHl(hl string) Option { return func(p *playlist) { p.Hl = hl } } func WithMaxResults(maxResults int64) Option { return func(p *playlist) { if maxResults <= 0 { maxResults = 1 } p.MaxResults = maxResults } } func WithMine(mine *bool) Option { return func(p *playlist) { if mine != nil { p.Mine = mine } } } func WithOnBehalfOfContentOwner(contentOwner string) Option { return func(p *playlist) { p.OnBehalfOfContentOwner = contentOwner } } func WithOnBehalfOfContentOwnerChannel(channel string) Option { return func(p *playlist) { p.OnBehalfOfContentOwnerChannel = channel } } func WithService(svc *youtube.Service) Option { return func(_ *playlist) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package playlistImage import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetPlaylistImage = errors.New("failed to get playlist image") errInsertPlaylistImage = errors.New("failed to insert playlist image") errUpdatePlaylistImage = errors.New("failed to update playlist image") errDeletePlaylistImage = errors.New("failed to delete playlist image") ) type playlistImage struct { IDs []string `yaml:"ids" json:"ids"` Height int64 `yaml:"height" json:"height"` PlaylistID string `yaml:"playlistId" json:"playlistId"` Type string `yaml:"type" json:"type"` Width int64 `yaml:"width" json:"width"` File string `yaml:"file" json:"file"` Parent string `yaml:"parent" json:"parent"` MaxResults int64 `yaml:"max_results" json:"max_results"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` OnBehalfOfContentOwnerChannel string `yaml:"on_behalf_of_content_owner_channel" json:"on_behalf_of_content_owner_channel"` } type PlaylistImage interface { Get([]string) ([]*youtube.PlaylistImage, error) List([]string, string, string, io.Writer) error Insert(string, string, io.Writer) error Update(string, string, io.Writer) error Delete(io.Writer) error } type Option func(*playlistImage) func NewPlaylistImage(opts ...Option) PlaylistImage { pi := &playlistImage{} for _, opt := range opts { opt(pi) } return pi } func (pi *playlistImage) Get(parts []string) ([]*youtube.PlaylistImage, error) { call := service.PlaylistImages.List() call = call.Part(parts...) if pi.Parent != "" { call = call.Parent(pi.Parent) } if pi.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(pi.OnBehalfOfContentOwner) } if pi.OnBehalfOfContentOwnerChannel != "" { call = call.OnBehalfOfContentOwnerChannel(pi.OnBehalfOfContentOwnerChannel) } call = call.MaxResults(pi.MaxResults) res, err := call.Do() if err != nil { return nil, errors.Join(errGetPlaylistImage, err) } return res.Items, nil } func (pi *playlistImage) List( parts []string, output string, jpath string, writer io.Writer, ) error { playlistImages, err := pi.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(playlistImages, jpath, writer) case "yaml": utils.PrintYAML(playlistImages, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Kind", "Playlist ID", "Type"}) for _, img := range playlistImages { tb.AppendRow( table.Row{img.Id, img.Kind, img.Snippet.PlaylistId, img.Snippet.Type}, ) } } return nil } func (pi *playlistImage) Insert( output string, jpath string, writer io.Writer, ) error { file, err := pkg.Root.Open(pi.File) if err != nil { return errors.Join(errInsertPlaylistImage, err) } defer file.Close() playlistImage := &youtube.PlaylistImage{ Kind: "youtube#playlistImages", Snippet: &youtube.PlaylistImageSnippet{ PlaylistId: pi.PlaylistID, Type: pi.Type, Height: pi.Height, Width: pi.Width, }, } call := service.PlaylistImages.Insert(playlistImage) if pi.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(pi.OnBehalfOfContentOwner) } if pi.OnBehalfOfContentOwnerChannel != "" { call = call.OnBehalfOfContentOwnerChannel(pi.OnBehalfOfContentOwnerChannel) } call = call.Media(file) call = call.Part("kind", "snippet") res, err := call.Do() if err != nil { return errors.Join(errInsertPlaylistImage, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "PlaylistImage inserted: %s\n", res.Id) } return nil } func (pi *playlistImage) Update( output string, jpath string, writer io.Writer, ) error { playlistImages, err := pi.Get([]string{"id", "kind", "snippet"}) if err != nil { return errors.Join(errUpdatePlaylistImage, err) } if len(playlistImages) == 0 { return errGetPlaylistImage } playlistImage := playlistImages[0] if pi.PlaylistID != "" { playlistImage.Snippet.PlaylistId = pi.PlaylistID } if pi.Type != "" { playlistImage.Snippet.Type = pi.Type } if pi.Height != 0 { playlistImage.Snippet.Height = pi.Height } if pi.Width != 0 { playlistImage.Snippet.Width = pi.Width } call := service.PlaylistImages.Update(playlistImage) if pi.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(pi.OnBehalfOfContentOwner) } if pi.File != "" { file, err := pkg.Root.Open(pi.File) if err != nil { return errors.Join(errUpdatePlaylistImage, err) } defer file.Close() call = call.Media(file) } call = call.Part("id", "kind", "snippet") res, err := call.Do() if err != nil { return errors.Join(errUpdatePlaylistImage, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "PlaylistImage updated: %s\n", res.Id) } return nil } func (pi *playlistImage) Delete(writer io.Writer) error { for _, id := range pi.IDs { call := service.PlaylistImages.Delete() call = call.Id(id) if pi.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(pi.OnBehalfOfContentOwner) } err := call.Do() if err != nil { return errors.Join(errDeletePlaylistImage, err) } _, _ = fmt.Fprintf(writer, "PlaylistImage %s deleted\n", id) } return nil } func WithIDs(ids []string) Option { return func(pi *playlistImage) { pi.IDs = ids } } func WithHeight(height int64) Option { return func(pi *playlistImage) { pi.Height = height } } func WithPlaylistID(playlistID string) Option { return func(pi *playlistImage) { pi.PlaylistID = playlistID } } func WithType(t string) Option { return func(pi *playlistImage) { pi.Type = t } } func WithWidth(width int64) Option { return func(pi *playlistImage) { pi.Width = width } } func WithFile(file string) Option { return func(pi *playlistImage) { pi.File = file } } func WithParent(parent string) Option { return func(pi *playlistImage) { pi.Parent = parent } } func WithMaxResults(maxResults int64) Option { return func(pi *playlistImage) { if maxResults <= 0 { maxResults = 1 } pi.MaxResults = maxResults } } func WithOnBehalfOfContentOwner(onBehalfOfContentOwner string) Option { return func(pi *playlistImage) { pi.OnBehalfOfContentOwner = onBehalfOfContentOwner } } func WithOnBehalfOfContentOwnerChannel(onBehalfOfContentOwnerChannel string) Option { return func(pi *playlistImage) { pi.OnBehalfOfContentOwnerChannel = onBehalfOfContentOwnerChannel } } func WithService(svc *youtube.Service) Option { return func(pi *playlistImage) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package playlistItem import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetPlaylistItem = errors.New("failed to get playlist item") errUpdatePlaylistItem = errors.New("failed to update playlist item") errInsertPlaylistItem = errors.New("failed to insert playlist item") errDeletePlaylistItem = errors.New("failed to delete playlist item") ) type playlistItem struct { IDs []string `yaml:"ids" json:"ids"` Title string `yaml:"title" json:"title"` Description string `yaml:"description" json:"description"` Kind string `yaml:"kind" json:"kind"` KVideoId string `yaml:"k_video_id" json:"k_video_id"` KChannelId string `yaml:"k_channel_id" json:"k_channel_id"` KPlaylistId string `yaml:"k_playlist_id" json:"k_playlist_id"` VideoId string `yaml:"video_id" json:"video_id"` PlaylistId string `yaml:"playlist_id" json:"playlist_id"` ChannelId string `yaml:"channel_id" json:"channel_id"` Privacy string `yaml:"privacy" json:"privacy"` MaxResults int64 `yaml:"max_results" json:"max_results"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` } type PlaylistItem interface { List([]string, string, string, io.Writer) error Insert(string, string, io.Writer) error Update(string, string, io.Writer) error Delete(io.Writer) error Get([]string) ([]*youtube.PlaylistItem, error) } type Option func(*playlistItem) func NewPlaylistItem(opts ...Option) PlaylistItem { p := &playlistItem{} for _, opt := range opts { opt(p) } return p } func (pi *playlistItem) Get(parts []string) ([]*youtube.PlaylistItem, error) { call := service.PlaylistItems.List(parts) if len(pi.IDs) > 0 { call = call.Id(pi.IDs...) } if pi.PlaylistId != "" { call = call.PlaylistId(pi.PlaylistId) } if pi.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(pi.OnBehalfOfContentOwner) } if pi.VideoId != "" { call = call.VideoId(pi.VideoId) } call = call.MaxResults(pi.MaxResults) res, err := call.Do() if err != nil { return nil, errors.Join(errGetPlaylistItem, err) } return res.Items, nil } func (pi *playlistItem) List( parts []string, output string, jpath string, writer io.Writer, ) error { playlistItems, err := pi.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(playlistItems, jpath, writer) case "yaml": utils.PrintYAML(playlistItems, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Title", "Kind", "Resource ID"}) for _, item := range playlistItems { var resourceId string switch item.Snippet.ResourceId.Kind { case "youtube#video": resourceId = item.Snippet.ResourceId.VideoId case "youtube#channel": resourceId = item.Snippet.ResourceId.ChannelId case "youtube#playlist": resourceId = item.Snippet.ResourceId.PlaylistId } tb.AppendRow( table.Row{ item.Id, item.Snippet.Title, item.Snippet.ResourceId.Kind, resourceId, }, ) } } return nil } func (pi *playlistItem) Insert( output string, jpath string, writer io.Writer, ) error { var resourceId *youtube.ResourceId switch pi.Kind { case "video": resourceId = &youtube.ResourceId{ Kind: "youtube#video", VideoId: pi.KVideoId, } case "channel": resourceId = &youtube.ResourceId{ Kind: "youtube#channel", ChannelId: pi.KChannelId, } case "playlist": resourceId = &youtube.ResourceId{ Kind: "youtube#playlist", PlaylistId: pi.KPlaylistId, } } playlistItem := &youtube.PlaylistItem{ Snippet: &youtube.PlaylistItemSnippet{ Title: pi.Title, Description: pi.Description, ResourceId: resourceId, PlaylistId: pi.PlaylistId, ChannelId: pi.ChannelId, }, Status: &youtube.PlaylistItemStatus{ PrivacyStatus: pi.Privacy, }, } call := service.PlaylistItems.Insert( []string{"snippet", "status"}, playlistItem, ) if pi.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(pi.OnBehalfOfContentOwner) } res, err := call.Do() if err != nil { return errors.Join(errInsertPlaylistItem, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Playlist Item inserted: %s\n", res.Id) } return nil } func (pi *playlistItem) Update( output string, jpath string, writer io.Writer, ) error { playlistItems, err := pi.Get([]string{"id", "snippet", "status"}) if err != nil { return errors.Join(errUpdatePlaylistItem, err) } if len(playlistItems) == 0 { return errGetPlaylistItem } playlistItem := playlistItems[0] if pi.Title != "" { playlistItem.Snippet.Title = pi.Title } if pi.Description != "" { playlistItem.Snippet.Description = pi.Description } if pi.Privacy != "" { playlistItem.Status.PrivacyStatus = pi.Privacy } call := service.PlaylistItems.Update( []string{"snippet", "status"}, playlistItem, ) if pi.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(pi.OnBehalfOfContentOwner) } res, err := call.Do() if err != nil { return errors.Join(errUpdatePlaylistItem, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Playlist Item updated: %s\n", res.Id) } return nil } func (pi *playlistItem) Delete(writer io.Writer) error { for _, id := range pi.IDs { call := service.PlaylistItems.Delete(id) if pi.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(pi.OnBehalfOfContentOwner) } err := call.Do() if err != nil { return errors.Join(errDeletePlaylistItem, err) } _, _ = fmt.Fprintf(writer, "Playlsit Item %s deleted", id) } return nil } func WithIDs(ids []string) Option { return func(p *playlistItem) { p.IDs = ids } } func WithTitle(title string) Option { return func(p *playlistItem) { p.Title = title } } func WithDescription(description string) Option { return func(p *playlistItem) { p.Description = description } } func WithKind(kind string) Option { return func(p *playlistItem) { p.Kind = kind } } func WithKVideoId(kVideoId string) Option { return func(p *playlistItem) { p.KVideoId = kVideoId } } func WithKChannelId(kChannelId string) Option { return func(p *playlistItem) { p.KChannelId = kChannelId } } func WithKPlaylistId(kPlaylistId string) Option { return func(p *playlistItem) { p.KPlaylistId = kPlaylistId } } func WithVideoId(videoId string) Option { return func(p *playlistItem) { p.VideoId = videoId } } func WithPlaylistId(playlistId string) Option { return func(p *playlistItem) { p.PlaylistId = playlistId } } func WithChannelId(channelId string) Option { return func(p *playlistItem) { p.ChannelId = channelId } } func WithPrivacy(privacy string) Option { return func(p *playlistItem) { p.Privacy = privacy } } func WithMaxResults(maxResults int64) Option { return func(p *playlistItem) { if maxResults <= 0 { maxResults = 1 } p.MaxResults = maxResults } } func WithOnBehalfOfContentOwner(onBehalfOfContentOwner string) Option { return func(p *playlistItem) { p.OnBehalfOfContentOwner = onBehalfOfContentOwner } } func WithService(svc *youtube.Service) Option { return func(_ *playlistItem) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package search import ( "errors" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetSearch = errors.New("failed to get search") ) type search struct { ChannelId string `yaml:"channel_id" json:"channel_id"` ChannelType string `yaml:"channel_type" json:"channel_type"` EventType string `yaml:"event_type" json:"event_type"` ForContentOwner *bool `yaml:"for_content_owner" json:"for_content_owner"` ForDeveloper *bool `yaml:"for_developer" json:"for_developer"` ForMine *bool `yaml:"for_mine" json:"for_mine"` Location string `yaml:"location" json:"location"` LocationRadius string `yaml:"location_radius" json:"location_radius"` MaxResults int64 `yaml:"max_results" json:"max_results"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` Order string `yaml:"order" json:"order"` PublishedAfter string `yaml:"published_after" json:"published_after"` PublishedBefore string `yaml:"published_before" json:"published_before"` Q string `yaml:"q" json:"q"` RegionCode string `yaml:"region_code" json:"region_code"` RelevanceLanguage string `yaml:"relevance_language" json:"relevance_language"` SafeSearch string `yaml:"safe_search" json:"safe_search"` TopicId string `yaml:"topic_id" json:"topic_id"` Types []string `yaml:"types" json:"types"` VideoCaption string `yaml:"video_caption" json:"video_caption"` VideoCategoryId string `yaml:"video_category_id" json:"video_category_id"` VideoDefinition string `yaml:"video_definition" json:"video_definition"` VideoDimension string `yaml:"video_dimension" json:"video_dimension"` VideoDuration string `yaml:"video_duration" json:"video_duration"` VideoEmbeddable string `yaml:"video_embeddable" json:"video_embeddable"` VideoLicense string `yaml:"video_license" json:"video_license"` VideoPaidProductPlacement string `yaml:"video_paid_product_placement" json:"video_paid_product_placement"` VideoSyndicated string `yaml:"video_syndicated" json:"video_syndicated"` VideoType string `yaml:"video_type" json:"video_type"` } type Search interface { Get([]string) ([]*youtube.SearchResult, error) List([]string, string, string, io.Writer) error } type Option func(*search) func NewSearch(opts ...Option) Search { s := &search{} for _, opt := range opts { opt(s) } return s } func (s *search) Get(parts []string) ([]*youtube.SearchResult, error) { call := service.Search.List(parts) if s.ChannelId != "" { call.ChannelId(s.ChannelId) } if s.ChannelType != "" { call.ChannelType(s.ChannelType) } if s.EventType != "" { call.EventType(s.EventType) } if s.ForContentOwner != nil { call.ForContentOwner(*s.ForContentOwner) } if s.ForDeveloper != nil { call.ForDeveloper(*s.ForDeveloper) } if s.ForMine != nil { call.ForMine(*s.ForMine) } if s.Location != "" { call.Location(s.Location) } if s.LocationRadius != "" { call.LocationRadius(s.LocationRadius) } call.MaxResults(s.MaxResults) if s.OnBehalfOfContentOwner != "" { call.OnBehalfOfContentOwner(s.OnBehalfOfContentOwner) } if s.Order != "" { call.Order(s.Order) } if s.PublishedAfter != "" { call.PublishedAfter(s.PublishedAfter) } if s.PublishedBefore != "" { call.PublishedBefore(s.PublishedBefore) } if s.Q != "" { call.Q(s.Q) } if s.RegionCode != "" { call.RegionCode(s.RegionCode) } if s.RelevanceLanguage != "" { call.RelevanceLanguage(s.RelevanceLanguage) } if s.SafeSearch != "" { call.SafeSearch(s.SafeSearch) } if s.TopicId != "" { call.TopicId(s.TopicId) } if len(s.Types) > 0 { call.Type(s.Types...) } if s.VideoCaption != "" { call.VideoCaption(s.VideoCaption) } if s.VideoCategoryId != "" { call.VideoCategoryId(s.VideoCategoryId) } if s.VideoDefinition != "" { call.VideoDefinition(s.VideoDefinition) } if s.VideoDimension != "" { call.VideoDimension(s.VideoDimension) } if s.VideoDuration != "" { call.VideoDuration(s.VideoDuration) } if s.VideoEmbeddable != "" { call.VideoEmbeddable(s.VideoEmbeddable) } if s.VideoLicense != "" { call.VideoLicense(s.VideoLicense) } if s.VideoPaidProductPlacement != "" { call.VideoPaidProductPlacement(s.VideoPaidProductPlacement) } if s.VideoSyndicated != "" { call.VideoSyndicated(s.VideoSyndicated) } if s.VideoType != "" { call.VideoType(s.VideoType) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetSearch, err) } return res.Items, nil } func (s *search) List( parts []string, output string, jpath string, writer io.Writer, ) error { results, err := s.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(results, jpath, writer) case "yaml": utils.PrintYAML(results, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"Kind", "Title", "Resource ID"}) for _, result := range results { var resourceId string switch result.Id.Kind { case "youtube#video": resourceId = result.Id.VideoId case "youtube#channel": resourceId = result.Id.ChannelId case "youtube#playlist": resourceId = result.Id.PlaylistId } tb.AppendRow( table.Row{result.Id.Kind, result.Snippet.Title, resourceId}, ) } } return nil } func WithChannelId(channelId string) Option { return func(s *search) { s.ChannelId = channelId } } func WithChannelType(channelType string) Option { return func(s *search) { s.ChannelType = channelType } } func WithEventType(eventType string) Option { return func(s *search) { s.EventType = eventType } } func WithForContentOwner(forContentOwner *bool) Option { return func(s *search) { if forContentOwner != nil { s.ForContentOwner = forContentOwner } } } func WithForDeveloper(forDeveloper *bool) Option { return func(s *search) { if forDeveloper != nil { s.ForDeveloper = forDeveloper } } } func WithForMine(forMine *bool) Option { return func(s *search) { if forMine != nil { s.ForMine = forMine } } } func WithLocation(location string) Option { return func(s *search) { s.Location = location } } func WithLocationRadius(locationRadius string) Option { return func(s *search) { s.LocationRadius = locationRadius } } func WithMaxResults(maxResults int64) Option { return func(s *search) { if maxResults <= 0 { maxResults = 1 } s.MaxResults = maxResults } } func WithOnBehalfOfContentOwner(onBehalfOfContentOwner string) Option { return func(s *search) { s.OnBehalfOfContentOwner = onBehalfOfContentOwner } } func WithOrder(order string) Option { return func(s *search) { s.Order = order } } func WithPublishedAfter(publishedAfter string) Option { return func(s *search) { s.PublishedAfter = publishedAfter } } func WithPublishedBefore(publishedBefore string) Option { return func(s *search) { s.PublishedBefore = publishedBefore } } func WithQ(q string) Option { return func(s *search) { s.Q = q } } func WithRegionCode(regionCode string) Option { return func(s *search) { s.RegionCode = regionCode } } func WithRelevanceLanguage(relevanceLanguage string) Option { return func(s *search) { s.RelevanceLanguage = relevanceLanguage } } func WithSafeSearch(safeSearch string) Option { return func(s *search) { s.SafeSearch = safeSearch } } func WithTopicId(topicId string) Option { return func(s *search) { s.TopicId = topicId } } func WithTypes(types []string) Option { return func(s *search) { s.Types = types } } func WithVideoCaption(videoCaption string) Option { return func(s *search) { s.VideoCaption = videoCaption } } func WithVideoCategoryId(videoCategoryId string) Option { return func(s *search) { s.VideoCategoryId = videoCategoryId } } func WithVideoDefinition(videoDefinition string) Option { return func(s *search) { s.VideoDefinition = videoDefinition } } func WithVideoDimension(videoDimension string) Option { return func(s *search) { s.VideoDimension = videoDimension } } func WithVideoDuration(videoDuration string) Option { return func(s *search) { s.VideoDuration = videoDuration } } func WithVideoEmbeddable(videoEmbeddable string) Option { return func(s *search) { s.VideoEmbeddable = videoEmbeddable } } func WithVideoLicense(videoLicense string) Option { return func(s *search) { s.VideoLicense = videoLicense } } func WithVideoPaidProductPlacement(videoPaidProductPlacement string) Option { return func(s *search) { s.VideoPaidProductPlacement = videoPaidProductPlacement } } func WithVideoSyndicated(videoSyndicated string) Option { return func(s *search) { s.VideoSyndicated = videoSyndicated } } func WithVideoType(videoType string) Option { return func(s *search) { s.VideoType = videoType } } func WithService(svc *youtube.Service) Option { return func(_ *search) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package subscription import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetSubscription = errors.New("failed to get subscription") errDeleteSubscription = errors.New("failed to delete subscription") errInsertSubscription = errors.New("failed to insert subscription") ) type subscription struct { IDs []string `yaml:"ids" json:"ids"` SubscriberChannelId string `yaml:"subscriber_channel_id" json:"subscriber_channel_id"` Description string `yaml:"description" json:"description"` ChannelId string `yaml:"channel_id" json:"channel_id"` ForChannelId string `yaml:"for_channel_id" json:"for_channel_id"` MaxResults int64 `yaml:"max_results" json:"max_results"` Mine *bool `yaml:"mine" json:"mine"` MyRecentSubscribers *bool `yaml:"my_recent_subscribers" json:"my_recent_subscribers"` MySubscribers *bool `yaml:"my_subscribers" json:"my_subscribers"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` OnBehalfOfContentOwnerChannel string `yaml:"on_behalf_of_content_owner_channel" json:"on_behalf_of_content_owner_channel"` Order string `yaml:"order" json:"order"` Title string `yaml:"title" json:"title"` } type Subscription interface { Get([]string) ([]*youtube.Subscription, error) List([]string, string, string, io.Writer) error Insert(string, string, io.Writer) error Delete(io.Writer) error } type Option func(*subscription) func NewSubscription(opts ...Option) Subscription { s := &subscription{} for _, opt := range opts { opt(s) } return s } func (s *subscription) Get(parts []string) ([]*youtube.Subscription, error) { call := service.Subscriptions.List(parts) if len(s.IDs) > 0 { call = call.Id(s.IDs...) } if s.ChannelId != "" { call = call.ChannelId(s.ChannelId) } if s.ForChannelId != "" { call = call.ForChannelId(s.ForChannelId) } call = call.MaxResults(s.MaxResults) if s.Mine != nil { call = call.Mine(*s.Mine) } if s.MyRecentSubscribers != nil { call = call.MyRecentSubscribers(*s.MyRecentSubscribers) } if s.MySubscribers != nil { call = call.MySubscribers(*s.MySubscribers) } if s.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(s.OnBehalfOfContentOwner) } if s.OnBehalfOfContentOwnerChannel != "" { call = call.OnBehalfOfContentOwnerChannel(s.OnBehalfOfContentOwnerChannel) } if s.Order != "" { call = call.Order(s.Order) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetSubscription, err) } return res.Items, nil } func (s *subscription) List( parts []string, output string, jpath string, writer io.Writer, ) error { subscriptions, err := s.Get(parts) if err != nil { return errors.Join(errGetSubscription, err) } switch output { case "json": utils.PrintJSON(subscriptions, jpath, writer) case "yaml": utils.PrintYAML(subscriptions, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Kind", "Resource ID", "Channel Title"}) for _, sub := range subscriptions { var resourceId string switch sub.Snippet.ResourceId.Kind { case "youtube#video": resourceId = sub.Snippet.ResourceId.VideoId case "youtube#channel": resourceId = sub.Snippet.ResourceId.ChannelId case "youtube#playlist": resourceId = sub.Snippet.ResourceId.PlaylistId } tb.AppendRow( table.Row{ sub.Id, sub.Snippet.ResourceId.Kind, resourceId, sub.Snippet.Title, }, ) } } return nil } func (s *subscription) Insert( output string, jpath string, writer io.Writer, ) error { subscription := &youtube.Subscription{ Snippet: &youtube.SubscriptionSnippet{ ChannelId: s.SubscriberChannelId, Description: s.Description, ResourceId: &youtube.ResourceId{ ChannelId: s.ChannelId, }, Title: s.Title, }, } call := service.Subscriptions.Insert([]string{"snippet"}, subscription) res, err := call.Do() if err != nil { return errors.Join(errInsertSubscription, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) default: _, _ = fmt.Fprintf(writer, "Subscription inserted: %s\n", res.Id) } return nil } func (s *subscription) Delete(writer io.Writer) error { for _, id := range s.IDs { call := service.Subscriptions.Delete(id) err := call.Do() if err != nil { return errors.Join(errDeleteSubscription, err) } _, _ = fmt.Fprintf(writer, "Subscription %s deleted", id) } return nil } func WithIDs(ids []string) Option { return func(s *subscription) { s.IDs = ids } } func WithSubscriberChannelId(id string) Option { return func(s *subscription) { s.SubscriberChannelId = id } } func WithDescription(description string) Option { return func(s *subscription) { s.Description = description } } func WithChannelId(channelId string) Option { return func(s *subscription) { s.ChannelId = channelId } } func WithForChannelId(forChannelId string) Option { return func(s *subscription) { s.ForChannelId = forChannelId } } func WithMaxResults(maxResults int64) Option { return func(s *subscription) { if maxResults <= 0 { maxResults = 1 } s.MaxResults = maxResults } } func WithMine(mine *bool) Option { return func(s *subscription) { if mine != nil { s.Mine = mine } } } func WithMyRecentSubscribers(myRecentSubscribers *bool) Option { return func(s *subscription) { if myRecentSubscribers != nil { s.MyRecentSubscribers = myRecentSubscribers } } } func WithMySubscribers(mySubscribers *bool) Option { return func(s *subscription) { if mySubscribers != nil { s.MySubscribers = mySubscribers } } } func WithOnBehalfOfContentOwner(onBehalfOfContentOwner string) Option { return func(s *subscription) { s.OnBehalfOfContentOwner = onBehalfOfContentOwner } } func WithOnBehalfOfContentOwnerChannel(onBehalfOfContentOwnerChannel string) Option { return func(s *subscription) { s.OnBehalfOfContentOwnerChannel = onBehalfOfContentOwnerChannel } } func WithOrder(order string) Option { return func(s *subscription) { s.Order = order } } func WithTitle(title string) Option { return func(s *subscription) { s.Title = title } } func WithService(svc *youtube.Service) Option { return func(_ *subscription) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package superChatEvent import ( "errors" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetSuperChatEvent = errors.New("failed to get super chat event") ) type superChatEvent struct { Hl string `yaml:"hl" json:"hl"` MaxResults int64 `yaml:"max_results" json:"max_results"` } type SuperChatEvent interface { Get([]string) ([]*youtube.SuperChatEvent, error) List([]string, string, string, io.Writer) error } type Option func(*superChatEvent) func NewSuperChatEvent(opts ...Option) SuperChatEvent { s := &superChatEvent{} for _, opt := range opts { opt(s) } return s } func (s *superChatEvent) Get(parts []string) ([]*youtube.SuperChatEvent, error) { call := service.SuperChatEvents.List(parts) if s.Hl != "" { call = call.Hl(s.Hl) } call = call.MaxResults(s.MaxResults) res, err := call.Do() if err != nil { return nil, errors.Join(errGetSuperChatEvent, err) } return res.Items, nil } func (s *superChatEvent) List( parts []string, output string, jpath string, writer io.Writer, ) error { events, err := s.Get(parts) if err != nil { return errors.Join(errGetSuperChatEvent, err) } switch output { case "json": utils.PrintJSON(events, jpath, writer) case "yaml": utils.PrintYAML(events, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Amount", "Comment", "Supporter"}) for _, event := range events { tb.AppendRow( table.Row{ event.Id, event.Snippet.DisplayString, event.Snippet.CommentText, event.Snippet.SupporterDetails.DisplayName, }, ) } } return nil } func WithHl(hl string) Option { return func(s *superChatEvent) { s.Hl = hl } } func WithMaxResults(maxResults int64) Option { return func(s *superChatEvent) { if maxResults <= 0 { maxResults = 1 } s.MaxResults = maxResults } } func WithService(svc *youtube.Service) Option { return func(_ *superChatEvent) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package thumbnail import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errSetThumbnail = errors.New("failed to set thumbnail") ) type thumbnail struct { File string `yaml:"file" json:"file"` VideoId string `yaml:"video_id" json:"video_id"` } type Thumbnail interface { Set(string, string, io.Writer) error } type Option func(*thumbnail) func NewThumbnail(opts ...Option) Thumbnail { t := &thumbnail{} for _, opt := range opts { opt(t) } return t } func (t *thumbnail) Set(output string, jpath string, writer io.Writer) error { file, err := pkg.Root.Open(t.File) if err != nil { return errors.Join(errSetThumbnail, err) } call := service.Thumbnails.Set(t.VideoId).Media(file) res, err := call.Do() if err != nil { return errors.Join(errSetThumbnail, err) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Thumbnail set for video %s", t.VideoId) } return nil } func WithVideoId(videoId string) Option { return func(t *thumbnail) { t.VideoId = videoId } } func WithFile(file string) Option { return func(t *thumbnail) { t.File = file } } func WithService(svc *youtube.Service) Option { return func(_ *thumbnail) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package utils import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "io" "os/exec" "path/filepath" "regexp" "runtime" "github.com/ohler55/ojg/jp" "github.com/spf13/pflag" "gopkg.in/yaml.v3" ) func PrintJSON(data interface{}, jpath string, writer io.Writer) { j, err := jp.ParseString(jpath) if err != nil && jpath != "" { _, _ = fmt.Fprintln(writer, "Invalid JSONPath:", jpath) return } else if jpath != "" { data = j.Get(data) } marshalled, _ := json.MarshalIndent(data, "", " ") _, _ = fmt.Fprintln(writer, string(marshalled)) } func PrintYAML(data interface{}, jpath string, writer io.Writer) { j, err := jp.ParseString(jpath) if err != nil && jpath != "" { _, _ = fmt.Fprintln(writer, "Invalid JSONPath:", jpath) return } else if jpath != "" { data = j.Get(data) } marshalled, _ := yaml.Marshal(data) _, _ = fmt.Fprintln(writer, string(marshalled)) } func OpenURL(url string) error { var err error switch runtime.GOOS { case "windows": err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() case "linux": err = exec.Command("xdg-open", url).Start() case "darwin": err = exec.Command("open", url).Start() default: err = fmt.Errorf("cannot open URL %s on this platform", url) } return err } func RandomStage() string { b := make([]byte, 128) _, _ = rand.Read(b) state := base64.URLEncoding.EncodeToString(b) return state } func GetFileName(file string) string { base := filepath.Base(file) fileName := base[:len(base)-len(filepath.Ext(base))] return fileName } func IsJson(s string) bool { var js json.RawMessage return json.Unmarshal([]byte(s), &js) == nil } func BoolPtr(b string) *bool { if b == "" { return nil } val := b == "true" return &val } func ResetBool(m map[string]**bool, flagSet *pflag.FlagSet) { for k := range m { flag := flagSet.Lookup(k) if flag != nil && !flag.Changed { *m[k] = nil } } } func ExtractHl(uri string) string { pattern := `i18n://(?:language|region)/([^/]+)` matches := regexp.MustCompile(pattern).FindStringSubmatch(uri) if len(matches) > 1 { return matches[1] } return "" }
package pkg import ( "log/slog" "os" ) const ( PartsUsage = "Comma separated parts" MRUsage = "The maximum number of items that should be returned" TableUsage = "json, yaml, or table" SilentUsage = "json, yaml, or silent" JPUsage = "JSONPath expression to filter the output" JsonMIME = "application/json" ) var ( Root *os.Root Logger *slog.Logger ) func init() { var err error rootDir, ok := os.LookupEnv("YUTU_ROOT") if !ok { rootDir = "/" } Root, err = os.OpenRoot(rootDir) if err != nil { panic(err) } logLevel := slog.LevelInfo if lvl, ok := os.LookupEnv("YUTU_LOG_LEVEL"); ok { switch lvl { case "DEBUG", "debug": logLevel = slog.LevelDebug case "INFO", "info": logLevel = slog.LevelInfo case "WARN", "warn": logLevel = slog.LevelWarn case "ERROR", "error": logLevel = slog.LevelError } } Logger = slog.New( slog.NewTextHandler( os.Stdout, &slog.HandlerOptions{ Level: logLevel, }, ), ) slog.SetDefault(Logger) }
package video import ( "errors" "fmt" "io" "slices" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/playlistItem" "github.com/eat-pray-ai/yutu/pkg/thumbnail" "github.com/jedib0t/go-pretty/v6/table" "github.com/eat-pray-ai/yutu/pkg/utils" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetVideo = errors.New("failed to get video") errInsertVideo = errors.New("failed to insert video") errUpdateVideo = errors.New("failed to update video") errRating = errors.New("failed to rate video") errGetRating = errors.New("failed to get rating") errDeleteVideo = errors.New("failed to delete video") errReportAbuse = errors.New("failed to report abuse") ) type video struct { IDs []string `yaml:"ids" json:"ids"` AutoLevels *bool `yaml:"auto_levels" json:"auto_levels"` File string `yaml:"file" json:"file"` Title string `yaml:"title" json:"title"` Description string `yaml:"description" json:"description"` Hl string `yaml:"hl" json:"hl"` Tags []string `yaml:"tags" json:"tags"` Language string `yaml:"language" json:"language"` Locale string `yaml:"locale" json:"locale"` License string `yaml:"license" json:"license"` Thumbnail string `yaml:"thumbnail" json:"thumbnail"` Rating string `yaml:"rating" json:"rating"` Chart string `yaml:"chart" json:"chart"` ChannelId string `yaml:"channel_id" json:"channel_id"` Comments string `yaml:"comments" json:"comments"` PlaylistId string `yaml:"playlist_id" json:"playlist_id"` CategoryId string `yaml:"category_id" json:"category_id"` Privacy string `yaml:"privacy" json:"privacy"` ForKids *bool `yaml:"for_kids" json:"for_kids"` Embeddable *bool `yaml:"embeddable" json:"embeddable"` PublishAt string `yaml:"publish_at" json:"publish_at"` RegionCode string `yaml:"region_code" json:"region_code"` ReasonId string `yaml:"reason_id" json:"reason_id"` SecondaryReasonId string `yaml:"secondary_reason_id" json:"secondary_reason_id"` Stabilize *bool `yaml:"stabilize" json:"stabilize"` MaxHeight int64 `yaml:"max_height" json:"max_height"` MaxWidth int64 `yaml:"max_width" json:"max_width"` MaxResults int64 `yaml:"max_results" json:"max_results"` NotifySubscribers *bool `yaml:"notify_subscribers" json:"notify_subscribers"` PublicStatsViewable *bool `yaml:"public_stats_viewable" json:"public_stats_viewable"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` OnBehalfOfContentOwnerChannel string `yaml:"on_behalf_of_content_owner_channel" json:"on_behalf_of_content_owner_channel"` } type Video interface { List([]string, string, string, io.Writer) error Insert(string, string, io.Writer) error Update(string, string, io.Writer) error Rate(io.Writer) error GetRating(string, string, io.Writer) error Delete(io.Writer) error ReportAbuse(io.Writer) error Get([]string) ([]*youtube.Video, error) } type Option func(*video) func NewVideo(opts ...Option) Video { v := &video{} for _, opt := range opts { opt(v) } return v } func (v *video) Get(parts []string) ([]*youtube.Video, error) { call := service.Videos.List(parts) if len(v.IDs) > 0 { call = call.Id(v.IDs...) } if v.Chart != "" { call = call.Chart(v.Chart) } if v.Hl != "" { call = call.Hl(v.Hl) } if v.Locale != "" { call = call.Locale(v.Locale) } if v.CategoryId != "" { call = call.VideoCategoryId(v.CategoryId) } if v.Rating != "" { call = call.MyRating(v.Rating) } if v.RegionCode != "" { call = call.RegionCode(v.RegionCode) } if v.MaxHeight != 0 { call = call.MaxHeight(v.MaxHeight) } if v.MaxWidth != 0 { call = call.MaxWidth(v.MaxWidth) } if v.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(v.OnBehalfOfContentOwner) } call = call.MaxResults(v.MaxResults) res, err := call.Do() if err != nil { return nil, errors.Join(errGetVideo, err) } return res.Items, nil } func (v *video) List( parts []string, output string, jpath string, writer io.Writer, ) error { videos, err := v.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(videos, jpath, writer) case "yaml": utils.PrintYAML(videos, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Title", "Channel ID", "Views"}) for _, video := range videos { tb.AppendRow( table.Row{ video.Id, video.Snippet.Title, video.Snippet.ChannelId, video.Statistics.ViewCount, }, ) } } return nil } func (v *video) Insert(output string, jpath string, writer io.Writer) error { file, err := pkg.Root.Open(v.File) if err != nil { return errors.Join(errInsertVideo, err) } defer file.Close() if !slices.Contains(v.Tags, "yutu🐰") { v.Tags = append(v.Tags, "yutu🐰") } if v.Title == "" { v.Title = utils.GetFileName(v.File) } video := &youtube.Video{ Snippet: &youtube.VideoSnippet{ Title: v.Title, Description: v.Description, Tags: v.Tags, CategoryId: v.CategoryId, ChannelId: v.ChannelId, DefaultLanguage: v.Language, DefaultAudioLanguage: v.Language, }, Status: &youtube.VideoStatus{ License: v.License, PublishAt: v.PublishAt, PrivacyStatus: v.Privacy, ForceSendFields: []string{"SelfDeclaredMadeForKids"}, }, } if v.Embeddable != nil { video.Status.Embeddable = *v.Embeddable } if v.ForKids != nil { video.Status.SelfDeclaredMadeForKids = *v.ForKids } if v.PublicStatsViewable != nil { video.Status.PublicStatsViewable = *v.PublicStatsViewable } call := service.Videos.Insert([]string{"snippet,status"}, video) if v.AutoLevels != nil { call = call.AutoLevels(*v.AutoLevels) } if v.NotifySubscribers != nil { call = call.NotifySubscribers(*v.NotifySubscribers) } if v.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(v.OnBehalfOfContentOwner) } if v.OnBehalfOfContentOwnerChannel != "" { call = call.OnBehalfOfContentOwnerChannel(v.OnBehalfOfContentOwnerChannel) } if v.Stabilize != nil { call = call.Stabilize(*v.Stabilize) } res, err := call.Media(file).Do() if err != nil { return errors.Join(errInsertVideo, err) } if v.Thumbnail != "" { t := thumbnail.NewThumbnail( thumbnail.WithVideoId(res.Id), thumbnail.WithFile(v.Thumbnail), thumbnail.WithService(service), ) _ = t.Set("silent", "", nil) } if v.PlaylistId != "" { pi := playlistItem.NewPlaylistItem( playlistItem.WithTitle(res.Snippet.Title), playlistItem.WithDescription(res.Snippet.Description), playlistItem.WithKind("video"), playlistItem.WithKVideoId(res.Id), playlistItem.WithPlaylistId(v.PlaylistId), playlistItem.WithChannelId(res.Snippet.ChannelId), playlistItem.WithPrivacy(res.Status.PrivacyStatus), playlistItem.WithService(service), ) _ = pi.Insert("silent", "", writer) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Video inserted: %s\n", res.Id) } return nil } func (v *video) Update(output string, jpath string, writer io.Writer) error { videos, err := v.Get([]string{"id", "snippet", "status"}) if err != nil { return errors.Join(errUpdateVideo, err) } if len(videos) == 0 { return errGetVideo } video := videos[0] if v.Title != "" { video.Snippet.Title = v.Title } if v.Description != "" { video.Snippet.Description = v.Description } if v.Tags != nil { if !slices.Contains(v.Tags, "yutu🐰") { v.Tags = append(v.Tags, "yutu🐰") } video.Snippet.Tags = v.Tags } if v.Language != "" { video.Snippet.DefaultLanguage = v.Language video.Snippet.DefaultAudioLanguage = v.Language } if v.License != "" { video.Status.License = v.License } if v.CategoryId != "" { video.Snippet.CategoryId = v.CategoryId } if v.Privacy != "" { video.Status.PrivacyStatus = v.Privacy } if v.Embeddable != nil { video.Status.Embeddable = *v.Embeddable } call := service.Videos.Update([]string{"snippet,status"}, video) if v.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(v.OnBehalfOfContentOwner) } res, err := call.Do() if err != nil { return errors.Join(errUpdateVideo, err) } if v.Thumbnail != "" { t := thumbnail.NewThumbnail( thumbnail.WithVideoId(res.Id), thumbnail.WithFile(v.Thumbnail), thumbnail.WithService(service), ) _ = t.Set("silent", "", nil) } if v.PlaylistId != "" { pi := playlistItem.NewPlaylistItem( playlistItem.WithTitle(res.Snippet.Title), playlistItem.WithDescription(res.Snippet.Description), playlistItem.WithKind("video"), playlistItem.WithKVideoId(res.Id), playlistItem.WithPlaylistId(v.PlaylistId), playlistItem.WithChannelId(res.Snippet.ChannelId), playlistItem.WithPrivacy(res.Status.PrivacyStatus), playlistItem.WithService(service), ) _ = pi.Insert("silent", "", writer) } switch output { case "json": utils.PrintJSON(res, jpath, writer) case "yaml": utils.PrintYAML(res, jpath, writer) case "silent": default: _, _ = fmt.Fprintf(writer, "Video updated: %s\n", res.Id) } return nil } func (v *video) Rate(writer io.Writer) error { for _, id := range v.IDs { call := service.Videos.Rate(id, v.Rating) err := call.Do() if err != nil { return errors.Join(errRating, err) } _, _ = fmt.Fprintf(writer, "Video %s rated %s\n", id, v.Rating) } return nil } func (v *video) GetRating(output string, jpath string, writer io.Writer) error { call := service.Videos.GetRating(v.IDs) if v.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(v.OnBehalfOfContentOwnerChannel) } res, err := call.Do() if err != nil { return errors.Join(errGetRating, err) } switch output { case "json": utils.PrintJSON(res.Items, jpath, writer) case "yaml": utils.PrintYAML(res.Items, jpath, writer) default: tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Rating"}) for _, item := range res.Items { tb.AppendRow(table.Row{item.VideoId, item.Rating}) } } return nil } func (v *video) Delete(writer io.Writer) error { for _, id := range v.IDs { call := service.Videos.Delete(id) if v.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(v.OnBehalfOfContentOwner) } err := call.Do() if err != nil { return errors.Join(errDeleteVideo, err) } _, _ = fmt.Fprintf(writer, "Video %s deleted", id) } return nil } func (v *video) ReportAbuse(writer io.Writer) error { for _, id := range v.IDs { videoAbuseReport := &youtube.VideoAbuseReport{ Comments: v.Comments, Language: v.Language, ReasonId: v.ReasonId, SecondaryReasonId: v.SecondaryReasonId, VideoId: id, } call := service.Videos.ReportAbuse(videoAbuseReport) if v.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(v.OnBehalfOfContentOwner) } err := call.Do() if err != nil { return errors.Join(errReportAbuse, err) } _, _ = fmt.Fprintf(writer, "Video %s reported for abuse", id) } return nil } func WithIDs(ids []string) Option { return func(v *video) { v.IDs = ids } } func WithAutoLevels(autoLevels *bool) Option { return func(v *video) { if autoLevels != nil { v.AutoLevels = autoLevels } } } func WithFile(file string) Option { return func(v *video) { v.File = file } } func WithTitle(title string) Option { return func(v *video) { v.Title = title } } func WithDescription(description string) Option { return func(v *video) { v.Description = description } } func WithHl(hl string) Option { return func(v *video) { v.Hl = hl } } func WithTags(tags []string) Option { return func(v *video) { v.Tags = tags } } func WithLanguage(language string) Option { return func(v *video) { v.Language = language } } func WithLocale(locale string) Option { return func(v *video) { v.Locale = locale } } func WithLicense(license string) Option { return func(v *video) { v.License = license } } func WithThumbnail(thumbnail string) Option { return func(v *video) { v.Thumbnail = thumbnail } } func WithRating(rating string) Option { return func(v *video) { v.Rating = rating } } func WithChart(chart string) Option { return func(v *video) { v.Chart = chart } } func WithForKids(forKids *bool) Option { return func(v *video) { if forKids != nil { v.ForKids = forKids } } } func WithEmbeddable(embeddable *bool) Option { return func(v *video) { if embeddable != nil { v.Embeddable = embeddable } } } func WithCategory(categoryId string) Option { return func(v *video) { v.CategoryId = categoryId } } func WithPrivacy(privacy string) Option { return func(v *video) { v.Privacy = privacy } } func WithChannelId(channelId string) Option { return func(v *video) { v.ChannelId = channelId } } func WithPlaylistId(playlistId string) Option { return func(v *video) { v.PlaylistId = playlistId } } func WithPublicStatsViewable(publicStatsViewable *bool) Option { return func(v *video) { if publicStatsViewable != nil { v.PublicStatsViewable = publicStatsViewable } } } func WithPublishAt(publishAt string) Option { return func(v *video) { v.PublishAt = publishAt } } func WithRegionCode(regionCode string) Option { return func(v *video) { v.RegionCode = regionCode } } func WithStabilize(stabilize *bool) Option { return func(v *video) { if stabilize != nil { v.Stabilize = stabilize } } } func WithMaxHeight(maxHeight int64) Option { return func(v *video) { v.MaxHeight = maxHeight } } func WithMaxWidth(maxWidth int64) Option { return func(v *video) { v.MaxWidth = maxWidth } } func WithMaxResults(maxResults int64) Option { return func(v *video) { if maxResults <= 0 { maxResults = 1 } v.MaxResults = maxResults } } func WithNotifySubscribers(notifySubscribers *bool) Option { return func(v *video) { if notifySubscribers != nil { v.NotifySubscribers = notifySubscribers } } } func WithOnBehalfOfContentOwner(onBehalfOfContentOwner string) Option { return func(v *video) { v.OnBehalfOfContentOwner = onBehalfOfContentOwner } } func WithOnBehalfOfContentOwnerChannel(onBehalfOfContentOwnerChannel string) Option { return func(v *video) { v.OnBehalfOfContentOwnerChannel = onBehalfOfContentOwnerChannel } } func WithComments(comments string) Option { return func(v *video) { v.Comments = comments } } func WithReasonId(reasonId string) Option { return func(v *video) { v.ReasonId = reasonId } } func WithSecondaryReasonId(secondaryReasonId string) Option { return func(v *video) { v.SecondaryReasonId = secondaryReasonId } } func WithService(svc *youtube.Service) Option { return func(_ *video) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package videoAbuseReportReason import ( "errors" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetVideoAbuseReportReason = errors.New("failed to get video abuse report reason") ) type videoAbuseReportReason struct { Hl string `yaml:"hl" json:"hl"` } type VideoAbuseReportReason interface { Get([]string) ([]*youtube.VideoAbuseReportReason, error) List([]string, string, string, io.Writer) error } type Option func(*videoAbuseReportReason) func NewVideoAbuseReportReason(opt ...Option) VideoAbuseReportReason { va := &videoAbuseReportReason{} for _, o := range opt { o(va) } return va } func (va *videoAbuseReportReason) Get(parts []string) ( []*youtube.VideoAbuseReportReason, error, ) { call := service.VideoAbuseReportReasons.List(parts) if va.Hl != "" { call = call.Hl(va.Hl) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetVideoAbuseReportReason, err) } return res.Items, nil } func (va *videoAbuseReportReason) List( parts []string, output string, jpath string, writer io.Writer, ) error { videoAbuseReportReasons, err := va.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(videoAbuseReportReasons, jpath, writer) case "yaml": utils.PrintYAML(videoAbuseReportReasons, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Label"}) for _, reason := range videoAbuseReportReasons { tb.AppendRow(table.Row{reason.Id, reason.Snippet.Label}) } } return nil } func WithHL(hl string) Option { return func(vc *videoAbuseReportReason) { vc.Hl = hl } } func WithService(svc *youtube.Service) Option { return func(_ *videoAbuseReportReason) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package videoCategory import ( "errors" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errGetVideoCategory = errors.New("failed to get video categoryId") ) type videoCategory struct { IDs []string `yaml:"ids" json:"ids"` Hl string `yaml:"hl" json:"hl"` RegionCode string `yaml:"region_code" json:"region_code"` } type VideoCategory interface { Get([]string) ([]*youtube.VideoCategory, error) List([]string, string, string, io.Writer) error } type Option func(*videoCategory) func NewVideoCategory(opt ...Option) VideoCategory { vc := &videoCategory{} for _, o := range opt { o(vc) } return vc } func (vc *videoCategory) Get(parts []string) ([]*youtube.VideoCategory, error) { call := service.VideoCategories.List(parts) if len(vc.IDs) > 0 { call = call.Id(vc.IDs...) } if vc.Hl != "" { call = call.Hl(vc.Hl) } if vc.RegionCode != "" { call = call.RegionCode(vc.RegionCode) } res, err := call.Do() if err != nil { return nil, errors.Join(errGetVideoCategory, err) } return res.Items, nil } func (vc *videoCategory) List( parts []string, output string, jpath string, writer io.Writer, ) error { videoCategories, err := vc.Get(parts) if err != nil { return err } switch output { case "json": utils.PrintJSON(videoCategories, jpath, writer) case "yaml": utils.PrintYAML(videoCategories, jpath, writer) case "table": tb := table.NewWriter() defer tb.Render() tb.SetOutputMirror(writer) tb.SetStyle(table.StyleLight) tb.SetAutoIndex(true) tb.AppendHeader(table.Row{"ID", "Title", "Assignable"}) for _, cat := range videoCategories { tb.AppendRow(table.Row{cat.Id, cat.Snippet.Title, cat.Snippet.Assignable}) } } return nil } func WithIDs(ids []string) Option { return func(vc *videoCategory) { vc.IDs = ids } } func WithHl(hl string) Option { return func(vc *videoCategory) { vc.Hl = hl } } func WithRegionCode(regionCode string) Option { return func(vc *videoCategory) { vc.RegionCode = regionCode } } func WithService(svc *youtube.Service) Option { return func(_ *videoCategory) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }
package watermark import ( "errors" "fmt" "io" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/auth" "google.golang.org/api/youtube/v3" ) var ( service *youtube.Service errSetWatermark = errors.New("failed to set watermark") errUnsetWatermark = errors.New("failed to unset watermark") ) type watermark struct { ChannelId string `yaml:"channel_id" json:"channel_id"` File string `yaml:"file" json:"file"` InVideoPosition string `yaml:"in_video_position" json:"in_video_position"` DurationMs uint64 `yaml:"duration_ms" json:"duration_ms"` OffsetMs uint64 `yaml:"offset_ms" json:"offset_ms"` OffsetType string `yaml:"offset_type" json:"offset_type"` OnBehalfOfContentOwner string `yaml:"on_behalf_of_content_owner" json:"on_behalf_of_content_owner"` } type Watermark interface { Set(io.Writer) error Unset(io.Writer) error } type Option func(*watermark) func NewWatermark(opts ...Option) Watermark { w := &watermark{} for _, opt := range opts { opt(w) } return w } func (w *watermark) Set(writer io.Writer) error { file, err := pkg.Root.Open(w.File) if err != nil { return errors.Join(errSetWatermark, err) } defer file.Close() inVideoBranding := &youtube.InvideoBranding{ Position: &youtube.InvideoPosition{}, Timing: &youtube.InvideoTiming{}, } if w.InVideoPosition != "" { inVideoBranding.Position.Type = "corner" inVideoBranding.Position.CornerPosition = w.InVideoPosition } if w.DurationMs != 0 { inVideoBranding.Timing.DurationMs = w.DurationMs } if w.OffsetMs != 0 { inVideoBranding.Timing.OffsetMs = w.OffsetMs } if w.OffsetType != "" { inVideoBranding.Timing.Type = w.OffsetType } call := service.Watermarks.Set(w.ChannelId, inVideoBranding).Media(file) if w.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(w.OnBehalfOfContentOwner) } err = call.Do() if err != nil { return errors.Join(errSetWatermark, err) } _, _ = fmt.Fprintf(writer, "Watermark set for channel %s\n", w.ChannelId) return nil } func (w *watermark) Unset(writer io.Writer) error { call := service.Watermarks.Unset(w.ChannelId) if w.OnBehalfOfContentOwner != "" { call = call.OnBehalfOfContentOwner(w.OnBehalfOfContentOwner) } err := call.Do() if err != nil { return errors.Join(errUnsetWatermark, err) } _, _ = fmt.Fprintf(writer, "Watermark unset for channel %s\n", w.ChannelId) return nil } func WithChannelId(channelId string) Option { return func(w *watermark) { w.ChannelId = channelId } } func WithFile(file string) Option { return func(w *watermark) { w.File = file } } func WithInVideoPosition(inVideoPosition string) Option { return func(w *watermark) { w.InVideoPosition = inVideoPosition } } func WithDurationMs(durationMs uint64) Option { return func(w *watermark) { w.DurationMs = durationMs } } func WithOffsetMs(offsetMs uint64) Option { return func(w *watermark) { w.OffsetMs = offsetMs } } func WithOffsetType(offsetType string) Option { return func(w *watermark) { w.OffsetType = offsetType } } func WithOnBehalfOfContentOwner(onBehalfOfContentOwner string) Option { return func(w *watermark) { w.OnBehalfOfContentOwner = onBehalfOfContentOwner } } func WithService(svc *youtube.Service) Option { return func(_ *watermark) { if svc == nil { svc = auth.NewY2BService( auth.WithCredential("", pkg.Root.FS()), auth.WithCacheToken("", pkg.Root.FS()), ).GetService() } service = svc } }