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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Eporner_API
3
- Version: 2.0
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.9
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
- - Proxy support
44
- - 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!
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
- # Fetch a video
77
- 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
+
78
97
 
79
- # Search for videos
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
- - 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
  ```
@@ -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
- def _normalize_quality_value(q) -> Union[str, int]:
73
- if isinstance(q, int):
74
- return q
75
- s = str(q).lower().strip()
76
- if s in {"best", "half", "worst"}:
77
- return s
78
- m = re.search(r'(\d{3,4})', s)
79
- if m:
80
- return int(m.group(1))
81
- raise ValueError(f"Invalid quality: {q}")
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
- def _choose_quality_from_list(available: List[str | int], target: Union[str, int]):
85
- # available like ["240", "360", "480", "720", "1080"]
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, enable_html_scraping: bool = True, core: Optional[BaseCore] = None, html_content=None):
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 self.core.fetch(f"{ROOT_URL}{API_VIDEO_ID}?id={self.video_id}&thumbsize=medium&format=json")
161
- parsed_data = json.loads(data)
162
- return parsed_data
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 = html.unescape(await self.core.fetch(self.url))
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 = _normalize_quality_value(quality) # -> 'best' | 'half' | 'worst' | int
398
- chosen_height = _choose_quality_from_list(available_heights, qn)
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, enable_html_scraping: bool = False, core: Optional[BaseCore] = None, html_content=None):
436
- super().__init__(core=core, video=Video)
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.fetch(self.url)
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 = None) -> AsyncGenerator[Video, None]:
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.config.videos_concurrency
457
- pages_concurrency = pages_concurrency or self.core.config.pages_concurrency
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(page_urls=page_urls, extractor=extractor, pages_concurrency=pages_concurrency,
462
- videos_concurrency=videos_concurrency):
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: Optional[BaseCore] = None):
571
- super().__init__(core, video=Video)
572
- self.core = core or BaseCore(config=RuntimeConfig())
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: Union[str, Gay], sorting_order: Union[str, Order],
587
- sorting_low_quality: Union[str, LowQuality],
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
- response = await self.core.fetch(f"{ROOT_URL}{API_SEARCH}?query={query}&per_page={per_page}&%page={page}"
591
- f"&thumbsize=medium&order={sorting_order}&gay={sorting_gay}&lq="
592
- f"{sorting_low_quality}&format=json")
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_, enable_html_scraping, core=self.core)
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: Union[str, Category], enable_html_scraping: bool = False,
601
- videos_concurrency: int = None, pages_concurrency: int = None) -> AsyncGenerator[Video, None]:
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.config.videos_concurrency
606
- pages_concurrency = pages_concurrency or self.core.config.pages_concurrency
607
- async for video in self.iterator(page_urls=page_urls, videos_concurrency=videos_concurrency,
608
- pages_concurrency=pages_concurrency, extractor=extractor):
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 = BaseCore().str_to_bool(args.no_title)
653
+ no_title = str_to_bool(args.no_title)
632
654
 
633
655
  if args.download:
634
656
  client = Client()
@@ -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
@@ -0,0 +1,4 @@
1
+ from typing import Callable, Awaitable
2
+
3
+ type callback_hint = Callable[[int, int], None] | None
4
+ type on_error_hint = Callable[[str, Exception, int], Awaitable[bool]] | None
@@ -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.config.pages_concurrency = 1
9
- core.config.videos_concurrency = 1
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.config.pages_concurrency = 1
10
- core.config.videos_concurrency = 1
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-bTwP6vsFj5U/human-anal-sex-toy/"
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.0"
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.9" # 3.9 due to httpx requirements
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