eaf-base-api 2.2.6__tar.gz → 2.2.7__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
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: eaf_base_api
3
- Version: 2.2.6
3
+ Version: 2.2.7
4
4
  Summary: A base API for EchterAlsFake's Porn APIs
5
5
  Home-page: https://github.com/EchterAlsFake/eaf_base_api
6
6
  Author: Johannes Habel
@@ -20,6 +20,7 @@ Dynamic: description
20
20
  Dynamic: description-content-type
21
21
  Dynamic: home-page
22
22
  Dynamic: license
23
+ Dynamic: license-file
23
24
  Dynamic: requires-dist
24
25
  Dynamic: summary
25
26
 
@@ -1,12 +1,15 @@
1
+ import re
1
2
  import os
2
3
  import sys
3
4
  import time
4
5
  import m3u8
6
+ import json
5
7
  import httpx
6
8
  import random
7
9
  import logging
8
10
  import traceback
9
11
  import threading
12
+ import http.client
10
13
 
11
14
  from typing import Union
12
15
  from functools import lru_cache
@@ -15,11 +18,11 @@ from ffmpeg_progress_yield import FfmpegProgress
15
18
  from concurrent.futures import ThreadPoolExecutor, as_completed
16
19
 
17
20
  try:
18
- from modules import consts
21
+ from modules import config
19
22
  from modules.progress_bars import Callback
20
23
 
21
24
  except (ModuleNotFoundError, ImportError):
22
- from .modules import consts
25
+ from .modules import config
23
26
  from .modules.progress_bars import Callback
24
27
 
25
28
  loggers = {}
@@ -56,19 +59,52 @@ class ColoredFormatter(logging.Formatter):
56
59
  return f"{color}{log_message}{RESET_COLOR}"
57
60
 
58
61
 
59
- def setup_logger(name, log_file=None, level=logging.CRITICAL):
62
+ def send_log_message(ip, port, message):
63
+ """Sends a log message to a remote server via HTTP."""
64
+ try:
65
+ conn = http.client.HTTPConnection(ip, port, timeout=2)
66
+ headers = {'Content-Type': 'application/json'}
67
+ data = json.dumps({"message": message})
68
+ conn.request("POST", "/log", body=data, headers=headers)
69
+ conn.getresponse() # Read response to ensure completion
70
+ conn.close()
71
+ except Exception as e:
72
+ print(f"Failed to send log to {ip}:{port} - {e}", file=sys.stderr)
73
+
74
+
75
+ class HTTPLogHandler(logging.Handler):
76
+ """Custom log handler that sends logs to a remote HTTP server."""
77
+
78
+ def __init__(self, ip, port):
79
+ super().__init__()
80
+ self.ip = ip
81
+ self.port = port
82
+
83
+ def emit(self, record):
84
+ log_entry = self.format(record)
85
+ send_log_message(self.ip, self.port, log_entry)
86
+
87
+
88
+ def setup_logger(name, log_file=None, level=logging.CRITICAL, http_ip=None, http_port=None):
60
89
  """Creates or updates a logger for a specific module."""
61
90
  if name in loggers:
62
91
  logger = loggers[name]
63
92
  logger.setLevel(level)
64
93
 
65
94
  file_handler_exists = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
95
+ http_handler_exists = any(isinstance(h, HTTPLogHandler) for h in logger.handlers)
96
+
66
97
  if log_file and not file_handler_exists:
67
98
  log_file = get_log_file_path(log_file)
68
99
  fh = logging.FileHandler(log_file, mode='a')
69
100
  fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
70
101
  logger.addHandler(fh)
71
102
 
103
+ if http_ip and http_port and not http_handler_exists:
104
+ http_handler = HTTPLogHandler(http_ip, http_port)
105
+ http_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
106
+ logger.addHandler(http_handler)
107
+
72
108
  return logger
73
109
 
74
110
  logger = logging.getLogger(name)
@@ -76,10 +112,9 @@ def setup_logger(name, log_file=None, level=logging.CRITICAL):
76
112
 
77
113
  if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
78
114
  ch = logging.StreamHandler()
79
- ch.setFormatter(ColoredFormatter("%(asctime)s - %(levelname)s - %(message)s"))
115
+ ch.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
80
116
  logger.addHandler(ch)
81
117
 
82
- # Ensure the log file is overwritten only on the first program start
83
118
  if log_file:
84
119
  log_file = get_log_file_path(log_file)
85
120
  if not hasattr(sys, '_first_run'):
@@ -87,11 +122,15 @@ def setup_logger(name, log_file=None, level=logging.CRITICAL):
87
122
  file_mode = 'w'
88
123
  else:
89
124
  file_mode = 'a'
90
-
91
125
  fh = logging.FileHandler(log_file, mode=file_mode)
92
126
  fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
93
127
  logger.addHandler(fh)
94
128
 
129
+ if http_ip and http_port:
130
+ http_handler = HTTPLogHandler(http_ip, http_port)
131
+ http_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
132
+ logger.addHandler(http_handler)
133
+
95
134
  loggers[name] = logger
96
135
  return logger
97
136
 
@@ -100,7 +139,7 @@ class Cache:
100
139
  Caches content from network requests
101
140
  """
102
141
 
103
- def __init__(self):
142
+ def __init__(self, config):
104
143
  self.cache_dictionary = {}
105
144
  self.lock = threading.Lock()
106
145
  self.logger = setup_logger("BASE API - [Cache]", level=logging.CRITICAL)
@@ -121,7 +160,7 @@ class Cache:
121
160
 
122
161
  def save_cache(self, url, content):
123
162
  with self.lock:
124
- if len(self.cache_dictionary.keys()) >= consts.MAX_CACHE_ITEMS:
163
+ if len(self.cache_dictionary.keys()) >= config.config.MAX_CACHE_ITEMS:
125
164
  first_key = next(iter(self.cache_dictionary))
126
165
  # Delete the first item
127
166
  del self.cache_dictionary[first_key]
@@ -133,11 +172,13 @@ class BaseCore:
133
172
  """
134
173
  The base class which has all necessary functions for other API packages
135
174
  """
136
- def __init__(self):
175
+ def __init__(self, config=config.config):
137
176
  self.last_request_time = time.time()
138
177
  self.total_requests = 0 # Tracks how many requests have been made
139
178
  self.session = None
140
- self.cache = Cache()
179
+ self.kill_switch = False
180
+ self.config = config
181
+ self.cache = Cache(self.config)
141
182
  self.initialize_session()
142
183
  self.logger = setup_logger("BASE API - [BaseCore]", log_file=False, level=logging.ERROR)
143
184
 
@@ -150,23 +191,58 @@ class BaseCore:
150
191
 
151
192
  def update_user_agent(self):
152
193
  """Updates the User-Agent"""
153
- self.session.headers.update({"User-Agent": random.choice(consts.USER_AGENTS)})
194
+ self.config.rotate_user_agent()
195
+ self.session.headers.update({"User-Agent": self.config.headers["User-Agent"]})
196
+
197
+ def enable_kill_switch(self):
198
+ """This is a function that will check and verify if your proxy is working before every request.
199
+ As soon as there's a mismatch in the actual response IP and the proxy IP, the program will exit."""
200
+ self.kill_switch = True
201
+
202
+ def check_kill_switch(self):
203
+ proxy_ip = self.config.PROXY
204
+ pattern = re.compile(
205
+ r'^(?P<scheme>http|socks5)://'
206
+ r'(?:(?:\w+:\w+)@)?' # optional user:pass@
207
+ r'(?P<host>[a-zA-Z0-9.-]+)'
208
+ r':(?P<port>\d{1,5})$'
209
+ )
210
+
211
+ match = pattern.match(proxy_ip
212
+ )
213
+ if match:
214
+ self.logger.info(f"Proxy has valid scheme: {match.group('scheme')} ")
154
215
 
155
- def initialize_session(self):
156
- verify = True
216
+ else:
217
+ self.logger.critical("Proxy is INVALID! Exiting.")
157
218
 
158
- if consts.PROXY is not None:
159
- verify = False
219
+ self.logger.info("Checking if proxy is working...")
220
+ self.logger.info("Doing request to httpbin.org to get your real IP to compare it with another request with activated proxy")
160
221
 
