i create batch file, look better and lift my feet up and rest while they doing the rest of episodes every season. course with subtitles and chapter
https://www.youtube.com/watch?v=oUzISmBO4FQ
+ Reply to Thread
Results 91 to 120 of 124
-
-
-
-
It was more than just the original arguments for VT that were clunky., to be honest. An absolute headache to set up right and maintain.
Personally, run a self written binary through a loop in fish(https://fishshell.com/ other shells are available) keeps it all neat and consistent. -
-
Thanks for sharing your script, it works great!
However, for some episodes, I get:
Code:[!] Invalid URL !!! [!] Failed getting VOD stream !!!
(e.g. for https://www.channel4.com/programmes/8-out-of-10-cats-does-countdown/on-demand/60792-002)
Long shot, but is there anything that can be done for these? I notice there are some assets from the browser call (/vod/stream/60792-002) -
-
when you get invalid url just wait 10-15 seconds and retry
same thing happens with diazole's script sometimes, at least for me -
Indeed, your script worked a treat! Thanks for sharing it! Is there anyway to automate fetching the license URL, mpd url and token, rather than copying the cURL command to clipboard (and just pass the episode URL)? Or have you found any other ways to batch download many episodes?
Thanks! -
Thanks for this, it worked perfectly for me.
Can anyone help me in regard to selecting lower quality video? For some shows I'm happy with 720p but the script defaults to 1080p.
I can see the below when editing the script but does this relate to the selection of the quality?
Code:def get_resolution(file_name: str): high_quality = '1080p' med_quality = '720p' low_quality = '420p' if high_quality in file_name: return high_quality if med_quality in file_name: return med_quality if low_quality in file_name: return low_quality return ''
-
Code:
yt-dlp --no-warnings --allow-u -f "bv*[height<=720]" <mpd> -o <outputfilename.mp4> yt-dlp --no-warnings --allow-u -f "audio_eng=128000" <mpd> -o <outputfilename.m4a>
Last edited by Sorenb; 1st Mar 2023 at 09:13.
-
-
-
Thanks again for the pointers.
I've edited the script to the below and saved it separately so I have the option to run best vid or 720p and it works perfectly.
Code:'bv*[height<=720],wa', # Prevent audo description
-
Glad it works, but I still feel using wa is just as bad as using ba.
Since ytdlp fails gracefully, set up the script to grab the streams individually and name them in a manner to suit your needs.
Code:720p.enc.mp4 audio.enc.m4a audio_eng.enc.m4a
So,Code:yt-dlp --allow-u -f 'bv*[height<=720] -o 720p.enc.mp4 yt-dlp --allow-u -f "audio=128000" -o audio.enc.m4a yt-dlp --allow-u -f ""audio_eng=128000" -o audio_eng.enc.m4a"
Code:if audio.enc.m4a exists but audio_eng.enc.m4a doesn't, then do this..... if audio_eng.enc.m4a exists but audio.enc.m4a doesn't, then do this.... if audio.enc.m4a AND audio_eng.enc.m4a exit do this....
Code:if [ -f audio.enc.m4a]
Code:if [ ! -f audio.enc.m4a]
No idea what it is in python offhand, most likely
Code:import os.path os.path.isfile(audio.enc.m4a)
Last edited by Sorenb; 2nd Mar 2023 at 14:13.
-
Can anyone see why the below won't work (I've edited out the main part of the URL for this post)? (the video plays fine via the site).
xxxxxxxx.com/programmes/the-great-house-giveaway/on-demand/68376-015
The script responds with:
[!] Invalid URL !!!
[!] Failed getting VOD stream !!! -
work fine with vinetrimmer
Code:--key ba0dfab470a1593fbb265b93e48eef2c:7fee5511dbe81b0a6ca89d52c26c2846
-
@iamghost:
That's the wrong episode. He wants S01E19, not S01E01.
The script by Diazole, and VT and other tools, are fetching the 5000kb/s streams, and after listing all the episodes, S01E19 is actually missing:
Code:Season 1: 19 episodes │ ├── 01. Series 1 Episode 1: Bradford, West Yorkshire │ ├── 02. Series 1 Episode 2: Gorton, Manchester │ ├── 03. Series 1 Episode 3: Leicester │ ├── 04. Series 1 Episode 4: Rugby │ ├── 05. Series 1 Episode 5: Valley, Isle of Anglesey │ ├── 06. Series 1 Episode 6: Pontlottyn, Welsh Valleys │ ├── 07. Series 1 Episode 7: Kidderminster │ ├── 08. Series 1 Episode 8: Mold, Wales │ ├── 09. Series 1 Episode 9: Prescot │ ├── 10. Series 1 Episode 10: Porthmadog │ ├── 11. Series 1 Episode 11: Newport │ ├── 12. Series 1 Episode 12: Neath │ ├── 13. Series 1 Episode 13: Oldbury │ ├── 14. Series 1 Episode 14: Liverpool │ ├── 15. Series 1 Episode 15: Wakefield │ ├── 16. Series 1 Episode 16: Kirkham, Lancashire │ ├── 17. Series 1 Episode 17: Glossop │ ├── 18. Series 1 Episode 18: Wigan │ └── 20. Series 1 Episode 20: Ashton-under-Lyne
Code:00000000000000000000000003778569:fc04e31c5f88cb978592eb541a846e72
-
Last edited by T33V33; 28th Mar 2023 at 15:28.
-
-
Yeah. The first version of Diazole's script grabbed the regular(web) 1080p version, and I still had it lying around. It works exactly the same.
Code:import argparse import base64 import json import os import re import shutil import subprocess import struct import sys import requests from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from pywidevine.pssh import PSSH from pywidevine.device import Device from pywidevine.cdm import Cdm from globals import DEFAULT_HEADERS, DOWNLOAD_DIR, MPD_HEADERS, TMP_DIR # pylint: disable=no-name-in-module _script_dir = os.path.dirname(os.path.realpath(__file__)) _proto_path = os.path.join(_script_dir, 'generated') sys.path.insert(0, _proto_path) # pylint: disable=wrong-import-position, wrong-import-order, import-error import widevine_pssh_data_pb2 as widevine # nopep8 class ComplexJsonEncoder(json.JSONEncoder): def default(self, o): if hasattr(o, 'to_json'): return o.to_json() return json.JSONEncoder.default(self, o) class Video: def __init__(self, video_type: str, url: str): self.video_type = video_type self.url = url def to_json(self): resp = {} if self.video_type != "": resp['type'] = self.video_type if self.url != "": resp['url'] = self.url return resp class DrmToday: def __init__(self, request_id: str, token: str, video: Video, message: str): self.request_id = request_id self.token = token self.video = video self.message = message def to_json(self): resp = {} if self.request_id != "": resp['request_id'] = self.request_id if self.token != "": resp['token'] = self.token if self.video != "": resp['video'] = self.video if self.message != "": resp['message'] = self.message return resp class Status: def __init__(self, success: bool, status_type: str): self.success = success self.status_type = status_type class VodConfig: def __init__(self, vodbs_url: str, key: str, iv: str, drm_today: DrmToday, message: str): self.vodbs_url = vodbs_url self.key = key self.iv = iv self.drm_today = drm_today self.message = message class VodStream: def __init__(self, token: str, uri: str, brand_title: str, episode_title: str): self.token = token self.uri = uri self.brand_title = brand_title self.episode_title = episode_title def to_json(self): resp = {} if self.token != "": resp['token'] = self.token if self.uri != "": resp['uri'] = self.uri return resp class LicenseResponse: def __init__(self, license_response: str, status: Status): self.license_response = license_response self.status = status def to_json(self): resp = {} if self.license_response != "": resp['license'] = self.license_response if self.status != "": resp['status'] = self.status return resp def decrypt_token(key: str, iv: str, token: str): try: cipher = AES.new(bytes(key, 'UTF-8'), AES.MODE_CBC, bytes(iv, 'UTF-8')) decoded_token = base64.b64decode(token) decrypted_string = unpad(cipher.decrypt( decoded_token), 16, style='pkcs7').decode('UTF-8') license_info = decrypted_string.split('|') return VodStream(license_info[1], license_info[0], '', '') except: # pylint:disable=bare-except print('[!] Failed decrypting VOD stream !!!') raise def get_vod_stream(url: str): try: req = requests.get(url) if req.status_code == requests.codes['not_found']: print('[!] Invalid URL !!!') sys.exit(1) req.raise_for_status resp = req.json() brand_title = str(resp['brandTitle']) brand_title = brand_title.replace(':', ' ') brand_title = brand_title.replace('/', ' ') episode_title = str(resp['episodeTitle']) episode_title = episode_title.replace(':', ' ') episode_title = episode_title.replace('/', ' ') vod_stream = VodStream('', '', brand_title, episode_title) for field in resp['videoProfiles']: if field['name'] == 'dashwv-dyn-stream-1': stream = field['streams'][0] vod_stream.token = stream['token'] vod_stream.uri = stream['uri'] return vod_stream raise # pylint: disable=misplaced-bare-raise except: # pylint:disable=bare-except print('[!] Failed getting VOD stream !!!') raise def get_asset_id(url: str): try: req = requests.get(url) req.raise_for_status init_data = re.search( '<script>window\.__PARAMS__ = (.*)</script>', ''.join( req.content.decode() .replace('\u200c', '') .replace('\r\n', '') .replace('undefined', 'null') ) ) init_data = json.loads(init_data.group(1)) asset_id = int(init_data['initialData']['selectedEpisode']['assetId']) if asset_id == 0: raise # pylint: disable=misplaced-bare-raise return asset_id except: # pylint:disable=bare-except print('[!] Failed getting asset ID !!!') raise def get_config(): try: req = requests.get( 'https://static.c4assets.com/all4-player/latest/bundle.app.js') req.raise_for_status configs = re.findall( "JSON\.parse\(\'(.*?)\'\)", ''.join( req.content.decode() .replace('\u200c', '') .replace('\\"', '\"') ) ) config = json.loads(configs[1]) video_type = config['protectionData']['com.widevine.alpha']['drmtoday']['video']['type'] message = config['protectionData']['com.widevine.alpha']['drmtoday']['message'] video = Video(video_type, '') drm_today = DrmToday('', '', video, message) vod_config = VodConfig( config['vodbsUrl'], config['bytes1'], config['bytes2'], drm_today, '') return vod_config except: # pylint:disable=bare-except print('[!] Failed getting production config !!!') raise def get_service_certificate(url: str, drm_today: DrmToday): try: req = requests.post(url, data=json.dumps( drm_today.to_json(), cls=ComplexJsonEncoder), headers=DEFAULT_HEADERS) req.raise_for_status resp = json.loads(req.content) license_response = resp['license'] status = Status(resp['status']['success'], resp['status']['type']) return LicenseResponse(license_response, status) except: # pylint:disable=bare-except print('[!] Failed getting signed DRM certificate !!!') raise def get_license_response(url: str, drm_today: DrmToday): try: req = requests.post(url, data=json.dumps( drm_today.to_json(), cls=ComplexJsonEncoder), headers=DEFAULT_HEADERS) req.raise_for_status resp = json.loads(req.content) license_response = resp['license'] status = Status(resp['status']['success'], resp['status']['type']) if not status.success: raise # pylint:disable=misplaced-bare-raise return LicenseResponse(license_response, status) except: # pylint:disable=bare-except print('[!] Failed getting license challenge !!!') raise def get_kid(url: str): try: req = requests.get(url, headers=MPD_HEADERS) req.raise_for_status kid = re.search('cenc:default_KID="(.*)"', req.text).group(1) return kid except: # pylint:disable=bare-except print('[!] Failed getting KID !!!') raise def generate_pssh(kid: str): try: wide_vine = widevine.WidevinePsshData() # pylint: disable=no-member wide_vine.key_id.append(base64.b16decode(kid.replace('-', ''))) wide_vine.provider = 'rbmch4tv' wide_vine.content_id = bytes(kid, 'UTF-8') wide_vine.policy = '' wide_vine.algorithm = 1 pssh_data = wide_vine.SerializeToString() ret = b'pssh' + struct.pack('>i', 0 << 24) ret += base64.b16decode('EDEF8BA979D64ACEA3C827DCD51D21ED') ret += struct.pack('>i', len(pssh_data)) ret += pssh_data pssh = struct.pack('>i', len(ret) + 4) + ret return base64.b64encode(pssh).decode() except: # pylint:disable=bare-except print('[!] Failed generating PSSH !!!') raise def get_file_output_title(brand_title: str, episode_title: str): try: title = re.search('^Series\s+(\d+)\s+Episode\s+(\d+)$', episode_title) if title is None: output_title = f'{brand_title} {episode_title} WEB-DL' output_title = ' '.join(output_title.split()) return output_title.replace(' ', '.') series = title.group(1) episode = title.group(2) if len(series) == 1: series = '0' + series if len(episode) == 1: episode = '0' + episode output_title = f'{brand_title} S{series}E{episode} WEB-DL' output_title = ' '.join(output_title.split()) return output_title.replace(' ', '.') except: # pylint:disable=bare-except print('[!] Failed getting output title !!!') raise def download_streams(mpd: str, output_title: str): try: args = [ './bin/yt-dlp.exe', '--downloader', 'aria2c', '--allow-unplayable-formats', '-q', '--no-warnings', '--progress', '-f', 'bv,wa', # Prevent audo description mpd, '-o', f'{TMP_DIR}/{output_title}/encrypted_{output_title}.%(height)sp.%(vcodec)s%(acodec)s.%(ext)s' ] subprocess.run(args, check=True) except: # pylint:disable=bare-except print('[!] Failed downloading streams !!!') raise def decrypt_streams(decryption_key: str, output_title: str): try: files = [] for file in os.listdir(f'{TMP_DIR}/{output_title}'): if output_title in file: input_file = f'{TMP_DIR}/{output_title}/{file}' file = file.replace('encrypted_', 'decrypted_') output_file = f'{TMP_DIR}/{output_title}/{file}' files.append(output_file) args = [ './bin/mp4decrypt.exe', '--key', decryption_key, input_file, output_file ] subprocess.run(args, check=True) return files except: # pylint:disable=bare-except print('[!] Failed decrypting streams !!!') raise def get_audio_codec(file_name: str): aac_codec = 'mp4a.40' ac3_codec = 'ac-3' mp3_codec = 'mp4a.6' if aac_codec in file_name: return 'AAC' if mp3_codec in file_name: return 'MP3' if ac3_codec in file_name: return 'AC-3' return '' def get_video_codec(file_name: str): h_264 = 'avc1.' h_265 = 'hev1.1' if h_264 in file_name: return 'H.264' if h_265 in file_name: return 'H.265' return '' def get_resolution(file_name: str): high_quality = '1080p' med_quality = '720p' low_quality = '420p' if high_quality in file_name: return high_quality if med_quality in file_name: return med_quality if low_quality in file_name: return low_quality return '' def merge_streams(files: list, output_title: str): try: video_codec = 'unknown' audio_codec = 'unknown' resolution = 'unknown' for file in files: a_codec = get_audio_codec(file) if a_codec: audio_codec = a_codec v_codec = get_video_codec(file) if v_codec: video_codec = v_codec v_resolution = get_resolution(file) if v_resolution: resolution = v_resolution output_dir = f'{DOWNLOAD_DIR}/{output_title}.{resolution}.{video_codec}.{audio_codec}' # This should check for the correct ext. output_file = f'{output_dir}/{output_title}.{resolution}.{video_codec}.{audio_codec}.mp4' os.mkdir(output_dir) args = [ './bin/ffmpeg.exe', '-hide_banner', '-loglevel', 'error', '-i', files[0], '-i', files[1], '-c', 'copy', output_file ] subprocess.run(args, check=True) shutil.rmtree(f'{TMP_DIR}/{output_title}') except: # pylint:disable=bare-except print('[!] Failed merging streams !!!') raise def create_argument_parser(): parser = argparse.ArgumentParser(description='Channel 4 downloader.') parser.add_argument( '--download', help='Download the episode', action='store_true' ) parser.add_argument( '--wvd', help='The file path to the WVD file generated by pywidevine' ) parser.add_argument( '--url', help='The URL of the episode to download' ) args = parser.parse_args() if not args.wvd or not args.url: parser.print_help() sys.exit(1) return args def main(): parser = create_argument_parser() wvd = parser.wvd url = parser.url download = parser.download # Get the prod config config = get_config() # Get the MPD and encrypted stream token encrypted_vod_stream = get_vod_stream(config.vodbs_url.replace( '{programmeId}', url.strip('/').split('/')[-1])) # Decrypt the stream token decrypted_vod_stream = decrypt_token( config.key, config.iv, encrypted_vod_stream.token) # Setup the initial license request config.drm_today.video.url = encrypted_vod_stream.uri # MPD config.drm_today.token = decrypted_vod_stream.token # Decrypted Token config.drm_today.request_id = get_asset_id(url) # Video asset ID # Get the SignedDrmCertificate (common privacy cert) service_cert = get_service_certificate( decrypted_vod_stream.uri, config.drm_today).license_response # Load the WVD and generate a session ID device = Device.load(wvd) cdm = Cdm.from_device(device) session_id = cdm.open() cdm.set_service_certificate(session_id, service_cert) # Get license challenge kid = get_kid(config.drm_today.video.url) # Generate the PSSH pssh = generate_pssh(kid) challenge = cdm.get_license_challenge( session_id, PSSH(pssh), privacy_mode=True) config.drm_today.message = base64.b64encode(challenge).decode('UTF-8') # Get license response license_response = get_license_response( decrypted_vod_stream.uri, config.drm_today) # Parse license challenge cdm.parse_license(session_id, license_response.license_response) terminal_size = os.get_terminal_size().columns print('*' * terminal_size) print(f'[ URL ] {url}') decryption_key = '' # Return keys for key in cdm.get_keys(session_id): if key.type == 'CONTENT': decryption_key = f'{key.kid.hex}:{key.key.hex()}' print(f'[{key.type}] {key.kid.hex}:{key.key.hex()}') print(f'[ MPD ] {config.drm_today.video.url}') print('*' * terminal_size) # Close session, disposes of session data cdm.close(session_id) if download: output_title = get_file_output_title( encrypted_vod_stream.brand_title, encrypted_vod_stream.episode_title) download_streams(config.drm_today.video.url, output_title) files = decrypt_streams(decryption_key, output_title) merge_streams(files, output_title) if __name__ == '__main__': main()
-
Thought I'd share my solution for the random descriptive audio track problem: get both!
. My mp4s have two tracks (if an audio descriptive track is available, it is put second). I quite like the audio descriptive tracks, since they'll be useful if I ever want to make some audiobooks...
In download_streams I added another parameter to args:Code:'--audio-multistreams',
Code:video_codec = 'unknown' audio_codec = 'unknown' resolution = 'unknown' video_stream_index = -1 audio_stream_index1 = -1 audio_stream_index2 = -1 fi = 0 for file in files: if 'decrypted_' in file: a_codec = get_audio_codec(file) if '.mp4' in file: video_codec = get_video_codec(file) video_stream_index = fi elif a_codec: audio_codec = a_codec if audio_stream_index1 == -1: audio_stream_index1 = fi else: audio_stream_index2 = fi if len(file) < len(files[audio_stream_index1]): #swap audio streams so that audio_stream_index1 points to the shorter file (without _1) audio_stream_index1, audio_stream_index2 = audio_stream_index2, audio_stream_index1 v_resolution = get_resolution(file) if v_resolution: resolution = v_resolution fi = fi + 1 output_dir = f'{DOWNLOAD_DIR}' output_file = f'{output_dir}/{output_title}.{resolution}.{video_codec}.{audio_codec}.mp4' if audio_stream_index2 == -1: args = [ 'ffmpeg', '-hide_banner', '-loglevel', 'error', '-i', files[video_stream_index], '-i', files[audio_stream_index1], '-map', '0:v', '-map', '1:a', '-c', 'copy', output_file ] else: args = [ 'ffmpeg', '-hide_banner', '-loglevel', 'error', '-i', files[video_stream_index], '-i', files[audio_stream_index1], '-i', files[audio_stream_index2], '-map', '0:v', '-map', '1:a', '-map', '2:a', '-c', 'copy', output_file ] subprocess.run(args, check=True)
-
Deccavox, would you mind explaining why I would be getting this result when running your all4.py script please...
Win 10, all pip installs have been installed,... and all requirements have been copied to WKS-KEYS folder. (script is being ran from an elevated cmd promt.)
It's probably the most obvious of answers but after searching the nest and looking through the code I cannot see where I am going wrong..
prog I am trying to d/l https://www.channel4.com/programmes/alone/on-demand/73983-001
within cmd prompt after running the script I have this:
Filter for wide (or acquire), do `Copy as cURL(cmd)` to get it on your clipboard NOTE: NOT `cURL(bash)`
then press Enter to continue....
After pressing Enter I get:
Filter for wide (or acquire), do `Copy as cURL(cmd)` to get it on your clipboard NOTE: NOT `cURL(bash)`
then press Enter to continue....
usage: all4.py [-h] [-d DATA] [-b DATA_BINARY] [-X X] [-H HEADER] [--compressed] [-k] [--user USER] [-i] [-s]
command url
all4.py: error: the following arguments are required: command, url
D:\YT_Rips\WKS-KEYS>
I have tried copying the ch4 page into my clipboard, thinking this is what is required, but obviously I have this wrong. any pointers please as to where I am going wrong. -
I type all that out and its staring me in the eyes.... pip install command and pip install url (although the "pip install url" did throw up some error messages, so not sure if that is actually installed or not.
hhmmmm I actually thought this was going to work... till this happened..
D:\YT_Rips\WKS-KEYS>pip install url
Collecting url
Using cached url-0.4.2.tar.gz (140 kB)
Installing build dependencies ... done
Getting requirements to build wheel ... done
Preparing metadata (pyproject.toml) ... done
Requirement already satisfied: six in c:\users\xxxxxxxxxx\appdata\local\programs\python\ python311\lib\site-packages (from url) (1.16.0)
Building wheels for collected packages: url
Building wheel for url (pyproject.toml) ... error
error: subprocess-exited-with-error
× Building wheel for url (pyproject.toml) did not run successfully.
│ exit code: 1
╰─> [17 lines of output]
C:\Users\xxxxxxxxxx\AppData\Local\Temp\pip-build-env-cbe9ubzo\overlay\Lib\site-packages\setuptools\dist.py:745: SetuptoolsDeprecationWarning: Invalid dash-separated options
!!
************************************************** ******************************
Usage of dash-separated 'description-file' will not be supported in future
versions. Please use the underscore name 'description_file' instead.
By 2023-Sep-26, you need to update your project and remove deprecated calls
or your builds will no longer be supported.
See https://setuptools.pypa.io/en/latest/userguide/declarative_config.html for details.
************************************************** ******************************
!!
opt = self.warn_dash_deprecation(opt, section)
Building from C++
error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-tools/
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
ERROR: Failed building wheel for url
Failed to build url
ERROR: Could not build wheels for url, which is required to install pyproject.toml-based projects
D:\YT_Rips\WKS-KEYS>
any ideas please... as i'm guessing i need that "url" command installed... -
@LastResort:
I have no idea what script you're using, but this error: "error: the following arguments are required: command, url" does not mean that you're missing any installations. It means that whatever function that takes your input isn't getting the data it expects. And based on that bit of instruction with cURL, it wants you to curl the license url and then press enter.
Similar Threads
-
Y2Mate.ch Downloader Youtube
By jimwnola in forum Video Streaming DownloadingReplies: 25Last Post: 1st Jun 2022, 10:58 -
ITVHUB downloader?
By bal2001bc in forum Video Streaming DownloadingReplies: 6Last Post: 24th Jun 2021, 07:32 -
Shudder downloader?
By throwawayjz1 in forum Video Streaming DownloadingReplies: 1Last Post: 25th Jan 2021, 15:31 -
Using TV downloader
By frankopstaele in forum Newbie / General discussionsReplies: 0Last Post: 2nd Feb 2019, 18:44 -
Downmixing 6 channel AAC to 2 channel?
By bizzybody in forum Video ConversionReplies: 33Last Post: 12th Nov 2017, 10:19