Repository: yt-dlp/yt-dlp (python)

Issue:
[Vimeo] Vimeo subtitles fail with error 403 (CAPTCHA page) due to use of vimeo.com instead of player.vimeo.com
### 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)
- [ ] 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

Global (I am accessing from Australia)

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

### Short Version:

I believe Vimeo subtitles should be loaded from `player.vimeo.com` NOT `vimeo.com` (even though both often work) because the latter domain appears to have stricter Cloudflare rules that display a CAPTCHA page for some users (especially when on a VPN). 

This breaks the downloading of VTT subtitles. 

I don't seem to get a CAPTCHA page when downloading subtitles from `player.vimeo.com`.

### Further Details With Example:

When I attempt to download the following Vimeo video _WITH SUBTITLES_ the download fails due to a 403 Error on the VTT subtitles file. The 403 is being triggered by a Vimeo CAPTCHA page (presumably because I'm using a VPN) that requires a browser to interact with.

Sample Vimeo URL: https://player.vimeo.com/video/1049393408?h=2cb35698c0
Another video with same issue: https://player.vimeo.com/video/1048984535?h=a31d608c45

- I am NOT logged into Vimeo. These are Vimeo embed URLs with a hash in the URL that can be accessed by anybody that knows the URL. I'm not sure whether it being private makes any difference to the issue.
- The video and subtitles both display fine in the browser. 
- The video also downloads perfectly well when subtitles are not requested.
- The 403 error appears when loading the VTT subtitle file from `vimeo.com` domain
- The VTT subtitle file downloads correctly if I manually download from `player.vimeo.com` domain

I can see that yt-dlp is looking for the subtitles at the following URL:
https://vimeo.com/texttrack/208674890.vtt?token=679603b0_0x7ccc9366954d27f115285bec40f878866c8a37d3
(Error 403)

When I check Chrome dev tools it is downloading the VTT file from:
https://player.vimeo.com/texttrack/208674890.vtt?token=6795ffb1_0xeddbfa0b80f86caa13c0f963d9be29fc5146c67c
(URL loads successfully)

I presume that the path to the VTT is being parsed from the JSON embedded in the HTML page (https://player.vimeo.com/video/1049393408?h=2cb35698c0) where the JSON looks like this:

```
"text_tracks": [{
    "id": 208674890,
    "lang": "en-x-autogen",
    "url": "/texttrack/208674890.vtt?token=6795ffb1_0xeddbfa0b80f86caa13c0f963d9be29fc5146c67c",
    "kind": "captions",
    "label": "English (auto-generated)"
}],
```

If the HTML is coming from `player.vimeo.com` and browsers like Chrome also load the VTT files from `player.vimeo.com`, perhaps yt-dlp should also use this domain. Would there be any downsides to fetching the VTT subtitles from `player.vimeo.com`?

### Provide verbose output that clearly demonstrates the problem

- [x] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [ ] 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
yt-dlp -f "bestvideo*[height=720]+bestaudio*/best" --embed-subs --write-subs  "https://player.vimeo.com/video/1049393408?h=2cb35698c0" --download-sections "*0-20" --verbose -U

[debug] Command-line config: ['-f', 'bestvideo*[height=720]+bestaudio*/best', '--embed-subs', '--write-subs', 'https://player.vimeo.com/video/1049393408?h=2cb35698c0', '--download-sections', '*0-20', '--verbose', '-U']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version nightly@2025.01.26.034637 from yt-dlp/yt-dlp-nightly-builds [3b4531934] (win_exe)
[debug] Python 3.10.11 (CPython AMD64 64bit) - Windows-10-10.0.19045-SP0 (OpenSSL 1.1.1t  7 Feb 2023)
[debug] exe versions: ffmpeg 7.1-essentials_build-www.gyan.dev (setts), ffprobe 7.1-essentials_build-www.gyan.dev
[debug] Optional libraries: Cryptodome-3.21.0, brotli-1.1.0, certifi-2024.12.14, curl_cffi-0.5.10, mutagen-1.47.0, requests-2.32.3, sqlite3-3.40.1, urllib3-2.3.0, websockets-14.2
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets, curl_cffi
[debug] Loaded 1839 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp-nightly-builds/releases/latest
Latest version: nightly@2025.01.26.034637 from yt-dlp/yt-dlp-nightly-builds
yt-dlp is up to date (nightly@2025.01.26.034637 from yt-dlp/yt-dlp-nightly-builds)
[vimeo] Extracting URL: https://player.vimeo.com/video/1049393408?h=2cb35698c0
[vimeo] 1049393408: Downloading webpage
[vimeo] 1049393408: Downloading akfire_interconnect_quic m3u8 information
[vimeo] 1049393408: Downloading fastly_skyfire m3u8 information
[vimeo] 1049393408: Downloading akfire_interconnect_quic MPD information
WARNING: [vimeo] 1049393408: Failed to parse XML: not well-formed (invalid token): line 1, column 0
[vimeo] 1049393408: Downloading fastly_skyfire MPD information
WARNING: [vimeo] 1049393408: Failed to parse XML: not well-formed (invalid token): line 1, column 0
[info] 1049393408: Downloading subtitles: en-x-autogen
[debug] Sort order given by extractor: quality, res, fps, hdr:12, source
[debug] Formats sorted by: hasvid, ie_pref, quality, res, fps, hdr:12(7), source, lang, vcodec, channels, acodec, size, br, asr, proto, vext, aext, hasaud, id
[info] 1049393408: Downloading 1 format(s): hls-fastly_skyfire-960+hls-fastly_skyfire-audio-high-English
[info] 1049393408: Downloading 1 time ranges: 0.0-20.0
[info] Writing video subtitles to: 2025-01-22 mid-week video [1049393408].en-x-autogen.vtt
[debug] Invoking http downloader on "https://vimeo.com/texttrack/208674890.vtt?token=679603b0_0x7ccc9366954d27f115285bec40f878866c8a37d3"
ERROR: Unable to download video subtitles for 'en-x-autogen': HTTP Error 403: Forbidden
Traceback (most recent call last):
  File "yt_dlp\YoutubeDL.py", line 4364, in _write_subtitles
  File "yt_dlp\YoutubeDL.py", line 3212, in dl
  File "yt_dlp\downloader\common.py", line 464, in download
  File "yt_dlp\downloader\http.py", line 367, in real_download
  File "yt_dlp\downloader\http.py", line 118, in establish_connection
  File "yt_dlp\YoutubeDL.py", line 4175, in urlopen
  File "yt_dlp\networking\common.py", line 117, in send
  File "yt_dlp\networking\_helper.py", line 208, in wrapper
  File "yt_dlp\networking\common.py", line 340, in send
  File "yt_dlp\networking\_requests.py", line 365, in _send
yt_dlp.networking.exceptions.HTTPError: HTTP Error 403: Forbidden

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "yt_dlp\YoutubeDL.py", line 1637, in wrapper
  File "yt_dlp\YoutubeDL.py", line 1793, in __extract_info
  File "yt_dlp\YoutubeDL.py", line 1852, in process_ie_result
  File "yt_dlp\YoutubeDL.py", line 3024, in process_video_result
  File "yt_dlp\YoutubeDL.py", line 177, in wrapper
  File "yt_dlp\YoutubeDL.py", line 3280, in process_info
  File "yt_dlp\YoutubeDL.py", line 4372, in _write_subtitles
yt_dlp.utils.DownloadError: Unable to download video subtitles for 'en-x-autogen': HTTP Error 403: Forbidden
```

Pull request description:
[ie/vimeo] Extract subtitles from player subdomain
Closes #12198


<details open><summary>Template</summary> <!-- OPEN is intentional -->


### Before submitting a *pull request* make sure you have:
- [x] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
- [x] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests

### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check those that apply and remove the others:
- [x] I am the original author of the code in this PR, and I am willing to release it under [Unlicense](http://unlicense.org/)

### What is the purpose of your *pull request*? Check those that apply and remove the others:
- [x] Fix or improvement to an extractor (Make sure to add/update tests)

</details>


Patch:
diff --git a/yt_dlp/extractor/adobepass.py b/yt_dlp/extractor/adobepass.py
index 91c40b32e..8c2d9d934 100644
--- a/yt_dlp/extractor/adobepass.py
+++ b/yt_dlp/extractor/adobepass.py
@@ -1574,18 +1574,29 @@ def extract_redirect_url(html, url=None, fatal=False):
                             post_form(mvpd_confirm_page_res, 'Confirming Login')
                 elif mso_id == 'Philo':
                     # Philo has very unique authentication method
-                    self._download_webpage(
-                        'https://idp.philo.com/auth/init/login_code', video_id, 'Requesting auth code', data=urlencode_postdata({
+                    self._request_webpage(
+                        'https://idp.philo.com/auth/init/login_code', video_id,
+                        'Requesting Philo auth code', data=json.dumps({
                             'ident': username,
                             'device': 'web',
                             'send_confirm_link': False,
                             'send_token': True,
-                        }))
+                            'device_ident': f'web-{uuid.uuid4().hex}',
+                            'include_login_link': True,
+                        }).encode(), headers={
+                            'Content-Type': 'application/json',
+                            'Accept': 'application/json',
+                        })
+
                     philo_code = getpass.getpass('Type auth code you have received [Return]: ')
-                    self._download_webpage(
-                        'https://idp.philo.com/auth/update/login_code', video_id, 'Submitting token', data=urlencode_postdata({
-                            'token': philo_code,
-                        }))
+                    self._request_webpage(
+                        'https://idp.philo.com/auth/update/login_code', video_id,
+                        'Submitting token', data=json.dumps({'token': philo_code}).encode(),
+                        headers={
+                            'Content-Type': 'application/json',
+                            'Accept': 'application/json',
+                        })
+
                     mvpd_confirm_page_res = self._download_webpage_handle('https://idp.philo.com/idp/submit', video_id, 'Confirming Philo Login')
                     post_form(mvpd_confirm_page_res, 'Confirming Login')
                 elif mso_id == 'Verizon':
diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py
index d5607296d..1174bd4f5 100644
--- a/yt_dlp/extractor/common.py
+++ b/yt_dlp/extractor/common.py
@@ -1675,9 +1675,9 @@ def extract_video_object(e):
                 'ext': mimetype2ext(e.get('encodingFormat')),
                 'title': unescapeHTML(e.get('name')),
                 'description': unescapeHTML(e.get('description')),
-                'thumbnails': [{'url': unescapeHTML(url)}
-                               for url in variadic(traverse_obj(e, 'thumbnailUrl', 'thumbnailURL'))
-                               if url_or_none(url)],
+                'thumbnails': traverse_obj(e, (('thumbnailUrl', 'thumbnailURL', 'thumbnail_url'), (None, ...), {
+                    'url': ({str}, {unescapeHTML}, {self._proto_relative_url}, {url_or_none}),
+                })),
                 'duration': parse_duration(e.get('duration')),
                 'timestamp': unified_timestamp(e.get('uploadDate')),
                 # author can be an instance of 'Organization' or 'Person' types.
diff --git a/yt_dlp/extractor/odnoklassniki.py b/yt_dlp/extractor/odnoklassniki.py
index d27d1c3f0..18eba42e6 100644
--- a/yt_dlp/extractor/odnoklassniki.py
+++ b/yt_dlp/extractor/odnoklassniki.py
@@ -273,6 +273,8 @@ def _extract_desktop(self, url):
             return self._extract_desktop(smuggle_url(url, {'referrer': 'https://boosty.to'}))
         elif error:
             raise ExtractorError(error, expected=True)
+        elif '>Access to this video is restricted</div>' in webpage:
+            self.raise_login_required()
 
         player = self._parse_json(
             unescapeHTML(self._search_regex(
@@ -429,7 +431,7 @@ def _extract_mobile(self, url):
         video_id = self._match_id(url)
 
         webpage = self._download_webpage(
-            f'http://m.ok.ru/video/{video_id}', video_id,
+            f'https://m.ok.ru/video/{video_id}', video_id,
             note='Downloading mobile webpage')
 
         error = self._search_regex(
diff --git a/yt_dlp/extractor/vimeo.py b/yt_dlp/extractor/vimeo.py
index 09497b699..b268fad56 100644
--- a/yt_dlp/extractor/vimeo.py
+++ b/yt_dlp/extractor/vimeo.py
@@ -236,7 +236,7 @@ def _parse_config(self, config, video_id):
         for tt in (request.get('text_tracks') or []):
             subtitles.setdefault(tt['lang'], []).append({
                 'ext': 'vtt',
-                'url': urljoin('https://vimeo.com', tt['url']),
+                'url': urljoin('https://player.vimeo.com/', tt['url']),
             })
 
         thumbnails = []


Test patch:
diff --git a/test/test_InfoExtractor.py b/test/test_InfoExtractor.py
index c6ff6209a..bc89b2955 100644
--- a/test/test_InfoExtractor.py
+++ b/test/test_InfoExtractor.py
@@ -314,6 +314,20 @@ def test_search_json_ld_realworld(self):
                 },
                 {},
             ),
+            (
+                # test thumbnail_url key without URL scheme
+                r'''
+<script type="application/ld+json">
+{
+"@context": "https://schema.org",
+"@type": "VideoObject",
+"thumbnail_url": "//www.nobelprize.org/images/12693-landscape-medium-gallery.jpg"
+}</script>''',
+                {
+                    'thumbnails': [{'url': 'https://www.nobelprize.org/images/12693-landscape-medium-gallery.jpg'}],
+                },
+                {},
+            ),
         ]
         for html, expected_dict, search_json_ld_kwargs in _TESTS:
             expect_dict(