VideoHelp Forum


Try StreamFab Downloader and download from Netflix, Amazon, Youtube! Or Try DVDFab and copy Blu-rays! or rip iTunes movies!


Try StreamFab Downloader and download streaming video from Youtube, Netflix, Amazon! Download free trial.


+ Reply to Thread
Page 4 of 5
FirstFirst ... 2 3 4 5 LastLast
Results 91 to 120 of 124
Thread
  1. Member
    Join Date
    Dec 2021
    Location
    england
    Search Comp PM
    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
    Quote Quote  
  2. Member
    Join Date
    Dec 2022
    Location
    Lesotho
    Search Comp PM
    Originally Posted by iamghost View Post
    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
    You may have your feet up but your arms must get tired holding the phone.
    Quote Quote  
  3. Originally Posted by iamghost View Post
    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
    That's a nice setup for vinetrimmer. Anything you care to share? I always found the original arguments to be clunky.
    Quote Quote  
  4. Do not send me DM's
    Join Date
    Dec 2021
    Location
    Tórshavn
    Search Comp PM
    Originally Posted by larits View Post
    Originally Posted by iamghost View Post
    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
    That's a nice setup for vinetrimmer. Anything you care to share? I always found the original arguments to be clunky.
    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.
    Quote Quote  
  5. Originally Posted by Sorenb View Post
    Originally Posted by larits View Post
    Originally Posted by iamghost View Post
    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
    That's a nice setup for vinetrimmer. Anything you care to share? I always found the original arguments to be clunky.
    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.
    Yeah. As a relative beginner, setting up VT to behave was a pain. Especially since all I really needed from it was PCOK, which is like taking a tank to go buy some milk
    Quote Quote  
  6. Do not send me DM's
    Join Date
    Dec 2021
    Location
    Tórshavn
    Search Comp PM
    Originally Posted by larits View Post
    Originally Posted by Sorenb View Post
    Originally Posted by larits View Post
    Originally Posted by iamghost View Post
    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
    That's a nice setup for vinetrimmer. Anything you care to share? I always found the original arguments to be clunky.
    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.
    Yeah. As a relative beginner, setting up VT to behave was a pain. Especially since all I really needed from it was PCOK, which is like taking a tank to go buy some milk

    hehehe
    Quote Quote  
  7. Thanks for sharing your script, it works great!

    However, for some episodes, I get:
    Code:
    [!] Invalid URL !!!
    [!] Failed getting VOD stream !!!
    With C4 reporting "Asset not found"

    (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)
    Quote Quote  
  8. Member
    Join Date
    Dec 2021
    Location
    Scotland
    Search Comp PM
    Originally Posted by Ocyus View Post
    Thanks for sharing your script, it works great!

    However, for some episodes, I get:
    Code:
    [!] Invalid URL !!!
    [!] Failed getting VOD stream !!!
    With C4 reporting "Asset not found"

    (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)
    Well my script listed above works fine with that 8 out of 10 cats. In fact, I haven't found an All4 show that doesn't work yet.
    Quote Quote  
  9. Member
    Join Date
    Dec 2020
    Location
    Croatia
    Search PM
    when you get invalid url just wait 10-15 seconds and retry
    same thing happens with diazole's script sometimes, at least for me
    Quote Quote  
  10. Originally Posted by deccavox View Post
    Well my script listed above works fine with that 8 out of 10 cats. In fact, I haven't found an All4 show that doesn't work yet.
    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!
    Quote Quote  
  11. Originally Posted by Diazole View Post
    I couldn't find a working C4 script so I wrote my own - https://github.com/Diazole/c4-dl.
    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 ''
    Quote Quote  
  12. easiest way is to take the .mpd it displays, and feed that into yt-dlp or N_m3u8DL-RE, and download which ever resolution you like. also a good way to get the subs aswell.
    Quote Quote  
  13. Member
    Join Date
    Dec 2020
    Location
    Croatia
    Search PM
    Originally Posted by coltseavers View Post
    I can see the below when editing the script but does this relate to the selection of the quality?
    No, the part where he's using yt-dlp and selects "bv,ba" (best video, best audio) does that
    I'm really bad at format selection so somebody else will have to help you with it
    Quote Quote  
  14. Do not send me DM's
    Join Date
    Dec 2021
    Location
    Tórshavn
    Search Comp PM
    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>
    BA as an audio selection on Channel 4 is just idiotic. On some shows there are two audio tracks, with equal bitrate, so yt-dlp with best audio (ba) flagged will randomly select whatever it thinks is best. Resulting in at times the audio description for the deaf audio track. So, best to select the audio track by the track name, instead of hoping BA selects the right one for you. (Might be trial and error to find which track is english and which is audio described, but you'll get there.)
    Last edited by Sorenb; 1st Mar 2023 at 09:13.
    Quote Quote  
  15. Originally Posted by Sorenb View Post
    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>
    BA as an audio selection on Channel 4 is just idiotic. On some shows there are two audio tracks, with equal bitrate, so yt-dlp with best audio (ba) flagged will randomly select whatever it thinks is best. Resulting in at times the audio description for the deaf audio track. So, best to select the audio track by the track name, instead of hoping BA selects the right one for you. (Might be trial and error to find which track is english and which is audio described, but you'll get there.)
    Thanks for all of your help (and to the previous posters).
    Quote Quote  
  16. Member
    Join Date
    Dec 2020
    Location
    Croatia
    Search PM
    Originally Posted by Sorenb View Post
    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>
    BA as an audio selection on Channel 4 is just idiotic. On some shows there are two audio tracks, with equal bitrate, so yt-dlp with best audio (ba) flagged will randomly select whatever it thinks is best. Resulting in at times the audio description for the deaf audio track. So, best to select the audio track by the track name, instead of hoping BA selects the right one for you. (Might be trial and error to find which track is english and which is audio described, but you'll get there.)
    I see now that in the new script it's changed into "bv,wa" since the real track is always (or at least in every case I've seen so far) designated as the worse track of the two
    Quote Quote  
  17. 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
    Quote Quote  
  18. Do not send me DM's
    Join Date
    Dec 2021
    Location
    Tórshavn
    Search Comp PM
    Originally Posted by coltseavers View Post
    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
    And use file detection to loop in the merge.
    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"
    and set up loops for recombine,
    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....
    tip
    Code:
     if [ -f audio.enc.m4a]
    if file exists
    Code:
     if [ ! -f audio.enc.m4a]
    if file does not exist
    No idea what it is in python offhand, most likely
    Code:
    import os.path
    os.path.isfile(audio.enc.m4a)
    You'll find that the audio description track name is the same in the c4 manifest. And you can do what you wish using the if exists then loops
    Last edited by Sorenb; 2nd Mar 2023 at 14:13.
    Quote Quote  
  19. 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 !!!
    Quote Quote  
  20. Member
    Join Date
    Dec 2021
    Location
    england
    Search Comp PM
    work fine with vinetrimmer
    Code:
     --key ba0dfab470a1593fbb265b93e48eef2c:7fee5511dbe81b0a6ca89d52c26c2846
    Quote Quote  
  21. @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
    So this is an error on CH4, not in the scripts. In the meantime, you can get the regular 1080p with this key:

    Code:
    00000000000000000000000003778569:fc04e31c5f88cb978592eb541a846e72
    Quote Quote  
  22. Originally Posted by larits View Post

    So this is an error on CH4, not in the scripts. In the meantime, you can get the regular 1080p with this key:

    Code:
    00000000000000000000000003778569:fc04e31c5f88cb978592eb541a846e72
    Its not the only instance on ALL4 where the mobile manifest is missing for some reason
    Last edited by T33V33; 28th Mar 2023 at 15:28.
    Quote Quote  
  23. Originally Posted by larits View Post
    @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
    So this is an error on CH4, not in the scripts. In the meantime, you can get the regular 1080p with this key:

    Code:
    00000000000000000000000003778569:fc04e31c5f88cb978592eb541a846e72
    Thanks for this, is there a way to use this script to find the key for the regular 1080p format (and how to decrypt)?
    Quote Quote  
  24. Originally Posted by coltseavers View Post

    Thanks for this, is there a way to use this script to find the key for the regular 1080p format (and how to decrypt)?
    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()
    Quote Quote  
  25. Originally Posted by larits View Post
    Originally Posted by coltseavers View Post

    Thanks for this, is there a way to use this script to find the key for the regular 1080p format (and how to decrypt)?
    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.
    Lifesaver, thank you so much for your help.
    Quote Quote  
  26. Member
    Join Date
    May 2023
    Location
    England
    Search Comp PM
    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',
    Then my merge_streams is a bit clunky, but it works:


    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)
    n.b. I'm also dispensing with the subfolder of downloads/
    Quote Quote  
  27. Member
    Join Date
    Dec 2022
    Location
    Lesotho
    Search Comp PM
    I always download both audio tracks but the AD track is downloaded separately and muxed later with ffmpeg to give it a nice title. Thanks @PhilipG for the script.
    Quote Quote  
  28. 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.
    Quote Quote  
  29. 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...
    Quote Quote  
  30. @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.
    Quote Quote  



Similar Threads

Visit our sponsor! Try DVDFab and backup Blu-rays!