Skip to content

Commit a8d43e6

Browse files
committed
Improve logging, video discovery, and auto-refresh
Logging: - Add PYTHONUNBUFFERED=1 to all subprocess calls so output streams live - Color errors/warnings in console output (red/amber) - Only filter bootstrap noise (lines starting with '<frozen importlib') - Tracebacks and exception lines now shown in red Video discovery: - Add pagination in _iter_panopto_folder (250/page, was single 500-cap request) - Remove duration>0 filter — was silently dropping sessions with Duration=None - Collect subfolders on first page only, recurse cleanly after pagination - Add per-strategy verbose logging: shows count from each of 3 strategies - Log subfolder names as they are scanned Auto-refresh: - Save All Settings now auto-triggers course refresh in background thread so courses populate immediately without a separate Refresh button click
1 parent 475d75c commit a8d43e6

2 files changed

Lines changed: 82 additions & 49 deletions

File tree

downloader.py

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ def _add(v: dict) -> None:
168168
videos.append(v)
169169

170170
# ── Strategy 1: module ExternalTool items ─────────────────────────────────
171+
s1_count = 0
171172
try:
172173
for module in course.get_modules():
173174
for item in module.get_module_items():
@@ -180,16 +181,21 @@ def _add(v: dict) -> None:
180181
"module_name": module.name,
181182
"title": item.title,
182183
"lti_url": url,
183-
"item_id": item.id, # integer Canvas item ID
184-
"viewer_url": None, # resolved via LTI launch
184+
"item_id": item.id,
185+
"viewer_url": None,
185186
})
187+
s1_count += 1
186188
except Exception as e:
187-
tqdm.write(f" [warn] modules scan {course.id}: {e}")
189+
tqdm.write(f" [warn] Strategy 1 (modules) {course.id}: {e}")
190+
tqdm.write(f" [info] Strategy 1 (modules): {s1_count} video(s)")
188191

189192
# ── Strategy 2: Panopto folder API (Videos/Panopto tab) ───────────────────
193+
s2_count = 0
190194
try:
195+
tqdm.write(f" [info] Strategy 2 (Panopto tab): launching browser…")
191196
folder_id, panopto_cookies, bearer_token = _get_panopto_tab_folder(course.id)
192197
if folder_id:
198+
tqdm.write(f" [info] Strategy 2: folder_id={folder_id}, scanning…")
193199
for s in _iter_panopto_folder(folder_id, panopto_cookies, bearer_token=bearer_token):
194200
viewer_url = (f"https://{PANOPTO_HOST}/Panopto/Pages/Viewer.aspx"
195201
f"?id={s['session_id']}")
@@ -199,24 +205,27 @@ def _add(v: dict) -> None:
199205
"module_name": s.get("folder_name", "Videos/Panopto"),
200206
"title": s["name"],
201207
"lti_url": None,
202-
"item_id": s["session_id"], # UUID string used as key
208+
"item_id": s["session_id"],
203209
"viewer_url": viewer_url,
204210
"_panopto_cookies": panopto_cookies,
205211
})
212+
s2_count += 1
213+
else:
214+
tqdm.write(f" [warn] Strategy 2: could not find Panopto folder (browser may have failed)")
206215
except Exception as e:
207-
tqdm.write(f" [warn] Panopto folder scan {course.id}: {e}")
216+
tqdm.write(f" [warn] Strategy 2 (Panopto folder) {course.id}: {e}")
217+
tqdm.write(f" [info] Strategy 2 (Panopto tab): {s2_count} video(s)")
208218

209219
# ── Strategy 3: Canvas Pages scan (fallback only) ─────────────────────────
210-
# Only run if strategies 1+2 found nothing. Pages embed Panopto using
211-
# delivery/embed IDs that differ from SessionIDs and cannot be resolved
212-
# as standalone sessions, so we avoid false entries when the folder API
213-
# already provides complete coverage.
214220
if not videos:
221+
tqdm.write(f" [info] Strategy 3 (pages scan): trying…")
215222
try:
223+
s3_before = len(videos)
216224
for v in _find_panopto_in_pages(course):
217225
_add(v)
226+
tqdm.write(f" [info] Strategy 3 (pages scan): {len(videos) - s3_before} video(s)")
218227
except Exception as e:
219-
tqdm.write(f" [warn] pages scan {course.id}: {e}")
228+
tqdm.write(f" [warn] Strategy 3 (pages scan) {course.id}: {e}")
220229

221230
return videos
222231

