From 1656973fe0048979fd6c0d7fae42dc889436b551 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Tue, 7 Oct 2025 23:58:14 +0100 Subject: [PATCH 1/5] get requests are being sent it seems --- yt_dlp/extractor/_extractors.py | 1 + yt_dlp/extractor/porndead.py | 138 ++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 yt_dlp/extractor/porndead.py diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index e6b34e9e64..e07fecfb70 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -1554,6 +1554,7 @@ from .polskieradio import ( from .popcorntimes import PopcorntimesIE from .popcorntv import PopcornTVIE from .pornbox import PornboxIE +from .porndead import PornDeadIE from .pornflip import PornFlipIE from .pornhub import ( PornHubIE, diff --git a/yt_dlp/extractor/porndead.py b/yt_dlp/extractor/porndead.py new file mode 100644 index 0000000000..b42b212f94 --- /dev/null +++ b/yt_dlp/extractor/porndead.py @@ -0,0 +1,138 @@ +import re +import urllib.parse + +from yt_dlp.utils._utils import int_or_none + +from .common import InfoExtractor +from ..utils import ExtractorError + + +class PornDeadIE(InfoExtractor): + _VALID_URL = r'https?://(?:www\.)?porndead\.org/video/(?P[0-9a-f]+)' + _TESTS = [ + { + 'url': 'https://porndead.org/video/65fefcb523810', + 'info_dict': { + 'id': '65fefcb523810', + 'ext': 'mp4', + 'title': 'Hysterical Literature - Isabel Love', + }, + }, + ] + + def _real_extract(self, url): + video_id = self._match_id(url) + webpage = self._download_webpage(url, video_id) + + # find video title ideally + title = ( + self._html_search_regex( + r']+class=["\']title_video["\'][^>]*>([^<]+)', + webpage, + 'title', + default=None, + ) + or self._og_search_title(webpage, default=None) + or f'Video {video_id}' + ) + + # extract variable player_url from + player_rel = self._search_regex( + r'(?is)player[_-]?url\s*=\s*(["\'])(?P[^"\']+)\1', + webpage, + 'player url', + default=None, + group='u', + ) + + if not player_rel: + raise ExtractorError('Could not find player_url on page', expected=True) + + # resolve relative URL and append type=1 like the JS on the page does + player_url = urllib.parse.urljoin(url, player_rel) + player_endpoint = player_url + ('&type=1' if '?' in player_url else '?type=1') + + ajax_headers = { + 'Referer': url, + 'X-Requested-With': 'XMLHttpRequest', + 'User-Agent': 'Mozilla/5.0 (compatible)', + 'Accept': '*/*', + } + + # get the options html + options_html = None + try: + options_html = self._download_webpage( + player_endpoint, + video_id, + headers=ajax_headers, + data=b'', # empty body to force POST where supported + ) + except Exception as e: + print(e) + raise ExtractorError( + f'Failed to download options from {player_endpoint}: {e}', + expected=True, + ) + + formats = [] + + # write options_html to a file for debugging + with open(f'/tmp/porndead_{video_id}_options.mp4', 'w') as f: + f.write(options_html or '') + print(f'Wrote options HTML to /tmp/porndead_{video_id}_options.mp4') + + # try to find direct mp4 links in the returned HTML (anchors with class href_mp4) + links = re.findall( + r']+class=["\']href_mp4["\'][^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)', + options_html or '', + flags=re.IGNORECASE, + ) + + print(links) + + for href, label in links: + full_url = urllib.parse.urljoin(url, href) + + # try to infer height from label (e.g., '240p', '720p') or from filename (720P_) + m_h = re.search(r'(\d{3,4})[pP]', label) or re.search(r'(\d{3,4})P_', href) + height = int_or_none(m_h.group(1)) + + # try to infer bitrate (e.g., '4000K' or rate=500k in query) + m_k = re.search(r'([0-9]+)[kK]', href) or re.search(r'rate=([0-9]+)k', href) + tbr = int_or_none(m_k.group(1)) + + fmt_id = f'{height}p' if height else label.strip() + + fmt = { + 'format_id': fmt_id, + 'url': full_url, + 'ext': 'mp4', + } + if height: + fmt['height'] = height + if tbr: + fmt['tbr'] = tbr + + fmt['http_headers'] = {'Referer': url, 'User-Agent': 'Mozilla/5.0'} + + formats.append(fmt) + + # we can also get the m3u8 by GET on the player_url without &type=1 + formats.extend( + self._extract_m3u8_formats( + player_url, + video_id, + 'mp4', + entry_protocol='m3u8_native', + m3u8_id='hls', + fatal=False, + ), + ) + + return { + 'id': video_id, + 'title': title, + 'formats': formats, + 'age_limit': 18, + } From c0259f9df61f329d1a62b05bfbb64db36d0ff5dc Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Wed, 8 Oct 2025 00:09:59 +0100 Subject: [PATCH 2/5] working tests! --- yt_dlp/extractor/porndead.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/yt_dlp/extractor/porndead.py b/yt_dlp/extractor/porndead.py index b42b212f94..38472a064e 100644 --- a/yt_dlp/extractor/porndead.py +++ b/yt_dlp/extractor/porndead.py @@ -16,11 +16,16 @@ class PornDeadIE(InfoExtractor): 'id': '65fefcb523810', 'ext': 'mp4', 'title': 'Hysterical Literature - Isabel Love', + 'age_limit': 18, }, }, ] def _real_extract(self, url): + # if www is missing, add it because the relative URLs seem to depend on it + if '://porndead.org' in url: + url = url.replace('://porndead.org', '://www.porndead.org') + video_id = self._match_id(url) webpage = self._download_webpage(url, video_id) @@ -69,7 +74,6 @@ class PornDeadIE(InfoExtractor): data=b'', # empty body to force POST where supported ) except Exception as e: - print(e) raise ExtractorError( f'Failed to download options from {player_endpoint}: {e}', expected=True, @@ -77,11 +81,6 @@ class PornDeadIE(InfoExtractor): formats = [] - # write options_html to a file for debugging - with open(f'/tmp/porndead_{video_id}_options.mp4', 'w') as f: - f.write(options_html or '') - print(f'Wrote options HTML to /tmp/porndead_{video_id}_options.mp4') - # try to find direct mp4 links in the returned HTML (anchors with class href_mp4) links = re.findall( r']+class=["\']href_mp4["\'][^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)', @@ -89,8 +88,6 @@ class PornDeadIE(InfoExtractor): flags=re.IGNORECASE, ) - print(links) - for href, label in links: full_url = urllib.parse.urljoin(url, href) @@ -118,18 +115,6 @@ class PornDeadIE(InfoExtractor): formats.append(fmt) - # we can also get the m3u8 by GET on the player_url without &type=1 - formats.extend( - self._extract_m3u8_formats( - player_url, - video_id, - 'mp4', - entry_protocol='m3u8_native', - m3u8_id='hls', - fatal=False, - ), - ) - return { 'id': video_id, 'title': title, From 13e0cad11ae847b42afa4c109416e4712c89f83e Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Wed, 8 Oct 2025 00:18:26 +0100 Subject: [PATCH 3/5] sexdead added --- yt_dlp/extractor/_extractors.py | 1 + yt_dlp/extractor/porndead.py | 2 + yt_dlp/extractor/sexdead.py | 124 ++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 yt_dlp/extractor/sexdead.py diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index e07fecfb70..5e0a5a5e19 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -1819,6 +1819,7 @@ from .senategov import ( from .sendtonews import SendtoNewsIE from .servus import ServusIE from .sevenplus import SevenPlusIE +from .sexdead import SexDeadIE from .sexu import SexuIE from .seznamzpravy import ( SeznamZpravyArticleIE, diff --git a/yt_dlp/extractor/porndead.py b/yt_dlp/extractor/porndead.py index 38472a064e..9249bd57d0 100644 --- a/yt_dlp/extractor/porndead.py +++ b/yt_dlp/extractor/porndead.py @@ -22,6 +22,8 @@ class PornDeadIE(InfoExtractor): ] def _real_extract(self, url): + url = url.strip().lower() + # if www is missing, add it because the relative URLs seem to depend on it if '://porndead.org' in url: url = url.replace('://porndead.org', '://www.porndead.org') diff --git a/yt_dlp/extractor/sexdead.py b/yt_dlp/extractor/sexdead.py new file mode 100644 index 0000000000..23fbe90b3b --- /dev/null +++ b/yt_dlp/extractor/sexdead.py @@ -0,0 +1,124 @@ +import re +import urllib.parse + +from yt_dlp.utils._utils import int_or_none + +from .common import InfoExtractor +from ..utils import ExtractorError + + +class SexDeadIE(InfoExtractor): + _VALID_URL = r'https?://(?:www\.)?sexdead\.org/video/(?P[0-9a-f]+)' + _TESTS = [ + { + 'url': 'https://sexdead.org/video/65fefcb523810', + 'info_dict': { + 'id': '65fefcb523810', + 'ext': 'mp4', + 'title': 'Hysterical Literature - Isabel Love', + 'age_limit': 18, + }, + }, + ] + + def _real_extract(self, url): + url = url.strip().lower() + # if www is missing, add it because the relative URLs seem to depend on it + if '://sexdead.org' in url: + url = url.replace('://sexdead.org', '://www.sexdead.org') + + video_id = self._match_id(url) + webpage = self._download_webpage(url, video_id) + + # find video title ideally + title = ( + self._html_search_regex( + r']+class=["\']title_video["\'][^>]*>([^<]+)', + webpage, + 'title', + default=None, + ) + or self._og_search_title(webpage, default=None) + or f'Video {video_id}' + ) + + # extract variable player_url from + player_rel = self._search_regex( + r'(?is)player[_-]?url\s*=\s*(["\'])(?P[^"\']+)\1', + webpage, + 'player url', + default=None, + group='u', + ) + + if not player_rel: + raise ExtractorError('Could not find player_url on page', expected=True) + + # resolve relative URL and append type=1 like the JS on the page does + player_url = urllib.parse.urljoin(url, player_rel) + player_endpoint = player_url + ('&type=1' if '?' in player_url else '?type=1') + + ajax_headers = { + 'Referer': url, + 'X-Requested-With': 'XMLHttpRequest', + 'User-Agent': 'Mozilla/5.0 (compatible)', + 'Accept': '*/*', + } + + # get the options html + options_html = None + try: + options_html = self._download_webpage( + player_endpoint, + video_id, + headers=ajax_headers, + data=b'', # empty body to force POST where supported + ) + except Exception as e: + raise ExtractorError( + f'Failed to download options from {player_endpoint}: {e}', + expected=True, + ) + + formats = [] + + # try to find direct mp4 links in the returned HTML (anchors with class href_mp4) + links = re.findall( + r']+class=["\']href_mp4["\'][^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)', + options_html or '', + flags=re.IGNORECASE, + ) + + for href, label in links: + full_url = urllib.parse.urljoin(url, href) + + # try to infer height from label (e.g., '240p', '720p') or from filename (720P_) + m_h = re.search(r'(\d{3,4})[pP]', label) or re.search(r'(\d{3,4})P_', href) + height = int_or_none(m_h.group(1)) + + # try to infer bitrate (e.g., '4000K' or rate=500k in query) + m_k = re.search(r'([0-9]+)[kK]', href) or re.search(r'rate=([0-9]+)k', href) + tbr = int_or_none(m_k.group(1)) + + fmt_id = f'{height}p' if height else label.strip() + + fmt = { + 'format_id': fmt_id, + 'url': full_url, + 'ext': 'mp4', + } + if height: + fmt['height'] = height + if tbr: + fmt['tbr'] = tbr + + fmt['http_headers'] = {'Referer': url, 'User-Agent': 'Mozilla/5.0'} + + formats.append(fmt) + + return { + 'id': video_id, + 'title': title, + 'formats': formats, + 'age_limit': 18, + } From e3d2f61bd4b884f530807867bb731bbc0da7ec28 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Wed, 8 Oct 2025 00:25:45 +0100 Subject: [PATCH 4/5] update supportedsites.md --- supportedsites.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/supportedsites.md b/supportedsites.md index 513fd93989..658c232571 100644 --- a/supportedsites.md +++ b/supportedsites.md @@ -1826,4 +1826,6 @@ The only reliable way to check if a site is supported is to try it. - **zingmp3:week-chart** - **zoom** - **Zype** + - **Porndead** + - **SexDead** - **generic**: Generic downloader that works on some sites From c2821d18aaaf94fb06570502c88f9284f6a2de58 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Wed, 8 Oct 2025 00:27:11 +0100 Subject: [PATCH 5/5] fix github secuity bot comment --- yt_dlp/extractor/porndead.py | 6 ++++-- yt_dlp/extractor/sexdead.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/yt_dlp/extractor/porndead.py b/yt_dlp/extractor/porndead.py index 9249bd57d0..588b97a02d 100644 --- a/yt_dlp/extractor/porndead.py +++ b/yt_dlp/extractor/porndead.py @@ -25,8 +25,10 @@ class PornDeadIE(InfoExtractor): url = url.strip().lower() # if www is missing, add it because the relative URLs seem to depend on it - if '://porndead.org' in url: - url = url.replace('://porndead.org', '://www.porndead.org') + parsed = urllib.parse.urlparse(url) + if parsed.netloc == 'porndead.org': + parsed = parsed._replace(netloc='www.porndead.org') + url = urllib.parse.urlunparse(parsed) video_id = self._match_id(url) webpage = self._download_webpage(url, video_id) diff --git a/yt_dlp/extractor/sexdead.py b/yt_dlp/extractor/sexdead.py index 23fbe90b3b..137a204a58 100644 --- a/yt_dlp/extractor/sexdead.py +++ b/yt_dlp/extractor/sexdead.py @@ -24,8 +24,10 @@ class SexDeadIE(InfoExtractor): def _real_extract(self, url): url = url.strip().lower() # if www is missing, add it because the relative URLs seem to depend on it - if '://sexdead.org' in url: - url = url.replace('://sexdead.org', '://www.sexdead.org') + parsed = urllib.parse.urlparse(url) + if parsed.netloc == 'sexdead.org': + parsed = parsed._replace(netloc='www.sexdead.org') + url = urllib.parse.urlunparse(parsed) video_id = self._match_id(url) webpage = self._download_webpage(url, video_id)