Eporner_API 2.0__tar.gz → 2.2__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.2}/PKG-INFO +28 -12
- {eporner_api-2.0 → eporner_api-2.2}/README.md +26 -10
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/eporner_api.py +97 -75
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/modules/consts.py +2 -2
- eporner_api-2.2/eporner_api/modules/errors.py +48 -0
- eporner_api-2.2/eporner_api/modules/type_hints.py +4 -0
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/tests/test_category.py +2 -2
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/tests/test_pornstar.py +2 -2
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/tests/test_video.py +1 -1
- {eporner_api-2.0 → eporner_api-2.2}/pyproject.toml +2 -2
- eporner_api-2.0/eporner_api/modules/errors.py +0 -23
- {eporner_api-2.0 → eporner_api-2.2}/LICENSE +0 -0
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/__init__.py +0 -0
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/modules/__init__.py +0 -0
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/modules/locals.py +0 -0
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/modules/progressbar.py +0 -0
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/modules/sorting.py +0 -0
- {eporner_api-2.0 → eporner_api-2.2}/eporner_api/tests/__init__.py +0 -0
- {eporner_api-2.0 → eporner_api-2.2}/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.2
|
|
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.12
|
|
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
|
```
|
|
@@ -8,6 +8,8 @@ import argparse
|
|
|
8
8
|
import traceback
|
|
9
9
|
import threading
|
|
10
10
|
|
|
11
|
+
from eporner_api.modules.type_hints import on_error_hint
|
|
12
|
+
|
|
11
13
|
try:
|
|
12
14
|
from .modules.consts import *
|
|
13
15
|
from .modules.locals import *
|
|
@@ -31,11 +33,14 @@ except (ModuleNotFoundError, ImportError):
|
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
from bs4 import BeautifulSoup
|
|
36
|
+
from curl_cffi import Response, AsyncSession
|
|
34
37
|
from urllib.parse import urljoin
|
|
38
|
+
from typing import AsyncGenerator
|
|
35
39
|
from functools import cached_property
|
|
36
40
|
from base_api.modules.config import RuntimeConfig
|
|
37
|
-
from typing import AsyncGenerator, Generator, Union, Optional, List
|
|
38
41
|
from base_api.base import BaseCore, setup_logger, Helper
|
|
42
|
+
from base_api.modules.static_functions import normalize_quality_value, choose_quality_from_list, str_to_bool
|
|
43
|
+
from base_api.modules.errors import InvalidProxy, BotProtectionDetected, NetworkingError, UnknownError, VideoFetchError, PageFetchError, ResourceGone
|
|
39
44
|
|
|
40
45
|
"""
|
|
41
46
|
Copyright (c) 2024-2026 Johannes Habel
|
|
@@ -67,43 +72,45 @@ If you still need additional functionalities and information from videos / Eporn
|
|
|
67
72
|
HTML Content. See the Documentation for more details.
|
|
68
73
|
"""
|
|
69
74
|
|
|
75
|
+
async def on_error(url: str, error: Exception, attempt: int) -> bool:
|
|
76
|
+
print(f"URL: {url}, ERROR: {error}, Attempt: {attempt}")
|
|
77
|
+
|
|
78
|
+
if isinstance(error, ResourceGone):
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def get_html_content(core: BaseCore, url: str, get_json: bool = False) -> str | None | dict:
|
|
85
|
+
# What should I do here?
|
|
86
|
+
try:
|
|
87
|
+
content = await core.fetch(url)
|
|
88
|
+
if isinstance(content, str):
|
|
89
|
+
if get_json:
|
|
90
|
+
return json.loads(content)
|
|
91
|
+
|
|
92
|
+
return content
|
|
70
93
|
|
|
94
|
+
if isinstance(content, Response):
|
|
95
|
+
if content.status_code == 404:
|
|
96
|
+
raise NotFound(f"Server returned 404 for: {url}")
|
|
71
97
|
|
|
72
|
-
|
|
73
|
-
|
|
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}")
|
|
98
|
+
except NetworkingError as e:
|
|
99
|
+
raise NetworkError(str(e)) from e
|
|
82
100
|
|
|
101
|
+
except InvalidProxy as e:
|
|
102
|
+
raise ProxyError(str(e)) from e
|
|
83
103
|
|
|
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))
|
|
104
|
+
except BotProtectionDetected as e:
|
|
105
|
+
raise BotDetection(str(e)) from e
|
|
101
106
|
|
|
107
|
+
except UnknownError as e:
|
|
108
|
+
raise UnknownNetworkError(str(e)) from e
|
|
102
109
|
|
|
103
110
|
|
|
104
111
|
|
|
105
112
|
class Video:
|
|
106
|
-
def __init__(self, url: str,
|
|
113
|
+
def __init__(self, url: str, core: BaseCore, enable_html_scraping: bool = True, html_content=None):
|
|
107
114
|
self.core = core
|
|
108
115
|
self.url = url
|
|
109
116
|
self.enable_html = enable_html_scraping
|
|
@@ -125,7 +132,7 @@ class Video:
|
|
|
125
132
|
self.html_json_data = self.extract_json_from_html()
|
|
126
133
|
return self
|
|
127
134
|
|
|
128
|
-
def enable_logging(self, log_file: str, level, log_ip: str = None, log_port: int = None):
|
|
135
|
+
def enable_logging(self, log_file: str, level, log_ip: str | None = None, log_port: int | None = None):
|
|
129
136
|
self.logger = setup_logger(name="EPorner API - [Video]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
130
137
|
|
|
131
138
|
@cached_property
|
|
@@ -156,10 +163,10 @@ class Video:
|
|
|
156
163
|
Uses the V2 API to retrieve information from a video
|
|
157
164
|
:return:
|
|
158
165
|
"""
|
|
159
|
-
|
|
160
|
-
data = await
|
|
161
|
-
|
|
162
|
-
return
|
|
166
|
+
url = f"{ROOT_URL}{API_VIDEO_ID}?id={self.video_id}&thumbsize=medium&format=json"
|
|
167
|
+
data = await get_html_content(url=url, get_json=True, core=self.core)
|
|
168
|
+
assert isinstance(data, dict)
|
|
169
|
+
return data
|
|
163
170
|
|
|
164
171
|
@cached_property
|
|
165
172
|
def tags(self) -> list:
|
|
@@ -223,10 +230,11 @@ class Video:
|
|
|
223
230
|
if not self.enable_html:
|
|
224
231
|
raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
|
|
225
232
|
|
|
226
|
-
self.html_content =
|
|
227
|
-
|
|
233
|
+
self.html_content = await get_html_content(core=self.core, url=self.url)
|
|
234
|
+
assert isinstance(self.html_content, str)
|
|
235
|
+
self.html_content = html.unescape(self.html_content)
|
|
228
236
|
|
|
229
|
-
def extract_json_from_html(self):
|
|
237
|
+
def extract_json_from_html(self) -> dict:
|
|
230
238
|
if not self.enable_html:
|
|
231
239
|
raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
|
|
232
240
|
|
|
@@ -268,13 +276,13 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
268
276
|
return dict(items)
|
|
269
277
|
|
|
270
278
|
@cached_property
|
|
271
|
-
def bitrate(self) -> str:
|
|
279
|
+
def bitrate(self) -> str | None:
|
|
272
280
|
"""Return the bitrate of the video? (I don't know)"""
|
|
273
281
|
return self.html_json_data["bitrate"] if self.enable_html else None
|
|
274
282
|
|
|
275
283
|
|
|
276
284
|
@cached_property
|
|
277
|
-
def source_video_url(self) -> str:
|
|
285
|
+
def source_video_url(self) -> str | None:
|
|
278
286
|
"""
|
|
279
287
|
Returns the .mp4 video location URL
|
|
280
288
|
|
|
@@ -284,7 +292,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
284
292
|
|
|
285
293
|
|
|
286
294
|
@cached_property
|
|
287
|
-
def rating(self) -> str:
|
|
295
|
+
def rating(self) -> str | None:
|
|
288
296
|
"""
|
|
289
297
|
Returns the rating value. Highest (best) is 100, least is zero (worst)
|
|
290
298
|
:return: str
|
|
@@ -297,7 +305,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
297
305
|
raise NotAvailable("No rating available. This isn't an error!")
|
|
298
306
|
|
|
299
307
|
@cached_property
|
|
300
|
-
def likes(self) -> str:
|
|
308
|
+
def likes(self) -> str | None:
|
|
301
309
|
"""
|
|
302
310
|
Returns the video likes
|
|
303
311
|
:return: str
|
|
@@ -305,7 +313,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
305
313
|
return REGEX_VIDEO_LIKES.search(self.html_content).group(1) if self.enable_html else None
|
|
306
314
|
|
|
307
315
|
@cached_property
|
|
308
|
-
def dislikes(self) -> str:
|
|
316
|
+
def dislikes(self) -> str | None:
|
|
309
317
|
"""
|
|
310
318
|
Returns the video dislikes
|
|
311
319
|
:return:
|
|
@@ -313,7 +321,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
313
321
|
return REGEX_VIDEO_DISLIKES.search(self.html_content).group(1) if self.enable_html else None
|
|
314
322
|
|
|
315
323
|
@cached_property
|
|
316
|
-
def rating_count(self) -> str:
|
|
324
|
+
def rating_count(self) -> str | None:
|
|
317
325
|
"""
|
|
318
326
|
Returns how many people have rated the video
|
|
319
327
|
:return: str
|
|
@@ -321,7 +329,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
321
329
|
return self.html_json_data["aggregateRating_ratingCount"] if self.enable_html else None
|
|
322
330
|
|
|
323
331
|
@cached_property
|
|
324
|
-
def author(self) -> str:
|
|
332
|
+
def author(self) -> str | None:
|
|
325
333
|
"""
|
|
326
334
|
Returns the Uploader of the Video
|
|
327
335
|
:return: str
|
|
@@ -394,8 +402,8 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
394
402
|
|
|
395
403
|
# Choose the appropriate height using your helpers
|
|
396
404
|
available_heights = sorted(quality_to_url.keys()) # e.g., [240, 360, 480, 720, 1080]
|
|
397
|
-
qn =
|
|
398
|
-
chosen_height =
|
|
405
|
+
qn = normalize_quality_value(quality) # -> 'best' | 'half' | 'worst' | int
|
|
406
|
+
chosen_height = choose_quality_from_list(available_heights, qn)
|
|
399
407
|
|
|
400
408
|
# Map back to URL and return absolute URL
|
|
401
409
|
chosen_url = quality_to_url[chosen_height]
|
|
@@ -405,7 +413,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
405
413
|
return full_url
|
|
406
414
|
|
|
407
415
|
async def download(self, quality, path, callback=None, mode=Encoding.mp4_h264, no_title=False, use_workaround=False,
|
|
408
|
-
stop_event: threading.Event = None):
|
|
416
|
+
stop_event: threading.Event | None = None):
|
|
409
417
|
if not self.enable_html:
|
|
410
418
|
raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
|
|
411
419
|
|
|
@@ -432,8 +440,8 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
432
440
|
|
|
433
441
|
|
|
434
442
|
class Pornstar(Helper):
|
|
435
|
-
def __init__(self, url: str,
|
|
436
|
-
super().__init__(core=core,
|
|
443
|
+
def __init__(self, url: str, core: BaseCore, enable_html_scraping: bool = False, html_content=None):
|
|
444
|
+
super().__init__(core=core, video_constructor=Video)
|
|
437
445
|
self.core = core
|
|
438
446
|
self.url = url
|
|
439
447
|
self.enable_html_scraping = enable_html_scraping
|
|
@@ -442,24 +450,32 @@ class Pornstar(Helper):
|
|
|
442
450
|
|
|
443
451
|
async def init(self):
|
|
444
452
|
if not self.html_content:
|
|
445
|
-
self.html_content = await self.core
|
|
453
|
+
self.html_content = await get_html_content(core=self.core, url=self.url)
|
|
454
|
+
assert isinstance(self.html_content, str)
|
|
455
|
+
|
|
446
456
|
return self
|
|
447
457
|
|
|
448
|
-
def enable_logging(self, log_file: str, level, log_ip: str = None, log_port: int = None):
|
|
458
|
+
def enable_logging(self, log_file: str, level, log_ip: str | None = None, log_port: int | None = None):
|
|
449
459
|
self.logger = setup_logger(name="EPorner API - [Pornstar]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
450
460
|
|
|
451
|
-
async def videos(self, pages: int = 0, videos_concurrency: int = None, pages_concurrency: int
|
|
461
|
+
async def videos(self, pages: int = 0, videos_concurrency: int | None = None, pages_concurrency: int | None = None,
|
|
462
|
+
on_video_error: on_error_hint = on_error, on_page_error: on_error_hint = None) -> AsyncGenerator[Video, None]:
|
|
452
463
|
if pages == 0:
|
|
453
464
|
video_amount = str(self.video_amount).replace(",", "")
|
|
454
465
|
pages = round(int(video_amount)) / 37 # One page contains 37 videos
|
|
455
466
|
|
|
456
|
-
videos_concurrency = videos_concurrency or self.core.
|
|
457
|
-
pages_concurrency = pages_concurrency or self.core.
|
|
467
|
+
videos_concurrency = videos_concurrency or self.core.configuration.videos_concurrency
|
|
468
|
+
pages_concurrency = pages_concurrency or self.core.configuration.pages_concurrency
|
|
469
|
+
assert videos_concurrency and pages_concurrency
|
|
458
470
|
|
|
459
471
|
pages = round(pages) # Dont ask
|
|
460
472
|
page_urls = [urljoin(f"{self.url}/", str(page)) for page in range(1, pages + 1)]
|
|
461
|
-
async for video in self.iterator(
|
|
462
|
-
|
|
473
|
+
async for video in self.iterator(target_page_urls=page_urls, video_link_extractor=extractor, max_page_concurrency=pages_concurrency,
|
|
474
|
+
max_video_concurrency=videos_concurrency,
|
|
475
|
+
on_video_error=on_video_error, on_page_error=on_page_error):
|
|
476
|
+
if isinstance(video, (VideoFetchError, PageFetchError)):
|
|
477
|
+
self.logger.error(f"Error during iteration: {video}")
|
|
478
|
+
continue
|
|
463
479
|
yield await video.init()
|
|
464
480
|
|
|
465
481
|
@cached_property
|
|
@@ -567,14 +583,15 @@ class Pornstar(Helper):
|
|
|
567
583
|
|
|
568
584
|
|
|
569
585
|
class Client(Helper):
|
|
570
|
-
def __init__(self, core:
|
|
571
|
-
super().__init__(core,
|
|
572
|
-
self.core = core
|
|
586
|
+
def __init__(self, core: BaseCore = BaseCore(RuntimeConfig())):
|
|
587
|
+
super().__init__(core, video_constructor=Video)
|
|
588
|
+
self.core = core
|
|
573
589
|
self.core.initialize_session()
|
|
590
|
+
assert isinstance(self.core.session, AsyncSession)
|
|
574
591
|
self.core.session.headers.update(headers)
|
|
575
592
|
self.logger = setup_logger(name="EPorner API - [Client]", log_file=None, level=logging.CRITICAL)
|
|
576
593
|
|
|
577
|
-
def enable_logging(self, log_file: str, level, log_ip: str = None, log_port: int = None):
|
|
594
|
+
def enable_logging(self, log_file: str, level, log_ip: str | None = None, log_port: int | None = None):
|
|
578
595
|
self.logger = setup_logger(name="EPorner API - [Client]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
579
596
|
|
|
580
597
|
async def get_video(self, url: str, enable_html_scraping: bool = True) -> Video:
|
|
@@ -583,35 +600,40 @@ class Client(Helper):
|
|
|
583
600
|
video = Video(url, enable_html_scraping=enable_html_scraping, core=self.core)
|
|
584
601
|
return await video.init()
|
|
585
602
|
|
|
586
|
-
async def search_videos(self, query: str, sorting_gay:
|
|
587
|
-
sorting_low_quality:
|
|
603
|
+
async def search_videos(self, query: str, sorting_gay: str | Gay, sorting_order: str | Order,
|
|
604
|
+
sorting_low_quality: str | LowQuality,
|
|
588
605
|
page: int, per_page: int, enable_html_scraping: bool = True) -> AsyncGenerator[Video, None]:
|
|
589
606
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
607
|
+
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"
|
|
608
|
+
json_data = await get_html_content(core=self.core, url=url, get_json=True)
|
|
609
|
+
assert isinstance(json_data, dict)
|
|
593
610
|
|
|
594
|
-
json_data = json.loads(response)
|
|
595
611
|
for video_ in json_data.get("videos", []): # Don't know why this works lmao
|
|
596
612
|
id_ = video_["url"]
|
|
597
|
-
video = Video(id_,
|
|
613
|
+
video = Video(url=id_, core=self.core, enable_html_scraping=enable_html_scraping)
|
|
598
614
|
yield await video.init()
|
|
599
615
|
|
|
600
|
-
async def get_videos_by_category(self, category:
|
|
601
|
-
videos_concurrency: int = None, pages_concurrency: int
|
|
616
|
+
async def get_videos_by_category(self, category: str | Category,
|
|
617
|
+
videos_concurrency: int | None = None, pages_concurrency: int | None = None,
|
|
618
|
+
on_video_error: on_error_hint = on_error, on_page_error: on_error_hint = None) -> AsyncGenerator[Video, None]:
|
|
602
619
|
|
|
603
620
|
page_urls = [f"{ROOT_URL}cat/{category}/{page}" for page in range(1, 100)]
|
|
604
621
|
|
|
605
|
-
videos_concurrency = videos_concurrency or self.core.
|
|
606
|
-
pages_concurrency = pages_concurrency or self.core.
|
|
607
|
-
|
|
608
|
-
|
|
622
|
+
videos_concurrency = videos_concurrency or self.core.configuration.videos_concurrency
|
|
623
|
+
pages_concurrency = pages_concurrency or self.core.configuration.pages_concurrency
|
|
624
|
+
assert videos_concurrency and pages_concurrency
|
|
625
|
+
async for video in self.iterator(target_page_urls=page_urls, max_video_concurrency=videos_concurrency,
|
|
626
|
+
max_page_concurrency=pages_concurrency, video_link_extractor=extractor,
|
|
627
|
+
on_video_error=on_video_error, on_page_error=on_page_error):
|
|
628
|
+
if isinstance(video, (VideoFetchError, PageFetchError)):
|
|
629
|
+
self.logger.error(f"Error during iteration: {video}")
|
|
630
|
+
continue
|
|
609
631
|
yield await video.init()
|
|
610
632
|
|
|
611
633
|
|
|
612
634
|
async def get_pornstar(self, url: str, enable_html_scraping: bool = True) -> Pornstar:
|
|
613
635
|
self.logger.info(f"Returning Pornstar object for: {url} HTML Scraping -> {enable_html_scraping}")
|
|
614
|
-
pornstar = Pornstar(url, enable_html_scraping, core=self.core)
|
|
636
|
+
pornstar = Pornstar(url=url, enable_html_scraping=enable_html_scraping, core=self.core)
|
|
615
637
|
return await pornstar.init()
|
|
616
638
|
|
|
617
639
|
|
|
@@ -628,7 +650,7 @@ async def run_main():
|
|
|
628
650
|
help="Whether to apply video title automatically to output path or not", required=True)
|
|
629
651
|
|
|
630
652
|
args = parser.parse_args()
|
|
631
|
-
no_title =
|
|
653
|
+
no_title = str_to_bool(args.no_title)
|
|
632
654
|
|
|
633
655
|
if args.download:
|
|
634
656
|
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.2"
|
|
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.12"
|
|
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
|