Downloader fixes with videos

This commit is contained in:
2026-05-16 22:51:38 +02:00
parent ea9d4dc2dc
commit 522a8b22f8
2 changed files with 87 additions and 20 deletions

View File

@@ -240,7 +240,7 @@ func (c *Client) DownloadModules(courseRoot string) {
indent := strings.Repeat(" ", len(subHeaderStack)+1)
fmt.Printf("%s- Found direct video link: %s\n", indent, item.Title)
panopto.DownloadExternalPanoptoURL(c.HTTPClient, c.AccessToken, item.HTMLURL, item.ExternalURL, targetDir, item.Title)
panopto.DownloadVideo(c.HTTPClient, c.AccessToken, c.CourseID, targetDir, item.ExternalURL, item.Title)
}
case "Page":

View File

@@ -384,14 +384,23 @@ func DownloadMainRecordings(httpClient *http.Client, accessToken, courseID, root
// moduleItemURL item.URL (https://canvas.vub.be/api/v1/.../module_item_redirect/<id>)
// panoptoURL item.ExternalURL (https://vub.cloud.panopto.eu/Panopto/Pages/...)
func DownloadExternalPanoptoURL(httpClient *http.Client, accessToken, moduleItemURL, panoptoURL, modDir, title string) {
fmt.Printf(" [dbg] moduleItemURL: %s\n", moduleItemURL)
fmt.Printf(" [dbg] panoptoURL: %s\n", panoptoURL)
jar, _ := cookiejar.New(nil)
// Follow all redirects automatically so Login.aspx → CookieCheck.aspx sets cookies.
webClient := &http.Client{Jar: jar}
// Manual redirect following so we can track cross-domain hops correctly.
noRedirectClient := &http.Client{
Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// Step 1: exchange the module item URL for a Canvas web session URL.
returnTo := moduleItemURL + "?display=borderless"
bridgeReq, _ := http.NewRequest("GET",
config.BaseURL+"/login/session_token?return_to="+url.QueryEscape(returnTo), nil)
sessionTokenURL := config.BaseURL + "/login/session_token?return_to=" + url.QueryEscape(returnTo)
fmt.Printf(" [dbg] session_token URL: %s\n", sessionTokenURL)
bridgeReq, _ := http.NewRequest("GET", sessionTokenURL, nil)
bridgeReq.Header.Set("Authorization", "Bearer "+accessToken)
bridgeReq.Header.Set("User-Agent", config.UserAgent)
bResp, err := httpClient.Do(bridgeReq)
@@ -399,26 +408,51 @@ func DownloadExternalPanoptoURL(httpClient *http.Client, accessToken, moduleItem
fmt.Printf(" [!] session_token request failed: %v\n", err)
return
}
rawBody, _ := io.ReadAll(bResp.Body)
bResp.Body.Close()
fmt.Printf(" [dbg] session_token response (%d): %s\n", bResp.StatusCode, string(rawBody))
var bridgeData struct {
SessionURL string `json:"session_url"`
}
json.NewDecoder(bResp.Body).Decode(&bridgeData)
bResp.Body.Close()
json.Unmarshal(rawBody, &bridgeData)
if bridgeData.SessionURL == "" {
fmt.Printf(" [!] No session_url returned (skipping %s)\n", title)
return
}
fmt.Printf(" [dbg] session_url: %s\n", bridgeData.SessionURL)
// Step 2: GET session URL → Canvas shows OAuth2 confirm page for Panopto.
formResp, err := webClient.Get(bridgeData.SessionURL)
if err != nil {
fmt.Printf(" [!] Failed to load OAuth confirm page: %v\n", err)
return
// We follow redirects manually so cross-domain hops (canvas→panopto) work correctly.
currentURL := bridgeData.SessionURL
var formHTML string
var formFinalURL string
for hop := 0; hop < 10; hop++ {
hopReq, _ := http.NewRequest("GET", currentURL, nil)
hopReq.Header.Set("User-Agent", config.UserAgent)
hopResp, err := noRedirectClient.Do(hopReq)
if err != nil {
fmt.Printf(" [!] Failed hop %d to %s: %v\n", hop, currentURL, err)
return
}
body, _ := io.ReadAll(hopResp.Body)
hopResp.Body.Close()
fmt.Printf(" [dbg] hop %d: status=%d url=%s body=%d\n", hop, hopResp.StatusCode, currentURL, len(body))
if hopResp.StatusCode == 301 || hopResp.StatusCode == 302 || hopResp.StatusCode == 303 {
loc, _ := hopResp.Location()
if loc == nil {
fmt.Printf(" [!] Redirect with no Location header\n")
return
}
currentURL = loc.String()
continue
}
formHTML = string(body)
formFinalURL = currentURL
break
}
formHTMLBytes, _ := io.ReadAll(formResp.Body)
formResp.Body.Close()
formHTML := string(formHTMLBytes)
fmt.Printf(" [dbg] OAuth page final URL: %s, length: %d\n", formFinalURL, len(formHTML))
if strings.Contains(formHTML, "U hebt geen toegang") || strings.Contains(formHTML, "You do not have access") {
fmt.Printf(" [!] Access denied. Skipping %s\n", title)
@@ -427,25 +461,58 @@ func DownloadExternalPanoptoURL(httpClient *http.Client, accessToken, moduleItem
// Step 3: POST /login/oauth2/accept → Canvas redirects to Panopto Login.aspx?code=...
// webClient follows all hops automatically, ending with Panopto setting cookies.
action := utils.ResolveAction(bridgeData.SessionURL, formHTML)
action := utils.ResolveAction(formFinalURL, formHTML)
formData := utils.ExtractFormFields(formHTML)
fmt.Printf(" [dbg] POST action: %s\n", action)
postReq, _ := http.NewRequest("POST", action, strings.NewReader(formData.Encode()))
postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
postReq.Header.Set("User-Agent", config.UserAgent)
postReq.Header.Set("Origin", config.BaseURL)
postReq.Header.Set("Referer", bridgeData.SessionURL)
postResp, err := webClient.Do(postReq)
postReq.Header.Set("Referer", formFinalURL)
postResp, err := noRedirectClient.Do(postReq)
if err != nil {
fmt.Printf(" [!] OAuth accept POST failed: %v\n", err)
return
}
io.ReadAll(postResp.Body)
postBody, _ := io.ReadAll(postResp.Body)
postResp.Body.Close()
fmt.Printf(" [dbg] POST response status: %d, body len: %d\n", postResp.StatusCode, len(postBody))
// webClient.Jar now holds Panopto session cookies from the CookieCheck chain.
// Now follow the redirect chain from the POST (canvas → panopto Login.aspx → CookieCheck).
if postResp.StatusCode == 302 || postResp.StatusCode == 303 {
currentURL2 := ""
if loc, _ := postResp.Location(); loc != nil {
currentURL2 = loc.String()
}
for hop := 0; hop < 10 && currentURL2 != ""; hop++ {
hopReq, _ := http.NewRequest("GET", currentURL2, nil)
hopReq.Header.Set("User-Agent", config.UserAgent)
hopResp, err := noRedirectClient.Do(hopReq)
if err != nil {
fmt.Printf(" [dbg] post-redirect hop %d error: %v\n", hop, err)
break
}
body2, _ := io.ReadAll(hopResp.Body)
hopResp.Body.Close()
fmt.Printf(" [dbg] post-redirect hop %d: status=%d url=%s body=%d\n", hop, hopResp.StatusCode, currentURL2, len(body2))
if hopResp.StatusCode == 301 || hopResp.StatusCode == 302 || hopResp.StatusCode == 303 {
if loc, _ := hopResp.Location(); loc != nil {
currentURL2 = loc.String()
continue
}
}
break
}
}
// noRedirectClient.Jar now holds Panopto session cookies from the CookieCheck chain.
panoptoDomain, _ := url.Parse("https://vub.cloud.panopto.eu")
cookies := webClient.Jar.Cookies(panoptoDomain)
cookies := noRedirectClient.Jar.Cookies(panoptoDomain)
fmt.Printf(" [dbg] Panopto cookies: %d\n", len(cookies))
for _, c := range cookies {
fmt.Printf(" [dbg] cookie: %s=%s\n", c.Name, c.Value[:min(20, len(c.Value))])
}
if len(cookies) == 0 {
fmt.Printf(" [!] No Panopto cookies after auth falling back for: %s\n", title)
DownloadVideo(httpClient, accessToken, "", modDir, panoptoURL, title)