-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
executable file
·295 lines (241 loc) · 10.3 KB
/
Copy pathmain.py
File metadata and controls
executable file
·295 lines (241 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#!/usr/bin/env python3
"""Main script for Canvas subscriber - downloads files from newly published modules."""
import argparse
import os
import sys
from dotenv import load_dotenv
from src.canvas_client import CanvasClient
from src.file_manager import FileManager
from src.state_tracker import StateTracker
from src.notifier import EmailNotifier
from src.course_config import CourseConfigManager
from src.course_selector import select_courses_interactive
from src.course_cleaner import CourseCleaner
def str_to_bool(value: str) -> bool:
"""Convert string to boolean."""
return value.lower() in ('true', '1', 'yes', 'on')
def main():
"""Main function to run Canvas subscriber."""
# Parse command-line arguments
parser = argparse.ArgumentParser(
description='Canvas subscriber - downloads files from newly published modules'
)
parser.add_argument(
'--reconfigure-courses',
action='store_true',
help='Reconfigure which courses to track'
)
parser.add_argument(
'--clean',
action='store_true',
help='Remove files from courses that are no longer tracked'
)
parser.add_argument(
'--dry',
action='store_true',
help='Dry run mode - show what would happen without making changes'
)
args = parser.parse_args()
# Load environment variables
load_dotenv()
# Validate required environment variables
required_vars = ['CANVAS_API_URL', 'CANVAS_API_TOKEN', 'DOWNLOAD_PATH']
missing_vars = [var for var in required_vars if not os.getenv(var)]
if missing_vars:
print(f"Error: Missing required environment variables: {', '.join(missing_vars)}")
print("Please configure your .env file (see .env.example)")
sys.exit(1)
# Initialize components
print("Initializing Canvas subscriber...")
canvas_client = CanvasClient(
api_url=os.getenv('CANVAS_API_URL'),
api_token=os.getenv('CANVAS_API_TOKEN')
)
file_manager = FileManager(
download_path=os.getenv('DOWNLOAD_PATH')
)
state_tracker = StateTracker()
email_notifier = EmailNotifier(
enabled=str_to_bool(os.getenv('EMAIL_ENABLED', 'false')),
smtp_host=os.getenv('SMTP_HOST', ''),
smtp_port=int(os.getenv('SMTP_PORT', '587')),
smtp_user=os.getenv('SMTP_USER', ''),
smtp_password=os.getenv('SMTP_PASSWORD', ''),
from_email=os.getenv('EMAIL_FROM', ''),
to_email=os.getenv('EMAIL_TO', '')
)
course_config = CourseConfigManager()
# Track new modules for notification
new_modules = []
# Get enrolled courses
print("\nFetching enrolled courses...")
courses = canvas_client.get_enrolled_courses()
print(f"Found {len(courses)} active courses")
# Handle course selection
if args.reconfigure_courses or not course_config.config_exists() or not course_config.has_tracked_courses():
if args.reconfigure_courses:
print("\nReconfiguring course selection...")
else:
print("\nNo course configuration found. Let's set up which courses to track.")
selected_course_ids = select_courses_interactive(courses)
if not selected_course_ids:
print("\nNo courses selected. Exiting.")
sys.exit(0)
course_config.save_config(selected_course_ids)
print(f"\nConfiguration saved! Tracking {len(selected_course_ids)} course(s).")
# Filter courses to only tracked ones
tracked_courses = [c for c in courses if course_config.is_course_tracked(c.id)]
if not tracked_courses:
print("\nNo tracked courses found. Run with --reconfigure-courses to update selection.")
sys.exit(0)
# Check if all tracked courses have Obsidian folder mappings
print("\nConfiguring Obsidian folder mappings...")
for course in tracked_courses:
if not course_config.has_obsidian_folder(course.id):
print(f"\n Course: {course.name}")
while True:
folder_name = input(f" Enter Obsidian folder name (e.g., 'CS375-003'): ").strip()
if folder_name:
course_config.set_obsidian_folder(course.id, folder_name)
print(f" ✓ Mapped to: {folder_name}")
break
else:
print(" Error: Folder name cannot be empty. Please try again.")
else:
folder_name = course_config.get_obsidian_folder(course.id)
print(f" ✓ {course.name} → {folder_name}")
print()
# Handle cleanup command
if args.clean:
print("\n" + "=" * 60)
if args.dry:
print("CLEANUP MODE - DRY RUN")
else:
print("CLEANUP MODE")
print("=" * 60)
if not course_config.config_exists():
print("\nError: No course configuration found.")
print("Please run the script normally first to configure which courses to track.")
sys.exit(1)
# Create cleaner instance
cleaner = CourseCleaner(
download_path=os.getenv('DOWNLOAD_PATH'),
course_config_manager=course_config,
canvas_client=canvas_client,
file_manager=file_manager,
state_tracker=state_tracker
)
# Preview what will be cleaned
untracked = cleaner.preview_cleanup(courses)
if not untracked:
print("\nNothing to clean up!")
sys.exit(0)
# Handle dry run mode
if args.dry:
print("\n[DRY RUN] No files will be deleted.")
print("Run without --dry to perform actual cleanup.")
sys.exit(0)
# Ask for confirmation
print("\n⚠️ WARNING: This action cannot be undone!")
confirm = input("Proceed with cleanup? (yes/no): ").strip().lower()
if confirm in ('yes', 'y'):
print("\nPerforming cleanup...")
deleted_count = cleaner.perform_cleanup(untracked, cleanup_state=True)
print(f"\n✓ Cleanup complete! Deleted {deleted_count} course director(y/ies).")
else:
print("\nCleanup cancelled.")
sys.exit(0)
if args.dry:
print("\n" + "=" * 60)
print("DRY RUN MODE - NO FILES WILL BE DOWNLOADED")
print("=" * 60)
print(f"\nProcessing {len(tracked_courses)} tracked course(s)...")
# Process each course
for course in tracked_courses:
# Get the Obsidian folder name for this course
obsidian_folder = course_config.get_obsidian_folder(course.id)
print(f"\n{'='*60}")
print(f"Processing: {course.name}")
print(f"Obsidian folder: {obsidian_folder}")
print(f"{'='*60}")
# Get modules for this course
modules = canvas_client.get_course_modules(course)
print(f" Found {len(modules)} modules")
for module in modules:
module_id = module.id
is_new_module = state_tracker.is_module_new(course.id, module_id)
if is_new_module:
print(f"\n [NEW MODULE] {module.name}")
else:
print(f"\n [TRACKED] {module.name}")
# Get items in this module
items = canvas_client.get_module_items(course, module)
file_count = 0
for item in items:
# Only process file items
if item.type != 'File':
continue
# Check if we've already downloaded this file
file_id = item.content_id
if state_tracker.is_file_downloaded(course.id, module_id, file_id):
print(f" [SKIP] {item.title} (already downloaded)")
continue
# Download the file (or preview in dry run mode)
download_url = canvas_client.get_file_download_url(file_id)
if download_url:
if args.dry:
# Dry run: just show what would be downloaded
print(f" [WOULD DOWNLOAD] {item.title}")
file_count += 1
else:
# Normal mode: actually download
result = file_manager.download_file(
url=download_url,
course_folder_name=obsidian_folder,
module_name=module.name,
filename=item.title
)
if result:
state_tracker.mark_file_downloaded(course.id, module_id, file_id)
file_count += 1
print(f" [DOWNLOADED] {item.title}")
# Mark module as tracked (skip in dry run mode)
if is_new_module:
if not args.dry:
state_tracker.mark_module_tracked(course.id, module_id, module.name)
new_modules.append({
'course_name': course.name,
'module_name': module.name,
'file_count': file_count
})
# Update last run timestamp (skip in dry run mode)
if not args.dry:
state_tracker.update_last_run()
# Send notification if there are new modules
if new_modules:
print(f"\n{'='*60}")
if args.dry:
print(f"DRY RUN SUMMARY: {len(new_modules)} new module(s) detected")
else:
print(f"SUMMARY: {len(new_modules)} new module(s) found")
print(f"{'='*60}")
for module in new_modules:
if args.dry:
print(f" - {module['course_name']}: {module['module_name']} ({module['file_count']} files would be downloaded)")
else:
print(f" - {module['course_name']}: {module['module_name']} ({module['file_count']} files)")
# Skip email notification in dry run mode
if not args.dry:
email_notifier.send_notification(new_modules)
else:
if args.dry:
print("\n[DRY RUN] No new modules detected")
else:
print("\nNo new modules found")
if args.dry:
print("\n[DRY RUN] No changes were made to the filesystem or state.")
print("Run without --dry to download files.")
else:
print("\nCanvas subscriber completed successfully!")
if __name__ == '__main__':
main()