Eporner_API 2.0__tar.gz → 2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eporner_api-2.0 → eporner_api-2.1}/PKG-INFO +28 -12
- {eporner_api-2.0 → eporner_api-2.1}/README.md +26 -10
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/eporner_api.py +83 -75
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/modules/consts.py +2 -2
- eporner_api-2.1/eporner_api/modules/errors.py +48 -0
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/tests/test_category.py +2 -2
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/tests/test_pornstar.py +2 -2
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/tests/test_video.py +1 -1
- {eporner_api-2.0 → eporner_api-2.1}/pyproject.toml +2 -2
- eporner_api-2.0/eporner_api/modules/errors.py +0 -23
- {eporner_api-2.0 → eporner_api-2.1}/LICENSE +0 -0
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/__init__.py +0 -0
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/modules/__init__.py +0 -0
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/modules/locals.py +0 -0
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/modules/progressbar.py +0 -0
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/modules/sorting.py +0 -0
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/tests/__init__.py +0 -0
- {eporner_api-2.0 → eporner_api-2.1}/eporner_api/tests/test_search.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Eporner_API
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1
|
|
4
4
|
Summary: A Python API for the Porn Site Eporner.com
|
|
5
5
|
Author: Johannes Habel
|
|
6
6
|
Author-email: Johannes Habel <EchterAlsFake@proton.me>
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python
|
|
|
10
10
|
Requires-Dist: bs4
|
|
11
11
|
Requires-Dist: eaf-base-api
|
|
12
12
|
Requires-Dist: lxml ; extra == 'full'
|
|
13
|
-
Requires-Python: >=3.
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
14
|
Project-URL: Homepage, https://github.com/EchterAlsFake/EPorner_API
|
|
15
15
|
Provides-Extra: full
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
@@ -32,6 +32,7 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
> and applicable laws. Do not use it to bypass access controls or scrape at disruptive rates.
|
|
33
33
|
|
|
34
34
|
# Features
|
|
35
|
+
- Asynchronous
|
|
35
36
|
- Fetch videos + metadata
|
|
36
37
|
- Download videos
|
|
37
38
|
- Fetch Pornstars
|
|
@@ -40,8 +41,17 @@ Description-Content-Type: text/markdown
|
|
|
40
41
|
- Built-in caching
|
|
41
42
|
- Easy interface
|
|
42
43
|
- Great type hinting
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
#### Networking Features
|
|
46
|
+
- HTTP 2.0 / HTTP 3.0
|
|
47
|
+
- Browser impersonation
|
|
48
|
+
- Custom JA3
|
|
49
|
+
- All proxy types
|
|
50
|
+
- Proxy authentication
|
|
51
|
+
- Speed Limit
|
|
52
|
+
- DNS over HTTPS
|
|
53
|
+
- And even more...
|
|
54
|
+
- All of this is configurable and can be adjusted as you like!
|
|
45
55
|
|
|
46
56
|
# Supported Platforms
|
|
47
57
|
This API has been tested and confirmed working on:
|
|
@@ -67,19 +77,25 @@ pip install --upgrade Eporner-API
|
|
|
67
77
|
pip install --upgrade git+https://github.com/EchterAlsFake/EPorner_API.git
|
|
68
78
|
```
|
|
69
79
|
|
|
70
|
-
|
|
71
80
|
```python
|
|
81
|
+
import asyncio
|
|
72
82
|
from eporner_api import Client
|
|
73
83
|
# Initialize a Client object
|
|
74
|
-
client = Client()
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
async def do_something():
|
|
86
|
+
client = Client()
|
|
87
|
+
|
|
88
|
+
# Fetch a video
|
|
89
|
+
video_object = await client.get_video("<insert_url_here>") # Can also be a Video ID
|
|
90
|
+
print(video_object.title)
|
|
91
|
+
|
|
92
|
+
# Search for videos
|
|
93
|
+
videos = client.search_videos(query="Your query here",..sortings..) # See Documentation!
|
|
94
|
+
async for video in videos:
|
|
95
|
+
print(video.title)
|
|
96
|
+
|
|
78
97
|
|
|
79
|
-
|
|
80
|
-
videos = client.search_videos(query="Your query here", ..sortings..) # See Documentation!
|
|
81
|
-
for video in videos:
|
|
82
|
-
print(video.title)
|
|
98
|
+
asyncio.run(do_something())
|
|
83
99
|
|
|
84
100
|
# SEE DOCUMENTATION FOR MORE
|
|
85
101
|
```
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
> and applicable laws. Do not use it to bypass access controls or scrape at disruptive rates.
|
|
16
16
|
|
|
17
17
|
# Features
|
|
18
|
+
- Asynchronous
|
|
18
19
|
- Fetch videos + metadata
|
|
19
20
|
- Download videos
|
|
20
21
|
- Fetch Pornstars
|
|
@@ -23,8 +24,17 @@
|
|
|
23
24
|
- Built-in caching
|
|
24
25
|
- Easy interface
|
|
25
26
|
- Great type hinting
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
#### Networking Features
|
|
29
|
+
- HTTP 2.0 / HTTP 3.0
|
|
30
|
+
- Browser impersonation
|
|
31
|
+
- Custom JA3
|
|
32
|
+
- All proxy types
|
|
33
|
+
- Proxy authentication
|
|
34
|
+
- Speed Limit
|
|
35
|
+
- DNS over HTTPS
|
|
36
|
+
- And even more...
|
|
37
|
+
- All of this is configurable and can be adjusted as you like!
|
|
28
38
|
|
|
29
39
|
# Supported Platforms
|
|
30
40
|
This API has been tested and confirmed working on:
|
|
@@ -50,19 +60,25 @@ pip install --upgrade Eporner-API
|
|
|
50
60
|
pip install --upgrade git+https://github.com/EchterAlsFake/EPorner_API.git
|
|
51
61
|
```
|
|
52
62
|
|
|
53
|
-
|
|
54
63
|
```python
|
|
64
|
+
import asyncio
|
|
55
65
|
from eporner_api import Client
|
|
56
66
|
# Initialize a Client object
|
|
57
|
-
client = Client()
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
async def do_something():
|
|
69
|
+
client = Client()
|
|
70
|
+
|
|
71
|
+
# Fetch a video
|
|
72
|
+
video_object = await client.get_video("<insert_url_here>") # Can also be a Video ID
|
|
73
|
+
print(video_object.title)
|
|
74
|
+
|
|
75
|
+
# Search for videos
|
|
76
|
+
videos = client.search_videos(query="Your query here",..sortings..) # See Documentation!
|
|
77
|
+
async for video in videos:
|
|
78
|
+
print(video.title)
|
|
79
|
+
|
|
61
80
|
|
|
62
|
-
|
|
63
|
-
videos = client.search_videos(query="Your query here", ..sortings..) # See Documentation!
|
|
64
|
-
for video in videos:
|
|
65
|
-
print(video.title)
|
|
81
|
+
asyncio.run(do_something())
|
|
66
82
|
|
|
67
83
|
# SEE DOCUMENTATION FOR MORE
|
|
68
84
|
```
|
|
@@ -31,11 +31,14 @@ except (ModuleNotFoundError, ImportError):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
from bs4 import BeautifulSoup
|
|
34
|
+
from curl_cffi import Response, AsyncSession
|
|
34
35
|
from urllib.parse import urljoin
|
|
36
|
+
from typing import AsyncGenerator
|
|
35
37
|
from functools import cached_property
|
|
36
38
|
from base_api.modules.config import RuntimeConfig
|
|
37
|
-
from typing import AsyncGenerator, Generator, Union, Optional, List
|
|
38
39
|
from base_api.base import BaseCore, setup_logger, Helper
|
|
40
|
+
from base_api.modules.errors import InvalidProxy, BotProtectionDetected, NetworkingError, UnknownError, VideoFetchError, PageFetchError
|
|
41
|
+
from base_api.modules.static_functions import normalize_quality_value, choose_quality_from_list, str_to_bool
|
|
39
42
|
|
|
40
43
|
"""
|
|
41
44
|
Copyright (c) 2024-2026 Johannes Habel
|
|
@@ -68,42 +71,36 @@ HTML Content. See the Documentation for more details.
|
|
|
68
71
|
"""
|
|
69
72
|
|
|
70
73
|
|
|
74
|
+
async def get_html_content(core: BaseCore, url: str, get_json: bool = False) -> str | None | dict:
|
|
75
|
+
# What should I do here?
|
|
76
|
+
try:
|
|
77
|
+
content = await core.fetch(url)
|
|
78
|
+
if isinstance(content, str):
|
|
79
|
+
if get_json:
|
|
80
|
+
return json.loads(content)
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
if isinstance(q, int):
|
|
74
|
-
return q
|
|
75
|
-
s = str(q).lower().strip()
|
|
76
|
-
if s in {"best", "half", "worst"}:
|
|
77
|
-
return s
|
|
78
|
-
m = re.search(r'(\d{3,4})', s)
|
|
79
|
-
if m:
|
|
80
|
-
return int(m.group(1))
|
|
81
|
-
raise ValueError(f"Invalid quality: {q}")
|
|
82
|
+
return content
|
|
82
83
|
|
|
84
|
+
if isinstance(content, Response):
|
|
85
|
+
if content.status_code == 404:
|
|
86
|
+
raise NotFound(f"Server returned 404 for: {url}")
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
av = sorted({int(x) for x in available})
|
|
87
|
-
if isinstance(target, str):
|
|
88
|
-
if target == "best":
|
|
89
|
-
return av[-1]
|
|
90
|
-
if target == "worst":
|
|
91
|
-
return av[0]
|
|
92
|
-
if target == "half":
|
|
93
|
-
return av[len(av) // 2]
|
|
94
|
-
raise ValueError("Invalid label.")
|
|
95
|
-
# numeric: highest ≤ target, else closest
|
|
96
|
-
le = [h for h in av if h <= target]
|
|
97
|
-
if le:
|
|
98
|
-
return le[-1]
|
|
99
|
-
# fallback closest (ties -> higher)
|
|
100
|
-
return min(av, key=lambda h: (abs(h - target), -h))
|
|
88
|
+
except NetworkingError as e:
|
|
89
|
+
raise NetworkError(str(e)) from e
|
|
101
90
|
|
|
91
|
+
except InvalidProxy as e:
|
|
92
|
+
raise ProxyError(str(e)) from e
|
|
93
|
+
|
|
94
|
+
except BotProtectionDetected as e:
|
|
95
|
+
raise BotDetection(str(e)) from e
|
|
96
|
+
|
|
97
|
+
except UnknownError as e:
|
|
98
|
+
raise UnknownNetworkError(str(e)) from e
|
|
102
99
|
|
|
103
100
|
|
|
104
101
|
|
|
105
102
|
class Video:
|
|
106
|
-
def __init__(self, url: str,
|
|
103
|
+
def __init__(self, url: str, core: BaseCore, enable_html_scraping: bool = True, html_content=None):
|
|
107
104
|
self.core = core
|
|
108
105
|
self.url = url
|
|
109
106
|
self.enable_html = enable_html_scraping
|
|
@@ -125,7 +122,7 @@ class Video:
|
|
|
125
122
|
self.html_json_data = self.extract_json_from_html()
|
|
126
123
|
return self
|
|
127
124
|
|
|
128
|
-
def enable_logging(self, log_file: str, level, log_ip: str = None, log_port: int = None):
|
|
125
|
+
def enable_logging(self, log_file: str, level, log_ip: str | None = None, log_port: int | None = None):
|
|
129
126
|
self.logger = setup_logger(name="EPorner API - [Video]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
130
127
|
|
|
131
128
|
@cached_property
|
|
@@ -156,10 +153,10 @@ class Video:
|
|
|
156
153
|
Uses the V2 API to retrieve information from a video
|
|
157
154
|
:return:
|
|
158
155
|
"""
|
|
159
|
-
|
|
160
|
-
data = await
|
|
161
|
-
|
|
162
|
-
return
|
|
156
|
+
url = f"{ROOT_URL}{API_VIDEO_ID}?id={self.video_id}&thumbsize=medium&format=json"
|
|
157
|
+
data = await get_html_content(url=url, get_json=True, core=self.core)
|
|
158
|
+
assert isinstance(data, dict)
|
|
159
|
+
return data
|
|
163
160
|
|
|
164
161
|
@cached_property
|
|
165
162
|
def tags(self) -> list:
|
|
@@ -223,10 +220,11 @@ class Video:
|
|
|
223
220
|
if not self.enable_html:
|
|
224
221
|
raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
|
|
225
222
|
|
|
226
|
-
self.html_content =
|
|
227
|
-
|
|
223
|
+
self.html_content = await get_html_content(core=self.core, url=self.url)
|
|
224
|
+
assert isinstance(self.html_content, str)
|
|
225
|
+
self.html_content = html.unescape(self.html_content)
|
|
228
226
|
|
|
229
|
-
def extract_json_from_html(self):
|
|
227
|
+
def extract_json_from_html(self) -> dict:
|
|
230
228
|
if not self.enable_html:
|
|
231
229
|
raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
|
|
232
230
|
|
|
@@ -268,13 +266,13 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
268
266
|
return dict(items)
|
|
269
267
|
|
|
270
268
|
@cached_property
|
|
271
|
-
def bitrate(self) -> str:
|
|
269
|
+
def bitrate(self) -> str | None:
|
|
272
270
|
"""Return the bitrate of the video? (I don't know)"""
|
|
273
271
|
return self.html_json_data["bitrate"] if self.enable_html else None
|
|
274
272
|
|
|
275
273
|
|
|
276
274
|
@cached_property
|
|
277
|
-
def source_video_url(self) -> str:
|
|
275
|
+
def source_video_url(self) -> str | None:
|
|
278
276
|
"""
|
|
279
277
|
Returns the .mp4 video location URL
|
|
280
278
|
|
|
@@ -284,7 +282,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
284
282
|
|
|
285
283
|
|
|
286
284
|
@cached_property
|
|
287
|
-
def rating(self) -> str:
|
|
285
|
+
def rating(self) -> str | None:
|
|
288
286
|
"""
|
|
289
287
|
Returns the rating value. Highest (best) is 100, least is zero (worst)
|
|
290
288
|
:return: str
|
|
@@ -297,7 +295,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
297
295
|
raise NotAvailable("No rating available. This isn't an error!")
|
|
298
296
|
|
|
299
297
|
@cached_property
|
|
300
|
-
def likes(self) -> str:
|
|
298
|
+
def likes(self) -> str | None:
|
|
301
299
|
"""
|
|
302
300
|
Returns the video likes
|
|
303
301
|
:return: str
|
|
@@ -305,7 +303,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
305
303
|
return REGEX_VIDEO_LIKES.search(self.html_content).group(1) if self.enable_html else None
|
|
306
304
|
|
|
307
305
|
@cached_property
|
|
308
|
-
def dislikes(self) -> str:
|
|
306
|
+
def dislikes(self) -> str | None:
|
|
309
307
|
"""
|
|
310
308
|
Returns the video dislikes
|
|
311
309
|
:return:
|
|
@@ -313,7 +311,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
313
311
|
return REGEX_VIDEO_DISLIKES.search(self.html_content).group(1) if self.enable_html else None
|
|
314
312
|
|
|
315
313
|
@cached_property
|
|
316
|
-
def rating_count(self) -> str:
|
|
314
|
+
def rating_count(self) -> str | None:
|
|
317
315
|
"""
|
|
318
316
|
Returns how many people have rated the video
|
|
319
317
|
:return: str
|
|
@@ -321,7 +319,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
321
319
|
return self.html_json_data["aggregateRating_ratingCount"] if self.enable_html else None
|
|
322
320
|
|
|
323
321
|
@cached_property
|
|
324
|
-
def author(self) -> str:
|
|
322
|
+
def author(self) -> str | None:
|
|
325
323
|
"""
|
|
326
324
|
Returns the Uploader of the Video
|
|
327
325
|
:return: str
|
|
@@ -394,8 +392,8 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
394
392
|
|
|
395
393
|
# Choose the appropriate height using your helpers
|
|
396
394
|
available_heights = sorted(quality_to_url.keys()) # e.g., [240, 360, 480, 720, 1080]
|
|
397
|
-
qn =
|
|
398
|
-
chosen_height =
|
|
395
|
+
qn = normalize_quality_value(quality) # -> 'best' | 'half' | 'worst' | int
|
|
396
|
+
chosen_height = choose_quality_from_list(available_heights, qn)
|
|
399
397
|
|
|
400
398
|
# Map back to URL and return absolute URL
|
|
401
399
|
chosen_url = quality_to_url[chosen_height]
|
|
@@ -405,7 +403,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
405
403
|
return full_url
|
|
406
404
|
|
|
407
405
|
async def download(self, quality, path, callback=None, mode=Encoding.mp4_h264, no_title=False, use_workaround=False,
|
|
408
|
-
stop_event: threading.Event = None):
|
|
406
|
+
stop_event: threading.Event | None = None):
|
|
409
407
|
if not self.enable_html:
|
|
410
408
|
raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
|
|
411
409
|
|
|
@@ -432,8 +430,8 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
432
430
|
|
|
433
431
|
|
|
434
432
|
class Pornstar(Helper):
|
|
435
|
-
def __init__(self, url: str,
|
|
436
|
-
super().__init__(core=core,
|
|
433
|
+
def __init__(self, url: str, core: BaseCore, enable_html_scraping: bool = False, html_content=None):
|
|
434
|
+
super().__init__(core=core, video_constructor=Video)
|
|
437
435
|
self.core = core
|
|
438
436
|
self.url = url
|
|
439
437
|
self.enable_html_scraping = enable_html_scraping
|
|
@@ -442,24 +440,30 @@ class Pornstar(Helper):
|
|
|
442
440
|
|
|
443
441
|
async def init(self):
|
|
444
442
|
if not self.html_content:
|
|
445
|
-
self.html_content = await self.core
|
|
443
|
+
self.html_content = await get_html_content(core=self.core, url=self.url)
|
|
444
|
+
assert isinstance(self.html_content, str)
|
|
445
|
+
|
|
446
446
|
return self
|
|
447
447
|
|
|
448
|
-
def enable_logging(self, log_file: str, level, log_ip: str = None, log_port: int = None):
|
|
448
|
+
def enable_logging(self, log_file: str, level, log_ip: str | None = None, log_port: int | None = None):
|
|
449
449
|
self.logger = setup_logger(name="EPorner API - [Pornstar]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
450
450
|
|
|
451
|
-
async def videos(self, pages: int = 0, videos_concurrency: int = None, pages_concurrency: int = None) -> AsyncGenerator[Video, None]:
|
|
451
|
+
async def videos(self, pages: int = 0, videos_concurrency: int | None = None, pages_concurrency: int | None = None) -> AsyncGenerator[Video, None]:
|
|
452
452
|
if pages == 0:
|
|
453
453
|
video_amount = str(self.video_amount).replace(",", "")
|
|
454
454
|
pages = round(int(video_amount)) / 37 # One page contains 37 videos
|
|
455
455
|
|
|
456
|
-
videos_concurrency = videos_concurrency or self.core.
|
|
457
|
-
pages_concurrency = pages_concurrency or self.core.
|
|
456
|
+
videos_concurrency = videos_concurrency or self.core.configuration.videos_concurrency
|
|
457
|
+
pages_concurrency = pages_concurrency or self.core.configuration.pages_concurrency
|
|
458
|
+
assert videos_concurrency and pages_concurrency
|
|
458
459
|
|
|
459
460
|
pages = round(pages) # Dont ask
|
|
460
461
|
page_urls = [urljoin(f"{self.url}/", str(page)) for page in range(1, pages + 1)]
|
|
461
|
-
async for video in self.iterator(
|
|
462
|
-
|
|
462
|
+
async for video in self.iterator(target_page_urls=page_urls, video_link_extractor=extractor, max_page_concurrency=pages_concurrency,
|
|
463
|
+
max_video_concurrency=videos_concurrency):
|
|
464
|
+
if isinstance(video, (VideoFetchError, PageFetchError)):
|
|
465
|
+
self.logger.error(f"Error during iteration: {video}")
|
|
466
|
+
continue
|
|
463
467
|
yield await video.init()
|
|
464
468
|
|
|
465
469
|
@cached_property
|
|
@@ -567,14 +571,15 @@ class Pornstar(Helper):
|
|
|
567
571
|
|
|
568
572
|
|
|
569
573
|
class Client(Helper):
|
|
570
|
-
def __init__(self, core:
|
|
571
|
-
super().__init__(core,
|
|
572
|
-
self.core = core
|
|
574
|
+
def __init__(self, core: BaseCore = BaseCore(RuntimeConfig())):
|
|
575
|
+
super().__init__(core, video_constructor=Video)
|
|
576
|
+
self.core = core
|
|
573
577
|
self.core.initialize_session()
|
|
578
|
+
assert isinstance(self.core.session, AsyncSession)
|
|
574
579
|
self.core.session.headers.update(headers)
|
|
575
580
|
self.logger = setup_logger(name="EPorner API - [Client]", log_file=None, level=logging.CRITICAL)
|
|
576
581
|
|
|
577
|
-
def enable_logging(self, log_file: str, level, log_ip: str = None, log_port: int = None):
|
|
582
|
+
def enable_logging(self, log_file: str, level, log_ip: str | None = None, log_port: int | None = None):
|
|
578
583
|
self.logger = setup_logger(name="EPorner API - [Client]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
579
584
|
|
|
580
585
|
async def get_video(self, url: str, enable_html_scraping: bool = True) -> Video:
|
|
@@ -583,35 +588,38 @@ class Client(Helper):
|
|
|
583
588
|
video = Video(url, enable_html_scraping=enable_html_scraping, core=self.core)
|
|
584
589
|
return await video.init()
|
|
585
590
|
|
|
586
|
-
async def search_videos(self, query: str, sorting_gay:
|
|
587
|
-
sorting_low_quality:
|
|
591
|
+
async def search_videos(self, query: str, sorting_gay: str | Gay, sorting_order: str | Order,
|
|
592
|
+
sorting_low_quality: str | LowQuality,
|
|
588
593
|
page: int, per_page: int, enable_html_scraping: bool = True) -> AsyncGenerator[Video, None]:
|
|
589
594
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
595
|
+
url = f"{ROOT_URL}{API_SEARCH}?query={query}&per_page={per_page}&%page={page}&thumbsize=medium&order={sorting_order}&gay={sorting_gay}&lq={sorting_low_quality}&format=json"
|
|
596
|
+
json_data = await get_html_content(core=self.core, url=url, get_json=True)
|
|
597
|
+
assert isinstance(json_data, dict)
|
|
593
598
|
|
|
594
|
-
json_data = json.loads(response)
|
|
595
599
|
for video_ in json_data.get("videos", []): # Don't know why this works lmao
|
|
596
600
|
id_ = video_["url"]
|
|
597
|
-
video = Video(id_,
|
|
601
|
+
video = Video(url=id_, core=self.core, enable_html_scraping=enable_html_scraping)
|
|
598
602
|
yield await video.init()
|
|
599
603
|
|
|
600
|
-
async def get_videos_by_category(self, category:
|
|
601
|
-
videos_concurrency: int = None, pages_concurrency: int = None) -> AsyncGenerator[Video, None]:
|
|
604
|
+
async def get_videos_by_category(self, category: str | Category, enable_html_scraping: bool = False,
|
|
605
|
+
videos_concurrency: int | None = None, pages_concurrency: int | None = None) -> AsyncGenerator[Video, None]:
|
|
602
606
|
|
|
603
607
|
page_urls = [f"{ROOT_URL}cat/{category}/{page}" for page in range(1, 100)]
|
|
604
608
|
|
|
605
|
-
videos_concurrency = videos_concurrency or self.core.
|
|
606
|
-
pages_concurrency = pages_concurrency or self.core.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
+
videos_concurrency = videos_concurrency or self.core.configuration.videos_concurrency
|
|
610
|
+
pages_concurrency = pages_concurrency or self.core.configuration.pages_concurrency
|
|
611
|
+
assert videos_concurrency and pages_concurrency
|
|
612
|
+
async for video in self.iterator(target_page_urls=page_urls, max_video_concurrency=videos_concurrency,
|
|
613
|
+
max_page_concurrency=pages_concurrency, video_link_extractor=extractor):
|
|
614
|
+
if isinstance(video, (VideoFetchError, PageFetchError)):
|
|
615
|
+
self.logger.error(f"Error during iteration: {video}")
|
|
616
|
+
continue
|
|
609
617
|
yield await video.init()
|
|
610
618
|
|
|
611
619
|
|
|
612
620
|
async def get_pornstar(self, url: str, enable_html_scraping: bool = True) -> Pornstar:
|
|
613
621
|
self.logger.info(f"Returning Pornstar object for: {url} HTML Scraping -> {enable_html_scraping}")
|
|
614
|
-
pornstar = Pornstar(url, enable_html_scraping, core=self.core)
|
|
622
|
+
pornstar = Pornstar(url=url, enable_html_scraping=enable_html_scraping, core=self.core)
|
|
615
623
|
return await pornstar.init()
|
|
616
624
|
|
|
617
625
|
|
|
@@ -628,7 +636,7 @@ async def run_main():
|
|
|
628
636
|
help="Whether to apply video title automatically to output path or not", required=True)
|
|
629
637
|
|
|
630
638
|
args = parser.parse_args()
|
|
631
|
-
no_title =
|
|
639
|
+
no_title = str_to_bool(args.no_title)
|
|
632
640
|
|
|
633
641
|
if args.download:
|
|
634
642
|
client = Client()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class InvalidURL(Exception):
|
|
2
|
+
def __init__(self, msg):
|
|
3
|
+
self.msg = msg
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HTML_IS_DISABLED(Exception):
|
|
7
|
+
def __init__(self, msg):
|
|
8
|
+
self.msg = msg
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NotAvailable(Exception):
|
|
12
|
+
def __init__(self, msg):
|
|
13
|
+
self.msg = msg
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VideoDisabled(Exception):
|
|
17
|
+
def __init__(self, msg):
|
|
18
|
+
self.msg = msg
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InvalidVideo(Exception):
|
|
22
|
+
def __init__(self, msg):
|
|
23
|
+
self.msg = msg
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NotFound(Exception):
|
|
27
|
+
def __init__(self, msg: str):
|
|
28
|
+
self.msg = msg
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NetworkError(Exception):
|
|
32
|
+
def __init__(self, msg: str):
|
|
33
|
+
self.msg = msg
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BotDetection(Exception):
|
|
37
|
+
def __init__(self, msg: str):
|
|
38
|
+
self.msg = msg
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProxyError(Exception):
|
|
42
|
+
def __init__(self, msg: str):
|
|
43
|
+
self.msg = msg
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UnknownNetworkError(Exception):
|
|
47
|
+
def __init__(self, msg):
|
|
48
|
+
self.msg = msg
|
|
@@ -5,8 +5,8 @@ from base_api import BaseCore
|
|
|
5
5
|
@pytest.mark.asyncio
|
|
6
6
|
async def test_category():
|
|
7
7
|
core = BaseCore()
|
|
8
|
-
core.
|
|
9
|
-
core.
|
|
8
|
+
core.configuration.pages_concurrency = 1
|
|
9
|
+
core.configuration.videos_concurrency = 1
|
|
10
10
|
|
|
11
11
|
videos_1 = Client(core).get_videos_by_category(category=Category.JAPANESE)
|
|
12
12
|
videos_2 = Client(core).get_videos_by_category(category=Category.HD)
|
|
@@ -6,8 +6,8 @@ from base_api import BaseCore
|
|
|
6
6
|
async def test_pornstar():
|
|
7
7
|
url = "https://www.eporner.com/pornstar/riley-reid/"
|
|
8
8
|
core = BaseCore()
|
|
9
|
-
core.
|
|
10
|
-
core.
|
|
9
|
+
core.configuration.pages_concurrency = 1
|
|
10
|
+
core.configuration.videos_concurrency = 1
|
|
11
11
|
pornstar = await Client(core).get_pornstar(url, enable_html_scraping=True)
|
|
12
12
|
|
|
13
13
|
videos = pornstar.videos(pages=1)
|
|
@@ -3,7 +3,7 @@ from ..eporner_api import Client, Encoding, NotAvailable
|
|
|
3
3
|
|
|
4
4
|
@pytest.mark.asyncio
|
|
5
5
|
async def test_video():
|
|
6
|
-
url = "https://www.eporner.com/video-
|
|
6
|
+
url = "https://www.eporner.com/video-pDRNfJoN7dN/granny-with-young-guy/"
|
|
7
7
|
video = await Client().get_video(url, enable_html_scraping=True)
|
|
8
8
|
assert isinstance(video.title, str) and len(video.title) > 0
|
|
9
9
|
assert isinstance(video.video_id, str) and len(video.video_id) > 0
|
|
@@ -4,10 +4,10 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "Eporner_API"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1"
|
|
8
8
|
description = "A Python API for the Porn Site Eporner.com"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = "LGPL-3.0-only"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
class InvalidURL(Exception):
|
|
2
|
-
def __init__(self, msg):
|
|
3
|
-
self.msg = msg
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class HTML_IS_DISABLED(Exception):
|
|
7
|
-
def __init__(self, msg):
|
|
8
|
-
self.msg = msg
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class NotAvailable(Exception):
|
|
12
|
-
def __init__(self, msg):
|
|
13
|
-
self.msg = msg
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class VideoDisabled(Exception):
|
|
17
|
-
def __init__(self, msg):
|
|
18
|
-
self.msg = msg
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class InvalidVideo(Exception):
|
|
22
|
-
def __init__(self, msg):
|
|
23
|
-
self.msg = msg
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|