1
0
mirror of https://github.com/mxpv/podsync.git synced 2024-05-11 05:55:04 +00:00

Initial SoundCloud playlist support

This commit is contained in:
Unshuffled
2021-10-02 21:32:46 -04:00
committed by Maksym Pavlenko
parent da93686a57
commit c1130f3f97
7 changed files with 183 additions and 3 deletions

1
go.mod
View File

@@ -16,6 +16,7 @@ require (
github.com/silentsokolov/go-vimeo v0.0.0-20190116124215-06829264260c
github.com/sirupsen/logrus v1.2.0
github.com/stretchr/testify v1.4.0
github.com/zackradisic/soundcloud-api v0.1.5
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd
golang.org/x/sync v0.0.0-20190423024810-112230192c58

4
go.sum
View File

@@ -27,6 +27,8 @@ github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
@@ -76,6 +78,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zackradisic/soundcloud-api v0.1.5 h1:OVg8XlbNjrSpSAJhIdBDjUC/Vm1lQYPrDOhnNnImNGg=
github.com/zackradisic/soundcloud-api v0.1.5/go.mod h1:ycGIZFVZdUVC7B8pcfgze1bRBePPmjYlIGnRptKByQ0=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

View File

@@ -19,6 +19,8 @@ func New(ctx context.Context, provider model.Provider, key string) (Builder, err
return NewYouTubeBuilder(key)
case model.ProviderVimeo:
return NewVimeoBuilder(ctx, key)
case model.ProviderSoundcloud:
return NewSoundcloudBuilder()
default:
return nil, errors.Errorf("unsupported provider %q", provider)
}

96
pkg/builder/soundcloud.go Normal file
View File

@@ -0,0 +1,96 @@
package builder
import (
"strconv"
"time"
"github.com/pkg/errors"
soundcloudapi "github.com/zackradisic/soundcloud-api"
"golang.org/x/net/context"
"github.com/mxpv/podsync/pkg/config"
"github.com/mxpv/podsync/pkg/model"
)
type SoundCloudBuilder struct {
client *soundcloudapi.API
}
func (s *SoundCloudBuilder) Build(ctx context.Context, cfg *config.Feed) (*model.Feed, error) {
info, err := ParseURL(cfg.URL)
if err != nil {
return nil, err
}
feed := &model.Feed{
ItemID: info.ItemID,
Provider: info.Provider,
LinkType: info.LinkType,
Format: cfg.Format,
Quality: cfg.Quality,
PageSize: cfg.PageSize,
UpdatedAt: time.Now().UTC(),
}
if info.LinkType == model.TypePlaylist {
if soundcloudapi.IsPlaylistURL(cfg.URL) {
scplaylist, err := s.client.GetPlaylistInfo(cfg.URL)
if err != nil {
return nil, err
}
feed.Title = scplaylist.Title
feed.Description = scplaylist.Description
feed.ItemURL = cfg.URL
date, err := time.Parse(time.RFC3339, scplaylist.CreatedAt)
if err == nil {
feed.PubDate = date
}
feed.Author = scplaylist.User.Username
feed.CoverArt = scplaylist.ArtworkURL
var added = 0
for _, track := range scplaylist.Tracks {
pubDate, _ := time.Parse(time.RFC3339, track.CreatedAt)
var (
videoID = strconv.FormatInt(track.ID, 10)
duration = track.DurationMS / 1000
mediaURL = track.PermalinkURL
trackSize = track.DurationMS * 15 // very rough estimate
)
feed.Episodes = append(feed.Episodes, &model.Episode{
ID: videoID,
Title: track.Title,
Description: track.Description,
Duration: duration,
Size: trackSize,
VideoURL: mediaURL,
PubDate: pubDate,
Thumbnail: track.ArtworkURL,
Status: model.EpisodeNew,
})
added++
if added >= feed.PageSize {
return feed, nil
}
}
return feed, nil
}
}
return nil, errors.New(("unsupported soundcloud feed type"))
}
func NewSoundcloudBuilder() (*SoundCloudBuilder, error) {
sc, err := soundcloudapi.New(soundcloudapi.APIOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to create soundcloud client")
}
return &SoundCloudBuilder{client: sc}, nil
}

View File

@@ -0,0 +1,42 @@
package builder
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mxpv/podsync/pkg/config"
)
func TestSC_BUILDFEED(t *testing.T) {
builder, err := NewSoundcloudBuilder()
require.NoError(t, err)
urls := []string{
"https://soundcloud.com/moby/sets/remixes",
"https://soundcloud.com/npr/sets/soundscapes",
}
for _, addr := range urls {
t.Run(addr, func(t *testing.T) {
feed, err := builder.Build(testCtx, &config.Feed{URL: addr})
require.NoError(t, err)
assert.NotEmpty(t, feed.Title)
assert.NotEmpty(t, feed.Description)
assert.NotEmpty(t, feed.Author)
assert.NotEmpty(t, feed.ItemURL)
assert.NotZero(t, len(feed.Episodes))
for _, item := range feed.Episodes {
assert.NotEmpty(t, item.Title)
assert.NotEmpty(t, item.VideoURL)
assert.NotZero(t, item.Duration)
assert.NotEmpty(t, item.Title)
assert.NotEmpty(t, item.Thumbnail)
}
})
}
}

34
pkg/builder/url.go Normal file → Executable file
View File

@@ -43,6 +43,19 @@ func ParseURL(link string) (model.Info, error) {
return info, nil
}
if strings.HasSuffix(parsed.Host, "soundcloud.com") {
kind, id, err := parseSoundcloudURL(parsed)
if err != nil {
return model.Info{}, err
}
info.Provider = model.ProviderSoundcloud
info.LinkType = kind
info.ItemID = id
return info, nil
}
return model.Info{}, errors.New("unsupported URL host")
}
@@ -152,3 +165,24 @@ func parseVimeoURL(parsed *url.URL) (model.Type, string, error) {
return "", "", errors.New("unsupported link format")
}
func parseSoundcloudURL(parsed *url.URL) (model.Type, string, error) {
parts := strings.Split(parsed.EscapedPath(), "/")
if len(parts) <= 3 {
return "", "", errors.New("invald soundcloud link path")
}
var kind model.Type
// - https://soundcloud.com/user/sets/example-set
switch parts[2] {
case "sets":
kind = model.TypePlaylist
default:
return "", "", errors.New("invalid soundcloud url, missing sets")
}
id := parts[3]
return kind, id, nil
}

7
pkg/model/link.go Normal file → Executable file
View File

@@ -12,13 +12,14 @@ const (
type Provider string
const (
ProviderYoutube = Provider("youtube")
ProviderVimeo = Provider("vimeo")
ProviderYoutube = Provider("youtube")
ProviderVimeo = Provider("vimeo")
ProviderSoundcloud = Provider("soundcloud")
)
// Info represents data extracted from URL
type Info struct {
LinkType Type // Either group, channel or user
Provider Provider // Youtube or Vimeo
Provider Provider // Youtube, Vimeo, or SoundCloud
ItemID string
}