161
- self.session = httpx.Client(proxy=consts.PROXY,
162
- headers=consts.HEADERS,
163
- verify=verify,
164
- timeout=consts.TIMEOUT,
165
- follow_redirects=True)
222
+ your_ip = httpx.Client().get("https://httpbin.org/ip").json()["origin"]
223
+ self.logger.info(f"Your IP is: {your_ip}")
224
+
225
+ self.logger.info("Doing request with Proxy on...")
226
+ proxy_ip_ = self.fetch("https://httpbin.org/ip", bypass_kill_switch=True, get_response=True).json()["origin"]
227
+ self.logger.info(f"Proxy IP is: {proxy_ip_}")
228
+
229
+ if your_ip == proxy_ip_:
230
+ self.logger.critical("IP is the same on both requests... Proxy is not working, exiting!")
231
+ raise "CRITICAL PROXY ERROR, CHECK LOGS!"
232
+
233
+
234
+ def initialize_session(self, verify=True): # Disable SSL verification only if you really need it....
235
+ self.session = httpx.Client(
236
+ proxy=self.config.PROXY,
237
+ headers=self.config.HEADERS,
238
+ timeout=self.config.TIMEOUT,
239
+ follow_redirects=True,
240
+ verify=verify
241
+ )
166
242
 
167
243
  def enforce_delay(self):
168
244
  """Enforces the specified delay in consts.REQUEST_DELAY"""
169
- delay = consts.REQUEST_DELAY
245
+ delay = self.config.REQUEST_DELAY
170
246
  if delay > 0:
171
247
  time_since_last_request = time.time() - self.last_request_time
172
248
  self.logger.debug(f"Time since last request: {time_since_last_request:.2f} seconds.")
@@ -179,78 +255,86 @@ class BaseCore:
179
255
  self,
180
256
  url: str,
181
257
  get_bytes: bool = False,
182
- timeout: int = consts.TIMEOUT,
258
+ timeout: int = None,
183
259
  get_response: bool = False,
184
260
  save_cache: bool = True,
185
261
  cookies: dict = None,
186
262
  allow_redirects: bool = True,
263
+ bypass_kill_switch: bool = False # prevents infinite loop
187
264
  ) -> Union[bytes, str, httpx.Response, None]:
188
265
  """
189
266
  Fetches content in UTF-8 Text, Bytes, or as a stream using multiple request attempts,
190
267
  support for proxies and custom timeout.
191
268
  """
192
269
  # Check cache first
270
+
271
+ if timeout is None:
272
+ timeout = self.config.timeout # pls don't ask thanks
273
+
193
274
  content = self.cache.handle_cache(url)
194
- if content is not None:
275
+ if content:
195
276
  self.logger.info(f"Fetched content for: {url} from cache!")
196
277
  return content
197
278
 
198
- for attempt in range(1, consts.MAX_RETRIES + 1):
279
+ for attempt in range(1, self.config.MAX_RETRIES + 1):
199
280
  if attempt != 1:
200
281
  time.sleep(1.5) # Sleeping for 1.5 seconds to minimize site overload when doing a lot of requests
201
282
 
202
283
  try:
203
284
  # Update user agent periodically
204
- if self.total_requests % 5 == 0:
285
+ if self.total_requests % 30 == 0: # Changing all 30 attempts
205
286
  self.update_user_agent()
206
287
 
207
288
  self.enforce_delay()
208
289
 
290
+ if self.kill_switch and not bypass_kill_switch:
291
+ self.check_kill_switch()
292
+
209
293
  # Perform the request with stream handling
210
- with self.session.stream("GET", url, timeout=timeout, cookies=cookies,
211
- follow_redirects=allow_redirects) as response:
212
- self.total_requests += 1
294
+ response = self.session.get(url, timeout=timeout, cookies=cookies,
295
+ follow_redirects=allow_redirects)
296
+ self.total_requests += 1
213
297
 
