coursera-transcripts 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Kavin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: coursera-transcripts
3
+ Version: 0.1.0
4
+ Summary: CLI tool to extract Coursera course transcripts/subtitles
5
+ Author-email: Kavin <your-email@example.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Environment :: Console
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: requests>=2.31.0
18
+ Requires-Dist: rich>=13.0.0
19
+ Dynamic: license-file
20
+
21
+ # 📚 Coursera Transcript Generator
22
+
23
+ A beautiful CLI tool to bulk-download transcripts and subtitles from any Coursera course you're enrolled in.
24
+
25
+ ![CLI Preview](./preview.png)
26
+
27
+ ![Python](https://img.shields.io/badge/Python-3.10+-3776AB?logo=python&logoColor=white)
28
+ ![License](https://img.shields.io/badge/License-MIT-brightgreen)
29
+
30
+ ---
31
+
32
+ ## ✨ Features
33
+
34
+ - **Interactive prompts** — guided step-by-step experience, no need to memorize flags
35
+ - **Bulk download** — grabs every lecture transcript in a course at once
36
+ - **Organized output** — files are neatly sorted into module folders
37
+ - **Progress tracking** — real-time progress bar with download status
38
+ - **Retry logic** — automatic retries with exponential backoff on failures
39
+ - **Multiple formats** — supports both `.txt` (plain text) and `.srt` (subtitle) formats
40
+ - **Multi-language** — download transcripts in any available language
41
+
42
+ ---
43
+
44
+ ## 📦 Installation
45
+
46
+ ```bash
47
+ # Clone the repo
48
+ git clone https://github.com/your-username/coursera-transcript-generator.git
49
+ cd coursera-transcript-generator
50
+
51
+ # Install in editable mode
52
+ pip install -e .
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 🚀 Usage
58
+
59
+ ### Interactive Mode (recommended)
60
+
61
+ Just run the command with no arguments — it will guide you through everything:
62
+
63
+ ```bash
64
+ coursera-transcripts
65
+ ```
66
+
67
+ You'll be prompted for:
68
+
69
+ 1. **CAUTH cookie** — your Coursera authentication token
70
+ 2. **Course slug** — the identifier from the course URL
71
+ 3. **Options** — language, format, and output directory
72
+
73
+ ### CLI Mode
74
+
75
+ Pass everything as flags for scripting / automation:
76
+
77
+ ```bash
78
+ coursera-transcripts \
79
+ --cookie "YOUR_CAUTH_VALUE" \
80
+ --slug "machine-learning" \
81
+ --language en \
82
+ --format txt \
83
+ --output ./transcripts
84
+ ```
85
+
86
+ ### All Options
87
+
88
+ | Flag | Short | Default | Description |
89
+ | ------------ | ----- | ------------ | ------------------------------ |
90
+ | `--cookie` | `-c` | _(prompted)_ | CAUTH cookie value |
91
+ | `--slug` | `-s` | _(prompted)_ | Course slug from URL |
92
+ | `--language` | `-l` | `en` | Subtitle language code |
93
+ | `--format` | | `txt` | Output format (`txt` or `srt`) |
94
+ | `--output` | `-o` | `./output` | Parent output directory |
95
+
96
+ ---
97
+
98
+ ## 🔑 Getting Your CAUTH Cookie
99
+
100
+ 1. Open [coursera.org](https://www.coursera.org) and **log in**
101
+ 2. Open **DevTools** (`F12` or `Ctrl+Shift+I`)
102
+ 3. Go to **Application** → **Cookies** → `https://www.coursera.org`
103
+ 4. Find the cookie named **`CAUTH`**
104
+ 5. Copy its **Value**
105
+
106
+ > [!IMPORTANT]
107
+ > You must be **enrolled** in the course to download its transcripts.
108
+
109
+ ---
110
+
111
+ ## 📁 Output Structure
112
+
113
+ Transcripts are organized by module:
114
+
115
+ ```
116
+ output/
117
+ └── machine-learning/
118
+ ├── introduction-to-ml/
119
+ │ ├── Welcome to Machine Learning.txt
120
+ │ ├── What is Machine Learning.txt
121
+ │ └── Supervised Learning.txt
122
+ ├── linear-regression/
123
+ │ ├── Model Representation.txt
124
+ │ └── Cost Function.txt
125
+ └── ...
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 🔧 Finding the Course Slug
131
+
132
+ The slug is the part of the URL after `/learn/`:
133
+
134
+ ```
135
+ https://www.coursera.org/learn/machine-learning
136
+ └── this is the slug
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 📋 Requirements
142
+
143
+ - Python **3.10+**
144
+ - A Coursera account with enrollment in the target course
145
+
146
+ ---
147
+
148
+ ## 📄 License
149
+
150
+ MIT
@@ -0,0 +1,130 @@
1
+ # 📚 Coursera Transcript Generator
2
+
3
+ A beautiful CLI tool to bulk-download transcripts and subtitles from any Coursera course you're enrolled in.
4
+
5
+ ![CLI Preview](./preview.png)
6
+
7
+ ![Python](https://img.shields.io/badge/Python-3.10+-3776AB?logo=python&logoColor=white)
8
+ ![License](https://img.shields.io/badge/License-MIT-brightgreen)
9
+
10
+ ---
11
+
12
+ ## ✨ Features
13
+
14
+ - **Interactive prompts** — guided step-by-step experience, no need to memorize flags
15
+ - **Bulk download** — grabs every lecture transcript in a course at once
16
+ - **Organized output** — files are neatly sorted into module folders
17
+ - **Progress tracking** — real-time progress bar with download status
18
+ - **Retry logic** — automatic retries with exponential backoff on failures
19
+ - **Multiple formats** — supports both `.txt` (plain text) and `.srt` (subtitle) formats
20
+ - **Multi-language** — download transcripts in any available language
21
+
22
+ ---
23
+
24
+ ## 📦 Installation
25
+
26
+ ```bash
27
+ # Clone the repo
28
+ git clone https://github.com/your-username/coursera-transcript-generator.git
29
+ cd coursera-transcript-generator
30
+
31
+ # Install in editable mode
32
+ pip install -e .
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 🚀 Usage
38
+
39
+ ### Interactive Mode (recommended)
40
+
41
+ Just run the command with no arguments — it will guide you through everything:
42
+
43
+ ```bash
44
+ coursera-transcripts
45
+ ```
46
+
47
+ You'll be prompted for:
48
+
49
+ 1. **CAUTH cookie** — your Coursera authentication token
50
+ 2. **Course slug** — the identifier from the course URL
51
+ 3. **Options** — language, format, and output directory
52
+
53
+ ### CLI Mode
54
+
55
+ Pass everything as flags for scripting / automation:
56
+
57
+ ```bash
58
+ coursera-transcripts \
59
+ --cookie "YOUR_CAUTH_VALUE" \
60
+ --slug "machine-learning" \
61
+ --language en \
62
+ --format txt \
63
+ --output ./transcripts
64
+ ```
65
+
66
+ ### All Options
67
+
68
+ | Flag | Short | Default | Description |
69
+ | ------------ | ----- | ------------ | ------------------------------ |
70
+ | `--cookie` | `-c` | _(prompted)_ | CAUTH cookie value |
71
+ | `--slug` | `-s` | _(prompted)_ | Course slug from URL |
72
+ | `--language` | `-l` | `en` | Subtitle language code |
73
+ | `--format` | | `txt` | Output format (`txt` or `srt`) |
74
+ | `--output` | `-o` | `./output` | Parent output directory |
75
+
76
+ ---
77
+
78
+ ## 🔑 Getting Your CAUTH Cookie
79
+
80
+ 1. Open [coursera.org](https://www.coursera.org) and **log in**
81
+ 2. Open **DevTools** (`F12` or `Ctrl+Shift+I`)
82
+ 3. Go to **Application** → **Cookies** → `https://www.coursera.org`
83
+ 4. Find the cookie named **`CAUTH`**
84
+ 5. Copy its **Value**
85
+
86
+ > [!IMPORTANT]
87
+ > You must be **enrolled** in the course to download its transcripts.
88
+
89
+ ---
90
+
91
+ ## 📁 Output Structure
92
+
93
+ Transcripts are organized by module:
94
+
95
+ ```
96
+ output/
97
+ └── machine-learning/
98
+ ├── introduction-to-ml/
99
+ │ ├── Welcome to Machine Learning.txt
100
+ │ ├── What is Machine Learning.txt
101
+ │ └── Supervised Learning.txt
102
+ ├── linear-regression/
103
+ │ ├── Model Representation.txt
104
+ │ └── Cost Function.txt
105
+ └── ...
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 🔧 Finding the Course Slug
111
+
112
+ The slug is the part of the URL after `/learn/`:
113
+
114
+ ```
115
+ https://www.coursera.org/learn/machine-learning
116
+ └── this is the slug
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 📋 Requirements
122
+
123
+ - Python **3.10+**
124
+ - A Coursera account with enrollment in the target course
125
+
126
+ ---
127
+
128
+ ## 📄 License
129
+
130
+ MIT
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "coursera-transcripts"
3
+ version = "0.1.0"
4
+ description = "CLI tool to extract Coursera course transcripts/subtitles"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name="Kavin", email="your-email@example.com" }
8
+ ]
9
+ license = { text = "MIT" }
10
+ requires-python = ">=3.10"
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.10",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Environment :: Console",
19
+ ]
20
+ dependencies = [
21
+ "requests>=2.31.0",
22
+ "rich>=13.0.0",
23
+ ]
24
+
25
+ [project.scripts]
26
+ coursera-transcripts = "coursera_transcripts.cli:main"
27
+
28
+ [build-system]
29
+ requires = ["setuptools>=68.0"]
30
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,85 @@
1
+ import time
2
+ import requests
3
+ from urllib.parse import urljoin
4
+
5
+ from rich.console import Console
6
+
7
+
8
+ COURSERA_BASE = "https://www.coursera.org"
9
+ COURSERA_VERSION = "e184c443bbe09b70cbcebf2ba22b3b1067d7e119"
10
+
11
+ HEADERS = {
12
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0",
13
+ "Accept": "*/*",
14
+ "Accept-Language": "en",
15
+ "X-Coursera-Application": "ondemand",
16
+ "X-Coursera-Version": COURSERA_VERSION,
17
+ "X-Requested-With": "XMLHttpRequest",
18
+ }
19
+
20
+
21
+ def _build_headers(cookie: str, referer: str | None = None) -> dict:
22
+ headers = HEADERS.copy()
23
+ headers["Cookie"] = cookie
24
+ if referer:
25
+ headers["Referer"] = referer
26
+ return headers
27
+
28
+
29
+ class CourseAPI:
30
+ def __init__(self, cookie: str, console: Console | None = None):
31
+ self.cookie = cookie
32
+ self.session = requests.Session()
33
+ self.console = console or Console()
34
+
35
+ def _get(self, url: str, referer: str | None = None, max_retries: int = 3) -> requests.Response:
36
+ headers = _build_headers(self.cookie, referer)
37
+ last_exception: Exception | None = None
38
+ for attempt in range(max_retries):
39
+ try:
40
+ response = self.session.get(url, headers=headers, timeout=30)
41
+ response.raise_for_status()
42
+ return response
43
+ except requests.exceptions.RequestException as e:
44
+ last_exception = e
45
+ if attempt == max_retries - 1:
46
+ break
47
+ wait = 2 ** attempt
48
+ self.console.print(
49
+ f" [warning]⟳ Request failed, retrying in {wait}s…[/warning] [muted]({e})[/muted]"
50
+ )
51
+ time.sleep(wait)
52
+ raise last_exception # type: ignore[misc]
53
+
54
+ def get_course_materials(self, slug: str) -> dict:
55
+ url = (
56
+ f"{COURSERA_BASE}/api/onDemandCourseMaterials.v2/"
57
+ f"?q=slug&slug={slug}"
58
+ f"&includes=modules%2Clessons%2CpassableItemGroups%2CpassableItemGroupChoices%2CpassableLessonElements%2Citems%2Ctracks%2CgradePolicy%2CgradingParameters%2CembeddedContentMapping"
59
+ f"&fields=moduleIds%2ConDemandCourseMaterialModules.v1(name%2Cslug%2Cdescription%2CtimeCommitment%2ClessonIds%2Coptional%2ClearningObjectives)%2ConDemandCourseMaterialLessons.v1(name%2Cslug%2CtimeCommitment%2CelementIds%2Coptional%2CtrackId)%2ConDemandCourseMaterialPassableItemGroups.v1(requiredPassedCount%2CpassableItemGroupChoiceIds%2CtrackId)%2ConDemandCourseMaterialPassableItemGroupChoices.v1(name%2Cdescription%2CitemIds)%2ConDemandCourseMaterialPassableLessonElements.v1(gradingWeight%2CisRequiredForPassing)%2ConDemandCourseMaterialItems.v2(name%2CoriginalName%2Cslug%2CtimeCommitment%2CcontentSummary%2CisLocked%2ClockableByItem%2CitemLockedReasonCode%2CtrackId%2ClockedStatus%2CitemLockSummary%2CcustomDisplayTypenameOverride)%2ConDemandCourseMaterialTracks.v1(passablesCount)%2ConDemandGradingParameters.v1(gradedAssignmentGroups)%2CcontentAtomRelations.v1(embeddedContentSourceCourseId%2CsubContainerId)"
60
+ f"&showLockedItems=true"
61
+ )
62
+ referer = f"{COURSERA_BASE}/learn/{slug}/home/module/1"
63
+ response = self._get(url, referer)
64
+ data = response.json()
65
+
66
+ if not data.get("elements"):
67
+ raise ValueError(f"Course '{slug}' not found or no data returned")
68
+
69
+ return data
70
+
71
+ def get_lecture_video(self, course_id: str, item_id: str) -> dict:
72
+ url = (
73
+ f"{COURSERA_BASE}/api/onDemandLectureVideos.v1/"
74
+ f"{course_id}~{item_id}"
75
+ f"?includes=video"
76
+ f"&fields=onDemandVideos.v1(sources%2Csubtitles%2CsubtitlesTxt%2CsubtitlesAssetTags%2CdubbedSources%2CdubbedSubtitlesVtt%2CaudioDescriptionVideoSources)"
77
+ f"%2CdisableSkippingForward%2CstartMs%2CendMs"
78
+ )
79
+ response = self._get(url)
80
+ return response.json()
81
+
82
+ def download_subtitle(self, relative_url: str) -> str:
83
+ url = urljoin(COURSERA_BASE, relative_url)
84
+ response = self._get(url)
85
+ return response.text
@@ -0,0 +1,200 @@
1
+ import argparse
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.prompt import Prompt
8
+ from rich.text import Text
9
+ from rich.theme import Theme
10
+
11
+ from .api import CourseAPI
12
+ from .downloader import TranscriptDownloader
13
+
14
+ # ── Custom theme ──────────────────────────────────────────────────────
15
+ custom_theme = Theme({
16
+ "brand": "bold bright_cyan",
17
+ "accent": "bright_magenta",
18
+ "success": "bold bright_green",
19
+ "warning": "bold yellow",
20
+ "error": "bold red",
21
+ "muted": "dim white",
22
+ "info": "bright_blue",
23
+ })
24
+
25
+ console = Console(theme=custom_theme)
26
+
27
+ BANNER = r"""[bright_cyan]
28
+ ██████╗ ██████╗ ██╗ ██╗██████╗ ███████╗███████╗██████╗ █████╗
29
+ ██╔════╝██╔═══██╗██║ ██║██╔══██╗██╔════╝██╔════╝██╔══██╗██╔══██╗
30
+ ██║ ██║ ██║██║ ██║██████╔╝███████╗█████╗ ██████╔╝███████║
31
+ ██║ ██║ ██║██║ ██║██╔══██╗╚════██║██╔══╝ ██╔══██╗██╔══██║
32
+ ╚██████╗╚██████╔╝╚██████╔╝██║ ██║███████║███████╗██║ ██║██║ ██║
33
+ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
34
+ [/bright_cyan]"""
35
+
36
+
37
+ def _show_banner() -> None:
38
+ console.print(BANNER)
39
+ subtitle = Text("Transcript Generator", style="bold bright_magenta")
40
+ subtitle.append(" • ", style="dim")
41
+ subtitle.append("v0.1.0", style="dim bright_cyan")
42
+ console.print(subtitle, justify="center")
43
+ console.print()
44
+
45
+
46
+ def _normalize_cookie(raw: str) -> str:
47
+ """Accept a raw CAUTH token, CAUTH=value, or a full Cookie header string."""
48
+ raw = raw.strip()
49
+ if "CAUTH=" in raw:
50
+ # Already contains the CAUTH key — treat as a full cookie string
51
+ return raw
52
+ # Bare token value — wrap it
53
+ return f"CAUTH={raw}"
54
+
55
+
56
+ def _prompt_cookie() -> str:
57
+ console.print(
58
+ Panel(
59
+ "[muted]Paste your [bold bright_cyan]Cookie[/bold bright_cyan] header or just the [bold bright_cyan]CAUTH[/bold bright_cyan] value.\n"
60
+ "You can copy the full Cookie header from DevTools → Network → any request → Headers.[/muted]",
61
+ title="[brand]🔑 Authentication[/brand]",
62
+ border_style="bright_cyan",
63
+ padding=(1, 2),
64
+ )
65
+ )
66
+ cookie = Prompt.ask("[bright_cyan] ›[/bright_cyan] [bold]Cookie / CAUTH[/bold]")
67
+ if not cookie.strip():
68
+ console.print("[error] ✖ Cookie cannot be empty.[/error]")
69
+ raise SystemExit(1)
70
+ return _normalize_cookie(cookie)
71
+
72
+
73
+ def _prompt_slug() -> str:
74
+ console.print()
75
+ console.print(
76
+ Panel(
77
+ "[muted]Enter the course slug from the URL.\n"
78
+ "Example: [bold bright_cyan]coursera.org/learn/[underline]unreal-engine-fundamentals[/underline][/bold bright_cyan][/muted]",
79
+ title="[brand]📚 Course[/brand]",
80
+ border_style="bright_cyan",
81
+ padding=(1, 2),
82
+ )
83
+ )
84
+ slug = Prompt.ask("[bright_cyan] ›[/bright_cyan] [bold]Course slug[/bold]")
85
+ if not slug.strip():
86
+ console.print("[error] ✖ Slug cannot be empty.[/error]")
87
+ raise SystemExit(1)
88
+ return slug.strip()
89
+
90
+
91
+ def _prompt_options() -> tuple[str, str, Path]:
92
+ console.print()
93
+ console.print(
94
+ Panel(
95
+ "[muted]Configure optional settings (press Enter for defaults).[/muted]",
96
+ title="[brand]⚙ Options[/brand]",
97
+ border_style="bright_cyan",
98
+ padding=(1, 2),
99
+ )
100
+ )
101
+ language = Prompt.ask(
102
+ "[bright_cyan] ›[/bright_cyan] [bold]Language[/bold]",
103
+ default="en",
104
+ )
105
+ fmt = Prompt.ask(
106
+ "[bright_cyan] ›[/bright_cyan] [bold]Format[/bold] [muted](srt/txt)[/muted]",
107
+ choices=["srt", "txt"],
108
+ default="txt",
109
+ )
110
+ output = Prompt.ask(
111
+ "[bright_cyan] ›[/bright_cyan] [bold]Output directory[/bold]",
112
+ default="./output",
113
+ )
114
+ return language, fmt, Path(output).resolve()
115
+
116
+
117
+ def parse_args():
118
+ parser = argparse.ArgumentParser(
119
+ description="Download transcripts/subtitles from a Coursera course",
120
+ )
121
+ parser.add_argument(
122
+ "--cookie", "-c",
123
+ help="Coursera authentication cookie (CAUTH value). If omitted, you will be prompted.",
124
+ )
125
+ parser.add_argument(
126
+ "--slug", "-s",
127
+ help="Course slug (e.g. 'unreal-engine-fundamentals'). If omitted, you will be prompted.",
128
+ )
129
+ parser.add_argument(
130
+ "--output", "-o",
131
+ default=None,
132
+ help="Parent output directory (default: ./output). Transcripts saved to {output}/{slug}/",
133
+ )
134
+ parser.add_argument(
135
+ "--language", "-l",
136
+ default=None,
137
+ help="Subtitle language code (default: en)",
138
+ )
139
+ parser.add_argument(
140
+ "--format",
141
+ choices=["srt", "txt"],
142
+ default=None,
143
+ help="Subtitle format (default: txt)",
144
+ )
145
+ return parser.parse_args()
146
+
147
+
148
+ def main():
149
+ args = parse_args()
150
+
151
+ # ── Interactive mode when cookie/slug not provided ────────────────
152
+ interactive = args.cookie is None or args.slug is None
153
+
154
+ if interactive:
155
+ _show_banner()
156
+
157
+ # Cookie
158
+ if args.cookie:
159
+ cookie = _normalize_cookie(args.cookie)
160
+ else:
161
+ cookie = _prompt_cookie()
162
+
163
+ # Slug
164
+ slug = args.slug if args.slug else _prompt_slug()
165
+
166
+ # Options
167
+ if interactive and (args.language is None and args.format is None and args.output is None):
168
+ language, fmt, output_dir = _prompt_options()
169
+ else:
170
+ language = args.language or "en"
171
+ fmt = args.format or "txt"
172
+ output_dir = Path(args.output or "./output").resolve()
173
+
174
+ console.print()
175
+
176
+ # ── Run ───────────────────────────────────────────────────────────
177
+ api = CourseAPI(cookie, console)
178
+ downloader = TranscriptDownloader(
179
+ api=api,
180
+ output_dir=output_dir,
181
+ language=language,
182
+ fmt=fmt,
183
+ console=console,
184
+ )
185
+
186
+ try:
187
+ downloader.fetch_all_transcripts(slug)
188
+ except ValueError as e:
189
+ console.print(f"\n[error] ✖ {e}[/error]")
190
+ raise SystemExit(1)
191
+ except KeyboardInterrupt:
192
+ console.print("\n[warning] ⚠ Interrupted by user.[/warning]")
193
+ raise SystemExit(130)
194
+ except Exception as e:
195
+ console.print(f"\n[error] ✖ Unexpected error: {e}[/error]")
196
+ raise SystemExit(1)
197
+
198
+
199
+ if __name__ == "__main__":
200
+ main()
@@ -0,0 +1,238 @@
1
+ import re
2
+ import time
3
+ from pathlib import Path
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.progress import (
8
+ Progress,
9
+ SpinnerColumn,
10
+ TextColumn,
11
+ BarColumn,
12
+ MofNCompleteColumn,
13
+ TimeElapsedColumn,
14
+ )
15
+ from rich.table import Table
16
+ from rich.text import Text
17
+ from rich.tree import Tree
18
+
19
+ from .api import CourseAPI
20
+
21
+
22
+ def _sanitize_filename(name: str) -> str:
23
+ name = re.sub(r'[<>:"/\\|?*]', '', name)
24
+ name = name.strip()
25
+ name = name[:200]
26
+ return name
27
+
28
+
29
+ def _extract_course_id(materials_data: dict) -> str:
30
+ return materials_data["elements"][0]["id"]
31
+
32
+
33
+ def _build_module_lookup(materials_data: dict) -> dict:
34
+ lookup = {}
35
+ for module in materials_data.get("linked", {}).get("onDemandCourseMaterialModules.v1", []):
36
+ lookup[module["id"]] = {
37
+ "name": module["name"],
38
+ "slug": module.get("slug", module["id"]),
39
+ }
40
+ return lookup
41
+
42
+
43
+ def _build_lesson_lookup(materials_data: dict) -> dict:
44
+ lookup = {}
45
+ for lesson in materials_data.get("linked", {}).get("onDemandCourseMaterialLessons.v1", []):
46
+ lookup[lesson["id"]] = {
47
+ "name": lesson["name"],
48
+ "slug": lesson.get("slug", lesson["id"]),
49
+ }
50
+ return lookup
51
+
52
+
53
+ def _get_lecture_items(materials_data: dict) -> list:
54
+ items = materials_data.get("linked", {}).get("onDemandCourseMaterialItems.v2", [])
55
+ lectures = []
56
+ for item in items:
57
+ content_type = item.get("contentSummary", {}).get("typeName", "")
58
+ if content_type != "lecture":
59
+ continue
60
+ if item.get("isLocked", False):
61
+ continue
62
+ lectures.append(item)
63
+ return lectures
64
+
65
+
66
+ class TranscriptDownloader:
67
+ def __init__(
68
+ self,
69
+ api: CourseAPI,
70
+ output_dir: Path,
71
+ language: str = "en",
72
+ fmt: str = "txt",
73
+ console: Console | None = None,
74
+ ):
75
+ self.api = api
76
+ self.output_dir = output_dir
77
+ self.language = language
78
+ self.fmt = fmt
79
+ self.console = console or Console()
80
+
81
+ def _get_subtitle_url(self, video_data: dict) -> str | None:
82
+ videos = video_data.get("linked", {}).get("onDemandVideos.v1", [])
83
+ if not videos:
84
+ return None
85
+
86
+ video = videos[0]
87
+
88
+ if self.fmt == "txt":
89
+ subtitles = video.get("subtitlesTxt", {})
90
+ else:
91
+ subtitles = video.get("subtitles", {})
92
+
93
+ return subtitles.get(self.language)
94
+
95
+ def fetch_all_transcripts(self, course_slug: str) -> dict:
96
+ c = self.console
97
+
98
+ # ── Fetch course data ─────────────────────────────────────────
99
+ with c.status("[bright_cyan] Fetching course materials…[/bright_cyan]", spinner="dots"):
100
+ materials = self.api.get_course_materials(course_slug)
101
+
102
+ course_id = _extract_course_id(materials)
103
+ module_lookup = _build_module_lookup(materials)
104
+ lesson_lookup = _build_lesson_lookup(materials)
105
+ lecture_items = _get_lecture_items(materials)
106
+
107
+ course_dir = self.output_dir / course_slug
108
+ course_dir.mkdir(parents=True, exist_ok=True)
109
+
110
+ # ── Course overview panel ─────────────────────────────────────
111
+ info_table = Table.grid(padding=(0, 2))
112
+ info_table.add_column(style="muted", justify="right")
113
+ info_table.add_column(style="bold white")
114
+ info_table.add_row("Course", course_slug)
115
+ info_table.add_row("Lectures", str(len(lecture_items)))
116
+ info_table.add_row("Language", self.language.upper())
117
+ info_table.add_row("Format", self.fmt.upper())
118
+ info_table.add_row("Output", str(course_dir))
119
+
120
+ c.print(
121
+ Panel(
122
+ info_table,
123
+ title="[brand]📋 Course Overview[/brand]",
124
+ border_style="bright_cyan",
125
+ padding=(1, 2),
126
+ )
127
+ )
128
+ c.print()
129
+
130
+ if not lecture_items:
131
+ c.print("[warning] ⚠ No lecture videos found in this course.[/warning]")
132
+ return {"success": 0, "skipped": 0, "failed": 0, "total": 0}
133
+
134
+ # ── Download with progress bar ────────────────────────────────
135
+ stats = {"success": 0, "skipped": 0, "failed": 0, "total": len(lecture_items)}
136
+ results: list[tuple[str, str, str]] = [] # (status_icon, name, detail)
137
+
138
+ with Progress(
139
+ SpinnerColumn(style="bright_cyan"),
140
+ TextColumn("[bold]{task.description}[/bold]"),
141
+ BarColumn(
142
+ bar_width=30,
143
+ style="dim white",
144
+ complete_style="bright_cyan",
145
+ finished_style="bright_green",
146
+ ),
147
+ MofNCompleteColumn(),
148
+ TextColumn("•"),
149
+ TimeElapsedColumn(),
150
+ console=c,
151
+ transient=False,
152
+ ) as progress:
153
+ task = progress.add_task(" Downloading transcripts", total=stats["total"])
154
+
155
+ for idx, item in enumerate(lecture_items, 1):
156
+ item_id = item["id"]
157
+ item_name = _sanitize_filename(item["name"])
158
+ module_id = item.get("moduleId", "")
159
+
160
+ module_info = module_lookup.get(module_id, {})
161
+ module_slug = module_info.get("slug", f"module-{module_id}")
162
+
163
+ module_dir = course_dir / module_slug
164
+ module_dir.mkdir(parents=True, exist_ok=True)
165
+
166
+ # Fetch video data
167
+ try:
168
+ video_data = self.api.get_lecture_video(course_id, item_id)
169
+ except Exception as e:
170
+ results.append(("❌", item_name, f"[error]Failed to fetch video data: {e}[/error]"))
171
+ stats["failed"] += 1
172
+ progress.advance(task)
173
+ continue
174
+
175
+ subtitle_url = self._get_subtitle_url(video_data)
176
+
177
+ if not subtitle_url:
178
+ results.append(("⊘", item_name, f"[warning]No {self.language} {self.fmt} subtitles[/warning]"))
179
+ stats["skipped"] += 1
180
+ progress.advance(task)
181
+ continue
182
+
183
+ # Download subtitle
184
+ try:
185
+ subtitle_text = self.api.download_subtitle(subtitle_url)
186
+ except Exception as e:
187
+ results.append(("❌", item_name, f"[error]Download failed: {e}[/error]"))
188
+ stats["failed"] += 1
189
+ progress.advance(task)
190
+ continue
191
+
192
+ filename = f"{item_name}.{self.fmt}"
193
+ filepath = module_dir / filename
194
+ filepath.write_text(subtitle_text, encoding="utf-8")
195
+
196
+ rel_path = filepath.relative_to(self.output_dir)
197
+ results.append(("✔", item_name, f"[muted]{rel_path}[/muted]"))
198
+ stats["success"] += 1
199
+ progress.advance(task)
200
+
201
+ # ── Results tree ──────────────────────────────────────────────
202
+ c.print()
203
+
204
+ tree = Tree("[bold bright_cyan]📂 Results[/bold bright_cyan]")
205
+ for icon, name, detail in results:
206
+ if icon == "✔":
207
+ style_icon = f"[bright_green]{icon}[/bright_green]"
208
+ elif icon == "⊘":
209
+ style_icon = f"[yellow]{icon}[/yellow]"
210
+ else:
211
+ style_icon = f"[red]{icon}[/red]"
212
+ tree.add(f"{style_icon} [bold]{name}[/bold] {detail}")
213
+ c.print(tree)
214
+
215
+ # ── Summary panel ─────────────────────────────────────────────
216
+ c.print()
217
+
218
+ summary_parts = []
219
+ if stats["success"]:
220
+ summary_parts.append(f"[bright_green]✔ {stats['success']} downloaded[/bright_green]")
221
+ if stats["skipped"]:
222
+ summary_parts.append(f"[yellow]⊘ {stats['skipped']} skipped[/yellow]")
223
+ if stats["failed"]:
224
+ summary_parts.append(f"[red]✖ {stats['failed']} failed[/red]")
225
+
226
+ summary_text = " ".join(summary_parts)
227
+ summary_text += f"\n\n[muted]Files saved to [bold]{course_dir}[/bold][/muted]"
228
+
229
+ c.print(
230
+ Panel(
231
+ summary_text,
232
+ title="[brand]✨ Summary[/brand]",
233
+ border_style="bright_green" if not stats["failed"] else "yellow",
234
+ padding=(1, 2),
235
+ )
236
+ )
237
+
238
+ return stats
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: coursera-transcripts
3
+ Version: 0.1.0
4
+ Summary: CLI tool to extract Coursera course transcripts/subtitles
5
+ Author-email: Kavin <your-email@example.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Environment :: Console
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: requests>=2.31.0
18
+ Requires-Dist: rich>=13.0.0
19
+ Dynamic: license-file
20
+
21
+ # 📚 Coursera Transcript Generator
22
+
23
+ A beautiful CLI tool to bulk-download transcripts and subtitles from any Coursera course you're enrolled in.
24
+
25
+ ![CLI Preview](./preview.png)
26
+
27
+ ![Python](https://img.shields.io/badge/Python-3.10+-3776AB?logo=python&logoColor=white)
28
+ ![License](https://img.shields.io/badge/License-MIT-brightgreen)
29
+
30
+ ---
31
+
32
+ ## ✨ Features
33
+
34
+ - **Interactive prompts** — guided step-by-step experience, no need to memorize flags
35
+ - **Bulk download** — grabs every lecture transcript in a course at once
36
+ - **Organized output** — files are neatly sorted into module folders
37
+ - **Progress tracking** — real-time progress bar with download status
38
+ - **Retry logic** — automatic retries with exponential backoff on failures
39
+ - **Multiple formats** — supports both `.txt` (plain text) and `.srt` (subtitle) formats
40
+ - **Multi-language** — download transcripts in any available language
41
+
42
+ ---
43
+
44
+ ## 📦 Installation
45
+
46
+ ```bash
47
+ # Clone the repo
48
+ git clone https://github.com/your-username/coursera-transcript-generator.git
49
+ cd coursera-transcript-generator
50
+
51
+ # Install in editable mode
52
+ pip install -e .
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 🚀 Usage
58
+
59
+ ### Interactive Mode (recommended)
60
+
61
+ Just run the command with no arguments — it will guide you through everything:
62
+
63
+ ```bash
64
+ coursera-transcripts
65
+ ```
66
+
67
+ You'll be prompted for:
68
+
69
+ 1. **CAUTH cookie** — your Coursera authentication token
70
+ 2. **Course slug** — the identifier from the course URL
71
+ 3. **Options** — language, format, and output directory
72
+
73
+ ### CLI Mode
74
+
75
+ Pass everything as flags for scripting / automation:
76
+
77
+ ```bash
78
+ coursera-transcripts \
79
+ --cookie "YOUR_CAUTH_VALUE" \
80
+ --slug "machine-learning" \
81
+ --language en \
82
+ --format txt \
83
+ --output ./transcripts
84
+ ```
85
+
86
+ ### All Options
87
+
88
+ | Flag | Short | Default | Description |
89
+ | ------------ | ----- | ------------ | ------------------------------ |
90
+ | `--cookie` | `-c` | _(prompted)_ | CAUTH cookie value |
91
+ | `--slug` | `-s` | _(prompted)_ | Course slug from URL |
92
+ | `--language` | `-l` | `en` | Subtitle language code |
93
+ | `--format` | | `txt` | Output format (`txt` or `srt`) |
94
+ | `--output` | `-o` | `./output` | Parent output directory |
95
+
96
+ ---
97
+
98
+ ## 🔑 Getting Your CAUTH Cookie
99
+
100
+ 1. Open [coursera.org](https://www.coursera.org) and **log in**
101
+ 2. Open **DevTools** (`F12` or `Ctrl+Shift+I`)
102
+ 3. Go to **Application** → **Cookies** → `https://www.coursera.org`
103
+ 4. Find the cookie named **`CAUTH`**
104
+ 5. Copy its **Value**
105
+
106
+ > [!IMPORTANT]
107
+ > You must be **enrolled** in the course to download its transcripts.
108
+
109
+ ---
110
+
111
+ ## 📁 Output Structure
112
+
113
+ Transcripts are organized by module:
114
+
115
+ ```
116
+ output/
117
+ └── machine-learning/
118
+ ├── introduction-to-ml/
119
+ │ ├── Welcome to Machine Learning.txt
120
+ │ ├── What is Machine Learning.txt
121
+ │ └── Supervised Learning.txt
122
+ ├── linear-regression/
123
+ │ ├── Model Representation.txt
124
+ │ └── Cost Function.txt
125
+ └── ...
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 🔧 Finding the Course Slug
131
+
132
+ The slug is the part of the URL after `/learn/`:
133
+
134
+ ```
135
+ https://www.coursera.org/learn/machine-learning
136
+ └── this is the slug
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 📋 Requirements
142
+
143
+ - Python **3.10+**
144
+ - A Coursera account with enrollment in the target course
145
+
146
+ ---
147
+
148
+ ## 📄 License
149
+
150
+ MIT
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/coursera_transcripts/__init__.py
5
+ src/coursera_transcripts/api.py
6
+ src/coursera_transcripts/cli.py
7
+ src/coursera_transcripts/downloader.py
8
+ src/coursera_transcripts.egg-info/PKG-INFO
9
+ src/coursera_transcripts.egg-info/SOURCES.txt
10
+ src/coursera_transcripts.egg-info/dependency_links.txt
11
+ src/coursera_transcripts.egg-info/entry_points.txt
12
+ src/coursera_transcripts.egg-info/requires.txt
13
+ src/coursera_transcripts.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ coursera-transcripts = coursera_transcripts.cli:main
@@ -0,0 +1,2 @@
1
+ requests>=2.31.0
2
+ rich>=13.0.0