From 614b49263c8a47ca0f26f308ad0ef27426f8c741 Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 08:43:19 -0500 Subject: [PATCH 01/10] Update stdout related to cleaning up temp files --- tvd.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tvd.go b/tvd.go index f0c5526..6aae7d9 100644 --- a/tvd.go +++ b/tvd.go @@ -205,9 +205,10 @@ func DownloadVOD(cfg Config) error { return err } defer func() { + fmt.Println("Cleaning up temp files") err = os.RemoveAll(tempDir) if err != nil { - fmt.Printf("Failed to remove dir <%s>\n", tempDir) + fmt.Printf("Failed to remove tempdir <%s>\n", tempDir) log.Fatalln(err) } }() From 0ec1d3e31fff09b2a7cb5d9f55b7190d51bcb4a5 Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 11:10:35 -0500 Subject: [PATCH 02/10] Add new structs and helper to get auth token/sig from GQL API --- structs.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/structs.go b/structs.go index a90224a..b2076fb 100644 --- a/structs.go +++ b/structs.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "log" "net/url" "path/filepath" @@ -57,3 +58,40 @@ type Chunk struct { URL *url.URL Path string } + +// AuthGQLPayload represents the payload sent to the GQL endpoint to get the +// auth token and signature +type AuthGQLPayload struct { + OperationName string `json:"operationName"` + Query string `json:"query"` + Variables struct { + IsLive bool `json:"isLive"` + IsVod bool `json:"isVod"` + Login string `json:"login"` + PlayerType string `json:"playerType"` + VodID string `json:"vodID"` + } `json:"variables"` +} + +func generateAuthPayload(vodID string) ([]byte, error) { + ap := AuthGQLPayload{ + OperationName: "PlaybackAccessToken_Template", + Query: "query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}", + } + ap.Variables.IsLive = false + ap.Variables.IsVod = true + ap.Variables.PlayerType = "site" + ap.Variables.VodID = vodID + return json.Marshal(ap) +} + +// AuthGQLPayload represents the response from to the GQL endpoint containing +// the auth token and signature +type AuthGQLResponse struct { + Data struct { + VideoPlaybackAccessToken struct { + Value string `json:"value"` + Signature string `json:"signature"` + } `json:"videoPlaybackAccessToken"` + } `json:"data"` +} From 301c6fa637ba02e92d970b9b7ecf8451efd8ca11 Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 11:11:13 -0500 Subject: [PATCH 03/10] Add log message for 'final' config object --- tvd.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tvd.go b/tvd.go index 6aae7d9..91d4ba5 100644 --- a/tvd.go +++ b/tvd.go @@ -135,6 +135,7 @@ func main() { fmt.Println(err) log.Fatalln(err) } + log.Printf("final config: %+v\n", config.Privatize()) // go get it! err = DownloadVOD(config) From f8db4e1c6a18301022e06e4e587887178f2379fe Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 11:15:14 -0500 Subject: [PATCH 04/10] Update sig/token and stream options requests to use newer API endpoints The original endpoint used to get the sig/token values appears to be completely dead now so switching to the newer GQL API endpoint to query for access details. Additionally, use osrtss/rtss to parse the m3u8 files rather than regex. --- go.mod | 1 + go.sum | 2 + tvd.go | 153 +++++++++++++++++++++++++++++++++++++++------------------ 3 files changed, 107 insertions(+), 49 deletions(-) diff --git a/go.mod b/go.mod index d5bbf5b..a15f42b 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/BurntSushi/toml v0.3.0 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect + github.com/osrtss/rtss v0.0.0-20170322072109-b1617c76f6da github.com/pkg/errors v0.8.0 github.com/schollz/progressbar/v3 v3.7.3 github.com/stretchr/testify v1.6.1 // indirect diff --git a/go.sum b/go.sum index c32f1cf..46b89cc 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/osrtss/rtss v0.0.0-20170322072109-b1617c76f6da h1:/Z/TuENqR/F8BVs0WUpAIegWgfKY8EoP93i3kRMapRQ= +github.com/osrtss/rtss v0.0.0-20170322072109-b1617c76f6da/go.mod h1:HP7OZkbc9jh3kEc5zfljE3gG++ejTIbq3HZRui5n+mo= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/tvd.go b/tvd.go index 91d4ba5..2689e7b 100644 --- a/tvd.go +++ b/tvd.go @@ -3,6 +3,7 @@ package main // Based on https://github.com/ArneVogel/concat import ( + "bytes" "encoding/json" "fmt" "io" @@ -12,11 +13,11 @@ import ( "net/url" "os" "path/filepath" - "regexp" "strconv" "strings" "github.com/BurntSushi/toml" + "github.com/osrtss/rtss/m3u8" "github.com/schollz/progressbar/v3" "gopkg.in/alecthomas/kingpin.v2" ) @@ -166,13 +167,13 @@ func createDefaultConfigFile() error { // DownloadVOD downloads a VOD based on the various info passed in the config func DownloadVOD(cfg Config) error { fmt.Println("Fetching access token") - atr, err := getAuthToken(cfg.VodID, cfg.AuthToken) + ar, err := getAuthToken(cfg.VodID, cfg.ClientID) if err != nil { return err } fmt.Println("Fetching VOD stream options") - ql, err := getStreamOptions(cfg.VodID, atr) + ql, err := getStreamOptions(cfg.VodID, ar) if err != nil { return err } @@ -229,54 +230,95 @@ func DownloadVOD(cfg Config) error { return nil } -func getAuthToken(vodID int, authToken string) (AuthTokenResponse, error) { +func getAuthToken(vodID int, clientID string) (AuthGQLResponse, error) { log.Printf("[getAuthToken] vodID=%d\n", vodID) - var atr AuthTokenResponse - url := fmt.Sprintf("https://api.twitch.tv/api/vods/%d/access_token?oauth_token=%s", vodID, authToken) - respData, err := readURL(url) + var ar AuthGQLResponse + + ap, err := generateAuthPayload(strconv.Itoa(vodID)) if err != nil { - return atr, err + return ar, err } - err = json.Unmarshal(respData, &atr) + url := "https://gql.twitch.tv/gql" + req, err := http.NewRequest("POST", url, bytes.NewBuffer(ap)) if err != nil { - return atr, err + return ar, err } - if len(atr.Sig) == 0 || len(atr.Token) == 0 { - return atr, fmt.Errorf("error: sig and/or token were empty: %+v", atr) + req.Header.Set("Client-ID", clientID) + req.Header.Set("Content-Type", "text/plain; charset=UTF-8") + + rsp, err := http.DefaultClient.Do(req) + if err != nil { + return ar, err } + defer func() { + err = rsp.Body.Close() + if err != nil { + fmt.Printf("error closing URL body for <%s>: %s", url, err.Error()) + log.Println(err) + } + }() - log.Printf("access token: %+v\n", atr) + rspData, err := ioutil.ReadAll(rsp.Body) + if err != nil { + return ar, err + } + + err = json.Unmarshal(rspData, &ar) + if err != nil { + return ar, err + } + if len(ar.Data.VideoPlaybackAccessToken.Signature) == 0 || len(ar.Data.VideoPlaybackAccessToken.Value) == 0 { + log.Printf("response: %s\n", rspData) + return ar, fmt.Errorf("error: sig and/or token were empty: %+v", ar) + } - return atr, nil + log.Printf("access token: %+v\n", ar) + + return ar, nil } -func getStreamOptions(vodID int, atr AuthTokenResponse) (map[string]string, error) { - log.Printf("[getAuthToken] vodID=%d, atr=%+v\n", vodID, atr) +func getStreamOptions(vodID int, ar AuthGQLResponse) (map[string]string, error) { + log.Printf("[getStreamOptions] vodID=%d, ar=%+v\n", vodID, ar) var ql = make(map[string]string) - url := fmt.Sprintf("http://usher.twitch.tv/vod/%d?nauthsig=%s&nauth=%s&allow_source=true", vodID, atr.Sig, atr.Token) - respData, err := readURL(url) + url := fmt.Sprintf( + "https://usher.ttvnw.net/vod/%d.m3u8?allow_source=true&sig=%s&token=%s", + vodID, + ar.Data.VideoPlaybackAccessToken.Signature, + ar.Data.VideoPlaybackAccessToken.Value, + ) + rsp, err := http.Get(url) if err != nil { return nil, err } + defer func() { + err = rsp.Body.Close() + if err != nil { + fmt.Printf("error closing URL body for <%s>: %s", url, err.Error()) + log.Println(err) + } + }() - re := regexp.MustCompile(`BANDWIDTH=(\d+),.*?VIDEO="(.*?)"\n(.*?)\n`) - matches := re.FindAllStringSubmatch(string(respData), -1) - if len(matches) == 0 { - log.Printf("response for m3u:\n%s\n", respData) - return nil, fmt.Errorf("error: no matches found") + p, listType, err := m3u8.DecodeFrom(rsp.Body, true) + if err != nil { + return nil, err } - bestBandwidth := 0 - for _, match := range matches { - ql[match[2]] = match[3] - // "safe" to ignore error as regex only matches digits for this capture grouop - bandwidth, _ := strconv.Atoi(match[1]) - if bandwidth > bestBandwidth { - bestBandwidth = bandwidth - ql["best"] = match[3] + switch listType { + case m3u8.MASTER: + masterPl := p.(*m3u8.MasterPlaylist) + var bestBandwidth uint32 + for _, v := range masterPl.Variants { + ql[v.Resolution] = v.URI + if v.Bandwidth > bestBandwidth { + bestBandwidth = v.Bandwidth + ql["best"] = v.URI + } + } + default: + return nil, fmt.Errorf("m3u8 playlist was not the expected 'master' format") } log.Printf("qualities options found: %+v\n", ql) @@ -286,31 +328,44 @@ func getStreamOptions(vodID int, atr AuthTokenResponse) (map[string]string, erro func getChunks(streamURL string) ([]Chunk, int, error) { var chunks []Chunk + var chunkDur int - respData, err := readURL(streamURL) + rsp, err := http.Get(streamURL) if err != nil { return nil, 0, err } + defer func() { + err = rsp.Body.Close() + if err != nil { + fmt.Printf("error closing URL body for <%s>: %s", streamURL, err.Error()) + log.Println(err) + } + }() - re := regexp.MustCompile(`#EXTINF:(\d+\.\d+),\n(.*?)\n`) - matches := re.FindAllStringSubmatch(string(respData), -1) - - // "safe" to ignore because we already fetched it - baseURL, _ := url.Parse(streamURL) - for _, match := range matches { - // "safe" to ignore error due to regex capture group - length, _ := strconv.ParseFloat(match[1], 64) - // "safe" to ignore error ... due to capture group? - chunkURL, _ := url.Parse(match[2]) - chunkURL = baseURL.ResolveReference(chunkURL) - chunks = append(chunks, Chunk{Name: match[2], Length: length, URL: chunkURL}) + p, listType, err := m3u8.DecodeFrom(rsp.Body, true) + if err != nil { + return nil, 0, err } - log.Printf("found %d chunks", len(chunks)) - re = regexp.MustCompile(`#EXT-X-TARGETDURATION:(\d+)\n`) - match := re.FindStringSubmatch(string(respData)) - chunkDur, _ := strconv.Atoi(match[1]) - log.Printf("target chunk duration: %d", chunkDur) + switch listType { + case m3u8.MEDIA: + mediaPl := p.(*m3u8.MediaPlaylist) + + chunkDur = int(mediaPl.TargetDuration) + log.Printf("target chunk duration: %d", chunkDur) + + // "safe" to ignore - previously fetched + baseURL, _ := url.Parse(streamURL) + for i := 0; i < int(mediaPl.Count()); i++ { + // "safe" to ignore - per format spec + s := mediaPl.Segments[i] + chunkPath, _ := url.Parse(s.URI) + chunkURL := baseURL.ResolveReference(chunkPath) + chunks = append(chunks, Chunk{Name: s.URI, Length: s.Duration, URL: chunkURL}) + } + default: + return nil, 0, fmt.Errorf("m3u8 playlist was not the expected 'media' format") + } return chunks, chunkDur, nil } From 7124aaf8ec42f9209d214de11e20f3d86c91915e Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 11:15:33 -0500 Subject: [PATCH 05/10] Removed no longer used AuthTokenResponse struct and readURL func --- structs.go | 6 ------ tvd.go | 22 ---------------------- 2 files changed, 28 deletions(-) diff --git a/structs.go b/structs.go index b2076fb..4cfae1f 100644 --- a/structs.go +++ b/structs.go @@ -45,12 +45,6 @@ func isValidFilename(fn string) bool { return true } -// AuthTokenResponse represents the (happy) JSON response to a token request call -type AuthTokenResponse struct { - Sig string `json:"sig"` - Token string `json:"token"` -} - // Chunk represents a video chunk from the m3u type Chunk struct { Name string diff --git a/tvd.go b/tvd.go index 2689e7b..ff45ea7 100644 --- a/tvd.go +++ b/tvd.go @@ -574,25 +574,3 @@ func secondsToTimeMask(s int) string { log.Printf("masked %d seconds as '%s'\n", s, res) return res } - -func readURL(url string) ([]byte, error) { - log.Printf("requesting URL: %s\n", url) - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer func() { - err = resp.Body.Close() - if err != nil { - fmt.Printf("error closing URL body for <%s>: %s", url, err.Error()) - log.Println(err) - } - }() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - return body, nil -} From 27b56152cfc65c4187e0557ba2317987a7a59cc0 Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 11:27:21 -0500 Subject: [PATCH 06/10] Update README regarding Client-ID requirements --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c2de0c..dddde4b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ tvd (**T**witch **V**OD **D**ownloader) is a command-line tool to download VODs ## Prerequisites -* If building from source, you must register a new app on the Twitch Dev site to get your own client ID +* If building from source, you must have a client ID with appropriate privileges to query the GQL API for VODs * Provided releases have an embedded client ID * You must have an active auth token from an account sign-in * In a browser, sign into your account and get the value of the `auth-token` cookie From 8c302a7984b4f30a17a03b6ba77078f979eace50 Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 11:27:49 -0500 Subject: [PATCH 07/10] Remove no-longer-necessary AuthToken --- README.md | 4 ---- config-sample.toml | 1 - config.go | 11 ----------- tvd.go | 2 -- 4 files changed, 18 deletions(-) diff --git a/README.md b/README.md index dddde4b..69a5c81 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ tvd (**T**witch **V**OD **D**ownloader) is a command-line tool to download VODs * If building from source, you must have a client ID with appropriate privileges to query the GQL API for VODs * Provided releases have an embedded client ID -* You must have an active auth token from an account sign-in - * In a browser, sign into your account and get the value of the `auth-token` cookie ## Download @@ -43,7 +41,6 @@ Using a config file is alternative to command-line arguments. It can be used in The accepted values are: -* `AuthToken` - your login session’s auth token (stored in `auth-token` cookies) * `ClientID` - your Twitch app’s client ID * `Quality` (optional) - desired quality (e.g. “720p60”, “480p30”); can use “best” for best available (default: "best") * `StartTime` – start time in the format "HOURS MINUTES SECONDS" (e.g. "1 24 35" is 1h24m35s) @@ -59,7 +56,6 @@ The accepted values are: All options supported above are also supported through the command-line under the following flags: -* `auth` => `AuthToken` * `client` => `ClientID` * `quality` => `Quality` * `start` => `StartTime` diff --git a/config-sample.toml b/config-sample.toml index b4f080b..d956d63 100644 --- a/config-sample.toml +++ b/config-sample.toml @@ -1,4 +1,3 @@ -AuthToken = "" ClientID="" Quality="best" StartTime="0 0 0" diff --git a/config.go b/config.go index e5f8280..bb369f6 100644 --- a/config.go +++ b/config.go @@ -12,7 +12,6 @@ import ( // Config represents a config object containing everything needed to download a VOD type Config struct { - AuthToken string ClientID string Quality string StartTime string @@ -29,16 +28,12 @@ type Config struct { // Privatize returns a copy of the struct with the ClientID field censored (e.g. for logging) func (c Config) Privatize() Config { c2 := c - c2.AuthToken = "********" c2.ClientID = "********" return c2 } // Update replaces any config values in the base object with those present in the passed argument func (c *Config) Update(c2 Config) { - if c2.AuthToken != "" { - c.AuthToken = c2.AuthToken - } if c2.ClientID != "" { c.ClientID = c2.ClientID } @@ -73,9 +68,6 @@ func (c *Config) Update(c2 Config) { // // Currently, "OutputFolder" is not validated (needs logic to support Windows paths) func (c Config) Validate() error { - if len(c.AuthToken) == 0 { - return fmt.Errorf("error: AuthToken missing") - } if len(c.ClientID) == 0 { return fmt.Errorf("error: ClientID missing") } @@ -171,9 +163,6 @@ func loadConfig(f string) (Config, error) { func buildConfigFromFlags() (Config, error) { var config Config - if *authToken != "" { - config.AuthToken = *authToken - } if *clientID != "" { config.ClientID = *clientID } diff --git a/tvd.go b/tvd.go index ff45ea7..8e3baa5 100644 --- a/tvd.go +++ b/tvd.go @@ -32,7 +32,6 @@ var ( date = "n/a" DefaultConfig = Config{ - AuthToken: "", ClientID: ClientID, Workers: 4, StartTime: "0 0 0", @@ -46,7 +45,6 @@ var ( // command-line args/flags var ( - authToken = kingpin.Flag("auth", "Account session access token from browser sign-in session").Short('T').String() clientID = kingpin.Flag("client", "Twitch app Client ID").Short('C').String() workers = kingpin.Flag("workers", "Max number of concurrent downloads (default: 4)").Short('w').Int() configFile = kingpin.Flag("config", "Path to config file (default: $HOME/.config/tvd/config.toml)").Short('c').String() From 456840f827b854e07f5b710bb73272cdbf2ef354 Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 11:36:17 -0500 Subject: [PATCH 08/10] Switch m3u8 lib to grafov/m3u8 --- go.mod | 2 +- go.sum | 4 ++-- tvd.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index a15f42b..b17d28a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/BurntSushi/toml v0.3.0 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect - github.com/osrtss/rtss v0.0.0-20170322072109-b1617c76f6da + github.com/grafov/m3u8 v0.11.1 github.com/pkg/errors v0.8.0 github.com/schollz/progressbar/v3 v3.7.3 github.com/stretchr/testify v1.6.1 // indirect diff --git a/go.sum b/go.sum index 46b89cc..0b387fe 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ 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/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA= +github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -15,8 +17,6 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/osrtss/rtss v0.0.0-20170322072109-b1617c76f6da h1:/Z/TuENqR/F8BVs0WUpAIegWgfKY8EoP93i3kRMapRQ= -github.com/osrtss/rtss v0.0.0-20170322072109-b1617c76f6da/go.mod h1:HP7OZkbc9jh3kEc5zfljE3gG++ejTIbq3HZRui5n+mo= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/tvd.go b/tvd.go index 8e3baa5..69d58a1 100644 --- a/tvd.go +++ b/tvd.go @@ -17,7 +17,7 @@ import ( "strings" "github.com/BurntSushi/toml" - "github.com/osrtss/rtss/m3u8" + "github.com/grafov/m3u8" "github.com/schollz/progressbar/v3" "gopkg.in/alecthomas/kingpin.v2" ) From 1ff254a14bf0397f4a14417b1030a7096d72206f Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 11:49:10 -0500 Subject: [PATCH 09/10] Rename getAuthToken to getAccessData --- tvd.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tvd.go b/tvd.go index 69d58a1..42a774a 100644 --- a/tvd.go +++ b/tvd.go @@ -165,7 +165,7 @@ func createDefaultConfigFile() error { // DownloadVOD downloads a VOD based on the various info passed in the config func DownloadVOD(cfg Config) error { fmt.Println("Fetching access token") - ar, err := getAuthToken(cfg.VodID, cfg.ClientID) + ar, err := getAccessData(cfg.VodID, cfg.ClientID) if err != nil { return err } @@ -228,7 +228,7 @@ func DownloadVOD(cfg Config) error { return nil } -func getAuthToken(vodID int, clientID string) (AuthGQLResponse, error) { +func getAccessData(vodID int, clientID string) (AuthGQLResponse, error) { log.Printf("[getAuthToken] vodID=%d\n", vodID) var ar AuthGQLResponse From b6f708261d8cf0c0c5e551eab0bd38411a207c26 Mon Sep 17 00:00:00 2001 From: Domenic Date: Fri, 15 Jan 2021 11:49:36 -0500 Subject: [PATCH 10/10] Add some additional error logging --- tvd.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tvd.go b/tvd.go index 42a774a..9e7505a 100644 --- a/tvd.go +++ b/tvd.go @@ -268,7 +268,7 @@ func getAccessData(vodID int, clientID string) (AuthGQLResponse, error) { } if len(ar.Data.VideoPlaybackAccessToken.Signature) == 0 || len(ar.Data.VideoPlaybackAccessToken.Value) == 0 { log.Printf("response: %s\n", rspData) - return ar, fmt.Errorf("error: sig and/or token were empty: %+v", ar) + return ar, fmt.Errorf("error: sig and/or token were empty; response body: %+v", ar) } log.Printf("access token: %+v\n", ar) @@ -300,6 +300,7 @@ func getStreamOptions(vodID int, ar AuthGQLResponse) (map[string]string, error) p, listType, err := m3u8.DecodeFrom(rsp.Body, true) if err != nil { + log.Printf("failed to decode m3u8: %s\n", err.Error()) return nil, err } @@ -316,6 +317,7 @@ func getStreamOptions(vodID int, ar AuthGQLResponse) (map[string]string, error) } default: + log.Println("m3u8 playlist was not the expected 'master' format") return nil, fmt.Errorf("m3u8 playlist was not the expected 'master' format") } @@ -342,6 +344,7 @@ func getChunks(streamURL string) ([]Chunk, int, error) { p, listType, err := m3u8.DecodeFrom(rsp.Body, true) if err != nil { + log.Printf("failed to decode m3u8: %s\n", err.Error()) return nil, 0, err } @@ -362,6 +365,7 @@ func getChunks(streamURL string) ([]Chunk, int, error) { chunks = append(chunks, Chunk{Name: s.URI, Length: s.Duration, URL: chunkURL}) } default: + log.Println("m3u8 playlist was not the expected 'media' format") return nil, 0, fmt.Errorf("m3u8 playlist was not the expected 'media' format") }