eaf-base-api 2.2.4__tar.gz → 2.2.6__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.2
2
2
  Name: eaf_base_api
3
- Version: 2.2.4
3
+ Version: 2.2.6
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
@@ -1,9 +1,9 @@
1
1
  import os
2
+ import sys
2
3
  import time
3
4
  import m3u8
4
5
  import httpx
5
6
  import random
6
- import string
7
7
  import logging
8
8
  import traceback
9
9
  import threading
@@ -22,13 +22,78 @@ except (ModuleNotFoundError, ImportError):
22
22
  from .modules import consts
23
23
  from .modules.progress_bars import Callback
24
24
 
25
- logging.basicConfig(format='%(name)s %(levelname)s %(asctime)s %(message)s', datefmt='%I:%M:%S %p')
26
- logger = logging.getLogger("BASE API")
27
- logger.setLevel(logging.DEBUG)
25
+ loggers = {}
26
+
27
+ # Default formatter
28
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
29
+
30
+
31
+ def is_android():
32
+ """Detects if the script is running on an Android device."""
33
+ return "ANDROID_ROOT" in os.environ and "ANDROID_DATA" in os.environ
34
+
35
+ def get_log_file_path(filename="app.log"):
36
+ """Returns a valid log file path that works on Android and other OS."""
37
+ if is_android():
38
+ return os.path.join(os.environ["HOME"], filename) # Internal app storage
39
+ return filename # Default for Linux, Windows, Mac
40
+
41
+
42
+ # Define ANSI color codes for different log levels
43
+ LOG_COLORS = {
44
+ 'DEBUG': "\033[37m", # White
45
+ 'INFO': "\033[34m", # Blue
46
+ 'WARNING': "\033[33m", # Yellow
47
+ 'ERROR': "\033[31m", # Red
48
+ 'CRITICAL': "\033[41m" # Red background
49
+ }
50
+ RESET_COLOR = "\033[0m"
51
+
52
+ class ColoredFormatter(logging.Formatter):
53
+ def format(self, record):
54
+ log_message = super().format(record)
55
+ color = LOG_COLORS.get(record.levelname, RESET_COLOR)
56
+ return f"{color}{log_message}{RESET_COLOR}"
57
+
58
+
59
+ def setup_logger(name, log_file=None, level=logging.CRITICAL):
60
+ """Creates or updates a logger for a specific module."""
61
+ if name in loggers:
62
+ logger = loggers[name]
63
+ logger.setLevel(level)
64
+
65
+ file_handler_exists = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
66
+ if log_file and not file_handler_exists:
67
+ log_file = get_log_file_path(log_file)
68
+ fh = logging.FileHandler(log_file, mode='a')
69
+ fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
70
+ logger.addHandler(fh)
71
+
72
+ return logger
73
+
74
+ logger = logging.getLogger(name)
75
+ logger.setLevel(level)
76
+
77
+ if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
78
+ ch = logging.StreamHandler()
79
+ ch.setFormatter(ColoredFormatter("%(asctime)s - %(levelname)s - %(message)s"))
80
+ logger.addHandler(ch)
81
+
82
+ # Ensure the log file is overwritten only on the first program start
83
+ if log_file:
84
+ log_file = get_log_file_path(log_file)
85
+ if not hasattr(sys, '_first_run'):
86
+ sys._first_run = True
87
+ file_mode = 'w'
88
+ else:
89
+ file_mode = 'a'
28
90
 
29
- def disable_logging():
30
- logger.setLevel(logging.CRITICAL)
91
+ fh = logging.FileHandler(log_file, mode=file_mode)
92
+ fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
93
+ logger.addHandler(fh)
31
94
 
95
+ loggers[name] = logger
96
+ return logger
32
97
 
33
98
  class Cache:
34
99
  """
@@ -38,6 +103,13 @@ class Cache:
38
103
  def __init__(self):
39
104
  self.cache_dictionary = {}
40
105
  self.lock = threading.Lock()
106
+ self.logger = setup_logger("BASE API - [Cache]", level=logging.CRITICAL)
107
+
108
+ def enable_logging(self, log_file=None, level=logging.DEBUG):
109
+ """
110
+ Enables logging dynamically for this module.
111
+ """
112
+ self.logger = setup_logger(name="BASE API - [Cache]", log_file=log_file, level=level)
41
113
 
42
114
  def handle_cache(self, url):
43
115
  if url is None:
@@ -53,13 +125,10 @@ class Cache:
53
125
  first_key = next(iter(self.cache_dictionary))
54
126
  # Delete the first item
55
127
  del self.cache_dictionary[first_key]
56
- logger.info(f"Deleting: {first_key} from cache, due to caching limits...")
128
+ self.logger.info(f"Deleting: {first_key} from cache, due to caching limits...")
57
129
 
58
130
  self.cache_dictionary[url] = content
59
131
 
60
- cache = Cache()
61
-
62
-
63
132
  class BaseCore:
64
133
  """
65
134
  The base class which has all necessary functions for other API packages
@@ -68,8 +137,16 @@ class BaseCore:
68
137
  self.last_request_time = time.time()
69
138
  self.total_requests = 0 # Tracks how many requests have been made
70
139
  self.session = None
140
+ self.cache = Cache()
71
141
  self.initialize_session()
142
+ self.logger = setup_logger("BASE API - [BaseCore]", log_file=False, level=logging.ERROR)
72
143
 
144
+ def enable_logging(self, log_file=None, level=logging.DEBUG):
145
+ """
146
+ Enables logging dynamically for this module.
147
+ """
148
+ self.logger = setup_logger(name="BASE API - [BaseCore]", log_file=log_file, level=level)
149
+ self.cache.logger = setup_logger(name="BASE API - [Cache]", log_file=log_file, level=level)
73
150
 
74
151
  def update_user_agent(self):
75
152
  """Updates the User-Agent"""
@@ -92,10 +169,10 @@ class BaseCore:
92
169
  delay = consts.REQUEST_DELAY
93
170
  if delay > 0:
94
171
  time_since_last_request = time.time() - self.last_request_time
95
- logger.debug(f"Time since last request: {time_since_last_request:.2f} seconds.")
172
+ self.logger.debug(f"Time since last request: {time_since_last_request:.2f} seconds.")
96
173
  if time_since_last_request < delay:
97
174
  sleep_time = delay - time_since_last_request
98
- logger.debug(f"Enforcing delay of {sleep_time:.2f} seconds.")
175
+ self.logger.debug(f"Enforcing delay of {sleep_time:.2f} seconds.")
99
176
  time.sleep(sleep_time)
100
177
 
101
178
  def fetch(
@@ -113,9 +190,9 @@ class BaseCore:
113
190
  support for proxies and custom timeout.
114
191
  """
115
192
  # Check cache first
116
- content = cache.handle_cache(url)
193
+ content = self.cache.handle_cache(url)
117
194
  if content is not None:
118
- logger.info(f"Fetched content for: {url} from cache!")
195
+ self.logger.info(f"Fetched content for: {url} from cache!")
119
196
  return content
120
197
 
121
198
  for attempt in range(1, consts.MAX_RETRIES + 1):
@@ -124,7 +201,7 @@ class BaseCore:
124
201
 
125
202
  try:
126
203
  # Update user agent periodically
127
- if self.total_requests % 3 == 0:
204
+ if self.total_requests % 5 == 0:
128
205
  self.update_user_agent()
129
206
 
130
207
  self.enforce_delay()
@@ -136,41 +213,54 @@ class BaseCore:
136
213
 
137
214
  # Log and handle non-200 status codes
138
215
  if response.status_code != 200:
139
- logger.warning(
216
+ self.logger.warning(
140
217
  f"Attempt {attempt}: Unexpected status code {response.status_code} for URL: {url}")
141
218
 
142
219
  if response.status_code == 404:
143
- logger.error("Resource not found (404). This may indicate the content is unavailable.")
220
+ self.logger.error("Resource not found (404). This may indicate the content is unavailable.")
144
221
  return None # Return None for unavailable resources
145
222
 
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
226
+
146
227
  continue # Retry for other non-200 status codes
147
228
 
148
- logger.debug(f"Attempt {attempt}: Successfully fetched URL: {url}")
229
+ self.logger.debug(f"Attempt {attempt}: Successfully fetched URL: {url}")
149
230
 
150
231
  # Return response if requested
151
232
  if get_response:
152
233
  return response
153
234
 
154
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())
238
+
155
239
  if get_bytes:
156
- content = b"".join([chunk for chunk in response.iter_bytes()])
240
+ content = raw_content
241
+
157
242
  else:
158
- content = "".join([chunk.decode("utf-8") for chunk in response.iter_bytes()])
159
- if save_cache:
160
- logger.debug(f"Saving content of {url} to local cache.")
161
- cache.save_cache(url, content)
243
+ try:
244
+ content = raw_content.decode("utf-8")
245
+
246
+ except UnicodeDecodeError:
247
+ content = raw_content.decode("latin1") # Fallback, hope that works somehow idk
248
+
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)
162
252
 
163
253
  return content
164
254
 
165
255
  except httpx.RequestError as e:
166
- logger.error(f"Attempt {attempt}: Request error for URL {url}: {e}")
256
+ self.logger.error(f"Attempt {attempt}: Request error for URL {url}: {e}")
167
257
 
168
258
  except Exception:
169
- logger.error(f"Attempt {attempt}: Unexpected error for URL {url}: {traceback.format_exc()}")
259
+ self.logger.error(f"Attempt {attempt}: Unexpected error for URL {url}: {traceback.format_exc()}")
170
260
 
171
- logger.info(f"Retrying ({attempt}/{consts.MAX_RETRIES}) for URL: {url}")
261
+ self.logger.info(f"Retrying ({attempt}/{consts.MAX_RETRIES}) for URL: {url}")
172
262
 
173
- logger.error(f"Failed to fetch URL {url} after {consts.MAX_RETRIES} attempts.")
263
+ self.logger.error(f"Failed to fetch URL {url} after {consts.MAX_RETRIES} attempts.")
174
264
  return None # Return None if all attempts fail
175
265
 
176
266
  @classmethod
@@ -179,9 +269,11 @@ class BaseCore:
179
269
  :param title:
180
270
  :return: str: strips out non UTF-8 chars of the title
181
271
  """
182
- illegal_chars = '<>:"/\\|?*'
183
- cleaned_title = ''.join([char for char in title if char in string.printable and char not in illegal_chars])
184
- return cleaned_title
272
+ illegal_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
273
+ for char in illegal_chars:
274
+ title = title.replace(char, "")
275
+
276
+ return title
185
277
 
186
278
  @lru_cache(maxsize=250)
187
279
  def get_m3u8_by_quality(self, m3u8_url: str, quality: str) -> str:
@@ -189,7 +281,7 @@ class BaseCore:
189
281
  m3u8 playlist"""
190
282
  playlist_content = self.fetch(url=m3u8_url)
191
283
  playlist = m3u8.loads(playlist_content)
192
- logger.debug(f"Resolved m3u8 playlist: {m3u8_url}")
284
+ self.logger.debug(f"Resolved m3u8 playlist: {m3u8_url}")
193
285
 
194
286
  if not playlist.is_variant:
195
287
  raise ValueError("Provided URL is not a master playlist.")
@@ -226,7 +318,7 @@ class BaseCore:
226
318
  def get_segments(self, m3u8_url_master: str, quality: str) -> list:
227
319
  """Gets all video segments from a given quality and the primary m3u8 URL"""
228
320
  m3u8_url = self.get_m3u8_by_quality(m3u8_url=m3u8_url_master, quality=quality)
229
- logger.debug(f"Trying to fetch segment from m3u8 ->: {m3u8_url}")
321
+ self.logger.debug(f"Trying to fetch segment from m3u8 ->: {m3u8_url}")
230
322
  content = self.fetch(url=m3u8_url)
231
323
  segments_ = m3u8.loads(content).segments
232
324
  segments = []
@@ -234,7 +326,7 @@ class BaseCore:
234
326
  for segment in segments_:
235
327
  segments.append(urljoin(m3u8_url, segment.uri)) # Get the full URL path to segment
236
328
 
237
- logger.debug(f"Fetched {len(segments)} segments from m3u8 URL")
329
+ self.logger.debug(f"Fetched {len(segments)} segments from m3u8 URL")
238
330
  return segments
239
331
 
240
332
  def download(self, video, quality: str, downloader: str, path: str, callback=None) -> None:
@@ -298,7 +390,7 @@ class BaseCore:
298
390
  successful_downloads += 1
299
391
  callback(completed, length) # Update progress callback
300
392
  except Exception as e:
301
- logger.error(f"Error processing segment {hls_part}: {e}")
393
+ self.logger.error(f"Error processing segment {hls_part}: {e}")
302
394
 
303
395
  # Write only successful segments to the output file
304
396
  with open(path, 'wb') as file:
@@ -314,7 +406,7 @@ class BaseCore:
314
406
  if success:
315
407
  file.write(data)
316
408
  except Exception as e:
317
- logger.error(f"Exception writing segment {segment_url}: {e}")
409
+ self.logger.error(f"Exception writing segment {segment_url}: {e}")
318
410
 
319
411
  return wrapper
320
412
 
@@ -399,10 +491,10 @@ class BaseCore:
399
491
  return True
400
492
 
401
493
  except httpx.RequestError as e:
402
- logger.error(f"Request failed for URL {url}: {e}")
494
+ self.logger.error(f"Request failed for URL {url}: {e}")
403
495
  return False
404
496
  except Exception as e:
405
- logger.error(f"Unexpected error during download: {e}")
497
+ self.logger.error(f"Unexpected error during download: {e}")
406
498
  return False
407
499
 
408
500
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: eaf_base_api
3
- Version: 2.2.4
3
+ Version: 2.2.6
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
@@ -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.4",
5
+ version="2.2.6",
6
6
  packages=find_packages(),
7
7
  install_requires=["ffmpeg-progress-yield", "m3u8", "httpx"],
8
8
  entry_points={
File without changes
File without changes
File without changes