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.
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/PKG-INFO +1 -1
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/base_api/base.py +117 -34
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/eaf_base_api.egg-info/PKG-INFO +1 -1
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/setup.py +1 -1
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/LICENSE +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/README.md +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/base_api/__init__.py +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/base_api/modules/consts.py +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/eaf_base_api.egg-info/SOURCES.txt +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/eaf_base_api.egg-info/dependency_links.txt +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/eaf_base_api.egg-info/requires.txt +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/eaf_base_api.egg-info/top_level.txt +0 -0
- {eaf_base_api-2.2.5 → eaf_base_api-2.2.6}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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 %
|
|
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
|
-
|
|
193
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|