diff --git a/internal/canvas/client.go b/internal/canvas/client.go index b1fcdb4..2b2220e 100644 --- a/internal/canvas/client.go +++ b/internal/canvas/client.go @@ -166,6 +166,14 @@ func (c *Client) DownloadModules(courseRoot string) { fmt.Printf("%s- Found video tool: %s\n", indent, item.Title) panopto.DownloadVideo(c.HTTPClient, c.AccessToken, c.CourseID, targetDir, item.URL, item.Title) + case "ExternalUrl": + if strings.Contains(item.ExternalURL, "panopto.eu") { + indent := strings.Repeat(" ", len(subHeaderStack)+1) + fmt.Printf("%s- Found direct video link: %s\n", indent, item.Title) + + panopto.DownloadVideo(c.HTTPClient, c.AccessToken, c.CourseID, targetDir, item.ExternalURL, item.Title) + } + case "Page": c.searchPageForVideos(item, targetDir) } diff --git a/internal/models/models.go b/internal/models/models.go index b91a430..68e8589 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -30,10 +30,11 @@ type Module struct { } type ModuleItem struct { - Title string `json:"title"` - Type string `json:"type"` - URL string `json:"url"` - HTMLURL string `json:"html_url"` - ContentID int `json:"content_id"` - Indent int `json:"indent"` + Title string `json:"title"` + Type string `json:"type"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + ExternalURL string `json:"external_url"` + ContentID int `json:"content_id"` + Indent int `json:"indent"` } diff --git a/internal/panopto/downloader.go b/internal/panopto/downloader.go index bb3fa23..362c544 100644 --- a/internal/panopto/downloader.go +++ b/internal/panopto/downloader.go @@ -45,12 +45,13 @@ func DownloadVideo(httpClient *http.Client, accessToken, courseID, modDir, input } var launchURL string + isDirectLink := false + if strings.Contains(inputURL, "/api/v1/") { launchURL = inputURL - } else if strings.Contains(inputURL, "panopto.eu") { - launchURL = fmt.Sprintf("%s/api/v1/courses/%s/external_tools/sessionless_launch?url=%s", - config.BaseURL, courseID, url.QueryEscape(inputURL)) } else { + + isDirectLink = true launchURL = fmt.Sprintf("%s/api/v1/courses/%s/external_tools/sessionless_launch?id=%s&launch_type=course_navigation", config.BaseURL, courseID, config.PanoptoID) } @@ -70,6 +71,7 @@ func DownloadVideo(httpClient *http.Client, accessToken, courseID, modDir, input resp.Body.Close() if launchData.URL == "" { + fmt.Printf(" [!] No launch URL found (Video skipped)\n") return } @@ -84,17 +86,21 @@ func DownloadVideo(httpClient *http.Client, accessToken, courseID, modDir, input json.NewDecoder(bResp.Body).Decode(&bridgeData) bResp.Body.Close() - formReq, _ := http.NewRequest("GET", bridgeData.SessionURL, nil) - formReq.Header.Set("User-Agent", config.UserAgent) - formResp, err := httpClient.Do(formReq) + formResp, err := httpClient.Get(bridgeData.SessionURL) if err != nil { return } - formHTML, _ := io.ReadAll(formResp.Body) + formHTMLBytes, _ := io.ReadAll(formResp.Body) formResp.Body.Close() + formHTML := string(formHTMLBytes) - action := utils.ResolveAction(bridgeData.SessionURL, string(formHTML)) - formData := utils.ExtractFormFields(string(formHTML)) + if strings.Contains(formHTML, "U hebt geen toegang") || strings.Contains(formHTML, "You do not have access") { + fmt.Printf(" [!] Access denied by Panopto (U hebt geen toegang). Skipping.\n") + return + } + + action := utils.ResolveAction(bridgeData.SessionURL, formHTML) + formData := utils.ExtractFormFields(formHTML) pReq, _ := http.NewRequest("POST", action, strings.NewReader(formData.Encode())) pReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -150,7 +156,31 @@ func DownloadVideo(httpClient *http.Client, accessToken, courseID, modDir, input } } + // This is for making sure yt-dlp does not auto-start downloading all videos, when access to a hyperlink is denied if finalURL != "" && !strings.Contains(finalURL, "NonFatalError") { + + targetURL := finalURL + if isDirectLink { + targetURL = inputURL + + checkReq, _ := http.NewRequest("GET", targetURL, nil) + checkReq.Header.Set("User-Agent", config.UserAgent) + + checkResp, err := panoptoClient.Do(checkReq) + if err == nil { + checkResp.Body.Close() + + if checkResp.StatusCode == http.StatusFound || checkResp.StatusCode == http.StatusSeeOther { + loc, _ := checkResp.Location() + if loc != nil && (strings.Contains(loc.String(), "Login.aspx") || strings.Contains(loc.String(), "Auth")) { + fmt.Printf(" [!] Video inaccessible (redirects to Login). Skipping to prevent mass download.\n") + return + } + } + } + + } + cookieFile := filepath.Join(modDir, ".cookies_temp.txt") cData := "# Netscape HTTP Cookie File\n" panoptoDomain, _ := url.Parse("https://vub.cloud.panopto.eu") @@ -162,14 +192,14 @@ func DownloadVideo(httpClient *http.Client, accessToken, courseID, modDir, input fmt.Printf(" [*] Downloading video: %s\n", title) ytCmd := getYoutubeDLCommand() - cmd := exec.Command(ytCmd, "--no-playlist", "--cookies", cookieFile, "--referer", config.BaseURL+"/", "-P", modDir, "-o", utils.Sanitize(title)+".%(ext)s", - finalURL) + targetURL) + cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil {