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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Eporner_API
3
- Version: 1.9.7
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-Dist: httpx[http2] ; extra == 'full'
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
- - Proxy support
46
- - Very customizable
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
- # Fetch a video
79
- video_object = client.get_video("<insert_url_here>") # Can also be a Video ID
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
- # Search for videos
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
- - Proxy support
27
- - Very customizable
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
- # Fetch a video
60
- video_object = client.get_video("<insert_url_here>") # Can also be a Video ID
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
- # Search for videos
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
- def _normalize_quality_value(q) -> Union[str, int]:
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
- def _choose_quality_from_list(available: List[str | int], target: Union[str, int]):
84
- # available like ["240", "360", "480", "720", "1080"]
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, core: Optional[BaseCore] = None):
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 = None
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 = self.raw_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.request_html_content()
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 raw_json_data(self):
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 = self.core.fetch(f"{ROOT_URL}{API_VIDEO_ID}?id={self.video_id}&thumbsize=medium&format=json")
154
- parsed_data = json.loads(data)
155
- return parsed_data
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 = html.unescape(self.core.fetch(self.url))
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 = _normalize_quality_value(quality) # -> 'best' | 'half' | 'worst' | int
391
- chosen_height = _choose_quality_from_list(available_heights, qn)
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, core: Optional[BaseCore] = None):
429
- super().__init__(core=core, video=Video)
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 = self.core.fetch(self.url)
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) -> Generator[Video, None, 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.config.videos_concurrency
445
- pages_concurrency = pages_concurrency or self.core.config.pages_concurrency
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
- yield from self.iterator(page_urls=page_urls, extractor=extractor, pages_concurrency=pages_concurrency,
450
- videos_concurrency=videos_concurrency)
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: Optional[BaseCore] = None):
558
- super().__init__(core, video=Video)
559
- self.core = core or BaseCore(config=RuntimeConfig())
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
- return Video(url, enable_html_scraping=enable_html_scraping, core=self.core)
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: Union[str, Gay], sorting_order: Union[str, Order],
573
- sorting_low_quality: Union[str, LowQuality],
574
- page: int, per_page: int, enable_html_scraping: bool = True) -> Generator[Video, None, None]:
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
- response = self.core.fetch(f"{ROOT_URL}{API_SEARCH}?query={query}&per_page={per_page}&%page={page}"
577
- f"&thumbsize=medium&order={sorting_order}&gay={sorting_gay}&lq="
578
- f"{sorting_low_quality}&format=json")
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
- yield Video(id_, enable_html_scraping, core=self.core)
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: Union[str, Category], enable_html_scraping: bool = False,
586
- videos_concurrency: int = None, pages_concurrency: int = None) -> Generator[Video, None, 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.config.videos_concurrency
591
- pages_concurrency = pages_concurrency or self.core.config.pages_concurrency
592
- yield from self.iterator(page_urls=page_urls, videos_concurrency=videos_concurrency,
593
- pages_concurrency=pages_concurrency, extractor=extractor)
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
- return Pornstar(url, enable_html_scraping, core=self.core)
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 main():
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 = BaseCore().str_to_bool(args.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()
@@ -1,8 +1,8 @@
1
1
  import re
2
2
 
3
3
  # ROOT URLs
4
- ROOT_URL = "https://eporner.com/"
5
- PORNSTAR = "https://eporner.com/pornstar/"
4
+ ROOT_URL = "https://www.eporner.com/"
5
+ PORNSTAR = "https://www.eporner.com/pornstar/"
6
6
 
7
7
  # API Calls
8
8
  API_V2 = "api/v2/"
@@ -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 time
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
- def test_category():
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
- for idx, video in enumerate(videos_1):
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
- for idx, video in enumerate(videos_2):
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
- for idx, video in enumerate(videos_3):
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
- pornstar = Client(core).get_pornstar(url, enable_html_scraping=True)
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
- for idx, video in enumerate(videos):
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 # would be weird if this is 1-9 lmao (just kidding)
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
- client = Client()
4
+
5
5
  query = "Mia Khalifa"
6
- pages = 2
7
- per_page = 10
6
+ pages = 1
7
+ per_page = 1
8
+
9
+ @pytest.fixture
10
+ def client():
11
+ return Client()
8
12
 
9
- def test_search_1():
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
- def test_title():
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.9.7"
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.9" # 3.9 due to httpx requirements
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
- "eaf_base_api",
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", "httpx[http2]", "httpx[socks]"]
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