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/silentsokolov/go-vimeo v0.0.0-20190116124215-06829264260c
|
||||||
github.com/sirupsen/logrus v1.2.0
|
github.com/sirupsen/logrus v1.2.0
|
||||||
github.com/stretchr/testify v1.4.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/net v0.0.0-20190620200207-3b0461eec859
|
||||||
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd
|
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
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/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 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
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 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
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/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/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/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 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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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)
|
return NewYouTubeBuilder(key)
|
||||||
case model.ProviderVimeo:
|
case model.ProviderVimeo:
|
||||||
return NewVimeoBuilder(ctx, key)
|
return NewVimeoBuilder(ctx, key)
|
||||||
|
case model.ProviderSoundcloud:
|
||||||
|
return NewSoundcloudBuilder()
|
||||||
default:
|
default:
|
||||||
return nil, errors.Errorf("unsupported provider %q", provider)
|
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
|
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")
|
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")
|
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
|
type Provider string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ProviderYoutube = Provider("youtube")
|
ProviderYoutube = Provider("youtube")
|
||||||
ProviderVimeo = Provider("vimeo")
|
ProviderVimeo = Provider("vimeo")
|
||||||
|
ProviderSoundcloud = Provider("soundcloud")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Info represents data extracted from URL
|
// Info represents data extracted from URL
|
||||||
type Info struct {
|
type Info struct {
|
||||||
LinkType Type // Either group, channel or user
|
LinkType Type // Either group, channel or user
|
||||||
Provider Provider // Youtube or Vimeo
|
Provider Provider // Youtube, Vimeo, or SoundCloud
|
||||||
ItemID string
|
ItemID string
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user