@@ -305,46 +314,59 @@ def _iter_panopto_folder(
305314
if bearer_token:
306315
headers["Authorization"] = f"Bearer {bearer_token}"
307316

308-
results: list[dict] = []
309-
310-
r = _sess.post(
311-
f"https://{PANOPTO_HOST}/Panopto/Services/Data.svc/GetSessions",
312-
json={"queryParameters": {
313-
"folderID": folder_id,
314-
"startIndex": 0,
315-
"maxResults": 500,
316-
"sortColumn": 1,
317-
"sortAscending": True,
318-
"getFolderData": True, # required to get Subfolders list
319-
"includeArchived": True,
320-
"includePlaylists": True,
321-
"includePlaceholderSessions": False,
322-
}},
323-
headers=headers,
324-
timeout=30,
325-
)
326-
if r.status_code != 200:
327-
return results
328-
329-
d = r.json().get("d") or {}
330-
331-
for s in d.get("Results") or []:
332-
# DeliveryID is what Viewer.aspx and DeliveryInfo.aspx use for streaming.
333-
# SessionID is the internal DB identifier and does NOT work with DeliveryInfo.
334-
sid = s.get("DeliveryID") or s.get("SessionID")
335-
name = s.get("SessionName", "")
336-
duration = s.get("Duration") # None/0 → navigation placeholder, not a video
337-
if sid and name and duration:
338-
results.append({
339-
"session_id": sid,
340-
"name": name,
341-
"folder_name": folder_name,
342-
})
317+
results: list[dict] = []
318+
subfolders: list[dict] = [] # collected from first page (getFolderData)
319+
PAGE_SIZE = 250
320+
start_idx = 0
321+
322+
while True:
323+
r = _sess.post(
324+
f"https://{PANOPTO_HOST}/Panopto/Services/Data.svc/GetSessions",
325+
json={"queryParameters": {
326+
"folderID": folder_id,
327+
"startIndex": start_idx,
328+
"maxResults": PAGE_SIZE,
329+
"sortColumn": 1,
330+
"sortAscending": True,
331+
"getFolderData": start_idx == 0, # only needed on first page
332+
"includeArchived": True,
333+
"includePlaylists": True,
334+
"includePlaceholderSessions": False,
335+
}},
336+
headers=headers,
337+
timeout=30,
338+
)
339+
if r.status_code != 200:
340+
tqdm.write(f" [warn] GetSessions HTTP {r.status_code} for folder {folder_id}")
341+
break
342+
343+
d = r.json().get("d") or {}
344+
page_results = d.get("Results") or []
345+
346+
for s in page_results:
347+
sid = s.get("DeliveryID") or s.get("SessionID")
348+
name = s.get("SessionName", "")
349+
if sid and name:
350+
results.append({
351+
"session_id": sid,
352+
"name": name,
353+
"folder_name": folder_name,
354+
})
355+
356+
if start_idx == 0:
357+
subfolders = d.get("Subfolders") or []
358+
359+
start_idx += len(page_results)
360+
total = d.get("TotalNumberOfResults") or 0
361+
if not page_results or start_idx >= total:
362+
break
343363

344-
for sf in d.get("Subfolders") or []:
364+
# Recurse into subfolders
365+
for sf in subfolders:
345366
sf_id = sf.get("ID") or sf.get("FolderID") or sf.get("Id")
346367
sf_name = sf.get("Name") or folder_name
347368
if sf_id:
369+
tqdm.write(f" [info] Scanning subfolder: {sf_name}")
348370
results.extend(
349371
_iter_panopto_folder(sf_id, cookies, sf_name, bearer_token, _sess)
350372
)

gui.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -475,19 +475,28 @@ def _flush_thread() -> None:
475475

476476
def _worker() -> None:
477477
try:
478+
env = {**os.environ, "PYTHONUNBUFFERED": "1"}
478479
state.proc = subprocess.Popen(
479480
cmd,
480481
stdout=subprocess.PIPE,
481482
stderr=subprocess.STDOUT,
482483
text=True,
483484
cwd=str(_get_output_dir()),
484485
bufsize=1,
486+
env=env,
485487
)
486488
for line in state.proc.stdout:
487-
# Filter out frozen importlib noise
488-
if "<frozen importlib" in line or "OpenSSL 3" in line:
489+
# Filter only the noisiest frozen-importlib bootstrap lines
490+
stripped = line.strip()
491+
if stripped.startswith("<frozen importlib"):
489492
continue
490-
_line_q.put((line.rstrip(), None))
493+
color = None
494+
low = stripped.lower()
495+
if low.startswith(("error", "traceback", "exception")):
496+
color = C_ERROR
497+
elif low.startswith("warning"):
498+
color = C_WARN
499+
_line_q.put((line.rstrip(), color))
491500
state.proc.wait()
492501
rc = state.proc.returncode
493502

@@ -1753,6 +1762,8 @@ def _save_all(_):
17531762
_snack("Errors: " + "; ".join(errors), ok=False)
17541763
else:
17551764
_snack("All settings saved successfully!")
1765+
# Auto-refresh courses after saving so the user sees results immediately
1766+
threading.Thread(target=_do_refresh, daemon=True).start()
17561767

17571768
save_btn = ft.FilledButton(
17581769
"Save All Settings",

0 commit comments

Comments
 (0)