eaf-base-api 2.2.6__tar.gz → 2.2.8__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.8
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,14 @@
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
- import random
7
8
  import logging
8
9
  import traceback
9
10
  import threading
11
+ import http.client
10
12
 
11
13
  from typing import Union
12
14
  from functools import lru_cache
@@ -15,11 +17,11 @@ from ffmpeg_progress_yield import FfmpegProgress
15
17
  from concurrent.futures import ThreadPoolExecutor, as_completed
16
18
 
17
19
  try:
18
- from modules import consts
20
+ from modules import config
19
21
  from modules.progress_bars import Callback
20
22
 
21
23
  except (ModuleNotFoundError, ImportError):
22
- from .modules import consts
24
+ from .modules import config
23
25
  from .modules.progress_bars import Callback
24
26
 
25
27
  loggers = {}
@@ -56,19 +58,52 @@ class ColoredFormatter(logging.Formatter):
56
58
  return f"{color}{log_message}{RESET_COLOR}"
57
59
 
58
60
 
59
- def setup_logger(name, log_file=None, level=logging.CRITICAL):
61
+ def send_log_message(ip, port, message):
62
+ """Sends a log message to a remote server via HTTP."""
63
+ try:
64
+ conn = http.client.HTTPConnection(ip, port, timeout=2)
65
+ headers = {'Content-Type': 'application/json'}
66
+ data = json.dumps({"message": message})
67
+ conn.request("POST", "/log", body=data, headers=headers)
68
+ conn.getresponse() # Read response to ensure completion
69
+ conn.close()
70
+ except Exception as e:
71
+ print(f"Failed to send log to {ip}:{port} - {e}", file=sys.stderr)
72
+
73
+
74
+ class HTTPLogHandler(logging.Handler):
75
+ """Custom log handler that sends logs to a remote HTTP server."""
76
+
77
+ def __init__(self, ip, port):
78
+ super().__init__()
79
+ self.ip = ip
80
+ self.port = port
81
+
82
+ def emit(self, record):
83
+ log_entry = self.format(record)
84
+ send_log_message(self.ip, self.port, log_entry)
85
+
86
+
87
+ def setup_logger(name, log_file=None, level=logging.CRITICAL, http_ip=None, http_port=None):
60
88
  """Creates or updates a logger for a specific module."""
61
89
  if name in loggers:
62
90
  logger = loggers[name]
63
91
  logger.setLevel(level)
64
92
 
65
93
  file_handler_exists = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
94
+ http_handler_exists = any(isinstance(h, HTTPLogHandler) for h in logger.handlers)
95
+
66
96
  if log_file and not file_handler_exists:
67
97
  log_file = get_log_file_path(log_file)
68
98
  fh = logging.FileHandler(log_file, mode='a')
69
99
  fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
70
100
  logger.addHandler(fh)
71
101
 
102
+ if http_ip and http_port and not http_handler_exists:
103
+ http_handler = HTTPLogHandler(http_ip, http_port)
104
+ http_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
105
+ logger.addHandler(http_handler)
106
+
72
107
  return logger
73
108
 
74
109
  logger = logging.getLogger(name)
@@ -76,10 +111,9 @@ def setup_logger(name, log_file=None, level=logging.CRITICAL):
76
111
 
77
112
  if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
78
113
  ch = logging.StreamHandler()
79
- ch.setFormatter(ColoredFormatter("%(asctime)s - %(levelname)s - %(message)s"))
114
+ ch.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
80
115
  logger.addHandler(ch)
81
116
 
82
- # Ensure the log file is overwritten only on the first program start
83
117
  if log_file:
84
118
  log_file = get_log_file_path(log_file)
85
119
  if not hasattr(sys, '_first_run'):
@@ -87,11 +121,15 @@ def setup_logger(name, log_file=None, level=logging.CRITICAL):
87
121
  file_mode = 'w'
88
122
  else:
89
123
  file_mode = 'a'
90
-
91
124
  fh = logging.FileHandler(log_file, mode=file_mode)
92
125
  fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
93
126
  logger.addHandler(fh)
94
127
 
128
+ if http_ip and http_port:
129
+ http_handler = HTTPLogHandler(http_ip, http_port)
130
+ http_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
131
+ logger.addHandler(http_handler)
132
+
95
133
  loggers[name] = logger
96
134
  return logger
97
135
 
@@ -100,10 +138,11 @@ class Cache:
100
138
  Caches content from network requests
101
139
  """
102
140
 
103
- def __init__(self):
141
+ def __init__(self, config):
104
142
  self.cache_dictionary = {}
105
143
  self.lock = threading.Lock()
106
144
  self.logger = setup_logger("BASE API - [Cache]", level=logging.CRITICAL)
145
+ self.config = config
107
146
 
108
147
  def enable_logging(self, log_file=None, level=logging.DEBUG):
109
148
  """
@@ -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()) >= self.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.8
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.8",
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