eaf-base-api 2.2.5__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.5
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,16 +213,20 @@ 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:
@@ -166,20 +247,20 @@ class BaseCore:
166
247
  content = raw_content.decode("latin1") # Fallback, hope that works somehow idk
167
248
 
168
249
  if save_cache and not get_bytes:
169
- logger.debug(f"Saving content of {url} to local cache.")
170
- cache.save_cache(url, content)
250
+ self.logger.debug(f"Saving content of {url} to local cache.")
251
+ self.cache.save_cache(url, content)
171
252
 
172
253
  return content
173
254
 
174
255
  except httpx.RequestError as e:
175
- logger.error(f"Attempt {attempt}: Request error for URL {url}: {e}")
256
+ self.logger.error(f"Attempt {attempt}: Request error for URL {url}: {e}")
176
257
 
177
258
  except Exception:
178
- 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()}")
179
260
 
180
- logger.info(f"Retrying ({attempt}/{consts.MAX_RETRIES}) for URL: {url}")
261
+ self.logger.info(f"Retrying ({attempt}/{consts.MAX_RETRIES}) for URL: {url}")
181
262
 
182
- 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.")
183
264
  return None # Return None if all attempts fail
184
265
 
185
266
  @classmethod
@@ -188,9 +269,11 @@ class BaseCore:
188
269
  :param title:
189
270
  :return: str: strips out non UTF-8 chars of the title
190
271
  """
191
- illegal_chars = '<>:"/\\|?*'
192
- cleaned_title = ''.join([char for char in title if char in string.printable and char not in illegal_chars])
193
- return cleaned_title
272
+ illegal_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
273
+ for char in illegal_chars:
274
+ title = title.replace(char, "")
275
+
276
+ return title
194
277
 
195
278
  @lru_cache(maxsize=250)
196
279
  def get_m3u8_by_quality(self, m3u8_url: str, quality: str) -> str:
@@ -198,7 +281,7 @@ class BaseCore:
198
281
  m3u8 playlist"""
199
282
  playlist_content = self.fetch(url=m3u8_url)
200
283
  playlist = m3u8.loads(playlist_content)
201
- logger.debug(f"Resolved m3u8 playlist: {m3u8_url}")
284
+ self.logger.debug(f"Resolved m3u8 playlist: {m3u8_url}")
202
285
 
203
286
  if not playlist.is_variant:
204
287
  raise ValueError("Provided URL is not a master playlist.")
@@ -235,7 +318,7 @@ class BaseCore:
235
318
  def get_segments(self, m3u8_url_master: str, quality: str) -> list:
236
319
  """Gets all video segments from a given quality and the primary m3u8 URL"""
237
320
  m3u8_url = self.get_m3u8_by_quality(m3u8_url=m3u8_url_master, quality=quality)
238
- logger.debug(f"Trying to fetch segment from m3u8 ->: {m3u8_url}")
321
+ self.logger.debug(f"Trying to fetch segment from m3u8 ->: {m3u8_url}")
239
322
  content = self.fetch(url=m3u8_url)
240
323
  segments_ = m3u8.loads(content).segments
241
324
  segments = []
@@ -243,7 +326,7 @@ class BaseCore:
243
326
  for segment in segments_:
244
327
  segments.append(urljoin(m3u8_url, segment.uri)) # Get the full URL path to segment
245
328
 
246
- logger.debug(f"Fetched {len(segments)} segments from m3u8 URL")
329
+ self.logger.debug(f"Fetched {len(segments)} segments from m3u8 URL")
247
330
  return segments
248
331
 
249
332
  def download(self, video, quality: str, downloader: str, path: str, callback=None) -> None:
@@ -307,7 +390,7 @@ class BaseCore:
307
390
  successful_downloads += 1
308
391
  callback(completed, length) # Update progress callback
309
392
  except Exception as e:
310
- logger.error(f"Error processing segment {hls_part}: {e}")
393
+ self.logger.error(f"Error processing segment {hls_part}: {e}")
311
394
 
312
395
  # Write only successful segments to the output file
313
396
  with open(path, 'wb') as file:
@@ -323,7 +406,7 @@ class BaseCore:
323
406
  if success:
324
407
  file.write(data)
325
408
  except Exception as e:
326
- logger.error(f"Exception writing segment {segment_url}: {e}")
409
+ self.logger.error(f"Exception writing segment {segment_url}: {e}")
327
410
 
328
411
  return wrapper
329
412
 
@@ -408,10 +491,10 @@ class BaseCore:
408
491
  return True
409
492
 
410
493
  except httpx.RequestError as e:
411
- logger.error(f"Request failed for URL {url}: {e}")
494
+ self.logger.error(f"Request failed for URL {url}: {e}")
412
495
  return False
413
496
  except Exception as e:
414
- logger.error(f"Unexpected error during download: {e}")
497
+ self.logger.error(f"Unexpected error during download: {e}")
415
498
  return False
416
499
 
417
500
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: eaf_base_api
3
- Version: 2.2.5
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.5",
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