✨ 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:
Jouy authored and GitHub committed 2026-08-20 21:19:52 +08:00
1 parent 7ae41f3182
commit 15c01c9534
3 files changed
+107 -12

No files matched your search

+18 -7
View File
@@ -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
View File
@@ -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{