[templating] Add currentVOD function

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2024-09-11 20:30:55 +02:00
parent 740a71a173
commit 19038dbc6e
Signed by: luzifer
SSH key fingerprint: SHA256:/xtE5lCgiRDQr8SLxHMS92ZBlACmATUmF1crK16Ks4E
4 changed files with 215 additions and 2 deletions

View file

@ -165,6 +165,19 @@ Example:
* 1 6 * 1 6
``` ```
### `currentVOD`
Returns the VOD of the currently running stream in the given channel (causes an error if no current stream / VOD is found)
Syntax: `currentVOD <username>`
Example:
```
# {{ currentVOD .channel }}
* https://www.twitch.tv/videos/123456789
```
### `displayName` ### `displayName`
Returns the display name the specified user set for themselves Returns the display name the specified user set for themselves
@ -467,7 +480,7 @@ Example:
``` ```
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}% # Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
< Your int this hour: 41% < Your int this hour: 23%
``` ```
### `spotifyCurrentPlaying` ### `spotifyCurrentPlaying`

View file

@ -116,7 +116,7 @@ SocketMessage received for every event and passed to the new `(eventObj) => { ..
| Name | Type | Description | | Name | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| [event_id] | <code>Number</code> | UID of the event used to re-trigger an event | | [event_id] | <code>String</code> | UID of the event used to re-trigger an event |
| [is_live] | <code>Boolean</code> | Whether the event was sent through a replay (false) or occurred live (true) | | [is_live] | <code>Boolean</code> | Whether the event was sent through a replay (false) or occurred live (true) |
| [reason] | <code>String</code> | Reason of this message (one of `bulk-replay`, `live-event`, `single-replay`) | | [reason] | <code>String</code> | Reason of this message (one of `bulk-replay`, `live-event`, `single-replay`) |
| [time] | <code>String</code> | RFC3339 timestamp of the event | | [time] | <code>String</code> | RFC3339 timestamp of the event |

View file

@ -0,0 +1,50 @@
package twitch
import (
"context"
"fmt"
"strings"
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
"github.com/Luzifer/twitch-bot/v3/plugins"
)
func init() {
regFn = append(
regFn,
tplTwitchCurrentVOD,
)
}
func tplTwitchCurrentVOD(args plugins.RegistrationArguments) {
args.RegisterTemplateFunction("currentVOD", plugins.GenericTemplateFunctionGetter(func(username string) (string, error) {
si, err := args.GetTwitchClient().GetCurrentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))
if err != nil {
return "", fmt.Errorf("getting stream info: %w", err)
}
vids, err := args.GetTwitchClient().GetVideos(context.TODO(), twitch.GetVideoOpts{
UserID: si.UserID,
})
if err != nil {
return "", fmt.Errorf("getting videos: %w", err)
}
for _, v := range vids {
if v.StreamID == nil || *v.StreamID != si.ID {
continue
}
return v.URL, nil
}
return "", fmt.Errorf("no matching VOD found")
}), plugins.TemplateFuncDocumentation{
Description: "Returns the VOD of the currently running stream in the given channel (causes an error if no current stream / VOD is found)",
Syntax: "currentVOD <username>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ currentVOD .channel }}`,
FakedOutput: "https://www.twitch.tv/videos/123456789",
},
})
}

150
pkg/twitch/videos.go Normal file
View file

