Repository: yt-dlp/yt-dlp

Issue:
[Amazon] Can't download a product review video
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

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

### Checklist

- [X] I'm reporting a broken site
- [X] I've verified that I'm running yt-dlp version **2022.11.11** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [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 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

_No response_

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

I can't download a product review product from Amazon

### 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] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
yt-dlp -vU https://www.amazon.com/gp/customer-reviews/R10VE9VUSY19L3/ref=cm_cr_arp_d_rvw_ttl?ie=UTF8&ASIN=B09G9FPHY6
[1] 4014
[user@artix ~]$ [debug] Command-line config: ['-vU', 'https://www.amazon.com/gp/customer-reviews/R10VE9VUSY19L3/ref=cm_cr_arp_d_rvw_ttl?ie=UTF8']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.11.11 [8b64402] (pip)
[debug] Python 3.10.8 (CPython x86_64 64bit) - Linux-6.0.7-artix1-1-x86_64-with-glibc2.36.9000 (OpenSSL 3.0.7 1 Nov 2022, glibc 2.36.9000)
[debug] exe versions: ffmpeg N-109374-gc2a870222d (fdk,setts), ffprobe N-109374-gc2a870222d, rtmpdump 2.4
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.09.24, mutagen-1.46.0, sqlite3-2.6.0, websockets-10.4
[debug] Proxy map: {}
[debug] Loaded 1723 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.11.11, Current version: 2022.11.11
yt-dlp is up to date (2022.11.11)
[debug] [generic] Extracting URL: https://www.amazon.com/gp/customer-reviews/R10VE9VUSY19L3/ref=cm_cr_arp_d_rvw_ttl?ie=UTF8
[generic] ref=cm_cr_arp_d_rvw_ttl?ie=UTF8: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] ref=cm_cr_arp_d_rvw_ttl?ie=UTF8: Extracting information
[debug] Looking for embeds
ERROR: Unsupported URL: https://www.amazon.com/gp/customer-reviews/R10VE9VUSY19L3/ref=cm_cr_arp_d_rvw_ttl?ie=UTF8
Traceback (most recent call last):
  File "/home/user/.local/lib/python3.10/site-packages/yt_dlp/YoutubeDL.py", line 1485, in wrapper
    return func(self, *args, **kwargs)
  File "/home/user/.local/lib/python3.10/site-packages/yt_dlp/YoutubeDL.py", line 1561, in __extract_info
    ie_result = ie.extract(url)
  File "/home/user/.local/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 674, in extract
    ie_result = self._real_extract(url)
  File "/home/user/.local/lib/python3.10/site-packages/yt_dlp/extractor/generic.py", line 2721, in _real_extract
    raise UnsupportedError(url)
yt_dlp.utils.UnsupportedError: Unsupported URL: https://www.amazon.com/gp/customer-reviews/R10VE9VUSY19L3/ref=cm_cr_arp_d_rvw_ttl?ie=UTF8


