From 43392a4132acf86fac39f9a85e1775b3748b5237 Mon Sep 17 00:00:00 2001 From: Joren Date: Sat, 16 May 2026 19:27:36 +0200 Subject: [PATCH] fix: normalize List.aspx?folderID= to fragment form for yt-dlp Canvas stores folder links as List.aspx?folderID=X (query param). yt-dlp's PanoptoList extractor requires List.aspx#folderID="X" (fragment with quoted ID) to scope the download to that folder. Without the fragment form it downloaded the entire Panopto instance (1806 items instead of 3). Also drop --no-playlist for list URLs since they are intentional playlists, and use title/%(title)s.%(ext)s output template for them. --- internal/panopto/downloader.go | 57 +++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/internal/panopto/downloader.go b/internal/panopto/downloader.go index 968b4f7..f059785 100644 --- a/internal/panopto/downloader.go +++ b/internal/panopto/downloader.go @@ -17,6 +17,27 @@ import ( "git.directme.in/Joren/CanvasArchiver/internal/utils" ) +// normalizePanoptoURL converts the query-param form that Canvas stores +// (List.aspx?folderID=X) to the fragment form that yt-dlp's PanoptoList +// extractor understands (List.aspx#folderID="X"). Without this yt-dlp +// ignores the folder filter and downloads the entire Panopto instance. +func normalizePanoptoURL(rawURL string) string { + parsed, err := url.Parse(rawURL) + if err != nil { + return rawURL + } + if strings.Contains(parsed.Path, "List.aspx") { + folderID := parsed.Query().Get("folderID") + if folderID != "" { + // Strip query, set fragment: List.aspx#folderID="" + parsed.RawQuery = "" + parsed.Fragment = fmt.Sprintf(`folderID="%s"`, folderID) + return parsed.String() + } + } + return rawURL +} + func getYoutubeDLCommand() string { exePath, err := os.Executable() if err == nil { @@ -192,13 +213,35 @@ 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", - targetURL) + + // Normalize folder URLs so yt-dlp scopes to the right folder. + normalizedURL := normalizePanoptoURL(targetURL) + + // Folder/list URLs are intentional playlists; don't pass --no-playlist. + isList := strings.Contains(normalizedURL, "List.aspx") + var outputTpl string + var args []string + if isList { + outputTpl = utils.Sanitize(title) + "/%(title)s.%(ext)s" + args = []string{ + "--cookies", cookieFile, + "--referer", config.BaseURL + "/", + "-P", modDir, + "-o", outputTpl, + normalizedURL, + } + } else { + outputTpl = utils.Sanitize(title) + ".%(ext)s" + args = []string{ + "--no-playlist", + "--cookies", cookieFile, + "--referer", config.BaseURL + "/", + "-P", modDir, + "-o", outputTpl, + normalizedURL, + } + } + cmd := exec.Command(ytCmd, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr