Eporner_API 2.0__tar.gz → 2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Eporner_API
3
- Version: 2.0
3
+ Version: 2.1
4
4
  Summary: A Python API for the Porn Site Eporner.com
5
5
  Author: Johannes Habel
6
6
  Author-email: Johannes Habel <EchterAlsFake@proton.me>
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python
10
10
  Requires-Dist: bs4
11
11
  Requires-Dist: eaf-base-api
12
12
  Requires-Dist: lxml ; extra == 'full'
13
- Requires-Python: >=3.9
13
+ Requires-Python: >=3.10
14
14
  Project-URL: Homepage, https://github.com/EchterAlsFake/EPorner_API
15
15
  Provides-Extra: full
16
16
  Description-Content-Type: text/markdown
@@ -32,6 +32,7 @@ Description-Content-Type: text/markdown
32
32
  > and applicable laws. Do not use it to bypass access controls or scrape at disruptive rates.
33
33
 
34
34
  # Features
35
+ - Asynchronous
35
36
  - Fetch videos + metadata
36
37
  - Download videos
37
38
  - Fetch Pornstars
@@ -40,8 +41,17 @@ Description-Content-Type: text/markdown
40
41
  - Built-in caching
41
42
  - Easy interface
42
43
  - Great type hinting
43
- - 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
  ```
@@ -31,11 +31,14 @@ except (ModuleNotFoundError, ImportError):
31
31
 
32
32
 
33
33
  from bs4 import BeautifulSoup
34
+ from curl_cffi import Response, AsyncSession
34
35
  from urllib.parse import urljoin
36
+ from typing import AsyncGenerator
35
37
  from functools import cached_property
36
38
  from base_api.modules.config import RuntimeConfig
37
- from typing import AsyncGenerator, Generator, Union, Optional, List
38
39
  from base_api.base import BaseCore, setup_logger, Helper
40
+ from base_api.modules.errors import InvalidProxy, BotProtectionDetected, NetworkingError, UnknownError, VideoFetchError, PageFetchError
41
+ from base_api.modules.static_functions import normalize_quality_value, choose_quality_from_list, str_to_bool
39
42
 
40
43
  """
41
44
  Copyright (c) 2024-2026 Johannes Habel
@@ -68,42 +71,36 @@ HTML Content. See the Documentation for more details.
68
71
  """
69
72
 
70
73
 
74
+ async def get_html_content(core: BaseCore, url: str, get_json: bool = False) -> str | None | dict:
75
+ # What should I do here?
76
+ try:
77
+ content = await core.fetch(url)
78
+ if isinstance(content, str):
79
+ if get_json:
80
+ return json.loads(content)
71
81
 
72
- 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}")
82
+ return content
82
83
 
84
+ if isinstance(content, Response):
85
+ if content.status_code == 404:
86
+ raise NotFound(f"Server returned 404 for: {url}")
83
87
 
84
- 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))
88
+ except NetworkingError as e:
89
+ raise NetworkError(str(e)) from e
101
90
 
91
+ except InvalidProxy as e:
92
+ raise ProxyError(str(e)) from e
93
+
94
+ except BotProtectionDetected as e:
95
+ raise BotDetection(str(e)) from e
96
+
97
+ except UnknownError as e:
98
+ raise UnknownNetworkError(str(e)) from e
102
99
 
103
100
 
104
101
 
105
102
  class Video:
106
- def __init__(self, url: str, enable_html_scraping: bool = True, core: Optional[BaseCore] = None, html_content=None):
103
+ def __init__(self, url: str, core: BaseCore, enable_html_scraping: bool = True, html_content=None):
107
104
  self.core = core
108
105
  self.url = url
109
106
  self.enable_html = enable_html_scraping
@@ -125,7 +122,7 @@ class Video:
125
122
  self.html_json_data = self.extract_json_from_html()
126
123
  return self
127
124
 
128
- def enable_logging(self, log_file: str, level, log_ip: str = None, log_port: int = None):
125
+ def enable_logging(self, log_file: str, level, log_ip: str | None = None, log_port: int | None = None):
129
126
  self.logger = setup_logger(name="EPorner API - [Video]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
130
127
 
131
128
  @cached_property
@@ -156,10 +153,10 @@ class Video:
156
153
  Uses the V2 API to retrieve information from a video
157
154
  :return:
158
155
  """
159
-
160
- data = await 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
156
+ url = f"{ROOT_URL}{API_VIDEO_ID}?id={self.video_id}&thumbsize=medium&format=json"
157
+ data = await get_html_content(url=url, get_json=True, core=self.core)
158
+ assert isinstance(data, dict)
159
+ return data
163
160
 
164
161
  @cached_property
165
162
  def tags(self) -> list:
@@ -223,10 +220,11 @@ class Video:
223
220
  if not self.enable_html:
224
221
  raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
225
222
 
226
- self.html_content = html.unescape(await self.core.fetch(self.url))
227
-
223
+ self.html_content = await get_html_content(core=self.core, url=self.url)
224
+ assert isinstance(self.html_content, str)
225
+ self.html_content = html.unescape(self.html_content)
228
226
 
229
- def extract_json_from_html(self):
227
+ def extract_json_from_html(self) -> dict:
230
228
  if not self.enable_html:
231
229
  raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
232
230
 
@@ -268,13 +266,13 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
268
266
  return dict(items)
269
267
 
270
268
  @cached_property
271
- def bitrate(self) -> str:
269
+ def bitrate(self) -> str | None:
272
270
  """Return the bitrate of the video? (I don't know)"""
273
271
  return self.html_json_data["bitrate"] if self.enable_html else None
274
272
 
275
273
 
276
274
  @cached_property
277
- def source_video_url(self) -> str:
275
+ def source_video_url(self) -> str | None:
278
276
  """
279
277
  Returns the .mp4 video location URL
280
278
 
@@ -284,7 +282,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
284
282
 
285
283
 
286
284
  @cached_property
287
- def rating(self) -> str:
285
+ def rating(self) -> str | None:
288
286
  """
289
287
  Returns the rating value. Highest (best) is 100, least is zero (worst)
290
288
  :return: str
@@ -297,7 +295,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
297
295
  raise NotAvailable("No rating available. This isn't an error!")
298
296
 
299
297
  @cached_property
300
- def likes(self) -> str:
298
+ def likes(self) -> str | None:
301
299
  """
302
300
  Returns the video likes
303
301
  :return: str
@@ -305,7 +303,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
305
303
  return REGEX_VIDEO_LIKES.search(self.html_content).group(1) if self.enable_html else None
306
304
 
307
305
  @cached_property
308
- def dislikes(self) -> str:
306
+ def dislikes(self) -> str | None:
309
307
  """
310
308
  Returns the video dislikes
311
309
  :return:
@@ -313,7 +311,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
313
311
  return REGEX_VIDEO_DISLIKES.search(self.html_content).group(1) if self.enable_html else None
314
312
 
315
313
  @cached_property
316
- def rating_count(self) -> str:
314
+ def rating_count(self) -> str | None:
317
315
  """
318
316
  Returns how many people have rated the video
319
317
  :return: str
@@ -321,7 +319,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
321
319
  return self.html_json_data["aggregateRating_ratingCount"] if self.enable_html else None
322
320
 
323
321
  @cached_property
324
- def author(self) -> str:
322
+ def author(self) -> str | None:
325
323
  """
326
324
  Returns the Uploader of the Video
327
325
  :return: str
@@ -394,8 +392,8 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
394
392
 
395
393
  # Choose the appropriate height using your helpers
396
394
  available_heights = sorted(quality_to_url.keys()) # e.g., [240, 360, 480, 720, 1080]
397
- qn = _normalize_quality_value(quality) # -> 'best' | 'half' | 'worst' | int
398
- 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)
399
397
 
400
398
  # Map back to URL and return absolute URL
401
399
  chosen_url = quality_to_url[chosen_height]
@@ -405,7 +403,7 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
405
403
  return full_url
406
404
 
407
405
  async def download(self, quality, path, callback=None, mode=Encoding.mp4_h264, no_title=False, use_workaround=False,
408
- stop_event: threading.Event = None):
406
+ stop_event: threading.Event | None = None):
409
407
  if not self.enable_html:
410
408
  raise HTML_IS_DISABLED("HTML content is disabled! See Documentation for more details")
411
409
 
@@ -432,8 +430,8 @@ JSONDecodeError: I need your help to fix this error. Please report the URL you'v
432
430
 
433
431
 
434
432
  class Pornstar(Helper):
435
- def __init__(self, url: str, enable_html_scraping: bool = False, core: Optional[BaseCore] = None, html_content=None):
436
- 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)
437
435
  self.core = core
438
436
  self.url = url
439
437
  self.enable_html_scraping = enable_html_scraping
@@ -442,24 +440,30 @@ class Pornstar(Helper):
442
440
 
443
441
  async def init(self):
444
442
  if not self.html_content:
445
- self.html_content = await self.core.fetch(self.url)
443
+ self.html_content = await get_html_content(core=self.core, url=self.url)
444
+ assert isinstance(self.html_content, str)
445
+
446
446
  return self
447
447
 
448
- def enable_logging(self, log_file: str, level, log_ip: str = None, log_port: int = None):
448
+ def enable_logging(self, log_file: str, level, log_ip: str | None = None, log_port: int | None = None):
449
449
  self.logger = setup_logger(name="EPorner API - [Pornstar]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
450
450
 
451
- async def videos(self, pages: int = 0, videos_concurrency: int = None, pages_concurrency: int = None) -> AsyncGenerator[Video, None]:
451
+ async def videos(self, pages: int = 0, videos_concurrency: int | None = None, pages_concurrency: int | None = None) -> AsyncGenerator[Video, None]:
452
452
  if pages == 0:
453
453
  video_amount = str(self.video_amount).replace(",", "")
454
454
  pages = round(int(video_amount)) / 37 # One page contains 37 videos
455
455
 
456
- videos_concurrency = videos_concurrency or self.core.config.videos_concurrency
457
- 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
458
459
 
459
460
  pages = round(pages) # Dont ask
460
461
  page_urls = [urljoin(f"{self.url}/", str(page)) for page in range(1, pages + 1)]
461
- async for video in self.iterator(page_urls=page_urls, extractor=extractor, pages_concurrency=pages_concurrency,
462
- 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
463
467
  yield await video.init()
464
468
 
465
469
  @cached_property
@@ -567,14 +571,15 @@ class Pornstar(Helper):
567
571
 
568
572
 
569
573
  class Client(Helper):
570
- def __init__(self, core: Optional[BaseCore] = None):
571
- super().__init__(core, video=Video)
572
- 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
573
577
  self.core.initialize_session()
578
+ assert isinstance(self.core.session, AsyncSession)
574
579
  self.core.session.headers.update(headers)
575
580
  self.logger = setup_logger(name="EPorner API - [Client]", log_file=None, level=logging.CRITICAL)
576
581
 
577
- def enable_logging(self, log_file: str, level, log_ip: str = None, log_port: int = None):
582
+ def enable_logging(self, log_file: str, level, log_ip: str | None = None, log_port: int | None = None):
578
583
  self.logger = setup_logger(name="EPorner API - [Client]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
579
584
 
580
585
  async def get_video(self, url: str, enable_html_scraping: bool = True) -> Video:
@@ -583,35 +588,38 @@ class Client(Helper):
583
588
  video = Video(url, enable_html_scraping=enable_html_scraping, core=self.core)
584
589
  return await video.init()
585
590
 
586
- async def search_videos(self, query: str, sorting_gay: Union[str, Gay], sorting_order: Union[str, Order],
587
- sorting_low_quality: Union[str, LowQuality],
591
+ async def search_videos(self, query: str, sorting_gay: str | Gay, sorting_order: str | Order,
592
+ sorting_low_quality: str | LowQuality,
588
593
  page: int, per_page: int, enable_html_scraping: bool = True) -> AsyncGenerator[Video, None]:
589
594
 
590
- 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")
595
+ url = f"{ROOT_URL}{API_SEARCH}?query={query}&per_page={per_page}&%page={page}&thumbsize=medium&order={sorting_order}&gay={sorting_gay}&lq={sorting_low_quality}&format=json"
596
+ json_data = await get_html_content(core=self.core, url=url, get_json=True)
597
+ assert isinstance(json_data, dict)
593
598
 
594
- json_data = json.loads(response)
595
599
  for video_ in json_data.get("videos", []): # Don't know why this works lmao
596
600
  id_ = video_["url"]
597
- video = Video(id_, enable_html_scraping, core=self.core)
601
+ video = Video(url=id_, core=self.core, enable_html_scraping=enable_html_scraping)
598
602
  yield await video.init()
599
603
 
600
- async def get_videos_by_category(self, category: Union[str, Category], enable_html_scraping: bool = False,
601
- videos_concurrency: int = None, pages_concurrency: int = None) -> AsyncGenerator[Video, None]:
604
+ async def get_videos_by_category(self, category: str | Category, enable_html_scraping: bool = False,
605
+ videos_concurrency: int | None = None, pages_concurrency: int | None = None) -> AsyncGenerator[Video, None]:
602
606
 
603
607
  page_urls = [f"{ROOT_URL}cat/{category}/{page}" for page in range(1, 100)]
604
608
 
605
- videos_concurrency = videos_concurrency or self.core.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):
609
+ videos_concurrency = videos_concurrency or self.core.configuration.videos_concurrency
610
+ pages_concurrency = pages_concurrency or self.core.configuration.pages_concurrency
611
+ assert videos_concurrency and pages_concurrency
612
+ async for video in self.iterator(target_page_urls=page_urls, max_video_concurrency=videos_concurrency,
613
+ max_page_concurrency=pages_concurrency, video_link_extractor=extractor):
614
+ if isinstance(video, (VideoFetchError, PageFetchError)):
615
+ self.logger.error(f"Error during iteration: {video}")
616
+ continue
609
617
  yield await video.init()
610
618
 
611
619
 
612
620
  async def get_pornstar(self, url: str, enable_html_scraping: bool = True) -> Pornstar:
613
621
  self.logger.info(f"Returning Pornstar object for: {url} HTML Scraping -> {enable_html_scraping}")
614
- pornstar = Pornstar(url, enable_html_scraping, core=self.core)
622
+ pornstar = Pornstar(url=url, enable_html_scraping=enable_html_scraping, core=self.core)
615
623
  return await pornstar.init()
616
624
 
617
625
 
@@ -628,7 +636,7 @@ async def run_main():
628
636
  help="Whether to apply video title automatically to output path or not", required=True)
629
637
 
630
638
  args = parser.parse_args()
631
- no_title = BaseCore().str_to_bool(args.no_title)
639
+ no_title = str_to_bool(args.no_title)
632
640
 
633
641
  if args.download:
634
642
  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
@@ -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.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 = [
@@ -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