mirror of
https://cdm-project.com/Download-Tools/udemy-downloader.git
synced 2025-04-30 12:54:25 +02:00
Compare commits
268 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
71ecdb6a47 | ||
![]() |
3e79463330 | ||
![]() |
2d6a3020aa | ||
![]() |
745ea117de | ||
![]() |
d19a018653 | ||
![]() |
d123403434 | ||
![]() |
4b80e32433 | ||
![]() |
c60f4e44a1 | ||
![]() |
db3ab180a4 | ||
![]() |
c7329b1d59 | ||
![]() |
2de870c009 | ||
![]() |
904cc41543 | ||
![]() |
1c0bca935e | ||
![]() |
ea59b43d4e | ||
![]() |
f688a5f263 | ||
![]() |
039816f695 | ||
![]() |
1b1b5d81bd | ||
![]() |
db7b0490e6 | ||
![]() |
b50dbd1ee2 | ||
![]() |
c8088cc081 | ||
![]() |
c21243554a | ||
![]() |
4880f7bfef | ||
![]() |
5d01e56756 | ||
![]() |
ef9d2a6be3 | ||
![]() |
7f522ebebb | ||
![]() |
72473188df | ||
![]() |
1d30c30f71 | ||
![]() |
4da85c0aff | ||
![]() |
c1967625cf | ||
![]() |
e4bbd47cdb | ||
![]() |
71101bad72 | ||
![]() |
fa444c6960 | ||
![]() |
3d44811616 | ||
![]() |
0ab393afb1 | ||
![]() |
47cf0cdfd8 | ||
![]() |
06bfa95a04 | ||
![]() |
375be73e0a | ||
![]() |
79ea132962 | ||
![]() |
935b2a41f3 | ||
![]() |
a461f5fc86 | ||
![]() |
68e4ffb787 | ||
![]() |
eb7189178d | ||
![]() |
35673f91e0 | ||
![]() |
875d888503 | ||
![]() |
8ab48230ed | ||
![]() |
271a426a8c | ||
![]() |
3a13ba5a54 | ||
![]() |
23e7e94f16 | ||
![]() |
f4f472de81 | ||
![]() |
84eb17b793 | ||
![]() |
43f6085e91 | ||
![]() |
f9634168d4 | ||
![]() |
b5741b2373 | ||
![]() |
fdf8cde414 | ||
![]() |
e5450b6f85 | ||
![]() |
e9b9d8a6a4 | ||
![]() |
45b6c621f9 | ||
![]() |
bc9ff0ba18 | ||
![]() |
88edbdf538 | ||
![]() |
7621d078da | ||
![]() |
62a924de3f | ||
![]() |
14095b8e72 | ||
![]() |
1433962f95 | ||
![]() |
4aaed934a0 | ||
![]() |
a76f14190a | ||
![]() |
e77bd8a959 | ||
![]() |
06e295d2b6 | ||
![]() |
340d4c6786 | ||
![]() |
f7cf66931c | ||
![]() |
b08f2569cb | ||
![]() |
d4a4ea1b17 | ||
![]() |
6570f7c45f | ||
![]() |
1ee5d79664 | ||
![]() |
4886098691 | ||
![]() |
f829d0fbce | ||
![]() |
cb906d5eaf | ||
![]() |
f321791819 | ||
![]() |
d8b5d3ca0e | ||
![]() |
705de30925 | ||
![]() |
18a9c364af | ||
![]() |
6355a3dcbc | ||
![]() |
1da41a9bde | ||
![]() |
6f9919ab9d | ||
![]() |
962904abd6 | ||
![]() |
8ad8faaca0 | ||
![]() |
85610e2fca | ||
![]() |
91bf5c3209 | ||
![]() |
502ac0e4fe | ||
![]() |
2ce865a81b | ||
![]() |
e5398a1333 | ||
![]() |
f824487d78 | ||
![]() |
3478095bb5 | ||
![]() |
07bfb9163b | ||
![]() |
885d920fba | ||
![]() |
e5d5285bf7 | ||
![]() |
c22fbfccaa | ||
![]() |
d8b711af89 | ||
![]() |
ffae516179 | ||
![]() |
1bdc581c65 | ||
![]() |
4326c4743a | ||
![]() |
13bc68e905 | ||
![]() |
007e5ea60f | ||
![]() |
ea37e8a397 | ||
![]() |
884be3b68a | ||
![]() |
b97b12344b | ||
![]() |
046344796c | ||
![]() |
e20699549e | ||
![]() |
52e3cd5b36 | ||
![]() |
f8fab9fec9 | ||
![]() |
bb11640ed7 | ||
![]() |
3176ac1191 | ||
![]() |
f3b1c74f13 | ||
![]() |
4bdbf68f58 | ||
![]() |
72a354ed69 | ||
![]() |
aeca63d671 | ||
![]() |
bc9f6ecb1a | ||
![]() |
68f5172e30 | ||
![]() |
8756bfc266 | ||
![]() |
ca34f90996 | ||
![]() |
51df2758b8 | ||
![]() |
28b2cc8e7d | ||
![]() |
d519d62c60 | ||
![]() |
666c5c2122 | ||
![]() |
3c0a95a0a1 | ||
![]() |
317eb7c6b1 | ||
![]() |
1f79fb7430 | ||
![]() |
5966b1662e | ||
![]() |
de1419e711 | ||
![]() |
064cbb028b | ||
![]() |
adfb204213 | ||
![]() |
b516c9a83a | ||
![]() |
b26c8f4b5b | ||
![]() |
8fb8d9f59d | ||
![]() |
60eee56233 | ||
![]() |
152e6e1fd1 | ||
![]() |
39041ff122 | ||
![]() |
7c508a0762 | ||
![]() |
b98f35cec6 | ||
![]() |
cd15ab76d9 | ||
![]() |
0719800145 | ||
![]() |
59538b24ce | ||
![]() |
95b30841dc | ||
![]() |
97ca2cf401 | ||
![]() |
a06ef516ad | ||
![]() |
8ba33270ce | ||
![]() |
eb3257f374 | ||
![]() |
ecc46deb6b | ||
![]() |
f6918e497c | ||
![]() |
2c2e0a5c23 | ||
![]() |
4ff903b247 | ||
![]() |
1dffcd24b0 | ||
![]() |
f4e26ff84b | ||
![]() |
69b44b9421 | ||
![]() |
5fff05c0d0 | ||
![]() |
58e0179d8d | ||
![]() |
1f33e28f5e | ||
![]() |
a2748d98a4 | ||
![]() |
bb60297821 | ||
![]() |
9a51347dfa | ||
![]() |
d47a3524dd | ||
![]() |
480173a462 | ||
![]() |
e835ab6eb1 | ||
![]() |
034fcc6b50 | ||
![]() |
778d8e6f56 | ||
![]() |
9f1d7d9119 | ||
![]() |
ca40ff2b6d | ||
![]() |
6137e44d76 | ||
![]() |
5ec615e4e3 | ||
![]() |
5b592923dc | ||
![]() |
1f72e875f5 | ||
![]() |
c8945b8091 | ||
![]() |
845c0bde58 | ||
![]() |
6d4d93c9d8 | ||
![]() |
52e4613add | ||
![]() |
400316e1b3 | ||
![]() |
96578c2327 | ||
![]() |
e6703e1106 | ||
![]() |
efb72b3d66 | ||
![]() |
171f7b7719 | ||
![]() |
59b6419ade | ||
![]() |
63fb7b4486 | ||
![]() |
6d0d3edf78 | ||
![]() |
4c0f4d2225 | ||
![]() |
d9a72c8878 | ||
![]() |
a596571694 | ||
![]() |
d6194589be | ||
![]() |
d7df8b18b5 | ||
![]() |
2b3fb655ca | ||
![]() |
26eac17f46 | ||
![]() |
a7c02ca6ac | ||
![]() |
bdf6357218 | ||
![]() |
3266b2a70c | ||
![]() |
4ac49ec30d | ||
![]() |
d7bfdda82e | ||
![]() |
1d43d19a47 | ||
![]() |
1d51b9be2f | ||
![]() |
31e3802cb1 | ||
![]() |
108d3bd19a | ||
![]() |
2afef1cb41 | ||
![]() |
60addf51d9 | ||
![]() |
56e719b59f | ||
![]() |
a205ec91bf | ||
![]() |
7a77a528aa | ||
![]() |
6a850d52f2 | ||
![]() |
5b548c737b | ||
![]() |
c90f9c7584 | ||
![]() |
9f3bda6c6c | ||
![]() |
e6dcde0335 | ||
![]() |
1ad4f1edde | ||
![]() |
66aad0dc50 | ||
![]() |
1fa5bdba90 | ||
![]() |
6a2f237969 | ||
![]() |
4d428ea89d | ||
![]() |
ee7be61f6a | ||
![]() |
f6ea730215 | ||
![]() |
88c32ea55d | ||
![]() |
cffbcbaa0a | ||
![]() |
758f78831b | ||
![]() |
354b85e142 | ||
![]() |
d20f15fb6a | ||
![]() |
05c6c84d55 | ||
![]() |
af8b565e23 | ||
![]() |
f8e0c790cc | ||
![]() |
10b22a6e0b | ||
![]() |
56a1994443 | ||
![]() |
3cc22520c8 | ||
![]() |
67748301de | ||
![]() |
6c2690b856 | ||
![]() |
f8521fd84c | ||
![]() |
5ffef4736e | ||
![]() |
f0e06106fc | ||
![]() |
b7b27419fd | ||
![]() |
b667420dc2 | ||
![]() |
2667629c93 | ||
![]() |
b496507e0b | ||
![]() |
a236156a4d | ||
![]() |
5af8a95925 | ||
![]() |
840a6f6815 | ||
![]() |
134652d6e6 | ||
![]() |
86fa241ded | ||
![]() |
50fb9534d8 | ||
![]() |
1471f58ef7 | ||
![]() |
65db666706 | ||
![]() |
7f399c71dd | ||
![]() |
cb98f57bd0 | ||
![]() |
9a1a318f93 | ||
![]() |
aab19bf66f | ||
![]() |
88a411d708 | ||
![]() |
f62bb52816 | ||
![]() |
0782c42df7 | ||
![]() |
5dc9211fa0 | ||
![]() |
6edc83d606 | ||
![]() |
37b210b7da | ||
![]() |
b8b79b2293 | ||
![]() |
ec3ba980b1 | ||
![]() |
e5247edbd8 | ||
![]() |
1a7314b62f | ||
![]() |
c88414189b | ||
![]() |
e722eb3fbc | ||
![]() |
e383ca945d | ||
![]() |
4803f46164 | ||
![]() |
048fbe09bf | ||
![]() |
77e57e324d | ||
![]() |
5667c3df13 | ||
![]() |
0f08002275 | ||
![]() |
d011533a6a | ||
![]() |
6c5b7870a9 | ||
![]() |
a867f82f2b |
@ -1,2 +1,3 @@
|
|||||||
UDEMY_BEARER=enter bearer token without the Bearer prefix
|
UDEMY_BEARER=Your bearer token here
|
||||||
UDEMY_COURSE_ID=course id goes here
|
# For docker compose only
|
||||||
|
COURSE_URL=Your course url here
|
||||||
|
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
github: Puyodead1
|
||||||
|
ko_fi: puyodead1
|
||||||
|
patreon: Puyodead1
|
60
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: File a bug report
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees:
|
||||||
|
- Puyodead1
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: Describe with as much detail as you can exactly what happened
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected-result
|
||||||
|
attributes:
|
||||||
|
label: Expected Result
|
||||||
|
description: What do you expect to happen if the bug didn't occur?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: branch
|
||||||
|
attributes:
|
||||||
|
label: Branch
|
||||||
|
description: What branch are you using?
|
||||||
|
options:
|
||||||
|
- master/main
|
||||||
|
- feat-shaka
|
||||||
|
- feat-selenium
|
||||||
|
- develop
|
||||||
|
- native-ytdlp-downloader
|
||||||
|
- Other (Enter below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: What operating systems are you seeing the problem on?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- Linux/Unix
|
||||||
|
- MacOS
|
||||||
|
- Other (Enter Below)
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant log output
|
||||||
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Remember to censor sensitive information before submitting!
|
||||||
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
id: other-information
|
||||||
|
attributes:
|
||||||
|
label: Other information
|
||||||
|
description: Enter other information here such as an unlisted OS or branch
|
44
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
title: "[Feature Request]: "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
assignees:
|
||||||
|
- Puyodead1
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: is-related-to-problem
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a problem?
|
||||||
|
options:
|
||||||
|
- label: "Yes"
|
||||||
|
- label: "No"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: If yes, please describe
|
||||||
|
placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
placeholder: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
placeholder: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
placeholder: Add any other context or screenshots about the feature request here.
|
||||||
|
validations:
|
||||||
|
required: false
|
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
19
.gitignore
vendored
19
.gitignore
vendored
@ -112,10 +112,23 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
*.mp4
|
*.mp4
|
||||||
keyfile.json
|
keyfile.json
|
||||||
.env
|
|
||||||
test_data.json
|
test_data.json
|
||||||
out_dir
|
out_dir/
|
||||||
working_dir
|
working_dir/
|
||||||
manifest.mpd
|
manifest.mpd
|
||||||
|
.vscode
|
||||||
|
saved/
|
||||||
|
*.aria2
|
||||||
|
info.py
|
||||||
|
.idea/
|
||||||
|
cookies.txt
|
||||||
|
selenium_test.py
|
||||||
|
selenium_data/
|
||||||
|
config.dev.toml
|
||||||
|
temp/
|
||||||
|
*.exe
|
||||||
|
# Docker Compose output volume
|
||||||
|
output/
|
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Use an official Python runtime as a parent image
|
||||||
|
FROM python:3.12-slim-bullseye
|
||||||
|
|
||||||
|
# Set the working directory in the container to /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install necessary packages.
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ffmpeg \
|
||||||
|
aria2 \
|
||||||
|
curl \
|
||||||
|
unzip \
|
||||||
|
wget
|
||||||
|
|
||||||
|
# Install Shaka Packager.
|
||||||
|
RUN wget https://github.com/shaka-project/shaka-packager/releases/download/v3.2.0/packager-linux-x64 -O /usr/local/bin/shaka-packager
|
||||||
|
RUN chmod +x /usr/local/bin/shaka-packager
|
||||||
|
|
||||||
|
# Copy the current directory contents into the container at /app.
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# Install Python application dependencies.
|
||||||
|
RUN pip install -r requirements.txt
|
243
README.md
243
README.md
@ -1,79 +1,226 @@
|
|||||||
# Udemy Downloader with DRM support
|
# Udemy Downloader with DRM support
|
||||||
|
|
||||||
|
[](https://forthebadge.com)
|
||||||
|
[](https://forthebadge.com)
|
||||||
|
[](https://forthebadge.com)
|
||||||
|
[](https://forthebadge.com)
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
# NOTE
|
# NOTE
|
||||||
|
|
||||||
This program is WIP, the code is provided as-is and i am not held resposible for any legal repercussions resulting from the use of this program.
|
- **This tool will not work without decryption keys. Do not bother installing unless you already have keys or can obtain them!**
|
||||||
|
- If you ask about keys in the issues, your message will be deleted and you will be blocked.
|
||||||
# Support
|
- **Downloading courses is against Udemy's Terms of Service, I am NOT held responsible for your account getting suspended as a result from the use of this program!**
|
||||||
|
- This program is WIP, the code is provided as-is and I am not held resposible for any legal issues resulting from the use of this program.
|
||||||
if you want help using the program, join [my discord server](https://discord.gg/5B3XVb4RRX) or use [github issues](https://github.com/Puyodead1/udemy-downloader/issues)
|
|
||||||
|
|
||||||
# License
|
|
||||||
|
|
||||||
All code is licensed under the MIT license
|
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
|
||||||
Simple and hacky program to download a udemy course, has support for DRM videos but requires the user to aquire the decryption key (for legal reasons).
|
Utility script to download Udemy courses, has support for DRM videos but requires the user to acquire the decryption key (for legal reasons).<br>
|
||||||
|
Windows is the primary development OS, but I've made an effort to support Linux also (Mac untested).
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> The ability to download captions automatically is currently broken due to changes in Udemy's API!
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This tool will not work on encrypted courses without decryption keys being provided!
|
||||||
|
>
|
||||||
|
> Downloading courses is against Udemy's Terms of Service, I am NOT held responsible for your account getting suspended as a result from the use of this program!
|
||||||
|
>
|
||||||
|
> This program is WIP, the code is provided as-is and I am not held resposible for any legal issues resulting from the use of this program.
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
1. You would need to download ffmpeg and mp4decrypter from Bento4 SDK and ensure they are in path(typing their name in cmd invokes them).
|
The following are a list of required third-party tools, you will need to ensure they are in your systems path and that typing their name in a terminal invokes them.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> These are seperate requirements that are not installed with the pip command!
|
||||||
|
>
|
||||||
|
> You will need to download and install these manually!
|
||||||
|
|
||||||
|
- [Python 3](https://python.org/)
|
||||||
|
- [ffmpeg](https://www.ffmpeg.org/) - This tool is also available in Linux package repositories.
|
||||||
|
- NOTE: It is recommended to use a custom build from the yt-dlp team that contains various patches for issues when used alongside yt-dlp, however it is not required. Latest builds can be found [here](https://github.com/yt-dlp/FFmpeg-Builds/releases/tag/latest)
|
||||||
|
- [aria2/aria2c](https://github.com/aria2/aria2/) - This tool is also available in Linux package repositories
|
||||||
|
- [shaka-packager](https://github.com/shaka-project/shaka-packager/releases/latest)
|
||||||
|
- [yt-dlp](https://github.com/yt-dlp/yt-dlp/) - This tool is also available in Linux package repositories, but can also be installed using pip if desired (`pip install yt-dlp`)
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
_quick and dirty how-to_
|
|
||||||
|
|
||||||
You will need to get a few things before you can use this program:
|
You will need to get a few things before you can use this program:
|
||||||
|
|
||||||
- Decryption Key ID
|
- Decryption Key ID
|
||||||
- Decryption Key
|
- Decryption Key
|
||||||
- Udemy Course ID
|
- Udemy Course URL
|
||||||
- Udemy Bearer Token
|
- Udemy Bearer Token (aka acccess token for udemy-dl users)
|
||||||
|
- Udemy cookies (only required for subscription plans - see [Udemy Subscription Plans](#udemy-subscription-plans))
|
||||||
|
|
||||||
### Setting up
|
## Setting up
|
||||||
|
|
||||||
- rename `.env.sample` to `.env`
|
- rename `.env.sample` to `.env` _(you only need to do this if you plan to use the .env file to store your bearer token)_
|
||||||
- rename `keyfile.example.json` to `keyfile.json`
|
- rename `keyfile.example.json` to `keyfile.json`
|
||||||
|
|
||||||
### Aquire bearer token
|
## Acquire Bearer Token
|
||||||
|
|
||||||
- open dev tools
|
- Firefox: [Udemy-DL Guide](https://github.com/r0oth3x49/udemy-dl/issues/389#issuecomment-491903900)
|
||||||
- go to network tab
|
- Chrome: [Udemy-DL Guide](https://github.com/r0oth3x49/udemy-dl/issues/389#issuecomment-492569372)
|
||||||
- in the search field, enter `api-2.0/courses`
|
- If you want to use the .env file to store your Bearer Token, edit the .env and add your token.
|
||||||
- 
|
|
||||||
- click a random request
|
|
||||||
- locate the `Request Headers` section
|
|
||||||
- copy the the text after `Authorization`, it should look like `Bearer xxxxxxxxxxx`
|
|
||||||
- 
|
|
||||||
- enter this in the `.env` file after `UDEMY_BEARER=`
|
|
||||||
|
|
||||||
### Aquire Course ID
|
## Key ID and Key
|
||||||
|
|
||||||
- Follow above before following this
|
> [!IMPORTANT]
|
||||||
- locate the request url field
|
> For courses that are encrypted, It is up to you to acquire the decryption keys.
|
||||||
- 
|
>
|
||||||
- copy the number after `/api-2.0/courses/` as seen highlighed in the above picture
|
> Please **DO NOT** ask me for help acquiring these!
|
||||||
- enter this in the `.env` file after `UDEMY_COURSE_ID=`
|
|
||||||
|
|
||||||
### Key ID and Key
|
- Enter the key and key id in the `keyfile.json`
|
||||||
|
- 
|
||||||
|
- 
|
||||||
|
|
||||||
It is up to you to aquire the key and key id.
|
## Cookies
|
||||||
|
|
||||||
- Enter the key and key id in the `keyfile.json`
|
> [!TIP]
|
||||||
- 
|
> Cookies are not required for individually purchased courses.
|
||||||
- 
|
|
||||||
|
|
||||||
### Start Downloading
|
To download a course included in a subscription plan that you did not purchase individually, you will need to use cookies. You can also use cookies as an alternative to Bearer Tokens.
|
||||||
|
|
||||||
You can now run `python main.py` to start downloading. The course will download to `out_dir`, chapters are seperated into folders.
|
The program can automatically extract them from your browser. You can specify what browser to extract cookies from with the `--browser` argument. Supported browsers are:
|
||||||
|
|
||||||
# Getting an error about "Accepting the latest terms of service"?
|
- `chrome`
|
||||||
|
- `firefox`
|
||||||
|
- `opera`
|
||||||
|
- `edge`
|
||||||
|
- `brave`
|
||||||
|
- `chromium`
|
||||||
|
- `vivaldi`
|
||||||
|
- `safari`
|
||||||
|
|
||||||
- If you are using Udemy business, you must edit `main.py` and change `udemy.com` to `<portal name>.udemy.com`
|
## Ready to go
|
||||||
|
|
||||||
|
You can now run the program, see the examples below. The course will download to `out_dir`.
|
||||||
|
|
||||||
|
# Advanced Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: main.py [-h] -c COURSE_URL [-b BEARER_TOKEN] [-q QUALITY] [-l LANG] [-cd CONCURRENT_DOWNLOADS] [--skip-lectures] [--download-assets]
|
||||||
|
[--download-captions] [--download-quizzes] [--keep-vtt] [--skip-hls] [--info] [--id-as-course-name] [-sc] [--save-to-file] [--load-from-file]
|
||||||
|
[--log-level LOG_LEVEL] [--browser {chrome,firefox,opera,edge,brave,chromium,vivaldi,safari}] [--use-h265] [--h265-crf H265_CRF] [--h265-preset H265_PRESET]
|
||||||
|
[--use-nvenc] [--out OUT] [--continue-lecture-numbers]
|
||||||
|
|
||||||
|
Udemy Downloader
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-c COURSE_URL, --course-url COURSE_URL
|
||||||
|
The URL of the course to download
|
||||||
|
-b BEARER_TOKEN, --bearer BEARER_TOKEN
|
||||||
|
The Bearer token to use
|
||||||
|
-q QUALITY, --quality QUALITY
|
||||||
|
Download specific video quality. If the requested quality isn't available, the closest quality will be used. If not specified, the best quality will be
|
||||||
|
downloaded for each lecture
|
||||||
|
-l LANG, --lang LANG The language to download for captions, specify 'all' to download all captions (Default is 'en')
|
||||||
|
-cd CONCURRENT_DOWNLOADS, --concurrent-downloads CONCURRENT_DOWNLOADS
|
||||||
|
The number of maximum concurrent downloads for segments (HLS and DASH, must be a number 1-30)
|
||||||
|
--skip-lectures If specified, lectures won't be downloaded
|
||||||
|
--download-assets If specified, lecture assets will be downloaded
|
||||||
|
--download-captions If specified, captions will be downloaded
|
||||||
|
--download-quizzes If specified, quizzes will be downloaded
|
||||||
|
--keep-vtt If specified, .vtt files won't be removed
|
||||||
|
--skip-hls If specified, hls streams will be skipped (faster fetching) (hls streams usually contain 1080p quality for non-drm lectures)
|
||||||
|
--info If specified, only course information will be printed, nothing will be downloaded
|
||||||
|
--id-as-course-name If specified, the course id will be used in place of the course name for the output directory. This is a 'hack' to reduce the path length
|
||||||
|
-sc, --subscription-course
|
||||||
|
Mark the course as a subscription based course, use this if you are having problems with the program auto detecting it
|
||||||
|
--save-to-file If specified, course content will be saved to a file that can be loaded later with --load-from-file, this can reduce processing time (Note that asset
|
||||||
|
links expire after a certain amount of time)
|
||||||
|
--load-from-file If specified, course content will be loaded from a previously saved file with --save-to-file, this can reduce processing time (Note that asset links
|
||||||
|
expire after a certain amount of time)
|
||||||
|
--log-level LOG_LEVEL
|
||||||
|
Logging level: one of DEBUG, INFO, ERROR, WARNING, CRITICAL (Default is INFO)
|
||||||
|
--browser {chrome,firefox,opera,edge,brave,chromium,vivaldi,safari}
|
||||||
|
The browser to extract cookies from
|
||||||
|
--use-h265 If specified, videos will be encoded with the H.265 codec
|
||||||
|
--h265-crf H265_CRF Set a custom CRF value for H.265 encoding. FFMPEG default is 28
|
||||||
|
--h265-preset H265_PRESET
|
||||||
|
Set a custom preset value for H.265 encoding. FFMPEG default is medium
|
||||||
|
--use-nvenc Whether to use the NVIDIA hardware transcoding for H.265. Only works if you have a supported NVIDIA GPU and ffmpeg with nvenc support
|
||||||
|
--out OUT, -o OUT Set the path to the output directory
|
||||||
|
--continue-lecture-numbers, -n
|
||||||
|
Use continuous lecture numbering instead of per-chapter
|
||||||
|
```
|
||||||
|
|
||||||
|
- Passing a Bearer Token and Course ID as an argument
|
||||||
|
- `python main.py -c <Course URL> -b <Bearer Token>`
|
||||||
|
- `python main.py -c https://www.udemy.com/courses/myawesomecourse -b <Bearer Token>`
|
||||||
|
- Download a specific quality
|
||||||
|
- `python main.py -c <Course URL> -q 720`
|
||||||
|
- Download assets along with lectures
|
||||||
|
- `python main.py -c <Course URL> --download-assets`
|
||||||
|
- Download assets and specify a quality
|
||||||
|
- `python main.py -c <Course URL> -q 360 --download-assets`
|
||||||
|
- Download captions (Defaults to English)
|
||||||
|
- `python main.py -c <Course URL> --download-captions`
|
||||||
|
- Download captions with specific language
|
||||||
|
- `python main.py -c <Course URL> --download-captions -l en` - English subtitles
|
||||||
|
- `python main.py -c <Course URL> --download-captions -l es` - Spanish subtitles
|
||||||
|
- `python main.py -c <Course URL> --download-captions -l it` - Italian subtitles
|
||||||
|
- `python main.py -c <Course URL> --download-captions -l pl` - Polish Subtitles
|
||||||
|
- `python main.py -c <Course URL> --download-captions -l all` - Downloads all subtitles
|
||||||
|
- etc
|
||||||
|
- Skip downloading lecture videos
|
||||||
|
- `python main.py -c <Course URL> --skip-lectures --download-captions` - Downloads only captions
|
||||||
|
- `python main.py -c <Course URL> --skip-lectures --download-assets` - Downloads only assets
|
||||||
|
- Keep .VTT caption files:
|
||||||
|
- `python main.py -c <Course URL> --download-captions --keep-vtt`
|
||||||
|
- Skip parsing HLS Streams (HLS streams usually contain 1080p quality for Non-DRM lectures):
|
||||||
|
- `python main.py -c <Course URL> --skip-hls`
|
||||||
|
- Print course information only:
|
||||||
|
- `python main.py -c <Course URL> --info`
|
||||||
|
- Specify max number of concurrent downloads:
|
||||||
|
- `python main.py -c <Course URL> --concurrent-downloads 20`
|
||||||
|
- `python main.py -c <Course URL> -cd 20`
|
||||||
|
- Cache course information:
|
||||||
|
- `python main.py -c <Course URL> --save-to-file`
|
||||||
|
- Load course cache:
|
||||||
|
- `python main.py -c <Course URL> --load-from-file`
|
||||||
|
- Change logging level:
|
||||||
|
- `python main.py -c <Course URL> --log-level DEBUG`
|
||||||
|
- `python main.py -c <Course URL> --log-level WARNING`
|
||||||
|
- `python main.py -c <Course URL> --log-level INFO`
|
||||||
|
- `python main.py -c <Course URL> --log-level CRITICAL`
|
||||||
|
- Use course ID as the course name:
|
||||||
|
- `python main.py -c <Course URL> --id-as-course-name`
|
||||||
|
- Encode in H.265:
|
||||||
|
- `python main.py -c <Course URL> --use-h265`
|
||||||
|
- Encode in H.265 with custom CRF:
|
||||||
|
- `python main.py -c <Course URL> --use-h265 -h265-crf 20`
|
||||||
|
- Encode in H.265 with custom preset:
|
||||||
|
- `python main.py -c <Course URL> --use-h265 --h265-preset faster`
|
||||||
|
- Encode in H.265 using NVIDIA hardware transcoding:
|
||||||
|
- `python main.py -c <Course URL> --use-h265 --use-nvenc`
|
||||||
|
- Use continuous numbering (don't restart at 1 in every chapter):
|
||||||
|
- `python main.py -c <Course URL> --continue-lecture-numbers`
|
||||||
|
- `python main.py -c <Course URL> -n`
|
||||||
|
|
||||||
|
# Support
|
||||||
|
|
||||||
|
if you want help using the program, join my [Discord](https://discord.gg/tMzrSxQ) server or use [GitHub Issues](https://github.com/Puyodead1/udemy-downloader/issues)
|
||||||
|
|
||||||
# Credits
|
# Credits
|
||||||
|
|
||||||
- https://github.com/Jayapraveen/Drm-Dash-stream-downloader - for the original code which this is based on
|
- https://github.com/Jayapraveen/Drm-Dash-stream-downloader - For the original code which this is based on
|
||||||
- https://github.com/alastairmccormack/pywvpssh - For code related to PSSH extraction
|
- https://github.com/alastairmccormack/pywvpssh - For code related to PSSH extraction
|
||||||
- https://github.com/alastairmccormack/pymp4parse/ - For code related to mp4 box parsing (used by pywvpssh)
|
- https://github.com/alastairmccormack/pymp4parse - For code related to mp4 box parsing (used by pywvpssh)
|
||||||
|
- https://github.com/lbrayner/vtt-to-srt - For code related to converting subtitles from vtt to srt format
|
||||||
|
- https://github.com/r0oth3x49/udemy-dl - For some of the informaton related to using the udemy api
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
All code is licensed under the MIT license
|
||||||
|
|
||||||
|
## and finally, donations!
|
||||||
|
|
||||||
|
Woo, you made it this far!
|
||||||
|
|
||||||
|
I spend a lot of time coding things, and almost all of them are for nothing in return. When theres a lot of use of a program I make, I try to keep it updated, fix bugs, and even implement new features! But after a while, I do run out of motivation to keep doing it. If you like my work, and can help me out even a little, it would really help me out. If you are interested, you can find all the available options [here](https://github.com/Puyodead1/#supporting-me). Even if you don't, thank you anyways!
|
||||||
|
43
constants.py
Normal file
43
constants.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
HEADERS = {
|
||||||
|
"Origin": "www.udemy.com",
|
||||||
|
# "User-Agent":
|
||||||
|
# "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Encoding": None,
|
||||||
|
}
|
||||||
|
LOGIN_URL = "https://www.udemy.com/join/login-popup/?ref=&display_type=popup&loc"
|
||||||
|
LOGOUT_URL = "https://www.udemy.com/user/logout"
|
||||||
|
# COURSE_URL = "https://{portal_name}.udemy.com/api-2.0/courses/{course_id}/cached-subscriber-curriculum-items?fields[asset]=results,title,external_url,time_estimation,download_urls,slide_urls,filename,asset_type,captions,media_license_token,course_is_drmed,media_sources,stream_urls,body&fields[chapter]=object_index,title,sort_order&fields[lecture]=id,title,object_index,asset,supplementary_assets,view_html&page_size=10000"
|
||||||
|
CURRICULUM_ITEMS_URL = "https://{portal_name}.udemy.com/api-2.0/courses/{course_id}/subscriber-curriculum-items/"
|
||||||
|
COURSE_URL = "https://{portal_name}.udemy.com/api-2.0/courses/{course_id}/"
|
||||||
|
COURSE_SEARCH = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,title,published_title&page=1&page_size=500&search={course_name}"
|
||||||
|
SUBSCRIBED_COURSES = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses/?ordering=-last_accessed&fields[course]=id,title,url&page=1&page_size=12"
|
||||||
|
MY_COURSES_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,title,published_title&ordering=-last_accessed,-access_time&page=1&page_size=10000"
|
||||||
|
COLLECTION_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses-collections/?collection_has_courses=True&course_limit=20&fields[course]=last_accessed_time,title,published_title&fields[user_has_subscribed_courses_collection]=@all&page=1&page_size=1000"
|
||||||
|
QUIZ_URL = "https://{portal_name}.udemy.com/api-2.0/quizzes/{quiz_id}/assessments/?version=1&page_size=250&fields[assessment]=id,assessment_type,prompt,correct_response,section,question_plain,related_lectures"
|
||||||
|
|
||||||
|
CURRICULUM_ITEMS_PARAMS = {
|
||||||
|
"fields[lecture]": "title,object_index,created,asset,supplementary_assets,description,download_url",
|
||||||
|
"fields[quiz]": "title,object_index,type",
|
||||||
|
"fields[practice]": "title,object_index",
|
||||||
|
"fields[chapter]": "title,object_index",
|
||||||
|
"fields[asset]": "title,filename,asset_type,status,is_external,media_license_token,course_is_drmed,media_sources,captions,slides,slide_urls,download_urls,external_url,stream_urls,@min,status,delayed_asset_message,processing_errors,body",
|
||||||
|
"caching_intent": True,
|
||||||
|
"page_size": "200",
|
||||||
|
}
|
||||||
|
|
||||||
|
COURSE_URL_PARAMS = {"fields[course]": "title", "use_remote_version": True, "caching_intent": True}
|
||||||
|
|
||||||
|
HOME_DIR = os.getcwd()
|
||||||
|
SAVED_DIR = os.path.join(os.getcwd(), "saved")
|
||||||
|
KEY_FILE_PATH = os.path.join(os.getcwd(), "keyfile.json")
|
||||||
|
COOKIE_FILE_PATH = os.path.join(os.getcwd(), "cookies.txt")
|
||||||
|
LOG_DIR_PATH = os.path.join(os.getcwd(), "logs")
|
||||||
|
LOG_FILE_PATH = os.path.join(os.getcwd(), "logs", f"{time.strftime('%Y-%m-%d-%I-%M-%S')}.log")
|
||||||
|
LOG_FORMAT = "[%(asctime)s] [%(name)s] [%(funcName)s:%(lineno)d] %(levelname)s: %(message)s"
|
||||||
|
LOG_DATE_FORMAT = "%I:%M:%S"
|
||||||
|
LOG_LEVEL = logging.INFO
|
@ -1,203 +0,0 @@
|
|||||||
#dashdrmmultisegmentdownloader
|
|
||||||
import os,requests,shutil,json,glob
|
|
||||||
from mpegdash.parser import MPEGDASHParser
|
|
||||||
from mpegdash.nodes import Descriptor
|
|
||||||
from mpegdash.utils import (
|
|
||||||
parse_attr_value, parse_child_nodes, parse_node_value,
|
|
||||||
write_attr_value, write_child_node, write_node_value
|
|
||||||
)
|
|
||||||
from utils import extract_kid
|
|
||||||
|
|
||||||
#global ids
|
|
||||||
retry = 3
|
|
||||||
download_dir = os.getcwd() + '\out_dir' # set the folder to output
|
|
||||||
working_dir = os.getcwd() + "\working_dir" # set the folder to download ephemeral files
|
|
||||||
keyfile_path = os.getcwd() + "\keyfile.json"
|
|
||||||
|
|
||||||
if not os.path.exists(working_dir):
|
|
||||||
os.makedirs(working_dir)
|
|
||||||
|
|
||||||
#Get the keys
|
|
||||||
with open(keyfile_path,'r') as keyfile:
|
|
||||||
keyfile = keyfile.read()
|
|
||||||
keyfile = json.loads(keyfile)
|
|
||||||
|
|
||||||
|
|
||||||
#Patching the Mpegdash lib for keyID
|
|
||||||
def __init__(self):
|
|
||||||
self.scheme_id_uri = '' # xs:anyURI (required)
|
|
||||||
self.value = None # xs:string
|
|
||||||
self.id = None # xs:string
|
|
||||||
self.key_id = None # xs:string
|
|
||||||
|
|
||||||
def parse(self, xmlnode):
|
|
||||||
self.scheme_id_uri = parse_attr_value(xmlnode, 'schemeIdUri', str)
|
|
||||||
self.value = parse_attr_value(xmlnode, 'value', str)
|
|
||||||
self.id = parse_attr_value(xmlnode, 'id', str)
|
|
||||||
self.key_id = parse_attr_value(xmlnode, 'cenc:default_KID', str)
|
|
||||||
|
|
||||||
def write(self, xmlnode):
|
|
||||||
write_attr_value(xmlnode, 'schemeIdUri', self.scheme_id_uri)
|
|
||||||
write_attr_value(xmlnode, 'value', self.value)
|
|
||||||
write_attr_value(xmlnode, 'id', self.id)
|
|
||||||
write_attr_value(xmlnode, 'cenc:default_KID', self.key_id)
|
|
||||||
|
|
||||||
Descriptor.__init__ = __init__
|
|
||||||
Descriptor.parse = parse
|
|
||||||
Descriptor.write = write
|
|
||||||
|
|
||||||
def durationtoseconds(period):
|
|
||||||
#Duration format in PTxDxHxMxS
|
|
||||||
if(period[:2] == "PT"):
|
|
||||||
period = period[2:]
|
|
||||||
day = int(period.split("D")[0] if 'D' in period else 0)
|
|
||||||
hour = int(period.split("H")[0].split("D")[-1] if 'H' in period else 0)
|
|
||||||
minute = int(period.split("M")[0].split("H")[-1] if 'M' in period else 0)
|
|
||||||
second = period.split("S")[0].split("M")[-1]
|
|
||||||
print("Total time: " + str(day) + " days " + str(hour) + " hours " + str(minute) + " minutes and " + str(second) + " seconds")
|
|
||||||
total_time = float(str((day * 24 * 60 * 60) + (hour * 60 * 60) + (minute * 60) + (int(second.split('.')[0]))) + '.' + str(int(second.split('.')[-1])))
|
|
||||||
return total_time
|
|
||||||
|
|
||||||
else:
|
|
||||||
print("Duration Format Error")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def download_media(filename,url,epoch = 0):
|
|
||||||
if(os.path.isfile(filename)):
|
|
||||||
print("Segment already downloaded.. skipping..")
|
|
||||||
else:
|
|
||||||
media = requests.get(url, stream=True)
|
|
||||||
media_length = int(media.headers.get("content-length"))
|
|
||||||
if media.status_code == 200:
|
|
||||||
if(os.path.isfile(filename) and os.path.getsize(filename) >= media_length):
|
|
||||||
print("Segment already downloaded.. skipping write to disk..")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with open(filename, 'wb') as video_file:
|
|
||||||
shutil.copyfileobj(media.raw, video_file)
|
|
||||||
print("Segment downloaded: " + filename)
|
|
||||||
return False #Successfully downloaded the file
|
|
||||||
except:
|
|
||||||
print("Connection error: Reattempting download of segment..")
|
|
||||||
download_media(filename,url, epoch + 1)
|
|
||||||
|
|
||||||
if os.path.getsize(filename) >= media_length:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
print("Segment is faulty.. Redownloading...")
|
|
||||||
download_media(filename,url, epoch + 1)
|
|
||||||
elif(media.status_code == 404):
|
|
||||||
print("Probably end hit!\n",url)
|
|
||||||
return True #Probably hit the last of the file
|
|
||||||
else:
|
|
||||||
if (epoch > retry):
|
|
||||||
exit("Error fetching segment, exceeded retry times.")
|
|
||||||
print("Error fetching segment file.. Redownloading...")
|
|
||||||
download_media(filename,url, epoch + 1)
|
|
||||||
|
|
||||||
def cleanup(path):
|
|
||||||
leftover_files = glob.glob(path + '/*.mp4', recursive=True)
|
|
||||||
mpd_files = glob.glob(path + '/*.mpd', recursive=True)
|
|
||||||
leftover_files = leftover_files + mpd_files
|
|
||||||
for file_list in leftover_files:
|
|
||||||
try:
|
|
||||||
os.remove(file_list)
|
|
||||||
except OSError:
|
|
||||||
print(f"Error deleting file: {file_list}")
|
|
||||||
|
|
||||||
def mux_process(video_title,outfile):
|
|
||||||
if os.name == "nt":
|
|
||||||
command = f"ffmpeg -y -i decrypted_audio.mp4 -i decrypted_video.mp4 -acodec copy -vcodec copy -fflags +bitexact -map_metadata -1 -metadata title=\"{video_title}\" -metadata creation_time=2020-00-00T70:05:30.000000Z \"{outfile}.mp4\""
|
|
||||||
else:
|
|
||||||
command = f"nice -n 7 ffmpeg -y -i decrypted_audio.mp4 -i decrypted_video.mp4 -acodec copy -vcodec copy -fflags +bitexact -map_metadata -1 -metadata title=\"{video_title}\" -metadata creation_time=2020-00-00T70:05:30.000000Z {outfile}.mp4"
|
|
||||||
os.system(command)
|
|
||||||
|
|
||||||
def decrypt(kid,filename):
|
|
||||||
try:
|
|
||||||
key = keyfile[kid.lower()]
|
|
||||||
except KeyError as error:
|
|
||||||
exit("Key not found")
|
|
||||||
if(os.name == "nt"):
|
|
||||||
os.system(f"mp4decrypt --key 1:{key} encrypted_{filename}.mp4 decrypted_{filename}.mp4")
|
|
||||||
else:
|
|
||||||
os.system(f"nice -n 7 mp4decrypt --key 1:{key} encrypted_{filename}.mp4 decrypted_{filename}.mp4")
|
|
||||||
|
|
||||||
|
|
||||||
def handle_irregular_segments(media_info,video_title,output_path):
|
|
||||||
no_segment,video_url,video_init,video_extension,no_segment,audio_url,audio_init,audio_extension = media_info
|
|
||||||
download_media("video_0.seg.mp4",video_init)
|
|
||||||
video_kid = extract_kid("video_0.seg.mp4")
|
|
||||||
print("KID for video file is: " + video_kid)
|
|
||||||
download_media("audio_0.seg.mp4",audio_init)
|
|
||||||
audio_kid = extract_kid("audio_0.seg.mp4")
|
|
||||||
print("KID for audio file is: " + audio_kid)
|
|
||||||
for count in range(1,no_segment):
|
|
||||||
video_segment_url = video_url.replace("$Number$",str(count))
|
|
||||||
audio_segment_url = audio_url.replace("$Number$",str(count))
|
|
||||||
video_status = download_media(f"video_{str(count)}.seg.{video_extension}",video_segment_url)
|
|
||||||
audio_status = download_media(f"audio_{str(count)}.seg.{audio_extension}",audio_segment_url)
|
|
||||||
if(video_status):
|
|
||||||
if os.name == "nt":
|
|
||||||
video_concat_command = "copy /b " + "+".join([f"video_{i}.seg.{video_extension}" for i in range(0,count)]) + " encrypted_video.mp4"
|
|
||||||
audio_concat_command = "copy /b " + "+".join([f"audio_{i}.seg.{audio_extension}" for i in range(0,count)]) + " encrypted_audio.mp4"
|
|
||||||
else:
|
|
||||||
video_concat_command = "cat " + " ".join([f"video_{i}.seg.{video_extension}" for i in range(0,count)]) + " > encrypted_video.mp4"
|
|
||||||
audio_concat_command = "cat " + " ".join([f"audio_{i}.seg.{audio_extension}" for i in range(0,count)]) + " > encrypted_audio.mp4"
|
|
||||||
print(video_concat_command)
|
|
||||||
print(audio_concat_command)
|
|
||||||
os.system(video_concat_command)
|
|
||||||
os.system(audio_concat_command)
|
|
||||||
decrypt(video_kid,"video")
|
|
||||||
decrypt(audio_kid,"audio")
|
|
||||||
mux_process(video_title,output_path)
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def manifest_parser(mpd_url):
|
|
||||||
video = []
|
|
||||||
audio = []
|
|
||||||
manifest = requests.get(mpd_url).text
|
|
||||||
with open("manifest.mpd",'w') as manifest_handler:
|
|
||||||
manifest_handler.write(manifest)
|
|
||||||
mpd = MPEGDASHParser.parse("./manifest.mpd")
|
|
||||||
running_time = durationtoseconds(mpd.media_presentation_duration)
|
|
||||||
for period in mpd.periods:
|
|
||||||
for adapt_set in period.adaptation_sets:
|
|
||||||
print("Processing " + adapt_set.mime_type)
|
|
||||||
content_type = adapt_set.mime_type
|
|
||||||
repr = adapt_set.representations[-1] # Max Quality
|
|
||||||
for segment in repr.segment_templates:
|
|
||||||
if(segment.duration):
|
|
||||||
print("Media segments are of equal timeframe")
|
|
||||||
segment_time = segment.duration / segment.timescale
|
|
||||||
total_segments = running_time / segment_time
|
|
||||||
else:
|
|
||||||
print("Media segments are of inequal timeframe")
|
|
||||||
|
|
||||||
approx_no_segments = round(running_time / 6) + 20 # aproximate of 6 sec per segment
|
|
||||||
print("Expected No of segments:",approx_no_segments)
|
|
||||||
if(content_type == "audio/mp4"):
|
|
||||||
segment_extension = segment.media.split(".")[-1]
|
|
||||||
audio.append(approx_no_segments)
|
|
||||||
audio.append(segment.media)
|
|
||||||
audio.append(segment.initialization)
|
|
||||||
audio.append(segment_extension)
|
|
||||||
elif(content_type == "video/mp4"):
|
|
||||||
segment_extension = segment.media.split(".")[-1]
|
|
||||||
video.append(approx_no_segments)
|
|
||||||
video.append(segment.media)
|
|
||||||
video.append(segment.initialization)
|
|
||||||
video.append(segment_extension)
|
|
||||||
return video + audio
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
mpd = "mpd url"
|
|
||||||
base_url = mpd.split("index.mpd")[0]
|
|
||||||
os.chdir(working_dir)
|
|
||||||
media_info = manifest_parser(mpd)
|
|
||||||
video_title = "175. Inverse Transforming Vectors" # the video title that gets embeded into the mp4 file metadata
|
|
||||||
output_path = download_dir + "\\175. Inverse Transforming Vectors" # video title used in the filename, dont append .mp4
|
|
||||||
handle_irregular_segments(media_info,video_title,output_path)
|
|
||||||
cleanup(working_dir)
|
|
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
udemy-downloader:
|
||||||
|
image: udemy-downloader:latest
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- ./output:/app/out_dir:rw
|
||||||
|
- ./keyfile.json:/app/keyfile.json:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
command: python main.py -c $COURSE_URL
|
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"KeyID": "key"
|
"the key id goes here": "the key goes here"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,19 @@
|
|||||||
mpegdash
|
mpegdash
|
||||||
sanitize_filename
|
|
||||||
tqdm
|
tqdm
|
||||||
requests
|
requests
|
||||||
python-dotenv
|
python-dotenv
|
||||||
protobuf
|
protobuf
|
||||||
|
webvtt-py
|
||||||
|
pysrt
|
||||||
|
m3u8
|
||||||
|
colorama
|
||||||
|
yt-dlp
|
||||||
|
bitstring
|
||||||
|
unidecode
|
||||||
|
beautifulsoup4
|
||||||
|
lxml
|
||||||
|
six
|
||||||
|
pathvalidate
|
||||||
|
coloredlogs
|
||||||
|
browser_cookie3
|
||||||
|
demoji
|
69
templates/article_template.html
Normal file
69
templates/article_template.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>__title_placeholder__</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: var(--font-stack-text);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: #2d2f31;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 3.2rem 4.8rem;
|
||||||
|
word-break: break-word;
|
||||||
|
max-width: 69.6rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.heading {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", Helvetica, Arial, sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: 0;
|
||||||
|
font-size: 32px;
|
||||||
|
max-width: 36em;
|
||||||
|
}
|
||||||
|
.article-asset-container {
|
||||||
|
padding: 2.4rem;
|
||||||
|
}
|
||||||
|
.article-asset-container p {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #d1d7dc;
|
||||||
|
color: #b4690e;
|
||||||
|
font-size: 80%;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
font-family: sfmono-regular, Consolas, liberation mono, Menlo, Courier, monospace;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="content">
|
||||||
|
<div class="heading">__title_placeholder__</div>
|
||||||
|
<div class="article-asset-container">__data_placeholder__</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
122
templates/coding_assignment_template.html
Normal file
122
templates/coding_assignment_template.html
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Coding Assignment</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sf pro text, -apple-system, BlinkMacSystemFont, Roboto, segoe ui, Helvetica, Arial,
|
||||||
|
sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 22.4px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
.code-snippet {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #d1d7dc;
|
||||||
|
color: #b4690e;
|
||||||
|
font-size: 90%;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
}
|
||||||
|
.code-block {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #b4690e;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
.black-block {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.italic-text {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body onload="main()">
|
||||||
|
<h1 id="coding-title"></h1>
|
||||||
|
<div>
|
||||||
|
<h2>Instructions</h2>
|
||||||
|
<div id="coding-instructions"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Test(s)</h2>
|
||||||
|
<div id="coding-tests"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Solution(s)</h2>
|
||||||
|
<div id="coding-solutions"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const quizData = __data_placeholder__;
|
||||||
|
|
||||||
|
function renderCodeList(rootElement, codeList, className, titlePrefix) {
|
||||||
|
for (var i = 0; i < codeList.length; i++) {
|
||||||
|
var elem = codeList[i];
|
||||||
|
var jsElem = document.createElement("div");
|
||||||
|
jsElem.className = className;
|
||||||
|
var jsElemTitle = document.createElement("h3");
|
||||||
|
jsElemTitle.innerHTML = titlePrefix + " " + (i + 1);
|
||||||
|
var jsElemBody = document.createElement("code");
|
||||||
|
jsElemBody.className = "code-block black-block";
|
||||||
|
jsElemBody.innerHTML = "<pre>" + elem.content + "</pre>";
|
||||||
|
jsElem.appendChild(jsElemTitle);
|
||||||
|
jsElem.appendChild(jsElemBody);
|
||||||
|
rootElement.appendChild(jsElem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
// display the assignment
|
||||||
|
var codingTitle = document.getElementById("coding-title");
|
||||||
|
codingTitle.innerHTML = quizData.title;
|
||||||
|
|
||||||
|
var codingInstructions = document.getElementById("coding-instructions");
|
||||||
|
if (quizData.hasInstructions) {
|
||||||
|
codingInstructions.innerHTML = quizData.instructions;
|
||||||
|
} else {
|
||||||
|
codingInstructions.innerHTML = '<span class="italic-text">' + quizData.instructions + "</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// display the test(s)
|
||||||
|
var codingTests = document.getElementById("coding-tests");
|
||||||
|
if (!quizData.hasTests) {
|
||||||
|
codingTests.innerHTML = '<span class="italic-text">' + quizData.tests + "</span>";
|
||||||
|
} else {
|
||||||
|
renderCodeList(codingTests, quizData.tests, "coding-test", "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
// display the solution(s)
|
||||||
|
var codingSolutions = document.getElementById("coding-solutions");
|
||||||
|
if (!quizData.hasSolutions) {
|
||||||
|
codingSolutions.innerHTML = '<span class="italic-text">' + quizData.solutions + "</span>";
|
||||||
|
} else {
|
||||||
|
renderCodeList(codingSolutions, quizData.solutions, "coding-solution", "Solution");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
479
templates/quiz_template.html
Normal file
479
templates/quiz_template.html
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Quiz</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
|
||||||
|
"Open Sans", "Helvetica Neue", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding-top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--large-device-width: 850px;
|
||||||
|
--primary-color: #0f172a;
|
||||||
|
--secondary-color: #020617;
|
||||||
|
--primary-text-color: #c7d1dd;
|
||||||
|
--secondary-text-color: #061602;
|
||||||
|
--success-background: hsl(159, 82%, 24%);
|
||||||
|
--success-foreground: hsl(164, 86%, 16%);
|
||||||
|
--success: hsl(160, 84%, 39%);
|
||||||
|
--danger: #ef4444;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--info-background: hsl(218, 81%, 8%);
|
||||||
|
--info-foreground: hsl(217, 91%, 85%);
|
||||||
|
--border-color: #d1d7dc;
|
||||||
|
--check-box-size: 20px;
|
||||||
|
/* control the size */
|
||||||
|
--check-box-color: var(--info-foreground);
|
||||||
|
/* the active color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
background-color: #020617;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#score-stats-container {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
top: 0;
|
||||||
|
height: 40px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--info-background);
|
||||||
|
|
||||||
|
padding: 0px 16px;
|
||||||
|
color: var(--info-foreground);
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#quiz-container {
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
height: var(--check-box-size);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: calc(var(--check-box-size) / 8) solid #939393;
|
||||||
|
padding: calc(var(--check-box-size) / 8);
|
||||||
|
background: radial-gradient(farthest-side, var(--check-box-color) 94%, #0000) 50%/0 0 no-repeat
|
||||||
|
content-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
outline-offset: calc(var(--check-box-size) / 10);
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: inherit;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked {
|
||||||
|
border-color: var(--check-box-color);
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:disabled {
|
||||||
|
background: linear-gradient(#939393 0 0) 50%/100% 20% no-repeat content-box;
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
input[type="radio"],
|
||||||
|
label {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
#quiz-container {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PC (Desktop devices) */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: var(--large-device-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
#score-stats-container {
|
||||||
|
max-width: var(--large-device-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
max-width: var(--large-device-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
input[type="radio"] {
|
||||||
|
background: none !important;
|
||||||
|
border-color: #939393 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked {
|
||||||
|
border-color: #939393 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-lable {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: var(--success-background);
|
||||||
|
border: none;
|
||||||
|
color: #f4f5f7;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-btn {
|
||||||
|
border: none;
|
||||||
|
color: var(--success);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit-button:active::after {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-question-container {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-content {
|
||||||
|
padding: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
position: fixed;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: rgb(240, 241, 248);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 80vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: auto;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
-webkit-transform: translateX(-50%) translateY(-50%);
|
||||||
|
-moz-transform: translateX(-50%) translateY(-50%);
|
||||||
|
-ms-transform: translateX(-50%) translateY(-50%);
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#close-modal-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct-answer label {
|
||||||
|
border: 2px solid var(--success);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incorrect-answer label {
|
||||||
|
border: 2px solid var(--danger);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#quiz-meta-container {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#quiz-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#quiz-description {
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body onload="main()">
|
||||||
|
<main>
|
||||||
|
<section id="quiz-meta-container">
|
||||||
|
<h1 id="quiz-title"></h1>
|
||||||
|
<p id="quiz-description"></p>
|
||||||
|
</section>
|
||||||
|
<section id="score-stats-container">
|
||||||
|
<div id="score-card">
|
||||||
|
Score: <span id="current-score">999</span> of
|
||||||
|
<span id="pass-percent">999%</span>
|
||||||
|
</div>
|
||||||
|
<div>Correct: <span id="correct-answers">999</span></div>
|
||||||
|
<div>Incorrect: <span id="wrong-answers">999</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="quiz-container"></section>
|
||||||
|
|
||||||
|
<dialog id="modal" class="modal-container">
|
||||||
|
<div id="modal-content">
|
||||||
|
<p id="modal-text"></p>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const quizData = __data_placeholder__;
|
||||||
|
let correct = new Set();
|
||||||
|
let incorrect = new Set();
|
||||||
|
let totalNumberOfQuestions = 0;
|
||||||
|
const quizTitle = quizData.quiz_title;
|
||||||
|
const quizDescription = quizData.quiz_description;
|
||||||
|
const questionData = quizData.questions;
|
||||||
|
const passPercent = quizData.pass_percent;
|
||||||
|
const modalTextElement = document.getElementById("modal-text");
|
||||||
|
const quizContainerElement = document.getElementById("quiz-container");
|
||||||
|
|
||||||
|
const dialog = document.querySelector("dialog");
|
||||||
|
const showButton = document.getElementById("view-explanatin");
|
||||||
|
const closeButton = document.getElementById("close-modal-btn");
|
||||||
|
const quizTitleElement = document.getElementById("quiz-title");
|
||||||
|
const quizDescriptionElement = document.getElementById("quiz-description");
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
// update quiz meta data
|
||||||
|
document.title = quizTitle;
|
||||||
|
quizTitleElement.innerHTML = quizTitle;
|
||||||
|
quizDescriptionElement.innerHTML = quizDescription;
|
||||||
|
|
||||||
|
const passPercentElement = document.getElementById("pass-percent");
|
||||||
|
passPercentElement.innerHTML = passPercent + "%";
|
||||||
|
totalNumberOfQuestions = questionData.length;
|
||||||
|
// shuffle the questionData to randomize the order of the questions
|
||||||
|
for (let i = questionData.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[questionData[i], questionData[j]] = [questionData[j], questionData[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
let formattedQuestions = questionData.map(formatSingleQuestionData);
|
||||||
|
updateScore();
|
||||||
|
// display the formattedQuestions
|
||||||
|
formattedQuestions.forEach((question, idx) => {
|
||||||
|
renderSingleQuestion(question, idx + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the question data from the given QuizData object.
|
||||||
|
*
|
||||||
|
* @param {Object} singleQuizData - The singleQuizData object containing prompt and correct_response.
|
||||||
|
* @return {Object} The formatted question object with the following properties:
|
||||||
|
* - id: The ID of the question.
|
||||||
|
* - question: The text of the question.
|
||||||
|
* - answers: The array of answer options.
|
||||||
|
* - correctAnswer: The text of the correct answer.
|
||||||
|
* - explanation: The explanation of the correct answer.
|
||||||
|
*/
|
||||||
|
function formatSingleQuestionData(singleQuizData = null) {
|
||||||
|
const { prompt, correct_response, id } = singleQuizData;
|
||||||
|
const questionText = prompt.question;
|
||||||
|
const answers = prompt.answers;
|
||||||
|
const correctAnswer = correct_response[0];
|
||||||
|
const correctAnswerText = answers[correctAnswer.toLowerCase().charCodeAt(0) - 97];
|
||||||
|
const questionObj = {
|
||||||
|
id: id,
|
||||||
|
question: questionText,
|
||||||
|
answers: answers,
|
||||||
|
correctAnswer: correctAnswerText,
|
||||||
|
explanation: prompt?.explanation || "",
|
||||||
|
};
|
||||||
|
return questionObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a single question with its options and submit button.
|
||||||
|
*
|
||||||
|
* @param {Object} singleQuestionData - The data of the question to render.
|
||||||
|
* @param {number} rootIndex - The index of the question in the quiz.
|
||||||
|
* @return {void} return nothing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const renderSingleQuestion = (singleQuestionData = {}, rootIndex = 1) => {
|
||||||
|
const { id, explanation, answers, correctAnswer, question } = singleQuestionData;
|
||||||
|
// shuffle the answers to randomize the order of the answers
|
||||||
|
for (let i = answers.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[answers[i], answers[j]] = [answers[j], answers[i]];
|
||||||
|
}
|
||||||
|
const optionsHTML = answers
|
||||||
|
.map((option, index) => {
|
||||||
|
const optionId = `${id}_${index}`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="question-lable">
|
||||||
|
<input type="radio" id="${optionId}" name="${"answer"}" value="${option}" />
|
||||||
|
<label for="${optionId}">${option}</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.innerHTML = `
|
||||||
|
<form data-correct-answer="${correctAnswer}" data-question-id="${id}" class="single-question-container" onsubmit="submitButtonListener(event)">
|
||||||
|
<div style="display: flex;justify-content: space-between;">
|
||||||
|
<p style="font-weight: 600">Question ${rootIndex}:</p>
|
||||||
|
<button type="button" onclick="renderExplanation(event)" id="${`explanation-${id}`}" data-explanation="${explanation}" class="explanation-btn">View Explanation</button>
|
||||||
|
</div>
|
||||||
|
<p style="margin-bottom: 8px;line-height: 1.5">${question}</p>
|
||||||
|
<div class="options-container">
|
||||||
|
${optionsHTML}
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<button type="submit" id="submit-button" class="button">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
quizContainerElement.appendChild(container);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the score on the page based on the number of correct and incorrect answers.
|
||||||
|
*
|
||||||
|
* @return {void} This function does not return a value.
|
||||||
|
*/
|
||||||
|
function updateScore() {
|
||||||
|
const currentParcentageElement = document.getElementById("current-score");
|
||||||
|
const correctAnswerElement = document.getElementById("correct-answers");
|
||||||
|
const wrongAnswerElement = document.getElementById("wrong-answers");
|
||||||
|
correctAnswerElement.innerHTML = correct.size;
|
||||||
|
wrongAnswerElement.innerHTML = incorrect.size;
|
||||||
|
const score = Number((correct.size / totalNumberOfQuestions) * 100).toFixed(2);
|
||||||
|
currentParcentageElement.innerHTML = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the event when the submit button is clicked.
|
||||||
|
*
|
||||||
|
* @param {Event} e - The event object.
|
||||||
|
* @return {void} This function does not return anything.
|
||||||
|
*/
|
||||||
|
const submitButtonListener = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const form = e.target;
|
||||||
|
const selectedOption = e.target.querySelector('input[type="radio"]:checked');
|
||||||
|
if (!selectedOption) {
|
||||||
|
alert("Please select an answer!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isCorrect = false;
|
||||||
|
const { answer: userAnswer } = Object.fromEntries(formData.entries());
|
||||||
|
const correctAnswer = e.target.dataset.correctAnswer;
|
||||||
|
const questionId = e.target.dataset.questionId;
|
||||||
|
if (userAnswer == correctAnswer) {
|
||||||
|
correct.add(questionId);
|
||||||
|
incorrect.delete(questionId);
|
||||||
|
isCorrect = true;
|
||||||
|
} else {
|
||||||
|
incorrect.add(e.target.dataset.questionId);
|
||||||
|
correct.delete(questionId);
|
||||||
|
}
|
||||||
|
updateScore();
|
||||||
|
|
||||||
|
const resultClass = isCorrect ? "correct-answer" : "incorrect-answer";
|
||||||
|
|
||||||
|
form.querySelectorAll(".question-lable").forEach((label) => {
|
||||||
|
label.classList.remove("correct-answer", "incorrect-answer");
|
||||||
|
});
|
||||||
|
selectedOption.closest(".question-lable").classList.add(resultClass);
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderExplanation(ev) {
|
||||||
|
modalTextElement.innerHTML = ev.target.dataset?.explanation || "no explanation found";
|
||||||
|
dialog.showModal();
|
||||||
|
dialog.addEventListener("click", (event) => {
|
||||||
|
if (event.target === dialog) {
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
28
tls.py
Normal file
28
tls.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import ssl
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class SSLCiphers(HTTPAdapter):
|
||||||
|
"""
|
||||||
|
Custom HTTP Adapter to change the TLS Cipher set, and therefore it's fingerprint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cipher_list: Optional[str] = None, *args, **kwargs):
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False # For some reason this is needed to avoid a verification error
|
||||||
|
self._ssl_context = ctx
|
||||||
|
# You can set ciphers but Python's default cipher list should suffice.
|
||||||
|
# This cipher list differs to the default Python-requests one.
|
||||||
|
if cipher_list:
|
||||||
|
self._ssl_context.set_ciphers(cipher_list)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def init_poolmanager(self, *args, **kwargs):
|
||||||
|
kwargs["ssl_context"] = self._ssl_context
|
||||||
|
return super().init_poolmanager(*args, **kwargs)
|
||||||
|
|
||||||
|
def proxy_manager_for(self, *args, **kwargs):
|
||||||
|
kwargs["ssl_context"] = self._ssl_context
|
||||||
|
return super().proxy_manager_for(*args, **kwargs)
|
17
utils.py
17
utils.py
@ -1,7 +1,10 @@
|
|||||||
import mp4parse
|
|
||||||
import codecs
|
|
||||||
import widevine_pssh_pb2
|
|
||||||
import base64
|
import base64
|
||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
|
||||||
|
import mp4parse
|
||||||
|
import widevine_pssh_data_pb2
|
||||||
|
|
||||||
|
|
||||||
def extract_kid(mp4_file):
|
def extract_kid(mp4_file):
|
||||||
"""
|
"""
|
||||||
@ -18,15 +21,17 @@ def extract_kid(mp4_file):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
boxes = mp4parse.F4VParser.parse(filename=mp4_file)
|
boxes = mp4parse.F4VParser.parse(filename=mp4_file)
|
||||||
|
if not os.path.exists(mp4_file):
|
||||||
|
raise Exception("File does not exist")
|
||||||
for box in boxes:
|
for box in boxes:
|
||||||
if box.header.box_type == 'moov':
|
if box.header.box_type == "moov":
|
||||||
pssh_box = next(x for x in box.pssh if x.system_id == "edef8ba979d64acea3c827dcd51d21ed")
|
pssh_box = next(x for x in box.pssh if x.system_id == "edef8ba979d64acea3c827dcd51d21ed")
|
||||||
hex = codecs.decode(pssh_box.payload, "hex")
|
hex = codecs.decode(pssh_box.payload, "hex")
|
||||||
|
|
||||||
pssh = widevine_pssh_pb2.WidevinePsshData()
|
pssh = widevine_pssh_data_pb2.WidevinePsshData()
|
||||||
pssh.ParseFromString(hex)
|
pssh.ParseFromString(hex)
|
||||||
content_id = base64.b16encode(pssh.content_id)
|
content_id = base64.b16encode(pssh.content_id)
|
||||||
return content_id.decode("utf-8")
|
return content_id.decode("utf-8").lower()
|
||||||
|
|
||||||
# No Moof or PSSH header found
|
# No Moof or PSSH header found
|
||||||
return None
|
return None
|
20
vtt_to_srt.py
Normal file
20
vtt_to_srt.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from webvtt import WebVTT
|
||||||
|
import html
|
||||||
|
import os
|
||||||
|
from pysrt.srtitem import SubRipItem
|
||||||
|
from pysrt.srttime import SubRipTime
|
||||||
|
|
||||||
|
|
||||||
|
def convert(directory, filename):
|
||||||
|
index = 0
|
||||||
|
vtt_filepath = os.path.join(directory, filename + ".vtt")
|
||||||
|
srt_filepath = os.path.join(directory, filename + ".srt")
|
||||||
|
srt = open(srt_filepath, mode='w', encoding='utf8', errors='ignore')
|
||||||
|
|
||||||
|
for caption in WebVTT().read(vtt_filepath):
|
||||||
|
index += 1
|
||||||
|
start = SubRipTime(0, 0, caption.start_in_seconds)
|
||||||
|
end = SubRipTime(0, 0, caption.end_in_seconds)
|
||||||
|
srt.write(
|
||||||
|
SubRipItem(index, start, end, html.unescape(
|
||||||
|
caption.text)).__str__() + "\n")
|
56
widevine_pssh_data.proto
Normal file
56
widevine_pssh_data.proto
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file or at
|
||||||
|
// https://developers.google.com/open-source/licenses/bsd
|
||||||
|
//
|
||||||
|
// This file defines Widevine Pssh Data proto format.
|
||||||
|
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package shaka.media;
|
||||||
|
|
||||||
|
message WidevinePsshData {
|
||||||
|
enum Algorithm {
|
||||||
|
UNENCRYPTED = 0;
|
||||||
|
AESCTR = 1;
|
||||||
|
};
|
||||||
|
optional Algorithm algorithm = 1;
|
||||||
|
repeated bytes key_id = 2;
|
||||||
|
|
||||||
|
// Content provider name.
|
||||||
|
optional string provider = 3;
|
||||||
|
|
||||||
|
// A content identifier, specified by content provider.
|
||||||
|
optional bytes content_id = 4;
|
||||||
|
|
||||||
|
// The name of a registered policy to be used for this asset.
|
||||||
|
optional string policy = 6;
|
||||||
|
|
||||||
|
// Crypto period index, for media using key rotation.
|
||||||
|
optional uint32 crypto_period_index = 7;
|
||||||
|
|
||||||
|
// Optional protected context for group content. The grouped_license is a
|
||||||
|
// serialized SignedMessage.
|
||||||
|
optional bytes grouped_license = 8;
|
||||||
|
|
||||||
|
// Protection scheme identifying the encryption algorithm. Represented as one
|
||||||
|
// of the following 4CC values: 'cenc' (AES-CTR), 'cbc1' (AES-CBC),
|
||||||
|
// 'cens' (AES-CTR subsample), 'cbcs' (AES-CBC subsample).
|
||||||
|
optional uint32 protection_scheme = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derived from WidevinePsshData. The JSON format of this proto is used in
|
||||||
|
// Widevine HLS DRM signaling v1.
|
||||||
|
// We cannot build JSON from WidevinePsshData as |key_id| is required to be in
|
||||||
|
// hex format, while |bytes| type is translated to base64 by JSON formatter, so
|
||||||
|
// we have to use |string| type and do hex conversion in the code.
|
||||||
|
message WidevineHeader {
|
||||||
|
repeated string key_ids = 2;
|
||||||
|
|
||||||
|
// Content provider name.
|
||||||
|
optional string provider = 3;
|
||||||
|
|
||||||
|
// A content identifier, specified by content provider.
|
||||||
|
optional bytes content_id = 4;
|
||||||
|
}
|
29
widevine_pssh_data_pb2.py
Normal file
29
widevine_pssh_data_pb2.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: widevine_pssh_data.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18widevine_pssh_data.proto\x12\x0bshaka.media\"\x8f\x02\n\x10WidevinePsshData\x12:\n\talgorithm\x18\x01 \x01(\x0e\x32\'.shaka.media.WidevinePsshData.Algorithm\x12\x0e\n\x06key_id\x18\x02 \x03(\x0c\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c\x12\x0e\n\x06policy\x18\x06 \x01(\t\x12\x1b\n\x13\x63rypto_period_index\x18\x07 \x01(\r\x12\x17\n\x0fgrouped_license\x18\x08 \x01(\x0c\x12\x19\n\x11protection_scheme\x18\t \x01(\r\"(\n\tAlgorithm\x12\x0f\n\x0bUNENCRYPTED\x10\x00\x12\n\n\x06\x41\x45SCTR\x10\x01\"G\n\x0eWidevineHeader\x12\x0f\n\x07key_ids\x18\x02 \x03(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c')
|
||||||
|
|
||||||
|
_globals = globals()
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'widevine_pssh_data_pb2', _globals)
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
_globals['_WIDEVINEPSSHDATA']._serialized_start=42
|
||||||
|
_globals['_WIDEVINEPSSHDATA']._serialized_end=313
|
||||||
|
_globals['_WIDEVINEPSSHDATA_ALGORITHM']._serialized_start=273
|
||||||
|
_globals['_WIDEVINEPSSHDATA_ALGORITHM']._serialized_end=313
|
||||||
|
_globals['_WIDEVINEHEADER']._serialized_start=315
|
||||||
|
_globals['_WIDEVINEHEADER']._serialized_end=386
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
@ -1,141 +0,0 @@
|
|||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
||||||
# source: widevine_pssh.proto
|
|
||||||
|
|
||||||
import sys
|
|
||||||
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
|
||||||
from google.protobuf import message as _message
|
|
||||||
from google.protobuf import reflection as _reflection
|
|
||||||
from google.protobuf import symbol_database as _symbol_database
|
|
||||||
from google.protobuf import descriptor_pb2
|
|
||||||
# @@protoc_insertion_point(imports)
|
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
|
||||||
name='widevine_pssh.proto',
|
|
||||||
package='',
|
|
||||||
serialized_pb=_b('\n\x13widevine_pssh.proto\"\xfc\x01\n\x10WidevinePsshData\x12.\n\talgorithm\x18\x01 \x01(\x0e\x32\x1b.WidevinePsshData.Algorithm\x12\x0e\n\x06key_id\x18\x02 \x03(\x0c\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c\x12\x12\n\ntrack_type\x18\x05 \x01(\t\x12\x0e\n\x06policy\x18\x06 \x01(\t\x12\x1b\n\x13\x63rypto_period_index\x18\x07 \x01(\r\x12\x17\n\x0fgrouped_license\x18\x08 \x01(\x0c\"(\n\tAlgorithm\x12\x0f\n\x0bUNENCRYPTED\x10\x00\x12\n\n\x06\x41\x45SCTR\x10\x01')
|
|
||||||
)
|
|
||||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_WIDEVINEPSSHDATA_ALGORITHM = _descriptor.EnumDescriptor(
|
|
||||||
name='Algorithm',
|
|
||||||
full_name='WidevinePsshData.Algorithm',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
values=[
|
|
||||||
_descriptor.EnumValueDescriptor(
|
|
||||||
name='UNENCRYPTED', index=0, number=0,
|
|
||||||
options=None,
|
|
||||||
type=None),
|
|
||||||
_descriptor.EnumValueDescriptor(
|
|
||||||
name='AESCTR', index=1, number=1,
|
|
||||||
options=None,
|
|
||||||
type=None),
|
|
||||||
],
|
|
||||||
containing_type=None,
|
|
||||||
options=None,
|
|
||||||
serialized_start=236,
|
|
||||||
serialized_end=276,
|
|
||||||
)
|
|
||||||
_sym_db.RegisterEnumDescriptor(_WIDEVINEPSSHDATA_ALGORITHM)
|
|
||||||
|
|
||||||
|
|
||||||
_WIDEVINEPSSHDATA = _descriptor.Descriptor(
|
|
||||||
name='WidevinePsshData',
|
|
||||||
full_name='WidevinePsshData',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='algorithm', full_name='WidevinePsshData.algorithm', index=0,
|
|
||||||
number=1, type=14, cpp_type=8, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
options=None),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='key_id', full_name='WidevinePsshData.key_id', index=1,
|
|
||||||
number=2, type=12, cpp_type=9, label=3,
|
|
||||||
has_default_value=False, default_value=[],
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
options=None),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='provider', full_name='WidevinePsshData.provider', index=2,
|
|
||||||
number=3, type=9, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
options=None),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='content_id', full_name='WidevinePsshData.content_id', index=3,
|
|
||||||
number=4, type=12, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=_b(""),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
options=None),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='track_type', full_name='WidevinePsshData.track_type', index=4,
|
|
||||||
number=5, type=9, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
options=None),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='policy', full_name='WidevinePsshData.policy', index=5,
|
|
||||||
number=6, type=9, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
options=None),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='crypto_period_index', full_name='WidevinePsshData.crypto_period_index', index=6,
|
|
||||||
number=7, type=13, cpp_type=3, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
options=None),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='grouped_license', full_name='WidevinePsshData.grouped_license', index=7,
|
|
||||||
number=8, type=12, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=_b(""),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
options=None),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
_WIDEVINEPSSHDATA_ALGORITHM,
|
|
||||||
],
|
|
||||||
options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=24,
|
|
||||||
serialized_end=276,
|
|
||||||
)
|
|
||||||
|
|
||||||
_WIDEVINEPSSHDATA.fields_by_name['algorithm'].enum_type = _WIDEVINEPSSHDATA_ALGORITHM
|
|
||||||
_WIDEVINEPSSHDATA_ALGORITHM.containing_type = _WIDEVINEPSSHDATA
|
|
||||||
DESCRIPTOR.message_types_by_name['WidevinePsshData'] = _WIDEVINEPSSHDATA
|
|
||||||
|
|
||||||
WidevinePsshData = _reflection.GeneratedProtocolMessageType('WidevinePsshData', (_message.Message,), dict(
|
|
||||||
DESCRIPTOR = _WIDEVINEPSSHDATA,
|
|
||||||
__module__ = 'widevine_pssh_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:WidevinePsshData)
|
|
||||||
))
|
|
||||||
_sym_db.RegisterMessage(WidevinePsshData)
|
|
||||||
|
|
||||||
|
|
||||||
# @@protoc_insertion_point(module_scope)
|
|
Loading…
x
Reference in New Issue
Block a user