@ -0,0 +1,150 @@
package twitch
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"github.com/mitchellh/hashstructure/v2"
)
type (
// GetVideoOpts contain the query parameter for the GetVideos query
//
// See https://dev.twitch.tv/docs/api/reference/#get-videos for details
GetVideoOpts struct {
ID string // Required: Exactly one of ID, UserID, GameID
UserID string // Required: Exactly one of ID, UserID, GameID
GameID string // Required: Exactly one of ID, UserID, GameID
Language string // Optional: Use only with GameID
Period GetVideoOptsPeriod // Optional: Use only with GameID or UserID
Sort GetVideoOptsSort // Optional: Use only with GameID or UserID
Type GetVideoOptsType // Optional: Use only with GameID or UserID
First int64 // Optional: Use only with GameID or UserID
After string // Optional: Use only with UserID
Before string // Optional: Use only with UserID
}
// GetVideoOptsPeriod represents a filter used to filter the list of
// videos by when they were published
GetVideoOptsPeriod string
// GetVideoOptsSort represents the order to sort the returned videos in
GetVideoOptsSort string
// GetVideoOptsType represents a filter used to filter the list of
// videos by the video's type
GetVideoOptsType string
// Video contains information about a published video
Video struct {
ID string `json:"id"`
StreamID *string `json:"stream_id"`
UserID string `json:"user_id"`
UserLogin string `json:"user_login"`
UserName string `json:"user_name"`
Title string `json:"title"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
URL string `json:"url"`
ThumbnailURL string `json:"thumbnail_url"`
Viewable string `json:"viewable"`
ViewCount int64 `json:"view_count"`
Language string `json:"language"`
Type string `json:"type"`
Duration string `json:"duration"`
MutedSegments []struct {
Duration int64 `json:"duration"`
Offset int64 `json:"offset"`
} `json:"muted_segments"`
}
)
// List of filters for GetVideoOpts.Period
const (
GetVideoOptsPeriodAll GetVideoOptsPeriod = "all"
GetVideoOptsPeriodDay GetVideoOptsPeriod = "day"
GetVideoOptsPeriodMonth GetVideoOptsPeriod = "month"
GetVideoOptsPeriodWeek GetVideoOptsPeriod = "week"
)
// List of sort options for GetVideoOpts.Sort
const (
GetVideoOptsSortTime GetVideoOptsSort = "time"
GetVideoOptsSortTrending GetVideoOptsSort = "trending"
GetVideoOptsSortViews GetVideoOptsSort = "views"
)
// List of types for GetVideoOpts.Type
const (
GetVideoOptsTypeAll GetVideoOptsType = "all"
GetVideoOptsTypeArchive GetVideoOptsType = "archive"
GetVideoOptsTypeHighlight GetVideoOptsType = "highlight"
GetVideoOptsTypeUpload GetVideoOptsType = "upload"
)
// GetVideos fetches information about one or more published videos
func (c *Client) GetVideos(ctx context.Context, opts GetVideoOpts) (videos []Video, err error) {
optsCacheKey, err := opts.cacheKey()
if err != nil {
return nil, fmt.Errorf("getting opts cache key: %w", err)
}
cacheKey := []string{"currentVideos", optsCacheKey}
if vids := c.apiCache.Get(cacheKey); vids != nil {
return vids.([]Video), nil
}
var payload struct {
Data []Video `json:"data"`
}
if err := c.Request(ctx, ClientRequestOpts{
AuthType: AuthTypeAppAccessToken,
Method: http.MethodGet,
OKStatus: http.StatusOK,
Out: &payload,
URL: fmt.Sprintf("https://api.twitch.tv/helix/videos?%s", opts.queryParams()),
}); err != nil {
return nil, fmt.Errorf("requesting videos: %w", err)
}
// Videos can be changed at any moment, cache for a short period of time
c.apiCache.Set(cacheKey, twitchMinCacheTime, payload.Data)
return payload.Data, nil
}
func (g GetVideoOpts) cacheKey() (string, error) {
h, err := hashstructure.Hash(g, hashstructure.FormatV2, nil)
if err != nil {
return "", fmt.Errorf("hashing opts: %w", err)
}
return strconv.FormatUint(h, 10), nil
}
func (g GetVideoOpts) queryParams() string {
params := url.Values{}
for k, v := range map[string]string{
"id": g.ID,
"user_id": g.UserID,
"game_id": g.GameID,
"language": g.Language,
"period": string(g.Period),
"sort": string(g.Sort),
"type": string(g.Type),
"first": strconv.FormatInt(g.First, 10),
"after": g.After,
"before": g.Before,
} {
if v != "" && v != "0" {
params.Set(k, v)
}
}
return params.Encode()
}