Compare commits

...

268 Commits
1.0 ... master

Author SHA1 Message Date
Puyodead1
71ecdb6a47
fix non-drmed videos not downloading 2024-08-29 13:01:39 -04:00
Puyodead1
3e79463330
minor article template changes 2024-08-29 00:37:51 -04:00
Puyodead1
2d6a3020aa
fix articles 2024-08-29 00:24:37 -04:00
Puyodead1
745ea117de
remove format from fstrings 2024-07-20 15:26:13 -04:00
Puyodead1
d19a018653
remove duplicated methods 2024-07-20 15:25:03 -04:00
Puyodead1
d123403434
updates
- added back the ability to use cookies.txt file (netscape format)
- updated error message for course subscription when failed to find classes
- remove requirement for shaka-packager in favor of ffmpeg (v5+)
2024-07-18 18:46:43 -04:00
Puyodead1
4b80e32433
Merge branch 'feat/refactor' 2024-07-16 17:03:07 -04:00
Puyodead1
c60f4e44a1
Merge pull request #228 from fazlulShanto/master
update: quiz_template view
2024-07-08 17:10:14 -04:00
fazlulShanto
db3ab180a4 update: quiz_template view 2024-07-09 03:00:26 +06:00
Puyodead1
c7329b1d59
retry failed page fetch 2024-06-19 11:03:46 -04:00
Puyodead1
2de870c009
Merge pull request #226 from thebetauser/master
fix for audio overrun during multiplexing
2024-06-16 09:03:10 -04:00
h1pp0
904cc41543
Merge pull request #1 from thebetauser/thebetauser-patch-1
Update main.py
2024-06-16 08:55:15 -04:00
h1pp0
1c0bca935e
Update main.py
Added -shortest flag to force the shortest stream during multiplexing. This resolves issue #215 on linux.
2024-06-16 08:46:25 -04:00
Puyodead1
ea59b43d4e
Merge pull request #223 from thebetauser/patch-1
Update Dockerfile
2024-06-08 16:09:49 -04:00
h1pp0
f688a5f263
Update Dockerfile
[*] Fixed issue with wget output command parameter
[*] Update to latest version of shaka-packager
2024-06-08 12:35:35 -04:00
Puyodead1
039816f695
Update constants.py 2024-03-17 14:11:30 -04:00
Puyodead1
1b1b5d81bd
fix large courses 2024-03-17 14:11:30 -04:00
Puyodead1
db7b0490e6
format 2024-03-17 14:11:29 -04:00
Puyodead1
b50dbd1ee2
dont auto download torrents 2024-03-17 14:11:29 -04:00
Puyodead1
c8088cc081
Update requirements.txt 2024-03-17 14:11:29 -04:00
Puyodead1
c21243554a
strip emojis from file lecture path 2024-03-17 14:11:29 -04:00
eliottp1089
4880f7bfef
support accents in html files 2024-03-17 14:11:28 -04:00
Puyodead1
5d01e56756
Update main.py 2024-03-17 14:11:28 -04:00
Puyodead1
ef9d2a6be3
some refactoring 2024-03-17 14:11:17 -04:00
Puyodead1
7f522ebebb
bug fix
oopsie
2024-03-17 14:10:56 -04:00
Puyodead1
72473188df
stop aking about keys 2024-03-06 17:31:30 -05:00
Puyodead1
1d30c30f71
Merge pull request #209 from 4l3j0Ok/master
Docker Implementation: Local Deployment with Auto Course Download
2024-03-04 08:29:23 -05:00
4l3j0Ok
4da85c0aff Added comment to .env.sample 2024-03-03 20:55:38 -03:00
4l3j0Ok
c1967625cf Removed useless PATH envvar update 2024-03-03 20:54:19 -03:00
4l3j0Ok
e4bbd47cdb Added docker compose output volume 2024-03-03 20:53:46 -03:00
4l3j0Ok
71101bad72 Added docker compose 2024-03-03 20:41:20 -03:00
4l3j0Ok
fa444c6960 Added COURSE_URL to .env.sample 2024-03-03 20:41:12 -03:00
4l3j0Ok
3d44811616 Install Shaka Packager 2024-03-03 20:40:57 -03:00
Alejoide
0ab393afb1 Add Dockerfile 2024-03-03 19:44:52 -03:00
Puyodead1
47cf0cdfd8
Merge pull request #201 from runtlb64/shaka-packager-dash
Add en-dash (–) to shaka-packager replacement list
2024-01-17 00:58:22 -05:00
runtlb64
06bfa95a04 Add en-dash (–) to shaka-packager replacement list 2024-01-16 17:45:49 -07:00
Puyodead1
375be73e0a
add python to readme requirements 2023-10-27 13:23:17 -04:00
Puyodead1
79ea132962
add 'continuous numbering' option 2023-10-27 09:35:55 -04:00
Puyodead1
935b2a41f3
fix quizzes in info 2023-10-20 16:53:56 -04:00
Puyodead1
a461f5fc86
fix source code assets not being downloaded 2023-10-16 22:20:19 -04:00
Puyodead1
68e4ffb787
bug fix 2023-10-15 19:20:54 -04:00
Puyodead1
eb7189178d
Merge pull request #180 from runtlb64/master
Add functionality to parse coding assignments
2023-09-08 02:09:29 -04:00
runtlb64
35673f91e0 Add functionality to parse coding assignments 2023-09-06 01:45:44 -07:00
Puyodead1
875d888503
update protobuf 2023-08-20 14:24:23 -04:00
Puyodead1
8ab48230ed
feat: custom output path (Closes #168) 2023-08-20 14:23:11 -04:00
Puyodead1
271a426a8c
fix #175 2023-08-20 14:10:12 -04:00
Puyodead1
3a13ba5a54
add note about yt-dlp ffmpeg builds 2023-08-16 09:57:11 -04:00
Puyodead1
23e7e94f16
bug fix (#176) 2023-08-16 02:43:24 -04:00
Puyodead1
f4f472de81
Update _version.py 2023-08-16 01:37:58 -04:00
Puyodead1
84eb17b793
Merge pull request #177 from Puyodead1/feat/cookies
Merge feat/cookies
2023-08-12 23:56:15 -04:00
Puyodead1
43f6085e91
some bug fixes 2023-08-12 23:53:17 -04:00
Puyodead1
f9634168d4
Revert "bug fix?"
This reverts commit 0ab68cb95f97f05c5ef62309c5775ab4280efd58.
2023-08-12 23:53:17 -04:00
Puyodead1
b5741b2373
bug fix? 2023-08-12 23:53:17 -04:00
Puyodead1
fdf8cde414
bug fix 2023-08-12 23:53:16 -04:00
Puyodead1
e5450b6f85
Cookie extraction
- Removed cloudscraper
- Added cookie extraction from browser
2023-08-12 23:53:16 -04:00
Puyodead1
e9b9d8a6a4
fix for lectures without chapter 2023-08-12 23:52:42 -04:00
Puyodead1
45b6c621f9
Merge pull request #173 from Puyodead1/feat/quizes
Add support for quizzes
2023-08-06 23:12:38 -04:00
Puyodead1
bc9ff0ba18
fix quizzes, add cmd line switch 2023-07-20 11:57:59 -04:00
Puyodead1
88edbdf538
add initial quiz implementation 2023-07-06 06:36:46 -04:00
Puyodead1
7621d078da
bug fix 2023-07-02 17:21:05 -04:00
Puyodead1
62a924de3f
fix print info 2023-07-02 17:19:53 -04:00
Puyodead1
14095b8e72
Merge pull request #166 from debakarr/master
Skip Check for ffmpeg and shaka-packager if --skip-lectures flag is True
2023-06-20 09:57:28 -04:00
Debakar Roy
1433962f95 No need of ffmpeg in case we are not downloading lectures 2023-06-20 13:19:48 +00:00
Debakar Roy
4aaed934a0 No need of shaka in case we are not downloading lectures 2023-06-20 13:13:09 +00:00
Puyodead1
a76f14190a
Update README.md 2023-06-17 16:16:54 -04:00
Puyodead1
e77bd8a959
bug fixes 2023-05-30 22:04:42 -04:00
Puyodead1
06e295d2b6
update manifest downloading 2023-05-30 18:08:47 -04:00
Puyodead1
340d4c6786
Merge branch 'dev/otf-processing' 2023-05-30 18:07:30 -04:00
Puyodead1
f7cf66931c
use temp local mpd files for extracting metadata 2023-05-30 17:49:23 -04:00
Puyodead1
b08f2569cb
update ffmpeg h265 command 2023-05-29 21:47:50 -04:00
Puyodead1
d4a4ea1b17
fix incorrect ffmpeg commands 2023-05-29 21:47:40 -04:00
Puyodead1
6570f7c45f
update ffmpeg h265 command 2023-05-24 13:47:33 -04:00
Puyodead1
1ee5d79664
fix incorrect ffmpeg commands 2023-05-24 13:18:47 -04:00
Puyodead1
4886098691
Refactor processing system 2023-05-23 14:36:25 -04:00
Puyodead1
f829d0fbce
h265: bug fixes and nvenc 2023-03-06 17:46:34 -05:00
Puyodead1
cb906d5eaf
add h265 support 2023-03-06 17:16:00 -05:00
Puyodead1
f321791819
Attempt to fix encoding problems with caption conversion (#92, #98, #97) 2023-02-02 17:54:46 -05:00
Puyodead1
d8b5d3ca0e
fix typo 2023-01-25 15:29:48 -05:00
Puyodead1
705de30925
Custom TLS Cipher 2023-01-25 15:17:27 -05:00
Puyodead1
18a9c364af
Update README.md 2022-12-15 14:01:05 -05:00
Puyodead1
6355a3dcbc
Merge pull request #138 from rickeymandraque/master
Patch for Shaka-pakager files name
2022-10-13 20:48:29 -04:00
Puyodead1
1da41a9bde
update Discord invite 2022-08-24 13:58:30 -04:00
rickeymandraque
6f9919ab9d
Patch for Shaka-pakager files name
Patch for specials charset (Decryption returned a non-zero exit code.)
Add coding: utf-8 (line 1)
Add small list of characters (line 1028)
2022-08-18 12:16:53 +02:00
Puyodead1
962904abd6
Merge pull request #125 from HuzunluArtemis/master
fix little logger thing
2022-07-11 17:38:55 -04:00
Hüzünlü Artemis [HuzunluArtemis]
8ad8faaca0
fix little logger thing 2022-07-11 22:00:11 +03:00
Puyodead1
85610e2fca
version bump 2022-07-10 08:48:00 -04:00
Puyodead1
91bf5c3209
format main.py 2022-07-10 08:42:05 -04:00
Puyodead1
502ac0e4fe
lock protobuf version to 3.20.0 2022-07-10 08:41:51 -04:00
Puyodead1
2ce865a81b
fix some issues with subprocess logging 2022-07-10 08:41:03 -04:00
Puyodead1
e5398a1333
update shaka-packager link 2022-05-14 11:35:50 -04:00
Puyodead1
f824487d78
fix: latin-1 codec error
untested if this fixes the issue or not, but it doesnt break anything either that i can tell
2022-03-25 21:21:18 -04:00
Puyodead1
3478095bb5
Merge pull request #108 from Puyodead1/exp-ffmpeg-fix
Bug Fixes
2022-03-15 21:12:10 -04:00
Puyodead1
07bfb9163b
fix: large courses not always working correctly
Seems like Udemy changed something and large courses can return a 504 error now
2022-03-15 20:38:42 -04:00
Puyodead1
885d920fba
attempt to fix ffmpeg hanging
+ Added nostdin argument to ffmpeg call
+ Set ffmpeg log level to errors only
+ Fixed some stuff not getting printed correctly
2022-03-15 19:52:13 -04:00
Puyodead1
e5d5285bf7
Ensure pipe is not None
fixes #105
2022-02-19 10:25:37 -05:00
Puyodead1
c22fbfccaa
Update README.md 2022-02-12 18:59:37 -05:00
Puyodead1
d8b711af89
Update README.md 2022-02-12 18:55:02 -05:00
Puyodead1
ffae516179 Update _version.py 2022-01-16 17:25:01 -05:00
Puyodead1
1bdc581c65 bug fix for logging 2022-01-16 17:24:07 -05:00
Puyodead1
4326c4743a Update main.py 2022-01-16 15:01:51 -05:00
Puyodead1
13bc68e905 Switch to Shaka-Packager + bug fixes
+ Replaced mp4decrypt with shaka-packager
+ Fix logger not correctly setting level from arguments
+ switched all os.system to subprocess.Popen
+ extra verbosity, external commands will now print their output to make debugging much MUCH easier
+ Fixed a problem with shaka-packager failing on files with a comma
2022-01-16 14:30:49 -05:00
Puyodead1
007e5ea60f
Delete stale.yml 2022-01-15 19:03:01 -05:00
Puyodead1
ea37e8a397 bug fix 2022-01-12 19:55:30 -05:00
Puyodead1
884be3b68a
update branches in bug report form 2022-01-09 13:18:42 -05:00
Puyodead1
b97b12344b bug fix: subscription courses fail to download
+ Fixed cookies not getting loaded correctly leading to failures getting course information
2022-01-05 13:50:30 -05:00
Puyodead1
046344796c bug fixes
+ Fix debug logging not working
+ Fix captions not being downloaded
2022-01-05 12:47:20 -05:00
Puyodead1
e20699549e better debug exceptions 2022-01-01 15:29:17 -05:00
Puyodead1
52e3cd5b36 verbosity: print aria return code 2022-01-01 15:17:01 -05:00
Puyodead1
f8fab9fec9 fix: only download subtitles if there is a lecture also
+ Fix downloading subtitles for hls lectures that were skipped, only download subtitles if there is also a video lecture with it
2022-01-01 14:38:01 -05:00
Puyodead1
bb11640ed7 revert: fix: a few subprocess calls not piping output 2021-12-28 11:55:03 -05:00
Puyodead1
3176ac1191 feat: id as course name as an argument + readme update
+ adds the feature from the `id-as-course-name` branch to use the course id as the output directory name instead of the course name. this is useful to reduce the path length
+ Updated readme to reflect recent code changes
2021-12-28 11:14:35 -05:00
Puyodead1
f3b1c74f13 update: error handling for download function
+ raise exception if return code is non-0
+ version bump
2021-12-28 10:47:50 -05:00
Puyodead1
4bdbf68f58 update: more verbosity
+ Added more verbose logging during decryption and merging
+ Added error checking for decryption and merging
2021-12-28 10:42:41 -05:00
Puyodead1
72a354ed69 fix: a few subprocess calls not piping output 2021-12-28 10:19:03 -05:00
Puyodead1
aeca63d671 Update _version.py 2021-12-21 18:23:56 -05:00
Puyodead1
bc9f6ecb1a fix: empty html files being generated 2021-12-21 18:21:14 -05:00
Puyodead1
68f5172e30 refactoring, and better logging
+ Add colored console logging
+ Add file logging
+ Refactor code
+ Add `--log-level` argument
+ Moved constant variables to separate file
+ Renamed some variables to be consistent throughout the program (`concurrent_connections` -> `concurrent_downloads`, `access_token` -> `bearer_token`)
+ If a keyfile isnt detected, we only warn the user instead of warning and exiting
+ External links are now written to their own shortcut file (they are still written to a single file still also) (#26)
+ Arguments `load-from-file` and `save-to-file` are no longer suppressed
+ Added Quiz URL
+ bump version
2021-12-20 14:41:34 -05:00
Puyodead1
8756bfc266 fix for ebooks/pdf not correctly downloading 2021-12-17 08:51:02 -05:00
Puyodead1
ca34f90996
Update README.md 2021-12-05 17:49:58 -05:00
Puyodead1
51df2758b8
Update README.md 2021-12-05 17:48:14 -05:00
Puyodead1
28b2cc8e7d
Merge pull request #79 from perfectra1n/line-ending-patch 2021-12-03 16:08:13 -05:00
perf3ct
d519d62c60
strip line endings from reading cookies 2021-12-02 22:55:43 -08:00
Puyodead1
666c5c2122
Update feature_request.yml 2021-11-30 11:35:42 -05:00
Puyodead1
3c0a95a0a1
Update feature_request.yml 2021-11-30 11:34:50 -05:00
Puyodead1
317eb7c6b1
Update feature_request.yml 2021-11-30 11:32:44 -05:00
Puyodead1
1f79fb7430
fix feature request form 2021-11-30 11:30:19 -05:00
Puyodead1
5966b1662e
Delete feature_request.md 2021-11-30 11:29:05 -05:00
Puyodead1
de1419e711
Create feature_request.yml 2021-11-30 11:28:51 -05:00
Puyodead1
064cbb028b
Delete bug_report.md 2021-11-30 11:18:26 -05:00
Puyodead1
adfb204213
Create bug_report.yml 2021-11-30 11:18:10 -05:00
Puyodead1
b516c9a83a
Create FUNDING.yml 2021-11-30 11:01:04 -05:00
Puyodead1
b26c8f4b5b append lecture id to lecture assets 2021-11-28 00:53:54 -05:00
Puyodead1
8fb8d9f59d
remove unused dependency 2021-11-28 00:42:08 -05:00
Puyodead1
60eee56233 fix: attachments having id prepended to start 2021-11-28 00:04:24 -05:00
Puyodead1
152e6e1fd1 ci: stale action 2021-11-27 23:36:38 -05:00
Puyodead1
39041ff122 fix sanitization problems
+ Switched sanitization to pathvalidate package
- Removed sanitize.py file
2021-11-27 12:56:43 -05:00
Puyodead1
7c508a0762 some fixes for char decoding errors 2021-11-27 12:35:27 -05:00
Puyodead1
b98f35cec6
add warning about account suspensions 2021-11-26 22:38:40 -05:00
Puyodead1
cd15ab76d9 Create stale.yml 2021-11-24 15:11:11 -05:00
Puyodead1
0719800145 fix: KID extraction error handling
+ extract_kid method will raise an error if the file does not exist
+ If KID extraction fails, the lecture will be skipped instead of causing the program to exit
2021-11-22 11:36:05 -05:00
Puyodead1
59538b24ce
fix typo in readme 2021-11-21 00:52:32 -05:00
Puyodead1
95b30841dc Add --disable-ipv6 option
+ Added option to disable ipv6 in aria2
+ Updated README to reflect argument changes
2021-11-19 16:17:01 -05:00
Puyodead1
97ca2cf401 updates to README and some minor code changes
+ Use proper exit code of 1 on errors
+ Added error handling for subscription based course info extraction failure
+ Slightly more verbose login failure message
+ Updates to README to improve clarification among other things
2021-11-11 23:14:51 -05:00
Puyodead1
a06ef516ad
Update requirements.txt 2021-11-10 19:27:47 -05:00
Puyodead1
8ba33270ce fix: cookies should be a string not a dict
Resolves #72
2021-11-10 09:12:11 -05:00
Puyodead1
eb3257f374 update to allow downloading if using udemy subscription
+ New requirements: `beautifulsoup4` and `lxml`
+ Added support for downloading courses included in subscription plans
+ Updated README to reflect changes
2021-11-09 20:41:25 -05:00
Puyodead1
ecc46deb6b
Update README.md 2021-11-05 10:31:25 -04:00
Puyodead1
f6918e497c Bug fixes
- Remove user agent to fix 403 errors when fetching courses
- Fix ``saved`` folder not being created if it doesnt exist
2021-10-16 13:04:24 -04:00
Puyodead1
2c2e0a5c23 print stacktrace on lecture download error 2021-10-13 08:43:01 -04:00
Puyodead1
4ff903b247
Update bug_report.md 2021-09-25 22:24:30 -04:00
Puyodead1
1dffcd24b0 Update issue templates 2021-09-25 22:23:04 -04:00
Puyodead1
f4e26ff84b
bump version 2021-08-20 18:32:26 -04:00
Puyodead1
69b44b9421 Merge branch 'master' of https://github.com/Puyodead1/udemy-downloader 2021-08-20 18:30:00 -04:00
Puyodead1
5fff05c0d0 add fix for 403 errors - by gyuriX 2021-08-20 18:29:39 -04:00
Puyodead1
58e0179d8d
Update requirements.txt 2021-08-20 16:28:14 -04:00
Puyodead1
1f33e28f5e
Remove git dependency 2021-08-20 16:27:55 -04:00
Puyodead1
a2748d98a4 Revert "Merge branch 'feat-mkv'"
This reverts commit e835ab6eb1819c72dc23bb4bbe16d1238405975e, reversing
changes made to 034fcc6b50a3d4a4c8f7506ea9bac58579f55a9a.
2021-08-10 00:01:07 -04:00
Puyodead1
bb60297821 Revert "Update main.py"
This reverts commit 480173a462648b0f9db71c4e1559aecddc654e00.
2021-08-10 00:01:05 -04:00
Puyodead1
9a51347dfa Revert "bug fix"
This reverts commit d47a3524dd410c04ba2bf06b561413e9b1d9ebd9.
2021-08-10 00:01:02 -04:00
Puyodead1
d47a3524dd bug fix
+ Fix another error
2021-08-09 10:27:52 -04:00
Puyodead1
480173a462 Update main.py
+ Fix #44
2021-08-09 02:56:14 -04:00
Puyodead1
e835ab6eb1 Merge branch 'feat-mkv' 2021-08-08 14:21:56 -04:00
Puyodead1
034fcc6b50 bump version 2021-08-08 14:16:03 -04:00
Puyodead1
778d8e6f56 Bug fix
+ Fixes an error when trying to rename hls file
2021-08-08 14:15:07 -04:00
Puyodead1
9f1d7d9119 update the rest of the chapter titles 2021-08-08 14:06:21 -04:00
Puyodead1
ca40ff2b6d Updates
+ Update sanitization regex to clean exclaimation points
+ Update _sanitize method to use _clean method
+ Moved lecture file exist check to parse method, this will now also check if html files exist
+ Update chapter folder titles to use - for separating number from name
2021-08-08 14:03:24 -04:00
Puyodead1
6137e44d76 Update useragent 2021-08-08 13:43:06 -04:00
Puyodead1
5ec615e4e3 Updates and bug fixes
+ Fixed mis-named variable
+ Removed unused ``working_dir`` code
+ Fixed issue with course info feature not working
+ Added versioning system to help with debugging
+ Updated requirements file
2021-08-08 13:40:23 -04:00
Puyodead1
5b592923dc
Update README.md
Add a note about needing decryption keys
2021-08-04 15:10:45 -04:00
Puyodead1
1f72e875f5 Update main.py 2021-08-04 14:56:48 -04:00
Puyodead1
c8945b8091 Update README
+ added note about bearer token from env being deprecated
+ updated example commands to reflect code changes
2021-08-02 20:53:07 -04:00
Puyodead1
845c0bde58 restructure method parameters
+ made all variables global so we don't have to keep passing them to methods
+ renamed ``concurrent_downloads`` to ``concurrent_connections``
+ added ``use_mkv`` option
+ fixed bug where keyfile was required to just use help command
+ updated ``README.md`` to reflect code changes
2021-08-02 20:45:48 -04:00
Puyodead1
6d4d93c9d8 fix: issue with hls downloads being deleted during cleanup process
Encrypted HLS Files were being downloaded with the same name as the final merged lecture causing it to be removed during the cleanup process. To fix this, ``.encrypted`` is now appended to the encrypted files.
2021-07-30 12:35:52 -04:00
Puyodead1
52e4613add add missing requirements 2021-07-22 12:15:12 -04:00
Puyodead1
400316e1b3 Fix non-drm lectures not having proper file extension 2021-07-22 12:04:31 -04:00
Puyodead1
96578c2327
document yt-dlp dependency 2021-07-15 08:31:13 -07:00
Puyodead1
e6703e1106
Merge pull request #33 from evyatar/patch-1
Update requirements.txt
2021-06-30 15:18:52 -04:00
Evyatar
efb72b3d66
Update requirements.txt
added bitstring to requirements file
2021-06-30 20:13:54 +03:00
Puyodead1
171f7b7719 bug fixes 2021-06-28 14:50:56 -04:00
Puyodead1
59b6419ade change directory to chapter directory instead of using full path 2021-06-28 14:28:16 -04:00
Puyodead1
63fb7b4486 change space replacment character from - to _ 2021-06-28 14:19:16 -04:00
Puyodead1
6d0d3edf78 add better error handling to handle_segments method to catch failed downloads, the program will no longer exit 2021-06-27 21:51:42 -04:00
Puyodead1
4c0f4d2225 Merge branch 'master' of https://github.com/Puyodead1/udemy-downloader 2021-06-18 16:54:44 -04:00
Puyodead1
d9a72c8878 Limit the max characters for course and lecture names to 255 due to windows having a max of 260 2021-06-18 16:54:34 -04:00
Puyodead1
a596571694
fix a typo 2021-06-17 17:21:17 -04:00
Puyodead1
d6194589be
Merge pull request #31 from biplobsd/master
without any flag, ffmpeg, mp4decrypt screen output, standard error (stderr)
2021-06-06 19:00:16 -04:00
Biplob Sutradhar
d7df8b18b5
without flag run ffmpeg, mp4decrypt stderr streem 2021-06-06 21:36:06 +06:00
Biplob Sutradhar
2b3fb655ca Merge branch 'Puyodead1:master' into master 2021-06-06 21:27:25 +06:00
Puyodead1
26eac17f46
Merge pull request #30 from Puyodead1/revert-29-patch-1
Revert "Update main.py"
2021-06-05 19:34:44 -04:00
Puyodead1
a7c02ca6ac
Revert "Update main.py" 2021-06-05 19:33:50 -04:00
Puyodead1
bdf6357218
Merge pull request #29 from mthibaut-hbc/patch-1
Update main.py
2021-06-05 11:03:17 -04:00
mthibaut-hbc
3266b2a70c
Update main.py
Disable ipv6 for ariac2 to prevent "A socket operation was attempted to an unreachable network" error
2021-06-05 14:51:50 +02:00
Puyodead1
4ac49ec30d should i even add a commit message at this point? 2021-06-03 12:37:32 -04:00
Puyodead1
d7bfdda82e
Remove unused variable 2021-06-01 17:52:45 -04:00
Puyodead1
1d43d19a47
Merge pull request #27 from Puyodead1/feat-ytdlp
Feat: YT-DLP
2021-06-01 10:18:41 -04:00
Puyodead1
1d51b9be2f Fix: subscribed courses url missing portal name variable 2021-06-01 10:15:16 -04:00
Puyodead1
31e3802cb1 Fix: Append asset id to filename of assets
Fixes #25
2021-06-01 09:05:07 -04:00
Puyodead1
108d3bd19a Reduce max number of current downloads a user can specify 2021-05-30 17:34:03 -04:00
Puyodead1
2afef1cb41 Feat: Switch to yt-dlp for downloading HLS streams so we can use Aria2c
+ HLS streams are now downloaded with yt-dlp
- Removed FFMPEG download class
+ Added a new argument ``-cd``,``--concurrent-downloads`` to specify the max number of segments downloading at a time (default is 10)
+ Updated README to reflect code changes
2021-05-30 17:32:22 -04:00
Puyodead1
60addf51d9
Fix: FFMPEG check not being called
Resolves #23
2021-05-30 17:25:38 -04:00
Puyodead1
56e719b59f Feat: Switch to yt-dlp to avoid windows command line limitations 2021-05-30 16:49:29 -04:00
Biplob Sutradhar
a205ec91bf Merge branch 'Puyodead1:master' into master 2021-05-29 15:59:11 +06:00
Puyodead1
7a77a528aa Update README.md 2021-05-28 23:20:56 -04:00
Puyodead1
6a850d52f2 Update README and .env.sample
+ Add aria2c requirement
+ Change bearer token instructions to link to udemy-dl guides
+ Updated .env.sample to reflect code changes
2021-05-28 23:17:47 -04:00
Puyodead1
5b548c737b Update README.md 2021-05-28 23:11:23 -04:00
Puyodead1
c90f9c7584 Merge branch 'feat_performance_usage_improvements' 2021-05-28 23:07:59 -04:00
Puyodead1
9f3bda6c6c Revert "Merge branch 'master' into feat_aria2c"
This reverts commit e6dcde0335e8fdd7278dc72e9233034a5efc2748, reversing
changes made to 1ad4f1eddee0e34d028e3e74bf331a92a8388a36.
2021-05-28 23:05:54 -04:00
Puyodead1
e6dcde0335 Merge branch 'master' into feat_aria2c 2021-05-28 23:04:56 -04:00
Puyodead1
1ad4f1edde Feat: Info argument
+ Added info argument to print course information
+ Updated spacing of some text to be more 'tree' like and easier to read
2021-05-28 16:59:52 -04:00
Puyodead1
66aad0dc50 Fix: 1080p not being downloaded when selected 2021-05-28 15:28:03 -04:00
Puyodead1
1fa5bdba90 Fix: caption files having duplicated file extension 2021-05-28 10:19:37 -04:00
Puyodead1
6a2f237969
Add missing urls and methods
"My Courses" and collections
2021-05-28 10:09:50 -04:00
Puyodead1
4d428ea89d Add missing urls and methods for fetching "My Courses" and collections 2021-05-28 10:09:21 -04:00
Puyodead1
ee7be61f6a save external links 2021-05-28 09:42:40 -04:00
Puyodead1
f6ea730215 Remove unused variable and update some strings 2021-05-28 09:31:09 -04:00
Puyodead1
88c32ea55d Add checks for external tools a bug where assets would have their extension appended twice 2021-05-28 09:25:08 -04:00
Puyodead1
cffbcbaa0a Fix: TypeError when exception is caught by handle_segments method 2021-05-28 09:04:17 -04:00
Puyodead1
758f78831b Fix: incorrect worst quality being downloaded instead of best for non-drm videos with auto quality selection 2021-05-28 08:49:11 -04:00
Puyodead1
354b85e142 Fix downloaded files missing file extensions 2021-05-28 08:44:26 -04:00
Puyodead1
d20f15fb6a Use aria2c for asset downloading and non-drm lectures 2021-05-28 08:36:54 -04:00
Puyodead1
05c6c84d55 Remove unused function 2021-05-28 08:12:51 -04:00
Puyodead1
af8b565e23
Remove unused import 2021-05-28 08:10:55 -04:00
Puyodead1
f8e0c790cc Remove unused import 2021-05-28 08:10:27 -04:00
Puyodead1
10b22a6e0b
Remove debug print 2021-05-27 22:02:45 -04:00
Puyodead1
56a1994443 remove a debug print 2021-05-27 21:59:12 -04:00
Puyodead1
3cc22520c8 Added Aria2c
Aria2c is now used for downloading segments as fast as possible (Closes #20)
2021-05-27 21:18:40 -04:00
Puyodead1
67748301de Renamed ffmpeg.py so it doesnt conflict with the ffmpeg command
Removed the .exe from ffmpeg commands and renamed ffmpeg.py to pyffmpeg.py so that the program will still work on linux based systems and not try to execute the ffmpeg python file
2021-05-27 18:28:06 -04:00
Puyodead1
6c2690b856 Fixed an unexpected error where the program would try to execute the ffmpeg.py file instead of the .exe during mux process 2021-05-27 18:22:51 -04:00
Puyodead1
f8521fd84c remove todo tag 2021-05-27 18:10:38 -04:00
Puyodead1
5ffef4736e HLS parsing for 1080p+ quality
+ Added a new command argument ``--skip-hls`` to skip parsing hls playlists
+ Updated README to reflect code changes
2021-05-27 17:38:29 -04:00
Puyodead1
f0e06106fc
Remove un-needed requirements 2021-05-27 01:17:01 -04:00
Puyodead1
b7b27419fd Properly catch connection error and retry (maybe) 2021-05-26 23:39:09 -04:00
Puyodead1
b667420dc2 Removed unused method, update description for quality argument 2021-05-26 23:28:49 -04:00
Puyodead1
2667629c93 Big Changes
- Removed the old ``dashdownloader_multisegment.py`` file
- Removed the ``downloader.py`` file
+ Added missing requirement to ``requirements.txt``
+ Added sanitization class
+ Updated ``vtt_to_srt.py`` to use path.join for better cross-platform support
+ Updated README to reflect code changes
- Removed the quality restriction since there are some wacky non-standard qualities and I can't possibly predict and list them all :P
+ Changed the way fallback qualities are selected so it selects the closest quality to the requested one (ex. you want 576 but the closest are 480 and 720, 576 will be selected since its the closer to 576)
+ Switched to sessions
+ Program no longer quits if decryption key isn't found, we continue downloading segments, unencrypted video, and assets
+ Program will quit before starting downloads if the keyfile doesn't exist
+ Added an argument to keep vtt caption files ``--keep-vtt``
+ Properly handle large courses (code credit to r0oth3x49) (Fixes #18)
+ Updated parsing for course data (modified from code by r0oth3x49)

This update should be considered as unstable, it will probably have problems. If you find an issue, please create a GitHub issue.
2021-05-26 23:18:52 -04:00
Biplob Sutradhar
b496507e0b
undefine os 2021-05-26 12:59:23 +06:00
Puyodead1
a236156a4d
Merge pull request #16 from biplobsd/master
Unexpected behavior on ubuntu with path format fixs
2021-05-25 07:59:32 -04:00
Biplob Sutradhar
5af8a95925
Failed to parse creation_time fix both plf 2021-05-25 16:19:12 +06:00
Biplob Sutradhar
840a6f6815
Failed to parse creation_time fix 2021-05-25 12:04:12 +06:00
Biplob Sutradhar
134652d6e6
path 2021-05-24 22:35:49 +06:00
Biplob Sutradhar
86fa241ded
path 2021-05-24 22:29:00 +06:00
Biplob Sutradhar
50fb9534d8
path 2021-05-24 22:27:21 +06:00
Puyodead1
1471f58ef7 Merge branch 'master' of https://github.com/Puyodead1/Drm-Dash-stream-downloader 2021-05-23 20:50:30 -04:00
Puyodead1
65db666706
I like badges, do you? 2021-05-23 20:44:26 -04:00
Puyodead1
7f399c71dd
Fix: stray thread arg 2021-05-21 21:31:24 -04:00
Puyodead1
cb98f57bd0 Update README.md
- Removed threading related stuff
2021-05-21 19:52:29 -04:00
Puyodead1
9a1a318f93 Small bug fixes and removed experimental downloader
+ Fixed bug with video being downloaded in place of audio
+ Minor tweaks
- Removed the threaded downloader from main code, the current download system is the most reliable.
2021-05-21 19:50:39 -04:00
Puyodead1
aab19bf66f New Experimental Downloader, bug fixes, and small updates
+ Updated cleanup function to remove the entire temporary lecture folder instead of just leaving behind tons of empty folders
+ Fixed typo in mux function
+ Segment count is now properly calculated from segment timeline
+ Manifest is now parsed from the URL instead of being downloaded, this should be better for downloading multiple courses at once.
+ Fixed a bug where audio content_type would try to find a max quality
+ New Downloader: Threaded Downloader uses multiple threads to download files, this should improve download speeds greatly. By default, the threaded downloader is not used, you can use the threaded downloader by passing ``--use-threaded-downloader``. By default, it only uses 10 threads, you can set a custom number of threads with the ``--threads`` option
2021-05-21 13:38:24 -04:00
Puyodead1
88a411d708 Path updates
- Removed a few unused imports
+ Reworked the way paths are formed so they shouldn't be a problem on other operating systems

NOTE: the new dependencies in requirements.txt are NOT required at this time
2021-05-21 09:27:49 -04:00
Puyodead1
f62bb52816 Update README.md
+ Add badges to README
2021-05-20 22:56:42 -04:00
Puyodead1
0782c42df7 Major Updates to performance and usage
+ You can now pass the course url to the -c argument instead of the course id
+ Fixed function doc tags
+ Added experimental support for business accounts (the program should auto detect the subdomain from the course url and use it for all requests, you shouldn't need to edit the file)
- Removed the useless creation time metadata from ffmpeg
+ Updated arguments, ``course_url`` is now a required argument
- ``course_url`` will no longer be pulled from the .env file, you can still use the .env for bearer tokens
+ Courses are now downloaded into folders titled by the course name instead of the course id
+ Updated README to reflect updates
+ Fixed a bug where external url files would be appended to each time a the downloader is restarted on a course
2021-05-20 22:34:56 -04:00
Puyodead1
5dc9211fa0 Merge branch 'master' of https://github.com/Puyodead1/Drm-Dash-stream-downloader 2021-05-20 22:13:01 -04:00
Puyodead1
6edc83d606 Merge branch 'feat-assets-quality' 2021-05-20 20:55:31 -04:00
Puyodead1
37b210b7da Merge branch 'feat-assets-quality' 2021-05-20 20:51:09 -04:00
Puyodead1
b8b79b2293 Merge branch 'feat-improved-asset-downloads' 2021-05-20 20:50:34 -04:00
Puyodead1
ec3ba980b1 removed useless variable 2021-05-20 20:49:52 -04:00
Puyodead1
e5247edbd8
Create dependabot.yml 2021-05-20 19:56:09 -04:00
Puyodead1
1a7314b62f added articles and external urls to the asset downloads (UNTESTED, only pushing to github to transfer between computers :P) 2021-05-20 10:45:28 -04:00
Puyodead1
c88414189b Fix: course directory not being created 2021-05-19 19:43:29 -04:00
Puyodead1
e722eb3fbc fix: course directory not being created 2021-05-19 19:41:53 -04:00
Puyodead1
e383ca945d
Merge pull request #9 from Puyodead1/feat-assets-quality
Feat: Assets, Qualities, and more
2021-05-19 19:10:38 -04:00
Puyodead1
4803f46164 Course Folder
+ Course chapter folder are now placed in a folder, the name of the folder is the course id

**If you have any downloads in-progress, make a new folder and name it the course id, and move the chapter folders into it before starting the download again.**

Closes #8
2021-05-19 18:59:43 -04:00
Puyodead1
048fbe09bf Updates
+ Add ability to skip downloading lecture videos
+ You can now specify the Bearer token and course id as an argument to the program (see advanced usage)
+ Updated Advanced Usage section of readme
2021-05-19 17:50:20 -04:00
Puyodead1
77e57e324d remove a debug print 2021-05-19 17:01:46 -04:00
Puyodead1
5667c3df13 Add error handling and download retries for captions
+ Downloader will now raise an exception for failed requests
+ Captions will retry 3 times on a download failure before skipping
+ Catch errors from downloader
2021-05-19 17:00:10 -04:00
Puyodead1
0f08002275 updated information for captions 2021-05-19 16:09:47 -04:00
Puyodead1
d011533a6a Update credits in README
+ Add https://github.com/lbrayner/vtt-to-srt
2021-05-19 16:04:36 -04:00
Puyodead1
6c5b7870a9 Subtitle Support
+ Added support for downloading subtitles (see readme for usage)
2021-05-19 15:59:49 -04:00
Puyodead1
a867f82f2b Asset Downloading and Quality Selection
+ Added arguments
+ You can specify the download quality, if the quality you request cant be found, it will fallback to the closest (ex. you want 144, but the lowest is 360 than 360 gets downloaded. you want 1080 but 720 is the highest, 720 gets downloaded)
Note: Non-DRM and DRM video have different quality availabilities, by the looks of it, DRM has a min of 360 and a max of 1080. Non-DRM has a low of 144, and a max of 720
2021-05-19 14:43:20 -04:00
23 changed files with 3205 additions and 658 deletions

View File

@ -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
View File

@ -0,0 +1,3 @@
github: Puyodead1
ko_fi: puyodead1
patreon: Puyodead1

60
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View 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

View 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
View File

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"

21
.gitignore vendored
View File

@ -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
View 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
View File

@ -1,79 +1,226 @@
# Udemy Downloader with DRM support # Udemy Downloader with DRM support
[![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com)
[![forthebadge](https://forthebadge.com/images/badges/designed-in-ms-paint.svg)](https://forthebadge.com)
[![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://forthebadge.com)
[![forthebadge](https://forthebadge.com/images/badges/approved-by-george-costanza.svg)](https://forthebadge.com)
![GitHub forks](https://img.shields.io/github/forks/Puyodead1/udemy-downloader?style=for-the-badge)
![GitHub Repo stars](https://img.shields.io/github/stars/Puyodead1/udemy-downloader?style=for-the-badge)
![GitHub](https://img.shields.io/github/license/Puyodead1/udemy-downloader?style=for-the-badge)
# 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.
- ![Valid udemy api requests](https://i.imgur.com/Or371l7.png)
- click a random request
- locate the `Request Headers` section
- copy the the text after `Authorization`, it should look like `Bearer xxxxxxxxxxx`
- ![bearer token example](https://i.imgur.com/FhQdwgD.png)
- 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.
- ![request url](https://i.imgur.com/EUIV3bk.png) >
- 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`
- ![keyfile example](https://i.imgur.com/e5aU0ng.png)
- ![example key and kid from console](https://i.imgur.com/awgndZA.png)
It is up to you to aquire the key and key id. ## Cookies
- Enter the key and key id in the `keyfile.json` > [!TIP]
- ![keyfile example](https://i.imgur.com/wLPsqOR.png) > Cookies are not required for individually purchased courses.
- ![example key and kid from console](https://i.imgur.com/awgndZA.png)
### 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
View 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

View File

@ -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
View 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

View File

@ -1,3 +1,3 @@
{ {
"KeyID": "key" "the key id goes here": "the key goes here"
} }

2218
main.py

File diff suppressed because it is too large Load Diff

View File

@ -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

View 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>

View 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>

View 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
View 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)

View File

@ -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
View 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
View 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
View 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)

View File

@ -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)