[1]+  Exit 1                  yt-dlp -vU https://www.amazon.com/gp/customer-reviews/R10VE9VUSY19L3/ref=cm_cr_arp_d_rvw_ttl?ie=UTF8
```



Discussion:

Resolving patch:
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -87,7 +87,10 @@
     AluraCourseIE
 )
 from .amcnetworks import AMCNetworksIE
-from .amazon import AmazonStoreIE
+from .amazon import (
+    AmazonStoreIE,
+    AmazonReviewsIE,
+)
 from .amazonminitv import (
     AmazonMiniTVIE,
     AmazonMiniTVSeasonIE,
diff --git a/yt_dlp/extractor/amazon.py b/yt_dlp/extractor/amazon.py
--- a/yt_dlp/extractor/amazon.py
+++ b/yt_dlp/extractor/amazon.py
@@ -1,5 +1,17 @@
+import re
+
 from .common import InfoExtractor
-from ..utils import ExtractorError, int_or_none
+from ..utils import (
+    ExtractorError,
+    clean_html,
+    float_or_none,
+    get_element_by_attribute,
+    get_element_by_class,
+    int_or_none,
+    js_to_json,
+    traverse_obj,
+    url_or_none,
+)
 
 
 class AmazonStoreIE(InfoExtractor):
@@ -9,7 +21,7 @@ class AmazonStoreIE(InfoExtractor):
         'url': 'https://www.amazon.co.uk/dp/B098XNCHLD/',
         'info_dict': {
             'id': 'B098XNCHLD',
-            'title': 'md5:dae240564cbb2642170c02f7f0d7e472',
+            'title': str,
         },
         'playlist_mincount': 1,
         'playlist': [{
@@ -20,28 +32,32 @@ class AmazonStoreIE(InfoExtractor):
                 'thumbnail': r're:^https?://.*\.jpg$',
                 'duration': 34,
             },
-        }]
+        }],
+        'expected_warnings': ['Unable to extract data'],
     }, {
         'url': 'https://www.amazon.in/Sony-WH-1000XM4-Cancelling-Headphones-Bluetooth/dp/B0863TXGM3',
         'info_dict': {
             'id': 'B0863TXGM3',
-            'title': 'md5:d1d3352428f8f015706c84b31e132169',
+            'title': str,
         },
         'playlist_mincount': 4,
+        'expected_warnings': ['Unable to extract data'],
     }, {
         'url': 'https://www.amazon.com/dp/B0845NXCXF/',
         'info_dict': {
             'id': 'B0845NXCXF',
-            'title': 'md5:f3fa12779bf62ddb6a6ec86a360a858e',
+            'title': str,
         },
         'playlist-mincount': 1,
+        'expected_warnings': ['Unable to extract data'],
     }, {
         'url': 'https://www.amazon.es/Samsung-Smartphone-s-AMOLED-Quad-c%C3%A1mara-espa%C3%B1ola/dp/B08WX337PQ',
         'info_dict': {
             'id': 'B08WX337PQ',
-            'title': 'md5:f3fa12779bf62ddb6a6ec86a360a858e',
+            'title': str,
         },
         'playlist_mincount': 1,
+        'expected_warnings': ['Unable to extract data'],
     }]
 
     def _real_extract(self, url):
@@ -52,7 +68,7 @@ def _real_extract(self, url):
             try:
                 data_json = self._search_json(
                     r'var\s?obj\s?=\s?jQuery\.parseJSON\(\'', webpage, 'data', id,
-                    transform_source=lambda x: x.replace(R'\\u', R'\u'))
+                    transform_source=js_to_json)
             except ExtractorError as e:
                 retry.error = e
 
@@ -66,3 +82,89 @@ def _real_extract(self, url):
             'width': int_or_none(video.get('videoWidth')),
         } for video in (data_json.get('videos') or []) if video.get('isVideo') and video.get('url')]
         return self.playlist_result(entries, playlist_id=id, playlist_title=data_json.get('title'))
+
+
+class AmazonReviewsIE(InfoExtractor):
+    _VALID_URL = r'https?://(?:www\.)?amazon\.(?:[a-z]{2,3})(?:\.[a-z]{2})?/gp/customer-reviews/(?P<id>[^/&#$?]+)'
+    _TESTS = [{
+        'url': 'https://www.amazon.com/gp/customer-reviews/R10VE9VUSY19L3/ref=cm_cr_arp_d_rvw_ttl',
+        'info_dict': {
+            'id': 'R10VE9VUSY19L3',
+            'ext': 'mp4',
+            'title': 'Get squad #Suspicious',
+            'description': 'md5:7012695052f440a1e064e402d87e0afb',
+            'uploader': 'Kimberly Cronkright',
+            'average_rating': 1.0,
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'expected_warnings': ['Review body was not found in webpage'],
+    }, {
+        'url': 'https://www.amazon.com/gp/customer-reviews/R10VE9VUSY19L3/ref=cm_cr_arp_d_rvw_ttl?language=es_US',
+        'info_dict': {
+            'id': 'R10VE9VUSY19L3',
+            'ext': 'mp4',
+            'title': 'Get squad #Suspicious',
+            'description': 'md5:7012695052f440a1e064e402d87e0afb',
+            'uploader': 'Kimberly Cronkright',
+            'average_rating': 1.0,
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'expected_warnings': ['Review body was not found in webpage'],
+    }, {
+        'url': 'https://www.amazon.in/gp/customer-reviews/RV1CO8JN5VGXV/',
+        'info_dict': {
+            'id': 'RV1CO8JN5VGXV',
+            'ext': 'mp4',
+            'title': 'Not sure about its durability',
+            'description': 'md5:1a252c106357f0a3109ebf37d2e87494',
+            'uploader': 'Shoaib Gulzar',
+            'average_rating': 2.0,
+            'thumbnail': r're:^https?://.*\.jpg$',
+        },
+        'expected_warnings': ['Review body was not found in webpage'],
+    }]
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+
+        for retry in self.RetryManager():
+            webpage = self._download_webpage(url, video_id)
+            review_body = get_element_by_attribute('data-hook', 'review-body', webpage)
+            if not review_body:
+                retry.error = ExtractorError('Review body was not found in webpage', expected=True)
+
+        formats, subtitles = [], {}
+
+        manifest_url = self._search_regex(
+            r'data-video-url="([^"]+)"', review_body, 'm3u8 url', default=None)
+        if url_or_none(manifest_url):
+            fmts, subtitles = self._extract_m3u8_formats_and_subtitles(
+                manifest_url, video_id, 'mp4', fatal=False)
+            formats.extend(fmts)
+
+        video_url = self._search_regex(
+            r'<input[^>]+\bvalue="([^"]+)"[^>]+\bclass="video-url"', review_body, 'mp4 url', default=None)
+        if url_or_none(video_url):
+            formats.append({
+                'url': video_url,
+                'ext': 'mp4',
+                'format_id': 'http-mp4',
+            })
+
+        if not formats:
+            self.raise_no_formats('No video found for this customer review', expected=True)
+
+        return {
+            'id': video_id,
+            'title': (clean_html(get_element_by_attribute('data-hook', 'review-title', webpage))
+                      or self._html_extract_title(webpage)),
+            'description': clean_html(traverse_obj(re.findall(
+                r'<span(?:\s+class="cr-original-review-content")?>(.+?)</span>', review_body), -1)),
+            'uploader': clean_html(get_element_by_class('a-profile-name', webpage)),
+            'average_rating': float_or_none(clean_html(get_element_by_attribute(
+                'data-hook', 'review-star-rating', webpage) or '').partition(' ')[0]),
+            'thumbnail': self._search_regex(
+                r'data-thumbnail-url="([^"]+)"', review_body, 'thumbnail', default=None),
+            'formats': formats,
+            'subtitles': subtitles,
+        }


Test patch: