Skip to content

Commit 815dd2d

Browse files
authored
implemented ranges and skip for download episode (#21)
* implemented ranges and skip for download episode closes #20 * added return when there's no episode id provided * prevent duplicates from being returned in the parseRangeArg function
1 parent c814019 commit 815dd2d

File tree

3 files changed

+179
-8
lines changed

3 files changed

+179
-8
lines changed

cmd/download.go

+134-8
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616
package cmd
1717

1818
import (
19+
"fmt"
1920
"log"
2021
"net/http"
22+
"regexp"
23+
"sort"
2124
"strconv"
25+
"strings"
2226

2327
"github.com/cheggaaa/pb"
28+
"github.com/kvannotten/pcd"
2429
"github.com/spf13/cobra"
2530
)
2631

@@ -29,11 +34,37 @@ var downloadCmd = &cobra.Command{
2934
Use: "download <podcast> <episode_id>",
3035
Aliases: []string{"d"},
3136
Short: "Downloads an episode of a podcast.",
32-
Long: `This command will download an episode of a podcast that you define. The episode number can
33-
be obtained by running 'pcd ls <podcast>' For example:
37+
Long: `
38+
This command will download one or multiple episode(s) of a podcast that you
39+
define.
40+
41+
The episode number can be obtained by running 'pcd ls <podcast>'
42+
43+
For example:
44+
45+
To download one episode
3446
3547
pcd ls gnu_open_world
36-
pcd download gnu_open_world 1`,
48+
pcd download gnu_open_world 1
49+
50+
To download episode ranges:
51+
52+
pcd download gnu_open_world '20-30,!25'
53+
54+
This will download episode 20 to 30 and skip the 25.
55+
56+
Available formats:
57+
58+
Episode numbers: '1,5,105'
59+
Ranges: '2-15'
60+
Skipping: '!102,!121'
61+
62+
Combining those as follow:
63+
64+
pcd download gnu_open_world '1-30,40-47,!15,!17,!20,102'
65+
66+
Make sure to use the single-quote on bash otherwise the !105 will expand your
67+
bash history.`,
3768
Args: cobra.MinimumNArgs(1),
3869
Run: download,
3970
}
@@ -51,16 +82,23 @@ func download(cmd *cobra.Command, args []string) {
5182
log.Fatalf("Could not load podcast: %#v", err)
5283
}
5384

54-
var episodeN int
55-
if len(args) > 1 {
56-
episodeN, err = strconv.Atoi(args[1])
57-
} else {
58-
episodeN = len(podcast.Episodes) // download latest
85+
if len(args) < 2 {
86+
// download latest
87+
downloadEpisode(podcast, len(podcast.Episodes))
88+
return
5989
}
90+
91+
episodes, err := parseRangeArg(args[1])
6092
if err != nil {
6193
log.Fatalf("Could not parse episode number %s: %#v", args[1], err)
6294
}
6395

96+
for _, n := range episodes {
97+
downloadEpisode(podcast, n)
98+
}
99+
}
100+
101+
func downloadEpisode(podcast *pcd.Podcast, episodeN int) {
64102
if episodeN > len(podcast.Episodes) {
65103
log.Fatalf("There's only %d episodes in this podcast.", len(podcast.Episodes))
66104
}
@@ -110,3 +148,91 @@ func init() {
110148
// is called directly, e.g.:
111149
// downloadCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
112150
}
151+
152+
// parseRangeArg parses episodes number with the following format
153+
// 1,2,3-5,!4 returns [1, 2, 3, 5]
154+
func parseRangeArg(arg string) ([]int, error) {
155+
if len(arg) == 0 {
156+
return nil, nil
157+
}
158+
159+
// we try to convert the arg as a single episode
160+
n, err := strconv.Atoi(arg)
161+
if err == nil {
162+
return []int{n}, nil
163+
}
164+
165+
// this map helps preventing duplicate
166+
unique := make(map[int]bool)
167+
168+
// extract negative numbers !X
169+
negatives := regexp.MustCompile(`!\d+`)
170+
notWanted := negatives.FindAllString(arg, -1)
171+
172+
arg = negatives.ReplaceAllString(arg, "")
173+
174+
// extract ranges X-Y
175+
rangesPattern := regexp.MustCompile(`\d+-\d+`)
176+
ranges := rangesPattern.FindAllString(arg, -1)
177+
178+
arg = rangesPattern.ReplaceAllString(arg, "")
179+
180+
// extract the remaining single digit X
181+
digitsPattern := regexp.MustCompile(`\d+`)
182+
digits := digitsPattern.FindAllString(arg, -1)
183+
184+
for _, r := range ranges {
185+
parts := strings.Split(r, "-")
186+
if len(parts) != 2 {
187+
return nil, fmt.Errorf("range %s must have the format start-end", r)
188+
}
189+
190+
start, err := strconv.Atoi(parts[0])
191+
if err != nil {
192+
return nil, err
193+
}
194+
195+
end, err := strconv.Atoi(parts[1])
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
for i := start; i <= end; i++ {
201+
// make sure it's wanted
202+
wanted := true
203+
for _, nw := range notWanted {
204+
if fmt.Sprintf("!%d", i) == nw {
205+
wanted = false
206+
break
207+
}
208+
}
209+
210+
if !wanted {
211+
continue
212+
}
213+
214+
unique[i] = true
215+
}
216+
}
217+
218+
// let's add the remaining digits
219+
for _, d := range digits {
220+
i, err := strconv.Atoi(d)
221+
if err != nil {
222+
return nil, err
223+
}
224+
225+
unique[i] = true
226+
}
227+
228+
// we turn the unique map into the slice of episode numbers
229+
var results []int
230+
for k, _ := range unique {
231+
results = append(results, k)
232+
}
233+
234+
// we sort the result
235+
sort.Ints(results)
236+
237+
return results, nil
238+
}

cmd/download_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright © 2018 Kristof Vannotten <[email protected]>
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
package cmd
17+
18+
import (
19+
"reflect"
20+
"testing"
21+
)
22+
23+
func TestEpisodeRangeArgs(t *testing.T) {
24+
cases := make(map[string][]int)
25+
cases["1"] = []int{1}
26+
cases["2-5"] = []int{2, 3, 4, 5}
27+
cases["2-5,!4"] = []int{2, 3, 5}
28+
cases["1,2,!99,!102,97-105,!103"] = []int{1, 2, 97, 98, 100, 101, 104, 105}
29+
cases["!25,25-27,!26"] = []int{27}
30+
cases["22,12,10-13,!11,!22,!12"] = []int{10, 12, 13, 22}
31+
cases["101-106,7,!105,!104"] = []int{7, 101, 102, 103, 106}
32+
cases["1-5,!3,4-6"] = []int{1, 2, 4, 5, 6}
33+
cases[""] = nil
34+
35+
for arg, want := range cases {
36+
got, err := parseRangeArg(arg)
37+
if err != nil {
38+
t.Error(err)
39+
} else if reflect.DeepEqual(want, got) == false {
40+
t.Errorf("missmatch for %s: got %v want %v", arg, got, want)
41+
}
42+
}
43+
}

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module github.com/kvannotten/pcd
22

3+
go 1.15
4+
35
require (
46
github.com/BurntSushi/toml v0.3.0 // indirect
57
github.com/cheggaaa/pb v1.0.25

0 commit comments

Comments
 (0)