Skip to content

Commit

Permalink
Add start and end flag
Browse files Browse the repository at this point in the history
  • Loading branch information
jybp committed Apr 25, 2021
1 parent 5bed926 commit dad30cb
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 15 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ https://github.com/jybp/twitch-downloader/releases

## Flags

`-client-id` Use a specific twitch.tv API client ID. Usage is optional.
Note: Using any other client id other than twitch own client id might not work.

`-o` Path where the VOD will be downloaded. Usage is optional.

`-q` Quality of the VOD to download. Omit this flag to print the available qualities.

`-vod` The ID or absolute URL of the twitch VOD to download. https://www.twitch.tv/videos/12345 is the VOD with ID "12345".
|       Flag      | Description |
| --- | --- |
| `-vod` | The ID or absolute URL of the twitch VOD to download. https://www.twitch.tv/videos/12345 is the VOD with ID "12345". |
| `-q` | Quality of the VOD to download. Omit this flag to print the available qualities. |
| `-o` | Path where the VOD will be downloaded. (optional)|
| `-start` | Specify "start" to download a subset of the VOD. Example: 1h23m45s (optional) |
| `-end` | Specify "end" to download a subset of the VOD. Example: 1h34m56s (optional) |
| `-client-id` | Use a specific twitch.tv API client ID. Using any other client id other than twitch own client id might not work. (optional) |

## Build from source

Expand Down
12 changes: 7 additions & 5 deletions cmd/twitchdl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ var defaultClientID string

// Flags
var clientID, vodID, quality, output string
var start, end time.Duration

func init() {
log.SetFlags(0)

flag.StringVar(&clientID, "client-id", "", "Use a specific twitch.tv API client ID. Usage is optional.")
flag.StringVar(&output, "o", "", `Path where the VOD will be downloaded. Usage is optional.`)
flag.StringVar(&vodID, "vod", "", `The ID or absolute URL of the twitch VOD to download. https://www.twitch.tv/videos/12345 is the VOD with ID "12345".`)
flag.StringVar(&quality, "q", "", "Quality of the VOD to download. Omit this flag to print the available qualities.")
flag.StringVar(&vodID, "vod", "", `The ID or absolute URL of the twitch VOD to download.
https://www.twitch.tv/videos/12345 is the VOD with ID "12345".`)
flag.StringVar(&output, "o", "", `Path where the VOD will be downloaded. (optional)`)
flag.DurationVar(&start, "start", time.Duration(0), "Specify \"start\" to download a subset of the VOD. Example: 1h23m45s (optional)")
flag.DurationVar(&end, "end", time.Duration(0), "Specify \"end\" to download a subset of the VOD. Example: 1h34m56s (optional)")
flag.StringVar(&clientID, "client-id", "", "Use a specific twitch.tv API client ID. (optional)")
flag.Parse()
}

Expand Down Expand Up @@ -69,7 +71,7 @@ func main() {
return
}

