Skip to content

Commit 8e534f0

Browse files
b-abadiemxpv
authored andcommitted
feat: add Twitch support
1 parent 5ba8a89 commit 8e534f0

File tree

6 files changed

+176
-1
lines changed

6 files changed

+176
-1
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/golang/mock v1.6.0
1212
github.com/hashicorp/go-multierror v1.1.1
1313
github.com/jessevdk/go-flags v1.6.1
14+
github.com/nicklaw5/helix v1.25.0
1415
github.com/pelletier/go-toml v1.9.5
1516
github.com/pkg/errors v0.9.1
1617
github.com/robfig/cron/v3 v3.0.1
@@ -36,6 +37,7 @@ require (
3637
github.com/felixge/httpsnoop v1.0.4 // indirect
3738
github.com/go-logr/logr v1.4.2 // indirect
3839
github.com/go-logr/stdr v1.2.2 // indirect
40+
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
3941
github.com/golang/protobuf v1.5.4 // indirect
4042
github.com/google/s2a-go v0.1.9 // indirect
4143
github.com/google/uuid v1.6.0 // indirect

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,16 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
3838
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
3939
github.com/gilliek/go-opml v1.0.0 h1:X8xVjtySRXU/x6KvaiXkn7OV3a4DHqxY8Rpv6U/JvCY=
4040
github.com/gilliek/go-opml v1.0.0/go.mod h1:fOxmtlzyBvUjU6bjpdjyxCGlWz+pgtAHrHf/xRZl3lk=
41+
<<<<<<< HEAD
4142
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
4243
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
4344
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
4445
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
4546
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
47+
=======
48+
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
49+
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
50+
>>>>>>> da8d2ab (feat: add Twitch support)
4651
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
4752
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
4853
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -82,6 +87,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
8287
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
8388
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
8489
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
90+
github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M=
91+
github.com/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw=
8592
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
8693
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
8794
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=

pkg/builder/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ func New(ctx context.Context, provider model.Provider, key string, downloader Do
2121
return NewVimeoBuilder(ctx, key)
2222
case model.ProviderSoundcloud:
2323
return NewSoundcloudBuilder()
24+
case model.ProviderTwitch:
25+
return NewTwitchBuilder(key)
2426
default:
2527
return nil, errors.Errorf("unsupported provider %q", provider)
2628
}

pkg/builder/twitch.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package builder
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"github.com/mxpv/podsync/pkg/feed"
10+
"github.com/mxpv/podsync/pkg/model"
11+
"github.com/nicklaw5/helix"
12+
"github.com/pkg/errors"
13+
)
14+
15+
type TwitchBuilder struct {
16+
client *helix.Client
17+
}
18+
19+
func (t *TwitchBuilder) Build(_ctx context.Context, cfg *feed.Config) (*model.Feed, error) {
20+
info, err := ParseURL(cfg.URL)
21+
if err != nil {
22+
return nil, errors.Wrap(err, "failed to parse URL")
23+
}
24+
25+
feed := &model.Feed{
26+
ItemID: info.ItemID,
27+
Provider: info.Provider,
28+
LinkType: info.LinkType,
29+
Format: cfg.Format,
30+
Quality: cfg.Quality,
31+
PageSize: cfg.PageSize,
32+
UpdatedAt: time.Now().UTC(),
33+
}
34+
35+
if info.LinkType == model.TypeUser {
36+
37+
users, err := t.client.GetUsers(&helix.UsersParams{
38+
Logins: []string{info.ItemID},
39+
})
40+
if err != nil {
41+
return nil, errors.Wrapf(err, "failed to get user: %s", info.ItemID)
42+
}
43+
user := users.Data.Users[0]
44+
45+
feed.Title = user.DisplayName
46+
feed.Author = user.DisplayName
47+
feed.Description = user.Description
48+
feed.ItemURL = fmt.Sprintf("https://www.twitch.tv/%s", user.Login)
49+
feed.CoverArt = user.ProfileImageURL
50+
feed.PubDate = user.CreatedAt.Time
51+
52+
videos, err := t.client.GetVideos(&helix.VideosParams{
53+
UserID: user.ID,
54+
Period: "all",
55+
Type: "all",
56+
Sort: "time",
57+
First: 10,
58+
})
59+
if err != nil {
60+
return nil, errors.Wrapf(err, "failed to get videos for user: %s", info.ItemID)
61+
}
62+
63+
var added = 0
64+
for _, video := range videos.Data.Videos {
65+
66+
date, err := time.Parse(time.RFC3339, video.PublishedAt)
67+
if err != nil {
68+
return nil, errors.Wrapf(err, "cannot parse PublishedAt time: %s", video.PublishedAt)
69+
}
70+
71+
replacer := strings.NewReplacer("%{width}", "300", "%{height}", "300")
72+
thumbnailUrl := replacer.Replace(video.ThumbnailURL)
73+
74+
duration, err := time.ParseDuration(video.Duration)
75+
if err != nil {
76+
return nil, errors.Wrapf(err, "cannot parse duration: %s", video.Duration)
77+
}
78+
durationSeconds := int64(duration.Seconds())
79+
80+
feed.Episodes = append(feed.Episodes, &model.Episode{
81+
ID: video.ID,
82+
Title: fmt.Sprintf("%s (%s)", video.Title, date),
83+
Description: video.Description,
84+
Thumbnail: thumbnailUrl,
85+
Duration: durationSeconds,
86+
Size: durationSeconds * 33013, // Very rough estimate
87+
VideoURL: video.URL,
88+
PubDate: date,
89+
Status: model.EpisodeNew,
90+
})
91+
92+
added++
93+
if added >= feed.PageSize {
94+
return feed, nil
95+
}
96+
97+
}
98+
99+
return feed, nil
100+
101+
}
102+
103+
return nil, errors.New("unsupported feed type")
104+
}
105+
106+
func NewTwitchBuilder(clientIDSecret string) (*TwitchBuilder, error) {
107+
parts := strings.Split(clientIDSecret, ":")
108+
if len(parts) != 2 {
109+
return nil, errors.New("invalid twitch key, need to be \"CLIENT_ID:CLIENT_SECRET\"")
110+
}
111+
112+
clientID := parts[0]
113+
clientSecret := parts[1]
114+
115+
client, err := helix.NewClient(&helix.Options{
116+
ClientID: clientID,
117+
ClientSecret: clientSecret,
118+
})
119+
if err != nil {
120+
return nil, errors.Wrap(err, "failed to create twitch client")
121+
}
122+
123+
token, err := client.RequestAppAccessToken([]string{})
124+
if err != nil {
125+
return nil, errors.Wrap(err, "failed to request twitch app token")
126+
}
127+
128+
// Set the access token on the client
129+
client.SetAppAccessToken(token.Data.AccessToken)
130+
131+
return &TwitchBuilder{client: client}, nil
132+
}

