From 4063c82e6aa22589dfa779743ace7bb86889e0c0 Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sun, 3 Mar 2024 16:30:10 -0500 Subject: [PATCH] Add Schedule endpoints to API --- api/api.go | 12 ++- .../create_channel_stream_schedule_segment.go | 74 +++++++++++++++ .../delete_channel_stream_schedule_segment.go | 40 +++++++++ api/schedule/get_channel_icalendar.go | 16 ++++ api/schedule/get_channel_stream_schedule.go | 72 +++++++++++++++ api/schedule/models.go | 61 +++++++++++++ api/schedule/schedule.go | 18 ++++ .../update_channel_stream_schedule.go | 53 +++++++++++ .../update_channel_stream_schedule_segment.go | 90 +++++++++++++++++++ 9 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 api/schedule/create_channel_stream_schedule_segment.go create mode 100644 api/schedule/delete_channel_stream_schedule_segment.go create mode 100644 api/schedule/get_channel_icalendar.go create mode 100644 api/schedule/get_channel_stream_schedule.go create mode 100644 api/schedule/models.go create mode 100644 api/schedule/schedule.go create mode 100644 api/schedule/update_channel_stream_schedule.go create mode 100644 api/schedule/update_channel_stream_schedule_segment.go diff --git a/api/api.go b/api/api.go index 219b840..a58031a 100644 --- a/api/api.go +++ b/api/api.go @@ -24,6 +24,7 @@ import ( "go.fifitido.net/twitch/api/polls" "go.fifitido.net/twitch/api/predictions" "go.fifitido.net/twitch/api/raids" + "go.fifitido.net/twitch/api/schedule" ) const HelixBaseUrl = "https://api.twitch.tv/helix" @@ -52,10 +53,10 @@ type API struct { Polls *polls.Polls Predictions *predictions.Predictions Raids *raids.Raids + Schedule *schedule.Schedule } -func New() *API { - client := &http.Client{} +func NewWithClient(client *http.Client) *API { baseUrl, _ := url.Parse(HelixBaseUrl) return &API{ @@ -82,5 +83,12 @@ func New() *API { Polls: polls.New(client, baseUrl), Predictions: predictions.New(client, baseUrl), Raids: raids.New(client, baseUrl), + Schedule: schedule.New(client, baseUrl), } } + +func New() *API { + client := &http.Client{} + + return NewWithClient(client) +} diff --git a/api/schedule/create_channel_stream_schedule_segment.go b/api/schedule/create_channel_stream_schedule_segment.go new file mode 100644 index 0000000..9b243a2 --- /dev/null +++ b/api/schedule/create_channel_stream_schedule_segment.go @@ -0,0 +1,74 @@ +package schedule + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "time" +) + +type CreateChannelStreamScheduleSegmentRequest struct { + // The date and time that the broadcast segment starts. Specify the date and time in RFC3339 format (for example, 2021-07-01T18:00:00Z). + StartTime time.Time `json:"start_time"` + + // The time zone where the broadcast takes place. Specify the time zone using IANA time zone database format (for example, America/New_York). + Timezone string `json:"timezone"` + + // The length of time, in minutes, that the broadcast is scheduled to run. The duration must be in the range 30 through 1380 (23 hours). + Duration int `json:"duration"` + + // A Boolean value that determines whether the broadcast recurs weekly. Is true if the broadcast recurs weekly. + // Only partners and affiliates may add non-recurring broadcasts. + IsRecurring *bool `json:"is_recurring,omitempty"` + + // The ID of the category that best represents the broadcast’s content. + // To get the category ID, use the Search Categories endpoint. + CategoryId *string `json:"category_id"` + + // The broadcast’s title. The title may contain a maximum of 140 characters. + Title *string `json:"title"` +} + +type CreateChannelStreamScheduleSegmentResponse struct { + // The broadcaster’s streaming schedule. + Data ChannelStreamSchedule `json:"data"` +} + +// Adds a single or recurring broadcast to the broadcaster’s streaming schedule. For information about scheduling broadcasts, see Stream Schedule. +// +// Requires a user access token that includes the channel:manage:schedule scope. +// +// The broadcaster ID must match the user ID in the user access token. +func (s *Schedule) CreateChannelStreamScheduleSegment(ctx context.Context, broadcasterId string, body *CreateChannelStreamScheduleSegmentRequest) (*CreateChannelStreamScheduleSegmentResponse, error) { + endpoint := s.baseUrl.ResolveReference(&url.URL{Path: "schedule/segment", RawQuery: url.Values{"broadcaster_id": {broadcasterId}}.Encode()}) + + r, w := io.Pipe() + + go func() { + if err := json.NewEncoder(w).Encode(body); err != nil { + w.CloseWithError(err) + } else { + w.Close() + } + }() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), r) + if err != nil { + return nil, err + } + + res, err := s.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data CreateChannelStreamScheduleSegmentResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/schedule/delete_channel_stream_schedule_segment.go b/api/schedule/delete_channel_stream_schedule_segment.go new file mode 100644 index 0000000..7ca3851 --- /dev/null +++ b/api/schedule/delete_channel_stream_schedule_segment.go @@ -0,0 +1,40 @@ +package schedule + +import ( + "context" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" +) + +type DeleteChannelStreamScheduleSegmentParams struct { + // The ID of the broadcaster that owns the streaming schedule. This ID must match the user ID in the user access token. + BroadcasterId string `url:"broadcaster_id"` + + // The ID of the broadcast segment to remove. + Id string `url:"id"` +} + +// Removes a broadcast segment from the broadcaster’s streaming schedule. +// +// NOTE: For recurring segments, removing a segment removes all segments in the recurring schedule. +// +// Requires a user access token that includes the channel:manage:schedule scope. +func (s *Schedule) DeleteChannelStreamScheduleSegment(ctx context.Context, params *DeleteChannelStreamScheduleSegmentParams) error { + v, _ := query.Values(params) + endpoint := s.baseUrl.ResolveReference(&url.URL{Path: "schedule/segment", RawQuery: v.Encode()}) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint.String(), nil) + if err != nil { + return err + } + + res, err := s.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + return nil +} diff --git a/api/schedule/get_channel_icalendar.go b/api/schedule/get_channel_icalendar.go new file mode 100644 index 0000000..2fb6bdf --- /dev/null +++ b/api/schedule/get_channel_icalendar.go @@ -0,0 +1,16 @@ +package schedule + +import ( + "context" + "net/url" +) + +// Gets the broadcaster’s streaming schedule as an iCalendar. +// +// The Client-Id and Authorization headers are not required. +func (s *Schedule) GetChannelICalendarURL(ctx context.Context, broadcasterId string) *url.URL { + return s.baseUrl.ResolveReference(&url.URL{ + Path: "schedule/icalendar", + RawQuery: url.Values{"broadcaster_id": {broadcasterId}}.Encode(), + }) +} diff --git a/api/schedule/get_channel_stream_schedule.go b/api/schedule/get_channel_stream_schedule.go new file mode 100644 index 0000000..1f6f189 --- /dev/null +++ b/api/schedule/get_channel_stream_schedule.go @@ -0,0 +1,72 @@ +package schedule + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "time" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetChannelStreamScheduleParams struct { + // The ID of the broadcaster that owns the streaming schedule you want to get. + BroadcasterId string `url:"broadcaster_id"` + + // The ID of the scheduled segment to return. + // To specify more than one segment, include the ID of each segment you want to get. For example, id=1234&id=5678. + // You may specify a maximum of 100 IDs. + IDs []string `url:"id,omitempty"` + + // The UTC date and time that identifies when in the broadcaster’s schedule to start returning segments. + // If not specified, the request returns segments starting after the current UTC date and time. + // Specify the date and time in RFC3339 format (for example, 2022-09-01T00:00:00Z). + StartTime *time.Time `url:"start_time,omitempty"` + + // Not supported. + UTCOffset *string `url:"utc_offset,omitempty"` + + // The maximum number of items to return per page in the response. + // The minimum page size is 1 item per page and the maximum is 25 items per page. + // The default is 20. + First int `url:"first,omitempty"` + + // The cursor used to get the next page of results. + // The Pagination object in the response contains the cursor’s value. + // Read More: https://dev.twitch.tv/docs/api/guide#pagination + After *types.Cursor `url:"after,omitempty"` +} + +type GetChannelStreamScheduleResponse struct { + // The broadcaster’s streaming schedule. + Data ChannelStreamSchedule `json:"data"` +} + +// Gets the broadcaster’s streaming schedule. You can get the entire schedule or specific segments of the schedule. +// Learn More: https://help.twitch.tv/s/article/channel-page-setup#Schedule +// +// Requires an app access token or user access token. +func (s *Schedule) GetChannelStreamSchedule(ctx context.Context, params *GetChannelStreamScheduleParams) (*GetChannelStreamScheduleResponse, error) { + v, _ := query.Values(params) + endpoint := s.baseUrl.ResolveReference(&url.URL{Path: "schedule", RawQuery: v.Encode()}) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, err + } + + res, err := s.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data GetChannelStreamScheduleResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/schedule/models.go b/api/schedule/models.go new file mode 100644 index 0000000..1c17aa1 --- /dev/null +++ b/api/schedule/models.go @@ -0,0 +1,61 @@ +package schedule + +import "time" + +type ChannelStreamSchedule struct { + // The list of broadcasts in the broadcaster’s streaming schedule. + Segments []Segment `json:"segments"` + + // The ID of the broadcaster that owns the broadcast schedule. + BroadcasterId string `json:"broadcaster_id"` + + // The broadcaster’s display name. + BroadcasterName string `json:"broadcaster_name"` + + // The broadcaster’s login name. + BroadcasterLogin string `json:"broadcaster_login"` + + // The dates when the broadcaster is on vacation and not streaming. + Vacation *Vacation `json:"vacation"` +} + +type Segment struct { + // An ID that identifies this broadcast segment. + Id string `json:"id"` + + // The UTC date and time (in RFC3339 format) of when the broadcast starts. + StartTime time.Time `json:"start_time"` + + // The UTC date and time (in RFC3339 format) of when the broadcast ends. + EndTime time.Time `json:"end_time"` + + // The broadcast segment’s title. + Title string `json:"title"` + + // Indicates whether the broadcaster canceled this segment of a recurring broadcast. + // If the broadcaster canceled this segment, this field is set to the same value that’s in the end_time field; otherwise, it’s set to null. + CanceledUntil *time.Time `json:"canceled_until"` + + // The type of content that the broadcaster plans to stream or null if not specified. + Category *Category `json:"category"` + + // A Boolean value that determines whether the broadcast is part of a recurring series that streams at the same time each week or is a one-time broadcast. + // Is true if the broadcast is part of a recurring series. + IsRecurring bool `json:"is_recurring"` +} + +type Category struct { + // An ID that identifies the category that best represents the content that the broadcaster plans to stream. + Id string `json:"id"` + + // The name of the category. + Name string `json:"name"` +} + +type Vacation struct { + // The UTC date and time (in RFC3339 format) of when the broadcaster’s vacation starts. + StartTime time.Time `json:"start_time"` + + // The UTC date and time (in RFC3339 format) of when the broadcaster’s vacation ends. + EndTime time.Time `json:"end_time"` +} diff --git a/api/schedule/schedule.go b/api/schedule/schedule.go new file mode 100644 index 0000000..e62393d --- /dev/null +++ b/api/schedule/schedule.go @@ -0,0 +1,18 @@ +package schedule + +import ( + "net/http" + "net/url" +) + +type Schedule struct { + client *http.Client + baseUrl *url.URL +} + +func New(client *http.Client, baseUrl *url.URL) *Schedule { + return &Schedule{ + client: client, + baseUrl: baseUrl, + } +} diff --git a/api/schedule/update_channel_stream_schedule.go b/api/schedule/update_channel_stream_schedule.go new file mode 100644 index 0000000..ce86a54 --- /dev/null +++ b/api/schedule/update_channel_stream_schedule.go @@ -0,0 +1,53 @@ +package schedule + +import ( + "context" + "net/http" + "net/url" + "time" + + "github.com/google/go-querystring/query" +) + +type UpdateChannelStreamScheduleParams struct { + // The ID of the broadcaster whose schedule settings you want to update. + // The ID must match the user ID in the user access token. + BroadcasterId string `url:"broadcaster_id"` + + // A Boolean value that indicates whether the broadcaster has scheduled a vacation. + // Set to true to enable Vacation Mode and add vacation dates, or false to cancel a previously scheduled vacation. + IsVacationEnabled *bool `url:"is_vacation_enabled,omitempty"` + + // The UTC date and time of when the broadcaster’s vacation starts. + // Specify the date and time in RFC3339 format (for example, 2021-05-16T00:00:00Z). + VacationStartTime *time.Time `url:"vacation_start_time,omitempty"` + + // The UTC date and time of when the broadcaster’s vacation ends. + // Specify the date and time in RFC3339 format (for example, 2021-05-30T23:59:59Z). + VacationEndTime *time.Time `url:"vacation_end_time,omitempty"` + + // The time zone that the broadcaster broadcasts from. + // Specify the time zone using IANA time zone database format (for example, America/New_York). + Timezone *time.Location `url:"timezone,omitempty"` +} + +// Updates the broadcaster’s schedule settings, such as scheduling a vacation. +// +// Requires a user access token that includes the channel:manage:schedule scope. +func (s *Schedule) UpdateChannelStreamSchedule(ctx context.Context, params *UpdateChannelStreamScheduleParams) error { + v, _ := query.Values(params) + endpoint := s.baseUrl.ResolveReference(&url.URL{Path: "schedule/settings", RawQuery: v.Encode()}) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), nil) + if err != nil { + return err + } + + res, err := s.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + return nil +} diff --git a/api/schedule/update_channel_stream_schedule_segment.go b/api/schedule/update_channel_stream_schedule_segment.go new file mode 100644 index 0000000..b90c32f --- /dev/null +++ b/api/schedule/update_channel_stream_schedule_segment.go @@ -0,0 +1,90 @@ +package schedule + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "time" + + "github.com/google/go-querystring/query" +) + +type UpdateChannelStreamScheduleSegmentParams struct { + // The ID of the broadcaster who owns the broadcast segment to update. This ID must match the user ID in the user access token. + BroadcasterId string `url:"broadcaster_id"` + + // The ID of the broadcast segment to update. + Id string `url:"id"` +} + +type UpdateChannelStreamScheduleSegmentRequest struct { + // The date and time that the broadcast segment starts. Specify the date and time in RFC3339 format (for example, 2021-07-01T18:00:00Z). + // + // NOTE: Only partners and affiliates may update a broadcast’s start time and only for non-recurring segments. + StartTime *time.Time `json:"start_time"` + + // The length of time, in minutes, that the broadcast is scheduled to run. The duration must be in the range 30 through 1380 (23 hours). + Duration *int `json:"duration"` + + // The ID of the category that best represents the broadcast’s content. + // To get the category ID, use the Search Categories endpoint. + CategoryId *string `json:"category_id"` + + // The broadcast’s title. The title may contain a maximum of 140 characters. + Title *string `json:"title"` + + // A Boolean value that indicates whether the broadcast is canceled. Set to true to cancel the segment. + // + // NOTE: For recurring segments, the API cancels the first segment after the current UTC date and time and not the specified segment + // (unless the specified segment is the next segment after the current UTC date and time). + IsCanceled *bool `json:"is_canceled"` + + // The time zone where the broadcast takes place. Specify the time zone using IANA time zone database format (for example, America/New_York). + Timezone *time.Location `json:"timezone"` +} + +type UpdateChannelStreamScheduleSegmentResponse struct { + // The broadcaster’s streaming schedule. + Data ChannelStreamSchedule `json:"data"` +} + +// Updates a scheduled broadcast segment. +// +// For recurring segments, updating a segment’s title, category, duration, and timezone, +// changes all segments in the recurring schedule, not just the specified segment. +// +// Requires a user access token that includes the channel:manage:schedule scope. +func (s *Schedule) UpdateChannelStreamScheduleSegment(ctx context.Context, params *UpdateChannelStreamScheduleSegmentParams, body *UpdateChannelStreamScheduleSegmentRequest) (*UpdateChannelStreamScheduleSegmentResponse, error) { + v, _ := query.Values(params) + endpoint := s.baseUrl.ResolveReference(&url.URL{Path: "schedule/segment", RawQuery: v.Encode()}) + + r, w := io.Pipe() + + go func() { + if err := json.NewEncoder(w).Encode(body); err != nil { + w.CloseWithError(err) + } else { + w.Close() + } + }() + + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint.String(), r) + if err != nil { + return nil, err + } + + res, err := s.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data UpdateChannelStreamScheduleSegmentResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +}