Skip to content

Commit c1130f3

Browse files
unshuffledmxpv
authored andcommitted
Initial SoundCloud playlist support
1 parent da93686 commit c1130f3

File tree

7 files changed

+183
-3
lines changed

7 files changed

+183
-3
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/silentsokolov/go-vimeo v0.0.0-20190116124215-06829264260c
1717
github.com/sirupsen/logrus v1.2.0
1818
github.com/stretchr/testify v1.4.0
19+
github.com/zackradisic/soundcloud-api v0.1.5
1920
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
2021
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd
2122
golang.org/x/sync v0.0.0-20190423024810-112230192c58

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
2727
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
2828
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
2929
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
30+
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
31+
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
3032
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
3133
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
3234
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
@@ -76,6 +78,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
7678
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
7779
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
7880
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
81+
github.com/zackradisic/soundcloud-api v0.1.5 h1:OVg8XlbNjrSpSAJhIdBDjUC/Vm1lQYPrDOhnNnImNGg=
82+
github.com/zackradisic/soundcloud-api v0.1.5/go.mod h1:ycGIZFVZdUVC7B8pcfgze1bRBePPmjYlIGnRptKByQ0=
7983
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
8084
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
8185
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

pkg/builder/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ func New(ctx context.Context, provider model.Provider, key string) (Builder, err
1919
return NewYouTubeBuilder(key)
2020
case model.ProviderVimeo:
2121
return NewVimeoBuilder(ctx, key)
22+
case model.ProviderSoundcloud:
23+
return NewSoundcloudBuilder()
2224
default:
2325
return nil, errors.Errorf("unsupported provider %q", provider)
2426
}

pkg/builder/soundcloud.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package builder
2+
3+
import (
4+
"strconv"
5+
"time"
6+
7+
"github.com/pkg/errors"
8+
soundcloudapi "github.com/zackradisic/soundcloud-api"
9+
"golang.org/x/net/context"
10+
11+
"github.com/mxpv/podsync/pkg/config"
12+
"github.com/mxpv/podsync/pkg/model"
13+
)
14+
15+
type SoundCloudBuilder struct {
16+
client *soundcloudapi.API
17+
}
18+
19+
func (s *SoundCloudBuilder) Build(ctx context.Context, cfg *config.Feed) (*model.Feed, error) {
20+
info, err := ParseURL(cfg.URL)
21+
if err != nil {
22+
return nil, err
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.TypePlaylist {
36+
if soundcloudapi.IsPlaylistURL(cfg.URL) {
37+
scplaylist, err := s.client.GetPlaylistInfo(cfg.URL)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
feed.Title = scplaylist.Title
43+
feed.Description = scplaylist.Description
44+
feed.ItemURL = cfg.URL
45+
46+
date, err := time.Parse(time.RFC3339, scplaylist.CreatedAt)
47+
if err == nil {
48+
feed.PubDate = date
49+
}
50+
feed.Author = scplaylist.User.Username
51+
feed.CoverArt = scplaylist.ArtworkURL
52+
53+
var added = 0
54+
for _, track := range scplaylist.Tracks {
55+
pubDate, _ := time.Parse(time.RFC3339, track.CreatedAt)
56+
var (
57+
videoID = strconv.FormatInt(track.ID, 10)
58+
duration = track.DurationMS / 1000
59+
mediaURL = track.PermalinkURL
60+
trackSize = track.DurationMS * 15 // very rough estimate
61+
)
62+
63+
feed.Episodes = append(feed.Episodes, &model.Episode{
64+
ID: videoID,
65+
Title: track.Title,
66+
Description: track.Description,
67+
Duration: duration,
68+
Size: trackSize,
69+
VideoURL: mediaURL,
70+
PubDate: pubDate,
71+
Thumbnail: track.ArtworkURL,
72+
Status: model.EpisodeNew,
73+
})
74+
75+
added++
76+
77+
if added >= feed.PageSize {
78+
return feed, nil
79+
}
80+
}
81+
82+
return feed, nil
83+
}
84+
}
85+
86+
return nil, errors.New(("unsupported soundcloud feed type"))
87+
}
88+
89+
func NewSoundcloudBuilder() (*SoundCloudBuilder, error) {
90+
sc, err := soundcloudapi.New(soundcloudapi.APIOptions{})
91+
if err != nil {
92+
return nil, errors.Wrap(err, "failed to create soundcloud client")
93+
}
94+
95+
return &SoundCloudBuilder{client: sc}, nil
96+
}

pkg/builder/soundcloud_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package builder
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/mxpv/podsync/pkg/config"
10+
)
11+
12+
func TestSC_BUILDFEED(t *testing.T) {
13+
builder, err := NewSoundcloudBuilder()
14+
require.NoError(t, err)
15+
16+
urls := []string{
17+
"https://soundcloud.com/moby/sets/remixes",
18+
"https://soundcloud.com/npr/sets/soundscapes",
19+
}
20+
21+
for _, addr := range urls {
22+
t.Run(addr, func(t *testing.T) {
23+
feed, err := builder.Build(testCtx, &config.Feed{URL: addr})
24+
require.NoError(t, err)
25+
26+
assert.NotEmpty(t, feed.Title)
27+
assert.NotEmpty(t, feed.Description)
28+
assert.NotEmpty(t, feed.Author)
29+
assert.NotEmpty(t, feed.ItemURL)
30+
31+
assert.NotZero(t, len(feed.Episodes))
32+
33+
for _, item := range feed.Episodes {
34+
assert.NotEmpty(t, item.Title)
35+
assert.NotEmpty(t, item.VideoURL)
36+
assert.NotZero(t, item.Duration)
37+
assert.NotEmpty(t, item.Title)
38+
assert.NotEmpty(t, item.Thumbnail)
39+
}
40+
})
41+
}
42+
}

pkg/builder/url.go

100644100755
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ func ParseURL(link string) (model.Info, error) {
4343
return info, nil
4444
}
4545

46+
if strings.HasSuffix(parsed.Host, "soundcloud.com") {
47+
kind, id, err := parseSoundcloudURL(parsed)
48+
if err != nil {
49+
return model.Info{}, err
50+
}
51+
52+
info.Provider = model.ProviderSoundcloud
53+
info.LinkType = kind
54+
info.ItemID = id
55+
56+
return info, nil
57+
}
58+
4659
return model.Info{}, errors.New("unsupported URL host")
4760
}
4861

@@ -152,3 +165,24 @@ func parseVimeoURL(parsed *url.URL) (model.Type, string, error) {
152165

153166
return "", "", errors.New("unsupported link format")
154167
}
168+
169+
func parseSoundcloudURL(parsed *url.URL) (model.Type, string, error) {
170+
parts := strings.Split(parsed.EscapedPath(), "/")
171+
if len(parts) <= 3 {
172+
return "", "", errors.New("invald soundcloud link path")
173+
}
174+
175+
var kind model.Type
176+
177+
// - https://soundcloud.com/user/sets/example-set
178+
switch parts[2] {
179+
case "sets":
180+
kind = model.TypePlaylist
181+
default:
182+
return "", "", errors.New("invalid soundcloud url, missing sets")
183+
}
184+
185+
id := parts[3]
186+
187+
return kind, id, nil
188+
}

pkg/model/link.go

100644100755
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ const (
1212
type Provider string
1313

1414
const (
15-
ProviderYoutube = Provider("youtube")
16-
ProviderVimeo = Provider("vimeo")
15+
ProviderYoutube = Provider("youtube")
16+
ProviderVimeo = Provider("vimeo")
17+
ProviderSoundcloud = Provider("soundcloud")
1718
)
1819

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

0 commit comments

Comments
 (0)