Skip to content
This repository was archived by the owner on Feb 13, 2020. It is now read-only.

Commit 29cb18c

Browse files
committed
api, commands, menu: Use linked partitioning instead of offset-based pagination
Also delete offset flag More here: https://developers.soundcloud.com/blog/offset-pagination-deprecated and here: https://stackoverflow.com/questions/43940103/how-to-get-more-than-350-favorites-with-soundcloud-api Fix #17
1 parent c3eaf6c commit 29cb18c

File tree

9 files changed

+221
-194
lines changed

9 files changed

+221
-194
lines changed

README.md

-4
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ Also commands may be abbreviated to one symbol length. For example, you can inpu
6666

6767
$ nehm get 3
6868

69-
#### Download second like and don't add it to iTunes playlist
70-
71-
$ nehm get -o 1 -i ''
72-
7369
#### Download track from URL
7470

7571
$ nehm get soundcloud.com/nasa/golden-record-russian-greeting

api/api.go

+16-68
Original file line numberDiff line numberDiff line change
@@ -6,62 +6,29 @@ package api
66

77
import (
88
"encoding/json"
9-
"math"
10-
"net/url"
9+
u "net/url"
1110
"strconv"
1211

1312
"github.com/bogem/nehm/logs"
1413
"github.com/bogem/nehm/track"
1514
)
1615

1716
const (
18-
tracksLimit = 150
17+
maxLimit = 200
1918
soundCloudLink = "http://soundcloud.com/"
2019
)
2120

