Repository: yt-dlp/yt-dlp
Issue category: Bug Report

Issue:
[Globo] Unable to download JSON metadata: HTTP Error 404: Not Found
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting that yt-dlp is broken on a **supported** site
- [X] I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [X] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

Brazil

### Provide a description that is worded well enough to be understood

```shell
yt-dlp --cookies-from-browser chrome -F https://globoplay.globo.com/v/12450434
```

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['-vU', '--cookies-from-browser', 'chrome', '-F', 'https://globoplay.globo.com/v/12450434']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2024.03.10 from yt-dlp/yt-dlp [615a84447] (pip)
[debug] Python 3.12.2 (CPython x86_64 64bit) - macOS-14.2.1-x86_64-i386-64bit (OpenSSL 3.2.1 30 Jan 2024)
[debug] exe versions: ffmpeg 6.1.1 (setts), ffprobe 6.1.1, rtmpdump 2.4
[debug] Optional libraries: Cryptodome-3.20.0, brotli-1.1.0, certifi-2024.02.02, mutagen-1.47.0, requests-2.31.0, sqlite3-3.45.2, urllib3-2.2.1, websockets-12.0
[debug] Proxy map: {}
Extracting cookies from chrome
[debug] Extracting cookies from: "/Users/USER/Library/Application Support/Google/Chrome/Default/Cookies"
[debug] using find-generic-password to obtain password from OSX keychain
Extracted 3210 cookies from chrome
[debug] cookie version breakdown: {'v10': 3254, 'other': 0, 'unencrypted': 51}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1803 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: stable@2024.03.10 from yt-dlp/yt-dlp
yt-dlp is up to date (stable@2024.03.10 from yt-dlp/yt-dlp)
[Globo] Extracting URL: https://globoplay.globo.com/v/12450434
[Globo] 12450434: Getting cookies
[Globo] 12450434: Downloading JSON metadata
[Globo] 12450434: Downloading security hash for 12450434
ERROR: [Globo] 12450434: Unable to download JSON metadata: HTTP Error 404: Not Found (caused by <HTTPError 404: Not Found>)
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/extractor/common.py", line 732, in extract
    ie_result = self._real_extract(url)
                ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/extractor/globo.py", line 99, in _real_extract
    security = self._download_json(
               ^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/extractor/common.py", line 1086, in download_content
    res = getattr(self, download_handle.__name__)(url_or_request, video_id, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/extractor/common.py", line 1050, in download_handle
    res = self._download_webpage_handle(
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/extractor/common.py", line 920, in _download_webpage_handle
    urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal, data=data, headers=headers, query=query, expected_status=expected_status)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/extractor/common.py", line 877, in _request_webpage
    raise ExtractorError(errmsg, cause=err)

  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/extractor/common.py", line 864, in _request_webpage
    return self._downloader.urlopen(self._create_request(url_or_request, data, headers, query))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/YoutubeDL.py", line 4101, in urlopen
    return self._request_director.send(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/networking/common.py", line 115, in send
    response = handler.send(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/networking/_helper.py", line 204, in wrapper
    return func(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/networking/common.py", line 326, in send
    return self._send(request)
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/yt-dlp/2024.03.10/libexec/lib/python3.12/site-packages/yt_dlp/networking/_requests.py", line 351, in _send
    raise HTTPError(res, redirect_loop=max_redirects_exceeded)
yt_dlp.networking.exceptions.HTTPError: HTTP Error 404: Not Found
```



Hints:
Same thing here.

It happens also for videos that do not require an account, for example https://globoplay.globo.com/v/12458100/.
Did you find any fix? I have the same issue and don't know what to do anymore, it simply doesn't work
I don't know anything about Python or how this process worked but I tried to analyze what is happening.

One of the metadata URLs loaded by the script changed from `https://playback.video.globo.com/v2/video-session` to `https://playback.video.globo.com/v4/video-session`.

the variable `security['sources'][0]['url_template']` doesn't exists in this new version and the format of `security['sources'][0]['token']` changed from a fixed width hash to a JWT.

The decode part of the code extracts 3 variables (`received_time`, `received_md5` and `hash_prefix`) from the old token, but none of them seems to be clearly stated on the JWT.

This hash decoding part of the code will probably have to be redone.
any news?
Following this as well, getting the same issue
same here! +1

Patch:
diff --git a/yt_dlp/extractor/globo.py b/yt_dlp/extractor/globo.py
index d72296be6e0c..7acbd2820c03 100644
--- a/yt_dlp/extractor/globo.py
+++ b/yt_dlp/extractor/globo.py
@@ -1,32 +1,48 @@
-import base64
-import hashlib
 import json
-import random
 import re
+import uuid
 
 from .common import InfoExtractor
-from ..networking import HEADRequest
 from ..utils import (
-    ExtractorError,
+    determine_ext,
+    filter_dict,
     float_or_none,
+    int_or_none,
     orderedSet,
     str_or_none,
     try_get,
+    url_or_none,
 )
+from ..utils.traversal import subs_list_to_dict, traverse_obj
 
 
 class GloboIE(InfoExtractor):
-    _VALID_URL = r'(?:globo:|https?://.+?\.globo\.com/(?:[^/]+/)*(?:v/(?:[^/]+/)?|videos/))(?P<id>\d{7,})'
+    _VALID_URL = r'(?:globo:|https?://[^/?#]+?\.globo\.com/(?:[^/?#]+/))(?P<id>\d{7,})'
     _NETRC_MACHINE = 'globo'
+    _VIDEO_VIEW = '''
+    query getVideoView($videoId: ID!) {
+        video(id: $videoId) {
+            duration
+            description
+            relatedEpisodeNumber
+            relatedSeasonNumber
+            headline
+            title {
+                originProgramId
+                headline
+            }
+        }
+    }
+    '''
     _TESTS = [{
-        'url': 'http://g1.globo.com/carros/autoesporte/videos/t/exclusivos-do-g1/v/mercedes-benz-gla-passa-por-teste-de-colisao-na-europa/3607726/',
+        'url': 'https://globoplay.globo.com/v/3607726/',
         'info_dict': {
             'id': '3607726',
             'ext': 'mp4',
             'title': 'Mercedes-Benz GLA passa por teste de colisão na Europa',
             'duration': 103.204,
-            'uploader': 'G1',
-            'uploader_id': '2015',
+            'uploader': 'G1 ao vivo',
+            'uploader_id': '4209',
         },
         'params': {
             'skip_download': True,
@@ -38,39 +54,36 @@ class GloboIE(InfoExtractor):
             'ext': 'mp4',
             'title': 'Acidentes de trânsito estão entre as maiores causas de queda de energia em SP',
             'duration': 137.973,
-            'uploader': 'Rede Globo',
-            'uploader_id': '196',
+            'uploader': 'Bom Dia Brasil',
+            'uploader_id': '810',
         },
         'params': {
             'skip_download': True,
         },
-    }, {
-        'url': 'http://canalbrasil.globo.com/programas/sangue-latino/videos/3928201.html',
-        'only_matching': True,
-    }, {
-        'url': 'http://globosatplay.globo.com/globonews/v/4472924/',
-        'only_matching': True,
-    }, {
-        'url': 'http://globotv.globo.com/t/programa/v/clipe-sexo-e-as-negas-adeus/3836166/',
-        'only_matching': True,
-    }, {
-        'url': 'http://globotv.globo.com/canal-brasil/sangue-latino/t/todos-os-videos/v/ator-e-diretor-argentino-ricado-darin-fala-sobre-utopias-e-suas-perdas/3928201/',
-        'only_matching': True,
-    }, {
-        'url': 'http://canaloff.globo.com/programas/desejar-profundo/videos/4518560.html',
-        'only_matching': True,
     }, {
         'url': 'globo:3607726',
         'only_matching': True,
-    }, {
-        'url': 'https://globoplay.globo.com/v/10248083/',
+    },
+        {
+        'url': 'globo:8013907',  # needs subscription to globoplay
+        'info_dict': {
+            'id': '8013907',
+            'ext': 'mp4',
+            'title': 'Capítulo de 14⧸08⧸1989',
+            'episode_number': 1,
+        },
+        'params': {
+            'skip_download': True,
+        },
+    },
+        {
+        'url': 'globo:12824146',
         'info_dict': {
-            'id': '10248083',
+            'id': '12824146',
             'ext': 'mp4',
-            'title': 'Melhores momentos: Equador 1 x 1 Brasil pelas Eliminatórias da Copa do Mundo 2022',
-            'duration': 530.964,
-            'uploader': 'SporTV',
-            'uploader_id': '698',
+            'title': 'Acordo de damas',
+            'episode_number': 1,
+            'season_number': 2,
         },
         'params': {
             'skip_download': True,
@@ -80,98 +93,70 @@ class GloboIE(InfoExtractor):
     def _real_extract(self, url):
         video_id = self._match_id(url)
 
-        self._request_webpage(
-            HEADRequest('https://globo-ab.globo.com/v2/selected-alternatives?experiments=player-isolated-experiment-02&skipImpressions=true'),
-            video_id, 'Getting cookies')
-
-        video = self._download_json(
-            f'http://api.globovideos.com/videos/{video_id}/playlist',
-            video_id)['videos'][0]
-        if not self.get_param('allow_unplayable_formats') and video.get('encrypted') is True:
-            self.report_drm(video_id)
-
-        title = video['title']
+        info = self._download_json(
+            'https://cloud-jarvis.globo.com/graphql', video_id,
+            query={
+                'operationName': 'getVideoView',
+                'variables': json.dumps({'videoId': video_id}),
+                'query': self._VIDEO_VIEW,
+            }, headers={
+                'content-type': 'application/json',
+                'x-platform-id': 'web',
+                'x-device-id': 'desktop',
+                'x-client-version': '2024.12-5',
+            })['data']['video']
 
         formats = []
-        security = self._download_json(
-            'https://playback.video.globo.com/v2/video-session', video_id, f'Downloading security hash for {video_id}',
-            headers={'content-type': 'application/json'}, data=json.dumps({
-                'player_type': 'desktop',
+        video = self._download_json(
+            'https://playback.video.globo.com/v4/video-session', video_id,
+            f'Downloading resource info for {video_id}',
+            headers={'Content-Type': 'application/json'},
+            data=json.dumps(filter_dict({
+                'player_type': 'mirakulo_8k_hdr',
                 'video_id': video_id,
                 'quality': 'max',
                 'content_protection': 'widevine',
-                'vsid': '581b986b-4c40-71f0-5a58-803e579d5fa2',
-                'tz': '-3.0:00',
-            }).encode())
-
-        self._request_webpage(HEADRequest(security['sources'][0]['url_template']), video_id, 'Getting locksession cookie')
-
-        security_hash = security['sources'][0]['token']
-        if not security_hash:
-            message = security.get('message')
-            if message:
-                raise ExtractorError(
-                    f'{self.IE_NAME} returned error: {message}', expected=True)
-
-        hash_code = security_hash[:2]
-        padding = '%010d' % random.randint(1, 10000000000)
-        if hash_code in ('04', '14'):
-            received_time = security_hash[3:13]
-            received_md5 = security_hash[24:]
-            hash_prefix = security_hash[:23]
-        elif hash_code in ('02', '12', '03', '13'):
-            received_time = security_hash[2:12]
-            received_md5 = security_hash[22:]
-            padding += '1'
-            hash_prefix = '05' + security_hash[:22]
-
-        padded_sign_time = str(int(received_time) + 86400) + padding
-        md5_data = (received_md5 + padded_sign_time + '0xAC10FD').encode()
-        signed_md5 = base64.urlsafe_b64encode(hashlib.md5(md5_data).digest()).decode().strip('=')
-        signed_hash = hash_prefix + padded_sign_time + signed_md5
-        source = security['sources'][0]['url_parts']
-        resource_url = source['scheme'] + '://' + source['domain'] + source['path']
-        signed_url = '{}?h={}&k=html5&a={}'.format(resource_url, signed_hash, 'F' if video.get('subscriber_only') else 'A')
-
-        fmts, subtitles = self._extract_m3u8_formats_and_subtitles(
-            signed_url, video_id, 'mp4', entry_protocol='m3u8_native', m3u8_id='hls', fatal=False)
-        formats.extend(fmts)
-
-        for resource in video['resources']:
-            if resource.get('type') == 'subtitle':
-                subtitles.setdefault(resource.get('language') or 'por', []).append({
-                    'url': resource.get('url'),
-                })
-        subs = try_get(security, lambda x: x['source']['subtitles'], expected_type=dict) or {}
-        for sub_lang, sub_url in subs.items():
-            if sub_url:
-                subtitles.setdefault(sub_lang or 'por', []).append({
-                    'url': sub_url,
-                })
-        subs = try_get(security, lambda x: x['source']['subtitles_webvtt'], expected_type=dict) or {}
-        for sub_lang, sub_url in subs.items():
-            if sub_url:
-                subtitles.setdefault(sub_lang or 'por', []).append({
-                    'url': sub_url,
-                })
-
-        duration = float_or_none(video.get('duration'), 1000)
-        uploader = video.get('channel')
-        uploader_id = str_or_none(video.get('channel_id'))
+                'vsid': f'{uuid.uuid4()}',
+                'consumption': 'streaming',
+                'capabilities': {'low_latency': True},
+                'tz': '-03:00',
+                'Authorization': try_get(self._get_cookies('https://globo.com'),
+                                         lambda x: f'Bearer {x["GLBID"].value}'),
+                'version': 1,
+            })).encode())
+
+        if traverse_obj(video, ('resource', 'drm_protection_enabled', {bool})):
+            self.report_drm(video_id)
+
+        main_source = video['sources'][0]
+
+        # 4k streams are exclusively outputted in dash, so we need to filter these out
+        if determine_ext(main_source['url']) == 'mpd':
+            formats, subtitles = self._extract_mpd_formats_and_subtitles(main_source['url'], video_id, mpd_id='dash')
+        else:
+            formats, subtitles = self._extract_m3u8_formats_and_subtitles(
+                main_source['url'], video_id, 'mp4', m3u8_id='hls')
+        self._merge_subtitles(traverse_obj(main_source, ('text', ..., {
+            'url': ('subtitle', 'srt', 'url', {url_or_none}),
+        }, all, {subs_list_to_dict(lang='en')})), target=subtitles)
 
         return {
             'id': video_id,
-            'title': title,
-            'duration': duration,
-            'uploader': uploader,
-            'uploader_id': uploader_id,
+            **traverse_obj(info, {
+                'title': ('headline', {str}),
+                'duration': ('duration', {float_or_none(scale=1000)}),
+                'uploader': ('title', 'headline', {str}),
+                'uploader_id': ('title', 'originProgramId', {str_or_none}),
+                'episode_number': ('relatedEpisodeNumber', {int_or_none}),
+                'season_number': ('relatedSeasonNumber', {int_or_none}),
+            }),
             'formats': formats,
             'subtitles': subtitles,
         }
 
 
 class GloboArticleIE(InfoExtractor):
-    _VALID_URL = r'https?://.+?\.globo\.com/(?:[^/]+/)*(?P<id>[^/.]+)(?:\.html)?'
+    _VALID_URL = r'https?://(?!globoplay).+?\.globo\.com/(?:[^/?#]+/)*(?P<id>[^/?#.]+)(?:\.html)?'
 
     _VIDEOID_REGEXES = [
         r'\bdata-video-id=["\'](\d{7,})["\']',


Test patch: