Eporner_API 1.9.7__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-1.9.7 → eporner_api-2.1}/PKG-INFO +28 -14
- {eporner_api-1.9.7 → eporner_api-2.1}/README.md +26 -10
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/eporner_api.py +120 -92
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/modules/consts.py +2 -2
- eporner_api-2.1/eporner_api/modules/errors.py +48 -0
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/tests/test_category.py +16 -13
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/tests/test_pornstar.py +13 -12
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/tests/test_search.py +29 -24
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/tests/test_video.py +6 -51
- {eporner_api-1.9.7 → eporner_api-2.1}/pyproject.toml +4 -4
- eporner_api-1.9.7/eporner_api/modules/errors.py +0 -23
- {eporner_api-1.9.7 → eporner_api-2.1}/LICENSE +0 -0
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/__init__.py +0 -0
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/modules/__init__.py +0 -0
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/modules/locals.py +0 -0
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/modules/progressbar.py +0 -0
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/modules/sorting.py +0 -0
- {eporner_api-1.9.7 → eporner_api-2.1}/eporner_api/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Eporner_API
|
|
3
|
-
Version: 1
|
|
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,9 +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-
|
|
14
|
-
Requires-Dist: httpx[socks] ; extra == 'full'
|
|
15
|
-
Requires-Python: >=3.9
|
|
13
|
+
Requires-Python: >=3.10
|
|
16
14
|
Project-URL: Homepage, https://github.com/EchterAlsFake/EPorner_API
|
|
17
15
|
Provides-Extra: full
|
|
18
16
|
Description-Content-Type: text/markdown
|
|
@@ -34,6 +32,7 @@ Description-Content-Type: text/markdown
|
|
|
34
32
|
> and applicable laws. Do not use it to bypass access controls or scrape at disruptive rates.
|
|
35
33
|
|
|
36
34
|
# Features
|
|
35
|
+
- Asynchronous
|
|
37
36
|
- Fetch videos + metadata
|
|
38
37
|
- Download videos
|
|
39
38
|
- Fetch Pornstars
|
|
@@ -42,8 +41,17 @@ Description-Content-Type: text/markdown
|
|
|
42
41
|
- Built-in caching
|
|
43
42
|
- Easy interface
|
|
44
43
|
- Great type hinting
|
|
45
|
-
|
|
46
|
-
|
|
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!
|
|
47
55
|
|
|
48
56
|
# Supported Platforms
|
|
49
57
|
This API has been tested and confirmed working on:
|
|
@@ -69,19 +77,25 @@ pip install --upgrade Eporner-API
|
|
|
69
77
|
pip install --upgrade git+https://github.com/EchterAlsFake/EPorner_API.git
|
|
70
78
|
```
|
|
71
79
|
|
|
72
|
-
|
|
73
80
|
```python
|
|
81
|
+
import asyncio
|
|
74
82
|
from eporner_api import Client
|
|
75
83
|
# Initialize a Client object
|
|
76
|
-
client = Client()
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
|
|
80
97
|
|
|
81
|
-
|
|
82
|
-
videos = client.search_videos(query="Your query here", ..sortings..) # See Documentation!
|
|
83
|
-
for video in videos:
|
|
84
|
-
print(video.title)
|
|
98
|
+
asyncio.run(do_something())
|
|
85
99
|
|
|
86
100
|
# SEE DOCUMENTATION FOR MORE
|
|
87
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
|
```
|
|
@@ -3,6 +3,7 @@ import html
|
|
|
3
3
|
import json
|
|
4
4
|
import os.path
|
|
5
5
|
import logging
|
|
6
|
+
import asyncio
|
|
6
7
|
import argparse
|
|
7
8
|
import traceback
|
|
8
9
|
import threading
|
|
@@ -30,11 +31,14 @@ except (ModuleNotFoundError, ImportError):
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
from bs4 import BeautifulSoup
|
|
34
|
+
from curl_cffi import Response, AsyncSession
|
|
33
35
|
from urllib.parse import urljoin
|
|
36
|
+
from typing import AsyncGenerator
|
|
34
37
|
from functools import cached_property
|
|
35
38
|
from base_api.modules.config import RuntimeConfig
|
|
36
|
-
from typing import Generator, Union, Optional, List
|
|
37
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
|
|
38
42
|
|
|
39
43
|
"""
|
|
40
44
|
Copyright (c) 2024-2026 Johannes Habel
|
|
@@ -67,58 +71,58 @@ HTML Content. See the Documentation for more details.
|
|
|
67
71
|
"""
|
|
68
72
|
|
|
69
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)
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
if isinstance(q, int):
|
|
73
|
-
return q
|
|
74
|
-
s = str(q).lower().strip()
|
|
75
|
-
if s in {"best", "half", "worst"}:
|
|
76
|
-
return s
|
|
77
|
-
m = re.search(r'(\d{3,4})', s)
|
|
78
|
-
if m:
|
|
79
|
-
return int(m.group(1))
|
|
80
|
-
raise ValueError(f"Invalid quality: {q}")
|
|
82
|
+
return content
|
|
81
83
|
|
|
84
|
+
if isinstance(content, Response):
|
|
85
|
+
if content.status_code == 404:
|
|
86
|
+
raise NotFound(f"Server returned 404 for: {url}")
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
av = sorted({int(x) for x in available})
|
|
86
|
-
if isinstance(target, str):
|
|
87
|
-
if target == "best":
|
|
88
|
-
return av[-1]
|
|
89
|
-
if target == "worst":
|
|
90
|
-
return av[0]
|
|
91
|
-
if target == "half":
|
|
92
|
-
return av[len(av) // 2]
|
|
93
|
-
raise ValueError("Invalid label.")
|
|
94
|
-
# numeric: highest ≤ target, else closest
|
|
95
|
-
le = [h for h in av if h <= target]
|
|
96
|
-
if le:
|
|
97
|
-
return le[-1]
|
|
98
|
-
# fallback closest (ties -> higher)
|
|
99
|
-
return min(av, key=lambda h: (abs(h - target), -h))
|
|
88
|
+
except NetworkingError as e:
|
|
89
|
+
raise NetworkError(str(e)) from e
|
|
100
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
|
|
101
99
|
|
|
102
100
|
|
|
103
101
|
|
|
104
102
|
class Video:
|
|
105
|
-
def __init__(self, url: str, enable_html_scraping: bool = True,
|
|
103
|
+
def __init__(self, url: str, core: BaseCore, enable_html_scraping: bool = True, html_content=None):
|
|
106
104
|
self.core = core
|
|
107
105
|
self.url = url
|
|
108
106
|
self.enable_html = enable_html_scraping
|
|
109
|
-
self.html_content =
|
|
107
|
+
self.html_content = html_content
|
|
110
108
|
self.logger = setup_logger(name="EPorner API - [Video]", log_file=None, level=logging.CRITICAL)
|
|
111
|
-
self.json_data =
|
|
109
|
+
self.json_data = {}
|
|
110
|
+
self.html_json_data = {}
|
|
111
|
+
|
|
112
|
+
async def init(self):
|
|
113
|
+
self.json_data = await self.get_raw_json_data()
|
|
112
114
|
if self.enable_html:
|
|
113
|
-
self.
|
|
115
|
+
if not self.html_content:
|
|
116
|
+
await self.request_html_content()
|
|
114
117
|
is_removed = REGEX_VIDEO_DISABLED.findall(self.html_content)
|
|
115
118
|
for _ in is_removed:
|
|
116
119
|
if _ == "deletedfile":
|
|
117
120
|
raise VideoDisabled("Video has been removed because of a Copyright claim")
|
|
118
121
|
|
|
119
122
|
self.html_json_data = self.extract_json_from_html()
|
|
123
|
+
return self
|
|
120
124
|
|
|
121
|
-
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):
|
|
122
126
|
self.logger = setup_logger(name="EPorner API - [Video]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
123
127
|
|
|
124
128
|
@cached_property
|
|
@@ -144,15 +148,15 @@ class Video:
|
|
|
144
148
|
else:
|
|
145
149
|
return self.url # Assuming this is a video ID (hopefully)
|
|
146
150
|
|
|
147
|
-
def
|
|
151
|
+
async def get_raw_json_data(self):
|
|
148
152
|
"""
|
|
149
153
|
Uses the V2 API to retrieve information from a video
|
|
150
154
|
:return:
|
|
151
155
|
"""
|
|
152
|
-
|
|
153
|
-
data =
|
|
154
|
-
|
|
155
|
-
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
|
|
156
160
|
|
|
157
161
|
@cached_property
|
|
158
162
|
def tags(self) -> list:
|
|
@@ -212,14 +216,15 @@ class Video:
|
|
|
212
216
|
The following methods are using HTML scraping. This is against the ToS from EPorner.com!
|
|
213
217
|
"""
|
|
214
218
|
|
|
215
|
-
def request_html_content(self):
|
|
219
|
+
async def request_html_content(self):
|
|
216
220
|
if not self.enable_html:
|
|
217
221
|
raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
|
|
218
222
|
|
|
219
|
-
self.html_content =
|
|
220
|
-
|
|
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)
|
|
221
226
|
|
|
222
|
-
def extract_json_from_html(self):
|
|
227
|
+
def extract_json_from_html(self) -> dict:
|
|
223
228
|
if not self.enable_html:
|
|
224
229
|
raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
|
|
225
230
|
|
|
@@ -261,13 +266,13 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
261
266
|
return dict(items)
|
|
262
267
|
|
|
263
268
|
@cached_property
|
|
264
|
-
def bitrate(self) -> str:
|
|
269
|
+
def bitrate(self) -> str | None:
|
|
265
270
|
"""Return the bitrate of the video? (I don't know)"""
|
|
266
271
|
return self.html_json_data["bitrate"] if self.enable_html else None
|
|
267
272
|
|
|
268
273
|
|
|
269
274
|
@cached_property
|
|
270
|
-
def source_video_url(self) -> str:
|
|
275
|
+
def source_video_url(self) -> str | None:
|
|
271
276
|
"""
|
|
272
277
|
Returns the .mp4 video location URL
|
|
273
278
|
|
|
@@ -277,7 +282,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
277
282
|
|
|
278
283
|
|
|
279
284
|
@cached_property
|
|
280
|
-
def rating(self) -> str:
|
|
285
|
+
def rating(self) -> str | None:
|
|
281
286
|
"""
|
|
282
287
|
Returns the rating value. Highest (best) is 100, least is zero (worst)
|
|
283
288
|
:return: str
|
|
@@ -290,7 +295,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
290
295
|
raise NotAvailable("No rating available. This isn't an error!")
|
|
291
296
|
|
|
292
297
|
@cached_property
|
|
293
|
-
def likes(self) -> str:
|
|
298
|
+
def likes(self) -> str | None:
|
|
294
299
|
"""
|
|
295
300
|
Returns the video likes
|
|
296
301
|
:return: str
|
|
@@ -298,7 +303,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
298
303
|
return REGEX_VIDEO_LIKES.search(self.html_content).group(1) if self.enable_html else None
|
|
299
304
|
|
|
300
305
|
@cached_property
|
|
301
|
-
def dislikes(self) -> str:
|
|
306
|
+
def dislikes(self) -> str | None:
|
|
302
307
|
"""
|
|
303
308
|
Returns the video dislikes
|
|
304
309
|
:return:
|
|
@@ -306,7 +311,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
306
311
|
return REGEX_VIDEO_DISLIKES.search(self.html_content).group(1) if self.enable_html else None
|
|
307
312
|
|
|
308
313
|
@cached_property
|
|
309
|
-
def rating_count(self) -> str:
|
|
314
|
+
def rating_count(self) -> str | None:
|
|
310
315
|
"""
|
|
311
316
|
Returns how many people have rated the video
|
|
312
317
|
:return: str
|
|
@@ -314,7 +319,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
314
319
|
return self.html_json_data["aggregateRating_ratingCount"] if self.enable_html else None
|
|
315
320
|
|
|
316
321
|
@cached_property
|
|
317
|
-
def author(self) -> str:
|
|
322
|
+
def author(self) -> str | None:
|
|
318
323
|
"""
|
|
319
324
|
Returns the Uploader of the Video
|
|
320
325
|
:return: str
|
|
@@ -387,8 +392,8 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
387
392
|
|
|
388
393
|
# Choose the appropriate height using your helpers
|
|
389
394
|
available_heights = sorted(quality_to_url.keys()) # e.g., [240, 360, 480, 720, 1080]
|
|
390
|
-
qn =
|
|
391
|
-
chosen_height =
|
|
395
|
+
qn = normalize_quality_value(quality) # -> 'best' | 'half' | 'worst' | int
|
|
396
|
+
chosen_height = choose_quality_from_list(available_heights, qn)
|
|
392
397
|
|
|
393
398
|
# Map back to URL and return absolute URL
|
|
394
399
|
chosen_url = quality_to_url[chosen_height]
|
|
@@ -397,8 +402,8 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
397
402
|
self.logger.info(f"Using direct download link: {full_url} ({chosen_height}p)")
|
|
398
403
|
return full_url
|
|
399
404
|
|
|
400
|
-
def download(self, quality, path, callback=None, mode=Encoding.mp4_h264, no_title=False, use_workaround=False,
|
|
401
|
-
stop_event: threading.Event = None):
|
|
405
|
+
async def download(self, quality, path, callback=None, mode=Encoding.mp4_h264, no_title=False, use_workaround=False,
|
|
406
|
+
stop_event: threading.Event | None = None):
|
|
402
407
|
if not self.enable_html:
|
|
403
408
|
raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
|
|
404
409
|
|
|
@@ -408,13 +413,13 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
408
413
|
|
|
409
414
|
url = self.direct_download_link(quality, mode)
|
|
410
415
|
if use_workaround:
|
|
411
|
-
response_redirect_url = self.core.fetch(self.direct_download_link(quality, mode),
|
|
416
|
+
response_redirect_url = await self.core.fetch(self.direct_download_link(quality, mode),
|
|
412
417
|
allow_redirects=True, get_response=True) # Sometimes the site trolls me
|
|
413
418
|
|
|
414
419
|
url = response_redirect_url.url
|
|
415
420
|
|
|
416
421
|
try:
|
|
417
|
-
self.core.legacy_download(url=url, callback=callback, path=path, stop_event=stop_event)
|
|
422
|
+
await self.core.legacy_download(url=url, callback=callback, path=path, stop_event=stop_event)
|
|
418
423
|
return True
|
|
419
424
|
|
|
420
425
|
except Exception:
|
|
@@ -425,29 +430,41 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
|
|
|
425
430
|
|
|
426
431
|
|
|
427
432
|
class Pornstar(Helper):
|
|
428
|
-
def __init__(self, url: str, enable_html_scraping: bool = False,
|
|
429
|
-
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)
|
|
430
435
|
self.core = core
|
|
431
436
|
self.url = url
|
|
432
437
|
self.enable_html_scraping = enable_html_scraping
|
|
433
438
|
self.logger = setup_logger(name="EPorner API - [Pornstar]", log_file=None, level=logging.CRITICAL)
|
|
434
|
-
self.html_content =
|
|
439
|
+
self.html_content = html_content
|
|
440
|
+
|
|
441
|
+
async def init(self):
|
|
442
|
+
if not self.html_content:
|
|
443
|
+
self.html_content = await get_html_content(core=self.core, url=self.url)
|
|
444
|
+
assert isinstance(self.html_content, str)
|
|
445
|
+
|
|
446
|
+
return self
|
|
435
447
|
|
|
436
|
-
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):
|
|
437
449
|
self.logger = setup_logger(name="EPorner API - [Pornstar]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
438
450
|
|
|
439
|
-
def videos(self, pages: int = 0, videos_concurrency: int = None, pages_concurrency: int = None) ->
|
|
451
|
+
async def videos(self, pages: int = 0, videos_concurrency: int | None = None, pages_concurrency: int | None = None) -> AsyncGenerator[Video, None]:
|
|
440
452
|
if pages == 0:
|
|
441
453
|
video_amount = str(self.video_amount).replace(",", "")
|
|
442
454
|
pages = round(int(video_amount)) / 37 # One page contains 37 videos
|
|
443
455
|
|
|
444
|
-
videos_concurrency = videos_concurrency or self.core.
|
|
445
|
-
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
|
|
446
459
|
|
|
447
460
|
pages = round(pages) # Dont ask
|
|
448
461
|
page_urls = [urljoin(f"{self.url}/", str(page)) for page in range(1, pages + 1)]
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|
467
|
+
yield await video.init()
|
|
451
468
|
|
|
452
469
|
@cached_property
|
|
453
470
|
def name(self) -> str:
|
|
@@ -554,51 +571,59 @@ class Pornstar(Helper):
|
|
|
554
571
|
|
|
555
572
|
|
|
556
573
|
class Client(Helper):
|
|
557
|
-
def __init__(self, core:
|
|
558
|
-
super().__init__(core,
|
|
559
|
-
self.core = core
|
|
574
|
+
def __init__(self, core: BaseCore = BaseCore(RuntimeConfig())):
|
|
575
|
+
super().__init__(core, video_constructor=Video)
|
|
576
|
+
self.core = core
|
|
560
577
|
self.core.initialize_session()
|
|
578
|
+
assert isinstance(self.core.session, AsyncSession)
|
|
561
579
|
self.core.session.headers.update(headers)
|
|
562
580
|
self.logger = setup_logger(name="EPorner API - [Client]", log_file=None, level=logging.CRITICAL)
|
|
563
581
|
|
|
564
|
-
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):
|
|
565
583
|
self.logger = setup_logger(name="EPorner API - [Client]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
566
584
|
|
|
567
|
-
def get_video(self, url: str, enable_html_scraping: bool = True) -> Video:
|
|
585
|
+
async def get_video(self, url: str, enable_html_scraping: bool = True) -> Video:
|
|
568
586
|
"""Returns the Video object for a given URL"""
|
|
569
587
|
self.logger.info(f"Returning video object for: {url} HTML Scraping -> {enable_html_scraping}")
|
|
570
|
-
|
|
588
|
+
video = Video(url, enable_html_scraping=enable_html_scraping, core=self.core)
|
|
589
|
+
return await video.init()
|
|
571
590
|
|
|
572
|
-
def search_videos(self, query: str, sorting_gay:
|
|
573
|
-
sorting_low_quality:
|
|
574
|
-
page: int, per_page: int, enable_html_scraping: bool = True) ->
|
|
591
|
+
async def search_videos(self, query: str, sorting_gay: str | Gay, sorting_order: str | Order,
|
|
592
|
+
sorting_low_quality: str | LowQuality,
|
|
593
|
+
page: int, per_page: int, enable_html_scraping: bool = True) -> AsyncGenerator[Video, None]:
|
|
575
594
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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)
|
|
579
598
|
|
|
580
|
-
json_data = json.loads(response)
|
|
581
599
|
for video_ in json_data.get("videos", []): # Don't know why this works lmao
|
|
582
600
|
id_ = video_["url"]
|
|
583
|
-
|
|
601
|
+
video = Video(url=id_, core=self.core, enable_html_scraping=enable_html_scraping)
|
|
602
|
+
yield await video.init()
|
|
584
603
|
|
|
585
|
-
def get_videos_by_category(self, category:
|
|
586
|
-
videos_concurrency: int = None, pages_concurrency: int = 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]:
|
|
587
606
|
|
|
588
607
|
page_urls = [f"{ROOT_URL}cat/{category}/{page}" for page in range(1, 100)]
|
|
589
608
|
|
|
590
|
-
videos_concurrency = videos_concurrency or self.core.
|
|
591
|
-
pages_concurrency = pages_concurrency or self.core.
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
617
|
+
yield await video.init()
|
|
594
618
|
|
|
595
619
|
|
|
596
|
-
def get_pornstar(self, url: str, enable_html_scraping: bool = True) -> Pornstar:
|
|
620
|
+
async def get_pornstar(self, url: str, enable_html_scraping: bool = True) -> Pornstar:
|
|
597
621
|
self.logger.info(f"Returning Pornstar object for: {url} HTML Scraping -> {enable_html_scraping}")
|
|
598
|
-
|
|
622
|
+
pornstar = Pornstar(url=url, enable_html_scraping=enable_html_scraping, core=self.core)
|
|
623
|
+
return await pornstar.init()
|
|
599
624
|
|
|
600
625
|
|
|
601
|
-
def
|
|
626
|
+
async def run_main():
|
|
602
627
|
parser = argparse.ArgumentParser(description="API Command Line Interface")
|
|
603
628
|
parser.add_argument("--download", metavar="URL (str)", type=str, help="URL to download from")
|
|
604
629
|
parser.add_argument("--quality", metavar="best,half,worst", type=str, help="The video quality (best,half,worst)",
|
|
@@ -611,12 +636,12 @@ def main():
|
|
|
611
636
|
help="Whether to apply video title automatically to output path or not", required=True)
|
|
612
637
|
|
|
613
638
|
args = parser.parse_args()
|
|
614
|
-
no_title =
|
|
639
|
+
no_title = str_to_bool(args.no_title)
|
|
615
640
|
|
|
616
641
|
if args.download:
|
|
617
642
|
client = Client()
|
|
618
|
-
video = client.get_video(args.download, enable_html_scraping=True)
|
|
619
|
-
video.download(quality=args.quality, path=args.output, no_title=no_title)
|
|
643
|
+
video = await client.get_video(args.download, enable_html_scraping=True)
|
|
644
|
+
await video.download(quality=args.quality, path=args.output, no_title=no_title)
|
|
620
645
|
|
|
621
646
|
if args.file:
|
|
622
647
|
videos = []
|
|
@@ -626,11 +651,14 @@ def main():
|
|
|
626
651
|
content = file.read().splitlines()
|
|
627
652
|
|
|
628
653
|
for url in content:
|
|
629
|
-
videos.append(client.get_video(url, enable_html_scraping=True))
|
|
654
|
+
videos.append(await client.get_video(url, enable_html_scraping=True))
|
|
630
655
|
|
|
631
656
|
for video in videos:
|
|
632
|
-
video.download(quality=args.quality, path=args.output, no_title=no_title)
|
|
657
|
+
await video.download(quality=args.quality, path=args.output, no_title=no_title)
|
|
658
|
+
|
|
633
659
|
|
|
660
|
+
def main():
|
|
661
|
+
asyncio.run(run_main())
|
|
634
662
|
|
|
635
663
|
if __name__ == "__main__":
|
|
636
664
|
main()
|
|
@@ -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
|
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import pytest
|
|
3
2
|
from ..eporner_api import Client, Category
|
|
4
3
|
from base_api import BaseCore
|
|
5
|
-
core = BaseCore()
|
|
6
|
-
core.config.pages_concurrency = 1
|
|
7
|
-
core.config.videos_concurrency = 1
|
|
8
|
-
|
|
9
4
|
|
|
10
|
-
|
|
5
|
+
@pytest.mark.asyncio
|
|
6
|
+
async def test_category():
|
|
7
|
+
core = BaseCore()
|
|
8
|
+
core.configuration.pages_concurrency = 1
|
|
9
|
+
core.configuration.videos_concurrency = 1
|
|
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)
|
|
13
13
|
videos_3 = Client(core).get_videos_by_category(category=Category.BLONDE)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
idx = 0
|
|
16
|
+
async for video in videos_1:
|
|
16
17
|
if idx == 3:
|
|
17
18
|
break
|
|
18
|
-
|
|
19
19
|
assert isinstance(video.title, str) and len(video.title) > 0
|
|
20
|
+
idx += 1
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
idx = 0
|
|
23
|
+
async for video in videos_2:
|
|
22
24
|
if idx == 3:
|
|
23
25
|
break
|
|
24
|
-
|
|
25
26
|
assert isinstance(video.title, str) and len(video.title) > 0
|
|
27
|
+
idx += 1
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
idx = 0
|
|
30
|
+
async for video in videos_3:
|
|
28
31
|
if idx == 3:
|
|
29
32
|
break
|
|
30
|
-
|
|
31
33
|
assert isinstance(video.title, str) and len(video.title) > 0
|
|
34
|
+
idx += 1
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
+
import pytest
|
|
1
2
|
from ..eporner_api import Client
|
|
2
|
-
import time
|
|
3
|
-
url = "https://www.eporner.com/pornstar/riley-reid/"
|
|
4
3
|
from base_api import BaseCore
|
|
5
|
-
core = BaseCore()
|
|
6
|
-
core.config.pages_concurrency = 1
|
|
7
|
-
core.config.videos_concurrency = 1
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+
@pytest.mark.asyncio
|
|
6
|
+
async def test_pornstar():
|
|
7
|
+
url = "https://www.eporner.com/pornstar/riley-reid/"
|
|
8
|
+
core = BaseCore()
|
|
9
|
+
core.configuration.pages_concurrency = 1
|
|
10
|
+
core.configuration.videos_concurrency = 1
|
|
11
|
+
pornstar = await Client(core).get_pornstar(url, enable_html_scraping=True)
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
def test_videos():
|
|
13
13
|
videos = pornstar.videos(pages=1)
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
idx = 0
|
|
16
|
+
async for video in videos:
|
|
16
17
|
assert isinstance(video.title, str) and len(video.title) > 3
|
|
17
18
|
if idx == 5:
|
|
18
19
|
break
|
|
20
|
+
idx += 1
|
|
19
21
|
|
|
20
|
-
def test_information():
|
|
21
22
|
assert isinstance(pornstar.pornstar_rank, str) and len(pornstar.pornstar_rank) >= 1
|
|
22
23
|
assert isinstance(pornstar.aliases, list) and len(pornstar.aliases) > 1
|
|
23
24
|
assert isinstance(pornstar.biography, str) and len(pornstar.biography) > 10
|
|
24
|
-
assert isinstance(pornstar.age, str) and len(pornstar.age) >= 2
|
|
25
|
+
assert isinstance(pornstar.age, str) and len(pornstar.age) >= 2
|
|
25
26
|
assert isinstance(pornstar.cup, str) and len(pornstar.cup) >= 1
|
|
26
27
|
assert isinstance(pornstar.country, str) and len(pornstar.country) >= 2
|
|
27
28
|
assert isinstance(pornstar.weight, str) and len(pornstar.weight) >= 2
|
|
@@ -1,48 +1,53 @@
|
|
|
1
|
+
import pytest
|
|
1
2
|
from ..eporner_api import Client, Gay, Order, LowQuality
|
|
2
|
-
import time
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
query = "Mia Khalifa"
|
|
6
|
-
pages =
|
|
7
|
-
per_page =
|
|
6
|
+
pages = 1
|
|
7
|
+
per_page = 1
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def client():
|
|
11
|
+
return Client()
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
@pytest.mark.asyncio
|
|
14
|
+
async def test_search_1(client):
|
|
10
15
|
videos = client.search_videos(query, page=pages, per_page=per_page, sorting_gay=Gay.exclude_gay_content, sorting_order=Order.top_rated, sorting_low_quality=LowQuality.exclude_low_quality_content)
|
|
11
|
-
for video in videos:
|
|
16
|
+
async for video in videos:
|
|
12
17
|
assert len(video.title) > 0
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
def test_search_2():
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_search_2(client):
|
|
16
21
|
videos = client.search_videos(query, page=pages, per_page=per_page, sorting_gay=Gay.only_gay_content, sorting_order=Order.latest, sorting_low_quality=LowQuality.only_low_quality_content)
|
|
17
|
-
for video in videos:
|
|
22
|
+
async for video in videos:
|
|
18
23
|
assert len(video.title) > 0
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
def test_search_3():
|
|
25
|
+
@pytest.mark.asyncio
|
|
26
|
+
async def test_search_3(client):
|
|
22
27
|
videos = client.search_videos(query, page=pages, per_page=per_page, sorting_gay=Gay.include_gay_content, sorting_order=Order.longest, sorting_low_quality=LowQuality.include_low_quality_content)
|
|
23
|
-
for video in videos:
|
|
28
|
+
async for video in videos:
|
|
24
29
|
assert len(video.title) > 0
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
def test_search_4():
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_search_4(client):
|
|
28
33
|
videos = client.search_videos(query, page=pages, per_page=pages, sorting_gay=Gay.exclude_gay_content, sorting_order=Order.shortest, sorting_low_quality=LowQuality.include_low_quality_content)
|
|
29
|
-
for video in videos:
|
|
34
|
+
async for video in videos:
|
|
30
35
|
assert len(video.title) > 0
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
def test_search_5():
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
async def test_search_5(client):
|
|
34
39
|
videos = client.search_videos(query, page=pages, per_page=per_page, sorting_order=Gay.include_gay_content, sorting_gay=Order.top_weekly, sorting_low_quality=LowQuality.include_low_quality_content)
|
|
35
|
-
for video in videos:
|
|
40
|
+
async for video in videos:
|
|
36
41
|
assert len(video.title) > 0
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
def test_search_6():
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_search_6(client):
|
|
40
45
|
videos = client.search_videos(query, page=pages, per_page=per_page, sorting_order=Order.most_popular, sorting_low_quality=LowQuality.include_low_quality_content, sorting_gay=Gay.only_gay_content)
|
|
41
|
-
for video in videos:
|
|
46
|
+
async for video in videos:
|
|
42
47
|
assert len(video.title) > 0
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
def test_search_7():
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_search_7(client):
|
|
46
51
|
videos = client.search_videos(query, page=pages, per_page=per_page, sorting_gay=Gay.include_gay_content, sorting_order=Order.top_monthly, sorting_low_quality=LowQuality.include_low_quality_content)
|
|
47
|
-
for video in videos:
|
|
52
|
+
async for video in videos:
|
|
48
53
|
assert len(video.title) > 0
|
|
@@ -1,70 +1,26 @@
|
|
|
1
|
+
import pytest
|
|
1
2
|
from ..eporner_api import Client, Encoding, NotAvailable
|
|
2
|
-
import time
|
|
3
|
-
url = "https://www.eporner.com/video-bTwP6vsFj5U/human-anal-sex-toy/"
|
|
4
|
-
video = Client().get_video(url, enable_html_scraping=True)
|
|
5
|
-
time.sleep(5) # Lmao
|
|
6
3
|
|
|
7
|
-
|
|
4
|
+
@pytest.mark.asyncio
|
|
5
|
+
async def test_video():
|
|
6
|
+
url = "https://www.eporner.com/video-pDRNfJoN7dN/granny-with-young-guy/"
|
|
7
|
+
video = await Client().get_video(url, enable_html_scraping=True)
|
|
8
8
|
assert isinstance(video.title, str) and len(video.title) > 0
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_video_id():
|
|
12
9
|
assert isinstance(video.video_id, str) and len(video.video_id) > 0
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_tags():
|
|
16
10
|
assert isinstance(video.tags, list) and len(video.tags) > 0
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_views():
|
|
20
11
|
assert isinstance(video.views, int) and video.views > 0
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_rate():
|
|
24
12
|
assert isinstance(video.rate, str) and len(video.rate) > 0
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_publish_date():
|
|
28
13
|
assert isinstance(video.publish_date, str) and len(video.publish_date) > 0
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_length_seconds():
|
|
32
|
-
assert isinstance(video.length, int) > 0
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_length_minutes():
|
|
14
|
+
assert isinstance(video.length, int) and video.length > 0
|
|
36
15
|
assert isinstance(video.length_minutes, str) and len(video.length_minutes) > 0
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_embed_url():
|
|
40
16
|
assert isinstance(video.embed_url, str) and len(video.embed_url) > 0
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_thumbnails():
|
|
44
17
|
assert isinstance(video.thumbnail, str) and len(video.thumbnail) > 0
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_bitrate():
|
|
48
18
|
assert isinstance(video.bitrate, str) and len(video.bitrate) > 0
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_source_video_url():
|
|
52
19
|
assert isinstance(video.source_video_url, str) and len(video.source_video_url) > 0
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_rating():
|
|
56
20
|
assert isinstance(video.rating, str) and len(video.rating) > 0
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_rating_count():
|
|
60
21
|
assert isinstance(video.rating_count, str) and len(video.rating_count) > 0
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_author():
|
|
64
22
|
assert isinstance(video.author, str) and len(video.author) > 0
|
|
65
23
|
|
|
66
|
-
|
|
67
|
-
def test_direct_download_url():
|
|
68
24
|
assert isinstance(video.direct_download_link(quality=2160, mode=Encoding.mp4_h264), str)
|
|
69
25
|
assert isinstance(video.direct_download_link(quality="half", mode=Encoding.mp4_h264), str)
|
|
70
26
|
assert isinstance(video.direct_download_link(quality="worst", mode=Encoding.mp4_h264), str)
|
|
@@ -72,6 +28,5 @@ def test_direct_download_url():
|
|
|
72
28
|
assert isinstance(video.direct_download_link(quality="best", mode=Encoding.av1), str)
|
|
73
29
|
assert isinstance(video.direct_download_link(quality="half", mode=Encoding.av1), str)
|
|
74
30
|
assert isinstance(video.direct_download_link(quality="worst", mode=Encoding.av1), str)
|
|
75
|
-
|
|
76
31
|
except NotAvailable:
|
|
77
32
|
pass
|
|
@@ -4,10 +4,10 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "Eporner_API"
|
|
7
|
-
version = "1
|
|
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 = [
|
|
@@ -15,14 +15,14 @@ authors = [
|
|
|
15
15
|
]
|
|
16
16
|
dependencies = [
|
|
17
17
|
"bs4",
|
|
18
|
-
"
|
|
18
|
+
"eaf-base-api",
|
|
19
19
|
]
|
|
20
20
|
classifiers = [
|
|
21
21
|
"Programming Language :: Python",
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
|
-
full = ["lxml"
|
|
25
|
+
full = ["lxml"]
|
|
26
26
|
|
|
27
27
|
[project.urls]
|
|
28
28
|
Homepage = "https://github.com/EchterAlsFake/EPorner_API"
|
|
@@ -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
|