🐛 Preserve omitted video metadata
This commit is contained in:
1 parent
afdb8c8305
commit
49e092ea2e
7 files changed
+262
-338
No files matched your search
@@ -23,6 +23,5 @@ go_library(
|
||||
"@com_github_google_jsonschema_go//jsonschema",
|
||||
"@com_github_modelcontextprotocol_go_sdk//mcp",
|
||||
"@com_github_spf13_cobra//:cobra",
|
||||
"@com_github_spf13_pflag//:pflag",
|
||||
],
|
||||
)
|
||||
+18
-14
@@ -47,7 +47,7 @@ var insertInSchema = &jsonschema.Schema{
|
||||
"language": {Type: "string", Description: insertLangUsage},
|
||||
"license": {
|
||||
Type: "string", Enum: []any{"youtube", "creativeCommon"},
|
||||
Description: licenseUsage, Default: jsontext.Value(`"youtube"`),
|
||||
Description: licenseUsage,
|
||||
},
|
||||
"thumbnail": {Type: "string", Description: thumbnailUsage},
|
||||
"channel_id": {Type: "string", Description: chidUsage},
|
||||
@@ -57,14 +57,18 @@ var insertInSchema = &jsonschema.Schema{
|
||||
Type: "string", Description: privacyUsage,
|
||||
Enum: []any{"public", "private", "unlisted"},
|
||||
},
|
||||
"for_kids": {Type: "boolean", Description: fkUsage},
|
||||
"embeddable": {Type: "boolean", Description: embeddableUsage},
|
||||
"contains_synthetic_media": {Type: "boolean", Description: csmUsage},
|
||||
"recording_date": {Type: "string", Description: rdUsage},
|
||||
"publish_at": {Type: "string", Description: paUsage},
|
||||
"stabilize": {Type: "boolean", Description: stabilizeUsage},
|
||||
"notify_subscribers": {Type: "boolean", Description: nsUsage},
|
||||
"public_stats_viewable": {Type: "boolean", Description: psvUsage},
|
||||
"for_kids": {
|
||||
Type: "boolean", Description: fkUsage, Default: jsontext.Value("false"),
|
||||
},
|
||||
"contains_synthetic_media": {
|
||||
Type: "boolean", Description: csmUsage, Default: jsontext.Value("false"),
|
||||
},
|
||||
"embeddable": {Type: "boolean", Description: embeddableUsage},
|
||||
"recording_date": {Type: "string", Description: rdUsage},
|
||||
"publish_at": {Type: "string", Description: paUsage},
|
||||
"stabilize": {Type: "boolean", Description: stabilizeUsage},
|
||||
"notify_subscribers": {Type: "boolean", Description: nsUsage},
|
||||
"public_stats_viewable": {Type: "boolean", Description: psvUsage},
|
||||
"on_behalf_of_content_owner": {
|
||||
Type: "string",
|
||||
Description: pkg.OBOCOUsage,
|
||||
@@ -108,10 +112,10 @@ func init() {
|
||||
)
|
||||
insertCmd.Flags().StringVarP(&file, "file", "f", "", fileUsage)
|
||||
insertCmd.Flags().StringVarP(&title, "title", "t", "", titleUsage)
|
||||
insertCmd.Flags().StringVarP(&description, "description", "d", "", descUsage)
|
||||
insertCmd.Flags().StringVarP(description, "description", "d", "", descUsage)
|
||||
insertCmd.Flags().StringSliceVarP(&tags, "tags", "a", []string{}, tagsUsage)
|
||||
insertCmd.Flags().StringVarP(&language, "language", "l", "", insertLangUsage)
|
||||
insertCmd.Flags().StringVarP(&license, "license", "L", "youtube", licenseUsage)
|
||||
insertCmd.Flags().StringVarP(language, "language", "l", "", insertLangUsage)
|
||||
insertCmd.Flags().StringVarP(&license, "license", "L", "", licenseUsage)
|
||||
insertCmd.Flags().StringVarP(&thumbnail, "thumbnail", "u", "", thumbnailUsage)
|
||||
insertCmd.Flags().StringVarP(&channelId, "channelId", "c", "", chidUsage)
|
||||
insertCmd.Flags().StringVarP(&playListId, "playlistId", "y", "", pidUsage)
|
||||
@@ -124,8 +128,8 @@ func init() {
|
||||
insertCmd.Flags().BoolVarP(
|
||||
containsSyntheticMedia, "containsSyntheticMedia", "M", false, csmUsage,
|
||||
)
|
||||
insertCmd.Flags().StringVarP(&recordingDate, "recordingDate", "D", "", rdUsage)
|
||||
insertCmd.Flags().StringVarP(&publishAt, "publishAt", "U", "", paUsage)
|
||||
insertCmd.Flags().StringVarP(recordingDate, "recordingDate", "D", "", rdUsage)
|
||||
insertCmd.Flags().StringVarP(publishAt, "publishAt", "U", "", paUsage)
|
||||
insertCmd.Flags().BoolVarP(stabilize, "stabilize", "S", true, stabilizeUsage)
|
||||
insertCmd.Flags().BoolVarP(
|
||||
notifySubscribers, "notifySubscribers", "N", true, nsUsage,
|
||||
|
||||
@@ -83,7 +83,7 @@ func init() {
|
||||
reportAbuseCmd.Flags().StringVarP(
|
||||
&comments, "comments", "c", "", commentsUsage,
|
||||
)
|
||||
reportAbuseCmd.Flags().StringVarP(&language, "language", "l", "", raLangUsage)
|
||||
reportAbuseCmd.Flags().StringVarP(language, "language", "l", "", raLangUsage)
|
||||
reportAbuseCmd.Flags().StringVarP(
|
||||
&onBehalfOfContentOwner, "onBehalfOfContentOwner", "b", "", pkg.OBOCOUsage,
|
||||
)
|
||||
|
||||
+6
-6
@@ -53,7 +53,7 @@ var updateInSchema = &jsonschema.Schema{
|
||||
"language": {Type: "string", Description: updateLangUsage},
|
||||
"license": {
|
||||
Type: "string", Enum: []any{"youtube", "creativeCommon"},
|
||||
Description: licenseUsage, Default: jsontext.Value(`"youtube"`),
|
||||
Description: licenseUsage,
|
||||
},
|
||||
"thumbnail": {Type: "string", Description: thumbnailUsage},
|
||||
"playlist_id": {Type: "string", Description: pidUsage},
|
||||
@@ -99,22 +99,22 @@ func init() {
|
||||
|
||||
updateCmd.Flags().StringSliceVarP(&ids, "id", "i", []string{}, updateIdUsage)
|
||||
updateCmd.Flags().StringVarP(&title, "title", "t", "", titleUsage)
|
||||
updateCmd.Flags().StringVarP(&description, "description", "d", "", descUsage)
|
||||
updateCmd.Flags().StringVarP(description, "description", "d", "", descUsage)
|
||||
updateCmd.Flags().StringSliceVarP(&tags, "tags", "a", []string{}, tagsUsage)
|
||||
updateCmd.Flags().StringVarP(&language, "language", "l", "", updateLangUsage)
|
||||
updateCmd.Flags().StringVarP(&license, "license", "L", "youtube", licenseUsage)
|
||||
updateCmd.Flags().StringVarP(language, "language", "l", "", updateLangUsage)
|
||||
updateCmd.Flags().StringVarP(&license, "license", "L", "", licenseUsage)
|
||||
updateCmd.Flags().StringVarP(&thumbnail, "thumbnail", "u", "", thumbnailUsage)
|
||||
updateCmd.Flags().StringVarP(&playListId, "playlistId", "y", "", pidUsage)
|
||||
updateCmd.Flags().StringVarP(&categoryId, "categoryId", "g", "", caidUsage)
|
||||
updateCmd.Flags().StringVarP(&privacy, "privacy", "p", "", privacyUsage)
|
||||
updateCmd.Flags().StringVarP(&publishAt, "publishAt", "U", "", paUsage)
|
||||
updateCmd.Flags().StringVarP(publishAt, "publishAt", "U", "", paUsage)
|
||||
updateCmd.Flags().BoolVarP(
|
||||
embeddable, "embeddable", "E", true, embeddableUsage,
|
||||
)
|
||||
updateCmd.Flags().BoolVarP(
|
||||
containsSyntheticMedia, "containsSyntheticMedia", "M", false, csmUsage,
|
||||
)
|
||||
updateCmd.Flags().StringVarP(&recordingDate, "recordingDate", "D", "", rdUsage)
|
||||
updateCmd.Flags().StringVarP(recordingDate, "recordingDate", "D", "", rdUsage)
|
||||
updateCmd.Flags().StringP("output", "o", "", pkg.SilentUsage)
|
||||
updateCmd.Flags().Bool("yes", false, pkg.ConfirmedUsage)
|
||||
_ = updateCmd.MarkFlagRequired("id")
|
||||
|
||||
+25
-15
@@ -6,7 +6,6 @@ package video
|
||||
import (
|
||||
"github.com/eat-pray-ai/yutu/cmd"
|
||||
"github.com/eat-pray-ai/yutu/pkg/utils"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -49,10 +48,10 @@ var (
|
||||
autoLevels = new(false)
|
||||
file string
|
||||
title string
|
||||
description string
|
||||
description = new("")
|
||||
hl string
|
||||
tags []string
|
||||
language string
|
||||
language = new("")
|
||||
locale string
|
||||
license string
|
||||
thumbnail string
|
||||
@@ -65,8 +64,8 @@ var (
|
||||
privacy string
|
||||
forKids = new(false)
|
||||
embeddable = new(false)
|
||||
recordingDate string
|
||||
publishAt string
|
||||
recordingDate = new("")
|
||||
publishAt = new("")
|
||||
regionCode string
|
||||
reasonId string
|
||||
secondaryReasonId string
|
||||
@@ -88,7 +87,7 @@ var videoCmd = &cobra.Command{
|
||||
Short: short,
|
||||
Long: long,
|
||||
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
|
||||
resetFlags(cmd.Flags())
|
||||
resetFlags(cmd)
|
||||
},
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
_ = cmd.Help()
|
||||
@@ -99,16 +98,27 @@ func init() {
|
||||
cmd.RootCmd.AddCommand(videoCmd)
|
||||
}
|
||||
|
||||
func resetFlags(flagSet *pflag.FlagSet) {
|
||||
func resetFlags(cmd *cobra.Command) {
|
||||
boolMap := map[string]**bool{
|
||||
"autoLevels": &autoLevels,
|
||||
"forKids": &forKids,
|
||||
"embeddable": &embeddable,
|
||||
"containsSyntheticMedia": &containsSyntheticMedia,
|
||||
"stabilize": &stabilize,
|
||||
"notifySubscribers": ¬ifySubscribers,
|
||||
"publicStatsViewable": &publicStatsViewable,
|
||||
"autoLevels": &autoLevels,
|
||||
"embeddable": &embeddable,
|
||||
"stabilize": &stabilize,
|
||||
"notifySubscribers": ¬ifySubscribers,
|
||||
"publicStatsViewable": &publicStatsViewable,
|
||||
}
|
||||
stringFlag := map[string]**string{
|
||||
"description": &description,
|
||||
"language": &language,
|
||||
"publishAt": &publishAt,
|
||||
"recordingDate": &recordingDate,
|
||||
}
|
||||
|
||||
utils.ResetBool(boolMap, flagSet)
|
||||
if cmd.Name() == "update" {
|
||||
boolMap["containsSyntheticMedia"] = &containsSyntheticMedia
|
||||
}
|
||||
|
||||
flagSet := cmd.Flags()
|
||||
utils.ResetFlags(boolMap, flagSet)
|
||||
utils.ResetFlags(stringFlag, flagSet)
|
||||
utils.ResetFlags(map[string]*[]string{"tags": &tags}, flagSet)
|
||||
}
|
||||
+54
-33
@@ -38,9 +38,9 @@ type Video struct {
|
||||
AutoLevels *bool `yaml:"auto_levels" json:"auto_levels,omitzero"`
|
||||
File string `yaml:"file" json:"file,omitempty"`
|
||||
Title string `yaml:"title" json:"title,omitempty"`
|
||||
Description string `yaml:"description" json:"description,omitempty"`
|
||||
Description *string `yaml:"description" json:"description,omitzero"`
|
||||
Tags []string `yaml:"tags" json:"tags,omitempty"`
|
||||
Language string `yaml:"language" json:"language,omitempty"`
|
||||
Language *string `yaml:"language" json:"language,omitzero"`
|
||||
Locale string `yaml:"locale" json:"locale,omitempty"`
|
||||
License string `yaml:"license" json:"license,omitempty"`
|
||||
Thumbnail string `yaml:"thumbnail" json:"thumbnail,omitempty"`
|
||||
@@ -52,18 +52,19 @@ type Video struct {
|
||||
Privacy string `yaml:"privacy" json:"privacy,omitempty"`
|
||||
ForKids *bool `yaml:"for_kids" json:"for_kids,omitzero"`
|
||||
Embeddable *bool `yaml:"embeddable" json:"embeddable,omitzero"`
|
||||
PublishAt string `yaml:"publish_at" json:"publish_at,omitempty"`
|
||||
PublishAt *string `yaml:"publish_at" json:"publish_at,omitzero"`
|
||||
RegionCode string `yaml:"region_code" json:"region_code,omitempty"`
|
||||
ReasonId string `yaml:"reason_id" json:"reason_id,omitempty"`
|
||||
Stabilize *bool `yaml:"stabilize" json:"stabilize,omitzero"`
|
||||
MaxHeight int64 `yaml:"max_height" json:"max_height,omitzero"`
|
||||
MaxWidth int64 `yaml:"max_width" json:"max_width,omitzero"`
|
||||
|
||||
RecordingDate string `yaml:"recording_date" json:"recording_date,omitempty"`
|
||||
ContainsSyntheticMedia *bool `yaml:"contains_synthetic_media" json:"contains_synthetic_media,omitzero"`
|
||||
SecondaryReasonId string `yaml:"secondary_reason_id" json:"secondary_reason_id,omitempty"`
|
||||
NotifySubscribers *bool `yaml:"notify_subscribers" json:"notify_subscribers,omitzero"`
|
||||
PublicStatsViewable *bool `yaml:"public_stats_viewable" json:"public_stats_viewable,omitzero"`
|
||||
RecordingDate *string `yaml:"recording_date" json:"recording_date,omitzero"`
|
||||
ContainsSyntheticMedia *bool `yaml:"contains_synthetic_media" json:"contains_synthetic_media,omitzero"`
|
||||
SecondaryReasonId string `yaml:"secondary_reason_id" json:"secondary_reason_id,omitempty"`
|
||||
NotifySubscribers *bool `yaml:"notify_subscribers" json:"notify_subscribers,omitzero"`
|
||||
PublicStatsViewable *bool `yaml:"public_stats_viewable" json:"public_stats_viewable,omitzero"`
|
||||
|
||||
OnBehalfOfContentOwnerChannel string `yaml:"on_behalf_of_content_owner_channel" json:"on_behalf_of_content_owner_channel,omitempty"`
|
||||
}
|
||||
|
||||
@@ -179,24 +180,33 @@ func (v *Video) Insert(writer io.Writer) error {
|
||||
|
||||
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,
|
||||
Title: v.Title,
|
||||
Tags: v.Tags,
|
||||
CategoryId: v.CategoryId,
|
||||
ChannelId: v.ChannelId,
|
||||
},
|
||||
Status: &youtube.VideoStatus{
|
||||
License: v.License,
|
||||
PublishAt: v.PublishAt,
|
||||
PrivacyStatus: v.Privacy,
|
||||
ForceSendFields: []string{"SelfDeclaredMadeForKids", "ContainsSyntheticMedia"},
|
||||
},
|
||||
}
|
||||
if v.Description != nil {
|
||||
video.Snippet.Description = *v.Description
|
||||
}
|
||||
if v.Language != nil {
|
||||
video.Snippet.DefaultLanguage = *v.Language
|
||||
video.Snippet.DefaultAudioLanguage = *v.Language
|
||||
}
|
||||
if v.PublishAt != nil {
|
||||
video.Status.PublishAt = *v.PublishAt
|
||||
}
|
||||
|
||||
if v.Embeddable != nil {
|
||||
video.Status.Embeddable = *v.Embeddable
|
||||
video.Status.ForceSendFields = append(
|
||||
video.Status.ForceSendFields, "Embeddable",
|
||||
)
|
||||
}
|
||||
if v.ForKids != nil {
|
||||
video.Status.SelfDeclaredMadeForKids = *v.ForKids
|
||||
@@ -206,12 +216,15 @@ func (v *Video) Insert(writer io.Writer) error {
|
||||
}
|
||||
if v.PublicStatsViewable != nil {
|
||||
video.Status.PublicStatsViewable = *v.PublicStatsViewable
|
||||
video.Status.ForceSendFields = append(
|
||||
video.Status.ForceSendFields, "PublicStatsViewable",
|
||||
)
|
||||
}
|
||||
|
||||
insertParts := "snippet,status"
|
||||
if v.RecordingDate != "" {
|
||||
if v.RecordingDate != nil && *v.RecordingDate != "" {
|
||||
video.RecordingDetails = &youtube.VideoRecordingDetails{
|
||||
RecordingDate: v.RecordingDate,
|
||||
RecordingDate: *v.RecordingDate,
|
||||
}
|
||||
insertParts += ",recordingDetails"
|
||||
}
|
||||
@@ -269,6 +282,9 @@ func (v *Video) Insert(writer io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update preserves nil Description, Language, PublishAt, and RecordingDate inputs.
|
||||
// Non-nil empty strings clear those properties by omitting them from the updated
|
||||
// resource part; timestamp clears must not be sent as empty timestamp strings.
|
||||
func (v *Video) Update(writer io.Writer) error {
|
||||
if err := v.EnsureService(); err != nil {
|
||||
return err
|
||||
@@ -291,7 +307,10 @@ func (v *Video) Update(writer io.Writer) error {
|
||||
video := &youtube.Video{
|
||||
Id: original.Id,
|
||||
Snippet: &youtube.VideoSnippet{},
|
||||
Status: &youtube.VideoStatus{},
|
||||
Status: &youtube.VideoStatus{
|
||||
// Status updates replace existing values; omitempty must not drop false booleans.
|
||||
ForceSendFields: []string{"Embeddable", "PublicStatsViewable", "SelfDeclaredMadeForKids", "ContainsSyntheticMedia"},
|
||||
},
|
||||
}
|
||||
if original.Snippet != nil {
|
||||
video.Snippet.Title = original.Snippet.Title
|
||||
@@ -313,8 +332,8 @@ func (v *Video) Update(writer io.Writer) error {
|
||||
if v.Title != "" {
|
||||
video.Snippet.Title = v.Title
|
||||
}
|
||||
if v.Description != "" {
|
||||
video.Snippet.Description = v.Description
|
||||
if v.Description != nil {
|
||||
video.Snippet.Description = *v.Description
|
||||
}
|
||||
if v.Tags != nil {
|
||||
if !slices.Contains(v.Tags, "yutu🐰") {
|
||||
@@ -322,8 +341,8 @@ func (v *Video) Update(writer io.Writer) error {
|
||||
}
|
||||
video.Snippet.Tags = v.Tags
|
||||
}
|
||||
if v.Language != "" {
|
||||
video.Snippet.DefaultLanguage = v.Language
|
||||
if v.Language != nil {
|
||||
video.Snippet.DefaultLanguage = *v.Language
|
||||
}
|
||||
if v.License != "" {
|
||||
video.Status.License = v.License
|
||||
@@ -334,13 +353,13 @@ func (v *Video) Update(writer io.Writer) error {
|
||||
if v.Privacy != "" {
|
||||
video.Status.PrivacyStatus = v.Privacy
|
||||
}
|
||||
if v.PublishAt != "" {
|
||||
if v.PublishAt != nil {
|
||||
// YouTube only accepts a schedule on private videos. Do not coerce
|
||||
// public/unlisted to private here: that would take a live video offline.
|
||||
if video.Status.PrivacyStatus != "private" {
|
||||
if *v.PublishAt != "" && video.Status.PrivacyStatus != "private" {
|
||||
return errors.Join(errUpdateVideo, errScheduleNonPrivate)
|
||||
}
|
||||
video.Status.PublishAt = v.PublishAt
|
||||
video.Status.PublishAt = *v.PublishAt
|
||||
}
|
||||
if v.Embeddable != nil {
|
||||
video.Status.Embeddable = *v.Embeddable
|
||||
@@ -350,9 +369,9 @@ func (v *Video) Update(writer io.Writer) error {
|
||||
}
|
||||
|
||||
updateParts := "snippet,status"
|
||||
if v.RecordingDate != "" {
|
||||
if v.RecordingDate != nil {
|
||||
video.RecordingDetails = &youtube.VideoRecordingDetails{
|
||||
RecordingDate: v.RecordingDate,
|
||||
RecordingDate: *v.RecordingDate,
|
||||
}
|
||||
updateParts += ",recordingDetails"
|
||||
}
|
||||
@@ -469,11 +488,13 @@ 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,
|
||||
}
|
||||
if v.Language != nil {
|
||||
videoAbuseReport.Language = *v.Language
|
||||
}
|
||||
|
||||
call := v.Service.Videos.ReportAbuse(videoAbuseReport)
|
||||
if v.OnBehalfOfContentOwner != "" {
|
||||
@@ -510,7 +531,7 @@ func WithTitle(title string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithDescription(description string) Option {
|
||||
func WithDescription(description *string) Option {
|
||||
return func(v *Video) {
|
||||
v.Description = description
|
||||
}
|
||||
@@ -522,7 +543,7 @@ func WithTags(tags []string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLanguage(language string) Option {
|
||||
func WithLanguage(language *string) Option {
|
||||
return func(v *Video) {
|
||||
v.Language = language
|
||||
}
|
||||
@@ -608,13 +629,13 @@ func WithPublicStatsViewable(publicStatsViewable *bool) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithPublishAt(publishAt string) Option {
|
||||
func WithPublishAt(publishAt *string) Option {
|
||||
return func(v *Video) {
|
||||
v.PublishAt = publishAt
|
||||
}
|
||||
}
|
||||
|
||||
func WithRecordingDate(recordingDate string) Option {
|
||||
func WithRecordingDate(recordingDate *string) Option {
|
||||
return func(v *Video) {
|
||||
v.RecordingDate = recordingDate
|
||||
}
|
||||
|
||||
+158
-268
@@ -7,12 +7,10 @@ import (
|
||||
"bytes"
|
||||
"encoding/json/v2"
|
||||
"io"
|
||||
"math"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -21,265 +19,78 @@ import (
|
||||
"google.golang.org/api/youtube/v3"
|
||||
)
|
||||
|
||||
func TestNewVideo(t *testing.T) {
|
||||
type args struct {
|
||||
opts []Option
|
||||
}
|
||||
|
||||
autoLevelsTrue := true
|
||||
autoLevelsFalse := false
|
||||
forKidsTrue := true
|
||||
forKidsFalse := false
|
||||
embeddableTrue := true
|
||||
embeddableFalse := false
|
||||
containsSyntheticMediaTrue := true
|
||||
containsSyntheticMediaFalse := false
|
||||
stabilizeTrue := true
|
||||
stabilizeFalse := false
|
||||
notifySubscribersTrue := true
|
||||
notifySubscribersFalse := false
|
||||
publicStatsViewableTrue := true
|
||||
publicStatsViewableFalse := false
|
||||
svc := &youtube.Service{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want IVideo[youtube.Video]
|
||||
func TestVideo_UpdateClearableMetadata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
input string
|
||||
privacy string
|
||||
wantDescription string
|
||||
wantLanguage string
|
||||
wantPublishAt string
|
||||
wantRecordingDate string
|
||||
recordingPart bool
|
||||
}{
|
||||
{
|
||||
name: "with all options",
|
||||
args: args{
|
||||
opts: []Option{
|
||||
WithIds([]string{"video1", "video2"}),
|
||||
WithAutoLevels(&autoLevelsTrue),
|
||||
WithFile("/path/to/video.mp4"),
|
||||
WithTitle("Test Video"),
|
||||
WithDescription("Test video description"),
|
||||
WithHl("en"),
|
||||
WithTags([]string{"tag1", "tag2", "tag3"}),
|
||||
WithLanguage("en"),
|
||||
WithLocale("en_US"),
|
||||
WithLicense("youtube"),
|
||||
WithThumbnail("/path/to/thumbnail.jpg"),
|
||||
WithRating("like"),
|
||||
WithChart("mostPopular"),
|
||||
WithChannelId("channel123"),
|
||||
WithComments("Test comments"),
|
||||
WithPlaylistId("playlist123"),
|
||||
WithCategory("22"),
|
||||
WithPrivacy("public"),
|
||||
WithForKids(&forKidsTrue),
|
||||
WithEmbeddable(&embeddableTrue),
|
||||
WithContainsSyntheticMedia(&containsSyntheticMediaTrue),
|
||||
WithRecordingDate("2024-06-15T10:00:00Z"),
|
||||
WithPublishAt("2024-12-31T23:59:59Z"),
|
||||
WithRegionCode("US"),
|
||||
WithReasonId("reason123"),
|
||||
WithSecondaryReasonId("secondaryReason123"),
|
||||
WithStabilize(&stabilizeTrue),
|
||||
WithMaxHeight(1080),
|
||||
WithMaxWidth(1920),
|
||||
WithMaxResults(50),
|
||||
WithNotifySubscribers(¬ifySubscribersTrue),
|
||||
WithPublicStatsViewable(&publicStatsViewableTrue),
|
||||
WithOnBehalfOfContentOwner("owner123"),
|
||||
WithOnBehalfOfContentOwnerChannel("ownerChannel123"),
|
||||
WithParts([]string{"snippet", "contentDetails"}),
|
||||
WithOutput("json"),
|
||||
WithService(svc),
|
||||
},
|
||||
},
|
||||
want: &Video{
|
||||
Service: svc,
|
||||
Parts: []string{"snippet", "contentDetails"},
|
||||
Output: "json",
|
||||
Ids: []string{"video1", "video2"},
|
||||
MaxResults: 50,
|
||||
Hl: "en",
|
||||
ChannelId: "channel123",
|
||||
OnBehalfOfContentOwner: "owner123",
|
||||
AutoLevels: &autoLevelsTrue,
|
||||
File: "/path/to/video.mp4",
|
||||
Title: "Test Video",
|
||||
Description: "Test video description",
|
||||
Tags: []string{"tag1", "tag2", "tag3"},
|
||||
Language: "en",
|
||||
Locale: "en_US",
|
||||
License: "youtube",
|
||||
Thumbnail: "/path/to/thumbnail.jpg",
|
||||
Rating: "like",
|
||||
Chart: "mostPopular",
|
||||
Comments: "Test comments",
|
||||
PlaylistId: "playlist123",
|
||||
CategoryId: "22",
|
||||
Privacy: "public",
|
||||
ForKids: &forKidsTrue,
|
||||
Embeddable: &embeddableTrue,
|
||||
ContainsSyntheticMedia: &containsSyntheticMediaTrue,
|
||||
RecordingDate: "2024-06-15T10:00:00Z",
|
||||
PublishAt: "2024-12-31T23:59:59Z",
|
||||
RegionCode: "US",
|
||||
ReasonId: "reason123",
|
||||
SecondaryReasonId: "secondaryReason123",
|
||||
Stabilize: &stabilizeTrue,
|
||||
MaxHeight: 1080,
|
||||
MaxWidth: 1920,
|
||||
NotifySubscribers: ¬ifySubscribersTrue,
|
||||
PublicStatsViewable: &publicStatsViewableTrue,
|
||||
OnBehalfOfContentOwnerChannel: "ownerChannel123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with no options",
|
||||
args: args{
|
||||
opts: []Option{},
|
||||
},
|
||||
want: &Video{Fields: common.Fields{}},
|
||||
},
|
||||
{
|
||||
name: "with nil boolean options",
|
||||
args: args{
|
||||
opts: []Option{
|
||||
WithAutoLevels(nil),
|
||||
WithForKids(nil),
|
||||
WithEmbeddable(nil),
|
||||
WithContainsSyntheticMedia(nil),
|
||||
WithStabilize(nil),
|
||||
WithNotifySubscribers(nil),
|
||||
WithPublicStatsViewable(nil),
|
||||
},
|
||||
},
|
||||
want: &Video{Fields: common.Fields{}},
|
||||
},
|
||||
{
|
||||
name: "with false boolean options",
|
||||
args: args{
|
||||
opts: []Option{
|
||||
WithAutoLevels(&autoLevelsFalse),
|
||||
WithForKids(&forKidsFalse),
|
||||
WithEmbeddable(&embeddableFalse),
|
||||
WithContainsSyntheticMedia(&containsSyntheticMediaFalse),
|
||||
WithStabilize(&stabilizeFalse),
|
||||
WithNotifySubscribers(¬ifySubscribersFalse),
|
||||
WithPublicStatsViewable(&publicStatsViewableFalse),
|
||||
},
|
||||
},
|
||||
want: &Video{
|
||||
Fields: common.Fields{},
|
||||
AutoLevels: &autoLevelsFalse,
|
||||
ForKids: &forKidsFalse,
|
||||
Embeddable: &embeddableFalse,
|
||||
ContainsSyntheticMedia: &containsSyntheticMediaFalse,
|
||||
Stabilize: &stabilizeFalse,
|
||||
NotifySubscribers: ¬ifySubscribersFalse,
|
||||
PublicStatsViewable: &publicStatsViewableFalse,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with zero max results",
|
||||
args: args{
|
||||
opts: []Option{
|
||||
WithMaxResults(0),
|
||||
},
|
||||
},
|
||||
want: &Video{
|
||||
MaxResults: math.MaxInt64,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with negative max results",
|
||||
args: args{
|
||||
opts: []Option{
|
||||
WithMaxResults(-10),
|
||||
},
|
||||
},
|
||||
want: &Video{
|
||||
MaxResults: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with empty string values",
|
||||
args: args{
|
||||
opts: []Option{
|
||||
WithFile(""),
|
||||
WithTitle(""),
|
||||
WithDescription(""),
|
||||
WithHl(""),
|
||||
WithLanguage(""),
|
||||
WithLocale(""),
|
||||
WithLicense(""),
|
||||
WithThumbnail(""),
|
||||
WithRating(""),
|
||||
WithChart(""),
|
||||
WithChannelId(""),
|
||||
WithComments(""),
|
||||
WithPlaylistId(""),
|
||||
WithCategory(""),
|
||||
WithPrivacy(""),
|
||||
WithRecordingDate(""),
|
||||
WithPublishAt(""),
|
||||
WithRegionCode(""),
|
||||
WithReasonId(""),
|
||||
WithSecondaryReasonId(""),
|
||||
WithOnBehalfOfContentOwner(""),
|
||||
WithOnBehalfOfContentOwnerChannel(""),
|
||||
},
|
||||
},
|
||||
want: &Video{
|
||||
Fields: common.Fields{},
|
||||
File: "",
|
||||
Title: "",
|
||||
Description: "",
|
||||
Language: "",
|
||||
Locale: "",
|
||||
License: "",
|
||||
Thumbnail: "",
|
||||
Rating: "",
|
||||
Chart: "",
|
||||
Comments: "",
|
||||
PlaylistId: "",
|
||||
CategoryId: "",
|
||||
Privacy: "",
|
||||
RecordingDate: "",
|
||||
PublishAt: "",
|
||||
RegionCode: "",
|
||||
ReasonId: "",
|
||||
SecondaryReasonId: "",
|
||||
OnBehalfOfContentOwnerChannel: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with partial options",
|
||||
args: args{
|
||||
opts: []Option{
|
||||
WithTitle("My Video"),
|
||||
WithDescription("A great video"),
|
||||
WithTags([]string{"tutorial", "golang"}),
|
||||
WithPrivacy("private"),
|
||||
WithMaxResults(25),
|
||||
WithForKids(&forKidsFalse),
|
||||
},
|
||||
},
|
||||
want: &Video{
|
||||
MaxResults: 25,
|
||||
Title: "My Video",
|
||||
Description: "A great video",
|
||||
Tags: []string{"tutorial", "golang"},
|
||||
Privacy: "private",
|
||||
ForKids: &forKidsFalse,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
if got := NewVideo(tt.args.opts...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%s\nNewVideo() = %v\nwant %v", tt.name, got, tt.want)
|
||||
{name: "omitted preserves", input: `{}`, privacy: "private", wantDescription: "Original", wantLanguage: "en", wantPublishAt: "2027-01-01T00:00:00Z"},
|
||||
{name: "empty clears", input: `{"description":"","language":"","publish_at":"","recording_date":""}`, privacy: "private", recordingPart: true},
|
||||
{name: "clear does not require private", input: `{"publish_at":""}`, privacy: "public", wantDescription: "Original", wantLanguage: "en"},
|
||||
{name: "nonempty replaces", input: `{"description":"New","language":"ja","publish_at":"2028-01-01T00:00:00Z","recording_date":"2020-01-01T00:00:00Z"}`, privacy: "private", wantDescription: "New", wantLanguage: "ja", wantPublishAt: "2028-01-01T00:00:00Z", wantRecordingDate: "2020-01-01T00:00:00Z", recordingPart: true},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var body map[string]map[string]any
|
||||
var parts string
|
||||
svc := common.NewTestService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.Method == http.MethodGet {
|
||||
_, _ = io.WriteString(w, `{"items":[{"id":"video-id","snippet":{"title":"Title","categoryId":"22","description":"Original","defaultLanguage":"en"},"status":{"privacyStatus":"`+tt.privacy+`","publishAt":"2027-01-01T00:00:00Z"}}]}`)
|
||||
return
|
||||
}
|
||||
},
|
||||
)
|
||||
parts = r.URL.Query().Get("part")
|
||||
var raw struct {
|
||||
Snippet map[string]any `json:"snippet"`
|
||||
Status map[string]any `json:"status"`
|
||||
RecordingDetails map[string]any `json:"recordingDetails"`
|
||||
}
|
||||
if err := json.UnmarshalRead(r.Body, &raw); err != nil {
|
||||
t.Error(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
body = map[string]map[string]any{"snippet": raw.Snippet, "status": raw.Status, "recordingDetails": raw.RecordingDetails}
|
||||
_, _ = io.WriteString(w, `{"id":"video-id"}`)
|
||||
}))
|
||||
var input Video
|
||||
if err := json.Unmarshal([]byte(tt.input), &input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
input.Service, input.Ids, input.MaxResults, input.Output = svc, []string{"video-id"}, 1, "silent"
|
||||
if err := input.Update(io.Discard); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body == nil {
|
||||
t.Fatal("missing update request")
|
||||
}
|
||||
for _, field := range []struct{ part, key, want string }{
|
||||
{"snippet", "description", tt.wantDescription},
|
||||
{"snippet", "defaultLanguage", tt.wantLanguage},
|
||||
{"status", "publishAt", tt.wantPublishAt},
|
||||
{"recordingDetails", "recordingDate", tt.wantRecordingDate},
|
||||
} {
|
||||
got, present := body[field.part][field.key]
|
||||
if field.want == "" {
|
||||
if present {
|
||||
t.Errorf("%s.%s should be omitted for clearing, got %v", field.part, field.key, got)
|
||||
}
|
||||
} else if got != field.want {
|
||||
t.Errorf("%s.%s = %v, want %q", field.part, field.key, got, field.want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(parts, "recordingDetails") != tt.recordingPart {
|
||||
t.Errorf("unexpected update parts: %s", parts)
|
||||
}
|
||||
if tt.recordingPart && body["recordingDetails"] == nil {
|
||||
t.Error("recordingDetails object missing")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +369,39 @@ func TestVideo_Insert(t *testing.T) {
|
||||
verify func(*http.Request)
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "insert explicit false status fields",
|
||||
opts: []Option{
|
||||
WithFile("test_video.mp4"),
|
||||
WithEmbeddable(new(false)),
|
||||
WithPublicStatsViewable(new(false)),
|
||||
},
|
||||
verify: func(r *http.Request) {
|
||||
_, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
part, err := multipart.NewReader(r.Body, params["boundary"]).NextPart()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = part.Close() }()
|
||||
var body struct {
|
||||
Status map[string]any `json:"status"`
|
||||
}
|
||||
if err := json.UnmarshalRead(part, &body); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
for _, field := range []string{"embeddable", "publicStatsViewable"} {
|
||||
if value, present := body.Status[field]; !present || value != false {
|
||||
t.Errorf("status.%s must be present and false, got %v (present=%v)", field, value, present)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "insert video",
|
||||
opts: []Option{
|
||||
@@ -639,7 +483,7 @@ func TestVideo_Insert(t *testing.T) {
|
||||
WithFile("test_video.mp4"),
|
||||
WithTitle("Dated Video"),
|
||||
WithPrivacy("private"),
|
||||
WithRecordingDate("2024-06-15T10:00:00Z"),
|
||||
WithRecordingDate(new("2024-06-15T10:00:00Z")),
|
||||
WithForKids(&forKidsTrue),
|
||||
WithEmbeddable(&embeddableTrue),
|
||||
WithContainsSyntheticMedia(&containsSyntheticMediaTrue),
|
||||
@@ -815,7 +659,7 @@ func TestVideo_Update(t *testing.T) {
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithTitle("Updated Title"),
|
||||
WithDescription("Updated Description"),
|
||||
WithDescription(new("Updated Description")),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}}]}`,
|
||||
@@ -875,7 +719,7 @@ func TestVideo_Update(t *testing.T) {
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithTags([]string{"new-tag"}),
|
||||
WithLanguage("ja"),
|
||||
WithLanguage(new("ja")),
|
||||
WithLicense("creativeCommon"),
|
||||
WithCategory("22"),
|
||||
WithPrivacy("unlisted"),
|
||||
@@ -924,7 +768,7 @@ func TestVideo_Update(t *testing.T) {
|
||||
name: "update video with publishAt on private video",
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithPublishAt("2026-08-18T15:00:00Z"),
|
||||
WithPublishAt(new("2026-08-18T15:00:00Z")),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}, "status": {"privacyStatus": "private"}}]}`,
|
||||
@@ -955,7 +799,7 @@ func TestVideo_Update(t *testing.T) {
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithPrivacy("private"),
|
||||
WithPublishAt("2026-08-18T15:00:00Z"),
|
||||
WithPublishAt(new("2026-08-18T15:00:00Z")),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}, "status": {"privacyStatus": "public"}}]}`,
|
||||
@@ -985,7 +829,7 @@ func TestVideo_Update(t *testing.T) {
|
||||
name: "update video with publishAt rejects public without explicit private",
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithPublishAt("2026-08-18T15:00:00Z"),
|
||||
WithPublishAt(new("2026-08-18T15:00:00Z")),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}, "status": {"privacyStatus": "public"}}]}`,
|
||||
@@ -1000,7 +844,7 @@ func TestVideo_Update(t *testing.T) {
|
||||
name: "update video with publishAt rejects unlisted without explicit private",
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithPublishAt("2026-08-18T15:00:00Z"),
|
||||
WithPublishAt(new("2026-08-18T15:00:00Z")),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}, "status": {"privacyStatus": "unlisted"}}]}`,
|
||||
@@ -1017,7 +861,7 @@ func TestVideo_Update(t *testing.T) {
|
||||
WithIds([]string{"video-id"}),
|
||||
WithEmbeddable(&embeddableTrue),
|
||||
WithContainsSyntheticMedia(&containsSyntheticMediaTrue),
|
||||
WithRecordingDate("2024-01-01T00:00:00Z"),
|
||||
WithRecordingDate(new("2024-01-01T00:00:00Z")),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}, "status": {"privacyStatus": "public"}}]}`,
|
||||
@@ -1091,6 +935,52 @@ func TestVideo_Update(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideo_UpdateFalseStatusFields(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
status string
|
||||
opts []Option
|
||||
}{
|
||||
{
|
||||
name: "preserve existing false values",
|
||||
status: `{"embeddable":false,"publicStatsViewable":false,"selfDeclaredMadeForKids":false,"containsSyntheticMedia":false}`,
|
||||
},
|
||||
{
|
||||
name: "explicit false overrides true",
|
||||
status: `{"embeddable":true,"publicStatsViewable":false,"selfDeclaredMadeForKids":false,"containsSyntheticMedia":true}`,
|
||||
opts: []Option{WithEmbeddable(new(false)), WithContainsSyntheticMedia(new(false))},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var body struct {
|
||||
Status map[string]any `json:"status"`
|
||||
}
|
||||
svc := common.NewTestService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.Method == http.MethodGet {
|
||||
_, _ = io.WriteString(w, `{"items":[{"id":"video-id","snippet":{"title":"Old title","categoryId":"22"},"status":`+tt.status+`}]}`)
|
||||
return
|
||||
}
|
||||
if err := json.UnmarshalRead(r.Body, &body); err != nil {
|
||||
t.Error(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(w, `{"id":"video-id"}`)
|
||||
}))
|
||||
opts := append([]Option{WithService(svc), WithIds([]string{"video-id"}), WithMaxResults(1), WithTitle("New title"), WithOutput("silent")}, tt.opts...)
|
||||
if err := NewVideo(opts...).Update(io.Discard); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, field := range []string{"embeddable", "publicStatsViewable", "selfDeclaredMadeForKids", "containsSyntheticMedia"} {
|
||||
if value, present := body.Status[field]; !present || value != false {
|
||||
t.Errorf("status.%s must be present and false, got %v (present=%v)", field, value, present)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideo_Rate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1323,7 +1213,7 @@ func TestVideo_ReportAbuse(t *testing.T) {
|
||||
WithIds([]string{"video-id"}),
|
||||
WithReasonId("reason-id"),
|
||||
WithComments("spam"),
|
||||
WithLanguage("en"),
|
||||
WithLanguage(new("en")),
|
||||
WithSecondaryReasonId("secondary-reason"),
|
||||
},
|
||||
verify: func(r *http.Request) {
|
||||
|
||||
Reference in new issue
Block a user