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:
committed by
Maksym Pavlenko
parent
da93686a57
commit
c1130f3f97
1
go.mod
1
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
@@ -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
96
pkg/builder/soundcloud.go
Normal 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
|
||||
}
|
42
pkg/builder/soundcloud_test.go
Normal file
42
pkg/builder/soundcloud_test.go
Normal 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
34
pkg/builder/url.go
Normal file → Executable 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
7
pkg/model/link.go
Normal file → Executable 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
|
||||
}
|
||||
|
Reference in New Issue
Block a user