fix: revert ExternalUrl auth to DownloadVideo, remove dead debug code
DownloadVideo already handles the Panopto Login.aspx canvas auth form correctly through its multi-step form submission logic. The real fix for ExternalUrl Panopto folder links was the URL normalization (List.aspx?folderID= → List.aspx#folderID="...") already in DownloadVideo. Remove DownloadExternalPanoptoURL (dead code) and all debug prints. Update README with -vo flag documentation.
This commit is contained in:
12
README.md
12
README.md
@@ -21,11 +21,16 @@ go build -o canvasarchiver ./cmd/canvasarchiver
|
|||||||
./canvasarchiver
|
./canvasarchiver
|
||||||
```
|
```
|
||||||
|
|
||||||
Or for files-only mode:
|
Or for files-only mode (all files flat, no videos):
|
||||||
```bash
|
```bash
|
||||||
./canvasarchiver -fo
|
./canvasarchiver -fo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or for videos-only mode (all Panopto videos flat, no files):
|
||||||
|
```bash
|
||||||
|
./canvasarchiver -vo
|
||||||
|
```
|
||||||
|
|
||||||
2. On first run, you'll be prompted to authenticate:
|
2. On first run, you'll be prompted to authenticate:
|
||||||
- Visit the provided OAuth URL
|
- Visit the provided OAuth URL
|
||||||
- Authorize the application
|
- Authorize the application
|
||||||
@@ -38,11 +43,14 @@ go build -o canvasarchiver ./cmd/canvasarchiver
|
|||||||
- Module content (to `Modules/`)
|
- Module content (to `Modules/`)
|
||||||
- Panopto recordings (to `Recordings/`)
|
- Panopto recordings (to `Recordings/`)
|
||||||
|
|
||||||
|
In `-vo` mode, only videos are downloaded — all into the course root directory (no subdirectories).
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `-fo` | Files only mode - download all files to a single directory without module structure |
|
| `-fo` | Files only — download all files flat into one directory; skips videos and module structure |
|
||||||
|
| `-vo` | Videos only — scan recordings and all module video items, download everything flat into one directory; skips regular files |
|
||||||
| `-me` | Download all enrolled courses |
|
| `-me` | Download all enrolled courses |
|
||||||
| `-n` | Prefix modules with order numbers `[1]`, `[2]`, etc. |
|
| `-n` | Prefix modules with order numbers `[1]`, `[2]`, etc. |
|
||||||
|
|
||||||
|
|||||||
@@ -377,186 +377,3 @@ func DownloadMainRecordings(httpClient *http.Client, accessToken, courseID, root
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadExternalPanoptoURL authenticates via the module_item_redirect path
|
|
||||||
// (exactly what the Canvas mobile app does for ExternalUrl items) and then
|
|
||||||
// runs yt-dlp against the given Panopto URL with the resulting cookies.
|
|
||||||
//
|
|
||||||
// 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)
|
|
||||||
// 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"
|
|
||||||
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)
|
|
||||||
if err != nil {
|
|
||||||
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.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.
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(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", formFinalURL)
|
|
||||||
postResp, err := noRedirectClient.Do(postReq)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf(" [!] OAuth accept POST failed: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
postBody, _ := io.ReadAll(postResp.Body)
|
|
||||||
postResp.Body.Close()
|
|
||||||
fmt.Printf(" [dbg] POST response status: %d, body len: %d\n", postResp.StatusCode, len(postBody))
|
|
||||||
|
|
||||||
// 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 := 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)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cookieFile := filepath.Join(modDir, ".cookies_ext.txt")
|
|
||||||
cData := "# Netscape HTTP Cookie File\n"
|
|
||||||
for _, c := range cookies {
|
|
||||||
cData += fmt.Sprintf(".vub.cloud.panopto.eu\tTRUE\t/\tTRUE\t0\t%s\t%s\n", c.Name, c.Value)
|
|
||||||
}
|
|
||||||
os.WriteFile(cookieFile, []byte(cData), 0o644)
|
|
||||||
|
|
||||||
fmt.Printf(" [*] Downloading: %s\n", title)
|
|
||||||
|
|
||||||
normalizedURL := normalizePanoptoURL(panoptoURL)
|
|
||||||
isList := strings.Contains(normalizedURL, "List.aspx")
|
|
||||||
|
|
||||||
ytCmd := getYoutubeDLCommand()
|
|
||||||
var args []string
|
|
||||||
if isList {
|
|
||||||
args = []string{
|
|
||||||
"--cookies", cookieFile,
|
|
||||||
"--referer", config.BaseURL + "/",
|
|
||||||
"-P", modDir,
|
|
||||||
"-o", utils.Sanitize(title) + "/%(title)s.%(ext)s",
|
|
||||||
normalizedURL,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
args = []string{
|
|
||||||
"--no-playlist",
|
|
||||||
"--cookies", cookieFile,
|
|
||||||
"--referer", config.BaseURL + "/",
|
|
||||||
"-P", modDir,
|
|
||||||
"-o", utils.Sanitize(title) + ".%(ext)s",
|
|
||||||
normalizedURL,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(ytCmd, args...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
fmt.Printf(" [!] yt-dlp failed: %v\n", err)
|
|
||||||
}
|
|
||||||
os.Remove(cookieFile)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user