pkg/builder/url.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ func ParseURL(link string) (model.Info, error) {
5656
return info, nil
5757
}
5858

59+
if strings.HasSuffix(parsed.Host, "twitch.tv") {
60+
kind, id, err := parseTwitchURL(parsed)
61+
if err != nil {
62+
return model.Info{}, err
63+
}
64+
65+
info.Provider = model.ProviderTwitch
66+
info.LinkType = kind
67+
info.ItemID = id
68+
69+
return info, nil
70+
}
71+
5972
return model.Info{}, errors.New("unsupported URL host")
6073
}
6174

@@ -186,3 +199,21 @@ func parseSoundcloudURL(parsed *url.URL) (model.Type, string, error) {
186199

187200
return kind, id, nil
188201
}
202+
203+
func parseTwitchURL(parsed *url.URL) (model.Type, string, error) {
204+
// - https://www.twitch.tv/samueletienne
205+
path := parsed.EscapedPath()
206+
parts := strings.Split(path, "/")
207+
if len(parts) != 2 {
208+
return "", "", errors.Errorf("invald twitch user path: %s", path)
209+
}
210+
211+
kind := model.TypeUser
212+
213+
id := parts[1]
214+
if id == "" {
215+
return "", "", errors.New("invalid id")
216+
}
217+
218+
return kind, id, nil
219+
}

pkg/model/link.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ const (
1515
ProviderYoutube = Provider("youtube")
1616
ProviderVimeo = Provider("vimeo")
1717
ProviderSoundcloud = Provider("soundcloud")
18+
ProviderTwitch = Provider("twitch")
1819
)
1920

2021
// Info represents data extracted from URL
2122
type Info struct {
2223
LinkType Type // Either group, channel or user
23-
Provider Provider // Youtube, Vimeo, or SoundCloud
24+
Provider Provider // Youtube, Vimeo, SoundCloud or Twitch
2425
ItemID string
2526
}

0 commit comments

Comments
 (0)