214
- # Log and handle non-200 status codes
215
- if response.status_code != 200:
216
- self.logger.warning(
217
- f"Attempt {attempt}: Unexpected status code {response.status_code} for URL: {url}")
298
+ # Log and handle non-200 status codes
299
+ if response.status_code != 200:
300
+ self.logger.warning(
301
+ f"Attempt {attempt}: Unexpected status code {response.status_code} for URL: {url}")
218
302
 
219
- if response.status_code == 404:
220
- self.logger.error("Resource not found (404). This may indicate the content is unavailable.")
221
- return None # Return None for unavailable resources
303
+ if response.status_code == 404:
304
+ self.logger.error("Resource not found (404). This may indicate the content is unavailable.")
305
+ return None # Return None for unavailable resources
222
306
 
223
- elif response.status_code == 403 and attempt >= 2:
224
- self.logger.error(f"The website rejected access after {attempt} tries. Aborting!")
225
- return None # Return None for forbidden resources
307
+ elif response.status_code == 403 and attempt >= 2:
308
+ self.logger.error(f"The website rejected access after {attempt} tries. Aborting!")
309
+ return None # Return None for forbidden resources
226
310
 
227
- continue # Retry for other non-200 status codes
311
+ continue # Retry for other non-200 status codes
228
312
 
229
- self.logger.debug(f"Attempt {attempt}: Successfully fetched URL: {url}")
313
+ self.logger.debug(f"Attempt {attempt}: Successfully fetched URL: {url}")
230
314
 
231
- # Return response if requested
232
- if get_response:
233
- return response
315
+ # Return response if requested
316
+ if get_response:
317
+ return response
234
318
 
235
- # Process and return content
236
- # Collecting all chunks before processing because some sites cause issues with real-time decoding
237
- raw_content = b"".join(response.iter_bytes())
319
+ # Process and return content
320
+ # Collecting all chunks before processing because some sites cause issues with real-time decoding
321
+ raw_content = response.content
238
322
 
239
- if get_bytes:
240
- content = raw_content
323
+ if get_bytes:
324
+ content = raw_content
241
325
 
242
- else:
243
- try:
244
- content = raw_content.decode("utf-8")
326
+ else:
327
+ try:
328
+ content = raw_content.decode("utf-8")
245
329
 
246
- except UnicodeDecodeError:
247
- content = raw_content.decode("latin1") # Fallback, hope that works somehow idk
330
+ except UnicodeDecodeError:
331
+ content = raw_content.decode("latin1") # Fallback, hope that works somehow idk
248
332
 
249
- if save_cache and not get_bytes:
250
- self.logger.debug(f"Saving content of {url} to local cache.")
251
- self.cache.save_cache(url, content)
333
+ if save_cache and not get_bytes:
334
+ self.logger.debug(f"Saving content of {url} to local cache.")
335
+ self.cache.save_cache(url, content)
252
336
 
253
- return content
337
+ return content
254
338
 
255
339
  except httpx.RequestError as e:
256
340
  self.logger.error(f"Attempt {attempt}: Request error for URL {url}: {e}")
@@ -258,9 +342,9 @@ class BaseCore:
258
342
  except Exception:
259
343
  self.logger.error(f"Attempt {attempt}: Unexpected error for URL {url}: {traceback.format_exc()}")
260
344
 
261
- self.logger.info(f"Retrying ({attempt}/{consts.MAX_RETRIES}) for URL: {url}")
345
+ self.logger.info(f"Retrying ({attempt}/{self.config.MAX_RETRIES}) for URL: {url}")
262
346
 
263
- self.logger.error(f"Failed to fetch URL {url} after {consts.MAX_RETRIES} attempts.")
347
+ self.logger.error(f"Failed to fetch URL {url} after {self.config.MAX_RETRIES} attempts.")
264
348
  return None # Return None if all attempts fail
265
349
 
266
350
  @classmethod
@@ -352,14 +436,13 @@ class BaseCore:
352
436
  elif downloader == "FFMPEG":
353
437
  self.FFMPEG(video=video, quality=quality, path=path, callback=callback)
354
438
 
