122 lines
3.6 KiB
Plaintext
122 lines
3.6 KiB
Plaintext
TVP VOD Downloader
|
||||
|
|
==================
|
|||
|
|
|
|||
|
|
Downloads Widevine L3 DRM-protected videos from vod.tvp.pl as decrypted MP4 files.
|
|||
|
|
|
|||
|
|
|
|||
|
|
REQUIREMENTS
|
|||
|
|
------------
|
|||
|
|
Python packages:
|
|||
|
|
pip install pywidevine requests
|
|||
|
|
|
|||
|
|
System tools (already present in ./tools/):
|
|||
|
|
tools/N_m3u8DL-RE — DASH segment downloader
|
|||
|
|
tools/mp4decrypt — CENC decryption (Bento4)
|
|||
|
|
ffmpeg — must be in PATH (used for muxing)
|
|||
|
|
|
|||
|
|
Widevine device file (already present in ./L3/):
|
|||
|
|
L3/google_sdk_gphone64_x86_64_3198748f_22596_l3.wvd
|
|||
|
|
|
|||
|
|
|
|||
|
|
USAGE
|
|||
|
|
-----
|
|||
|
|
|
|||
|
|
1. Download a single episode by product ID:
|
|||
|
|
|
|||
|
|
python3 tvp_download.py 2993670
|
|||
|
|
|
|||
|
|
2. Download multiple episodes by product ID:
|
|||
|
|
|
|||
|
|
python3 tvp_download.py 2726892 2785411 2871666
|
|||
|
|
|
|||
|
|
3. Download all episodes of a series by series ID:
|
|||
|
|
|
|||
|
|
python3 tvp_download.py --series 2726890
|
|||
|
|
|
|||
|
|
Output files are saved to ./downloads/ as:
|
|||
|
|
sXXeXX_title.mp4 (e.g. s01e13_zaslubiny.mp4)
|
|||
|
|
|
|||
|
|
|
|||
|
|
FINDING IDs
|
|||
|
|
-----------
|
|||
|
|
|
|||
|
|
Product ID and series ID come from the vod.tvp.pl URL:
|
|||
|
|
|
|||
|
|
https://vod.tvp.pl/seriale,18/nazwa-serialu-odcinki,{SERIES_ID}/odcinek-N,S01E01,{PRODUCT_ID}
|
|||
|
|
|
|||
|
|
Example — "Wojna zastępcza":
|
|||
|
|
Series ID: 2726890 (from the URL segment "...-odcinki,2726890/...")
|
|||
|
|
Episode 13: 2993670 (from the URL segment "...,S01E13,2993670")
|
|||
|
|
|
|||
|
|
You can also pass the URL directly to the browser and copy the product ID from the address bar
|
|||
|
|
when playing an episode.
|
|||
|
|
|
|||
|
|
|
|||
|
|
HOW IT WORKS
|
|||
|
|
------------
|
|||
|
|
|
|||
|
|
For each episode:
|
|||
|
|
1. Fetches the DASH playlist:
|
|||
|
|
GET /api/products/{id}/videos/playlist?platform=BROWSER&videoType=MOVIE
|
|||
|
|
2. Extracts the Widevine PSSH from the MPD manifest.
|
|||
|
|
3. Sends a license challenge to TVP's Widevine license server using the
|
|||
|
|
L3 device certificate (.wvd file) and pywidevine.
|
|||
|
|
4. Gets the AES-128 content key from the license response.
|
|||
|
|
5. Downloads all encrypted video and audio segments via N_m3u8DL-RE.
|
|||
|
|
6. Decrypts segments with mp4decrypt using the content key.
|
|||
|
|
7. Muxes video + audio into a single MP4 with ffmpeg.
|
|||
|
|
|
|||
|
|
|
|||
|
|
OUTPUT
|
|||
|
|
------
|
|||
|
|
|
|||
|
|
Each episode is saved as a single MP4:
|
|||
|
|
./downloads/{product_id}_{title}.mp4
|
|||
|
|
|
|||
|
|
Quality: best available (typically 1920x1080 H.264, AAC audio, Polish).
|
|||
|
|
Size: roughly 1.2-1.5 GB per 45-minute episode at full quality.
|
|||
|
|
|
|||
|
|
|
|||
|
|
EXAMPLE SESSION
|
|||
|
|
---------------
|
|||
|
|
|
|||
|
|
# List all episodes of "Wojna zastępcza" and download them all:
|
|||
|
|
python3 tvp_download.py --series 2726890
|
|||
|
|
|
|||
|
|
# Output:
|
|||
|
|
# Found 13 episodes:
|
|||
|
|
# E01 | 2726892 | odc. 1 – Werbunek
|
|||
|
|
# E02 | 2785411 | odc. 2 – Legnica
|
|||
|
|
# ...
|
|||
|
|
# ============================================================
|
|||
|
|
# Product 2726892: 'odc. 1 – Werbunek' [EPISODE]
|
|||
|
|
# Key: d431b94c...:c3e2e41c...
|
|||
|
|
# Running: N_m3u8DL-RE 2726892_odc__1___Werbunek
|
|||
|
|
# [+] Saved: s01e01_werbunek.mp4 (1248 MB)
|
|||
|
|
# ...
|
|||
|
|
|
|||
|
|
|
|||
|
|
TROUBLESHOOTING
|
|||
|
|
---------------
|
|||
|
|
|
|||
|
|
"No .wvd file found":
|
|||
|
|
The Widevine device file is missing from ./L3/. The file
|
|||
|
|
google_sdk_gphone64_x86_64_3198748f_22596_l3.wvd must be present.
|
|||
|
|
|
|||
|
|
"Playlist fetch failed" / 404:
|
|||
|
|
The product ID may be wrong, or the content may have been removed from TVP VOD.
|
|||
|
|
Verify the URL on vod.tvp.pl.
|
|||
|
|
|
|||
|
|
"License server 406":
|
|||
|
|
Do not add Accept: application/json to the license POST request.
|
|||
|
|
The fetch_content_key() function uses only Content-Type: application/octet-stream.
|
|||
|
|
|
|||
|
|
"No CONTENT keys returned":
|
|||
|
|
The WVD file may have system_id=0 (broken). The script auto-selects the WVD
|
|||
|
|
with a non-zero system_id. If all WVDs have system_id=0, the device file needs
|
|||
|
|
to be regenerated.
|
|||
|
|
|
|||
|
|
Download hangs at 0%:
|
|||
|
|
Avoid piping N_m3u8DL-RE output through head or other tools — the broken pipe
|
|||
|
|
stalls the process. Always let it write to stdout directly or redirect to a file.
|