download, err := twitchdl.Download(context.Background(), http.DefaultClient, defaultClientID, vodID, quality)
download, err := twitchdl.Download(context.Background(), http.DefaultClient, defaultClientID, vodID, quality, start, end)
if err != nil {
log.Fatalf("Retrieving stream for VOD %s failed: %v", vodID, err)
}
Expand Down
48 changes: 46 additions & 2 deletions download.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package twitchdl
import (
"bytes"
"context"
"fmt"
"io"
"math"
"net/http"
"time"

"github.com/jybp/twitch-downloader/m3u8"
"github.com/jybp/twitch-downloader/twitch"
Expand Down Expand Up @@ -34,7 +37,7 @@ func Qualities(ctx context.Context, client *http.Client, clientID, vodID string)
// Download sets up the download of the VOD "vodId" with quality "quality"
// using the provided http.Client.
// The download is actually perfomed when the returned io.Reader is being read.
func Download(ctx context.Context, client *http.Client, clientID, vodID, quality string) (r *Merger, err error) {
func Download(ctx context.Context, client *http.Client, clientID, vodID, quality string, start, end time.Duration) (r *Merger, err error) {
api := twitch.New(client, clientID)
m3u8raw, err := api.M3U8(ctx, vodID)
if err != nil {
Expand Down Expand Up @@ -72,7 +75,11 @@ L:
}

var downloadFns []downloadFunc
for _, segment := range media.Segments {
segments, err := sliceSegments(media.Segments, start, end)
if err != nil {
return nil, err
}
for _, segment := range segments {
req, err := http.NewRequest(http.MethodGet, segment.URL, nil)
if err != nil {
return nil, errors.WithStack(err)
Expand All @@ -83,6 +90,43 @@ L:
return &Merger{downloads: downloadFns}, nil
}

func sliceSegments(segments []m3u8.MediaSegment, start, end time.Duration) ([]m3u8.MediaSegment, error) {
if start < 0 || end < 0 {
return nil, errors.New("Negative timestamps are not allowed")
}
if start >= end && end != time.Duration(0) {
return nil, errors.New("End timestamp is not after Start timestamp")
}
if start == time.Duration(0) && end == time.Duration(0) {
return segments, nil
}
if end == time.Duration(0) {
end = time.Duration(math.MaxInt64)
}
slice := []m3u8.MediaSegment{}
segmentStart := time.Duration(0)
for _, segment := range segments {
segmentEnd := segmentStart + segment.Duration
if segmentEnd <= start {
segmentStart += segment.Duration
continue
}
if segmentStart >= end {
break
}
slice = append(slice, segment)
segmentStart += segment.Duration
}
if len(slice) == 0 {
var dur time.Duration
for _, segment := range segments {
dur += segment.Duration
}
return nil, fmt.Errorf("Timestamps are not a subset of the video (video duration is %v)", dur)
}
return slice, nil
}

// downloadFunc describes a func that peform an HTTP request and returns the response.Body
type downloadFunc func() (io.ReadCloser, error)

Expand Down
94 changes: 94 additions & 0 deletions download_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package twitchdl

import (
"fmt"
"testing"
"time"

"github.com/jybp/twitch-downloader/m3u8"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSliceSegments(t *testing.T) {

segments := []m3u8.MediaSegment{
{Number: 0, Duration: time.Second * 5},
{Number: 1, Duration: time.Second * 5},
{Number: 2, Duration: time.Second * 5},
{Number: 3, Duration: time.Second * 5},
}

tcs := []struct {
start time.Duration
end time.Duration
expected []int
expectedErr bool
}{
{
start: time.Second * 2,
end: time.Second,
expectedErr: true,
},
{
start: time.Second * 8,
end: time.Second * 8,
expectedErr: true,
},
{
start: -time.Second * 2,
end: -time.Second,
expectedErr: true,
},
{
start: time.Second * 20,
end: time.Second * 21,
expectedErr: true,
},
{
start: time.Duration(0),
end: time.Duration(0),
expected: []int{0, 1, 2, 3},
},
{
start: time.Second * 5,
end: time.Duration(0),
expected: []int{1, 2, 3},
},
{
start: time.Duration(0),
end: time.Second * 5,
expected: []int{0},
},
{
start: time.Second * 6,
end: time.Second * 10,
expected: []int{1},
},
{
start: time.Second * 6,
end: time.Second * 11,
expected: []int{1, 2},
},
{
start: time.Second * 6,
end: time.Second * 22,
expected: []int{1, 2, 3},
},
}

for _, tc := range tcs {
t.Run(fmt.Sprintf("start: %v end: %v", tc.start, tc.end), func(t *testing.T) {
actual, err := sliceSegments(segments, tc.start, tc.end)
if tc.expectedErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Len(t, actual, len(tc.expected))
for i := 0; i < len(actual); i++ {
assert.Equal(t, tc.expected[i], actual[i].Number)
}
})
}
}
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ module github.com/jybp/twitch-downloader
go 1.12

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.4.0
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

0 comments on commit dad30cb

Please sign in to comment.