22-
func Favorites(count, offset uint, uid string) ([]track.Track, error) {
23-
requestsCount := float64(count) / float64(tracksLimit)
24-
requestsCount = math.Ceil(requestsCount)
25-
26-
var limit uint
27-
var tracks []track.Track
28-
params := url.Values{}
29-
for i := uint(0); i < uint(requestsCount); i++ {
30-
if count < tracksLimit {
31-
limit = count
32-
} else {
33-
limit = tracksLimit
34-
}
35-
count -= limit
36-
37-
params.Set("limit", strconv.Itoa(int(limit)))
38-
params.Set("offset", strconv.Itoa(int((i*tracksLimit)+offset)))
39-
40-
bFavs, err := get(formFavoritesURI(uid, params))
41-
if err == ErrNotFound {
42-
break
43-
}
44-
if err != nil {
45-
return nil, err
46-
}
47-
48-
var favs []track.Track
49-
if err := json.Unmarshal(bFavs, &favs); err != nil {
50-
logs.FATAL.Fatalln("could't unmarshal JSON with likes:", err)
51-
}
52-
tracks = append(tracks, favs...)
53-
}
54-
return tracks, nil
21+
func Favorites(limit uint, uid string) ([]track.Track, error) {
22+
p := NewPaginator(FormFavoritesURL(limit, uid))
23+
return p.NextPage()
5524
}
5625

5726
func AllFavorites(uid string) ([]track.Track, error) {
58-
offset := 0
27+
p := NewPaginator(FormFavoritesURL(maxLimit, uid))
5928
tracks := make([]track.Track, 0)
6029

61-
// Run loop while len(fav) != 0.
62-
// If len(fav) == 0, then we know, what there are no more favorites by user
63-
for {
64-
favs, err := Favorites(tracksLimit, uint(offset), uid)
30+
for !p.OnLastPage() {
31+
favs, err := p.NextPage()
6532
tracks = append(tracks, favs...)
6633
if err == ErrForbidden {
6734
break
@@ -72,8 +39,6 @@ func AllFavorites(uid string) ([]track.Track, error) {
7239
if len(favs) == 0 {
7340
break
7441
}
75-
76-
offset += tracksLimit
7742
}
7843

7944
return tracks, nil
@@ -84,10 +49,11 @@ type JSONUser struct {
8449
}
8550

8651
func UID(permalink string) string {
87-
params := url.Values{}
52+
params := u.Values{}
8853
params.Set("url", soundCloudLink+permalink)
54+
params.Set("client_id", clientID)
8955

90-
bUser, err := get(formResolveURI(params))
56+
bUser, err := get(formResolveURL(params.Encode()))
9157
if err != nil {
9258
logs.FATAL.Fatalln("there was a problem by resolving an id of user:", err)
9359
}
@@ -100,30 +66,12 @@ func UID(permalink string) string {
10066
return strconv.Itoa(jUser.ID)
10167
}
10268

103-
func Search(query string, limit, offset uint) ([]track.Track, error) {
104-
params := url.Values{}
105-
params.Set("q", query)
106-
params.Set("limit", strconv.Itoa(int(limit)))
107-
params.Set("offset", strconv.Itoa(int(offset)))
108-
109-
bFound, err := get(formSearchURI(params))
110-
if err != nil {
111-
return nil, err
112-
}
113-
114-
var found []track.Track
115-
if err := json.Unmarshal(bFound, &found); err != nil {
116-
logs.FATAL.Fatalln("couldn't unmarshal JSON with search results:", err)
117-
}
118-
119-
return found, nil
120-
}
121-
122-
func TrackFromURI(uri string) []track.Track {
123-
params := url.Values{}
124-
params.Set("url", uri)
69+
func TrackFromURL(url string) []track.Track {
70+
params := u.Values{}
71+
params.Set("url", url)
72+
params.Set("client_id", clientID)
12573

126-
bTrack, err := get(formResolveURI(params))
74+
bTrack, err := get(formResolveURL(params.Encode()))
12775
if err == ErrForbidden {
12876
logs.FATAL.Fatalln("you haven't got any access to this track:", err)
12977
}

api/http_client.go

+34-28
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"errors"
1010
"net/url"
11+
"strconv"
1112

1213
"github.com/bogem/nehm/logs"
1314
"github.com/valyala/fasthttp"
@@ -22,44 +23,49 @@ var (
2223
ErrForbidden = errors.New("403 - you're not allowed to see these tracks")
2324
ErrNotFound = errors.New("404 - there are no tracks")
2425

25-
uriBuffer = new(bytes.Buffer)
26+
urlBuffer = new(bytes.Buffer)
2627
)
2728

28-
func formResolveURI(params url.Values) string {
29-
params.Set("client_id", clientID)
30-
31-
uriBuffer.Reset()
32-
uriBuffer.WriteString(apiURL)
33-
uriBuffer.WriteString("/resolve?")
34-
uriBuffer.WriteString(params.Encode())
35-
return uriBuffer.String()
29+
func formResolveURL(query string) string {
30+
urlBuffer.Reset()
31+
urlBuffer.WriteString(apiURL)
32+
urlBuffer.WriteString("/resolve?")
33+
urlBuffer.WriteString(query)
34+
return urlBuffer.String()
3635
}
3736

38-
func formSearchURI(params url.Values) string {
39-
params.Set("client_id", clientID)
37+
func FormSearchURL(limit uint, query string) string {
38+
params := url.Values{}
39+
params.Set("limit", strconv.Itoa(int(limit)))
40+
params.Set("linked_partitioning", "1")
41+
params.Set("client_id", "11a37feb6ccc034d5975f3f803928a32")
42+
params.Set("q", query)
4043

41-
uriBuffer.Reset()
42-
uriBuffer.WriteString(apiURL)
43-
uriBuffer.WriteString("/tracks?")
44-
uriBuffer.WriteString(params.Encode())
45-
return uriBuffer.String()
44+
urlBuffer.Reset()
45+
urlBuffer.WriteString(apiURL)
46+
urlBuffer.WriteString("/tracks?")
47+
urlBuffer.WriteString(params.Encode())
48+
return urlBuffer.String()
4649
}
4750

48-
func formFavoritesURI(uid string, params url.Values) string {
49-
params.Set("client_id", clientID)
51+
func FormFavoritesURL(limit uint, uid string) string {
52+
params := url.Values{}
53+
params.Set("limit", strconv.Itoa(int(limit)))
54+
params.Set("linked_partitioning", "1")
55+
params.Set("client_id", "11a37feb6ccc034d5975f3f803928a32")
5056

51-
uriBuffer.Reset()
52-
uriBuffer.WriteString(apiURL)
53-
uriBuffer.WriteString("/users/")
54-
uriBuffer.WriteString(uid)
55-
uriBuffer.WriteString("/favorites?")
56-
uriBuffer.WriteString(params.Encode())
57-
return uriBuffer.String()
57+
urlBuffer.Reset()
58+
urlBuffer.WriteString(apiURL)
59+
urlBuffer.WriteString("/users/")
60+
urlBuffer.WriteString(uid)
61+
urlBuffer.WriteString("/favorites?")
62+
urlBuffer.WriteString(params.Encode())
63+
return urlBuffer.String()
5864
}
5965

60-
func get(uri string) ([]byte, error) {
61-
logs.INFO.Println("GET", uri)
62-
statusCode, body, err := fasthttp.Get(nil, uri)
66+
func get(url string) ([]byte, error) {
67+
logs.INFO.Println("GET", url)
68+
statusCode, body, err := fasthttp.Get(nil, url)
6369
if err != nil {
6470
return nil, err
6571
}

api/paginator.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
7+
"github.com/bogem/nehm/track"
8+
)
9+
10+
var (
11+
ErrLastPage = errors.New("current page is last")
12+
ErrFirstPage = errors.New("current page is first")
13+
)
14+
15+
type paginatedResponse struct {
16+
Collection []track.Track `json:"collection"`
17+
NextHref string `json:"next_href"`
18+
}
19+
20+
type Paginator struct {
21+
// currentPage corresponds to index of pages.
22+
// If it's -1, the Paginator was only initialized.
23+
currentPage int
24+
25+
nextHref string
26+
pages []string
27+
}
28+
29+
// NewPaginator returns new paginator.
30+
// It needs only the url to the first page.
31+
// More: https://developers.soundcloud.com/blog/offset-pagination-deprecated.
32+
func NewPaginator(firstPageURL string) *Paginator {
33+
return &Paginator{currentPage: -1, nextHref: firstPageURL}
34+
}
35+
36+
func getPage(url string) (paginatedResponse, error) {
37+
pResponse := paginatedResponse{}
38+
39+
response, err := get(url)
40+
if err != nil {
41+
return pResponse, err
42+
}
43+
44+
err = json.Unmarshal(response, &pResponse)
45+
return pResponse, err
46+
}
47+
48+
// NextPage returns tracks on the next page and error, if it occured.
49+
// If p is on last page, it returns ErrLastPage
50+
func (p *Paginator) NextPage() ([]track.Track, error) {
51+
if p.OnLastPage() {
52+
return nil, ErrLastPage
53+
}
54+
55+
p.currentPage++
56+
57+
// If p.pages already has a link to next page, then use it,
58+
// else append p.nextHref to p.pages.
59+
if len(p.pages) >= p.currentPage+1 {
60+
p.nextHref = p.pages[p.currentPage]
61+
} else if p.nextHref != "" {
62+
p.pages = append(p.pages, p.nextHref)
63+
}
64+
65+
response, err := getPage(p.nextHref)
66+
if err != nil {
67+
return nil, err
68+
}
69+
p.nextHref = response.NextHref
70+
return response.Collection, nil
71+
}
72+
73+
// OnLastPage checks, if current page is last.
74+
func (p Paginator) OnLastPage() bool {
75+
return p.nextHref == ""
76+
}
77+
78+
// PrevPage returns tracks on the previous page and error, if it occured.
79+
// If p is on first page, it returns ErrFirstPage
80+
func (p *Paginator) PrevPage() ([]track.Track, error) {
81+
if p.OnFirstPage() {
82+
return nil, ErrFirstPage
83+
}
84+
85+
p.currentPage--
86+
87+
var url string
88+
if len(p.pages) >= p.currentPage-1 {
89+
url = p.pages[p.currentPage]
90+
} else {
91+
// This should never happen! :)
92+
return nil, ErrFirstPage
93+
}
94+
95+
response, err := getPage(url)
96+
if err != nil {
97+
return nil, err
98+
}
99+
return response.Collection, nil
100+
}
101+
102+
// OnFirstPage checks, if current page is first.
103+
func (p Paginator) OnFirstPage() bool {
104+
return p.currentPage <= 0
105+
}

commands/commands.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ var rootCmd = listCommand
2121

2222
// Variables used in flags.
2323
var (
24-
limit, offset uint
24+
limit uint
2525
dlFolder, itunesPlaylist, permalink string
2626
verbose bool
2727
)
@@ -48,10 +48,6 @@ func addLimitFlag(cmd *cobra.Command) {
4848
cmd.Flags().UintVarP(&limit, "limit", "l", 9, "count of tracks on each page")
4949
}
5050

51-
func addOffsetFlag(cmd *cobra.Command) {
52-
cmd.Flags().UintVarP(&offset, "offset", "o", 0, "offset relative to first like")
53-
}
54-
5551
func addPermalinkFlag(cmd *cobra.Command) {
5652
cmd.Flags().StringVarP(&permalink, "permalink", "p", "", "user's permalink")
5753
}

commands/get.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ var (
2828
func init() {
2929
addDlFolderFlag(getCommand)
3030
addItunesPlaylistFlag(getCommand)
31-
addOffsetFlag(getCommand)
3231
addPermalinkFlag(getCommand)
3332
}
3433

@@ -59,14 +58,13 @@ func getTracks(cmd *cobra.Command, args []string) {
5958

6059
func getLastTracks(count uint) ([]track.Track, error) {
6160
logs.FEEDBACK.Println("Getting ID of user")
62-
uid := api.UID(config.Get("permalink"))
63-
return api.Favorites(count, offset, uid)
61+
return api.Favorites(count, api.UID(config.Get("permalink")))
6462
}
6563

6664
func isSoundCloudURL(url string) bool {
6765
return strings.Contains(url, "soundcloud.com")
6866
}
6967

7068
func getTrackFromURL(url string) []track.Track {
71-
return api.TrackFromURI(url)
69+
return api.TrackFromURL(url)
7270
}

0 commit comments

Comments
 (0)