msteams-transcripts 1.0.0__py3-none-any.whl
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.
- msteams_transcripts-1.0.0.dist-info/METADATA +315 -0
- msteams_transcripts-1.0.0.dist-info/RECORD +15 -0
- msteams_transcripts-1.0.0.dist-info/WHEEL +4 -0
- msteams_transcripts-1.0.0.dist-info/entry_points.txt +3 -0
- msteams_transcripts-1.0.0.dist-info/licenses/LICENSE +21 -0
- teams_transcripts/__init__.py +56 -0
- teams_transcripts/__main__.py +7 -0
- teams_transcripts/browser.py +73 -0
- teams_transcripts/cli.py +228 -0
- teams_transcripts/config.py +88 -0
- teams_transcripts/download.py +108 -0
- teams_transcripts/errors.py +10 -0
- teams_transcripts/formats.py +120 -0
- teams_transcripts/model.py +172 -0
- teams_transcripts/session.py +264 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: msteams-transcripts
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: List and download Microsoft Teams meeting transcripts from the command line, through your own signed-in browser session.
|
|
5
|
+
Project-URL: Homepage, https://github.com/divyavanmahajan/msteams-transcripts
|
|
6
|
+
Project-URL: Repository, https://github.com/divyavanmahajan/msteams-transcripts
|
|
7
|
+
Project-URL: Issues, https://github.com/divyavanmahajan/msteams-transcripts/issues
|
|
8
|
+
Author-email: Divya Van Mahajan <divyavanmahajan@users.noreply.github.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Divya Van Mahajan
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: cli,meeting,microsoft-teams,recap,sharepoint,transcript
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: MacOS
|
|
37
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
38
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
39
|
+
Classifier: Programming Language :: Python :: 3
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
44
|
+
Classifier: Topic :: Communications :: Conferencing
|
|
45
|
+
Classifier: Topic :: Utilities
|
|
46
|
+
Requires-Python: >=3.10
|
|
47
|
+
Requires-Dist: playwright>=1.40
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: hatch>=1.9; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
52
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
53
|
+
Description-Content-Type: text/markdown
|
|
54
|
+
|
|
55
|
+
# msteams-transcripts
|
|
56
|
+
|
|
57
|
+
Download Microsoft Teams meeting transcripts from the command line, including
|
|
58
|
+
the meetings where the Download button is greyed out.
|
|
59
|
+
|
|
60
|
+
It drives a browser you are already signed in to, so it needs no admin consent,
|
|
61
|
+
no app registration and no Graph permissions, and it reaches exactly the
|
|
62
|
+
meetings you can open yourself. Anything you can read in the Recap tab, this
|
|
63
|
+
can save to a file.
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
uvx msteams-transcripts browser
|
|
67
|
+
uvx msteams-transcripts list --from 2026-09-01 --to today
|
|
68
|
+
uvx msteams-transcripts get 3 --details
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Why the Download button being disabled does not matter
|
|
72
|
+
|
|
73
|
+
- **Listing meetings.** The Teams web client loads your calendar from its
|
|
74
|
+
middle-tier API. Each online meeting carries a thread identifier.
|
|
75
|
+
- **Finding the transcript.** The Recap tab asks the meeting-content service
|
|
76
|
+
about that thread. The reply lists resources; a transcript resource holds the
|
|
77
|
+
OneDrive drive, item and transcript identifiers.
|
|
78
|
+
- **Reading the transcript.** The recap page renders it from SharePoint, at
|
|
79
|
+
`/_api/v2.1/drives/<driveId>/items/<itemId>/media/transcripts/<id>/content`,
|
|
80
|
+
in JSON or WebVTT. That call is authenticated by your SharePoint cookies
|
|
81
|
+
alone. The whole transcript is already delivered to your browser in one
|
|
82
|
+
request, so no scrolling or scraping is involved. The disabled button is a
|
|
83
|
+
user-interface policy, not an access control.
|
|
84
|
+
- **Tokens.** The two Teams APIs want bearer tokens that the web client keeps
|
|
85
|
+
encrypted in local storage. The tool captures them from the requests the
|
|
86
|
+
Teams tab makes as it loads.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
With [uv](https://docs.astral.sh/uv/), nothing to install:
|
|
91
|
+
|
|
92
|
+
```powershell
|
|
93
|
+
uvx msteams-transcripts --help
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Or install it permanently:
|
|
97
|
+
|
|
98
|
+
```powershell
|
|
99
|
+
uv tool install msteams-transcripts # or: pipx install msteams-transcripts
|
|
100
|
+
pip install msteams-transcripts # into the current environment
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Both `msteams-transcripts` and the shorter `teams-transcripts` are installed as
|
|
104
|
+
commands. Python 3.10 or newer is required. Playwright comes as a dependency,
|
|
105
|
+
but **do not run `playwright install`**: the tool attaches to the Edge or Chrome
|
|
106
|
+
already on your machine and downloads no browsers of its own.
|
|
107
|
+
|
|
108
|
+
## Walkthrough
|
|
109
|
+
|
|
110
|
+
A first session from a clean machine, start to finish.
|
|
111
|
+
|
|
112
|
+
**1. Start the browser.**
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
uvx msteams-transcripts browser
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
An Edge window opens on Teams using a profile of its own. On a corporate,
|
|
119
|
+
Entra-joined PC it usually signs in by itself. If a sign-in page appears,
|
|
120
|
+
complete it there once. Leave the window open; you can minimise it. Recent Edge
|
|
121
|
+
and Chrome refuse remote debugging on your everyday profile, which is why a
|
|
122
|
+
separate one is used.
|
|
123
|
+
|
|
124
|
+
**2. See what is available.**
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
uvx msteams-transcripts list --from 2026-09-01 --to today
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
# Start (UTC) Subject Organizer
|
|
132
|
+
1 2026-09-15 05:41 Weekly one to one Priya Raman
|
|
133
|
+
2 2026-09-15 06:09 Platform team sync Tomas Lindqvist
|
|
134
|
+
3 2026-09-16 09:02 Architecture review: storage tiering Maya Okonkwo
|
|
135
|
+
12 transcript(s). Use: msteams-transcripts get <#>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The first run takes 30 to 60 seconds while Teams loads and issues its tokens;
|
|
139
|
+
later runs in the same browser session are quicker. Rows are transcripts rather
|
|
140
|
+
than meetings, so a recurring meeting recorded twice appears twice.
|
|
141
|
+
|
|
142
|
+
**3. Download one transcript with the meeting details.**
|
|
143
|
+
|
|
144
|
+
```powershell
|
|
145
|
+
uvx msteams-transcripts get 3 --details
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
This writes `transcripts\2026-09-16 0902 - Architecture review storage tiering.txt`:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
# Architecture review: storage tiering
|
|
152
|
+
# Date: 2026-09-16 09:02
|
|
153
|
+
# Source: https://contoso-my.sharepoint.com/personal/.../Recordings/Architecture-review....mp4
|
|
154
|
+
# Organizer: Maya Okonkwo <maya.okonkwo@example.com>
|
|
155
|
+
# Location: Microsoft Teams Meeting
|
|
156
|
+
# Invited (13):
|
|
157
|
+
# Priya Raman <priya.raman@example.com> (Required, Accepted)
|
|
158
|
+
# Tomas Lindqvist <tomas.lindqvist@example.com> (Required, Tentative)
|
|
159
|
+
# ...
|
|
160
|
+
# Speakers in transcript (5):
|
|
161
|
+
# Priya Raman (84 segments)
|
|
162
|
+
# Maya Okonkwo (72 segments)
|
|
163
|
+
# ...
|
|
164
|
+
# Files shared in meeting chat (1):
|
|
165
|
+
# capacity-plan.xlsx https://contoso.sharepoint.com/sites/...
|
|
166
|
+
# Invitation text:
|
|
167
|
+
# Agenda: ...
|
|
168
|
+
|
|
169
|
+
[00:00:03] Priya Raman:
|
|
170
|
+
Good morning everyone, let's start with the architecture review.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A sidecar `...meta.json` holds the same details as structured data: attendees
|
|
174
|
+
with response status, speakers with segment counts, shared files, invitation
|
|
175
|
+
text and the thread identifier.
|
|
176
|
+
|
|
177
|
+
**4. Download from a link instead.** Open the meeting in Teams, go to the Recap
|
|
178
|
+
tab, copy the address bar, and pass it:
|
|
179
|
+
|
|
180
|
+
```powershell
|
|
181
|
+
uvx msteams-transcripts get "https://teams.cloud.microsoft/l/meetingrecap?driveId=...&driveItemId=...&sitePath=..." --details -f all
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This path does not touch the calendar and finishes in a few seconds.
|
|
185
|
+
|
|
186
|
+
**5. Archive a whole period.**
|
|
187
|
+
|
|
188
|
+
```powershell
|
|
189
|
+
uvx msteams-transcripts batch --from 2026-09-01 --to 2026-09-30 --details -o C:\Archive\teams
|
|
190
|
+
uvx msteams-transcripts batch --from 2026-09-01 --to 2026-09-30 --only 3,7,12
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Files with the same name are overwritten. Batch ends with a count of what was
|
|
194
|
+
downloaded and what failed.
|
|
195
|
+
|
|
196
|
+
**6. Skipping the "Stay better connected with the Teams desktop app" popup**
|
|
197
|
+
when you open recap links yourself: change `/l/meetingrecap?` in the address to
|
|
198
|
+
`/_#/l/meetingrecap?`. That form opens the web client directly. The tool
|
|
199
|
+
already uses it internally.
|
|
200
|
+
|
|
201
|
+
## Commands
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
msteams-transcripts browser
|
|
205
|
+
msteams-transcripts list --from YYYY-MM-DD --to YYYY-MM-DD|today [--json]
|
|
206
|
+
msteams-transcripts get <row#|recap-url|thread-id> [-d] [-f txt|json|vtt|all] [-o DIR]
|
|
207
|
+
msteams-transcripts batch --from D --to D [-d] [-f FMT] [-o DIR] [--only 3,7,12]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| Option | Meaning |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `-d`, `--details` | organizer, location, invited attendees with response status, speakers, shared files, invitation text, plus a `.meta.json` sidecar |
|
|
213
|
+
| `-f`, `--format` | `txt` (default), `json` for the raw Stream document, `vtt` for WebVTT, or `all` |
|
|
214
|
+
| `-o`, `--out` | output directory, `./transcripts` by default |
|
|
215
|
+
| `--json` | on `list`, print the rows as JSON for scripting |
|
|
216
|
+
| `--only` | on `batch`, restrict to given row numbers |
|
|
217
|
+
| `--port`, `--profile`, `--browser` | override the debugging port, profile directory or browser executable |
|
|
218
|
+
|
|
219
|
+
The text format is:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
[00:00:03] Speaker Name:
|
|
223
|
+
what they said
|
|
224
|
+
the next sentence by the same speaker
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Timestamps are offsets from the start of the recording, not wall-clock time.
|
|
228
|
+
Row numbers used by `get N` come from the most recent `list`, cached in
|
|
229
|
+
`~/.teams_transcripts/last_list.json`.
|
|
230
|
+
|
|
231
|
+
## Using it from an agent
|
|
232
|
+
|
|
233
|
+
`.claude/skills/teams-transcripts/SKILL.md` describes the tool for Claude Code
|
|
234
|
+
or another agent: preconditions, commands, output format, typical flows and
|
|
235
|
+
error handling. Claude Code picks it up automatically when working in this
|
|
236
|
+
project.
|
|
237
|
+
|
|
238
|
+
## Library use
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
import asyncio
|
|
242
|
+
from pathlib import Path
|
|
243
|
+
from teams_transcripts import TeamsSession, download_ref, refs_from_recap_url
|
|
244
|
+
|
|
245
|
+
async def main():
|
|
246
|
+
async with TeamsSession() as s:
|
|
247
|
+
ref = refs_from_recap_url("https://teams.cloud.microsoft/l/meetingrecap?...")
|
|
248
|
+
await download_ref(s, ref, Path("out"), "txt", details=True)
|
|
249
|
+
|
|
250
|
+
asyncio.run(main())
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Anything the user can act on raises `TranscriptError`.
|
|
254
|
+
|
|
255
|
+
## Environment variables
|
|
256
|
+
|
|
257
|
+
| Variable | Default | Purpose |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `TT_CDP_PORT` | `9222` | browser debugging port |
|
|
260
|
+
| `TT_PROFILE_DIR` | platform application-data directory | browser profile |
|
|
261
|
+
| `TT_BROWSER_EXE` | Edge, then Chrome, from the usual locations | browser executable |
|
|
262
|
+
| `TT_STATE_DIR` | `~/.teams_transcripts` | where the last listing is cached |
|
|
263
|
+
|
|
264
|
+
## Troubleshooting
|
|
265
|
+
|
|
266
|
+
- **"The browser did not expose its debugging port"**: another window is
|
|
267
|
+
already using the dedicated profile without the port. Close it, then run
|
|
268
|
+
`browser` again.
|
|
269
|
+
- **"Could not capture a Teams token"**: the Teams tab in that window is not
|
|
270
|
+
signed in. Sign in there, then retry.
|
|
271
|
+
- **"Could not capture the meeting-content token"**: open any meeting's Recap
|
|
272
|
+
tab in that window, then retry.
|
|
273
|
+
- **HTTP 403 when listing or downloading**: the recording belongs to a tenant
|
|
274
|
+
or site you cannot read. The Recap tab would be empty for you too.
|
|
275
|
+
- **Not available through this tool**: the attendance report of who actually
|
|
276
|
+
joined and when, collaborative Loop notes, and Copilot AI notes. `--details`
|
|
277
|
+
gives the invited list from the calendar and who actually spoke per the
|
|
278
|
+
transcript.
|
|
279
|
+
- **Recurring meetings** share one thread identifier. Each occurrence's
|
|
280
|
+
transcript is listed separately with its own start time.
|
|
281
|
+
|
|
282
|
+
## Development
|
|
283
|
+
|
|
284
|
+
```powershell
|
|
285
|
+
git clone https://github.com/divyavanmahajan/msteams-transcripts
|
|
286
|
+
cd msteams-transcripts
|
|
287
|
+
pip install -e ".[dev]"
|
|
288
|
+
python -m pytest
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The tests cover the pure functions in `model.py` and `formats.py` and need
|
|
292
|
+
neither a browser nor the network.
|
|
293
|
+
|
|
294
|
+
Hatchling owns the version, read from `__version__` in
|
|
295
|
+
`src/teams_transcripts/__init__.py`. To release:
|
|
296
|
+
|
|
297
|
+
```powershell
|
|
298
|
+
hatch version patch # or minor, major, or an explicit 1.2.3
|
|
299
|
+
git commit -am "Release 0.1.1"
|
|
300
|
+
git tag v0.1.1
|
|
301
|
+
git push --follow-tags
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Pushing the tag runs `.github/workflows/publish.yml`, which refuses to continue
|
|
305
|
+
if the tag and the package version disagree, then builds, publishes to PyPI
|
|
306
|
+
through trusted publishing and creates a GitHub release. Trusted publishing is
|
|
307
|
+
configured once on PyPI under Publishing, naming this repository, the
|
|
308
|
+
`publish.yml` workflow and the `pypi` environment; no API token is stored.
|
|
309
|
+
Running the workflow by hand offers TestPyPI as the target.
|
|
310
|
+
|
|
311
|
+
## Licence
|
|
312
|
+
|
|
313
|
+
MIT. Transcripts are meeting content belonging to the people in them; this tool
|
|
314
|
+
only fetches what you already have access to, and what you do with it afterwards
|
|
315
|
+
is your responsibility.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
teams_transcripts/__init__.py,sha256=9w-YZn2HIpmtNwEXfmGzmYT4S-u3R4z7yPErrEnIbKc,1611
|
|
2
|
+
teams_transcripts/__main__.py,sha256=1Bj6t22eNhGRqEMkIGpEG6N5GvoAog7gkkWhBJQA0Cc,140
|
|
3
|
+
teams_transcripts/browser.py,sha256=fDrtNxNBxWeCCjmOE8QEj9vdss5zGb8dHe59Igr1iko,2472
|
|
4
|
+
teams_transcripts/cli.py,sha256=HPMQiAHwLiLNAHPyaECAbenfdUM-vme1prBUSOZIzz0,10009
|
|
5
|
+
teams_transcripts/config.py,sha256=QNvP88gJxaQt7j6I9AM_gj9Nemp6zIJmeyfWkny2xuI,3053
|
|
6
|
+
teams_transcripts/download.py,sha256=PWMU4OotAiM8477MGJutv4x4XvlDNpPtJg5w0XwtDs4,4662
|
|
7
|
+
teams_transcripts/errors.py,sha256=3zR5u_bJvbaKeXgswcKT_usEnLXHQwkvqGFUNSQuVD0,319
|
|
8
|
+
teams_transcripts/formats.py,sha256=Pg8ghbdtACdo9BgnEq7MCX_cF2UfrDLQaft-4NdbaVo,4639
|
|
9
|
+
teams_transcripts/model.py,sha256=_TrDSGZDL2EnkXUSvRPViEVljdKWBdBkBo-KCt-esvY,6406
|
|
10
|
+
teams_transcripts/session.py,sha256=nR3QNyTWYhDcs7tG2xlJjm8CchDYBZcNH5dlavfBzo4,11707
|
|
11
|
+
msteams_transcripts-1.0.0.dist-info/METADATA,sha256=AXWuvDrgUwbypzDkhr2YMqAzGnXhroIQSuZOp3VzHX4,12651
|
|
12
|
+
msteams_transcripts-1.0.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
13
|
+
msteams_transcripts-1.0.0.dist-info/entry_points.txt,sha256=Sg1pid83ofHGYhiUGyWDU1cUQShNuzqKR8-OdK1LwDU,114
|
|
14
|
+
msteams_transcripts-1.0.0.dist-info/licenses/LICENSE,sha256=rDxdB5fQvtRwfrjshCLNfl64Tp1Vu4RuVp1YsiwsuIw,1074
|
|
15
|
+
msteams_transcripts-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Divya Van Mahajan
|
|
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,56 @@
|
|
|
1
|
+
"""List and download Microsoft Teams meeting transcripts through a signed-in browser.
|
|
2
|
+
|
|
3
|
+
The Teams web client already receives the whole transcript when you open a meeting
|
|
4
|
+
Recap tab; the Download button is only a UI policy. This package attaches to a
|
|
5
|
+
browser you are signed in to and replays the same requests, so it sees exactly what
|
|
6
|
+
you can see and nothing more.
|
|
7
|
+
|
|
8
|
+
Library use::
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from teams_transcripts import TeamsSession, download_ref, refs_from_recap_url
|
|
12
|
+
|
|
13
|
+
async def main():
|
|
14
|
+
async with TeamsSession() as s:
|
|
15
|
+
ref = refs_from_recap_url("https://teams.cloud.microsoft/l/meetingrecap?...")
|
|
16
|
+
await download_ref(s, ref, Path("out"), "txt", details=True)
|
|
17
|
+
|
|
18
|
+
asyncio.run(main())
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
__version__ = "1.0.0"
|
|
23
|
+
|
|
24
|
+
from .errors import TranscriptError
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"__version__",
|
|
28
|
+
"TranscriptError",
|
|
29
|
+
"TeamsSession",
|
|
30
|
+
"download_ref",
|
|
31
|
+
"refs_from_recap_url",
|
|
32
|
+
"transcript_refs_from_contents",
|
|
33
|
+
"transcript_to_text",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
_LAZY = {
|
|
37
|
+
"TeamsSession": ".session",
|
|
38
|
+
"download_ref": ".download",
|
|
39
|
+
"refs_from_recap_url": ".model",
|
|
40
|
+
"transcript_refs_from_contents": ".model",
|
|
41
|
+
"transcript_to_text": ".formats",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __getattr__(name: str):
|
|
46
|
+
"""Import the browser-driving parts only when they are actually used."""
|
|
47
|
+
module = _LAZY.get(name)
|
|
48
|
+
if module is None:
|
|
49
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
50
|
+
from importlib import import_module
|
|
51
|
+
|
|
52
|
+
return getattr(import_module(module, __name__), name)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def __dir__() -> list[str]:
|
|
56
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Starting and finding the dedicated browser instance.
|
|
2
|
+
|
|
3
|
+
Recent Edge and Chrome refuse remote debugging on the user's normal profile, so
|
|
4
|
+
this always uses a separate profile directory. The user signs in there once;
|
|
5
|
+
corporate single sign-on usually does it without a prompt.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
import urllib.request
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .config import SETTINGS, TEAMS_URL, browser_candidates
|
|
17
|
+
from .errors import TranscriptError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def log(msg: str) -> None:
|
|
21
|
+
"""Progress goes to stderr so that stdout stays machine-readable."""
|
|
22
|
+
print(msg, file=sys.stderr, flush=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cdp_alive() -> bool:
|
|
26
|
+
try:
|
|
27
|
+
with urllib.request.urlopen(f"{SETTINGS.cdp_url}/json/version", timeout=2) as r:
|
|
28
|
+
return r.status == 200
|
|
29
|
+
except Exception:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def find_browser() -> str:
|
|
34
|
+
exe = next((p for p in browser_candidates(SETTINGS.browser_exe) if Path(p).exists()), None)
|
|
35
|
+
if not exe:
|
|
36
|
+
raise TranscriptError(
|
|
37
|
+
"No Edge or Chrome executable found. Set TT_BROWSER_EXE or pass --browser with the full path."
|
|
38
|
+
)
|
|
39
|
+
return exe
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ensure_browser(wait_seconds: int = 30) -> None:
|
|
43
|
+
"""Start the dedicated browser if it is not already listening on the debug port."""
|
|
44
|
+
if cdp_alive():
|
|
45
|
+
return
|
|
46
|
+
exe = find_browser()
|
|
47
|
+
SETTINGS.profile_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
log(f"Starting {Path(exe).name} with profile {SETTINGS.profile_dir} on port {SETTINGS.cdp_port} ...")
|
|
49
|
+
kwargs: dict = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "close_fds": True}
|
|
50
|
+
if os.name == "nt":
|
|
51
|
+
kwargs["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
52
|
+
else:
|
|
53
|
+
kwargs["start_new_session"] = True
|
|
54
|
+
subprocess.Popen(
|
|
55
|
+
[
|
|
56
|
+
exe,
|
|
57
|
+
f"--user-data-dir={SETTINGS.profile_dir}",
|
|
58
|
+
f"--remote-debugging-port={SETTINGS.cdp_port}",
|
|
59
|
+
"--no-first-run",
|
|
60
|
+
"--no-default-browser-check",
|
|
61
|
+
TEAMS_URL,
|
|
62
|
+
],
|
|
63
|
+
**kwargs,
|
|
64
|
+
)
|
|
65
|
+
for _ in range(wait_seconds):
|
|
66
|
+
time.sleep(1)
|
|
67
|
+
if cdp_alive():
|
|
68
|
+
log("Browser is up. If Teams asks you to sign in, do it in that window.")
|
|
69
|
+
return
|
|
70
|
+
raise TranscriptError(
|
|
71
|
+
"The browser did not expose its debugging port. "
|
|
72
|
+
f"Close any window already using the profile at {SETTINGS.profile_dir}, then try again."
|
|
73
|
+
)
|