355
- @classmethod
356
- def download_segment(cls, url: str, timeout: int) -> tuple[str, bytes, bool]:
439
+ def download_segment(self, url: str, timeout: int) -> tuple[str, bytes, bool]:
357
440
  """
358
441
  Attempt to download a single segment, retrying on failure.
359
442
  Returns a tuple of the URL, content (empty if failed after retries), and a success flag.
360
443
  """
361
444
 
362
- content = BaseCore().fetch(url, timeout=timeout, get_bytes=True)
445
+ content = self.fetch(url, timeout=timeout, get_bytes=True)
363
446
  return url, content, True # Success
364
447
 
365
448
  def threaded(self, max_workers: int = 20, timeout: int = 10):
@@ -437,7 +520,7 @@ class BaseCore:
437
520
 
438
521
  # Build the command for FFMPEG as a list directly
439
522
  command = [
440
- consts.FFMPEG_PATH,
523
+ self.config.FFMPEG_PATH,
441
524
  "-i", new_url, # Input URL
442
525
  "-bsf:a", "aac_adtstoasc",
443
526
  "-y", # Overwrite output files without asking
@@ -0,0 +1,36 @@
1
+ import random
2
+
3
+ class RuntimeConfig:
4
+ def __init__(self):
5
+ self.max_cache_items = 200
6
+ self.max_retries = 4
7
+ self.request_delay = 0
8
+ self.timeout = 20
9
+ self.ffmpeg_path = "ffmpeg"
10
+ self.proxy = None
11
+ self.verify_ssl = True
12
+ self.user_agents = [
13
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
14
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.1",
15
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3",
16
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.3",
17
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0."
18
+ ]
19
+ self.cookies = None
20
+ self.headers = {
21
+ "User-Agent": random.choice(self.user_agents)
22
+ }
23
+
24
+ def rotate_user_agent(self):
25
+ self.headers["User-Agent"] = random.choice(self.user_agents)
26
+
27
+ def as_httpx_kwargs(self):
28
+ return {
29
+ "proxies": self.proxy,
30
+ "verify": self.verify_ssl,
31
+ "headers": self.headers,
32
+ "timeout": self.timeout
33
+ }
34
+
35
+ # Singleton instance needed for my Porn Fetch project
36
+ config = RuntimeConfig()
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: eaf_base_api
3
- Version: 2.2.6
3
+ Version: 2.2.7
4
4
  Summary: A base API for EchterAlsFake's Porn APIs
5
5
  Home-page: https://github.com/EchterAlsFake/eaf_base_api
6
6
  Author: Johannes Habel
@@ -20,6 +20,7 @@ Dynamic: description
20
20
  Dynamic: description-content-type
21
21
  Dynamic: home-page
22
22
  Dynamic: license
23
+ Dynamic: license-file
23
24
  Dynamic: requires-dist
24
25
  Dynamic: summary
25
26
 
@@ -4,7 +4,7 @@ setup.py
4
4
  base_api/__init__.py
5
5
  base_api/base.py
6
6
  base_api/modules/__init__.py
7
- base_api/modules/consts.py
7
+ base_api/modules/config.py
8
8
  base_api/modules/progress_bars.py
9
9
  eaf_base_api.egg-info/PKG-INFO
10
10
  eaf_base_api.egg-info/SOURCES.txt
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="eaf_base_api",
5
- version="2.2.6",
5
+ version="2.2.7",
6
6
  packages=find_packages(),
7
7
  install_requires=["ffmpeg-progress-yield", "m3u8", "httpx"],
8
8
  entry_points={
@@ -1,16 +0,0 @@
1
- import random
2
-
3
- MAX_CACHE_ITEMS = 200 # Lower this value if you are on a low-ram system (<=4 GB)
4
- MAX_RETRIES = 4
5
- REQUEST_DELAY = 0
6
- TIMEOUT = 20 # Maximal request timeout
7
- FFMPEG_PATH = "ffmpeg"
8
- PROXY = None
9
- USER_AGENTS = ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
10
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.1",
11
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3",
12
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.3",
13
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0."]
14
-
15
- HEADERS = {"User-Agent": random.choice(USER_AGENTS)}
16
- COOKIES = None
File without changes
File without changes
File without changes