Repository: yt-dlp/yt-dlp

Issue:
zenporn.com support request
### 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 a new site support request
- [X] I've verified that I'm running yt-dlp version **2023.10.13** ([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 none of provided URLs [violate any copyrights](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
- [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 am willing to share it if required

### Region

Japan

### Example URLs

- Single video https://zenporn.com/video/15570701/pred-345-a-triple-slut-office-lady-harlem-threat-horny-elder-sister-types-will-hit-you-with-a-3-point-combo-and-make-you-creampie-cum-kano-kashii-yui-hatano-himari-kinoshita-scene-2-p2/

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

### Problem

The downloader yt-dlp cannot download the video from this site. 

Just like on Youtube let the downloader download the video from the url shown in address bar. No login is required to play video.

### Proposed Solution  

**Example url from address bar**   

https://zenporn.com/video/15570701/pred-345-a-triple-slut-office-lady-harlem-threat-horny-elder-sister-types-will-hit-you-with-a-3-point-combo-and-make-you-creampie-cum-kano-kashii-yui-hatano-himari-kinoshita-scene-2-p2/

And from the url shown in **share** menu which is the **same as the one** in the address bar. 

### Optional feature

**Optionally the downloader can use the** link for embedding video below the url given in share menu. Which after removing the extra words is - https://videomanysex.com/embed/2297875/

I assume it's the link to the same video but it can't be played in the browser. Nothing is shown.

###  Extra 

I wanted to add output of the second command but there was no text box. So I just had to paste it after the output of first command.

I tried my best to create this issue. I completed all the requirements. I hope it's clear and helps. English is not my first language. I am not good in writing this foreign language.

Thank you

### 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', 'https://videomanysex.com/embed/2297875/']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2023.10.13 [b634ba742] (win_x86_exe)
[debug] Python 3.7.9 (CPython AMD64 32bit) - Windows-10-10.0.22621-SP0 (OpenSSL 1.1.1g  21 Apr 2020)
[debug] exe versions: ffmpeg 6.0-full_build-www.gyan.dev (setts), ffprobe 6.0-full_build-www.gyan.dev
[debug] Optional libraries: Cryptodome-3.19.0, brotli-1.1.0, certifi-2023.07.22, mutagen-1.47.0, sqlite3-3.31.1, websockets-11.0.3
[debug] Proxy map: {}
[debug] Loaded 1890 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Available version: stable@2023.10.13, Current version: stable@2023.10.13
Current Build Hash: 886c73031ff13375df029cbd1ddaca7481c7cba9c8e56d85639c5411268805c7
yt-dlp is up to date (stable@2023.10.13)
[generic] Extracting URL: https://videomanysex.com/embed/2297875/
[generic] 2297875: Downloading webpage
ERROR: [generic] Unable to download webpage: HTTP Error 403: Forbidden (caused by <HTTPError 403: Forbidden>); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "yt_dlp\extractor\common.py", line 715, in extract
  File "yt_dlp\extractor\generic.py", line 2439, in _real_extract
  File "yt_dlp\extractor\common.py", line 860, in _request_webpage

  File "yt_dlp\networking\_urllib.py", line 410, in _send
  File "urllib\request.py", line 531, in open
  File "urllib\request.py", line 641, in http_response
  File "urllib\request.py", line 563, in error
  File "urllib\request.py", line 503, in _call_chain
  File "urllib\request.py", line 755, in http_error_302
  File "urllib\request.py", line 531, in open
  File "urllib\request.py", line 641, in http_response
  File "urllib\request.py", line 569, in error
  File "urllib\request.py", line 503, in _call_chain
  File "urllib\request.py", line 649, in http_error_default
urllib.error.HTTPError: HTTP Error 403: Forbidden

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "yt_dlp\YoutubeDL.py", line 4051, in urlopen
  File "yt_dlp\networking\common.py", line 114, in send
  File "yt_dlp\networking\_helper.py", line 204, in wrapper
  File "yt_dlp\networking\common.py", line 325, in send
  File "yt_dlp\networking\_urllib.py", line 415, in _send
yt_dlp.networking.exceptions.HTTPError: HTTP Error 403: Forbidden

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "yt_dlp\extractor\common.py", line 847, in _request_webpage
  File "yt_dlp\YoutubeDL.py", line 4070, in urlopen
yt_dlp.networking.exceptions._CompatHTTPError: HTTP Error 403: Forbidden



[debug] Command-line config: ['-vU', 'https://zenporn.com/video/15570701/pred-345-a-triple-slut-office-lady-harlem-threat-horny-elder-sister-types-will-hit-you-with-a-3-point-combo-and-make-you-creampie-cum-kano-kashii-yui-hatano-himari-kinoshita-scene-2-p2/']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2023.10.13 [b634ba742] (win_x86_exe)
[debug] Python 3.7.9 (CPython AMD64 32bit) - Windows-10-10.0.22621-SP0 (OpenSSL 1.1.1g  21 Apr 2020)
[debug] exe versions: ffmpeg 6.0-full_build-www.gyan.dev (setts), ffprobe 6.0-full_build-www.gyan.dev
[debug] Optional libraries: Cryptodome-3.19.0, brotli-1.1.0, certifi-2023.07.22, mutagen-1.47.0, sqlite3-3.31.1, websockets-11.0.3
[debug] Proxy map: {}
[debug] Loaded 1890 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Available version: stable@2023.10.13, Current version: stable@2023.10.13
Current Build Hash: 886c73031ff13375df029cbd1ddaca7481c7cba9c8e56d85639c5411268805c7
yt-dlp is up to date (stable@2023.10.13)
[generic] Extracting URL: https://zenporn.com/video/15570701/pred-345-a-triple-slut-office-lady-harlem-threat-horny-elder-sister-types-will-hit-you-with-a-3-point-combo-and-make-you-creampie-cum-kano-kashii-yui-hatano-himari-kinoshita-scene-2-p2/
[generic] pred-345-a-triple-slut-office-lady-harlem-threat-horny-elder-sister-types-will-hit-you-with-a-3-point-combo-and-make-you-creampie-cum-kano-kashii-yui-hatano-himari-kinoshita-scene-2-p2: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] pred-345-a-triple-slut-office-lady-harlem-threat-horny-elder-sister-types-will-hit-you-with-a-3-point-combo-and-make-you-creampie-cum-kano-kashii-yui-hatano-himari-kinoshita-scene-2-p2: Extracting information
[debug] Looking for embeds
ERROR: Unsupported URL: https://zenporn.com/video/15570701/pred-345-a-triple-slut-office-lady-harlem-threat-horny-elder-sister-types-will-hit-you-with-a-3-point-combo-and-make-you-creampie-cum-kano-kashii-yui-hatano-himari-kinoshita-scene-2-p2/
Traceback (most recent call last):
  File "yt_dlp\YoutubeDL.py", line 1567, in wrapper
  File "yt_dlp\YoutubeDL.py", line 1702, in __extract_info
  File "yt_dlp\extractor\common.py", line 715, in extract
  File "yt_dlp\extractor\generic.py", line 2575, in _real_extract
yt_dlp.utils.UnsupportedError: Unsupported URL: https://zenporn.com/video/15570701/pred-345-a-triple-slut-office-lady-harlem-threat-horny-elder-sister-types-will-hit-you-with-a-3-point-combo-and-make-you-creampie-cum-kano-kashii-yui-hatano-himari-kinoshita-scene-2-p2/
```



Discussion:
According to this https://zenporn.com/static/2257/ all videos are not fake and not breaking law. 

The site also works from USA.
I'm looking into creating this extractor.

The video location is retrieved by doing a call to `https://<FQDN>/api/videofile.php?video_id=???&lifetime=8640000`, in which `<FQDN>` is another website and `???` is another video id (this is easily extracted from the zenporn url).
This call returns a json object.

The challenge that I'm currently facing is decoding properly the value of the`video_url` property.
It appears to be a base64 safe URL encoding, but it contains non ASCII characters.

```json
[{"format":".mp4","timelines_count":"31","timelines_interval":"15","video_url":"L2dldF9maWxlLz\u0415vYjU2ZTUxOTU0\u041cTFjOG\u041cxYzRmOGNmNjYyNGQzOT\u041c0ZDdmNT\u04100N2\u0415yZmQ1LzIyOTcw\u041cD\u0410v\u041cjI5Nzg3NS8y\u041cjk3ODc1Lm1wN\u04218,ZD00NTQmYnI9\u041cjcwJnRpPT\u04152OTgx\u041cjk1ODQ~","is_default":1}]
```
I was able to reverse the js function used by the website. Got this sorted out now.

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
@@ -2498,6 +2498,7 @@
     Zee5SeriesIE,
 )
 from .zeenews import ZeeNewsIE
+from .zenporn import ZenPornIE
 from .zetland import ZetlandDKArticleIE
 from .zhihu import ZhihuIE
 from .zingmp3 import (
diff --git a/yt_dlp/extractor/zenporn.py b/yt_dlp/extractor/zenporn.py
new file mode 100644
--- /dev/null
+++ b/yt_dlp/extractor/zenporn.py
@@ -0,0 +1,118 @@
+import base64
+import binascii
+
+from .common import InfoExtractor
+from ..utils import ExtractorError, determine_ext, unified_strdate, url_or_none
+from ..utils.traversal import traverse_obj
+
+
+class ZenPornIE(InfoExtractor):
+    _VALID_URL = r'https?://(?:www\.)?zenporn\.com/video/(?P<id>\d+)'
+    _TESTS = [{
+        'url': 'https://zenporn.com/video/15627016/desi-bhabi-ki-chudai',
+        'md5': '07bd576b5920714d74975c054ca28dee',
+        'info_dict': {
+            'id': '9563799',
+            'display_id': '15627016',
+            'ext': 'mp4',
+            'title': 'md5:669eafd3bbc688aa29770553b738ada2',
+            'description': '',
+            'thumbnail': 'md5:2fc044a19bab450fef8f1931e7920a18',
+            'upload_date': '20230925',
+            'uploader': 'md5:9fae59847f1f58d1da8f2772016c12f3',
+            'age_limit': 18,
+        }
+    }, {
+        'url': 'https://zenporn.com/video/15570701',
+        'md5': 'acba0d080d692664fcc8c4e5502b1a67',
+        'info_dict': {
+            'id': '2297875',
+            'display_id': '15570701',
+            'ext': 'mp4',
+            'title': 'md5:47aebdf87644ec91e8b1a844bc832451',
+            'description': '',
+            'thumbnail': 'https://mstn.nv7s.com/contents/videos_screenshots/2297000/2297875/480x270/1.jpg',
+            'upload_date': '20230921',
+            'uploader': 'Lois Clarke',
+            'age_limit': 18,
+        }
+    }, {
+        'url': 'https://zenporn.com/video/8531117/amateur-students-having-a-fuck-fest-at-club/',
+        'md5': '67411256aa9451449e4d29f3be525541',
+        'info_dict': {
+            'id': '12791908',
+            'display_id': '8531117',
+            'ext': 'mp4',
+            'title': 'Amateur students having a fuck fest at club',
+            'description': '',
+            'thumbnail': 'https://tn.txxx.tube/contents/videos_screenshots/12791000/12791908/288x162/1.jpg',
+            'upload_date': '20191005',
+            'uploader': 'Jackopenass',
+            'age_limit': 18,
+        }
+    }, {
+        'url': 'https://zenporn.com/video/15872038/glad-you-came/',
+        'md5': '296ccab437f5bac6099433768449d8e1',
+        'info_dict': {
+            'id': '111585',
+            'display_id': '15872038',
+            'ext': 'mp4',
+            'title': 'Glad You Came',
+            'description': '',
+            'thumbnail': 'https://vpim.m3pd.com/contents/videos_screenshots/111000/111585/480x270/1.jpg',
+            'upload_date': '20231024',
+            'uploader': 'Martin Rudenko',
+            'age_limit': 18,
+        }
+    }]
+
+    def _gen_info_url(self, ext_domain, extr_id, lifetime=86400):
+        """ This function is a reverse engineering from the website javascript """
+        result = '/'.join(str(int(extr_id) // i * i) for i in (1_000_000, 1_000, 1))
+        return f'https://{ext_domain}/api/json/video/{lifetime}/{result}.json'
+
+    @staticmethod
+    def _decode_video_url(encoded_url):
+        """ This function is a reverse engineering from the website javascript """
+        # Replace lookalike characters and standardize map
+        translation = str.maketrans('АВСЕМ.,~', 'ABCEM+/=')
+        try:
+            return base64.b64decode(encoded_url.translate(translation), validate=True).decode()
+        except (binascii.Error, ValueError):
+            return None
+
+    def _real_extract(self, url):
+        display_id = self._match_id(url)
+        webpage = self._download_webpage(url, display_id)
+
+        ext_domain, video_id = self._search_regex(
+            r'https://(?P<ext_domain>[\w.-]+\.\w{3})/embed/(?P<extr_id>\d+)/',
+            webpage, 'embed info', group=('ext_domain', 'extr_id'))
+
+        info_json = self._download_json(
+            self._gen_info_url(ext_domain, video_id), video_id, fatal=False)
+
+        video_json = self._download_json(
+            f'https://{ext_domain}/api/videofile.php', video_id, query={
+                'video_id': video_id,
+                'lifetime': 8640000,
+            }, note='Downloading video file JSON', errnote='Failed to download video file JSON')
+
+        decoded_url = self._decode_video_url(video_json[0]['video_url'])
+        if not decoded_url:
+            raise ExtractorError('Unable to decode the video url')
+
+        return {
+            'id': video_id,
+            'display_id': display_id,
+            'ext': traverse_obj(video_json, (0, 'format', {determine_ext})),
+            'url': f'https://{ext_domain}{decoded_url}',
+            'age_limit': 18,
+            **traverse_obj(info_json, ('video', {
+                'title': ('title', {str}),
+                'description': ('description', {str}),
+                'thumbnail': ('thumb', {url_or_none}),
+                'upload_date': ('post_date', {unified_strdate}),
+                'uploader': ('user', 'username', {str}),
+            })),
+        }


Test patch: