✨ Add publishAt to video update (#238)
* Add publishAt support to video update video insert already scheduled uploads. update did not, so MCP and CLI could not set a publish time on an existing video. * Do not force public videos private when scheduling publishAt on update now errors unless the video is already private or privacy private is set explicitly, so a live video cannot go offline by accident.
This commit is contained in:
3 files changed
+107
-12
No files matched your search
+18
-7
@@ -21,13 +21,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
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")
|
||||
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")
|
||||
errScheduleNonPrivate = errors.New(
|
||||
"publishAt requires privacy private; a public or unlisted video would go offline until the scheduled time",
|
||||
)
|
||||
)
|
||||
|
||||
type Video struct {
|
||||
@@ -331,6 +334,14 @@ func (v *Video) Update(writer io.Writer) error {
|
||||
if v.Privacy != "" {
|
||||
video.Status.PrivacyStatus = v.Privacy
|
||||
}
|
||||
if v.PublishAt != "" {
|
||||
// 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" {
|
||||
return errors.Join(errUpdateVideo, errScheduleNonPrivate)
|
||||
}
|
||||
video.Status.PublishAt = v.PublishAt
|
||||
}
|
||||
if v.Embeddable != nil {
|
||||
video.Status.Embeddable = *v.Embeddable
|
||||
}
|
||||
|
||||
+84
-5
@@ -792,11 +792,11 @@ func TestVideo_Update(t *testing.T) {
|
||||
containsSyntheticMediaTrue := true
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts []Option
|
||||
getResponse string
|
||||
verify func(*http.Request)
|
||||
wantErr bool
|
||||
name string
|
||||
opts []Option
|
||||
getResponse string
|
||||
verify func(*http.Request)
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "update video",
|
||||
@@ -901,6 +901,85 @@ func TestVideo_Update(t *testing.T) {
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "update video with publishAt on private video",
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithPublishAt("2026-08-18T15:00:00Z"),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}, "status": {"privacyStatus": "private"}}]}`,
|
||||
verify: func(r *http.Request) {
|
||||
if r.Method == "PUT" {
|
||||
var body youtube.Video
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("failed to decode update body: %v", err)
|
||||
}
|
||||
if body.Status.PublishAt != "2026-08-18T15:00:00Z" {
|
||||
t.Errorf("expected publishAt=2026-08-18T15:00:00Z, got %s", body.Status.PublishAt)
|
||||
}
|
||||
if body.Status.PrivacyStatus != "private" {
|
||||
t.Errorf("expected privacyStatus=private when scheduling, got %s", body.Status.PrivacyStatus)
|
||||
}
|
||||
}
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "update video with publishAt and explicit private on public video",
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithPrivacy("private"),
|
||||
WithPublishAt("2026-08-18T15:00:00Z"),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}, "status": {"privacyStatus": "public"}}]}`,
|
||||
verify: func(r *http.Request) {
|
||||
if r.Method == "PUT" {
|
||||
var body youtube.Video
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("failed to decode update body: %v", err)
|
||||
}
|
||||
if body.Status.PublishAt != "2026-08-18T15:00:00Z" {
|
||||
t.Errorf("expected publishAt=2026-08-18T15:00:00Z, got %s", body.Status.PublishAt)
|
||||
}
|
||||
if body.Status.PrivacyStatus != "private" {
|
||||
t.Errorf("expected privacyStatus=private when scheduling, got %s", body.Status.PrivacyStatus)
|
||||
}
|
||||
}
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "update video with publishAt rejects public without explicit private",
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithPublishAt("2026-08-18T15:00:00Z"),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}, "status": {"privacyStatus": "public"}}]}`,
|
||||
verify: func(r *http.Request) {
|
||||
if r.Method == "PUT" {
|
||||
t.Error("expected no update request when scheduling a public video")
|
||||
}
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "update video with publishAt rejects unlisted without explicit private",
|
||||
opts: []Option{
|
||||
WithIds([]string{"video-id"}),
|
||||
WithPublishAt("2026-08-18T15:00:00Z"),
|
||||
WithMaxResults(1),
|
||||
},
|
||||
getResponse: `{"items": [{"id": "video-id", "snippet": {"title": "Old Title"}, "status": {"privacyStatus": "unlisted"}}]}`,
|
||||
verify: func(r *http.Request) {
|
||||
if r.Method == "PUT" {
|
||||
t.Error("expected no update request when scheduling an unlisted video")
|
||||
}
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "update video with embeddable, containsSyntheticMedia, recordingDate",
|
||||
opts: []Option{
|
||||
|
||||
Reference in new issue
Block a user